Skip to content

Add Claude Code LLM provider support#1799

Open
adelin-b wants to merge 3 commits intobrowserbase:mainfrom
adelin-b:claude/add-claude-code-provider-018hRjjGZDAxknFeDRNG4Gam
Open

Add Claude Code LLM provider support#1799
adelin-b wants to merge 3 commits intobrowserbase:mainfrom
adelin-b:claude/add-claude-code-provider-018hRjjGZDAxknFeDRNG4Gam

Conversation

@adelin-b
Copy link

@adelin-b adelin-b commented Mar 9, 2026

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

  • New ClaudeCodeClient: Created a new LLM client implementation (ClaudeCodeClient.ts) that extends LLMClient to support Claude Code models via the ai-sdk-provider-claude-code package
  • Model Registration: Added three new Claude Code model variants to the available models list:
    • claude-code-opus
    • claude-code-sonnet
    • claude-code-haiku
  • Provider Integration: Updated LLMProvider to instantiate ClaudeCodeClient when Claude Code models are requested
  • Dependency: Added ai-sdk-provider-claude-code as a dependency with lazy loading to handle optional installation

Implementation Details

  • The ClaudeCodeClient dynamically imports the ai-sdk-provider-claude-code package with helpful error messaging if not installed
  • Supports both text generation and structured object generation via the AI SDK's generateText and generateObject functions
  • Handles message formatting including vision capabilities (image support) and tool/function calling
  • Includes comprehensive logging for debugging and monitoring
  • Maps Stagehand model names to provider-specific model names (e.g., claude-code-opusopus)
  • Properly transforms AI SDK responses to match the standard LLMResponse format

https://claude.ai/code/session_018hRjjGZDAxknFeDRNG4Gam


Summary by cubic

Adds Claude Code as a first-class LLM provider with claude-code-opus, claude-code-sonnet, and claude-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

    • New ClaudeCodeClient using the AI SDK provider; supports text and object generation, vision, and tool calls; returns standard LLMResponse.
    • Registered provider in LLMProvider, updated model map, and extended AvailableModel/ModelProvider.
  • Dependencies

    • Added 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

- 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
@changeset-bot
Copy link

changeset-bot bot commented Mar 9, 2026

⚠️ No Changeset found

Latest commit: 770cf6a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 9, 2026

Greptile Summary

This PR adds ClaudeCodeClient, a new LLM provider that routes claude-code-opus, claude-code-sonnet, and claude-code-haiku model requests through the ai-sdk-provider-claude-code package (which wraps the Claude Code CLI). The integration is structurally sound — the dependency is correctly placed in optionalDependencies, the provider mapping and switch case in LLMProvider follow established patterns, and the model name aliases are cleanly defined.

Key issues found:

  • maxOutputTokens from ChatCompletionOptions is never forwarded to either generateObject or generateText as maxTokens, silently ignoring user-configured token limits — every other client in the codebase forwards this value.
  • The logger parameter from CreateChatCompletionOptions is destructured away and this.logger is used instead, breaking the per-request logger contract that all other client implementations follow.
  • A comment in the constructor incorrectly describes the require() call as "lazy loading" when it actually executes synchronously at construction time.

Confidence Score: 2/5

  • Not safe to merge as-is — two logic bugs cause maxOutputTokens to be silently dropped and the per-request logger to be ignored.
  • The structural additions (model registration, provider switch case, optional dependency) are correct and low-risk. However, maxOutputTokens is never passed to generateObject or generateText, meaning any caller relying on token limits for Claude Code models will be silently ignored — a behavioral regression compared to every other provider. The logger parameter from CreateChatCompletionOptions is also dropped, breaking the established per-request logging contract.
  • Focus on packages/core/lib/v3/llm/ClaudeCodeClient.ts — specifically the generateObject and generateText call sites and the createChatCompletion method signature.

Important Files Changed

Filename Overview
packages/core/lib/v3/llm/ClaudeCodeClient.ts New LLM client for Claude Code models. Has two logic bugs: maxOutputTokens is never forwarded to generateObject or generateText (silently dropping user-specified token limits), and the logger from CreateChatCompletionOptions is ignored in favour of this.logger, breaking the contract expected by all other clients. Also contains a misleading "lazy loading" comment when initialization is actually eager.
packages/core/lib/v3/llm/LLMProvider.ts Added claude-code provider case and model-to-provider mappings. Integration looks correct — model names map cleanly and the switch case follows the same pattern as other providers.
packages/core/lib/v3/types/public/model.ts Adds claude-code-opus, claude-code-sonnet, claude-code-haiku to AvailableModel and claude-code to ModelProvider. Changes are additive and follow existing naming conventions.
packages/core/package.json Correctly adds ai-sdk-provider-claude-code to optionalDependencies (not dependencies), consistent with the dynamic require() pattern used in the client and aligned with other optional AI SDK providers.

Sequence Diagram

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

Last reviewed commit: 770cf6a

Comment on lines +83 to +85
async createChatCompletion<T = LLMResponse>({
options,
}: CreateChatCompletionOptions): Promise<T> {
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Suggested change
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).

Comment on lines +189 to +194
objectResponse = await generateObject({
model: this.model,
messages: formattedMessages,
schema: options.response_model.schema,
temperature: options.temperature,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Suggested change
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,
});

Comment on lines +278 to +291
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,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Suggested change
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,
});

Comment on lines +55 to +58
// 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);
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

4 issues found across 4 files

Confidence score: 3/5

  • There is concrete regression risk in packages/core/lib/v3/llm/ClaudeCodeClient.ts: options.maxOutputTokens is not forwarded in both generateText and generateObject, so caller token limits are silently ignored.
  • packages/core/lib/v3/llm/LLMProvider.ts adds 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.ts is inconsistent with other clients (per-request logger not 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
Loading

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-ai with guidance or docs links (including llms.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",
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 9, 2026

Choose a reason for hiding this comment

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

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>
Fix with Cubic

return this.model;
}

async createChatCompletion<T = LLMResponse>({
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 9, 2026

Choose a reason for hiding this comment

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

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>
Fix with Cubic

}
}

const textResponse = await generateText({
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 9, 2026

Choose a reason for hiding this comment

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

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>
Fix with Cubic

let objectResponse: Awaited<ReturnType<typeof generateObject>>;
if (options.response_model) {
try {
objectResponse = await generateObject({
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 9, 2026

Choose a reason for hiding this comment

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

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>
Fix with Cubic

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