Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
812e340
fix: Claude Code Opus support
hannesrudolph Dec 14, 2025
09ba154
fix: correct maxTokens test expectations to match model definition
hannesrudolph Dec 14, 2025
1347c97
test: update claude-code tests to reflect supportsImages and valid mo…
hannesrudolph Dec 14, 2025
8182724
fix: update claude-code-caching.spec.ts to mock OAuth and streaming c…
hannesrudolph Dec 14, 2025
7c72b85
feat(claude-code): add completePrompt for context condensing and prom…
hannesrudolph Dec 14, 2025
7749af4
feat(claude-code): add backward compatibility for legacy model names
hannesrudolph Dec 14, 2025
1933f1a
feat: improve Claude Code auth error message with settings link
hannesrudolph Dec 14, 2025
0219766
fix: use Object.hasOwn instead of in operator for model validation
hannesrudolph Dec 14, 2025
f6be2db
fix: pass section=providers when navigating to settings from Claude C…
hannesrudolph Dec 14, 2025
33210aa
Remove Claude Code CLI wrapper and legacy fallbacks
hannesrudolph Dec 17, 2025
677b2c3
Fix ProviderSettingsManager migration test
hannesrudolph Dec 17, 2025
b04932b
chore(claude-code): remove unused legacy CLI message types
hannesrudolph Dec 17, 2025
48a8004
refactor: remove legacy model name conversion functions and related t…
hannesrudolph Dec 17, 2025
53733b8
Update src/api/providers/claude-code.ts
hannesrudolph Dec 17, 2025
1e5ae81
fix: use Object.hasOwn for safer property checks and update docstring
hannesrudolph Dec 17, 2025
095daee
docs: clarify filterNonAnthropicBlocks performs filtering only, no co…
hannesrudolph Dec 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,10 +200,7 @@ const anthropicSchema = apiModelIdProviderModelSchema.extend({
anthropicBeta1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window.
})

const claudeCodeSchema = apiModelIdProviderModelSchema.extend({
claudeCodePath: z.string().optional(),
claudeCodeMaxOutputTokens: z.number().int().min(1).max(200000).optional(),
})
const claudeCodeSchema = apiModelIdProviderModelSchema.extend({})

