Skip to content

fix(llm): OpenRouter reasoning-off — use effort=minimal (gpt-5 compat) - #769

Open
emooreatx wants to merge 2 commits into
mainfrom
fix/openrouter-reasoning-effort-minimal
Open

fix(llm): OpenRouter reasoning-off — use effort=minimal (gpt-5 compat)#769
emooreatx wants to merge 2 commits into
mainfrom
fix/openrouter-reasoning-effort-minimal

Conversation

@emooreatx

Copy link
Copy Markdown
Contributor

Summary

OpenRouter rejects reasoning.enabled=false for reasoning-mandatory
models like openai/gpt-5 with HTTP 400:

"Reasoning is mandatory for this endpoint and cannot be disabled."

Replace the OpenRouter branch of _build_reasoning_off_extras to send
reasoning.effort=minimal instead. This is the universal key — works
across reasoning-mandatory, reasoning-capable, and non-reasoning models.

Discovery context

Found while running the RATCHET 5-vendor CRCv2 extension smoke test
(CIRISAI/RATCHET workflow crcv2_5vendor.yml).
The openai/gpt-5 sweep ran 49 minutes without producing a single batch
because every chain was hitting the API-level 400 error and
qa_runner was silently retrying. The Claude Sonnet 4.6 sweep completed
fine in 17m.

Verification

Direct OpenRouter API calls against openai/gpt-5 (2026-05-16):

POST /api/v1/chat/completions
  body: { reasoning: { enabled: false }, ... }
  → 400 "Reasoning is mandatory for this endpoint and cannot be disabled."

POST /api/v1/chat/completions
  body: { reasoning: { effort: "minimal" }, ... }
  → 200, content="4", reasoning_tokens=0, cost=\$0.0002

reasoning.effort=minimal is honored by:

  • reasoning-mandatory models (openai/gpt-5, etc.) → emit 0 reasoning tokens (desired)
  • reasoning-capable models (openai/gpt-5-chat, claude-sonnet-4.6, etc.) → treats as lowest-effort
  • non-reasoning models (qwen, llama, etc.) → silently ignored

Backwards compatibility

The existing 3-vendor CRCv2 cohort (Gemini 2.5-Flash, Llama-4-Scout,
Qwen-3.5-35B) all use models that ignore the effort key, so re-running
them produces identical behavior. The change only affects models where
OpenRouter previously returned 400.

Tests

  • Updated test_openrouter_carries_reasoning_enabled_false
    test_openrouter_carries_reasoning_effort_minimal
  • Updated test_openrouter_includes_provider_config assertion to match
  • All 19 reasoning/openrouter tests pass locally

Test plan

  • Tests pass: pytest tests/ciris_engine/logic/services/runtime/llm_service/test_llm_service_coverage.py -k "openrouter or reasoning"
  • Verify the RATCHET 5-vendor smoke test passes for openai/gpt-5 once this lands and the SHA is bumped in crcv2_5vendor.yml

Process note

This change was initially pushed directly to main (commit 897bcdf24)
before the PR-only workflow was honored. That commit has been reverted
on main (66fbb2694), and this PR re-opens the same change through
the correct review path. Sorry for the noise.

🤖 Generated with Claude Code

…false

OpenRouter rejects `reasoning.enabled=false` for reasoning-mandatory
models like openai/gpt-5 with HTTP 400:

  "Reasoning is mandatory for this endpoint and cannot be disabled."

Verified via direct API call against openai/gpt-5 (2026-05-16):
  POST /api/v1/chat/completions with reasoning.enabled=false → 400
  POST /api/v1/chat/completions with reasoning.effort="minimal" → 200,
    reasoning_tokens=0, cost $0.0002 on a trivial prompt.

Replace the OpenRouter branch of _build_reasoning_off_extras to send
`reasoning.effort=minimal` instead. This key is honoured by:

  * reasoning-mandatory models (openai/gpt-5, etc.): emit 0 reasoning
    tokens — the desired behaviour. The model still routes through the
    reasoning endpoint but produces no reasoning content.
  * reasoning-capable models (openai/gpt-5-chat, anthropic/claude-sonnet-4.6
    in thinking mode, etc.): treats as lowest-effort setting.
  * non-reasoning models (qwen, llama, etc.): silently ignored.

Discovered while running the RATCHET 5-vendor CRCv2 extension smoke
test (CIRISAI/RATCHET workflow crcv2_5vendor.yml). The openai/gpt-5
sweep was failing every chain at the API layer with 400 errors,
masquerading as "slow" because qa_runner retries on 4xx errors.

Update test_openrouter_carries_reasoning_enabled_false →
test_openrouter_carries_reasoning_effort_minimal to reflect the new
expected payload. Update test_openrouter_includes_provider_config's
assertion accordingly. All 19 reasoning/openrouter tests pass.

This is a backwards-compatible fix at the LLM-routing level — the
existing 3-vendor CRCv2 cohort (Gemini, Llama, Qwen) all use models
that ignore the `effort` key, so re-running them produces identical
behaviour.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
emooreatx added a commit to CIRISAI/RATCHET that referenced this pull request May 17, 2026
…ndatory 400)

Direct OpenRouter API testing (2026-05-16) revealed that openai/gpt-5
is reasoning-mandatory and rejects {"reasoning":{"enabled":false}}
(the existing CIRISAgent dispatch) with HTTP 400:

  "Reasoning is mandatory for this endpoint and cannot be disabled."

