diff --git a/CHANGELOG.md b/CHANGELOG.md index 70f9d8e135..3a636bed42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ _Development cycle in progress — entries are added as work merges into `releas - **api/responses:** combo names without a slash (e.g. `paid-premium`, `n8n-text`) are no longer force-rewritten to `codex/` on `/v1/responses` — `resolveResponsesApiModel` now returns the request unchanged when the model resolves to a combo (regression from the v3.8.9 Codex WS→HTTP fallback) ([#3242](https://github.com/diegosouzapw/OmniRoute/pull/3242) — thanks @wilsonicdev; the same fix shipped via #3244, closing #3227 / #3233) - **sse/web-tools:** web-cookie providers (e.g. `ds-web`) that wrap tool calls as `{json}` are now parsed correctly — the real tool name is read from the JSON body instead of the tag attribute, and the call is no longer silently dropped when `arguments` is absent ([#3260](https://github.com/diegosouzapw/OmniRoute/issues/3260)) +- **sse/groq:** non-reasoning Groq models (`llama-3.3-70b-versatile`, `llama-4-scout`) are now flagged `supportsReasoning: false`, so `reasoning_effort` / `output_config.effort` / `thinking` are stripped before dispatch instead of being forwarded and rejected with HTTP 400 — fixes the Claude Code → Groq regression of #764 ([#3258](https://github.com/diegosouzapw/OmniRoute/issues/3258)) --- diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 53381ab64a..aefcb9d728 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -2335,8 +2335,13 @@ const _REGISTRY_EAGER: Record = { authType: "apikey", authHeader: "bearer", models: [ - { id: "meta-llama/llama-4-scout-17b-16e-instruct", name: "Llama 4 Scout" }, - { id: "llama-3.3-70b-versatile", name: "Llama 3.3 70B" }, + // Non-reasoning Llama models: Groq returns HTTP 400 if reasoning_effort is sent (#3258). + { + id: "meta-llama/llama-4-scout-17b-16e-instruct", + name: "Llama 4 Scout", + supportsReasoning: false, + }, + { id: "llama-3.3-70b-versatile", name: "Llama 3.3 70B", supportsReasoning: false }, { id: "openai/gpt-oss-120b", name: "GPT-OSS 120B" }, { id: "openai/gpt-oss-20b", name: "GPT-OSS 20B" }, { id: "qwen/qwen3-32b", name: "Qwen3 32B" }, diff --git a/open-sse/services/thinkingBudget.ts b/open-sse/services/thinkingBudget.ts index 6f792fd3bd..066698334b 100644 --- a/open-sse/services/thinkingBudget.ts +++ b/open-sse/services/thinkingBudget.ts @@ -226,6 +226,18 @@ function stripThinkingConfig(body: unknown) { delete result.reasoning_effort; delete result.reasoning; + // Claude Code output_config.effort — strip the effort hint too, otherwise the + // claude→openai translator re-injects reasoning_effort downstream (#3258). + if (result.output_config && typeof result.output_config === "object") { + const outputConfig = { ...toRecord(result.output_config) }; + delete outputConfig.effort; + if (Object.keys(outputConfig).length === 0) { + delete result.output_config; + } else { + result.output_config = outputConfig; + } + } + // Gemini format if (result.generationConfig) { const generationConfig = { ...toRecord(result.generationConfig) }; diff --git a/tests/unit/thinking-budget-groq-3258.test.ts b/tests/unit/thinking-budget-groq-3258.test.ts new file mode 100644 index 0000000000..bbd5c69403 --- /dev/null +++ b/tests/unit/thinking-budget-groq-3258.test.ts @@ -0,0 +1,59 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { applyThinkingBudget, setThinkingBudgetConfig, ThinkingMode, DEFAULT_THINKING_CONFIG } = + await import("../../open-sse/services/thinkingBudget.ts"); + +// Regression coverage for #3258 (regression of #764): Claude Code → Groq failed with +// `reasoning_effort` HTTP 400 because non-reasoning Groq models (llama-3.3-70b-versatile, +// llama-4-scout) were treated as reasoning-capable, so reasoning_effort / output_config.effort +// / thinking survived and Groq rejected them. Reasoning models (gpt-oss) must keep the field. + +test("#3258 groq/llama-3.3-70b-versatile strips reasoning_effort", () => { + setThinkingBudgetConfig({ mode: ThinkingMode.PASSTHROUGH }); + const out = applyThinkingBudget({ + model: "groq/llama-3.3-70b-versatile", + messages: [{ role: "user", content: "hi" }], + reasoning_effort: "medium", + }) as Record; + assert.equal(out.reasoning_effort, undefined, "reasoning_effort must be stripped for groq llama"); + setThinkingBudgetConfig(DEFAULT_THINKING_CONFIG); +}); + +test("#3258 groq/llama-3.3-70b-versatile strips output_config.effort and thinking", () => { + setThinkingBudgetConfig({ mode: ThinkingMode.PASSTHROUGH }); + const out = applyThinkingBudget({ + model: "groq/llama-3.3-70b-versatile", + messages: [{ role: "user", content: "hi" }], + output_config: { effort: "high" }, + thinking: { type: "enabled", budget_tokens: 10240 }, + }) as Record; + assert.equal(out.thinking, undefined, "thinking must be stripped"); + assert.ok( + !out.output_config || out.output_config.effort === undefined, + "output_config.effort must be stripped (else claude→openai re-injects reasoning_effort)" + ); + setThinkingBudgetConfig(DEFAULT_THINKING_CONFIG); +}); + +test("#3258 groq/meta-llama/llama-4-scout strips reasoning_effort", () => { + setThinkingBudgetConfig({ mode: ThinkingMode.PASSTHROUGH }); + const out = applyThinkingBudget({ + model: "groq/meta-llama/llama-4-scout-17b-16e-instruct", + messages: [{ role: "user", content: "hi" }], + reasoning_effort: "low", + }) as Record; + assert.equal(out.reasoning_effort, undefined); + setThinkingBudgetConfig(DEFAULT_THINKING_CONFIG); +}); + +test("#3258 groq/openai/gpt-oss-120b KEEPS reasoning_effort (reasoning model — no regression)", () => { + setThinkingBudgetConfig({ mode: ThinkingMode.PASSTHROUGH }); + const out = applyThinkingBudget({ + model: "groq/openai/gpt-oss-120b", + messages: [{ role: "user", content: "hi" }], + reasoning_effort: "high", + }) as Record; + assert.equal(out.reasoning_effort, "high", "gpt-oss is a reasoning model — must keep the field"); + setThinkingBudgetConfig(DEFAULT_THINKING_CONFIG); +});