Repository navigation
[Provider compatibility] command-code/mimo-v2.6-pro: tool-call markup after prose in the same text block bypasses CommandCodeToolTextFilter #5698
Description
Client or integration
Codex CLI
Provider or upstream service
command-code and opencode-go, both routing Xiaomi MiMo models (xiaomi/mimo-v2.6-pro, mimo-v2.6-pro, mimo-v2.6-flash)
OpenCodex version
2.64.0
Endpoint or capability
Tool calls over streaming (/v1/responses bridged to the provider adapters); MiMo native tool-call markup reaching the assistant text channel
Current behaviour
When the gateway cannot translate MiMo's native tool-call envelope into a structured call, the envelope reaches the client as visible assistant text. Two distinct gaps:
-
command-codeadapter:CommandCodeToolTextFilteronly holds a text block that opens with<tool_call>. The gateway echo can arrive after ordinary prose inside the same text block, so the probe mismatches on the first prose character, the block commits as ordinary text, and the markup streams to the client verbatim. A nativetool-callcarrying the same input follows, so the call executes and the raw markup also renders — a duplicate in the content channel (same shape as [Provider compatibility] opencode-go/mimo-v2.6: native XML tool-call envelope reaches visible assistant text #5499). -
openai-chatadapter (used byopencode-go): no tool-text handling at all, so the same echo always reaches the client (see [Provider compatibility] opencode-go/mimo-v2.6: native XML tool-call envelope reaches visible assistant text #5499).
Observed echo shapes: the complete envelope in a single text-delta after prose, and the variant that omits </function> (<tool_call><function=exec>RAW</parameter></tool_call>).
Expected behaviour
A recognized <tool_call> envelope is either consumed into the structured call or stripped from the content channel. It must never reach the client as assistant text, on any adapter that serves MiMo models.
Minimal redacted request or reproduction
Stream shape observed (simplified):
text-start
text-delta "ordinary prose..." <- probe mismatches, block commits as text
text-delta "<tool_call><function=exec>…</parameter></function></tool_call>"
tool-call (native, same input as the markup)
finish stop / tool_calls
Any request that leads the MiMo gateway to echo the envelope (for example the Codex freeform exec custom tool lowered to a single input string parameter) reproduces it.
Actual response or error
The client renders the raw envelope as assistant text while the native call also executes:
...ordinary prose...<tool_call><function=exec>const results = await Promise.all([
tools.exec_command({ cmd: "..." })
]);
...</parameter></function></tool_call>
Upstream documentation
No public specification exists for the command-code /alpha/generate stream or the opencode-go gateway; the MiMo envelope grammar is the one described in the vLLM and SGLang MiMo tool parsers (<tool_call><function=NAME><parameter=KEY>VALUE</parameter></function></tool_call>, freeform bodies raw without parameter tags). Cross-reference: #5499.
Suggested mapping or implementation notes
Validated local patch on 2.64.0 (both adapters):
-
CommandCodeToolTextFilter.textDelta(command-code-tool-text.ts): scan deltas forTOOL_CALL_MARKERwhenever the block state is notprobing/held. Split at the marker (index > 0, or index 0 on a non-probing block): prose keeps its queued/streamed path, the markup starts a fresh probing block and follows the normal hold/drop/restore route. Extracted the incremental probe into a helper reused by both paths. Known limitation: a marker split across deltas after prose is still released as text (the gateway echoes the whole envelope in one delta in practice). -
openai-chat.ts: for MiMo-routed requests (/(^|\/)mimo-v2/ion the routed model id), content and reasoning deltas route through the same filter. Two wire differences mattered: the chat stream has no text-start/text-end events, so the open content block must be ended explicitly before the native-call flush (otherwise the filter interrupts the held block instead of matching it), andstopReasonFor()here never returnsstop/tool_calls, so the restore verdict reads the rawfinish_reason.
Validated with an SSE replay against the real filter/adapter: prose+markup in separate deltas with a native duplicate (dropped, no leak), prose+markup clean stop (restored), prose+markup in one delta (restored), pure-markup block (restored), plain prose (unchanged), non-MiMo model (gate off, passthrough), truncated stream (released as text). Happy to open a PR.
Additional context and attachments
- [Provider compatibility] opencode-go/mimo-v2.6: native XML tool-call envelope reaches visible assistant text #5499 documents the opencode-go half of the same model behavior.
Checks
- I searched existing provider and compatibility issues.
- The request and response were redacted.
- The expected behaviour is based on an upstream specification or a concrete client requirement.
Activity
github-actions commentedon Sep 23, 2026
Issue reopened
The report now contains the information required by the automated check. Thanks for updating it.
marciodps commentedon Sep 23, 2026
Update: applied the same approach locally to the
openai-chatadapter (the oneopencode-gouses), which had no tool-text handling at all — matching the report in #5499. For MiMo-routed requests (/(^|\/)mimo-v2/ion the routed model id), content and reasoning deltas route throughCommandCodeToolTextFilter: markup is split off, held, dropped on a matching native call or restored on a cleanstop/tool_callsfinish. Two wire differences mattered: the chat stream has no text-start/text-end events, so the open content block must be ended explicitly before the native-call flush (otherwise the filter interrupts the held block instead of matching it), andstopReasonFor()here never returnsstop/tool_calls, so the restore verdict has to read the rawfinish_reason. Validated against the real adapter with an SSE replay: prose+markup+native duplicate (dropped, no leak), prose+markup clean stop (restored), plain prose (unchanged), non-MiMo model (gate off, passthrough), truncated stream (released as text).marciodps commentedon Sep 23, 2026
Both patches against 2.64.0, applied with
patch -p1from the package root. These are running in production here (same validation matrix as described above).src/adapters/command-code-tool-text.ts— mid-prose marker split intextDelta, shared probe helper, queued-prose helper:Patch (125 lines)
src/adapters/openai-chat.ts— MiMo gate inbuildRequest, filter wiring inparseStream(content/reasoning routing, native-call matching with explicit block end, finish verdict on rawfinish_reason):Patch (178 lines)
If you prefer, I can open a PR from a fork instead.
marciodps commentedon Sep 23, 2026
Follow-up with a production-only failure mode the earlier patch did not cover — observed live on 2.64.0+the patch below after deploying it: markup still leaked after interleaved stream events.
Root cause:
breakOpenBlocks()treats held blocks as interruptible. In production, reasoning deltas (and other event types routed throughenqueueEvent/boundary) interleave between the echo block and itstext-end. Each such event interrupts the held block:markupPartsis cleared, the block is markedinterrupted, and at settle it is released as text — the leak returns even though the block opened with the marker and parsed as a declared tool. Clean synthetic sequences (text block ended before anything else arrives) never hit this path, which is why the earlier validation missed it.Fix (10 lines in
breakOpenBlocks): a held block whose accumulatedmarkupPartsalready parse viaparseToolCallMarkupand name a declared tool is skipped — not interrupted, not cleared. Unresolved markup (never-closing envelopes) is still interrupted and released as text, preserving the bounded-release behavior. Held blocks remain bounded byMAX_HELD_TOOL_TEXT_BYTES.Validation matrix now includes interleaving scenarios: markup + interleaved reasoning + clean stop (restored), markup + interleaved reasoning + native duplicate (dropped, no leak), malformed
<const = "envelope variant with interleaved reasoning (restored), never-closing partial markup + interleaved reasoning (still released as text), plus all previous scenarios (plain prose, non-MiMo gate off, failed-turn release). All pass against the real filter class and the realopenai-chatadapter with SSE replay.Updated patch against pristine 2.64.0:
src/adapters/command-code-tool-text.ts (breakOpenBlocks change; earlier textDelta changes included)
The
openai-chat.tspatch from the previous comment is unchanged by this follow-up.marciodps commentedon Sep 23, 2026
Third follow-up: another production-only leak shape on 2.64.0+previous patches — the envelope can also arrive malformed, and there were two more release paths that streamed it.
Shape captured live (
command-code,xiaomi/mimo-v2.6-pro):Note the malformed parameter tag:
<parameter=with no key, no>, and no closing</parameter>— the body runs to</tool_call>.parseToolCallMarkupcorrectly rejects it, but the filter then released it as text through two paths:breakOpenBlocks(): interleaved reasoning deltas interrupted the held block beforetext-end(the earlier follow-up fixed this only for parseable envelopes).matchNative(): when the native duplicate call arrives and the strict parse fails, the block cannot match, so it was queued back as ordinary text.Fix against pristine 2.64.0 (supersedes the earlier diffs of this file):
breakOpenBlocks()no longer interrupts held blocks at all. A held block opens with the marker by construction, so it is an envelope echo with high confidence; it stays held until settle. Memory and wait stay bounded by the existing queued-byte cap (makeRoom→MAX_HELD_TOOL_TEXT_BYTES, 64 KiB), which flushes everything as text if a held envelope never resolves while the stream keeps producing.isLooseEnvelope(): opens with<tool_call>, ends with</tool_call>, names a declared function inside, no nested markers. Used in both release paths —settle()(clean finish) andmatchNative()(native duplicate arrives) — to drop malformed envelopes instead of releasing them as text. Dropping (rather than restoring) avoids double execution alongside the native call; releasing would put the raw markup back on screen, which is the reported bug.Validation (10 scenarios against the real filter class + the real
openai-chatadapter with SSE replay): the exact captured shape with interleaved reasoning and clean stop (dropped, prose intact, no restored call to avoid double execution), same shape + native duplicate (dropped, native passes, exactly one call), all previous scenarios (parseable envelopes restored/dropped, plain prose, non-MiMo gate off, failed-turn fail-open release, never-closing partial markup still released as text). All pass.src/adapters/command-code-tool-text.ts — full patch vs 2.64.0
fix(command-code): drop a malformed echo only for its own native call…
fix(command-code): keep MiMo tool-call markup after prose off the tex…
docs(command-code): describe the prose split, held envelopes and loos…
fix(command-code): drop a malformed echo only for its own native call…
fix(command-code): keep MiMo tool-call markup after prose off the tex…
docs(command-code): describe the prose split, held envelopes and loos…
fix(command-code): drop a malformed echo only for its own native call…
fix(providers): bundle L3 — MiMo, DeepSeek, Google, Command Code and …
lidge-jun commentedon Sep 24, 2026
Fixed on
devby bundle #5739 (squash-merged as a8d526f); it will ship in the next release. Closing manually becausedevis not the default branch, so GitHub does not auto-close. If you still see this on a build that includes a8d526f, please reopen with the version and a redacted reproduction.marciodps commentedon Sep 24, 2026
Thanks for the quick turnaround. The dev implementation matches what we validated locally (prose-split, held blocks left uninterruptible, loose-envelope drop keyed on the native duplicate), and the fence-aware shared module plus streaming bounds go beyond it. I will run our repro matrix against the first release that includes a8d526f and reopen with the version and a redacted reproduction if anything slips through.