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
|
||||
|
||||
## 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
|
||||
- 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.
|
||||
*/
|
||||
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 },
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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=<ver>.<build>; cc_entrypoint=sdk-cli; cch=<hash>;
|
||||
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":"<uuid>","session_id":"<uuid>"}
|
||||
// device_id/account_uuid derive from apiKey (stable per account), session_id per-conversation
|
||||
function generateFakeUserID(sessionId, apiKey) {
|
||||
|
||||
@@ -1009,4 +1009,4 @@
|
||||
},
|
||||
"format": "openai"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {} } }],
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user