Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions open-sse/executors/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { signRequestBody } from "../services/claudeCodeCCH.ts";
import {
appendAnthropicBetaHeader,
CONTEXT_1M_BETA_HEADER,
enforceThinkingTemperature,
modelSupportsContext1mBeta,
} from "../services/claudeCodeCompatible.ts";
import { getClaudeCodeCompatibleRequestDefaults } from "@/lib/providers/requestDefaults";
Expand Down Expand Up @@ -1117,6 +1118,18 @@ export class BaseExecutor {
tb.messages = stripTrailingAssistantForProvider(stripped, this.provider);
}
}

// Anthropic's extended-thinking contract forbids non-default sampling
// params: temperature must be 1 and top_p >= 0.95 (or unset) whenever
// thinking is enabled/adaptive. Thinking can be injected by per-model
// requestDefaults *after* the translator/constraint passes, so normalize
// at this final dispatch point — the single chokepoint every Claude
// routing mode (grouped/raw/combo) and the native passthrough share,
// before fingerprinting and CCH signing serialize the body.
if (this.provider === "claude" || isClaudeCodeCompatible(this.provider)) {
enforceThinkingTemperature(transformedBody as Record<string, unknown>);
}
Comment on lines +1129 to +1131

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If transformedBody is null, undefined, or not an object, casting it to Record<string, unknown> and passing it to enforceThinkingTemperature will cause a runtime crash (TypeError) when accessing body.thinking. We should add a defensive check to ensure transformedBody is a valid non-null object before calling enforceThinkingTemperature.

Suggested change
if (this.provider === "claude" || isClaudeCodeCompatible(this.provider)) {
enforceThinkingTemperature(transformedBody as Record<string, unknown>);
}
if (
(this.provider === "claude" || isClaudeCodeCompatible(this.provider)) &&
transformedBody &&
typeof transformedBody === "object" &&
!Array.isArray(transformedBody)
) {
enforceThinkingTemperature(transformedBody as Record<string, unknown>);
}


let bodyString = JSON.stringify(transformedBody);

const shouldFingerprint =
Expand Down
18 changes: 17 additions & 1 deletion open-sse/services/claudeCodeConstraints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,32 @@
* Claude Code API constraints.
*
* Enforces Anthropic API requirements that real Claude Code handles:
* 1. temperature=1 when thinking is enabled
* 1. Sampling params under extended thinking: temperature=1 and top_p>=0.95
* (or unset) when thinking is enabled/adaptive
* 2. Disable thinking when tool_choice forces a specific tool
* 3. Enforce max 4 cache_control breakpoints
* 4. Normalize cache_control TTL ordering
*/

/**
* Anthropic's extended-thinking contract rejects non-default sampling params:
* with thinking enabled/adaptive, `temperature` may only be 1 and `top_p` must
* be >= 0.95 (or unset) — otherwise the Messages API returns HTTP 400
* ("`temperature` may only be set to 1 ..." / "`top_p` must be greater than or
* equal to 0.95 ..."). Clients such as the VS Code Copilot "Ollama" provider
* routinely send other values (e.g. temperature 0.7, top_p 0.9), and thinking
* can be injected by per-model requestDefaults *after* the request is built, so
* normalize here: pin temperature to 1 and drop top_p (Anthropic's "unset"
* branch — which also preserves the "never send both temperature and top_p"
* invariant).
*/
export function enforceThinkingTemperature(body: Record<string, unknown>): void {
const thinking = body.thinking as Record<string, unknown> | undefined;
if (thinking?.type === "enabled" || thinking?.type === "adaptive") {
body.temperature = 1;
if (body.top_p !== undefined) {
delete body.top_p;
}
}
}
Comment on lines 24 to 32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To make enforceThinkingTemperature robust and prevent potential runtime errors from unexpected or improperly typed inputs (especially when called with type assertions like as Record<string, unknown>), we should add a defensive guard clause at the beginning of the function to verify that body is a valid non-null object.

Suggested change
export function enforceThinkingTemperature(body: Record<string, unknown>): void {
const thinking = body.thinking as Record<string, unknown> | undefined;
if (thinking?.type === "enabled" || thinking?.type === "adaptive") {
body.temperature = 1;
if (body.top_p !== undefined) {
delete body.top_p;
}
}
}
export function enforceThinkingTemperature(body: Record<string, unknown>): void {
if (!body || typeof body !== "object" || Array.isArray(body)) return;
const thinking = body.thinking as Record<string, unknown> | undefined;
if (thinking?.type === "enabled" || thinking?.type === "adaptive") {
body.temperature = 1;
if (body.top_p !== undefined) {
delete body.top_p;
}
}
}


Expand Down
27 changes: 27 additions & 0 deletions tests/unit/claude-code-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,33 @@ describe("enforceThinkingTemperature", () => {
enforceThinkingTemperature(body);
assert.equal(body.temperature, 0.7, "temperature should be unchanged when no thinking");
});

it("drops top_p and pins temperature when thinking is enabled (both sent)", () => {
const body: Record<string, unknown> = {
thinking: { type: "enabled", budget_tokens: 1000 },
temperature: 0.7,
top_p: 0.9,
};
enforceThinkingTemperature(body);
assert.equal(body.temperature, 1, "temperature must be pinned to 1 when thinking is active");
assert.ok(!("top_p" in body), "top_p must be dropped when thinking is active");
});

it("drops a lone top_p (< 0.95) under adaptive thinking", () => {
const body: Record<string, unknown> = {
thinking: { type: "adaptive" },
top_p: 0.9,
};
enforceThinkingTemperature(body);
assert.ok(!("top_p" in body), "top_p must be dropped under adaptive thinking");
assert.equal(body.temperature, 1, "temperature must be set to 1 under adaptive thinking");
});

it("leaves top_p untouched when thinking is not present", () => {
const body: Record<string, unknown> = { top_p: 0.9, messages: [] };
enforceThinkingTemperature(body);
assert.equal(body.top_p, 0.9, "top_p should be unchanged when no thinking");
});
});

describe("disableThinkingIfToolChoiceForced", () => {
Expand Down