fix(dispatch): stop leaking untranslated reasoning notation to providers - #858
Conversation
The auto-compat projection translated client reasoning intent into the target provider's dialect (reasoning_effort, reasoning object, enable_thinking, ...) but left the untranslated OpenAI-style fields in the payload. Strict upstreams such as the Meta Model API hard-400 on unknown parameters, and requests carrying a Responses-style reasoning object failed on every target instead of being translated. Also widen matchUnsupportedParameter to accept backtick-quoted param names (Meta: "unknown parameter `reasoning`") so the reactive same-target strip-and-retry safety net engages for that error shape instead of falling straight through to target failover.
…s it Follow-up to the egress stripping fix: the zai/deepseek/default branches only deleted the reasoning object, leaving a client-sent reasoning_effort in the payload whenever the provider's thinking dialect translates intent to another field but cannot consume the effort string (supportsReasoningEffort === false or no level mapping). Deletes now run before the conditional write in each branch.
The provider adapter pane filtered reasoning_rewrite out of its checkbox grid because the rules options editor only existed in the per-model dialog. Extract the rules editor into a shared ReasoningRewriteRulesEditor component used by both ProviderModelsEditor and ProviderAdvancedEditor, and show the adapter checkbox (with its rules editor) in the provider pane. Provider-level reasoning_rewrite config was previously editable only via API.
The ant-ling branch only stripped reasoning_effort, leaving the client's reasoning object in the outbound payload whenever the intent was a disable (or otherwise inexpressible in ant-ling's dialect). Delete the untranslated notations first, then write reasoning only when an enabled effort maps — matching pi-ai's from-scratch ant-ling serialization.
This comment has been minimized.
This comment has been minimized.
…ort unknown Addresses Kody review feedback: the default OpenAI-dialect branch deleted the client's reasoning_effort unconditionally, silently dropping the intent whenever compat.supportsReasoningEffort was undefined (unknown) rather than false (provably unsupported). Only strip when support is provably absent or a translated value is written; otherwise pass the client value through
This comment has been minimized.
This comment has been minimized.
…asoning object When the client sends both a reasoning object (the authoritative intent) and a stale reasoning_effort, deleting the object while preserving the effort could reverse the intent (e.g. disabled + effort high). Strip the effort whenever a recognized reasoning object was present, and only pass reasoning_effort through unchanged when it WAS the intent source and provider support is unknown
…not a malformed value extractReasoningIntent ignores non-object reasoning values, so a malformed field (e.g. reasoning: "high") never drives the intent; reasoning_effort remains authoritative and must pass through when support is unknown. Gate the stale-effort strip on reasoning being a recognized object
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
typeof [] === 'object', so an array-valued reasoning field would previously be treated as an authoritative recognized intent source and make a valid reasoning_effort look stale. Match the extractor's record-only semantics with an explicit Array.isArray guard
| // A client-side `reasoning` object is the AUTHORITATIVE intent source | ||
| // (extractReasoningIntent checks it before reasoning_effort), so when it is | ||
| // present a leftover reasoning_effort may contradict the intent we are about | ||
| // to translate — count it as stale no matter what the branch writes. A | ||
| // malformed non-object `reasoning` is IGNORED by the extractor, so it never | ||
| // drove the intent and must not make the effort look stale. | ||
| const hadReasoningObject = |
There was a problem hiding this comment.
Narrowing the guard to != null && typeof === 'object' drops the reasoning: null case, which the extractor resolves as a fallback to request.reasoning (line 91: source.reasoning ?? request.reasoning) — a field the anthropic and gemini request parsers populate. When the payload carries reasoning: null and request.reasoning holds the authoritative intent, the default branch deletes the null and lets a contradicting reasoning_effort (e.g. 'high' against enabled: false) reach the provider, reversing the translated intent pinned by the sibling test at line 434. Make the guard consult the same fallback source as the extractor.
// Mirror extractReasoningIntent's resolution order: a null/absent payload
// `reasoning` falls back to request.reasoning, which is then authoritative.
const resolvedReasoning = next.reasoning ?? intent.reasoning;
const hadReasoningObject = resolvedReasoning != null && typeof resolvedReasoning === 'object';Prompt for LLM
File packages/backend/src/services/dispatch/dispatcher-auto-compat.ts:
Line 205 to 211:
Narrowing the guard to `!= null && typeof === 'object'` drops the `reasoning: null` case, which the extractor resolves as a fallback to `request.reasoning` (line 91: `source.reasoning ?? request.reasoning`) — a field the anthropic and gemini request parsers populate. When the payload carries `reasoning: null` and `request.reasoning` holds the authoritative intent, the default branch deletes the null and lets a contradicting `reasoning_effort` (e.g. 'high' against `enabled: false`) reach the provider, reversing the translated intent pinned by the sibling test at line 434. Make the guard consult the same fallback source as the extractor.
Suggested Code:
// Mirror extractReasoningIntent's resolution order: a null/absent payload
// `reasoning` falls back to request.reasoning, which is then authoritative.
const resolvedReasoning = next.reasoning ?? intent.reasoning;
const hadReasoningObject = resolvedReasoning != null && typeof resolvedReasoning === 'object';
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
What would you like me to do with these changes?