diff --git a/changelog.d/features/14546-claude-opus-5-5.md b/changelog.d/features/14546-claude-opus-5-5.md new file mode 100644 index 000000000000..51e5e46a1b8c --- /dev/null +++ b/changelog.d/features/14546-claude-opus-5-5.md @@ -0,0 +1 @@ +- **feat(providers):** add Claude Opus 5.5 (`claude-opus-5-5`) to the Anthropic API, Claude OAuth, Bedrock and Vertex catalogs — native 1M context, 128K output, five effort tiers (default `medium`), $4/$20 pricing, Fast Mode, Opus 5 fallback; disabled thinking, manual budgets and forced `tool_choice` are normalized before dispatch since Opus 5.5 rejects them. The `cc/` Opus CLI default stays `claude-opus-5`, because `claude`/`anthropic` connections synced before the launch reject Opus 5.5 until their model list is refreshed (Refs #14612) ([#14546](https://github.com/diegosouzapw/OmniRoute/pull/14546)). diff --git a/open-sse/config/claudeCodeCompatibleIdentity.ts b/open-sse/config/claudeCodeCompatibleIdentity.ts index 29f996278c2e..757583d8cfda 100644 --- a/open-sse/config/claudeCodeCompatibleIdentity.ts +++ b/open-sse/config/claudeCodeCompatibleIdentity.ts @@ -15,13 +15,14 @@ export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = getClaudeCodeUserAgent("sdk-cli export { getClaudeCodeUserAgent }; export const CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION = CLAUDE_CODE_SDK_PACKAGE_VERSION; export const CLAUDE_CODE_COMPATIBLE_STAINLESS_RUNTIME_VERSION = CLAUDE_CODE_RUNTIME_VERSION; -const CONTEXT_1M_NATIVE_MODELS = ["claude-fable-5-1", "claude-opus-5"]; +const CONTEXT_1M_NATIVE_MODELS = ["claude-fable-5-1", "claude-opus-5-5", "claude-opus-5"]; export function modelHasNativeContext1m(model: string | null | undefined): boolean { const normalizedModel = String(model || "") .trim() .toLowerCase() .replace(/^.*?(?=claude-)/, "") + .replace(/(\d)\.(\d)/g, "$1-$2") .replace(/-\d{8}$/, ""); return CONTEXT_1M_NATIVE_MODELS.some( diff --git a/open-sse/config/providers/registry/anthropic/index.ts b/open-sse/config/providers/registry/anthropic/index.ts index 286b918c8f1d..4af138a6eea2 100644 --- a/open-sse/config/providers/registry/anthropic/index.ts +++ b/open-sse/config/providers/registry/anthropic/index.ts @@ -36,6 +36,17 @@ export const anthropicProvider: RegistryEntry = { supportsXHighEffort: true, unsupportedParams: ["temperature", "top_p", "top_k"], }, + { + id: "claude-opus-5-5", + name: "Claude Opus 5.5", + contextLength: 1000000, + maxOutputTokens: 128000, + supportsReasoning: true, + supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"], + supportsXHighEffort: true, + supportsVision: true, + unsupportedParams: ["temperature", "top_p", "top_k"], + }, { id: "claude-opus-5", name: "Claude Opus 5", diff --git a/open-sse/config/providers/registry/bedrock/index.ts b/open-sse/config/providers/registry/bedrock/index.ts index f4dc17d6fdd3..32186f856c08 100644 --- a/open-sse/config/providers/registry/bedrock/index.ts +++ b/open-sse/config/providers/registry/bedrock/index.ts @@ -20,6 +20,18 @@ export const bedrockProvider: RegistryEntry = { contextLength: 1000000, maxOutputTokens: 128000, }, + { + id: "anthropic.claude-opus-5-5", + name: "Claude Opus 5.5 (Bedrock)", + toolCalling: true, + supportsReasoning: true, + supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"], + supportsXHighEffort: true, + supportsVision: true, + contextLength: 1000000, + maxOutputTokens: 128000, + unsupportedParams: ["temperature", "top_p", "top_k"], + }, { id: "anthropic.claude-sonnet-4-6", name: "Claude Sonnet 4.6 (Bedrock)", diff --git a/open-sse/config/providers/registry/claude/index.ts b/open-sse/config/providers/registry/claude/index.ts index 300924236962..e173f2742a67 100644 --- a/open-sse/config/providers/registry/claude/index.ts +++ b/open-sse/config/providers/registry/claude/index.ts @@ -60,6 +60,18 @@ export const claudeProvider: RegistryEntry = { supportsXHighEffort: true, unsupportedParams: ["temperature", "top_p", "top_k"], }, + // Listed after Opus 5 so the cc/ Opus default stays on a model stale live catalogs still accept (#14612). + { + id: "claude-opus-5-5", + name: "Claude Opus 5.5", + contextLength: 1000000, + maxOutputTokens: 128000, + supportsReasoning: true, + supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"], + supportsXHighEffort: true, + supportsVision: true, + unsupportedParams: ["temperature", "top_p", "top_k"], + }, { id: "claude-opus-4-8", name: "Claude Opus 4.8", diff --git a/open-sse/config/providers/registry/vertex/index.ts b/open-sse/config/providers/registry/vertex/index.ts index fac4bc1f55b2..03674462787b 100644 --- a/open-sse/config/providers/registry/vertex/index.ts +++ b/open-sse/config/providers/registry/vertex/index.ts @@ -49,6 +49,7 @@ export const vertexProvider: RegistryEntry = { ...VERTEX_XAI_MODELS, { id: "claude-fable-5-1", name: "Claude Fable 5.1 (Vertex)", targetFormat: "claude" }, { id: "claude-fable-5", name: "Claude Fable 5 (Vertex)", targetFormat: "claude" }, + { id: "claude-opus-5-5", name: "Claude Opus 5.5 (Vertex)", targetFormat: "claude" }, { id: "claude-opus-5", name: "Claude Opus 5 (Vertex)", targetFormat: "claude" }, { id: "claude-sonnet-5", name: "Claude Sonnet 5 (Vertex)", targetFormat: "claude" }, { id: "claude-sonnet-5-5", name: "Claude Sonnet 5.5 (Vertex)", targetFormat: "claude" }, diff --git a/open-sse/config/providers/registry/vertex/partner/index.ts b/open-sse/config/providers/registry/vertex/partner/index.ts index 2db77403a7df..e1bbed583940 100644 --- a/open-sse/config/providers/registry/vertex/partner/index.ts +++ b/open-sse/config/providers/registry/vertex/partner/index.ts @@ -21,6 +21,7 @@ export const vertex_partnerProvider: RegistryEntry = { ...VERTEX_XAI_MODELS, { id: "claude-fable-5-1", name: "Claude Fable 5.1", targetFormat: "claude" }, { id: "claude-fable-5", name: "Claude Fable 5", targetFormat: "claude" }, + { id: "claude-opus-5-5", name: "Claude Opus 5.5", targetFormat: "claude" }, { id: "claude-opus-5", name: "Claude Opus 5", targetFormat: "claude" }, { id: "claude-sonnet-5", name: "Claude Sonnet 5", targetFormat: "claude" }, { id: "claude-sonnet-5-5", name: "Claude Sonnet 5.5", targetFormat: "claude" }, diff --git a/open-sse/services/modelFamilyFallback.ts b/open-sse/services/modelFamilyFallback.ts index 3366f960d10b..47c764bd441b 100644 --- a/open-sse/services/modelFamilyFallback.ts +++ b/open-sse/services/modelFamilyFallback.ts @@ -84,6 +84,7 @@ const FAMILY_FALLBACK_TEMPLATES: Record = { "claude-fable-5": ["claude-opus-4-8", "claude-opus-4-7", "claude-sonnet-5"], // Claude Opus family + "claude-opus-5-5": ["claude-opus-5", "claude-opus-4-8", "claude-sonnet-5"], "claude-opus-5": ["claude-opus-4-8", "claude-opus-4-7", "claude-sonnet-5"], "claude-opus-4-8": ["claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-5"], "claude-opus-4-7": ["claude-opus-4-6", "claude-opus-4-5-20251101", "claude-sonnet-5"], diff --git a/open-sse/services/providerCostData.ts b/open-sse/services/providerCostData.ts index 505f065e71b4..b795c745a9a2 100644 --- a/open-sse/services/providerCostData.ts +++ b/open-sse/services/providerCostData.ts @@ -17,6 +17,7 @@ export const KNOWN_MODEL_PRICING: Record = { "gpt-4o-mini": { inputCostPer1M: 0.15, outputCostPer1M: 0.6, isFree: false }, "claude-fable-5-1": { inputCostPer1M: 10.0, outputCostPer1M: 50.0, isFree: false }, "claude-fable-5": { inputCostPer1M: 15.0, outputCostPer1M: 75.0, isFree: false }, + "claude-opus-5-5": { inputCostPer1M: 4.0, outputCostPer1M: 20.0, isFree: false }, "claude-opus-5": { inputCostPer1M: 5.0, outputCostPer1M: 25.0, isFree: false }, "claude-opus-4-8": { inputCostPer1M: 15.0, outputCostPer1M: 75.0, isFree: false }, "claude-opus-4-7": { inputCostPer1M: 15.0, outputCostPer1M: 75.0, isFree: false }, diff --git a/src/lib/db/settings.ts b/src/lib/db/settings.ts index 448c38e6d7a9..1e1cca9168ee 100644 --- a/src/lib/db/settings.ts +++ b/src/lib/db/settings.ts @@ -210,6 +210,7 @@ export async function getSettings() { enabled: false, supportedModels: [ "claude-fable-5", + "claude-opus-5-5", "claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", diff --git a/src/lib/providers/claudeFastMode.ts b/src/lib/providers/claudeFastMode.ts index a007b2fee4f6..1f93be3ef23d 100644 --- a/src/lib/providers/claudeFastMode.ts +++ b/src/lib/providers/claudeFastMode.ts @@ -8,6 +8,7 @@ type JsonRecord = Record; */ export const CLAUDE_FAST_MODE_DEFAULT_MODELS = [ "claude-fable-5", + "claude-opus-5-5", "claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", diff --git a/src/lib/providers/staticModels.ts b/src/lib/providers/staticModels.ts index 9bbb7d9f3445..4af511990db9 100644 --- a/src/lib/providers/staticModels.ts +++ b/src/lib/providers/staticModels.ts @@ -37,6 +37,7 @@ const STATIC_MODEL_PROVIDERS: Record Array<{ id: string; name: str claude: () => [ { id: "claude-fable-5-1", name: "Claude Fable 5.1" }, { id: "claude-fable-5", name: "Claude Fable 5" }, + { id: "claude-opus-5-5", name: "Claude Opus 5.5" }, { id: "claude-opus-5", name: "Claude Opus 5" }, { id: "claude-opus-4-8", name: "Claude Opus 4.8" }, { id: "claude-opus-4-7", name: "Claude Opus 4.7" }, diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index e7f9f3037578..1d1fee4638ac 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -485,6 +485,22 @@ export const MODEL_SPECS: Record = { aliases: BEDROCK_CLAUDE_ALIASES("claude-fable-5"), }, + // ── Claude Opus 5.5 ───────────────────────────────────────────── + "claude-opus-5-5": { + maxOutputTokens: 128000, + contextWindow: 1000000, + defaultThinkingBudget: 32000, + thinkingBudgetCap: 120000, + supportsThinking: true, + supportsTools: true, + supportsVision: true, + rejectsThinkingDisabled: true, + adaptiveThinkingOnly: true, + rejectsForcedToolChoice: true, + defaultReasoningEffort: "medium", + aliases: BEDROCK_CLAUDE_ALIASES("claude-opus-5-5", "claude-opus-5.5"), + }, + // ── Claude Opus 5 ─────────────────────────────────────────────── "claude-opus-5": { maxOutputTokens: 128000, diff --git a/src/shared/constants/pricing/frontier-labs.ts b/src/shared/constants/pricing/frontier-labs.ts index 103b616e6abf..6319d2d33ff2 100644 --- a/src/shared/constants/pricing/frontier-labs.ts +++ b/src/shared/constants/pricing/frontier-labs.ts @@ -11,6 +11,7 @@ import { GPT_5_6_TERRA_PRICING, CLAUDE_FABLE_5_1_PRICING, CLAUDE_FABLE_5_PRICING, + CLAUDE_OPUS_5_5_PRICING, CLAUDE_OPUS_5_PRICING, CLAUDE_OPUS_4_PRICING, CLAUDE_SONNET_4_PRICING, @@ -218,6 +219,7 @@ export const DEFAULT_PRICING_FRONTIER = { // to cover hyphen-notation IDs (claude-opus-4-6) used by some clients "claude-fable-5-1": CLAUDE_FABLE_5_1_PRICING, "claude-fable-5": CLAUDE_FABLE_5_PRICING, + "claude-opus-5-5": CLAUDE_OPUS_5_5_PRICING, "claude-opus-5": CLAUDE_OPUS_5_PRICING, "claude-sonnet-5": CLAUDE_SONNET_5_PRICING, "claude-sonnet-5-5": CLAUDE_SONNET_5_PRICING, diff --git a/src/shared/constants/pricing/oauth-subscriptions.ts b/src/shared/constants/pricing/oauth-subscriptions.ts index 84df7cf369e9..080fcb268b2c 100644 --- a/src/shared/constants/pricing/oauth-subscriptions.ts +++ b/src/shared/constants/pricing/oauth-subscriptions.ts @@ -5,6 +5,7 @@ import { GPT_6_ASTRA_PRICING, CLAUDE_FABLE_5_1_PRICING, + CLAUDE_OPUS_5_5_PRICING, CLAUDE_OPUS_5_PRICING, GEMINI_3_7_FLASH_PROMO_PRICING, GPT_5_3_CODEX_PRICING, @@ -49,6 +50,7 @@ export const DEFAULT_PRICING_OAUTH = { reasoning: 50.0, cache_creation: 12.5, }, + "claude-opus-5-5": CLAUDE_OPUS_5_5_PRICING, "claude-opus-5": CLAUDE_OPUS_5_PRICING, "claude-opus-4-8": { input: 5.0, diff --git a/src/shared/constants/pricing/shared-tiers.ts b/src/shared/constants/pricing/shared-tiers.ts index 7f3e0f50404a..20053b3fbaba 100644 --- a/src/shared/constants/pricing/shared-tiers.ts +++ b/src/shared/constants/pricing/shared-tiers.ts @@ -78,6 +78,14 @@ export const CLAUDE_FABLE_5_1_PRICING = { cache_creation: 12.5, }; +export const CLAUDE_OPUS_5_5_PRICING = { + input: 4.0, + output: 20.0, + cached: 0.2, + reasoning: 20.0, + cache_creation: 5.0, +}; + export const CLAUDE_OPUS_5_PRICING = { input: 5.0, output: 25.0, diff --git a/tests/unit/claude-codex-identity-version-sync.test.ts b/tests/unit/claude-codex-identity-version-sync.test.ts index 7adb17d09202..f06cacae6daf 100644 --- a/tests/unit/claude-codex-identity-version-sync.test.ts +++ b/tests/unit/claude-codex-identity-version-sync.test.ts @@ -40,6 +40,15 @@ test("Claude CLI version constants are in lockstep across all 4 sources", () => ); }); +test("Claude CLI pin clears Anthropic's Opus 5.5 model gate (>= 2.1.280)", () => { + const [major, minor, patch] = canonical.CLAUDE_CODE_CLIENT_VERSION.split(".").map(Number); + const meetsGate = major > 2 || (major === 2 && (minor > 1 || (minor === 1 && patch >= 280))); + assert.ok( + meetsGate, + `claude-cli pin ${canonical.CLAUDE_CODE_CLIENT_VERSION} < 2.1.280 — Anthropic rejects claude-opus-5-5 with 400 "Claude Code ${canonical.CLAUDE_CODE_CLIENT_VERSION} does not support this model"` + ); +}); + test("Claude CLI wire versions match the captured 2.1.280 binary", () => { assert.equal(canonical.CLAUDE_CODE_CLIENT_VERSION, "2.1.280"); assert.equal(canonical.CLAUDE_CODE_CLIENT_BUILD_REVISION, "1e2"); @@ -94,10 +103,7 @@ test("Codex client version locksteps Dockerfile @openai/codex", () => { assert.equal(codexCfg.DEFAULT_CODEX_CLIENT_VERSION, pinned); assert.equal(codexCfg.getCodexClientVersion(), pinned); assert.equal(codexCfg.getCodexDefaultHeaders().Version, pinned); - assert.equal( - codexCfg.getCodexCliRsHeaders()["User-Agent"], - `codex_cli_rs/${pinned}`, - ); + assert.equal(codexCfg.getCodexCliRsHeaders()["User-Agent"], `codex_cli_rs/${pinned}`); }); test("Codex client version env override still wins", async () => { @@ -110,7 +116,7 @@ test("Codex client version env override still wins", async () => { test("test 7: live-empty GitHub catalog path does not call persist", () => { const src = fs.readFileSync( path.join(process.cwd(), "src/app/api/providers/[id]/models/route.ts"), - "utf8", + "utf8" ); // The githubCatalogModels fallback must use buildResponse, not buildApiDiscoveryResponse. const idx = src.indexOf("Codex live catalog unavailable — using GitHub model catalog"); @@ -137,16 +143,13 @@ test("Codex client version locksteps Dockerfile @openai/codex and env override", assert.equal(codexCfg.DEFAULT_CODEX_CLIENT_VERSION, pinned); assert.equal(codexCfg.getCodexClientVersion(), pinned); assert.equal(codexCfg.getCodexDefaultHeaders().Version, pinned); - assert.equal( - codexCfg.getCodexCliRsHeaders()["User-Agent"], - `codex_cli_rs/${pinned}`, - ); + assert.equal(codexCfg.getCodexCliRsHeaders()["User-Agent"], `codex_cli_rs/${pinned}`); }); test("test 7: live-empty GitHub catalog path does not call persist", () => { const src = fs.readFileSync( path.join(process.cwd(), "src/app/api/providers/[id]/models/route.ts"), - "utf8", + "utf8" ); // The githubCatalogModels fallback must use buildResponse, not buildApiDiscoveryResponse. const idx = src.indexOf("Codex live catalog unavailable — using GitHub model catalog"); diff --git a/tests/unit/claude-opus-5-5.test.ts b/tests/unit/claude-opus-5-5.test.ts new file mode 100644 index 000000000000..44c0e1326e66 --- /dev/null +++ b/tests/unit/claude-opus-5-5.test.ts @@ -0,0 +1,204 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + getModelTargetFormat, + getModelsByProviderId, + supportsClaudeMaxEffort, + supportsXHighEffort, +} from "../../open-sse/config/providerModels.ts"; +import { + getClaudeCodeDefaultModels, + getUnsupportedParams, +} from "../../open-sse/config/providerRegistry.ts"; +import { modelHasNativeContext1m } from "../../open-sse/config/claudeCodeCompatibleIdentity.ts"; +import { modelSupportsContext1mBeta } from "../../open-sse/config/context1m.ts"; +import { normalizeClaudeAdaptiveThinking } from "../../open-sse/services/claudeAdaptiveThinking.ts"; +import { getNextFamilyFallback } from "../../open-sse/services/modelFamilyFallback.ts"; +import { getModelPricing } from "../../open-sse/services/providerCostData.ts"; +import { shouldRequestClaudeFastMode } from "../../src/lib/providers/claudeFastMode.ts"; +import { getStaticModelsForProvider } from "../../src/lib/providers/staticModels.ts"; +import { getDefaultPricing } from "../../src/shared/constants/pricing.ts"; +import { + getModelSpec, + normalizeForcedToolChoiceForModel, + normalizeThinkingForModel, +} from "../../src/shared/constants/modelSpecs.ts"; + +const MODEL_ID = "claude-opus-5-5"; +const BEDROCK_MODEL_ID = "anthropic.claude-opus-5-5"; +const DOT_MODEL_ID = "claude-opus-5.5"; +const EFFORTS = ["low", "medium", "high", "xhigh", "max"]; + +test("Claude Opus 5.5 is registered only on verified launch surfaces", () => { + for (const [providerId, modelId] of [ + ["anthropic", MODEL_ID], + ["claude", MODEL_ID], + ["bedrock", BEDROCK_MODEL_ID], + ["vertex", MODEL_ID], + ["vertex-partner", MODEL_ID], + ] as const) { + const model = getModelsByProviderId(providerId).find((entry) => entry.id === modelId); + assert.ok(model, `${providerId} must expose ${modelId}`); + if (providerId === "vertex" || providerId === "vertex-partner") { + assert.equal(model.targetFormat, "claude", `${providerId} wire format`); + } else { + assert.equal(model.contextLength, 1_000_000, `${providerId} context window`); + assert.equal(model.maxOutputTokens, 128_000, `${providerId} max output`); + assert.deepEqual(model.supportedThinkingEfforts, EFFORTS, `${providerId} effort levels`); + } + } + + assert.equal(getModelTargetFormat("vertex", MODEL_ID), "claude"); + assert.equal(getModelTargetFormat("vertex-partner", MODEL_ID), "claude"); + + for (const providerId of ["claude-web", "github", "ghe-copilot", "kiro", "cursor"]) { + const ids = new Set(getModelsByProviderId(providerId).map((entry) => entry.id)); + assert.equal(ids.has(MODEL_ID), false, `${providerId} availability is not verified`); + } + + assert.ok( + getStaticModelsForProvider("claude")?.some((entry) => entry.id === MODEL_ID), + "Claude OAuth static discovery must expose Opus 5.5" + ); + assert.equal( + getNextFamilyFallback(`claude/${MODEL_ID}`, new Set([`claude/${MODEL_ID}`])), + "claude/claude-opus-5" + ); +}); + +test("Claude Code CLI keeps Opus 5 as the cc/ Opus default until live catalogs refresh", () => { + // Given a claude connection synced before the launch, whose live catalog lacks Opus 5.5 (#14612) + // When the Claude Code CLI defaults are derived from the registry + const { opus } = getClaudeCodeDefaultModels(); + + // Then ANTHROPIC_DEFAULT_OPUS_MODEL keeps pointing at a model that catalog still accepts + assert.equal(opus, "claude-opus-5"); +}); + +test("Claude Opus 5.5 has native 1M context and always-on adaptive thinking", () => { + assert.equal(modelHasNativeContext1m(MODEL_ID), true); + assert.equal(modelHasNativeContext1m(BEDROCK_MODEL_ID), true); + assert.equal(modelHasNativeContext1m(DOT_MODEL_ID), true); + assert.equal(modelSupportsContext1mBeta(MODEL_ID), false); + + const spec = getModelSpec(MODEL_ID); + assert.notEqual(spec, getModelSpec("claude-opus-5"), "must not inherit the Opus 5 spec"); + assert.equal(spec?.contextWindow, 1_000_000); + assert.equal(spec?.maxOutputTokens, 128_000); + assert.equal(spec?.supportsThinking, true); + assert.equal(spec?.supportsTools, true); + assert.equal(spec?.supportsVision, true); + assert.equal(spec?.adaptiveThinkingOnly, true); + assert.equal(spec?.rejectsThinkingDisabled, true); + assert.equal(spec?.rejectsForcedToolChoice, true); + assert.equal(spec?.maxEffortWhenThinkingDisabled, undefined); + + assert.equal(getModelSpec(`global.${BEDROCK_MODEL_ID}`), spec); + assert.equal(getModelSpec(DOT_MODEL_ID), spec, "dot notation must not prefix-match Opus 5"); + assert.equal(supportsXHighEffort("claude", MODEL_ID), true); + assert.equal(supportsClaudeMaxEffort(MODEL_ID), true); +}); + +test("Claude Opus 5.5 defaults reasoning effort to medium without overriding the client", async () => { + const { applyDefaultReasoningEffort } = + await import("../../open-sse/services/defaultReasoningEffort.ts"); + assert.equal(getModelSpec(MODEL_ID)?.defaultReasoningEffort, "medium"); + + const bare: Record = { model: MODEL_ID, messages: [] }; + assert.equal(applyDefaultReasoningEffort(bare, MODEL_ID).reasoning_effort, "medium"); + + const chosen: Record = { + model: MODEL_ID, + messages: [], + reasoning_effort: "max", + }; + assert.equal(applyDefaultReasoningEffort(chosen, MODEL_ID).reasoning_effort, "max"); +}); + +test("Claude Opus 5.5 strips unsupported sampling parameters", () => { + for (const [providerId, modelId] of [ + ["anthropic", MODEL_ID], + ["claude", MODEL_ID], + ["bedrock", BEDROCK_MODEL_ID], + ] as const) { + const unsupported = getUnsupportedParams(providerId, modelId); + for (const param of ["temperature", "top_p", "top_k"]) { + assert.ok(unsupported.includes(param), `${providerId}/${modelId} must strip ${param}`); + } + } +}); + +test("Claude Opus 5.5 drops disabled thinking and collapses manual budgets to adaptive", () => { + const withoutDisabled = normalizeThinkingForModel( + { model: MODEL_ID, thinking: { type: "disabled" }, output_config: { effort: "low" } }, + MODEL_ID + ); + assert.equal("thinking" in withoutDisabled, false); + assert.deepEqual(withoutDisabled.output_config, { effort: "low" }); + + const dotWithoutDisabled = normalizeThinkingForModel( + { model: DOT_MODEL_ID, thinking: { type: "disabled" } }, + DOT_MODEL_ID + ); + assert.equal("thinking" in dotWithoutDisabled, false); + + const adaptive = normalizeClaudeAdaptiveThinking( + { model: MODEL_ID, thinking: { type: "enabled", budget_tokens: 64_000 } }, + MODEL_ID + ); + assert.deepEqual(adaptive.thinking, { type: "adaptive" }); +}); + +test("Claude Opus 5.5 relaxes forced tool choices without removing tools", () => { + for (const toolChoice of [ + "required", + { type: "any" }, + { type: "tool", name: "read_file" }, + { type: "function", function: { name: "read_file" } }, + ]) { + const tools = [{ name: "read_file", input_schema: { type: "object" } }]; + const result = normalizeForcedToolChoiceForModel( + { model: MODEL_ID, tools, tool_choice: toolChoice }, + MODEL_ID + ); + assert.equal("tool_choice" in result, false); + assert.equal(result.tools, tools); + } + + const opus5 = { model: "claude-opus-5", tool_choice: { type: "tool", name: "read_file" } }; + assert.equal(normalizeForcedToolChoiceForModel(opus5, "claude-opus-5"), opus5); +}); + +test("Claude Opus 5.5 requests Fast Mode when the toggle is on", () => { + assert.equal(shouldRequestClaudeFastMode({ claudeFastMode: { enabled: true } }, MODEL_ID), true); + assert.equal( + shouldRequestClaudeFastMode({ claudeFastMode: { enabled: false } }, MODEL_ID), + false + ); + assert.equal( + shouldRequestClaudeFastMode( + { claudeFastMode: { enabled: true, supportedModels: [MODEL_ID] } }, + "claude-opus-5" + ), + false + ); +}); + +test("Claude Opus 5.5 pricing matches Anthropic's published rates", () => { + for (const providerId of ["anthropic", "cc"] as const) { + const price = getDefaultPricing()[providerId][MODEL_ID]; + assert.equal(price.input, 4); + assert.equal(price.output, 20); + assert.equal(price.cached, 0.2); + assert.equal(price.reasoning, 20); + assert.equal(price.cache_creation, 5); + } + + assert.deepEqual(getModelPricing("anthropic", MODEL_ID), { + inputCostPer1M: 4, + outputCostPer1M: 20, + isFree: false, + isEstimated: false, + }); +}); diff --git a/tests/unit/pricing-fallback.test.ts b/tests/unit/pricing-fallback.test.ts index 80179c2173f9..e0f1e0ad1097 100644 --- a/tests/unit/pricing-fallback.test.ts +++ b/tests/unit/pricing-fallback.test.ts @@ -35,7 +35,7 @@ test("empty model id falls back to the estimated guess without throwing", () => }); test("table hits are untouched", () => { - assert.equal(Object.keys(KNOWN_MODEL_PRICING).length, 21); + assert.equal(Object.keys(KNOWN_MODEL_PRICING).length, 23); assert.deepEqual(getModelPricing("openai", "gpt-4o"), { ...KNOWN_MODEL_PRICING["gpt-4o"], isEstimated: false,