const openRouterSchema = baseProviderSettingsSchema.extend({
openRouterApiKey: z.string().optional(),
Expand Down
60 changes: 33 additions & 27 deletions packages/types/src/providers/__tests__/claude-code.spec.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,46 @@
import { convertModelNameForVertex, getClaudeCodeModelId } from "../claude-code.js"
import { normalizeClaudeCodeModelId } from "../claude-code.js"

describe("convertModelNameForVertex", () => {
test("should convert hyphen-date format to @date format", () => {
expect(convertModelNameForVertex("claude-sonnet-4-20250514")).toBe("claude-sonnet-4@20250514")
expect(convertModelNameForVertex("claude-opus-4-20250514")).toBe("claude-opus-4@20250514")
expect(convertModelNameForVertex("claude-3-7-sonnet-20250219")).toBe("claude-3-7-sonnet@20250219")
expect(convertModelNameForVertex("claude-3-5-sonnet-20241022")).toBe("claude-3-5-sonnet@20241022")
expect(convertModelNameForVertex("claude-3-5-haiku-20241022")).toBe("claude-3-5-haiku@20241022")
describe("normalizeClaudeCodeModelId", () => {
test("should return valid model IDs unchanged", () => {
expect(normalizeClaudeCodeModelId("claude-sonnet-4-5")).toBe("claude-sonnet-4-5")
expect(normalizeClaudeCodeModelId("claude-opus-4-5")).toBe("claude-opus-4-5")
expect(normalizeClaudeCodeModelId("claude-haiku-4-5")).toBe("claude-haiku-4-5")
})

test("should not modify models without date pattern", () => {
expect(convertModelNameForVertex("some-other-model")).toBe("some-other-model")
expect(convertModelNameForVertex("claude-model")).toBe("claude-model")
expect(convertModelNameForVertex("model-with-short-date-123")).toBe("model-with-short-date-123")
test("should normalize sonnet models with date suffix to claude-sonnet-4-5", () => {
// Sonnet 4.5 with date
expect(normalizeClaudeCodeModelId("claude-sonnet-4-5-20250929")).toBe("claude-sonnet-4-5")
// Sonnet 4 (legacy)
expect(normalizeClaudeCodeModelId("claude-sonnet-4-20250514")).toBe("claude-sonnet-4-5")
// Claude 3.7 Sonnet
expect(normalizeClaudeCodeModelId("claude-3-7-sonnet-20250219")).toBe("claude-sonnet-4-5")
// Claude 3.5 Sonnet
expect(normalizeClaudeCodeModelId("claude-3-5-sonnet-20241022")).toBe("claude-sonnet-4-5")
})

test("should only convert 8-digit date patterns at the end", () => {
expect(convertModelNameForVertex("claude-20250514-sonnet")).toBe("claude-20250514-sonnet")
expect(convertModelNameForVertex("model-20250514-with-more")).toBe("model-20250514-with-more")
test("should normalize opus models with date suffix to claude-opus-4-5", () => {
// Opus 4.5 with date
expect(normalizeClaudeCodeModelId("claude-opus-4-5-20251101")).toBe("claude-opus-4-5")
// Opus 4.1 (legacy)
expect(normalizeClaudeCodeModelId("claude-opus-4-1-20250805")).toBe("claude-opus-4-5")
// Opus 4 (legacy)
expect(normalizeClaudeCodeModelId("claude-opus-4-20250514")).toBe("claude-opus-4-5")
})
})

describe("getClaudeCodeModelId", () => {
test("should return original model when useVertex is false", () => {
expect(getClaudeCodeModelId("claude-sonnet-4-20250514", false)).toBe("claude-sonnet-4-20250514")
expect(getClaudeCodeModelId("claude-opus-4-20250514", false)).toBe("claude-opus-4-20250514")
expect(getClaudeCodeModelId("claude-3-7-sonnet-20250219", false)).toBe("claude-3-7-sonnet-20250219")
test("should normalize haiku models with date suffix to claude-haiku-4-5", () => {
// Haiku 4.5 with date
expect(normalizeClaudeCodeModelId("claude-haiku-4-5-20251001")).toBe("claude-haiku-4-5")
// Claude 3.5 Haiku
expect(normalizeClaudeCodeModelId("claude-3-5-haiku-20241022")).toBe("claude-haiku-4-5")
})

test("should return converted model when useVertex is true", () => {
expect(getClaudeCodeModelId("claude-sonnet-4-20250514", true)).toBe("claude-sonnet-4@20250514")
expect(getClaudeCodeModelId("claude-opus-4-20250514", true)).toBe("claude-opus-4@20250514")
expect(getClaudeCodeModelId("claude-3-7-sonnet-20250219", true)).toBe("claude-3-7-sonnet@20250219")
test("should handle case-insensitive model family matching", () => {
expect(normalizeClaudeCodeModelId("Claude-Sonnet-4-5-20250929")).toBe("claude-sonnet-4-5")
expect(normalizeClaudeCodeModelId("CLAUDE-OPUS-4-5-20251101")).toBe("claude-opus-4-5")
})

test("should default to useVertex false when parameter not provided", () => {
expect(getClaudeCodeModelId("claude-sonnet-4-20250514")).toBe("claude-sonnet-4-20250514")
test("should fallback to default for unrecognized models", () => {
expect(normalizeClaudeCodeModelId("unknown-model")).toBe("claude-sonnet-4-5")
expect(normalizeClaudeCodeModelId("gpt-4")).toBe("claude-sonnet-4-5")
})
})
282 changes: 144 additions & 138 deletions packages/types/src/providers/claude-code.ts
Original file line number Diff line number Diff line change
@@ -1,154 +1,160 @@
import type { ModelInfo } from "../model.js"
import { anthropicModels } from "./anthropic.js"

// Regex pattern to match 8-digit date at the end of model names
const VERTEX_DATE_PATTERN = /-(\d{8})$/

/**
* Converts Claude model names from hyphen-date format to Vertex AI's @-date format.
*
* @param modelName - The original model name (e.g., "claude-sonnet-4-20250514")
* @returns The converted model name for Vertex AI (e.g., "claude-sonnet-4@20250514")
*
* @example
* convertModelNameForVertex("claude-sonnet-4-20250514") // returns "claude-sonnet-4@20250514"
* convertModelNameForVertex("claude-model") // returns "claude-model" (no change)
* Rate limit information from Claude Code API
*/
export function convertModelNameForVertex(modelName: string): string {
// Convert hyphen-date format to @date format for Vertex AI
return modelName.replace(VERTEX_DATE_PATTERN, "@$1")
export interface ClaudeCodeRateLimitInfo {
// 5-hour limit info
fiveHour: {
status: string
utilization: number
resetTime: number // Unix timestamp
}
// 7-day (weekly) limit info (Sonnet-specific)
weekly?: {
status: string
utilization: number
resetTime: number // Unix timestamp
}
// 7-day unified limit info
weeklyUnified?: {
status: string
utilization: number
resetTime: number // Unix timestamp
}
// Representative claim type
representativeClaim?: string
// Overage status
overage?: {
status: string
disabledReason?: string
}
// Fallback percentage
fallbackPercentage?: number
// Organization ID
organizationId?: string
// Timestamp when this was fetched
fetchedAt: number
}

// Claude Code
// Regex pattern to strip date suffix from model names
const DATE_SUFFIX_PATTERN = /-\d{8}$/

// Models that work with Claude Code OAuth tokens
// See: https://docs.anthropic.com/en/docs/claude-code
// NOTE: Claude Code is subscription-based with no per-token cost - pricing fields are 0
export const claudeCodeModels = {
"claude-haiku-4-5": {
maxTokens: 32768,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsReasoningEffort: ["disable", "low", "medium", "high"],
reasoningEffort: "medium",
description: "Claude Haiku 4.5 - Fast and efficient with thinking",
},
"claude-sonnet-4-5": {
maxTokens: 32768,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsReasoningEffort: ["disable", "low", "medium", "high"],
reasoningEffort: "medium",
description: "Claude Sonnet 4.5 - Balanced performance with thinking",
},
"claude-opus-4-5": {
maxTokens: 32768,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsReasoningEffort: ["disable", "low", "medium", "high"],
reasoningEffort: "medium",
description: "Claude Opus 4.5 - Most capable with thinking",
},
} as const satisfies Record<string, ModelInfo>

// Claude Code - Only models that work with Claude Code OAuth tokens
export type ClaudeCodeModelId = keyof typeof claudeCodeModels
export const claudeCodeDefaultModelId: ClaudeCodeModelId = "claude-sonnet-4-5"
export const CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS = 16000

/**
* Gets the appropriate model ID based on whether Vertex AI is being used.
* Model family patterns for normalization.
* Maps regex patterns to their canonical Claude Code model IDs.
*
* Order matters - more specific patterns should come first.
*/
const MODEL_FAMILY_PATTERNS: Array<{ pattern: RegExp; target: ClaudeCodeModelId }> = [
// Opus models (any version) → claude-opus-4-5
{ pattern: /opus/i, target: "claude-opus-4-5" },
// Haiku models (any version) → claude-haiku-4-5
{ pattern: /haiku/i, target: "claude-haiku-4-5" },
// Sonnet models (any version) → claude-sonnet-4-5
{ pattern: /sonnet/i, target: "claude-sonnet-4-5" },
]

/**
* Normalizes a Claude model ID to a valid Claude Code model ID.
*
* This function handles backward compatibility for legacy model names
* that may include version numbers or date suffixes. It maps:
* - claude-sonnet-4-5-20250929, claude-sonnet-4-20250514, claude-3-7-sonnet-20250219, claude-3-5-sonnet-20241022 → claude-sonnet-4-5
* - claude-opus-4-5-20251101, claude-opus-4-1-20250805, claude-opus-4-20250514 → claude-opus-4-5
* - claude-haiku-4-5-20251001, claude-3-5-haiku-20241022 → claude-haiku-4-5
*
* @param baseModelId - The base Claude Code model ID
* @param useVertex - Whether to format the model ID for Vertex AI (default: false)
* @returns The model ID, potentially formatted for Vertex AI
* @param modelId - The model ID to normalize (may be a legacy format)
* @returns A valid ClaudeCodeModelId, or the original ID if already valid
*
* @example
* getClaudeCodeModelId("claude-sonnet-4-20250514", true) // returns "claude-sonnet-4@20250514"
* getClaudeCodeModelId("claude-sonnet-4-20250514", false) // returns "claude-sonnet-4-20250514"
* normalizeClaudeCodeModelId("claude-sonnet-4-5") // returns "claude-sonnet-4-5"
* normalizeClaudeCodeModelId("claude-3-5-sonnet-20241022") // returns "claude-sonnet-4-5"
* normalizeClaudeCodeModelId("claude-opus-4-1-20250805") // returns "claude-opus-4-5"
*/
export function getClaudeCodeModelId(baseModelId: ClaudeCodeModelId, useVertex = false): string {
return useVertex ? convertModelNameForVertex(baseModelId) : baseModelId
export function normalizeClaudeCodeModelId(modelId: string): ClaudeCodeModelId {
// If already a valid model ID, return as-is
// Use Object.hasOwn() instead of 'in' operator to avoid matching inherited properties like 'toString'
if (Object.hasOwn(claudeCodeModels, modelId)) {
return modelId as ClaudeCodeModelId
}

// Strip date suffix if present (e.g., -20250514)
const withoutDate = modelId.replace(DATE_SUFFIX_PATTERN, "")

// Check if stripping the date makes it valid
if (Object.hasOwn(claudeCodeModels, withoutDate)) {
return withoutDate as ClaudeCodeModelId
}

// Match by model family
for (const { pattern, target } of MODEL_FAMILY_PATTERNS) {
if (pattern.test(modelId)) {
return target
}
}

// Fallback to default if no match (shouldn't happen with valid Claude models)
return claudeCodeDefaultModelId
}

export const claudeCodeModels = {
"claude-sonnet-4-5": {
...anthropicModels["claude-sonnet-4-5"],
supportsImages: false,
supportsPromptCache: true, // Claude Code does report cache tokens
supportsReasoningEffort: false,
supportsReasoningBudget: false,
requiredReasoningBudget: false,
// Claude Code manages its own tools and temperature via the CLI
supportsNativeTools: false,
supportsTemperature: false,
},
"claude-sonnet-4-5-20250929[1m]": {
...anthropicModels["claude-sonnet-4-5"],
contextWindow: 1_000_000, // 1M token context window (requires [1m] suffix)
supportsImages: false,
supportsPromptCache: true, // Claude Code does report cache tokens
supportsReasoningEffort: false,
supportsReasoningBudget: false,
requiredReasoningBudget: false,
// Claude Code manages its own tools and temperature via the CLI
supportsNativeTools: false,
supportsTemperature: false,
},
"claude-sonnet-4-20250514": {
...anthropicModels["claude-sonnet-4-20250514"],
supportsImages: false,
supportsPromptCache: true, // Claude Code does report cache tokens
supportsReasoningEffort: false,
supportsReasoningBudget: false,
requiredReasoningBudget: false,
// Claude Code manages its own tools and temperature via the CLI
supportsNativeTools: false,
supportsTemperature: false,
},
"claude-opus-4-5-20251101": {
...anthropicModels["claude-opus-4-5-20251101"],
supportsImages: false,
supportsPromptCache: true, // Claude Code does report cache tokens
supportsReasoningEffort: false,
supportsReasoningBudget: false,
requiredReasoningBudget: false,
// Claude Code manages its own tools and temperature via the CLI
supportsNativeTools: false,
supportsTemperature: false,
},
"claude-opus-4-1-20250805": {
...anthropicModels["claude-opus-4-1-20250805"],
supportsImages: false,
supportsPromptCache: true, // Claude Code does report cache tokens
supportsReasoningEffort: false,
supportsReasoningBudget: false,
requiredReasoningBudget: false,
// Claude Code manages its own tools and temperature via the CLI
supportsNativeTools: false,
supportsTemperature: false,
},
"claude-opus-4-20250514": {
...anthropicModels["claude-opus-4-20250514"],
supportsImages: false,
supportsPromptCache: true, // Claude Code does report cache tokens
supportsReasoningEffort: false,
supportsReasoningBudget: false,
requiredReasoningBudget: false,
// Claude Code manages its own tools and temperature via the CLI
supportsNativeTools: false,
supportsTemperature: false,
},
"claude-3-7-sonnet-20250219": {
...anthropicModels["claude-3-7-sonnet-20250219"],
supportsImages: false,
supportsPromptCache: true, // Claude Code does report cache tokens
supportsReasoningEffort: false,
supportsReasoningBudget: false,
requiredReasoningBudget: false,
// Claude Code manages its own tools and temperature via the CLI
supportsNativeTools: false,
supportsTemperature: false,
},
"claude-3-5-sonnet-20241022": {
...anthropicModels["claude-3-5-sonnet-20241022"],
supportsImages: false,
supportsPromptCache: true, // Claude Code does report cache tokens
supportsReasoningEffort: false,
supportsReasoningBudget: false,
requiredReasoningBudget: false,
// Claude Code manages its own tools and temperature via the CLI
supportsNativeTools: false,
supportsTemperature: false,
},
"claude-3-5-haiku-20241022": {
...anthropicModels["claude-3-5-haiku-20241022"],
supportsImages: false,
supportsPromptCache: true, // Claude Code does report cache tokens
supportsReasoningEffort: false,
supportsReasoningBudget: false,
requiredReasoningBudget: false,
// Claude Code manages its own tools and temperature via the CLI
supportsNativeTools: false,
supportsTemperature: false,
},
"claude-haiku-4-5-20251001": {
...anthropicModels["claude-haiku-4-5-20251001"],
supportsImages: false,
supportsPromptCache: true, // Claude Code does report cache tokens
supportsReasoningEffort: false,
supportsReasoningBudget: false,
requiredReasoningBudget: false,
// Claude Code manages its own tools and temperature via the CLI
supportsNativeTools: false,
supportsTemperature: false,
},
} as const satisfies Record<string, ModelInfo>
/**
* Reasoning effort configuration for Claude Code thinking mode.
* Maps reasoning effort level to budget_tokens for the thinking process.
*
* Note: With interleaved thinking (enabled via beta header), budget_tokens
* can exceed max_tokens as the token limit becomes the entire context window.
* The max_tokens is drawn from the model's maxTokens definition.
*
* @see https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#interleaved-thinking
*/
export const claudeCodeReasoningConfig = {
low: { budgetTokens: 16_000 },
medium: { budgetTokens: 32_000 },
high: { budgetTokens: 64_000 },
} as const

export type ClaudeCodeReasoningLevel = keyof typeof claudeCodeReasoningConfig
Loading
Loading