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:
1 parent
ccd0677dc1
commit
49ba54b2ba
5 files changed
+95
-1
No files matched your search
@@ -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 },
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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" },
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user