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
This commit is contained in:
@@ -40,6 +40,9 @@
|
|||||||
- **i18n**: pt-BR expanded to 1132 terms
|
- **i18n**: pt-BR expanded to 1132 terms
|
||||||
|
|
||||||
## Fixes
|
## 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
|
- **Stream**: record usage when a client closes on the terminal event — the
|
||||||
Responses API has no [DONE] sentinel, so codex closed the socket on
|
Responses API has no [DONE] sentinel, so codex closed the socket on
|
||||||
`response.completed` and cancelled the reader before flush() ran its usage
|
`response.completed` and cancelled the reader before flush() ran its usage
|
||||||
@@ -738,4 +741,4 @@
|
|||||||
# v0.4.46 (2026-05-15)
|
# v0.4.46 (2026-05-15)
|
||||||
|
|
||||||
## Breaking Changes
|
## Breaking Changes
|
||||||
- Tunnel public URL changed — old tunnel links no longer work, please reconnect to get the new URL
|
- Tunnel public URL changed — old tunnel links no longer work, please reconnect to get the new URL
|
||||||
|
|||||||
@@ -83,7 +83,8 @@ export function capabilitiesFromServiceKind(kind) {
|
|||||||
* otherwise mis-match. Only declare deltas vs DEFAULT.
|
* otherwise mis-match. Only declare deltas vs DEFAULT.
|
||||||
*/
|
*/
|
||||||
export const MODEL_CAPABILITIES = {
|
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": { 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-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 },
|
"claude-opus-5-agentic": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CLAUDE_CLI_SPOOF_HEADERS } from "../shared.js";
|
import { CLAUDE_CLI_VERSION } from "../shared.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: "claude",
|
id: "claude",
|
||||||
@@ -25,7 +25,7 @@ export default {
|
|||||||
"Anthropic-Version": "2023-06-01",
|
"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-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",
|
"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-App": "cli",
|
||||||
"X-Stainless-Helper-Method": "stream",
|
"X-Stainless-Helper-Method": "stream",
|
||||||
"X-Stainless-Retry-Count": "0",
|
"X-Stainless-Retry-Count": "0",
|
||||||
@@ -58,6 +58,7 @@ export default {
|
|||||||
},
|
},
|
||||||
models: [
|
models: [
|
||||||
{ 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", name: "Claude Fable 5" },
|
{ id: "claude-fable-5", name: "Claude Fable 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" },
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function mapStainlessArch() {
|
|||||||
|
|
||||||
// Anthropic API version (single source — reused across claude-format providers/executors)
|
// Anthropic API version (single source — reused across claude-format providers/executors)
|
||||||
export const ANTHROPIC_API_VERSION = "2023-06-01";
|
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)
|
// Shared Claude-compatible API headers (reused across claude-format providers)
|
||||||
export const CLAUDE_API_HEADERS = {
|
export const CLAUDE_API_HEADERS = {
|
||||||
@@ -34,7 +35,7 @@ export const CLAUDE_CLI_SPOOF_HEADERS = {
|
|||||||
"Anthropic-Version": ANTHROPIC_API_VERSION,
|
"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-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",
|
"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-App": "cli",
|
||||||
"X-Stainless-Helper-Method": "stream",
|
"X-Stainless-Helper-Method": "stream",
|
||||||
"X-Stainless-Retry-Count": "0",
|
"X-Stainless-Retry-Count": "0",
|
||||||
|
|||||||
@@ -237,12 +237,10 @@ function applyFormat(fmt, body, cfg, caps, supportedLevels) {
|
|||||||
}
|
}
|
||||||
case "claude-adaptive": {
|
case "claude-adaptive": {
|
||||||
if (none && canDisable) { body.thinking = { type: "disabled" }; break; }
|
if (none && canDisable) { body.thinking = { type: "disabled" }; break; }
|
||||||
// output_config.effort alone does NOT turn thinking on: Anthropic requires
|
// Models that can disable thinking need the explicit adaptive switch.
|
||||||
// an explicit thinking:{type:"adaptive"} on Opus 4.6/4.7/4.8 and Sonnet 4.6
|
// Permanently adaptive models such as Fable 5.1 accept effort directly.
|
||||||
// ("thinking is off unless you explicitly set it"), and Anthropic-compatible
|
if (canDisable) body.thinking = { type: "adaptive" };
|
||||||
// shims (e.g. GitHub Copilot /v1/messages) default thinking off even for
|
else delete body.thinking;
|
||||||
// Sonnet 5. Send both fields — the documented adaptive-thinking shape.
|
|
||||||
body.thinking = { type: "adaptive" };
|
|
||||||
const level = toLevel(eff);
|
const level = toLevel(eff);
|
||||||
body.output_config = { effort: level === "xhigh" ? "high" : level };
|
body.output_config = { effort: level === "xhigh" ? "high" : level };
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { createHash, randomBytes, randomUUID } from "crypto";
|
import { createHash, randomBytes, randomUUID } from "crypto";
|
||||||
import { CLAUDE_TOOL_SUFFIX, CC_DEFAULT_TOOLS } from "../config/appConstants.js";
|
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";
|
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=<ver>.<build>; cc_entrypoint=sdk-cli; cch=<hash>;
|
// x-anthropic-billing-header: cc_version=<ver>.<build>; cc_entrypoint=sdk-cli; cch=<hash>;
|
||||||
function generateBillingHeader(payload) {
|
function generateBillingHeader(payload) {
|
||||||
const content = JSON.stringify(payload);
|
const content = JSON.stringify(payload);
|
||||||
const cch = createHash("sha256").update(content).digest("hex").slice(0, 5);
|
const cch = createHash("sha256").update(content).digest("hex").slice(0, 5);
|
||||||
const buildHash = randomBytes(2).toString("hex").slice(0, 3);
|
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)
|
// 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)}`;
|
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":"<uuid>","session_id":"<uuid>"}
|
// {"device_id":"<64hex>","account_uuid":"<uuid>","session_id":"<uuid>"}
|
||||||
// device_id/account_uuid derive from apiKey (stable per account), session_id per-conversation
|
// device_id/account_uuid derive from apiKey (stable per account), session_id per-conversation
|
||||||
function generateFakeUserID(sessionId, apiKey) {
|
function generateFakeUserID(sessionId, apiKey) {
|
||||||
|
|||||||
@@ -1009,4 +1009,4 @@
|
|||||||
},
|
},
|
||||||
"format": "openai"
|
"format": "openai"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ describe("applyThinking per provider format", () => {
|
|||||||
// Sonnet 5). Both fields together are the documented adaptive shape.
|
// Sonnet 5). Both fields together are the documented adaptive shape.
|
||||||
expect(out.thinking).toEqual({ type: "adaptive" });
|
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", () => {
|
it("claude haiku → enabled+budget", () => {
|
||||||
const out = apply("claude", "claude-haiku-4.5", { reasoning_effort: "high" }, "claude");
|
const out = apply("claude", "claude-haiku-4.5", { reasoning_effort: "high" }, "claude");
|
||||||
expect(out.thinking).toEqual({ type: "enabled", budget_tokens: 24576 });
|
expect(out.thinking).toEqual({ type: "enabled", budget_tokens: 24576 });
|
||||||
|
|||||||
@@ -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", () => {
|
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", "claude-opus-4.8").contextWindow).toBe(1000000);
|
||||||
expect(getCapabilitiesForModel("kiro", "anthropic/claude-opus-4.8").contextWindow).toBe(1000000);
|
expect(getCapabilitiesForModel("kiro", "anthropic/claude-opus-4.8").contextWindow).toBe(1000000);
|
||||||
|
|||||||
@@ -7,9 +7,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from "vitest";
|
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";
|
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", () => {
|
describe("cloakClaudeTools", () => {
|
||||||
const baseBody = {
|
const baseBody = {
|
||||||
tools: [{ name: "todo_write", description: "write todos", input_schema: { type: "object", properties: {} } }],
|
tools: [{ name: "todo_write", description: "write todos", input_schema: { type: "object", properties: {} } }],
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ describe("DefaultExecutor.buildHeaders() — claude provider", () => {
|
|||||||
headers["Anthropic-Version"] === "2023-06-01" ||
|
headers["Anthropic-Version"] === "2023-06-01" ||
|
||||||
headers["anthropic-version"] === "2023-06-01";
|
headers["anthropic-version"] === "2023-06-01";
|
||||||
expect(hasVersion).toBe(true);
|
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", () => {
|
it("includes heavy-agent beta flags for claude-opus-5", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user