From 411d36d5d0bb6c67de83bc5c45e0b766b65b901a Mon Sep 17 00:00:00 2001 From: mhupfauer Date: Thu, 9 Jul 2026 23:07:21 +0200 Subject: [PATCH] Fix temperature=400 retry for OpenAI-compatible endpoints The temperature-unsupported retry handler only matched when the OpenAI SDK populated `err.code === "unsupported_value"` and `err.param === "temperature"`. Many OpenAI-*compatible* endpoints return the error as a bare message string ("Unsupported value: 'temperature' does not support 0.7 with this model. Only the default (1) value is supported.") without those structured fields, so the retry never triggered and the request failed outright. Extract detection into `isTemperatureUnsupportedError`, which matches the structured fields, a nested `err.error.*` shape, or the message text, and use it in both the non-streaming and streaming retry paths so they fall back to temperature=1. Add unit tests covering each error shape. Co-Authored-By: Claude Opus 4.8 --- backend/src/utils/llm/providers.ts | 33 ++++++++++++++--- backend/tests/temperatureRetry.test.ts | 51 ++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 backend/tests/temperatureRetry.test.ts diff --git a/backend/src/utils/llm/providers.ts b/backend/src/utils/llm/providers.ts index b31cee5..1aaaf2b 100644 --- a/backend/src/utils/llm/providers.ts +++ b/backend/src/utils/llm/providers.ts @@ -364,6 +364,31 @@ function clampTemperature(model: string, requested: number): number { return requested; } +/** + * Detects the "this model only supports the default temperature" error. + * + * Real OpenAI populates `err.code === "unsupported_value"` and + * `err.param === "temperature"`, but many OpenAI-*compatible* endpoints only + * return the message string (e.g. "Unsupported value: 'temperature' does not + * support 0.7 with this model. Only the default (1) value is supported.") + * without those structured fields, so we fall back to matching the text. + */ +export function isTemperatureUnsupportedError(err: any): boolean { + const param = err?.param ?? err?.error?.param; + const code = err?.code ?? err?.error?.code; + if (code === "unsupported_value" && param === "temperature") return true; + + const message = String( + err?.message ?? err?.error?.message ?? "", + ).toLowerCase(); + return ( + message.includes("temperature") && + (message.includes("does not support") || + message.includes("only the default") || + message.includes("unsupported value")) + ); +} + function normalizeFinishReason(raw: string | null | undefined): FinishReason { if (raw === "stop" || raw === "end_turn") return "stop"; if (raw === "tool_calls" || raw === "tool_use") return "tool_calls"; @@ -623,9 +648,7 @@ export async function invoke_llm(opts: InvokeOptions): Promise { return await tryCompletion(requestedTemp); } catch (err: any) { const isTempUnsupported = - err?.code === "unsupported_value" && - err?.param === "temperature" && - requestedTemp !== 1; + requestedTemp !== 1 && isTemperatureUnsupportedError(err); if (isTempUnsupported) { console.warn( @@ -1259,9 +1282,7 @@ export async function invoke_llm_streaming( return await runStream(requestedTemp); } catch (err: any) { const isTempUnsupported = - err?.code === "unsupported_value" && - err?.param === "temperature" && - requestedTemp !== 1; + requestedTemp !== 1 && isTemperatureUnsupportedError(err); if (isTempUnsupported) { console.warn(`[inference] Retrying stream with temperature=1`); diff --git a/backend/tests/temperatureRetry.test.ts b/backend/tests/temperatureRetry.test.ts new file mode 100644 index 0000000..0f7f9c7 --- /dev/null +++ b/backend/tests/temperatureRetry.test.ts @@ -0,0 +1,51 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { isTemperatureUnsupportedError } from "../src/utils/llm/providers"; + +test("detects structured OpenAI unsupported_value/temperature error", () => { + const err = { + status: 400, + code: "unsupported_value", + param: "temperature", + message: "Unsupported value: 'temperature' does not support 0.7 with this model.", + }; + assert.equal(isTemperatureUnsupportedError(err), true); +}); + +test("detects OpenAI-compatible endpoints that only return a message string", () => { + // Many OpenAI-compatible servers omit err.code/err.param and only set the + // message — this is the exact error users hit against such endpoints. + const err = { + status: 400, + message: + "400 Unsupported value: 'temperature' does not support 0.7 with this model. Only the default (1) value is supported.", + }; + assert.equal(isTemperatureUnsupportedError(err), true); +}); + +test("detects nested error object shape", () => { + const err = { + error: { + code: "unsupported_value", + param: "temperature", + message: "temperature does not support 0.7", + }, + }; + assert.equal(isTemperatureUnsupportedError(err), true); +}); + +test("ignores unrelated errors", () => { + assert.equal( + isTemperatureUnsupportedError({ status: 500, message: "internal server error" }), + false, + ); + assert.equal( + isTemperatureUnsupportedError({ + code: "unsupported_value", + param: "top_p", + message: "Unsupported value: 'top_p'", + }), + false, + ); + assert.equal(isTemperatureUnsupportedError(undefined), false); +});