fix(anthropic): strip top_p when temperature is set to avoid 400 (#3691)

Integrated into release/v3.8.23
This commit is contained in:
Felipe Almeman
2026-06-12 02:40:13 -03:00
committed by GitHub
parent c81211326c
commit 7ad96cca18
3 changed files with 46 additions and 21 deletions

View File

@@ -1986,12 +1986,13 @@ export async function handleChatCore({
// Use credentials.connectionId as a fallback so that requests without an
// explicit session-level connectionId still register in the pendingRequests map.
const pendingConnId = connectionId || credentials?.connectionId || null;
const pendingRequestId = trackPendingRequest(model, provider, pendingConnId, true, {
clientEndpoint: clientRawRequest?.endpoint || "/v1/chat/completions",
clientRequest: clientRawRequest?.body ?? body,
providerRequest: initialProviderRequest,
stage: "registered",
}) || generateRequestId();
const pendingRequestId =
trackPendingRequest(model, provider, pendingConnId, true, {
clientEndpoint: clientRawRequest?.endpoint || "/v1/chat/completions",
clientRequest: clientRawRequest?.body ?? body,
providerRequest: initialProviderRequest,
stage: "registered",
}) || generateRequestId();
// Initialize rate limit settings from persisted DB (once, lazy)
await initializeRateLimits();
@@ -2769,7 +2770,10 @@ export async function handleChatCore({
comboTargetLimits,
});
contextLimit = resolved.limit;
log?.info?.("CONTEXT", `Combo context limit: ${resolved.limit} (source=${resolved.source})`);
log?.info?.(
"CONTEXT",
`Combo context limit: ${resolved.limit} (source=${resolved.source})`
);
} catch (err) {
log?.warn?.("CONTEXT", "Failed to resolve combo limits for compression: " + err);
}
@@ -3104,6 +3108,12 @@ export async function handleChatCore({
translatedBody.messages,
DEFAULT_THINKING_CLAUDE_SIGNATURE
) as typeof translatedBody.messages;
// Anthropic API rejects requests with both temperature and top_p.
// VS Code Claude extension and similar clients send both; strip top_p.
if (translatedBody.temperature !== undefined && translatedBody.top_p !== undefined) {
delete translatedBody.top_p;
}
}
// Fix #2468: always extract role:"system" → top-level system.
@@ -5442,17 +5452,14 @@ export async function handleChatCore({
}
const responseHeaders: Record<string, string> = {
...buildStreamingResponseHeaders(
providerResponse.headers,
{
provider,
model,
cacheHit: false,
latencyMs: 0,
usage: null,
costUsd: 0,
}
),
...buildStreamingResponseHeaders(providerResponse.headers, {
provider,
model,
cacheHit: false,
latencyMs: 0,
usage: null,
costUsd: 0,
}),
"x-omniroute-request-id": pendingRequestId,
};
@@ -5560,7 +5567,9 @@ export async function handleChatCore({
});
} catch (e) {
// Best-effort — don't break the stream completion path if this fails
try { console.warn("finalizeMostRecentPendingRequest failed:", e && (e.message || e)); } catch {}
try {
console.warn("finalizeMostRecentPendingRequest failed:", e && (e.message || e));
} catch {}
}
if (apiKeyInfo?.id && streamUsage) {

View File

@@ -212,7 +212,7 @@ export function openaiToClaudeRequest(model, body, stream) {
if (body.temperature !== undefined) {
result.temperature = body.temperature;
}
if (body.top_p !== undefined) {
if (body.temperature === undefined && body.top_p !== undefined) {
result.top_p = body.top_p;
}
if (body.stop !== undefined) {

View File

@@ -84,7 +84,8 @@ test("OpenAI -> Claude maps system messages, parameters and assistant cache mark
assert.equal(result.stream, true);
assert.equal(result.max_tokens, 33);
assert.equal(result.temperature, 0.25);
assert.equal(result.top_p, 0.8);
// top_p is stripped when temperature is also present (Anthropic rejects both).
assert.equal(result.top_p, undefined);
assert.deepEqual(result.stop_sequences, ["DONE"]);
assert.equal(result.system[0].text, "Rule A\nRule B\nRule C");
assert.equal(result.messages[0].role, "user");
@@ -94,6 +95,21 @@ test("OpenAI -> Claude maps system messages, parameters and assistant cache mark
assert.deepEqual(result.messages[1].content[0].cache_control, { type: "ephemeral" });
});
test("OpenAI -> Claude strips top_p when temperature is also present", () => {
const result = openaiToClaudeRequest(
"claude-4-sonnet",
{
messages: [{ role: "user", content: "Hello" }],
temperature: 0.25,
top_p: 0.8,
},
false
);
assert.equal(result.temperature, 0.25);
assert.equal(result.top_p, undefined);
});
test("OpenAI -> Claude converts multimodal content, tool declarations, tool calls and tool results", () => {
const result = openaiToClaudeRequest(
"claude-4-sonnet",