Skip to content

feat: add Grok (xAI) API adapter#152

Open
uk0 wants to merge 1 commit intoclaude-code-best:mainfrom
uk0:feat/grok-api-adapter-v2
Open

feat: add Grok (xAI) API adapter#152
uk0 wants to merge 1 commit intoclaude-code-best:mainfrom
uk0:feat/grok-api-adapter-v2

Conversation

@uk0
Copy link
Copy Markdown
Contributor

@uk0 uk0 commented Apr 6, 2026

Summary

  • Add xAI Grok as a new API provider, reusing OpenAI-compatible converters and stream adapter
  • Default mapping: opus → grok-4.20-reasoning, sonnet/haiku → grok-3-mini-fast
  • User-customizable model mapping via GROK_MODEL, GROK_MODEL_MAP (JSON), or GROK_DEFAULT_{FAMILY}_MODEL
  • Integrated with /provider command for runtime switching (/provider grok)
  • Supports GROK_API_KEY / XAI_API_KEY, optional GROK_BASE_URL

Files changed

  • New: src/services/api/grok/ — client, modelMapping, index (+ 2 test files, 14 tests)
  • Modified: providers.ts, types.ts, claude.ts (dispatch), provider.ts (command)

Test plan

  • 14 new unit tests pass (client + modelMapping with custom map)
  • All 146 API + model tests pass
  • Smoke test: bun run src/entrypoints/cli.tsx --version passes

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Grok AI as a new provider option alongside existing providers
    • Users can now configure and use Grok with API key and custom model settings
  • Tests

    • Added test coverage for Grok client initialization and model mapping functionality

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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
Provider Integration
src/commands/provider.ts, src/utils/model/providers.ts, src/utils/settings/types.ts
Added grok to provider command options, updated APIProvider type to include 'grok', extended getAPIProvider() to return 'grok' based on settings or CLAUDE_CODE_USE_GROK env var, and updated settings schema to accept 'grok' as a modelType value.
Grok API Client
src/services/api/grok/client.ts
New module providing getGrokClient() that constructs and caches an OpenAI-compatible client for the xAI Grok API, reading credentials from GROK_API_KEY/XAI_API_KEY, configurable base URL, timeout, and proxy options. Exports clearGrokClientCache() for cache reset.
Grok Query Handler
src/services/api/grok/index.ts
New async generator queryModelGrok() that streams Grok API responses, handles message/tool conversion to OpenAI format, adapts OpenAI stream events to Anthropic-compatible format, tracks token usage and USDC cost, and includes error handling with standardized error messages.
Model Resolution
src/services/api/grok/modelMapping.ts
New module with resolveGrokModel() function that maps Anthropic model identifiers to Grok models with precedence handling: GROK_MODEL env var override, GROK_MODEL_MAP JSON mapping, per-family env var defaults, and built-in family/exact-name mappings.
API Routing
src/services/api/claude.ts
Added provider-specific execution branch that routes to Grok query handler when getAPIProvider() returns 'grok', inserted after existing OpenAI/Gemini delegations.
Test Suites
src/services/api/grok/__tests__/client.test.ts, src/services/api/grok/__tests__/modelMapping.test.ts
Comprehensive Bun test suites verifying Grok client caching behavior, credential/URL configuration, and model resolution precedence across environment variable overrides and mapping configurations.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PR #129: Extends provider switching logic with the same pattern applied to Grok, modifying provider command and env var handling in commands/provider.ts and utils/model/providers.ts.
  • PR #99: Implements a similar settings-backed API provider integration pattern across the same core files (utils/model/providers.ts, utils/settings/types.ts, services/api/claude.ts).

Poem

🐰 A hoppy new friend joins the warren today,
With Grok's speedy smarts in an OpenAI way!
From xAI it bounds, through models it maps,
Now Claude Code can dance in more API traps! 🚀✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add Grok (xAI) API adapter' accurately describes the main change: adding support for Grok as a new API provider integrated into the existing system.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@uk0
Copy link
Copy Markdown
Contributor Author

uk0 commented Apr 6, 2026

image

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) and createAssistantAPIErrorMessage, 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 any for contentBlocks and partialMessage loses 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 the message_stop case.

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 the case '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

📥 Commits

Reviewing files that changed from the base of the PR and between 4d62f63 and 06564cf.

📒 Files selected for processing (9)
  • src/commands/provider.ts
  • src/services/api/claude.ts
  • src/services/api/grok/__tests__/client.test.ts
  • src/services/api/grok/__tests__/modelMapping.test.ts
  • src/services/api/grok/client.ts
  • src/services/api/grok/index.ts
  • src/services/api/grok/modelMapping.ts
  • src/utils/model/providers.ts
  • src/utils/settings/types.ts

Comment on lines +20 to +21
if (cachedClient) return cachedClient

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +42 to +49
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>
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +12 to +26
| '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'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

@claude-code-best
Copy link
Copy Markdown
Owner

claude-code-best commented Apr 6, 2026

为啥有两个同名的 PR?

@uk0
Copy link
Copy Markdown
Contributor Author

uk0 commented Apr 6, 2026

为啥有两个同名的 PR?

因为你改了架构的一点东西,不是让我重新适配吗,之前的那个close了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants