Add Claude Code LLM provider support#1799
Conversation
- Create ClaudeCodeClient that wraps the AI SDK Claude Code provider - Add claude-code-opus, claude-code-sonnet, claude-code-haiku model names - Add claude-code to ModelProvider type and modelToProviderMap - Register ClaudeCodeClient in LLMProvider.getClient() This allows users to leverage their Claude Pro/Max subscription instead of API credits when using Stagehand.
Adds the Claude Code AI SDK provider package to optionalDependencies, allowing users to use their Claude Pro/Max subscription with Stagehand.
- Add hasVision = true since Claude models support vision - Use consistent category naming in error logs
|
Greptile SummaryThis PR adds Key issues found:
Confidence Score: 2/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Caller
participant LLMProvider
participant ClaudeCodeClient
participant ai_sdk as ai (generateText / generateObject)
participant provider as ai-sdk-provider-claude-code
participant CLI as Claude Code CLI
Caller->>LLMProvider: getClient("claude-code-sonnet")
LLMProvider->>ClaudeCodeClient: new ClaudeCodeClient({ modelName, logger })
ClaudeCodeClient->>provider: require("ai-sdk-provider-claude-code")
provider-->>ClaudeCodeClient: claudeCode("sonnet") → LanguageModelV2
ClaudeCodeClient-->>LLMProvider: instance
LLMProvider-->>Caller: ClaudeCodeClient
Caller->>ClaudeCodeClient: createChatCompletion({ options, logger })
alt response_model set
ClaudeCodeClient->>ai_sdk: generateObject({ model, messages, schema, temperature })
ai_sdk->>CLI: invoke claude code
CLI-->>ai_sdk: structured response
ai_sdk-->>ClaudeCodeClient: objectResponse
ClaudeCodeClient-->>Caller: { data, usage }
else no response_model
ClaudeCodeClient->>ai_sdk: generateText({ model, messages, tools, toolChoice, temperature })
ai_sdk->>CLI: invoke claude code
CLI-->>ai_sdk: text + toolCalls
ai_sdk-->>ClaudeCodeClient: textResponse
ClaudeCodeClient-->>Caller: LLMResponse (choices, usage)
end
Last reviewed commit: 770cf6a |
| async createChatCompletion<T = LLMResponse>({ | ||
| options, | ||
| }: CreateChatCompletionOptions): Promise<T> { |
There was a problem hiding this comment.
logger parameter from CreateChatCompletionOptions is ignored
createChatCompletion destructures only { options } from CreateChatCompletionOptions, silently dropping the logger argument that callers pass with each request. Every other client in this codebase (AnthropicClient, OpenAIClient, GroqClient, CerebrasClient, GoogleClient) uses the logger from the method parameters — not this.logger — for all per-request log lines. If a caller passes a request-scoped logger (e.g., for tracing), it will be completely ignored here.
| async createChatCompletion<T = LLMResponse>({ | |
| options, | |
| }: CreateChatCompletionOptions): Promise<T> { | |
| async createChatCompletion<T = LLMResponse>({ | |
| options, | |
| logger, | |
| }: CreateChatCompletionOptions): Promise<T> { |
Then replace each this.logger?.({ with (logger ?? this.logger)?.({ (or use the passed logger directly as other clients do).
| objectResponse = await generateObject({ | ||
| model: this.model, | ||
| messages: formattedMessages, | ||
| schema: options.response_model.schema, | ||
| temperature: options.temperature, | ||
| }); |
There was a problem hiding this comment.
maxOutputTokens silently ignored for generateObject
options.maxOutputTokens is present in ChatCompletionOptions and is respected by every other client (AnthropicClient passes it as max_tokens: options.maxOutputTokens || 8192, GroqClient and CerebrasClient also pass max_tokens, GoogleClient passes maxOutputTokens). Here it is never forwarded to generateObject, so any caller that sets maxOutputTokens on a Claude Code model will have that constraint silently dropped — potentially leading to unexpectedly long or truncated responses.
| objectResponse = await generateObject({ | |
| model: this.model, | |
| messages: formattedMessages, | |
| schema: options.response_model.schema, | |
| temperature: options.temperature, | |
| }); | |
| objectResponse = await generateObject({ | |
| model: this.model, | |
| messages: formattedMessages, | |
| schema: options.response_model.schema, | |
| temperature: options.temperature, | |
| maxTokens: options.maxOutputTokens, | |
| }); |
| const textResponse = await generateText({ | ||
| model: this.model, | ||
| messages: formattedMessages, | ||
| tools: Object.keys(tools).length > 0 ? tools : undefined, | ||
| toolChoice: | ||
| Object.keys(tools).length > 0 | ||
| ? options.tool_choice === "required" | ||
| ? "required" | ||
| : options.tool_choice === "none" | ||
| ? "none" | ||
| : "auto" | ||
| : undefined, | ||
| temperature: options.temperature, | ||
| }); |
There was a problem hiding this comment.
maxOutputTokens silently ignored for generateText
Same issue as the generateObject path above — options.maxOutputTokens is never forwarded to generateText. This means the token limit option is consistently dropped for all Claude Code calls.
| const textResponse = await generateText({ | |
| model: this.model, | |
| messages: formattedMessages, | |
| tools: Object.keys(tools).length > 0 ? tools : undefined, | |
| toolChoice: | |
| Object.keys(tools).length > 0 | |
| ? options.tool_choice === "required" | |
| ? "required" | |
| : options.tool_choice === "none" | |
| ? "none" | |
| : "auto" | |
| : undefined, | |
| temperature: options.temperature, | |
| }); | |
| const textResponse = await generateText({ | |
| model: this.model, | |
| messages: formattedMessages, | |
| tools: Object.keys(tools).length > 0 ? tools : undefined, | |
| toolChoice: | |
| Object.keys(tools).length > 0 | |
| ? options.tool_choice === "required" | |
| ? "required" | |
| : options.tool_choice === "none" | |
| ? "none" | |
| : "auto" | |
| : undefined, | |
| temperature: options.temperature, | |
| maxTokens: options.maxOutputTokens, | |
| }); |
| // Dynamically import the claude-code provider | ||
| // This is done lazily to avoid requiring the package if not used | ||
| const providerModelName = claudeCodeModelMap[modelName]; | ||
| this.model = this.createClaudeCodeModel(providerModelName); |
There was a problem hiding this comment.
Inaccurate "lazy loading" comment
The comment on line 56 says "This is done lazily to avoid requiring the package if not used", but createClaudeCodeModel is called synchronously on line 58 — directly in the constructor body. The require() call executes eagerly at construction time (i.e., when LLMProvider.getClient() instantiates the client), not lazily when the model is first used.
Consider either: (a) updating the comment to accurately describe eager initialization, or (b) actually deferring the require() to the first createChatCompletion call if lazy loading is truly desired.
There was a problem hiding this comment.
4 issues found across 4 files
Confidence score: 3/5
- There is concrete regression risk in
packages/core/lib/v3/llm/ClaudeCodeClient.ts:options.maxOutputTokensis not forwarded in bothgenerateTextandgenerateObject, so caller token limits are silently ignored. packages/core/lib/v3/llm/LLMProvider.tsadds more hardcoded model-name allowlisting (modelToProviderMap), which increases maintenance risk and can cause valid models to be rejected unless code is updated.- The logger handling in
packages/core/lib/v3/llm/ClaudeCodeClient.tsis inconsistent with other clients (per-requestloggernot used), which can reduce observability/debugging quality for request-scoped logs. - Pay close attention to
packages/core/lib/v3/llm/ClaudeCodeClient.ts,packages/core/lib/v3/llm/LLMProvider.ts- token-limit forwarding and model/provider mapping behavior are the highest-impact areas to verify before merge.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/core/lib/v3/llm/LLMProvider.ts">
<violation number="1" location="packages/core/lib/v3/llm/LLMProvider.ts:86">
P1: Custom agent: **Ensure we never check against hardcoded lists of allowed LLM model names**
Adding three new hardcoded model names (`claude-code-opus`, `claude-code-sonnet`, `claude-code-haiku`) to `modelToProviderMap` violates the rule against hardcoded allowed-model lists. Models change frequently and each addition requires updating multiple files (`model.ts`, `LLMProvider.ts`, `ClaudeCodeClient.ts`).
Consider supporting these models through the existing `provider/model-name` format (e.g. `claude-code/opus`) which already works via the dynamic `modelName.includes("/")` branch — that way new Claude Code model variants work automatically without code changes.</violation>
</file>
<file name="packages/core/lib/v3/llm/ClaudeCodeClient.ts">
<violation number="1" location="packages/core/lib/v3/llm/ClaudeCodeClient.ts:83">
P2: The `logger` parameter from `CreateChatCompletionOptions` is not destructured, so all per-request logging uses `this.logger` instead. Every other LLM client in this codebase extracts and uses the per-request `logger`, which supports request-scoped tracing. Destructure `logger` and use it (with a fallback to `this.logger`) for consistency.</violation>
<violation number="2" location="packages/core/lib/v3/llm/ClaudeCodeClient.ts:189">
P2: `options.maxOutputTokens` is never forwarded to the `generateObject` call. Other LLM clients in this codebase (e.g., `AnthropicClient`, `GoogleClient`) all forward this option, so callers setting a token limit will have it silently ignored for Claude Code models. Add `maxOutputTokens: options.maxOutputTokens` to the `generateObject` call.</violation>
<violation number="3" location="packages/core/lib/v3/llm/ClaudeCodeClient.ts:278">
P2: `options.maxOutputTokens` is never forwarded to the `generateText` call. This means the token limit option is silently dropped for all Claude Code text generation calls. Add `maxOutputTokens: options.maxOutputTokens` to the `generateText` call.</violation>
</file>
Architecture diagram
sequenceDiagram
participant App as Stagehand Application
participant Provider as LLMProvider
participant Client as ClaudeCodeClient
participant SDK as ai-sdk-provider-claude-code
participant CLI as Claude Code CLI
Note over App,CLI: Runtime Flow for Claude Code LLM Provider
App->>Provider: getInstance(model: "claude-code-sonnet")
Provider->>Provider: Map model to "claude-code" provider
Provider->>Client: NEW: Initialize ClaudeCodeClient(modelName, options)
Provider-->>App: LLMClient instance
App->>Client: query(messages, tools?)
rect rgb(23, 37, 84)
Note right of Client: NEW: Lazy Loading Dependency
Client->>Client: Dynamic import of 'ai-sdk-provider-claude-code'
alt Package not installed
Client-->>App: Throw Error (Instruction to install dependency)
else Package available
Client->>Client: Map "claude-code-sonnet" -> "sonnet"
end
end
Client->>SDK: generateText() / generateObject()
Note over SDK,CLI: Provider requires authenticated local session
SDK->>CLI: Check authentication (via 'claude login')
alt Session Valid
CLI-->>SDK: Credentials/Pipe
SDK-->>Client: AI SDK Response (text, toolCalls, etc.)
Client->>Client: CHANGED: Transform to Stagehand LLMResponse
Client-->>App: LLMResponse
else Session Invalid / CLI missing
CLI-->>SDK: Error
SDK-->>Client: Exception
Client-->>App: Error (Authentication Required)
end
Since this is your first cubic review, here's how it works:
- cubic automatically reviews your code and comments on bugs and improvements
- Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
- Add one-off context when rerunning by tagging
@cubic-dev-aiwith guidance or docs links (includingllms.txt) - Ask questions if you need clarification on any suggestion
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| "claude-3-5-sonnet-20241022": "anthropic", | ||
| "claude-3-7-sonnet-20250219": "anthropic", | ||
| "claude-3-7-sonnet-latest": "anthropic", | ||
| "claude-code-opus": "claude-code", |
There was a problem hiding this comment.
P1: Custom agent: Ensure we never check against hardcoded lists of allowed LLM model names
Adding three new hardcoded model names (claude-code-opus, claude-code-sonnet, claude-code-haiku) to modelToProviderMap violates the rule against hardcoded allowed-model lists. Models change frequently and each addition requires updating multiple files (model.ts, LLMProvider.ts, ClaudeCodeClient.ts).
Consider supporting these models through the existing provider/model-name format (e.g. claude-code/opus) which already works via the dynamic modelName.includes("/") branch — that way new Claude Code model variants work automatically without code changes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/llm/LLMProvider.ts, line 86:
<comment>Adding three new hardcoded model names (`claude-code-opus`, `claude-code-sonnet`, `claude-code-haiku`) to `modelToProviderMap` violates the rule against hardcoded allowed-model lists. Models change frequently and each addition requires updating multiple files (`model.ts`, `LLMProvider.ts`, `ClaudeCodeClient.ts`).
Consider supporting these models through the existing `provider/model-name` format (e.g. `claude-code/opus`) which already works via the dynamic `modelName.includes("/")` branch — that way new Claude Code model variants work automatically without code changes.</comment>
<file context>
@@ -79,6 +83,9 @@ const modelToProviderMap: { [key in AvailableModel]: ModelProvider } = {
"claude-3-5-sonnet-20241022": "anthropic",
"claude-3-7-sonnet-20250219": "anthropic",
"claude-3-7-sonnet-latest": "anthropic",
+ "claude-code-opus": "claude-code",
+ "claude-code-sonnet": "claude-code",
+ "claude-code-haiku": "claude-code",
</file context>
| return this.model; | ||
| } | ||
|
|
||
| async createChatCompletion<T = LLMResponse>({ |
There was a problem hiding this comment.
P2: The logger parameter from CreateChatCompletionOptions is not destructured, so all per-request logging uses this.logger instead. Every other LLM client in this codebase extracts and uses the per-request logger, which supports request-scoped tracing. Destructure logger and use it (with a fallback to this.logger) for consistency.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/llm/ClaudeCodeClient.ts, line 83:
<comment>The `logger` parameter from `CreateChatCompletionOptions` is not destructured, so all per-request logging uses `this.logger` instead. Every other LLM client in this codebase extracts and uses the per-request `logger`, which supports request-scoped tracing. Destructure `logger` and use it (with a fallback to `this.logger`) for consistency.</comment>
<file context>
@@ -0,0 +1,354 @@
+ return this.model;
+ }
+
+ async createChatCompletion<T = LLMResponse>({
+ options,
+ }: CreateChatCompletionOptions): Promise<T> {
</file context>
| } | ||
| } | ||
|
|
||
| const textResponse = await generateText({ |
There was a problem hiding this comment.
P2: options.maxOutputTokens is never forwarded to the generateText call. This means the token limit option is silently dropped for all Claude Code text generation calls. Add maxOutputTokens: options.maxOutputTokens to the generateText call.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/llm/ClaudeCodeClient.ts, line 278:
<comment>`options.maxOutputTokens` is never forwarded to the `generateText` call. This means the token limit option is silently dropped for all Claude Code text generation calls. Add `maxOutputTokens: options.maxOutputTokens` to the `generateText` call.</comment>
<file context>
@@ -0,0 +1,354 @@
+ }
+ }
+
+ const textResponse = await generateText({
+ model: this.model,
+ messages: formattedMessages,
</file context>
| let objectResponse: Awaited<ReturnType<typeof generateObject>>; | ||
| if (options.response_model) { | ||
| try { | ||
| objectResponse = await generateObject({ |
There was a problem hiding this comment.
P2: options.maxOutputTokens is never forwarded to the generateObject call. Other LLM clients in this codebase (e.g., AnthropicClient, GoogleClient) all forward this option, so callers setting a token limit will have it silently ignored for Claude Code models. Add maxOutputTokens: options.maxOutputTokens to the generateObject call.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/llm/ClaudeCodeClient.ts, line 189:
<comment>`options.maxOutputTokens` is never forwarded to the `generateObject` call. Other LLM clients in this codebase (e.g., `AnthropicClient`, `GoogleClient`) all forward this option, so callers setting a token limit will have it silently ignored for Claude Code models. Add `maxOutputTokens: options.maxOutputTokens` to the `generateObject` call.</comment>
<file context>
@@ -0,0 +1,354 @@
+ let objectResponse: Awaited<ReturnType<typeof generateObject>>;
+ if (options.response_model) {
+ try {
+ objectResponse = await generateObject({
+ model: this.model,
+ messages: formattedMessages,
</file context>
Summary
This PR adds support for the Claude Code models (opus, sonnet, haiku) as a new LLM provider in the Stagehand framework. Claude Code is a specialized model variant optimized for code generation and understanding.
Key Changes
ClaudeCodeClient.ts) that extendsLLMClientto support Claude Code models via theai-sdk-provider-claude-codepackageclaude-code-opusclaude-code-sonnetclaude-code-haikuLLMProviderto instantiateClaudeCodeClientwhen Claude Code models are requestedai-sdk-provider-claude-codeas a dependency with lazy loading to handle optional installationImplementation Details
ClaudeCodeClientdynamically imports theai-sdk-provider-claude-codepackage with helpful error messaging if not installedgenerateTextandgenerateObjectfunctionsclaude-code-opus→opus)LLMResponseformathttps://claude.ai/code/session_018hRjjGZDAxknFeDRNG4Gam
Summary by cubic
Adds Claude Code as a first-class LLM provider with
claude-code-opus,claude-code-sonnet, andclaude-code-haiku. Enables code-focused generation using a Claude Pro/Max subscription (via the Claude Code CLI) with vision and tool calling support.New Features
ClaudeCodeClientusing the AI SDK provider; supports text and object generation, vision, and tool calls; returns standardLLMResponse.LLMProvider, updated model map, and extendedAvailableModel/ModelProvider.Dependencies
ai-sdk-provider-claude-code; loaded dynamically with clear error messaging if missing. Requires the Claude Code CLI to be installed and authenticated (claude login).Written for commit 770cf6a. Summary will update on new commits. Review in cubic