From 53248a05961c2c0faa5518bdb54eaa95a94c5295 Mon Sep 17 00:00:00 2001 From: Giorgos Giakoumettis Date: Tue, 22 Sep 2026 21:36:16 +0300 Subject: [PATCH 1/8] feat(providers): add Claude Opus 5.5 support Register claude-opus-5-5 on the Anthropic API, Claude OAuth, Bedrock (anthropic.claude-opus-5-5) and Vertex catalogs with native 1M context, 128K output and the five effort tiers. The model spec marks it adaptive-only, rejecting disabled thinking and forced tool_choice, so the existing normalizers strip thinking.type:"disabled", collapse manual budgets and relax any/tool choices before dispatch. Adds $4/$20 pricing (cache read $0.20, write $5), Opus 5 family fallback, Fast Mode defaults, static discovery, and native-1M recognition so the context-1m beta is never forwarded for it. --- config/quality/eslint-suppressions.json | 5 - .../config/claudeCodeCompatibleIdentity.ts | 2 +- .../providers/registry/anthropic/index.ts | 11 ++ .../providers/registry/bedrock/index.ts | 11 ++ .../config/providers/registry/claude/index.ts | 11 ++ .../config/providers/registry/vertex/index.ts | 1 + .../registry/vertex/partner/index.ts | 1 + open-sse/services/modelFamilyFallback.ts | 1 + open-sse/services/providerCostData.ts | 1 + src/lib/db/settings.ts | 1 + src/lib/providers/claudeFastMode.ts | 1 + src/lib/providers/staticModels.ts | 1 + src/shared/constants/modelSpecs.ts | 15 ++ src/shared/constants/pricing/frontier-labs.ts | 2 + .../constants/pricing/oauth-subscriptions.ts | 2 + src/shared/constants/pricing/shared-tiers.ts | 8 + tests/unit/claude-opus-5-5.test.ts | 155 ++++++++++++++++++ tests/unit/pricing-fallback.test.ts | 2 +- 18 files changed, 224 insertions(+), 7 deletions(-) create mode 100644 tests/unit/claude-opus-5-5.test.ts diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json index 83a2feda4850..0bd67fd7dcef 100644 --- a/config/quality/eslint-suppressions.json +++ b/config/quality/eslint-suppressions.json @@ -24,11 +24,6 @@ "count": 6 } }, - "open-sse/config/providers/registry/vertex/index.ts": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, "open-sse/config/providers/shared.ts": { "@typescript-eslint/no-unused-vars": { "count": 1 diff --git a/open-sse/config/claudeCodeCompatibleIdentity.ts b/open-sse/config/claudeCodeCompatibleIdentity.ts index 29f996278c2e..2b1bec402272 100644 --- a/open-sse/config/claudeCodeCompatibleIdentity.ts +++ b/open-sse/config/claudeCodeCompatibleIdentity.ts @@ -15,7 +15,7 @@ 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 || "") diff --git a/open-sse/config/providers/registry/anthropic/index.ts b/open-sse/config/providers/registry/anthropic/index.ts index 2845296b15fa..e463aeb9e472 100644 --- a/open-sse/config/providers/registry/anthropic/index.ts +++ b/open-sse/config/providers/registry/anthropic/index.ts @@ -33,6 +33,17 @@ export const anthropicProvider: RegistryEntry = { contextLength: 1048576, 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 ce70091a2776..c9dae4d55b5e 100644 --- a/open-sse/config/providers/registry/bedrock/index.ts +++ b/open-sse/config/providers/registry/bedrock/index.ts @@ -20,6 +20,17 @@ 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, + }, { 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 83888123003b..8ff530bc5b75 100644 --- a/open-sse/config/providers/registry/claude/index.ts +++ b/open-sse/config/providers/registry/claude/index.ts @@ -47,6 +47,17 @@ export const claudeProvider: RegistryEntry = { // is fixed; reasoning is steered by output_config.effort). Strip them before dispatch. 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/vertex/index.ts b/open-sse/config/providers/registry/vertex/index.ts index 1a8975e2e307..d896b94f8ebd 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-opus-4-8", name: "Claude Opus 4.8 (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 0ce2be94ac6c..e33ebaaf4ddf 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-opus-4-8", name: "Claude Opus 4.8", targetFormat: "claude" }, diff --git a/open-sse/services/modelFamilyFallback.ts b/open-sse/services/modelFamilyFallback.ts index 6226c1217b04..d362485146ef 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 675514d816eb..24e54bdd41f8 100644 --- a/open-sse/services/providerCostData.ts +++ b/open-sse/services/providerCostData.ts @@ -14,6 +14,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 5bb6598db5df..5d1e0fb83e2b 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 3f84b77ef469..0b776a44673f 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -468,6 +468,21 @@ 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, + aliases: BEDROCK_CLAUDE_ALIASES("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 0d6e0ce8878d..60608c515089 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-opus-4.8": CLAUDE_OPUS_4_PRICING, diff --git a/src/shared/constants/pricing/oauth-subscriptions.ts b/src/shared/constants/pricing/oauth-subscriptions.ts index a6e68896ba3d..2a36c66eb80d 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, @@ -34,6 +35,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 f6c0382ee4cc..f8f2f2aeaca0 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-opus-5-5.test.ts b/tests/unit/claude-opus-5-5.test.ts new file mode 100644 index 000000000000..d5ff4ffc0482 --- /dev/null +++ b/tests/unit/claude-opus-5-5.test.ts @@ -0,0 +1,155 @@ +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 { CLAUDE_FAST_MODE_DEFAULT_MODELS } 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 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(getClaudeCodeDefaultModels().opus, MODEL_ID); + assert.equal( + getNextFamilyFallback(`claude/${MODEL_ID}`, new Set([`claude/${MODEL_ID}`])), + "claude/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(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(supportsXHighEffort("claude", MODEL_ID), true); + assert.equal(supportsClaudeMaxEffort(MODEL_ID), true); +}); + +test("Claude Opus 5.5 strips unsupported sampling parameters", () => { + for (const providerId of ["anthropic", "claude"] as const) { + const unsupported = getUnsupportedParams(providerId, MODEL_ID); + for (const param of ["temperature", "top_p", "top_k"]) { + assert.ok(unsupported.includes(param), `${providerId}/${MODEL_ID} 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 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 is a Fast Mode default model", () => { + assert.ok((CLAUDE_FAST_MODE_DEFAULT_MODELS as readonly string[]).includes(MODEL_ID)); +}); + +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, + }); +}); diff --git a/tests/unit/pricing-fallback.test.ts b/tests/unit/pricing-fallback.test.ts index 13e2f4fffca5..dce4790c7663 100644 --- a/tests/unit/pricing-fallback.test.ts +++ b/tests/unit/pricing-fallback.test.ts @@ -27,7 +27,7 @@ test("off-table paid model keeps the conservative fallback", () => { }); test("table hits are untouched", () => { - assert.equal(Object.keys(KNOWN_MODEL_PRICING).length, 21); + assert.equal(Object.keys(KNOWN_MODEL_PRICING).length, 22); assert.deepEqual(getModelPricing("openai", "gpt-4o"), KNOWN_MODEL_PRICING["gpt-4o"]); assert.deepEqual(getModelPricing("longcat", "LongCat-2.0"), KNOWN_MODEL_PRICING["longcat-2.0"]); }); From 5f0ef1c30fb637967ed751a6ce1085307e43eca1 Mon Sep 17 00:00:00 2001 From: Giorgos Giakoumettis Date: Tue, 22 Sep 2026 21:37:50 +0300 Subject: [PATCH 2/8] docs(changelog): add Claude Opus 5.5 fragment --- changelog.d/features/14546-claude-opus-5-5.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/features/14546-claude-opus-5-5.md 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..f182293bfd0b --- /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, $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 ([#14546](https://github.com/diegosouzapw/OmniRoute/pull/14546)). From f76f1059e068cf961e1bb230ac02741883916803 Mon Sep 17 00:00:00 2001 From: Giorgos Giakoumettis Date: Tue, 22 Sep 2026 21:53:03 +0300 Subject: [PATCH 3/8] fix(providers): strip sampling params on Bedrock Opus 5.5 and resolve claude-opus-5.5 Bedrock anthropic.claude-opus-5-5 now declares temperature/top_p/top_k as unsupported, so the Converse translator no longer forwards them into a 400. The Opus 5.5 spec gains a claude-opus-5.5 alias so dot-notation ids stop prefix-matching the Opus 5 spec (which skipped the disabled-thinking and forced tool_choice normalizers), and modelHasNativeContext1m folds dotted versions before matching. The fast-mode test now asserts behavior instead of list membership. --- .../config/claudeCodeCompatibleIdentity.ts | 1 + .../providers/registry/bedrock/index.ts | 1 + src/shared/constants/modelSpecs.ts | 2 +- tests/unit/claude-opus-5-5.test.ts | 36 +++++++++++++++---- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/open-sse/config/claudeCodeCompatibleIdentity.ts b/open-sse/config/claudeCodeCompatibleIdentity.ts index 2b1bec402272..757583d8cfda 100644 --- a/open-sse/config/claudeCodeCompatibleIdentity.ts +++ b/open-sse/config/claudeCodeCompatibleIdentity.ts @@ -22,6 +22,7 @@ export function modelHasNativeContext1m(model: string | null | undefined): boole .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/bedrock/index.ts b/open-sse/config/providers/registry/bedrock/index.ts index c9dae4d55b5e..6657bd994912 100644 --- a/open-sse/config/providers/registry/bedrock/index.ts +++ b/open-sse/config/providers/registry/bedrock/index.ts @@ -30,6 +30,7 @@ export const bedrockProvider: RegistryEntry = { supportsVision: true, contextLength: 1000000, maxOutputTokens: 128000, + unsupportedParams: ["temperature", "top_p", "top_k"], }, { id: "anthropic.claude-sonnet-4-6", diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index 0b776a44673f..eb6175550d66 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -480,7 +480,7 @@ export const MODEL_SPECS: Record = { rejectsThinkingDisabled: true, adaptiveThinkingOnly: true, rejectsForcedToolChoice: true, - aliases: BEDROCK_CLAUDE_ALIASES("claude-opus-5-5"), + aliases: BEDROCK_CLAUDE_ALIASES("claude-opus-5-5", "claude-opus-5.5"), }, // ── Claude Opus 5 ─────────────────────────────────────────────── diff --git a/tests/unit/claude-opus-5-5.test.ts b/tests/unit/claude-opus-5-5.test.ts index d5ff4ffc0482..cd94195cfc5d 100644 --- a/tests/unit/claude-opus-5-5.test.ts +++ b/tests/unit/claude-opus-5-5.test.ts @@ -16,7 +16,7 @@ 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 { CLAUDE_FAST_MODE_DEFAULT_MODELS } from "../../src/lib/providers/claudeFastMode.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 { @@ -27,6 +27,7 @@ import { 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", () => { @@ -70,6 +71,7 @@ test("Claude Opus 5.5 is registered only on verified launch surfaces", () => { 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); @@ -85,15 +87,20 @@ test("Claude Opus 5.5 has native 1M context and always-on adaptive thinking", () 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 strips unsupported sampling parameters", () => { - for (const providerId of ["anthropic", "claude"] as const) { - const unsupported = getUnsupportedParams(providerId, MODEL_ID); + 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}/${MODEL_ID} must strip ${param}`); + assert.ok(unsupported.includes(param), `${providerId}/${modelId} must strip ${param}`); } } }); @@ -106,6 +113,12 @@ test("Claude Opus 5.5 drops disabled thinking and collapses manual budgets to ad 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 @@ -133,8 +146,19 @@ test("Claude Opus 5.5 relaxes forced tool choices without removing tools", () => assert.equal(normalizeForcedToolChoiceForModel(opus5, "claude-opus-5"), opus5); }); -test("Claude Opus 5.5 is a Fast Mode default model", () => { - assert.ok((CLAUDE_FAST_MODE_DEFAULT_MODELS as readonly string[]).includes(MODEL_ID)); +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", () => { From 785b5ce261ef95333210c62ca20095b401ddac41 Mon Sep 17 00:00:00 2001 From: Giorgos Giakoumettis Date: Tue, 22 Sep 2026 22:46:31 +0300 Subject: [PATCH 4/8] fix(providers): bump Claude Code wire identity to 2.1.280 for Opus 5.5 Anthropic's OAuth endpoint rejects claude-opus-5-5 with "Claude Code 2.1.258 does not support this model; version 2.1.280 or newer is required". Values were captured from the real 2.1.280 CLI via a loopback listener: the version and billing revision (d7b) change; the Stainless package (0.112.1) and runtime (v26.3.0) are unchanged. The Devin bridge image pin stays at 2.1.258 until it is live-validated. --- .env.example | 2 +- .../maintenance/14546-claude-code-2-1-280.md | 1 + docs/providers/AGENTROUTER.md | 2 +- docs/reference/ENVIRONMENT.md | 4 ++-- docs/security/STEALTH_GUIDE.md | 4 ++-- src/shared/constants/claudeCodeClient.ts | 9 +++++--- tests/snapshots/provider/translate-path.json | 12 +++++----- .../unit/anthropic-cache-fingerprint.test.ts | 2 +- tests/unit/cc-bridge-transforms.test.ts | 2 +- ...claude-codex-identity-version-sync.test.ts | 22 +++++++------------ .../unit/cli-client-version-env-12417.test.ts | 4 ++-- tests/unit/client-identity-profiles.test.ts | 6 ++--- tests/unit/executor-default-base.test.ts | 2 +- tests/unit/glm-executor.test.ts | 2 +- tests/unit/system-transforms.test.ts | 2 +- 15 files changed, 37 insertions(+), 39 deletions(-) create mode 100644 changelog.d/maintenance/14546-claude-code-2-1-280.md diff --git a/.env.example b/.env.example index 5f52deb964ec..39ad41fbe319 100644 --- a/.env.example +++ b/.env.example @@ -1442,7 +1442,7 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98 # Used by: open-sse/executors/base.ts — buildHeaders() dynamic lookup. # Update these when providers release new CLI versions to avoid blocks. -CLAUDE_USER_AGENT="claude-cli/2.1.258 (external, cli)" +CLAUDE_USER_AGENT="claude-cli/2.1.280 (external, cli)" # Disable the deterministic tool-name cloak applied on both Anthropic-bound paths # (executors/base.ts native OAuth + executors/cliproxyapi.ts CLIProxyAPI) — diff --git a/changelog.d/maintenance/14546-claude-code-2-1-280.md b/changelog.d/maintenance/14546-claude-code-2-1-280.md new file mode 100644 index 000000000000..b07e55ce456b --- /dev/null +++ b/changelog.d/maintenance/14546-claude-code-2-1-280.md @@ -0,0 +1 @@ +- **chore(providers):** bump the Claude Code wire identity from `2.1.258` to `2.1.280` (billing revision `d7b`, captured from the real CLI) — Anthropic gates Claude Opus 5.5 behind Claude Code `>= 2.1.280` on the OAuth path ([#14546](https://github.com/diegosouzapw/OmniRoute/pull/14546)) diff --git a/docs/providers/AGENTROUTER.md b/docs/providers/AGENTROUTER.md index ea375f43c936..2561e35b0201 100644 --- a/docs/providers/AGENTROUTER.md +++ b/docs/providers/AGENTROUTER.md @@ -130,7 +130,7 @@ request (see `open-sse/services/claudeCodeCompatible.ts`): | Header | Value | | ------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `Authorization` | `Bearer ` | -| `User-Agent` | `claude-cli/2.1.258 (external, sdk-cli)` | +| `User-Agent` | `claude-cli/2.1.280 (external, sdk-cli)` | | `anthropic-version` | `2023-06-01` | | `anthropic-beta` | `claude-code-20250219,interleaved-thinking-2025-05-14,effort-2025-11-24` | | Per-connection redact-thinking beta toggle | Adds `redact-thinking-2026-02-12` for upstreams that specifically require redacted thinking streams | diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 931fda33c22b..9c73ae478e88 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -656,11 +656,11 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] | Variable | Default Value | When to Update | | -------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `CLAUDE_USER_AGENT` | `claude-cli/2.1.258 (external, cli)` | When Anthropic releases a new CLI version | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.280 (external, cli)` | When Anthropic releases a new CLI version | | `CLAUDE_DISABLE_TOOL_NAME_CLOAK` | `false` | `executors/base.ts` + `executors/cliproxyapi.ts` | Set to `1`/`true` to forward third-party harness tool names verbatim to Anthropic on both Anthropic-bound paths (native OAuth and CLIProxyAPI). By default the executor deterministically aliases non-Claude-Code tool names (Claude Code canonical mapping where one exists, otherwise PascalCase) and reverses them on the response via `_toolNameMap`, so harnesses with snake_case tools are not refused as fingerprinted third-party clients. Debugging only. | | `CODEX_USER_AGENT` | `codex-cli/0.155.0 (Windows 10.0.26200; x64)` | When OpenAI updates the Codex CLI | | `CODEX_CLIENT_VERSION` | `0.155.0` | Override Codex client version independently of full UA string | -| `CLAUDE_CODE_CLIENT_VERSION` | `2.1.258` | Override advertised Claude Code version independently of `CLAUDE_USER_AGENT`. Anthropic gates some models on this value (#12417). | +| `CLAUDE_CODE_CLIENT_VERSION` | `2.1.280` | Override advertised Claude Code version independently of `CLAUDE_USER_AGENT`. Anthropic gates some models on this value (#12417). | | `GITHUB_COPILOT_CLI_VERSION` | `1.0.81-6` | Override advertised Copilot CLI version independently of `GITHUB_USER_AGENT` | | `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.54.0` | When GitHub Copilot Chat updates | | `ANTIGRAVITY_USER_AGENT` | `antigravity/2.0.1 darwin/arm64` | When Antigravity IDE updates | diff --git a/docs/security/STEALTH_GUIDE.md b/docs/security/STEALTH_GUIDE.md index 93aef78aba61..419e6568cb55 100644 --- a/docs/security/STEALTH_GUIDE.md +++ b/docs/security/STEALTH_GUIDE.md @@ -124,7 +124,7 @@ Applied to: `system` blocks, all `messages[].content`, and `tools[].description` For third-party Anthropic relays that only accept "real Claude Code" traffic: -- `CLAUDE_CODE_COMPATIBLE_USER_AGENT = "claude-cli/2.1.258 (external, sdk-cli)"` +- `CLAUDE_CODE_COMPATIBLE_USER_AGENT = "claude-cli/2.1.280 (external, sdk-cli)"` - `CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION = "0.112.1"` - `CLAUDE_CODE_COMPATIBLE_STAINLESS_RUNTIME_VERSION = "v26.3.0"` - `anthropic-beta = "claude-code-20250219,interleaved-thinking-2025-05-14,effort-2025-11-24"` by default @@ -248,7 +248,7 @@ All MITM endpoints require management auth (`requireCliToolsAuth`). The sudo pas | Variable | Default | | ------------------------ | --------------------------------------------------------------- | -| `CLAUDE_USER_AGENT` | `claude-cli/2.1.258 (external, cli)` | +| `CLAUDE_USER_AGENT` | `claude-cli/2.1.280 (external, cli)` | | `CODEX_USER_AGENT` | `codex-cli/0.155.0 (Windows 10.0.26200; x64)` | | `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.54.0` | | `ANTIGRAVITY_USER_AGENT` | `antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0` | diff --git a/src/shared/constants/claudeCodeClient.ts b/src/shared/constants/claudeCodeClient.ts index 50be40b7404c..52b1854e1996 100644 --- a/src/shared/constants/claudeCodeClient.ts +++ b/src/shared/constants/claudeCodeClient.ts @@ -8,8 +8,8 @@ * advertise the version on the wire must go through getClaudeCodeClientVersion() * so operators can bump past Anthropic's model gate without a rebuild (#12417). */ -export const CLAUDE_CODE_CLIENT_VERSION = "2.1.258"; -export const CLAUDE_CODE_CLIENT_BUILD_REVISION = "1e2"; +export const CLAUDE_CODE_CLIENT_VERSION = "2.1.280"; +export const CLAUDE_CODE_CLIENT_BUILD_REVISION = "d7b"; export const CLAUDE_CODE_CLIENT_BILLING_VERSION = `${CLAUDE_CODE_CLIENT_VERSION}.${CLAUDE_CODE_CLIENT_BUILD_REVISION}`; export const CLAUDE_CODE_SDK_PACKAGE_VERSION = "0.112.1"; export const CLAUDE_CODE_RUNTIME_VERSION = "v26.3.0"; @@ -30,7 +30,10 @@ function getSafeEnvValue(name: string, pattern: RegExp): string | null { } export function getClaudeCodeClientVersion(): string { - return getSafeEnvValue(CLAUDE_VERSION_OVERRIDE_ENV, SAFE_HEADER_TOKEN_PATTERN) || CLAUDE_CODE_CLIENT_VERSION; + return ( + getSafeEnvValue(CLAUDE_VERSION_OVERRIDE_ENV, SAFE_HEADER_TOKEN_PATTERN) || + CLAUDE_CODE_CLIENT_VERSION + ); } export function getClaudeCodeClientBillingVersion(): string { diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index 217ee8166f5b..c84a90b65a06 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -28,7 +28,7 @@ "apiKey": { "Accept": "text/event-stream", "Content-Type": "application/json", - "User-Agent": "claude-cli/2.1.258 (external, sdk-cli)", + "User-Agent": "claude-cli/2.1.280 (external, sdk-cli)", "X-Stainless-Arch": "", "X-Stainless-Lang": "js", "X-Stainless-OS": "MacOS", @@ -47,7 +47,7 @@ "nonStream": { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "claude-cli/2.1.258 (external, sdk-cli)", + "User-Agent": "claude-cli/2.1.280 (external, sdk-cli)", "X-Stainless-Arch": "", "X-Stainless-Lang": "js", "X-Stainless-OS": "MacOS", @@ -66,7 +66,7 @@ "oauth": { "Accept": "text/event-stream", "Content-Type": "application/json", - "User-Agent": "claude-cli/2.1.258 (external, sdk-cli)", + "User-Agent": "claude-cli/2.1.280 (external, sdk-cli)", "X-Stainless-Arch": "", "X-Stainless-Lang": "js", "X-Stainless-OS": "MacOS", @@ -1012,7 +1012,7 @@ "Anthropic-Dangerous-Direct-Browser-Access": "true", "Anthropic-Version": "2023-06-01", "Content-Type": "application/json", - "User-Agent": "claude-cli/2.1.258 (external, cli)", + "User-Agent": "claude-cli/2.1.280 (external, cli)", "X-App": "cli", "X-Stainless-Arch": "", "X-Stainless-Helper-Method": "stream", @@ -1030,7 +1030,7 @@ "Anthropic-Dangerous-Direct-Browser-Access": "true", "Anthropic-Version": "2023-06-01", "Content-Type": "application/json", - "User-Agent": "claude-cli/2.1.258 (external, cli)", + "User-Agent": "claude-cli/2.1.280 (external, cli)", "X-App": "cli", "X-Stainless-Arch": "", "X-Stainless-Helper-Method": "stream", @@ -1049,7 +1049,7 @@ "Anthropic-Dangerous-Direct-Browser-Access": "true", "Anthropic-Version": "2023-06-01", "Content-Type": "application/json", - "User-Agent": "claude-cli/2.1.258 (external, cli)", + "User-Agent": "claude-cli/2.1.280 (external, cli)", "X-App": "cli", "X-Stainless-Arch": "", "X-Stainless-Helper-Method": "stream", diff --git a/tests/unit/anthropic-cache-fingerprint.test.ts b/tests/unit/anthropic-cache-fingerprint.test.ts index 59c736a63c50..743710d98dac 100644 --- a/tests/unit/anthropic-cache-fingerprint.test.ts +++ b/tests/unit/anthropic-cache-fingerprint.test.ts @@ -5,6 +5,6 @@ import { CLAUDE_CODE_CLIENT_BILLING_VERSION } from "../../src/shared/constants/c describe("Anthropic billing header fingerprint (#1638)", () => { it("uses the immutable build revision captured from the signed CLI", () => { - assert.equal(CLAUDE_CODE_CLIENT_BILLING_VERSION, "2.1.258.1e2"); + assert.equal(CLAUDE_CODE_CLIENT_BILLING_VERSION, "2.1.280.d7b"); }); }); diff --git a/tests/unit/cc-bridge-transforms.test.ts b/tests/unit/cc-bridge-transforms.test.ts index cbaf19c50af1..2d3136b118d3 100644 --- a/tests/unit/cc-bridge-transforms.test.ts +++ b/tests/unit/cc-bridge-transforms.test.ts @@ -52,7 +52,7 @@ test("DEFAULT_CC_BRIDGE_PIPELINE places billing header at [0] and identity at [1 DEFAULT_CC_BRIDGE_PIPELINE ); const blocks = result.body.system as any[]; - assert.ok(blocks[0].text.startsWith("x-anthropic-billing-header: cc_version=2.1.258.1e2;")); + assert.ok(blocks[0].text.startsWith("x-anthropic-billing-header: cc_version=2.1.280.d7b;")); assert.equal(blocks[1].text, CLAUDE_AGENT_SDK_IDENTITY); }); diff --git a/tests/unit/claude-codex-identity-version-sync.test.ts b/tests/unit/claude-codex-identity-version-sync.test.ts index b59f90a01c37..ed0b13fc2e0f 100644 --- a/tests/unit/claude-codex-identity-version-sync.test.ts +++ b/tests/unit/claude-codex-identity-version-sync.test.ts @@ -40,10 +40,10 @@ test("Claude CLI version constants are in lockstep across all 4 sources", () => ); }); -test("Claude CLI wire versions match the captured 2.1.258 binary", () => { - assert.equal(canonical.CLAUDE_CODE_CLIENT_VERSION, "2.1.258"); - assert.equal(canonical.CLAUDE_CODE_CLIENT_BUILD_REVISION, "1e2"); - assert.equal(canonical.CLAUDE_CODE_CLIENT_BILLING_VERSION, "2.1.258.1e2"); +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, "d7b"); + assert.equal(canonical.CLAUDE_CODE_CLIENT_BILLING_VERSION, "2.1.280.d7b"); assert.equal(canonical.CLAUDE_CODE_SDK_PACKAGE_VERSION, "0.112.1"); assert.equal(canonical.CLAUDE_CODE_RUNTIME_VERSION, "v26.3.0"); assert.equal( @@ -94,10 +94,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 +107,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 +134,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/cli-client-version-env-12417.test.ts b/tests/unit/cli-client-version-env-12417.test.ts index bc28100be934..3a4a424af4fb 100644 --- a/tests/unit/cli-client-version-env-12417.test.ts +++ b/tests/unit/cli-client-version-env-12417.test.ts @@ -40,8 +40,8 @@ async function withEnv( } } -test("#12417 Claude pin stays the captured 2.1.258 binary", () => { - assert.equal(canonical.CLAUDE_CODE_CLIENT_VERSION, "2.1.258"); +test("#12417 Claude pin stays the captured 2.1.280 binary", () => { + assert.equal(canonical.CLAUDE_CODE_CLIENT_VERSION, "2.1.280"); }); test("#12417 getClaudeCodeClientVersion falls back to the captured pin", async () => { diff --git a/tests/unit/client-identity-profiles.test.ts b/tests/unit/client-identity-profiles.test.ts index 698b6d60ea0e..07470cf682b9 100644 --- a/tests/unit/client-identity-profiles.test.ts +++ b/tests/unit/client-identity-profiles.test.ts @@ -40,7 +40,7 @@ test("getClientIdentityProfileHeaders: unknown profile id falls back to no heade test("getClientIdentityProfileHeaders: known CLI profiles expose their preset headers", () => { const claudeCli = getClientIdentityProfileHeaders("claude-cli"); - assert.equal(claudeCli["User-Agent"], "claude-cli/2.1.258 (external, cli)"); + assert.equal(claudeCli["User-Agent"], "claude-cli/2.1.280 (external, cli)"); assert.equal(claudeCli["X-App"], "cli"); const codexCli = getClientIdentityProfileHeaders("codex-cli"); @@ -56,7 +56,7 @@ test("getClientIdentityProfileHeaders: returns a fresh mutable copy (catalog sta headers["User-Agent"] = "tampered"; assert.equal( CLIENT_IDENTITY_PROFILES["claude-cli"].headers["User-Agent"], - "claude-cli/2.1.258 (external, cli)" + "claude-cli/2.1.280 (external, cli)" ); }); @@ -104,7 +104,7 @@ test("profile headers merged into customHeaders survive applyCustomHeaders sanit true ) as Record; - assert.equal(headers["User-Agent"], "claude-cli/2.1.258 (external, cli)"); + assert.equal(headers["User-Agent"], "claude-cli/2.1.280 (external, cli)"); assert.equal(headers["X-App"], "cli"); assert.equal(headers["Authorization"], "Bearer test-key"); }); diff --git a/tests/unit/executor-default-base.test.ts b/tests/unit/executor-default-base.test.ts index 25d5a8bda2b6..b5ae57ef30c3 100644 --- a/tests/unit/executor-default-base.test.ts +++ b/tests/unit/executor-default-base.test.ts @@ -1575,7 +1575,7 @@ test("DefaultExecutor.execute does not produce duplicate anthropic-version heade const sentBody = JSON.parse(capturedBody) as { system?: Array<{ text?: string }> }; assert.match( sentBody.system?.[0]?.text ?? "", - /^x-anthropic-billing-header: cc_version=2\.1\.258\.1e2; cc_entrypoint=cli; cch=[0-9a-f]{5};$/ + /^x-anthropic-billing-header: cc_version=2\.1\.280\.d7b; cc_entrypoint=cli; cch=[0-9a-f]{5};$/ ); }); diff --git a/tests/unit/glm-executor.test.ts b/tests/unit/glm-executor.test.ts index 26ef4f8526ba..a34b698ccef8 100644 --- a/tests/unit/glm-executor.test.ts +++ b/tests/unit/glm-executor.test.ts @@ -183,7 +183,7 @@ test("GlmExecutor separates OpenAI-compatible coding headers from Anthropic head assert.equal(anthropicHeaders["anthropic-version"], "2023-06-01"); assert.match(anthropicHeaders["anthropic-beta"], /claude-code-20250219/); assert.equal(anthropicHeaders["anthropic-dangerous-direct-browser-access"], "true"); - assert.match(anthropicHeaders["User-Agent"], /^claude-cli\/2\.1\.258 \(external, sdk-cli\)$/); + assert.match(anthropicHeaders["User-Agent"], /^claude-cli\/2\.1\.280 \(external, sdk-cli\)$/); assert.equal(anthropicHeaders["X-Stainless-Lang"], "js"); assert.equal(anthropicHeaders["X-Stainless-Runtime"], "node"); }); diff --git a/tests/unit/system-transforms.test.ts b/tests/unit/system-transforms.test.ts index e5683766eb96..c9eba1e0da7e 100644 --- a/tests/unit/system-transforms.test.ts +++ b/tests/unit/system-transforms.test.ts @@ -559,7 +559,7 @@ const UI_DEFAULTS_SNAPSHOT = { entrypoint: "sdk-cli", versionFormat: "ex-machina", cchAlgo: "sha256-first-user", - buildRevision: "1e2", + buildRevision: "d7b", }, ], }, From ded7b4406cb98fa6406ef36e52d66e0d65f198ee Mon Sep 17 00:00:00 2001 From: Giorgos Giakoumettis Date: Wed, 23 Sep 2026 20:25:33 +0300 Subject: [PATCH 5/8] fix(providers): keep claude-opus-5 as the cc/ Opus default Connections whose authoritative live catalog was synced before the launch reject claude-opus-5-5 with model_not_found until refreshed, so listing Opus 5.5 first made ANTHROPIC_DEFAULT_OPUS_MODEL point at a model those catalogs refuse. List it after Opus 5 until catalogs catch up. Refs #14612 --- .../config/providers/registry/claude/index.ts | 15 ++++++++------- tests/unit/claude-opus-5-5.test.ts | 10 +++++++++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/open-sse/config/providers/registry/claude/index.ts b/open-sse/config/providers/registry/claude/index.ts index 8ff530bc5b75..6a10b926eed1 100644 --- a/open-sse/config/providers/registry/claude/index.ts +++ b/open-sse/config/providers/registry/claude/index.ts @@ -48,22 +48,23 @@ export const claudeProvider: RegistryEntry = { unsupportedParams: ["temperature", "top_p", "top_k"], }, { - id: "claude-opus-5-5", - name: "Claude Opus 5.5", + id: "claude-opus-5", + name: "Claude Opus 5", contextLength: 1000000, maxOutputTokens: 128000, - supportsReasoning: true, - supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"], supportsXHighEffort: true, - supportsVision: 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", - name: "Claude Opus 5", + 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"], }, { diff --git a/tests/unit/claude-opus-5-5.test.ts b/tests/unit/claude-opus-5-5.test.ts index cd94195cfc5d..bf10752efb90 100644 --- a/tests/unit/claude-opus-5-5.test.ts +++ b/tests/unit/claude-opus-5-5.test.ts @@ -61,13 +61,21 @@ test("Claude Opus 5.5 is registered only on verified launch surfaces", () => { getStaticModelsForProvider("claude")?.some((entry) => entry.id === MODEL_ID), "Claude OAuth static discovery must expose Opus 5.5" ); - assert.equal(getClaudeCodeDefaultModels().opus, MODEL_ID); 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); From 52d175b6da901046f21042182ee08e0f6afebe97 Mon Sep 17 00:00:00 2001 From: Giorgos Giakoumettis Date: Wed, 23 Sep 2026 20:25:55 +0300 Subject: [PATCH 6/8] test(providers): assert the Claude CLI pin clears the Opus 5.5 gate Ported from #14557: guards against a future pin regressing below the 2.1.280 floor Anthropic enforces for claude-opus-5-5. Co-authored-by: PixmaNTS --- tests/unit/claude-codex-identity-version-sync.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/unit/claude-codex-identity-version-sync.test.ts b/tests/unit/claude-codex-identity-version-sync.test.ts index ed0b13fc2e0f..6332148f751f 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, "d7b"); From 31318a8e3e3a245c7ad75e5f7fe8c4a858d26668 Mon Sep 17 00:00:00 2001 From: Giorgos Giakoumettis Date: Wed, 23 Sep 2026 20:35:10 +0300 Subject: [PATCH 7/8] feat(providers): default Claude Opus 5.5 reasoning effort to medium Ported from #14548: requests without an explicit effort get reasoning_effort=medium; a client-chosen effort still wins. Co-authored-by: Minxi Hou --- src/shared/constants/modelSpecs.ts | 1 + tests/unit/claude-opus-5-5.test.ts | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index eb6175550d66..eabf885fccde 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -480,6 +480,7 @@ export const MODEL_SPECS: Record = { rejectsThinkingDisabled: true, adaptiveThinkingOnly: true, rejectsForcedToolChoice: true, + defaultReasoningEffort: "medium", aliases: BEDROCK_CLAUDE_ALIASES("claude-opus-5-5", "claude-opus-5.5"), }, diff --git a/tests/unit/claude-opus-5-5.test.ts b/tests/unit/claude-opus-5-5.test.ts index bf10752efb90..cee315a8bceb 100644 --- a/tests/unit/claude-opus-5-5.test.ts +++ b/tests/unit/claude-opus-5-5.test.ts @@ -100,6 +100,22 @@ test("Claude Opus 5.5 has native 1M context and always-on adaptive thinking", () 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], From 7f2eecceb3642a3eceff61c506ee8a3c211d747e Mon Sep 17 00:00:00 2001 From: Giorgos Giakoumettis Date: Wed, 23 Sep 2026 20:35:25 +0300 Subject: [PATCH 8/8] docs(changelog): note Opus 5.5 effort default and cc/ Opus default Co-authored-by: Minxi Hou --- changelog.d/features/14546-claude-opus-5-5.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/features/14546-claude-opus-5-5.md b/changelog.d/features/14546-claude-opus-5-5.md index f182293bfd0b..51e5e46a1b8c 100644 --- a/changelog.d/features/14546-claude-opus-5-5.md +++ b/changelog.d/features/14546-claude-opus-5-5.md @@ -1 +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, $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 ([#14546](https://github.com/diegosouzapw/OmniRoute/pull/14546)). +- **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)).