From ac9120fde314bafcdef4cab5a68df9a36da7f676 Mon Sep 17 00:00:00 2001 From: LucasOl1337 Date: Wed, 2 Sep 2026 20:26:02 +0700 Subject: [PATCH] fix(claude): support Fable 5.1 - add claude-fable-5-1 to the Claude Code model catalog (1M context, permanent adaptive thinking) - centralize the spoofed Claude Code version and update both request and billing identities to 2.1.257 (Fable 5.1 rejects < 2.1.251) - send output_config.effort without the redundant thinking switch for permanently adaptive models - add regression coverage for capabilities, headers, billing identity and adaptive-effort payload # Conflicts: # open-sse/providers/registry/claude.js # open-sse/providers/shared.js # open-sse/utils/claudeCloaking.js # tests/__baseline__/providers-baseline.json --- CHANGELOG.md | 5 ++++- open-sse/providers/capabilities.js | 3 ++- open-sse/providers/registry/claude.js | 5 +++-- open-sse/providers/shared.js | 3 ++- open-sse/translator/concerns/thinkingUnified.js | 10 ++++------ open-sse/utils/claudeCloaking.js | 8 ++++---- tests/__baseline__/providers-baseline.json | 2 +- tests/translator/thinking-unified.test.js | 5 +++++ tests/unit/capabilities.test.js | 7 +++++++ tests/unit/claude-cloaking.test.js | 7 ++++++- tests/unit/claude-header-forwarding.test.js | 1 + 11 files changed, 39 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d729f06..cd2278e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,9 @@ - **i18n**: pt-BR expanded to 1132 terms ## Fixes +- **Claude Code**: add Claude Fable 5.1 and advertise Claude Code 2.1.258 in + both the request header and billing identity; use its permanent adaptive-thinking + mode with `output_config.effort` - **Stream**: record usage when a client closes on the terminal event — the Responses API has no [DONE] sentinel, so codex closed the socket on `response.completed` and cancelled the reader before flush() ran its usage @@ -738,4 +741,4 @@ # v0.4.46 (2026-05-15) ## Breaking Changes -- Tunnel public URL changed — old tunnel links no longer work, please reconnect to get the new URL \ No newline at end of file +- Tunnel public URL changed — old tunnel links no longer work, please reconnect to get the new URL diff --git a/open-sse/providers/capabilities.js b/open-sse/providers/capabilities.js index dcdac7ea..25e0048c 100644 --- a/open-sse/providers/capabilities.js +++ b/open-sse/providers/capabilities.js @@ -83,7 +83,8 @@ export function capabilitiesFromServiceKind(kind) { * otherwise mis-match. Only declare deltas vs DEFAULT. */ export const MODEL_CAPABILITIES = { - // Claude Opus 5, 4.6/4.7/4.8, and Kiro Sonnet 5 have 1M context + adaptive thinking (override generic claude pattern) + // Claude Fable 5.1, Opus 5, 4.6/4.7/4.8, and Kiro Sonnet 5 have 1M context + adaptive thinking (override generic claude pattern) + "claude-fable-5-1": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 128000 }, "claude-opus-5": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 }, "claude-opus-5-thinking": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 }, "claude-opus-5-agentic": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 }, diff --git a/open-sse/providers/registry/claude.js b/open-sse/providers/registry/claude.js index 65bab07e..428e66ad 100644 --- a/open-sse/providers/registry/claude.js +++ b/open-sse/providers/registry/claude.js @@ -1,4 +1,4 @@ -import { CLAUDE_CLI_SPOOF_HEADERS } from "../shared.js"; +import { CLAUDE_CLI_VERSION } from "../shared.js"; export default { id: "claude", @@ -25,7 +25,7 @@ export default { "Anthropic-Version": "2023-06-01", "Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28", "Anthropic-Dangerous-Direct-Browser-Access": "true", - "User-Agent": "claude-cli/2.1.258 (external, sdk-cli)", + "User-Agent": `claude-cli/${CLAUDE_CLI_VERSION} (external, sdk-cli)`, "X-App": "cli", "X-Stainless-Helper-Method": "stream", "X-Stainless-Retry-Count": "0", @@ -58,6 +58,7 @@ export default { }, models: [ { 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", name: "Claude Sonnet 5" }, { id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" }, diff --git a/open-sse/providers/shared.js b/open-sse/providers/shared.js index 118a7593..7cce572f 100644 --- a/open-sse/providers/shared.js +++ b/open-sse/providers/shared.js @@ -22,6 +22,7 @@ export function mapStainlessArch() { // Anthropic API version (single source — reused across claude-format providers/executors) export const ANTHROPIC_API_VERSION = "2023-06-01"; +export const CLAUDE_CLI_VERSION = "2.1.258"; // Shared Claude-compatible API headers (reused across claude-format providers) export const CLAUDE_API_HEADERS = { @@ -34,7 +35,7 @@ export const CLAUDE_CLI_SPOOF_HEADERS = { "Anthropic-Version": ANTHROPIC_API_VERSION, "Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28", "Anthropic-Dangerous-Direct-Browser-Access": "true", - "User-Agent": "claude-cli/2.1.258 (external, sdk-cli)", + "User-Agent": `claude-cli/${CLAUDE_CLI_VERSION} (external, sdk-cli)`, "X-App": "cli", "X-Stainless-Helper-Method": "stream", "X-Stainless-Retry-Count": "0", diff --git a/open-sse/translator/concerns/thinkingUnified.js b/open-sse/translator/concerns/thinkingUnified.js index 9902c314..5ea27b2e 100644 --- a/open-sse/translator/concerns/thinkingUnified.js +++ b/open-sse/translator/concerns/thinkingUnified.js @@ -237,12 +237,10 @@ function applyFormat(fmt, body, cfg, caps, supportedLevels) { } case "claude-adaptive": { if (none && canDisable) { body.thinking = { type: "disabled" }; break; } - // output_config.effort alone does NOT turn thinking on: Anthropic requires - // an explicit thinking:{type:"adaptive"} on Opus 4.6/4.7/4.8 and Sonnet 4.6 - // ("thinking is off unless you explicitly set it"), and Anthropic-compatible - // shims (e.g. GitHub Copilot /v1/messages) default thinking off even for - // Sonnet 5. Send both fields — the documented adaptive-thinking shape. - body.thinking = { type: "adaptive" }; + // Models that can disable thinking need the explicit adaptive switch. + // Permanently adaptive models such as Fable 5.1 accept effort directly. + if (canDisable) body.thinking = { type: "adaptive" }; + else delete body.thinking; const level = toLevel(eff); body.output_config = { effort: level === "xhigh" ? "high" : level }; break; diff --git a/open-sse/utils/claudeCloaking.js b/open-sse/utils/claudeCloaking.js index 50395d53..7e3790df 100644 --- a/open-sse/utils/claudeCloaking.js +++ b/open-sse/utils/claudeCloaking.js @@ -1,16 +1,16 @@ import { createHash, randomBytes, randomUUID } from "crypto"; import { CLAUDE_TOOL_SUFFIX, CC_DEFAULT_TOOLS } from "../config/appConstants.js"; +import { CLAUDE_CLI_VERSION } from "../providers/shared.js"; -const CLAUDE_VERSION = "2.1.258"; const CC_ENTRYPOINT = "sdk-cli"; -// Generate billing header matching real Claude Code 2.1.258+ format: +// Generate the billing header expected from current Claude Code clients. // x-anthropic-billing-header: cc_version=.; cc_entrypoint=sdk-cli; cch=; function generateBillingHeader(payload) { const content = JSON.stringify(payload); const cch = createHash("sha256").update(content).digest("hex").slice(0, 5); const buildHash = randomBytes(2).toString("hex").slice(0, 3); - return `x-anthropic-billing-header: cc_version=${CLAUDE_VERSION}.${buildHash}; cc_entrypoint=${CC_ENTRYPOINT}; cch=${cch};`; + return `x-anthropic-billing-header: cc_version=${CLAUDE_CLI_VERSION}.${buildHash}; cc_entrypoint=${CC_ENTRYPOINT}; cch=${cch};`; } // Derive a deterministic UUID-v4-shaped string from a seed (stable per account) @@ -19,7 +19,7 @@ function deriveUuid(seed) { return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-${((parseInt(h[16], 16) & 0x3) | 0x8).toString(16)}${h.slice(17, 20)}-${h.slice(20, 32)}`; } -// Generate fake user ID in Claude Code 2.1.258+ JSON format: +// Generate fake user ID in the current Claude Code JSON format: // {"device_id":"<64hex>","account_uuid":"","session_id":""} // device_id/account_uuid derive from apiKey (stable per account), session_id per-conversation function generateFakeUserID(sessionId, apiKey) { diff --git a/tests/__baseline__/providers-baseline.json b/tests/__baseline__/providers-baseline.json index caeaa911..3ab6982c 100644 --- a/tests/__baseline__/providers-baseline.json +++ b/tests/__baseline__/providers-baseline.json @@ -1009,4 +1009,4 @@ }, "format": "openai" } -} \ No newline at end of file +} diff --git a/tests/translator/thinking-unified.test.js b/tests/translator/thinking-unified.test.js index e6214008..9ce75db5 100644 --- a/tests/translator/thinking-unified.test.js +++ b/tests/translator/thinking-unified.test.js @@ -82,6 +82,11 @@ describe("applyThinking per provider format", () => { // Sonnet 5). Both fields together are the documented adaptive shape. expect(out.thinking).toEqual({ type: "adaptive" }); }); + it("Fable 5.1 → effort without a redundant thinking switch", () => { + const out = apply("claude", "claude-fable-5-1", { reasoning_effort: "high" }, "claude"); + expect(out.output_config).toEqual({ effort: "high" }); + expect(out.thinking).toBeUndefined(); + }); it("claude haiku → enabled+budget", () => { const out = apply("claude", "claude-haiku-4.5", { reasoning_effort: "high" }, "claude"); expect(out.thinking).toEqual({ type: "enabled", budget_tokens: 24576 }); diff --git a/tests/unit/capabilities.test.js b/tests/unit/capabilities.test.js index a5c7b03d..ac544d47 100644 --- a/tests/unit/capabilities.test.js +++ b/tests/unit/capabilities.test.js @@ -32,6 +32,13 @@ describe("getCapabilitiesForModel", () => { } }); + it("reports Claude Fable 5.1 as a permanent adaptive-thinking model", () => { + expect(getCapabilitiesForModel("claude", "claude-fable-5-1")).toMatchObject({ + ...claudeSonnet5Expected, + thinkingCanDisable: false, + }); + }); + it("reports Kiro Claude Opus 4.8 as a 1M context model", () => { expect(getCapabilitiesForModel("kiro", "claude-opus-4.8").contextWindow).toBe(1000000); expect(getCapabilitiesForModel("kiro", "anthropic/claude-opus-4.8").contextWindow).toBe(1000000); diff --git a/tests/unit/claude-cloaking.test.js b/tests/unit/claude-cloaking.test.js index cd81081a..64994dc5 100644 --- a/tests/unit/claude-cloaking.test.js +++ b/tests/unit/claude-cloaking.test.js @@ -7,9 +7,14 @@ */ import { describe, it, expect } from "vitest"; -import { cloakClaudeTools, decloakStreamChunk } from "../../open-sse/utils/claudeCloaking.js"; +import { applyCloaking, cloakClaudeTools, decloakStreamChunk } from "../../open-sse/utils/claudeCloaking.js"; import { CLAUDE_TOOL_SUFFIX } from "../../open-sse/config/appConstants.js"; +it("advertises a Claude Code version accepted by Fable 5.1", () => { + const body = applyCloaking({ messages: [] }, "sk-ant-oat-test", "session-id"); + expect(body.system[0].text).toMatch(/^x-anthropic-billing-header: cc_version=2.1.258\./); +}); + describe("cloakClaudeTools", () => { const baseBody = { tools: [{ name: "todo_write", description: "write todos", input_schema: { type: "object", properties: {} } }], diff --git a/tests/unit/claude-header-forwarding.test.js b/tests/unit/claude-header-forwarding.test.js index 52318782..840a8559 100644 --- a/tests/unit/claude-header-forwarding.test.js +++ b/tests/unit/claude-header-forwarding.test.js @@ -29,6 +29,7 @@ describe("DefaultExecutor.buildHeaders() — claude provider", () => { headers["Anthropic-Version"] === "2023-06-01" || headers["anthropic-version"] === "2023-06-01"; expect(hasVersion).toBe(true); + expect(headers["User-Agent"]).toBe("claude-cli/2.1.258 (external, sdk-cli)"); }); it("includes heavy-agent beta flags for claude-opus-5", () => {