Skip to content

fix(llm): make reasoning_effort model-aware (denylist + retry-on-400)#65

Merged
ropoctl merged 1 commit into
mainfrom
fix/reasoning-effort-model-aware
Jun 26, 2026
Merged

fix(llm): make reasoning_effort model-aware (denylist + retry-on-400)#65
ropoctl merged 1 commit into
mainfrom
fix/reasoning-effort-model-aware

Conversation

@ropoctl

@ropoctl ropoctl commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Supersedes #47 (closed) — re-ported onto the current refactored native.py.

Problem

AsyncLLMClient defaulted reasoning_effort="medium" and sent it on every OpenAI-compat request unconditionally. Non-reasoning models served over OpenAI-compat endpoints reject the param with HTTP 400:

400 Bad Request: `reasoning_effort` is not supported with this model

This kills the sourcehunt hunter loop + ranker against Llama 3.x/4, Mistral, Mixtral, Qwen2/2.5, Gemma on Groq/Together/Fireworks/etc. The default Anthropic/Claude path was never affected.

Fix — two layers, both in clearwing/llm/native.py

  1. Model-name denylist with a sentinel default. The constructor default becomes "auto"; _auto_resolve_reasoning_effort() maps known non-reasoning families (_REASONING_EFFORT_UNSUPPORTED_PATTERNS) to None and everything else — including Claude — to "medium". So the default path is byte-for-byte unchanged for Anthropic. Any explicit value (including None) bypasses auto-resolution.
  2. Retry-on-400 safety net. If a provider still rejects the param at call time, achat/achat_stream detect that specific 400 (_is_unsupported_reasoning_effort_error: must mention reasoning_effort and 400/unsupported), drop it, latch reasoning_effort=None for the session, and retry once. Unrelated 400s/429s/500s and the existing OpenAI aiohttp stream fallback are preserved.

Notes on the re-port

native.py was refactored since #47's branch (centralized provider routing, codex/aiohttp streaming fallback), so the original patch no longer applied. Re-implemented against current main:

  • achat retry wraps the _with_rate_limit_retries call.
  • achat_stream retry is layered into the existing except alongside (not replacing) the OpenAI HTTP fallback, via a small _consume(opts) helper.

Original author credited via Co-authored-by.

Verification

.venv/bin/ruff check clearwing/llm/native.py tests/test_native_reasoning_effort.py   # clean
.venv/bin/pytest tests/test_native_reasoning_effort.py tests/test_llm_openai_fallback.py -q   # 30 passed

Tests from #47 carried over verbatim (denylist resolution, constructor wiring, error classification, options rebuild, and achat/achat_stream retry-once behavior).

AsyncLLMClient defaulted reasoning_effort to "medium" and sent it on every
OpenAI-compat request unconditionally. Non-reasoning models served over
OpenAI-compat endpoints (Llama 3.x/4, Mistral, Mixtral, Qwen2/2.5, Gemma on
Groq/Together/Fireworks/etc.) reject the param with HTTP 400, which killed
the sourcehunt hunter loop and ranker for those providers.

Two layers, both in clearwing/llm/native.py:

1. Model-name denylist with a sentinel default. The constructor default
   becomes "auto": _auto_resolve_reasoning_effort() maps known non-reasoning
   model families to None and everything else (incl. Anthropic/Claude) to
   "medium" — so the default Anthropic path is unchanged. Any explicit value
   (including None) bypasses auto-resolution.

2. Retry-on-400 safety net. If a provider still rejects reasoning_effort at
   call time, achat/achat_stream detect that specific 400, drop the param,
   latch reasoning_effort=None for the session, and retry once. Unrelated
   400s/429s/500s propagate untouched.

Re-port of #47 onto the refactored native.py (provider routing + codex/
aiohttp streaming fallback landed since the original branch). Tests from the
original PR carried over verbatim.

Co-authored-by: msyed-01 <288113432+msyed-01@users.noreply.github.com>
@msyed-01

Copy link
Copy Markdown
Contributor

Thanks for the fast and thorough re-port @ropoctl, just saw this and realizing I missed the rebase window. You were fast lol. The two-layer shape is preserved exactly as designed: denylist + sentinel-default for Layer 1, retry-once with instance-state latch for Layer 2. Putting the streaming retry before the existing aiohttp fallback in achat_stream is cleaner than what I had-- nice touch. co-author credit appreciated!!

One related item that also surfaced during the end-to-end smoke test against Groq Llama 3.3 70B: the Ranker hits a second 400, this time from response_format: json_schema (since Groq Llama 3.3 doesn't support OpenAI Structured Outputs). Same shape as this fix and as #51. Was going to open a separate issue but new issues are restricted in the repo. Want me to open a Discussion for it, or somewhere else you prefer? Happy to send a follow-up PR mirroring the pattern here once this lands, if thats welcome.

@ropoctl

ropoctl commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Sorry, I probably would have waited a bit longer for your response on this. btw groq llama supports year of our lord 2026 features https://console.groq.com/docs/responses-api https://console.groq.com/docs/structured-outputs

@msyed-01

Copy link
Copy Markdown
Contributor

lmao ur so right, my bad, kept it from the reasoning_effort repro (just ended up with it cuz its free tier and hits that bug). moving to gpt-oss / llama-4-scout for json_schema runs. thnx again chief

@ropoctl

ropoctl commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

ah does free tier not allow V1 Responses (openai_resp) api?

@msyed-01

Copy link
Copy Markdown
Contributor

honestly never tested it-- was just running openai_compat on groq, never tried the openai_resp path.

@msyed-01

Copy link
Copy Markdown
Contributor

actually just tested it, free tier allows /v1/responses on both llama-3.3-70b and gpt-oss-120b. we just hadnt tried the openai_resp path.

also, something I noticed: chat-completions says reasoning_effort in the 400 but /v1/responses says reasoning.effort (with a dot). i think the _is_unsupported_reasoning_effort_error substring check in #65 might not fire on Responses API errors

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants