From 49ba54b2baf9190472181272a7efa8edeba66cf4 Mon Sep 17 00:00:00 2001 From: MrBeanDev Date: Thu, 1 Oct 2026 10:07:10 +0700 Subject: [PATCH] feat(claude): add Claude Sonnet 5.5 - registry + exact capability entry (adaptive thinking, 1M context, 128k output) - explicit Sonnet 5 pricing for claude-sonnet-5-5 and claude-sonnet-5 ($2/$10 per 1M) - Sonnet 5.5 API restrictions: thinking "disabled" rewritten to "between_tools" (effort clamped to high), forced tool_choice any/tool rewritten to auto --- open-sse/providers/capabilities.js | 2 + open-sse/providers/pricing.js | 2 + open-sse/providers/registry/claude.js | 1 + open-sse/translator/formats/claude.js | 17 +++++- tests/unit/claude-sonnet-5-5.test.js | 74 +++++++++++++++++++++++++++ 5 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 tests/unit/claude-sonnet-5-5.test.js diff --git a/open-sse/providers/capabilities.js b/open-sse/providers/capabilities.js index 39d116ee..68e4fa0a 100644 --- a/open-sse/providers/capabilities.js +++ b/open-sse/providers/capabilities.js @@ -104,6 +104,8 @@ export const MODEL_CAPABILITIES = { "claude-opus-4-8-thinking": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 }, "claude-sonnet-4.6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 }, "claude-sonnet-4-6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 }, + // Sonnet 5.5 rejects thinking.type "disabled" (use "between_tools") and forced tool_choice (any/tool). + "claude-sonnet-5-5": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000, thinkingOffType: "between_tools", forcedToolChoice: false }, "claude-sonnet-5": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 }, "claude-sonnet-5-thinking": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 }, "claude-sonnet-5-agentic": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 }, diff --git a/open-sse/providers/pricing.js b/open-sse/providers/pricing.js index a57e6bda..4b62fa86 100644 --- a/open-sse/providers/pricing.js +++ b/open-sse/providers/pricing.js @@ -49,6 +49,8 @@ export const MODEL_PRICING = { "claude-opus-4-5-thinking": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 }, "claude-opus-4-6-thinking": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 }, "claude-fable-5": { input: 10.00, output: 50.00, cached: 1.00, reasoning: 50.00, cache_creation: 12.50 }, + "claude-sonnet-5-5": { input: 2.00, output: 10.00, cached: 0.20, reasoning: 10.00, cache_creation: 2.50 }, + "claude-sonnet-5": { input: 2.00, output: 10.00, cached: 0.20, reasoning: 10.00, cache_creation: 2.50 }, // === OpenAI / GPT === "gpt-3.5-turbo": { input: 0.50, output: 1.50, cached: 0.25, reasoning: 2.25, cache_creation: 0.50 }, diff --git a/open-sse/providers/registry/claude.js b/open-sse/providers/registry/claude.js index 3677ffb5..bbf9f1fb 100644 --- a/open-sse/providers/registry/claude.js +++ b/open-sse/providers/registry/claude.js @@ -63,6 +63,7 @@ export default { { id: "claude-opus-5", name: "Claude Opus 5" }, { id: "claude-fable-5-1", name: "Claude Fable 5.1" }, { id: "claude-fable-5", name: "Claude Fable 5" }, + { id: "claude-sonnet-5-5", name: "Claude Sonnet 5.5" }, { id: "claude-sonnet-5", name: "Claude Sonnet 5" }, { id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" }, ], diff --git a/open-sse/translator/formats/claude.js b/open-sse/translator/formats/claude.js index a9bbd19d..8fa44ccf 100644 --- a/open-sse/translator/formats/claude.js +++ b/open-sse/translator/formats/claude.js @@ -454,12 +454,27 @@ export function prepareClaudeRequest(body, provider = null, apiKey = null, conne delete body.output_config; } + // Models whose API rejects thinking "disabled" and forced tool use with a 400 + // (Sonnet 5.5). Runs on every Claude-bound body, so OpenAI clients, native + // passthrough and the provider-level "off" override are all covered. + const modelCaps = getCapabilitiesForModel(provider, body.model); + if (modelCaps.thinkingOffType && body.thinking?.type === "disabled") { + body.thinking = { type: modelCaps.thinkingOffType }; + // between_tools only accepts effort up to high. + const effort = body.output_config?.effort; + if (effort === "xhigh" || effort === "max") body.output_config.effort = "high"; + } + if (modelCaps.forcedToolChoice === false && (body.tool_choice?.type === "any" || body.tool_choice?.type === "tool")) { + const { disable_parallel_tool_use } = body.tool_choice; + body.tool_choice = { type: "auto", ...(disable_parallel_tool_use !== undefined ? { disable_parallel_tool_use } : {}) }; + } + // Clamp max_tokens to the model's real output ceiling. Models whose caps // declare a higher maxOutput (e.g. Opus 4.8 / Sonnet 4.6 = 128000) are allowed // up to it, so max-effort thinking gets full budget; others fall back to the // conservative 64000 default. if (body.max_tokens) { - const ceiling = getCapabilitiesForModel(provider, body.model).maxOutput || DEFAULT_MAX_TOKENS; + const ceiling = modelCaps.maxOutput || DEFAULT_MAX_TOKENS; if (body.max_tokens > ceiling) body.max_tokens = ceiling; // Reconcile against thinking budget. applyThinking (thinkingUnified.js) runs diff --git a/tests/unit/claude-sonnet-5-5.test.js b/tests/unit/claude-sonnet-5-5.test.js new file mode 100644 index 00000000..527759ab --- /dev/null +++ b/tests/unit/claude-sonnet-5-5.test.js @@ -0,0 +1,74 @@ +import { describe, expect, it } from "vitest"; + +import { getModelsByProviderId } from "../../open-sse/config/providerModels.js"; +import { getCapabilitiesForModel } from "../../open-sse/providers/capabilities.js"; +import { getPricingForModel } from "../../open-sse/providers/pricing.js"; +import { prepareClaudeRequest } from "../../open-sse/translator/formats/claude.js"; +import { translateRequest } from "../../open-sse/translator/index.js"; +import { FORMATS } from "../../open-sse/translator/formats.js"; +import "../translator/registerAll.js"; + +// Sonnet 5.5 keeps Sonnet 5's API price ($2 / $10 per 1M) and the 5.x +// adaptive-thinking family. Without explicit rows both fell through to the +// generic claude-sonnet-* pattern: $3 / $15 and budget thinking. +describe("Claude Sonnet 5.5", () => { + it("is listed for the claude provider", () => { + expect(getModelsByProviderId("claude").some((model) => model.id === "claude-sonnet-5-5")).toBe(true); + }); + + it("resolves to adaptive thinking with a 1M context", () => { + expect(getCapabilitiesForModel("claude", "claude-sonnet-5-5")).toMatchObject({ + reasoning: true, + thinkingFormat: "claude-adaptive", + contextWindow: 1000000, + maxOutput: 128000, + }); + }); + + it.each(["claude-sonnet-5-5", "claude-sonnet-5"])("prices %s at Sonnet 5 rates", (model) => { + expect(getPricingForModel("claude", model)).toEqual({ input: 2, output: 10, cached: 0.2, reasoning: 10, cache_creation: 2.5 }); + }); +}); + +// Sonnet 5.5 returns 400 for thinking.type "disabled" and for forced tool use. +describe("Claude Sonnet 5.5 request shape", () => { + const prepare = (body) => prepareClaudeRequest({ max_tokens: 1024, messages: [{ role: "user", content: "hi" }], ...body }, "claude"); + + it("turns thinking off with between_tools, clamping effort to high", () => { + const body = prepare({ model: "claude-sonnet-5-5", thinking: { type: "disabled" }, output_config: { effort: "max" } }); + expect(body.thinking).toEqual({ type: "between_tools" }); + expect(body.output_config.effort).toBe("high"); + }); + + it("maps forced tool_choice to auto", () => { + expect(prepare({ model: "claude-sonnet-5-5", tool_choice: { type: "any" } }).tool_choice).toEqual({ type: "auto" }); + expect(prepare({ model: "claude-sonnet-5-5", tool_choice: { type: "tool", name: "run", disable_parallel_tool_use: true } }).tool_choice) + .toEqual({ type: "auto", disable_parallel_tool_use: true }); + }); + + it("leaves other models untouched", () => { + const body = prepare({ model: "claude-sonnet-5", thinking: { type: "disabled" }, tool_choice: { type: "any" } }); + expect(body.thinking).toEqual({ type: "disabled" }); + expect(body.tool_choice).toEqual({ type: "any" }); + }); + + it("covers the native Claude passthrough path end to end", () => { + const out = translateRequest(FORMATS.CLAUDE, FORMATS.CLAUDE, "claude-sonnet-5-5", { + model: "claude-sonnet-5-5", max_tokens: 1000, thinking: { type: "disabled" }, tool_choice: { type: "any" }, + tools: [{ name: "run", input_schema: { type: "object", properties: {} } }], + messages: [{ role: "user", content: "hi" }], + }, true, null, "claude"); + expect(out.thinking).toEqual({ type: "between_tools" }); + expect(out.tool_choice).toEqual({ type: "auto" }); + }); + + it("covers the OpenAI-client path end to end", () => { + const out = translateRequest(FORMATS.OPENAI, FORMATS.CLAUDE, "claude-sonnet-5-5", { + model: "claude-sonnet-5-5", reasoning_effort: "none", tool_choice: "required", + tools: [{ type: "function", function: { name: "run", parameters: { type: "object", properties: {} } } }], + messages: [{ role: "user", content: "hi" }], + }, true, null, "claude"); + expect(out.thinking).toEqual({ type: "between_tools" }); + expect(out.tool_choice.type).toBe("auto"); + }); +});