fix(processors): treat Fireworks model ids as reasoning-hint incompatible - #123
fix(processors): treat Fireworks model ids as reasoning-hint incompatible#123himorishige wants to merge 1 commit into
Conversation
…ible
Fireworks AI's OpenAI-compatible API validates request bodies strictly
and rejects the vLLM chat_template_kwargs hint with HTTP 400 ("Extra
inputs are not permitted, field: 'chat_template_kwargs'"). Fireworks
model ids always carry the provider namespace
(accounts/fireworks/models/...), so extend the deny list used by
model_accepts_reasoning_hint() to match them.
This stops the LLM classifier's disable_reasoning auto-detect from
injecting the hint at Fireworks-served classifier models, and — once
the DeepSeek tier overrides consult the same check — deterministic
tier calls as well.
Signed-off-by: Hiroshi Morishige <hiroshi.morishige@gmail.com>
WalkthroughThe reasoning hint compatibility check now rejects model IDs containing ChangesReasoning Hint Compatibility
Estimated code review effort: 2 (Simple) | ~10 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/test_reasoning_hint.py (1)
21-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRename the parametrized test to cover Fireworks models.
The test now covers more than the Claude family, so
test_claude_family_rejects_hintis misleading. Rename it to a provider-neutral name such astest_models_rejecting_reasoning_hint.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_reasoning_hint.py` around lines 21 - 23, Rename the parametrized test function test_claude_family_rejects_hint to a provider-neutral name such as test_models_rejecting_reasoning_hint, keeping its existing parameters and assertions unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/test_reasoning_hint.py`:
- Around line 21-23: Rename the parametrized test function
test_claude_family_rejects_hint to a provider-neutral name such as
test_models_rejecting_reasoning_hint, keeping its existing parameters and
assertions unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: ecfbcbb0-16db-4487-b250-1d290351914a
📒 Files selected for processing (2)
switchyard/lib/processors/reasoning_hint.pytests/test_reasoning_hint.py
| # Fireworks ids always carry the provider namespace | ||
| # (``accounts/fireworks/models/...``). vLLM-served reasoning models that | ||
| # need the hint never carry these tokens. | ||
| _NO_REASONING_HINT_TAGS = ("anthropic", "bedrock", "claude", "fireworks") |
There was a problem hiding this comment.
This adds fireworks to the deny list so we stop sending chat_template_kwargs to Fireworks. Recap for anyone reading: chat_template_kwargs is a vLLM-specific request field — it passes keyword arguments into the model's chat template and isn't part of the OpenAI API. model_accepts_reasoning_hint decides whether we send it, answering "no" only when the model name contains one of these tags.
Two things I looked into that make me wonder if a different structure would hold up better:
1. This list has to grow one provider at a time, and it already misses one. azure/deepseek-ai/deepseek-v4-pro runs on the same gateway, its name matches none of these tags, so we'd still send it the field — and Azure also validates the body strictly. Every new strict provider needs another tag added here by hand.
2. Each provider turns reasoning off with a different knob, so a yes/no "does it accept the hint?" can only ever suppress the field — it can't send the right one. From the docs and source:
| Provider | Accepts chat_template_kwargs? |
How you actually turn reasoning off |
|---|---|---|
| vLLM | yes (unknown fields are ignored, not rejected) | chat_template_kwargs.enable_thinking=false for Qwen3 — DeepSeek-V3.1 uses the key thinking with the opposite default |
| Fireworks | no — 400 | top-level reasoning_effort: "none" |
| Azure AI (DeepSeek) | no — 400 | no request parameter; the model decides |
| OpenAI | no | reasoning_effort |
| Anthropic | no | thinking: {"type": "disabled"} |
| DeepSeek API | no | choose the model: deepseek-chat (off) vs deepseek-reasoner (on) |
Sources: vLLM defines the field and allows (ignores) unknown ones rather than rejecting them — protocol.py, engine/protocol.py, reasoning outputs; Fireworks reasoning guide; OpenAI reasoning; Anthropic extended thinking; DeepSeek thinking mode; Azure chat reasoning.
Concrete suggestion: instead of a deny-list, a map keyed by provider / served-model family → a small dict of per-provider request settings. reasoning_off is just one entry in that dict, so the next provider-specific quirk we hit (a required header, another body field, a format tweak) becomes another key alongside it — not a whole new parallel map to keep in sync:
# provider / model-family -> per-provider request settings
# reasoning_off is one key here; future per-provider quirks live alongside it
PROVIDER_SETTINGS = {
"vllm-qwen": {"reasoning_off": {"chat_template_kwargs": {"enable_thinking": False}}},
"vllm-deepseek": {"reasoning_off": {}}, # DeepSeek-V3.1 default is off (key is `thinking`)
"fireworks": {"reasoning_off": {"reasoning_effort": "none"}},
"openai": {"reasoning_off": {"reasoning_effort": "minimal"}},
"anthropic": {"reasoning_off": {"thinking": {"type": "disabled"}}},
# azure deepseek / deepseek-api: no per-request reasoning switch
}
# lookup: PROVIDER_SETTINGS.get(provider, {}).get("reasoning_off")One honest caveat so we don't over-build it: the hard part is the key, not the map — vLLM's own knob differs by model family (Qwen3 enable_thinking vs DeepSeek-V3.1 thinking), so keying on the model-id string still needs care. If we'd rather not maintain provider detection here at all, the other option is to let each target/endpoint declare its reasoning-off body in config (I left that note on #122).
| — usable directly as a classifier ``disable_reasoning`` default. | ||
| ``False`` for Anthropic/Bedrock/Claude and Fireworks-served ids (they | ||
| 400 on it), else ``True`` — usable directly as a classifier | ||
| ``disable_reasoning`` default. |
There was a problem hiding this comment.
Some context for this line: the LLM classifier is a small model that reads each incoming request and returns a JSON decision about which tier (weak or strong) should handle it. disable_reasoning controls whether we send enable_thinking=false. That hint matters because DeepSeek V4 is a reasoning model: without it, the model can put its JSON decision in the reasoning_content field and leave the normal content field empty. The classifier only reads content, so an empty content means it can't parse a decision and falls back to the default tier.
This change makes model_accepts_reasoning_hint return False for Fireworks, so the classifier stops sending the hint to a Fireworks classifier model. My question: does a Fireworks-served DeepSeek V4 still put its answer in content when we don't send the hint? If it behaves like the vLLM version and leaves content empty, we've replaced a clear error (the 400) with a silent one — the classifier gets nothing back and falls back on every request, with no error to notice.
Concrete suggestion: worth one real call to a Fireworks DeepSeek V4 with a classifier-style prompt and no hint, then check whether content is filled in. If it comes back empty, note that Fireworks disables reasoning with the top-level reasoning_effort: "none" field, not chat_template_kwargs (docs) — so the fix would be to send that for Fireworks rather than nothing, which is what the map on the line above would let us do. The two places that read disable_reasoning are llm_classifier/request_processor.py and stage_router/classifier.py.
Fixes the classifier half of #121.
What
Fireworks AI's OpenAI-compatible API validates request bodies strictly and rejects the vLLM
chat_template_kwargshint with HTTP 400 (Extra inputs are not permitted, field: 'chat_template_kwargs'). Fireworks model ids always carry the provider namespace (accounts/fireworks/models/...), so this PR addsfireworksto_NO_REASONING_HINT_TAGS.With this, the LLM classifier's
disable_reasoningauto-detect stops injecting the hint for Fireworks-served classifier models, and — combined with #122 — deterministic tier calls stop injecting it as well.Validation
deepseek-v4-flash,deepseek-v4-pro,glm-5p2);tests/test_reasoning_hint.py13 passed.accounts/fireworks/models/deepseek-v4-flash) classifies successfully end to end (main @060ad758, Fireworks serverless).Summary by CodeRabbit