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
This commit is contained in:
MrBeanDev authored and decolua committed 2026-10-01 10:09:14 +07:00
1 parent ccd0677dc1
commit 49ba54b2ba
5 files changed
+95 -1

No files matched your search

+2
View File
@@ -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-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 },
"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": { 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-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 }, "claude-sonnet-5-agentic": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 },
+2
View File
@@ -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-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-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-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 === // === OpenAI / GPT ===
"gpt-3.5-turbo": { input: 0.50, output: 1.50, cached: 0.25, reasoning: 2.25, cache_creation: 0.50 }, "gpt-3.5-turbo": { input: 0.50, output: 1.50, cached: 0.25, reasoning: 2.25, cache_creation: 0.50 },
+1
View File
@@ -63,6 +63,7 @@ export default {
{ id: "claude-opus-5", name: "Claude Opus 5" }, { id: "claude-opus-5", name: "Claude Opus 5" },
{ id: "claude-fable-5-1", name: "Claude Fable 5.1" }, { id: "claude-fable-5-1", name: "Claude Fable 5.1" },
{ id: "claude-fable-5", name: "Claude Fable 5" }, { 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-sonnet-5", name: "Claude Sonnet 5" },
{ id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" }, { id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" },
], ],
+16 -1
View File
@@ -454,12 +454,27 @@ export function prepareClaudeRequest(body, provider = null, apiKey = null, conne
delete body.output_config; 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 // 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 // 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 // up to it, so max-effort thinking gets full budget; others fall back to the
// conservative 64000 default. // conservative 64000 default.
if (body.max_tokens) { 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; if (body.max_tokens > ceiling) body.max_tokens = ceiling;
// Reconcile against thinking budget. applyThinking (thinkingUnified.js) runs // Reconcile against thinking budget. applyThinking (thinkingUnified.js) runs
+74
View File
@@ -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");
});
});