Conversation
Add xAI Grok as a new API provider. Reuses OpenAI-compatible message/tool
converters and stream adapter with Grok-specific client and model mapping.
Default model mapping:
opus → grok-4.20-reasoning
sonnet → grok-3-mini-fast
haiku → grok-3-mini-fast
Users can customize mapping via:
- GROK_MODEL env var (override all)
- GROK_MODEL_MAP env var (JSON family map, e.g. {"opus":"grok-4"})
- GROK_DEFAULT_{FAMILY}_MODEL env vars
Activation: CLAUDE_CODE_USE_GROK=1 or modelType: "grok" in settings.json
Also integrates with /provider command for runtime switching.
📝 WalkthroughWalkthroughThis PR adds integrated support for the xAI Grok API provider to Claude Code. Changes include adding Grok to the provider command interface, implementing a Grok-specific API client and query handler using OpenAI-compatible APIs, adding model name resolution with environment variable precedence, extending the provider type system to include Grok, and adding comprehensive test coverage. Changes
Sequence DiagramsequenceDiagram
participant User as Claude Code User
participant Cmd as Provider Command
participant API as claude.ts<br/>(queryModel)
participant Grok as Grok Handler<br/>(queryModelGrok)
participant Client as Grok Client
participant XAI as xAI Grok API
participant Stream as Stream Adapter
User->>Cmd: Switch provider to 'grok'
Cmd->>Cmd: Update settings.modelType = 'grok'
User->>API: Send message query
API->>API: getAPIProvider() → 'grok'
API->>Grok: Call queryModelGrok(messages, tools, ...)
Grok->>Grok: resolveGrokModel(anthropicModel)
Grok->>Grok: Convert messages/tools to OpenAI format
Grok->>Client: getGrokClient()
Client->>Client: Load GROK_API_KEY / XAI_API_KEY
Client->>Client: Set base URL from GROK_BASE_URL
Client-->>Grok: Return cached/new OpenAI client
Grok->>XAI: chat.completions.create(messages, tools, signal)
XAI-->>Grok: Stream OpenAI events
Grok->>Stream: adaptOpenAIStreamToAnthropic(events)
Stream-->>Grok: Anthropic-compatible events
Grok->>Grok: Rebuild content blocks from events
Grok->>Grok: Track usage and calculate USDC cost
Grok-->>API: Yield AssistantMessage & StreamEvents
API-->>User: Return streamed response
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
src/services/api/grok/index.ts (3)
10-10: Consolidate duplicate imports from the same module.
normalizeMessagesForAPI(line 10) andcreateAssistantAPIErrorMessage,normalizeContentFromAPI(lines 17-20) are both imported from../../../utils/messages.js. Merge into a single import statement.♻️ Suggested consolidation
-import { normalizeMessagesForAPI } from '../../../utils/messages.js' import { toolToAPISchema } from '../../../utils/api.js' import { logForDebugging } from '../../../utils/debug.js' import { addToTotalSessionCost } from '../../../cost-tracker.js' import { calculateUSDCost } from '../../../utils/modelCost.js' import type { Options } from '../claude.js' import { randomUUID } from 'crypto' import { createAssistantAPIErrorMessage, normalizeContentFromAPI, + normalizeMessagesForAPI, } from '../../../utils/messages.js'Also applies to: 17-20
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/api/grok/index.ts` at line 10, The import statements are duplicated for utilities from ../../../utils/messages.js; consolidate them by replacing separate imports so that normalizeMessagesForAPI, createAssistantAPIErrorMessage, and normalizeContentFromAPI are all imported in a single import declaration (i.e., import { normalizeMessagesForAPI, createAssistantAPIErrorMessage, normalizeContentFromAPI } from '../../../utils/messages.js') and remove the redundant import lines; update any local references if necessary to match the single imported symbols.
92-93: Consider adding type definitions for content blocks and partial message.Using
anyforcontentBlocksandpartialMessageloses type safety. Defining explicit interfaces for these structures would improve maintainability and catch errors at compile time.♻️ Example type definitions
interface TextBlock { type: 'text'; text: string } interface ToolUseBlock { type: 'tool_use'; id: string; name: string; input: string } interface ThinkingBlock { type: 'thinking'; thinking: string; signature: string } type ContentBlock = TextBlock | ToolUseBlock | ThinkingBlock const contentBlocks: Record<number, ContentBlock> = {}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/api/grok/index.ts` around lines 92 - 93, The current declarations contentBlocks: Record<number, any> and partialMessage: any lose type safety; define explicit interfaces (e.g., TextBlock, ToolUseBlock, ThinkingBlock) and a union type ContentBlock (e.g., type ContentBlock = TextBlock | ToolUseBlock | ThinkingBlock) and replace contentBlocks with Record<number, ContentBlock>; also define a PartialMessage (or similar) interface for the structure held in partialMessage and type partialMessage as PartialMessage | undefined (or ContentBlock | undefined if appropriate) and update any places that read/write these to match the new types (look for usages of contentBlocks and partialMessage in the file to adjust).
161-175: Move cost calculation into themessage_stopcase.The
event.type === 'message_stop'check at line 172 runs for every event in the stream but only evaluates to true for the final event. Moving this logic inside thecase 'message_stop'block avoids redundant checks.♻️ Suggested refactor
case 'message_delta': { const deltaUsage = (event as any).usage if (deltaUsage) { usage = { ...usage, ...deltaUsage } } break } - case 'message_stop': - break + case 'message_stop': { + if (usage.input_tokens + usage.output_tokens > 0) { + const costUSD = calculateUSDCost(grokModel, usage as any) + addToTotalSessionCost(costUSD, usage as any, options.model) + } + break + } } - if (event.type === 'message_stop' && usage.input_tokens + usage.output_tokens > 0) { - const costUSD = calculateUSDCost(grokModel, usage as any) - addToTotalSessionCost(costUSD, usage as any, options.model) - } - yield { type: 'stream_event',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/api/grok/index.ts` around lines 161 - 175, The cost calculation currently runs after the switch for every stream event but should only run when event.type === 'message_stop'; move the block that computes costUSD via calculateUSDCost(grokModel, usage) and calls addToTotalSessionCost(costUSD, usage, options.model) into the existing case 'message_stop' branch (after merging any deltaUsage into usage) so it only executes inside the case 'message_stop' path and avoids redundant checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/services/api/grok/client.ts`:
- Around line 20-21: The early "if (cachedClient) return cachedClient" ignores a
provided fetchOverride; update the logic in the getGrokClient (or the function
that references cachedClient and fetchOverride) so that if fetchOverride is
passed you either (a) inject/replace the transport on the existing cachedClient
with the provided fetchOverride or (b) recreate the client using the
fetchOverride before returning; apply the same change to the other occurrence
around the cachedClient check (lines ~35-37) so callers can override the
transport even after the singleton exists.
In `@src/services/api/grok/modelMapping.ts`:
- Around line 42-49: getUserModelMap currently casts JSON.parse(raw) to
Record<string,string> without checking that each value is a string, so
non-string values like {"opus":42} can slip through; update getUserModelMap to
validate parsed entries by iterating over Object.entries(parsed) and ensure
every value typeof === 'string' (either build and return a new
Record<string,string> containing only string values or return null/throw if any
value is non-string), and apply the same validation logic to the similar parsing
block around lines 79-80 so both model-map parsers only return maps whose values
are strings.
In `@src/utils/model/providers.ts`:
- Around line 12-26: You added 'grok' to the APIProvider returned by
getAPIProvider(), which makes downstream provider handling non‑exhaustive;
update the consumers src/utils/model/modelOptions.ts and
src/utils/model/modelSupportOverrides.ts to use an exhaustive switch on
APIProvider (or otherwise exhaustively guard all union members) instead of
current ternaries so 'grok' gets its own branch and does not fall back to
Anthropic logic; specifically locate and replace the ternary/provider-selection
expressions in the functions that compute model options and capability overrides
(search for usages of APIProvider, getAPIProvider, and existing ternary checks
for 'anthropic'/'openai'/'gemini') and add an explicit 'grok' case with
appropriate defaults/overrides.
---
Nitpick comments:
In `@src/services/api/grok/index.ts`:
- Line 10: The import statements are duplicated for utilities from
../../../utils/messages.js; consolidate them by replacing separate imports so
that normalizeMessagesForAPI, createAssistantAPIErrorMessage, and
normalizeContentFromAPI are all imported in a single import declaration (i.e.,
import { normalizeMessagesForAPI, createAssistantAPIErrorMessage,
normalizeContentFromAPI } from '../../../utils/messages.js') and remove the
redundant import lines; update any local references if necessary to match the
single imported symbols.
- Around line 92-93: The current declarations contentBlocks: Record<number, any>
and partialMessage: any lose type safety; define explicit interfaces (e.g.,
TextBlock, ToolUseBlock, ThinkingBlock) and a union type ContentBlock (e.g.,
type ContentBlock = TextBlock | ToolUseBlock | ThinkingBlock) and replace
contentBlocks with Record<number, ContentBlock>; also define a PartialMessage
(or similar) interface for the structure held in partialMessage and type
partialMessage as PartialMessage | undefined (or ContentBlock | undefined if
appropriate) and update any places that read/write these to match the new types
(look for usages of contentBlocks and partialMessage in the file to adjust).
- Around line 161-175: The cost calculation currently runs after the switch for
every stream event but should only run when event.type === 'message_stop'; move
the block that computes costUSD via calculateUSDCost(grokModel, usage) and calls
addToTotalSessionCost(costUSD, usage, options.model) into the existing case
'message_stop' branch (after merging any deltaUsage into usage) so it only
executes inside the case 'message_stop' path and avoids redundant checks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b11d2d7e-c20a-4820-8239-1f948595bbdd
📒 Files selected for processing (9)
src/commands/provider.tssrc/services/api/claude.tssrc/services/api/grok/__tests__/client.test.tssrc/services/api/grok/__tests__/modelMapping.test.tssrc/services/api/grok/client.tssrc/services/api/grok/index.tssrc/services/api/grok/modelMapping.tssrc/utils/model/providers.tssrc/utils/settings/types.ts
| if (cachedClient) return cachedClient | ||
|
|
There was a problem hiding this comment.
fetchOverride is ignored when a cached client already exists.
The early cache return prevents override-based callers from injecting custom transport once the singleton is initialized.
Proposed fix
export function getGrokClient(options?: {
maxRetries?: number
fetchOverride?: typeof fetch
source?: string
}): OpenAI {
- if (cachedClient) return cachedClient
+ if (cachedClient && !options?.fetchOverride) return cachedClient
@@
- if (!options?.fetchOverride) {
+ if (!options?.fetchOverride) {
cachedClient = client
}Also applies to: 35-37
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/services/api/grok/client.ts` around lines 20 - 21, The early "if
(cachedClient) return cachedClient" ignores a provided fetchOverride; update the
logic in the getGrokClient (or the function that references cachedClient and
fetchOverride) so that if fetchOverride is passed you either (a) inject/replace
the transport on the existing cachedClient with the provided fetchOverride or
(b) recreate the client using the fetchOverride before returning; apply the same
change to the other occurrence around the cachedClient check (lines ~35-37) so
callers can override the transport even after the singleton exists.
| function getUserModelMap(): Record<string, string> | null { | ||
| const raw = process.env.GROK_MODEL_MAP | ||
| if (!raw) return null | ||
| try { | ||
| const parsed = JSON.parse(raw) | ||
| if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { | ||
| return parsed as Record<string, string> | ||
| } |
There was a problem hiding this comment.
Validate GROK_MODEL_MAP values before returning them.
JSON.parse result is cast to Record<string, string> without checking value types. A value like {"opus":42} can return a non-string model at runtime.
Proposed fix
function getUserModelMap(): Record<string, string> | null {
const raw = process.env.GROK_MODEL_MAP
if (!raw) return null
try {
const parsed = JSON.parse(raw)
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
- return parsed as Record<string, string>
+ const out: Record<string, string> = {}
+ for (const [k, v] of Object.entries(parsed)) {
+ if (typeof v === 'string' && v.trim().length > 0) {
+ out[k.toLowerCase()] = v.trim()
+ }
+ }
+ return Object.keys(out).length > 0 ? out : null
}
} catch {
// ignore invalid JSON
}
return null
}Also applies to: 79-80
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/services/api/grok/modelMapping.ts` around lines 42 - 49, getUserModelMap
currently casts JSON.parse(raw) to Record<string,string> without checking that
each value is a string, so non-string values like {"opus":42} can slip through;
update getUserModelMap to validate parsed entries by iterating over
Object.entries(parsed) and ensure every value typeof === 'string' (either build
and return a new Record<string,string> containing only string values or return
null/throw if any value is non-string), and apply the same validation logic to
the similar parsing block around lines 79-80 so both model-map parsers only
return maps whose values are strings.
| | 'grok' | ||
|
|
||
| export function getAPIProvider(): APIProvider { | ||
| const modelType = getInitialSettings().modelType | ||
| if (modelType === 'openai') return 'openai' | ||
| if (modelType === 'gemini') return 'gemini' | ||
| if (modelType === 'grok') return 'grok' | ||
|
|
||
| if (isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)) return 'bedrock' | ||
| if (isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX)) return 'vertex' | ||
| if (isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)) return 'foundry' | ||
|
|
||
| if (isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI)) return 'openai' | ||
| if (isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI)) return 'gemini' | ||
| if (isEnvTruthy(process.env.CLAUDE_CODE_USE_GROK)) return 'grok' |
There was a problem hiding this comment.
Adding 'grok' here exposes non-exhaustive provider handling downstream.
With this new union member, existing provider ternaries in src/utils/model/modelOptions.ts and src/utils/model/modelSupportOverrides.ts now silently fall back to Anthropic branches for Grok, which can produce wrong defaults/capability overrides.
Please follow up by making provider selection exhaustive (e.g., switch on APIProvider) in those consumers.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/model/providers.ts` around lines 12 - 26, You added 'grok' to the
APIProvider returned by getAPIProvider(), which makes downstream provider
handling non‑exhaustive; update the consumers src/utils/model/modelOptions.ts
and src/utils/model/modelSupportOverrides.ts to use an exhaustive switch on
APIProvider (or otherwise exhaustively guard all union members) instead of
current ternaries so 'grok' gets its own branch and does not fall back to
Anthropic logic; specifically locate and replace the ternary/provider-selection
expressions in the functions that compute model options and capability overrides
(search for usages of APIProvider, getAPIProvider, and existing ternary checks
for 'anthropic'/'openai'/'gemini') and add an explicit 'grok' case with
appropriate defaults/overrides.
|
为啥有两个同名的 PR? |
因为你改了架构的一点东西,不是让我重新适配吗,之前的那个close了。 |

Summary
grok-4.20-reasoning, sonnet/haiku →grok-3-mini-fastGROK_MODEL,GROK_MODEL_MAP(JSON), orGROK_DEFAULT_{FAMILY}_MODEL/providercommand for runtime switching (/provider grok)GROK_API_KEY/XAI_API_KEY, optionalGROK_BASE_URLFiles changed
src/services/api/grok/— client, modelMapping, index (+ 2 test files, 14 tests)providers.ts,types.ts,claude.ts(dispatch),provider.ts(command)Test plan
bun run src/entrypoints/cli.tsx --versionpassesSummary by CodeRabbit
Release Notes
New Features
Tests