Result of the first smoke run (workflow run 25975925148): the
openai/gpt-5 sweep produced 0 batches in 49 minutes — qa_runner was
silently retrying against a 400 for every chain. Claude Sonnet 4.6
sweep completed cleanly in 17m on the same workflow.

Tested chat / non-reasoning-mandatory variants against the existing
CIRISAgent dispatch (POST /api/v1/chat/completions with
reasoning.enabled=false):

  openai/gpt-5-chat   → 400 (data-policy/guardrail; this account no access)
  openai/gpt-5.4      → 200, content="4", reasoning_tokens=0  ✓
  openai/gpt-5.4-mini → 200, content="4", reasoning_tokens=0  ✓
  openai/gpt-5.3-chat → 200, content="4", reasoning_tokens=0  ✓

Swap the workflow + script + run plan to openai/gpt-5.4 — latest
non-reasoning-mandatory GPT-5 variant, accepts CIRISAgent's existing
dispatch, emits 0 reasoning tokens.

Pricing differential (per OpenRouter rate card 2026-05):
  openai/gpt-5     prompt $1.25/M output $10/M
  openai/gpt-5.4   prompt $1.75/M output $14/M

~40% higher per-token cost but still well inside the $75/cell budget.
Expected smoke spend on the 5.4 variant: ~$3 (1 iter × 14 chains).

Files changed:
  - experiments/exp1b_boundary_active/run_crossfamily_5vendor.sh
      MODELS array → openai/gpt-5.4
  - .github/workflows/crcv2_5vendor.yml
      preflight matrix → openai/gpt-5.4 (model_tag openai-gpt-5.4)
  - experiments/exp1b_boundary_active/RUN_PLAN_5VENDOR_CRCV2.md
      slug + a "slug note" paragraph documenting why

This is a workaround, not a permanent fix. The architectural fix
(CIRISAI/CIRISAgent#769) sends reasoning.effort=minimal instead of
reasoning.enabled=false, which works across all OpenRouter reasoning
modes including the mandatory one. Once #769 merges + AGENT_SHA is
bumped, openai/gpt-5 (no suffix) becomes usable again; we may or may
not want to re-run with it for completeness.

Both files lint clean (bash -n; yaml.safe_load).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 909fa10fb8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Verified 2026-05-16 via direct OpenRouter API call against
# `openai/gpt-5` (returns content, reasoning_tokens=0, cost ~$0.0002
# for a trivial prompt).
return {"reasoning": {"effort": "minimal"}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve true reasoning-off behavior on OpenRouter

_build_reasoning_off_extras is called from the reasoning-off path, but this branch now sends {"reasoning": {"effort": "minimal"}}, which the code comment itself describes as only the lowest-effort setting for reasoning-capable models (not disabled). For any OpenRouter reasoning-capable model that previously accepted reasoning.enabled=false, this change can re-enable reasoning tokens, increasing latency/cost and violating the function’s disable contract; the model-specific fallback should only use effort=minimal where enabled=false is rejected (e.g., reasoning-mandatory models).

Useful? React with 👍 / 👎.

…nabled=false elsewhere

Addresses codex P2 on PR #769. The previous version universally swapped
reasoning.enabled=false → reasoning.effort=minimal on OpenRouter, but
effort=minimal is the *lowest-effort* setting on reasoning-capable models,
not a disable. That re-enables reasoning tokens on claude-sonnet-4.6,
openai/gpt-5-chat, deepseek, etc. — violating the reasoning-off contract
and adding latency/cost.

Correct behavior:
- Default OpenRouter path keeps reasoning.enabled=false (universal disable).
- gpt-5 reasoning family (openai/gpt-5{,-mini,-nano}) takes effort=minimal
  because OpenRouter rejects enabled=false there with 400 "Reasoning is
  mandatory for this endpoint and cannot be disabled."
- openai/gpt-5-chat is reasoning-capable, not mandatory — stays on
  enabled=false alongside the other reasoning-capable models.

Tests:
- openrouter_default_carries_reasoning_enabled_false
- openrouter_gpt5_uses_effort_minimal (gpt-5/gpt-5-mini/gpt-5-nano)
- openrouter_gpt5_chat_stays_on_enabled_false
- openrouter_includes_provider_config asserts enabled=false for gpt-4o

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@emooreatx

Copy link
Copy Markdown
Contributor Author

Addressed codex P2 in d5b78aa.

Default OpenRouter path stays on reasoning.enabled=false so reasoning-capable models (claude-sonnet-4.6, openai/gpt-5-chat, deepseek, …) keep emitting zero reasoning tokens. Only the gpt-5 reasoning family (openai/gpt-5, -mini, -nano) gets the effort=minimal fallback, since OpenRouter rejects enabled=false there with 400. openai/gpt-5-chat is reasoning-capable rather than mandatory, so it stays on the universal disable.

New tests: openrouter_default_carries_reasoning_enabled_false, openrouter_gpt5_uses_effort_minimal, openrouter_gpt5_chat_stays_on_enabled_false. 21 reasoning/openrouter tests pass.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
50.0% Coverage on New Code (required ≥ 80%)
B Maintainability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

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.

1 participant