fix(sse): strip reasoning_effort for non-reasoning Groq models (#3258) (#3277)

Regression of #764. Claude Code → Groq (llama-3.3-70b-versatile) returned HTTP 400
because the model was treated as reasoning-capable: supportsReasoning() defaulted to true,
so applyThinkingBudget did not strip reasoning params, and the claude→openai translator
forwarded reasoning_effort (and re-injected it from output_config.effort) — which Groq
rejects on non-reasoning models.

- providerRegistry: mark llama-3.3-70b-versatile + llama-4-scout supportsReasoning:false
  (gpt-oss / qwen3-32b keep reasoning — they accept reasoning_effort).
- stripThinkingConfig: also strip output_config.effort so the translator can't re-inject
  reasoning_effort downstream.

Regression test: tests/unit/thinking-budget-groq-3258.test.ts (RED before, GREEN after);
existing thinking-budget suites stay green (45/45).
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-06 02:48:38 -03:00
committed by GitHub
parent 41eb0091a2
commit 80c546eba9
4 changed files with 79 additions and 2 deletions

View File

@@ -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/<combo>` 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 `<tool_call name="...">{json}</tool_call>` 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))
---

View File

@@ -2335,8 +2335,13 @@ const _REGISTRY_EAGER: Record<string, RegistryEntry> = {
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" },

View File

@@ -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) };

View File

@@ -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<string, unknown>;
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<string, { effort?: unknown } | undefined>;
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<string, unknown>;
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<string, unknown>;
assert.equal(out.reasoning_effort, "high", "gpt-oss is a reasoning model — must keep the field");
setThinkingBudgetConfig(DEFAULT_THINKING_CONFIG);
});