mirror of
https://github.com/THU-MAIC/OpenMAIC.git
synced 2026-10-13 04:50:23 +08:00
* fix: pass reasoning_content back to DeepSeek in thinking mode (multi-turn)
DeepSeek rejects multi-turn thinking-mode requests whose assistant messages
lack reasoning_content:
400 invalid_request_error:
"The `reasoning_content` in the thinking mode must be passed back to the API."
Two gaps caused it:
1. Round-trip loss. Responses fold reasoning_content into an inline
<think> block (then extractReasoningMiddleware splits it into reasoning
parts), but on the next turn the OpenAI chat adapter drops reasoning
parts. Kimi already had a preservation mechanism (marker encoding +
restore); extend it to DeepSeek, in both compatFetch and the middleware
assembly.
2. Missing field. A response that skipped reasoning (direct tool call)
produces an assistant message with no reasoning part at all. When
thinking is enabled, DeepSeek still requires the field, so inject an
empty reasoning_content for such messages.
Also: send the thinking toggle alone (no reasoning_effort) when the route
sets no explicit effort. Tool-using transports (maic-agent-driver) cannot
combine reasoning_effort with function tools; DeepSeek applies its own
default effort.
Verified: vitest (reasoning-sse, thinking-config, openai-sdk-integration),
Docker production build, and end-to-end multi-turn agent sessions with
thinking enabled (DeepSeek v4-pro + v4-flash) completing successfully.
* fix: close the DeepSeek reasoning round-trip on the agent-driver path
Review follow-up on the DeepSeek reasoning_content fix.
- One shared predicate, preservesReasoning(providerId, modelId), derived from
the resolved request adapter (deepseek + atlascloud deepseek models) plus
Kimi K3, now gates both the request-side wiring and the agent driver's
includeReasoning. The driver previously gated on kimi-k3 alone, so every
thinking block was dropped before the AI SDK saw the messages and the
middleware had nothing to encode.
- Driving the gate through the predicate covers atlascloud deepseek models,
which share the request adapter but were previously excluded by a literal
provider-id check.
- Scope the effort-less thinking toggle to requests that carry tools: a
tool-carrying request can never set reasoning_effort, while non-tool calls
keep the historical default effort.
- Inject the catalog thinking default for preserved providers so the wire
always states the mode explicitly, and gate both restore and backfill on
that resolved mode (a disabled turn carries neither markers nor field).
- Tests: DeepSeek counterparts of the Kimi K3 recorded-shape test covering
the round-trip across tool continuations, the empty backfill, the
reasoning_effort scoping, a non-preserving provider, the non-streaming
path, and the predicate itself. Verified they fail when the predicate is
disabled.
* fix: key the reasoning round-trip on the resolved thinking mode; cover the driver wire
Round-2 review follow-up.
- A disabled turn no longer ships the internal marker. The request-side step
now keys on the resolved ThinkingConfig instead of the serialized body, so a
disabled turn strips the sentinel instead of sending it verbatim; this also
covers Kimi K3, whose adapter emits reasoning_effort and no thinking object.
- Backfill an empty reasoning_content only for the DeepSeek request adapter.
- Add the missing driver-wire test: createCallLlmStreamFn with a DeepSeek turn
carries the prior thinking block as non-empty reasoning_content, and a
disabled turn strips the marker. Reverting the driver gate to the old
Kimi-K3-only check fails the first test; disabling the strip fails the second.
- Formatting: prettier clean for the touched files.
* fix: scope the effort-less thinking toggle to tool-carrying requests
Review follow-up. `options.hasTools || config.effort === undefined` also
dropped reasoning_effort for NON-tool calls that enable thinking without an
explicit effort (e.g. {enabled:true}), where the historical wire carried
reasoning_effort:'high'. Only a tool-carrying request must omit the effort
(the transport rejects function tools combined with it); every other request
keeps the explicit value or the default.
Updated the scoping test to assert reasoning_effort:'high' for the non-tool
no-effort case, and verified the assertion fails when the old condition is
restored.
* fix: derive the round-trip decision from the wire, not the requested mode
Round-3 review follow-up. Two remaining issues:
1. Kimi K3 disabled strips reasoning_content while the request still reasons.
getThinkingMode({enabled:false}) is 'disabled', but the openai-adapter body
builder emits reasoning_effort:'low' (no 'none' level exists), so thinking
was never actually off on the wire. getCompatThinkingBodyParams now also
reports disablesThinking, computed per adapter from the params it really
emits (true only where the wire carries an explicit off switch); the
round-trip gate keys on that instead of the requested config. Same fix
covers DeepSeek {effort:'none'}, which sends thinking:{type:'disabled'} but
previously went through restore.
2. Empty reasoning leaks the private marker into content. extractKimiReasoning
now reports a found flag, restore/strip act on it (marker removed in both
cases; field emitted only by restore), and the preservation middleware no
longer encodes empty reasoning parts, so the degenerate `:0:` marker is
never produced. Restore/strip share one internal helper.
Tests: Kimi K3 {enabled:false} recorded body (field kept + reasoning_effort
low present, no thinking object); DeepSeek {effort:'none'} (disabled object,
no field, no marker); driver-wire empty-thinking cases (enabled: field '' +
clean, disabled: no field + clean); Kimi K3 no-backfill pin; unit cases for
the empty marker. Each pinned by mutation: old gate fails T1+T2, serialized
absent-strip gate fails T1 plus the pre-existing Kimi test, found-revert
fails the empty-marker tests, widened backfill fails T4. Full suites green,
tsc and prettier clean.
---------
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>