feat(grok-cli): add Grok CLI / Grok Build provider with OAuth device-code flow (#2502)
New OAuth provider routing through cli-chat-proxy.grok.com (OpenAI Responses
API), distinct from xai (api.x.ai) and grok-web (cookie SSO):
- Registry + GrokCliExecutor: Chat Completions -> Responses transform, CLI
fingerprint headers, virtual effort models grok-4.5-{low,medium,high}
- OAuth device-code flow (auth.x.ai) with no-PKCE, shared xAI token refresh
- store=false multi-turn continuity via reasoning encrypted_content
- Quota tracker: on-demand window + prepaid balance on dashboard
- Connection test: 402 spending-limit = soft success (auth OK, out of credits)
- Alias/oauth/provider baselines + unit tests
This commit is contained in:
committed by
decolua
parent
c73c419d09
commit
a11937cdd6
@@ -114,6 +114,10 @@ export const CODEBUDDY_CONFIG = { ...PROVIDER_OAUTH["codebuddy-cn"] };
|
||||
// Kimchi OAuth Configuration (Browser token callback flow)
|
||||
export const KIMCHI_CONFIG = { ...PROVIDER_OAUTH["kimchi"] };
|
||||
|
||||
// Grok CLI / Grok Build OAuth Configuration (Device Code Flow)
|
||||
// Endpoint: cli-chat-proxy.grok.com — same client_id as xai, different flow + scopes
|
||||
export const GROK_CLI_CONFIG = { ...PROVIDER_OAUTH["grok-cli"] };
|
||||
|
||||
// OAuth timeout (5 minutes)
|
||||
export const OAUTH_TIMEOUT = 300000;
|
||||
|
||||
@@ -137,4 +141,5 @@ export const PROVIDERS = {
|
||||
GITLAB: "gitlab",
|
||||
CODEBUDDY: "codebuddy-cn",
|
||||
KIMCHI: "kimchi",
|
||||
GROK_CLI: "grok-cli",
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
GITLAB_CONFIG,
|
||||
CODEBUDDY_CONFIG,
|
||||
KIMCHI_CONFIG,
|
||||
GROK_CLI_CONFIG,
|
||||
getOAuthClientMetadata,
|
||||
} from "./constants/oauth";
|
||||
import { XAI_CONFIG, XAI_PKCE_VERIFIER_BYTES } from "./constants/xai";
|
||||
@@ -255,6 +256,122 @@ const PROVIDERS = {
|
||||
},
|
||||
},
|
||||
|
||||
// Grok CLI / Grok Build — device code flow to auth.x.ai, inference on cli-chat-proxy.grok.com
|
||||
"grok-cli": {
|
||||
config: GROK_CLI_CONFIG,
|
||||
flowType: "device_code",
|
||||
requestDeviceCode: async (config) => {
|
||||
const body = new URLSearchParams({
|
||||
client_id: config.clientId,
|
||||
scope: config.scope,
|
||||
});
|
||||
// Official CLI sends referrer=grok-build
|
||||
if (config.referrer) body.set("referrer", config.referrer);
|
||||
|
||||
const response = await fetch(config.deviceCodeUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
"User-Agent": "grok-pager/0.2.93 grok-shell/0.2.93 (linux; x86_64)",
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Grok CLI device code request failed: ${error}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
},
|
||||
pollToken: async (config, deviceCode) => {
|
||||
const response = await fetch(config.tokenUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
"User-Agent": "grok-pager/0.2.93 grok-shell/0.2.93 (linux; x86_64)",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
||||
device_code: deviceCode,
|
||||
client_id: config.clientId,
|
||||
}),
|
||||
});
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch {
|
||||
const text = await response.text();
|
||||
data = { error: "invalid_response", error_description: text };
|
||||
}
|
||||
|
||||
// Device flow: 400 + authorization_pending is expected while user authorizes
|
||||
const pending =
|
||||
data?.error === "authorization_pending" ||
|
||||
data?.error === "slow_down";
|
||||
return {
|
||||
ok: response.ok || pending,
|
||||
data,
|
||||
};
|
||||
},
|
||||
postExchange: async (tokens) => {
|
||||
// Best-effort user profile from cli-chat-proxy (non-fatal)
|
||||
try {
|
||||
const res = await fetch("https://cli-chat-proxy.grok.com/v1/user", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens.access_token}`,
|
||||
Accept: "application/json",
|
||||
"User-Agent": "grok-pager/0.2.93 grok-shell/0.2.93 (linux; x86_64)",
|
||||
"x-xai-token-auth": "xai-grok-cli",
|
||||
"x-grok-client-version": "0.2.93",
|
||||
},
|
||||
});
|
||||
if (res.ok) return { user: await res.json() };
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return { user: null };
|
||||
},
|
||||
mapTokens: (tokens, extra) => {
|
||||
const email =
|
||||
decodeXaiIdTokenEmail(tokens.id_token) ||
|
||||
extractEmailFromAccessToken(tokens.access_token) ||
|
||||
extra?.user?.email ||
|
||||
null;
|
||||
const userId =
|
||||
extra?.user?.userId ||
|
||||
extra?.user?.principalId ||
|
||||
null;
|
||||
const displayName = [extra?.user?.firstName, extra?.user?.lastName]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.trim() || null;
|
||||
|
||||
return {
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token || null,
|
||||
expiresIn: tokens.expires_in,
|
||||
scope: tokens.scope,
|
||||
// Top-level for dashboard connection cards
|
||||
email: email || undefined,
|
||||
displayName: displayName || undefined,
|
||||
// Mirror identity into providerSpecificData so GrokCliExecutor can set
|
||||
// x-email / x-userid without depending on top-level credential shape.
|
||||
providerSpecificData: {
|
||||
authMethod: "device_code",
|
||||
idToken: tokens.id_token || null,
|
||||
email: email || null,
|
||||
userId,
|
||||
hasGrokCodeAccess: extra?.user?.hasGrokCodeAccess ?? null,
|
||||
subscriptionTier: extra?.user?.subscriptionTier ?? null,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
"gemini-cli": {
|
||||
config: GEMINI_CONFIG,
|
||||
flowType: "authorization_code",
|
||||
|
||||
Reference in New Issue
Block a user