diff --git a/open-sse/config/providerModels.js b/open-sse/config/providerModels.js index 8259eb61..827701a6 100644 --- a/open-sse/config/providerModels.js +++ b/open-sse/config/providerModels.js @@ -462,6 +462,7 @@ export const PROVIDER_MODELS = { { id: "grok-4-fast-reasoning", name: "Grok 4 Fast Reasoning" }, { id: "grok-code-fast-1", name: "Grok Code Fast" }, { id: "grok-3", name: "Grok 3" }, + { id: "grok-2-image-1212", name: "Grok 2 Image", type: "image", params: ["n", "response_format"] }, ], mistral: [ { id: "mistral-large-latest", name: "Mistral Large 3" }, diff --git a/open-sse/config/providers.js b/open-sse/config/providers.js index 5f9d6a66..e3990d20 100644 --- a/open-sse/config/providers.js +++ b/open-sse/config/providers.js @@ -272,7 +272,11 @@ export const PROVIDERS = { }, xai: { baseUrl: "https://api.x.ai/v1/chat/completions", - format: "openai" + responsesUrl: "https://api.x.ai/v1/responses", + format: "openai", + clientId: "b1a00492-073a-47ea-816f-4c329264a828", + tokenUrl: "https://auth.x.ai/oauth2/token", + refreshUrl: "https://auth.x.ai/oauth2/token" }, mistral: { baseUrl: "https://api.mistral.ai/v1/chat/completions", diff --git a/open-sse/handlers/imageProviders/index.js b/open-sse/handlers/imageProviders/index.js index 3f875432..7094a8ea 100644 --- a/open-sse/handlers/imageProviders/index.js +++ b/open-sse/handlers/imageProviders/index.js @@ -17,6 +17,7 @@ const ADAPTERS = { minimax: createOpenAIAdapter("minimax"), openrouter: createOpenAIAdapter("openrouter"), recraft: createOpenAIAdapter("recraft"), + xai: createOpenAIAdapter("xai"), gemini, codex, sdwebui, diff --git a/open-sse/handlers/imageProviders/openai.js b/open-sse/handlers/imageProviders/openai.js index 2e034071..7c28a3a6 100644 --- a/open-sse/handlers/imageProviders/openai.js +++ b/open-sse/handlers/imageProviders/openai.js @@ -5,6 +5,7 @@ const ENDPOINTS = { minimax: "https://api.minimaxi.com/v1/images/generations", openrouter: "https://openrouter.ai/api/v1/images/generations", recraft: "https://external.api.recraft.ai/v1/images/generations", + xai: "https://api.x.ai/v1/images/generations", }; export default function createOpenAIAdapter(providerId) { @@ -22,6 +23,12 @@ export default function createOpenAIAdapter(providerId) { }, buildBody: (model, body) => { const { prompt, n = 1, size = "1024x1024", quality, style, response_format } = body; + // xAI only accepts prompt, model, n, response_format + if (providerId === "xai") { + const req = { model, prompt, n }; + if (response_format) req.response_format = response_format; + return req; + } const req = { model, prompt, n, size }; if (quality) req.quality = quality; if (style) req.style = style; diff --git a/open-sse/services/tokenRefresh.js b/open-sse/services/tokenRefresh.js index f3b21b69..c449559f 100644 --- a/open-sse/services/tokenRefresh.js +++ b/open-sse/services/tokenRefresh.js @@ -2,6 +2,33 @@ import { PROVIDERS } from "../config/providers.js"; import { OAUTH_ENDPOINTS, GITHUB_COPILOT, REFRESH_LEAD_MS } from "../config/appConstants.js"; import { proxyAwareFetch } from "../utils/proxyFetch.js"; +// xAI refresh — wraps the class method from src/lib/oauth/services/xai.js so +// the token-refresh switches below can stay flat (one function per provider). +let _xaiServiceSingleton = null; +async function refreshXaiToken(refreshToken, log) { + if (!refreshToken) return null; + try { + if (!_xaiServiceSingleton) { + const mod = await import("../../src/lib/oauth/services/xai.js"); + _xaiServiceSingleton = new mod.XaiService(); + } + const tokens = await _xaiServiceSingleton.refreshAccessToken(refreshToken); + return { + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token || refreshToken, + expiresIn: tokens.expires_in, + idToken: tokens.id_token, + }; + } catch (e) { + log?.warn?.("TOKEN_REFRESH", `xai refresh failed: ${e?.message || e}`); + const msg = String(e?.message || ""); + if (msg.includes("invalid_grant") || msg.includes("invalid_request")) { + return { error: "invalid_grant" }; + } + return null; + } +} + // Default token expiry buffer (refresh if expires within 5 minutes) export const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; @@ -567,6 +594,9 @@ async function _getAccessTokenInternal(provider, credentials, log) { log ); + case "xai": + return await refreshXaiToken(credentials.refreshToken, log); + case "vertex": case "vertex-partner": { const saJson = parseVertexSaJson(credentials.apiKey); @@ -611,6 +641,8 @@ export async function refreshTokenByProvider(provider, credentials, log) { credentials.providerSpecificData, log ); + case "xai": + return refreshXaiToken(credentials.refreshToken, log); case "vertex": case "vertex-partner": { const saJson = parseVertexSaJson(credentials.apiKey); @@ -651,6 +683,7 @@ export function formatProviderCredentials(provider, credentials, log) { case "iflow": case "openai": case "openrouter": + case "xai": return { apiKey: credentials.apiKey, accessToken: credentials.accessToken @@ -798,4 +831,3 @@ export async function refreshWithRetry(refreshFn, maxRetries = 3, log = null) { log?.error?.("TOKEN_REFRESH", `All ${maxRetries} retry attempts failed`); return null; } - diff --git a/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js b/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js index 1a4c8b76..77fd9c07 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js @@ -11,10 +11,11 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa const NONE_PROXY_POOL_VALUE = "__none__"; const isOllamaLocal = provider === "ollama-local"; const isCookie = authType === "cookie"; + const isXaiApiKey = provider === "xai" && !isCookie; const credentialLabel = isCookie ? "Cookie Value" : "API Key"; const credentialPlaceholder = isCookie ? (provider === "grok-web" ? "sso=xxxxx... or just the raw value" : "eyJhbGciOi...") - : ""; + : (isXaiApiKey ? "xai-..." : ""); const isAzure = provider === "azure"; const isCloudflareAi = provider === "cloudflare-ai"; @@ -228,6 +229,11 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa )} + {isXaiApiKey && ( +
+ Use a direct xAI API key from console.x.ai. This is separate from Grok Build OAuth. +
+ )} {isCookie && authHint && ({authHint} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js b/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js index b97d8814..dbf140d7 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js @@ -65,10 +65,15 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst } }; + const rowAuthType = connection.authType || (isOAuth ? "oauth" : "apikey"); + const isOAuthConnection = rowAuthType === "oauth"; + const isCookieConnection = rowAuthType === "cookie"; + const authIcon = isCookieConnection ? "cookie" : isOAuthConnection ? "lock" : "key"; + const authLabel = isOAuthConnection ? "OAuth" : isCookieConnection ? "Cookie" : "API Key"; const isEmail = (v) => typeof v === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); - const displayName = isOAuth + const displayName = isOAuthConnection ? (isEmail(connection.email) ? connection.email : (isEmail(connection.name) ? connection.name : (connection.name || connection.email || connection.displayName || "OAuth Account"))) - : connection.name; + : (connection.name || connection.email || connection.displayName || "API Key"); // Use useState + useEffect for impure Date.now() to avoid calling during render const [isCooldown, setIsCooldown] = useState(false); @@ -130,7 +135,7 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst - {isOAuth ? "lock" : "key"} + {authIcon}
{displayName}
@@ -138,6 +143,9 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirstNo connections yet
+No connections yet
+ {hasDualAuthModes && ( ++ Choose {oauthConnectionLabel} or {apiKeyConnectionLabel}. +
+ )} +Step 1: Open this URL in your browser
++ Step 1: Open this {isXaiProvider ? "Grok Build OAuth URL" : "URL"} in your browser +
Step 2: Paste the callback URL here
++ Step 2: Paste the {provider === "xai" ? "callback URL or copied code" : "callback URL"} here +
- After authorization, copy the full URL from your browser. + {provider === "xai" + ? "If xAI shows a code instead of redirecting, paste that code here." + : "After authorization, copy the full URL from your browser."}
setCallbackUrl(e.target.value)} - placeholder={placeholderUrl} + placeholder={manualPlaceholder} className="font-mono text-xs" />