fix: reasoning effort 'ultra' no longer 400s on non-Anthropic wires (#89503 class, salvage #89509) - #90330
Conversation
Hermes' internal effort vocabulary extends the wire set with ultra (documented by /reasoning as none..xhigh|max|ultra). OpenAI-compatible wires — OpenRouter chief among them — accept exactly max|xhigh|high|medium|low|minimal|none and reject the extension with HTTP 400, so an ultra configured while the default model was Anthropic worked (the Anthropic adapter maps its own levels) but leaked untranslated the moment a per-job override pinned a non-Anthropic model, failing every call for that job. The wire-compat chokepoint for this transport previously mapped ultra to max only for gpt-5.6; generalize the cap to every model.
…class) The chat_completions chokepoint fix (ultra->max for every model, cherry-picked from #89509) has siblings with the same bug shape: - codex.py: ultra->max was gated on gpt-5.6 only; now baseline for all Responses-API models (backend-specific branches still override). - Kimi top-level reasoning_effort: K3 accepts low/high/max only — 'medium' and upper-ladder levels were dropped to the medium default (400s on K3, ladder inversion on K2). Full ladder mapped per family, mirroring the kimi-coding plugin's K3 map. - TokenHub: 'minimal' fell through to the 'high' default (asked least, got most); full ladder now mapped onto low/medium/high. - auxiliary_client Responses path: ultra->max alongside the existing minimal->low clamp. - custom provider plugin: ultra capped at max instead of forwarded verbatim to GLM/vLLM/SGLang backends that reject it. - copilot plugin: ad-hoc downgrade rules replaced with the shared clamp_reasoning_effort_to_supported ladder walk so ultra/max resolve to the strongest supported level instead of medium (#74295). Sabotage-verified: new sibling-site tests fail 6/10 without the fixes.
૮ >ﻌ< ა ci reviewran on 952e0d9 — fix: widen reasoning-effort wire translation to sibling site
|
andrexibiza
left a comment
There was a problem hiding this comment.
Post-merge root-cause review
Verdict
#90330 is a valid hotfix and should remain merged. Its claim that the “whole effort-vocabulary bug class is fixed at every translation site” is not supportable. It closes the known ultra leaks and several reasoning-ladder inversions. It does not remove the architecture that produced them.
Merged as 9ec5750aca33bf6e977cfde468868b0f0d66e339 from reviewed head 952e0d9330ee0fd73b35e134e48571d8bcde3a7f.
Root cause
The root cause is an uncompiled semantic intent crossing a late-bound provider boundary.
Hermes has a global, user-facing reasoning ladder:
none < minimal < low < medium < high < xhigh < max < ultra
But reasoning_config is represented as a largely provider-agnostic dictionary whose effort string is also treated as though it were already a valid wire value. Provider and model selection can change after that configuration is created—through cron overrides, fallback, auxiliary routing, MoA/delegation, or per-turn model selection.
There is no mandatory final step equivalent to:
compile_reasoning(
requested_intent,
final_provider,
final_model,
final_transport,
final_endpoint,
)Instead, each transport and provider profile independently chooses among:
- passing the value through;
- applying a local lookup table;
- recognizing a handful of special cases;
- substituting an arbitrary default when the value is unknown.
That creates two failure modes:
- Hard incompatibility: an internal value such as
ultrareaches a provider that accepts onlymax|xhigh|high|medium|low|minimal|none, producing HTTP 400. - Semantic inversion: an unsupported strong or weak level falls to an unrelated default—for example,
ultra → mediumorminimal → high—so the effective ordering contradicts the user-visible ordering.
The linked incidents exhibit both forms:
- #89503: a cron provider override reached OpenRouter with an invalid reasoning value and failed immediately, while Anthropic worked because it uses a different translation path.
- #70058: GLM rejected
ultra, after which Hermes could silently move the session to Claude through provider fallback. - #74295: Copilot mapped the strongest Hermes setting,
ultra, tomedium, making it weaker than explicitly selectinghigh.
Cron was the trigger, not the cause. OpenRouter and GLM were schema enforcers, not the cause. The defect was created locally before the request left Hermes.
Introducing change
The latent architecture predates the incident, but #62650 activated the class at ecosystem scale.
That PR promoted max and ultra into the shared contract across CLI, gateway, dashboard, desktop, delegation, batch processing, and provider routes. Its compatibility mechanism, however, normalized ultra → max only when the model name matched GPT-5.6.
This was effectively a schema migration treated as a feature addition:
- The producer vocabulary was widened globally.
- Existing consumers were not required to prove total handling of the widened enum.
- No registry-level conformance test failed when a consumer had no mapping for the new values.
- Validation sampled known happy paths instead of proving the provider × transport × effort cross-product.
The live proof attached to #62650 consisted of GPT-5.6 calls through OpenRouter and OpenAI direct. Those correctly emitted max, but did not test a non-GPT provider, late provider switch, custom endpoint, auxiliary route, or unsupported-level behavior.
So the process-level root cause is:
A globally shared enum was widened without a total-consumer migration gate.
What #90330 fixed correctly
The merged patch is broader than the original report and materially improves correctness:
- Generalizes Chat Completions
ultra → max. - Adds explicit full-ladder mappings for Kimi and TokenHub.
- Generalizes Codex/Responses
ultra → max. - Adds the missing auxiliary Responses clamp.
- Replaces Copilot’s ad hoc downgrade cases with the shared ordered-ladder helper.
- Prevents
customproviders from forwardingultraverbatim. - Adds regression tests for the general Chat Completions normalization and selected sibling sites.
The Copilot change is especially sound: it moves away from individually enumerated exceptions and toward an ordered semantic ladder. That directly repairs the monotonicity defect identified in #74295.
This should be understood as correct incident mitigation plus a substantial known-site sweep.
Why it does not close the class
1. Provider capabilities still are not first-class
ProviderProfile accepts a raw reasoning_config and exposes provider-specific hooks, but it does not declare a common reasoning capability contract: supported levels, wire field, disable semantics, model-specific discovery, or unsupported-value policy.
Consequently, every provider remains free to reinvent translation. Nous and Vercel AI Gateway still perform broad passthrough, while OpenRouter independently performs catalog-driven clamping. OpenRouter’s behavior is a good local implementation, but it remains local rather than constituting the system contract.
2. “Wire-compatible” is still not target-compatible
The new _reasoning_config_for_model() treats all values through max as generic wire-native values and only rewrites ultra. Its regression test deliberately asserts that none through max pass through for "any/model".
But this PR itself demonstrates that the abstraction is insufficient:
- Kimi K3 accepts only
low|high|max. - TokenHub accepts only
low|medium|high. - Copilot support is model-catalog-dependent.
- Some providers use binary thinking toggles rather than effort strings.
A value can therefore be valid in some “OpenAI-compatible” schema and still be invalid for the actual provider/model selected.
3. Custom endpoints remain unknowable without an explicit contract
The custom profile intentionally represents arbitrary Ollama, vLLM, llama.cpp, GLM, and other user-supplied endpoints. Their supported vocabularies are not uniform.
Changing ultra → max prevents one known invalid token. It cannot prove that max, xhigh, minimal, or explicit none is valid on an arbitrary endpoint.
For this route, the system needs one of:
- explicit user/provider capability metadata;
- trustworthy endpoint/model discovery;
- a conservative omission policy;
- or a local preflight error explaining that the requested intent cannot be represented.
Blanket passthrough with one exception cannot close the class.
4. Requested intent is erased before effective behavior is known
A global early rewrite from ultra to max discards the distinction between:
requested = ultra
effective = max
reason = target ceiling
Downstream code sees only max. That prevents accurate diagnostics, UI disclosure of adaptation, telemetry comparing requested and effective behavior, support for a future provider with a native ultra-equivalent tier, and auditing whether a fallback or model switch changed execution semantics.
A proper compiler must preserve both requested and effective values.
5. Deterministic local payload failures are still treated like provider failures
#70058 documents the operational consequence: Hermes generated an invalid request, retried it, and could then activate provider fallback.
That is the wrong failure domain. A deterministic locally generated schema error should not:
- retry the same invalid payload;
- count against provider health;
- rotate credentials;
- silently switch models/providers;
- obscure the original construction defect.
The still-open #34794 adds reactive classification and same-provider adaptation for rejected reasoning parameters. That is useful containment, but because it adapts only after a rejected request, it is not a substitute for correct compilation before transmission.
Verification gaps
The test count is useful but not equivalent to route-complete proof.
- The new Kimi test drives the transport’s legacy
is_kimi=Truepath, while registered Kimi providers normally useKimiProfile. The profile implementation has the correct full map, but existing profile coverage checks only thehighcase rather than the complete ladder. - The custom-profile regression suite parametrizes through
maxand does not directly assert the newly changedultra → maxbehavior. - Existing Copilot tests cover
xhigh,minimal, and unrecognized input, but do not directly pinultra/maxmonotonicity across catalog ceilings. - There is no single end-to-end matrix proving that the same final target gets the same effective reasoning behavior through main chat, cron, auxiliary calls, delegation/MoA, and fallback.
These are acceptance gaps, not evidence that the merged implementation is presently broken.
Class-closing architecture
There is already a canonical anchor: #34968 — “User-Centric Reasoning Architecture with Intelligent Adaptation.” It identifies the absence of capability awareness and proposes the correct three-layer division: user intent, provider capability, and adapter/compiler. It should be refreshed and interlocked with #62650, #90330, #89503, #70058, #74295, and #34794 rather than duplicated.
The minimum durable design is:
@dataclass(frozen=True)
class ReasoningIntent:
enabled: bool | None # None = unset, False = explicitly disabled
level: ReasoningLevel | None
@dataclass(frozen=True)
class ReasoningCapabilities:
supported_levels: tuple[ReasoningLevel, ...] | None
wire_shape: WireShape
disable_semantics: DisableSemantics
unknown_level_policy: UnknownLevelPolicy
source: CapabilitySource
@dataclass(frozen=True)
class CompiledReasoning:
requested: ReasoningIntent
effective_level: ReasoningLevel | None
wire_kwargs: dict
wire_extra_body: dict
adaptation_reason: str | NoneCompilation must occur after final provider, model, transport, base URL, and route are resolved. Cron, main chat, fallback, auxiliary calls, MoA, and delegation must all call the same compiler.
The permanent acceptance invariants are:
- Totality: every global reasoning level has an explicit policy for every capability family.
- Membership: every emitted wire value belongs to the final target’s supported set.
- Monotonicity: increasing requested effort never decreases effective effort.
- Path consistency: identical final targets compile identically across main, cron, auxiliary, delegate, MoA, and fallback paths.
- Unset/disabled separation: omission never silently replaces explicit
none. - Provenance: requested and effective values remain separately observable.
- Migration enforcement: adding a global reasoning level fails CI until all capability families are total over it.
- Local-fault containment: invalid locally constructed parameters do not trigger identical retries, provider-health penalties, or silent cross-provider fallback.
Disposition
Keep #90330 merged. It is a good repair.
Do not describe the reasoning-effort class as closed. The accurate claim is:
Known
ultraleakage and known ladder inversions were repaired across the audited translation sites.
Promote and refresh #34968 as the architectural closure vehicle. #34794 belongs underneath it as reactive containment. The class is closed only when raw reasoning intent can no longer cross a final provider boundary without one typed, capability-aware compilation step and cross-path conformance proof.
Summary
reasoning_effort: ultrano longer 400s on non-Anthropic wires — the whole effort-vocabulary bug class is fixed at every translation site, not just the DeepSeek/OpenRouter one reported in #89503.Root cause:
ultrais Hermes-internal ladder vocabulary (VALID_REASONING_EFFORTS), but the wire translation was per-vendor patchwork — the chat-completions chokepoint and the Codex transport only translatedultra → maxfor gpt-5.6, and several per-provider maps either dropped unknown levels to a weak default (inverting the ladder: the strongest ask resolved weaker than an explicithigh) or forwarded them verbatim (guaranteed HTTP 400).Changes
agent/transports/chat_completions.py_reasoning_config_for_model():ultra → maxfor every model on this transport (cherry-picked from fix(agent): cap the ultra reasoning level at the wire vocabulary #89509 by @liuhao1024, with tests). This alone fixes the reported Portal dsv4-pro-0813 400 (expected one of "max"|"xhigh"|…).agent/transports/chat_completions.pyKimi path: K3 acceptslow/high/maxonly — the old code forwarded only{low,medium,high}and dropped everything else tomedium(400s on K3 formedium, ladder inversion forultra). Full ladder now mapped per family (K3 map mirrors the kimi-coding plugin; K2-era caps upper levels athigh).agent/transports/chat_completions.pyTokenHub path: full ladder mapped ontolow/medium/high;minimalpreviously fell through to thehighdefault (asked least, got most).agent/transports/codex.py:ultra → maxbaseline for all Responses-API models (was gpt-5.6-gated); xAI/Actual branches still override with their narrower ceilings.agent/auxiliary_client.pyResponses path:ultra → maxbeside the existingminimal → lowclamp.plugins/model-providers/custom:ultracapped atmaxinstead of forwarded verbatim to GLM/vLLM/SGLang endpoints.plugins/model-providers/copilot: ad-hoc downgrade rules replaced with the sharedclamp_reasoning_effort_to_supportedladder walk —ultra/maxnow resolve to the strongest catalog-supported level instead ofmedium(Copilot route: reasoning_effort 'ultra' clamps to 'medium', making the strongest setting weaker than 'high' #74295).Sites audited and already correct (no change): Anthropic adapter (
ADAPTIVE_EFFORT_MAP), openrouter plugin (catalog-driven clamp), LM Studio (_LM_EFFORT_CLAMP), deepseek/zai/opencode-zen/ollama-cloud/meta-ai/upstage plugins, Gemini thinkingLevel mapping.Validation
ultraon Portal/OpenRouter chat-completionsmaxon the wireultraon Kimi K3mediumdefaultmaxmediumon Kimi K3highultraon Copilot (cataloglow..high)medium(ladder inversion)highminimalon TokenHubhigh(inversion)low195 targeted tests pass (transports + copilot/custom/kimi profiles). New sibling-site test file sabotage-verified: 6/10 tests fail with the fixes reverted.
Fixes #89503, #70058, #74295. Salvages #89509 (@liuhao1024, cherry-picked with authorship).
Infographic