mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(sse): strip temperature for Claude models with extended thinking (#4853)
Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.
This commit is contained in:
committed by
GitHub
parent
ac55b201ba
commit
8a47bf2f5f
@@ -223,7 +223,23 @@ export function openaiToClaudeRequest(model, body, stream) {
|
||||
};
|
||||
|
||||
// Temperature
|
||||
if (body.temperature !== undefined) {
|
||||
//
|
||||
// Claude's Messages API rejects `temperature` when extended thinking is active.
|
||||
// Two cases where thinking is on:
|
||||
// (a) Caller passes `body.thinking` or `body.reasoning_effort` (handled later —
|
||||
// `result.thinking` becomes truthy, and we strip temperature at the end).
|
||||
// (b) The request targets Claude OAuth (claude-code), which always sends
|
||||
// `Anthropic-Beta: ...,interleaved-thinking-2025-05-14,...` in headers.
|
||||
// The model is forced into thinking server-side, but neither `body.thinking`
|
||||
// nor `result.thinking` will be set, so we detect this by model name. This
|
||||
// affects claude-opus-4.x and claude-sonnet-4.x (the families that support
|
||||
// extended thinking).
|
||||
//
|
||||
// For models that don't force thinking (haiku, older sonnets), preserve temperature.
|
||||
// Note: Opus 4.7+/Fable 5 already drop sampling params upstream of the translator via
|
||||
// the registry `unsupportedParams` strip; this covers the remaining 4.x families.
|
||||
const modelForcesThinking = /claude-(?:opus|sonnet)-4/i.test(String(model));
|
||||
if (body.temperature !== undefined && !modelForcesThinking) {
|
||||
result.temperature = body.temperature;
|
||||
}
|
||||
if (body.temperature === undefined && body.top_p !== undefined) {
|
||||
@@ -527,6 +543,15 @@ export function openaiToClaudeRequest(model, body, stream) {
|
||||
|
||||
delete result[COPILOT_REASONING_SUMMARY_MARKER];
|
||||
|
||||
// Final guard: Claude rejects `temperature` whenever extended thinking is
|
||||
// enabled. If `result.thinking` was set above from `body.thinking` or
|
||||
// `body.reasoning_effort` (manual budget or adaptive effort), drop temperature
|
||||
// defensively. The model-name strip earlier already covers Claude OAuth's
|
||||
// forced-thinking case (claude-opus-4.x / claude-sonnet-4.x).
|
||||
if (result.thinking && result.temperature !== undefined) {
|
||||
delete result.temperature;
|
||||
}
|
||||
|
||||
// Attach toolNameMap to result for response translation
|
||||
if (toolNameMap.size > 0) {
|
||||
result._toolNameMap = toolNameMap;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* openaiToClaudeRequest — strip `temperature` for Claude models with extended thinking.
|
||||
*
|
||||
* Claude's Messages API rejects `temperature` when extended thinking is active.
|
||||
* Two cases the translator handles:
|
||||
* (a) Model-name detection for forced-thinking families (/claude-(opus|sonnet)-4/),
|
||||
* used by Claude OAuth which always sends the interleaved-thinking beta header.
|
||||
* (b) A final guard that drops `temperature` whenever `result.thinking` was set from
|
||||
* `body.thinking` or `body.reasoning_effort`.
|
||||
*
|
||||
* Ported from decolua/9router PR #1264 (thanks @noestelar).
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { openaiToClaudeRequest } = await import(
|
||||
"../../open-sse/translator/request/openai-to-claude.ts"
|
||||
);
|
||||
|
||||
const baseBody = () => ({
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
temperature: 0.7,
|
||||
});
|
||||
|
||||
test("strips temperature for claude-opus-4 family (forced thinking)", () => {
|
||||
const result = openaiToClaudeRequest("claude-opus-4-6", baseBody(), false);
|
||||
assert.equal(result.temperature, undefined);
|
||||
});
|
||||
|
||||
test("strips temperature for claude-sonnet-4 family (forced thinking)", () => {
|
||||
const result = openaiToClaudeRequest("claude-sonnet-4-5", baseBody(), false);
|
||||
assert.equal(result.temperature, undefined);
|
||||
});
|
||||
|
||||
test("preserves temperature for non-thinking models (haiku, older sonnets)", () => {
|
||||
const haiku = openaiToClaudeRequest("claude-haiku-5-20251001", baseBody(), false);
|
||||
assert.equal(haiku.temperature, 0.7);
|
||||
|
||||
const sonnet35 = openaiToClaudeRequest("claude-3-5-sonnet-20241022", baseBody(), false);
|
||||
assert.equal(sonnet35.temperature, 0.7);
|
||||
});
|
||||
|
||||
test("strips temperature when body.thinking is explicitly set (final guard)", () => {
|
||||
// Non-thinking-forced model so the model-based strip doesn't fire; the final
|
||||
// guard must catch this case.
|
||||
const body = {
|
||||
...baseBody(),
|
||||
thinking: { type: "enabled", budget_tokens: 4096 },
|
||||
};
|
||||
const result = openaiToClaudeRequest("claude-haiku-5-20251001", body, false);
|
||||
assert.equal(result.temperature, undefined);
|
||||
assert.ok(result.thinking, "thinking should still be present");
|
||||
});
|
||||
|
||||
test("strips temperature when reasoning_effort triggers thinking (final guard)", () => {
|
||||
const body = { ...baseBody(), reasoning_effort: "medium" };
|
||||
const result = openaiToClaudeRequest("claude-haiku-5-20251001", body, false);
|
||||
assert.equal(result.temperature, undefined);
|
||||
assert.ok(result.thinking, "reasoning_effort should produce a thinking block");
|
||||
});
|
||||
|
||||
test("preserves temperature when reasoning_effort is unrecognized on a non-thinking model", () => {
|
||||
// "none" is not in the effort→budget map, so no thinking block is produced and
|
||||
// temperature must survive on a model that does not force thinking.
|
||||
const body = { ...baseBody(), reasoning_effort: "none" };
|
||||
const result = openaiToClaudeRequest("claude-haiku-5-20251001", body, false);
|
||||
assert.equal(result.temperature, 0.7);
|
||||
assert.equal(result.thinking, undefined);
|
||||
});
|
||||
Reference in New Issue
Block a user