From 6da6d1894c48601e7e28c89d98b92a4131589ce1 Mon Sep 17 00:00:00 2001 From: shirruy Date: Fri, 16 Oct 2026 20:37:05 -0400 Subject: [PATCH 01/12] fix(chat): recover standalone MiMo bare blocks as synthetic freeform calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MiMo V2 Chat models (dotted mimo-v2.* IDs) can emit an entire freeform call only as a standalone bare block with no structured delta.tool_calls payload. The client then rendered the raw markup as visible assistant text while the harness executed it client-side, producing unreadable output (#7214 follow-up). When no structured call exists, qualifying bare blocks that name a declared freeform tool's wire name are now recovered as synthetic input calls, streamed and buffered. A block qualifies only with a non-empty body free of nested parameter wrappers and nested block headers — the same safety rules as the existing empty-input recovery — so prose, fenced examples, undeclared tools, other models, and hyphenated MiMo IDs keep the original no-recovery behavior. ADR-5724 follow-up and chat-compat contract updated. --- src/adapters/openai-chat.ts | 36 ++++++++++- .../serialized-tool-call-content.ts | 34 ++++++++++ .../ADR-5724-serialized-tool-call-content.md | 1 + structure/providers/chat-compat.md | 6 ++ ...-chat-serialized-tool-call-content.test.ts | 62 +++++++++++++++++++ 5 files changed, 138 insertions(+), 1 deletion(-) diff --git a/src/adapters/openai-chat.ts b/src/adapters/openai-chat.ts index 2badf4772f1..92b28a6fb25 100644 --- a/src/adapters/openai-chat.ts +++ b/src/adapters/openai-chat.ts @@ -47,7 +47,7 @@ import { messagesToChatFormat } from "./openai-chat/messages"; import { withOpenAIChatToolNames } from "./openai-chat/tool-name-registry"; import { openAIChatTransport, stripBracketedModelSuffix } from "./openai-chat/wire"; import { toolChoiceToChatFormat, toolsToChatFormatForProvider } from "./openai-chat/tool-schema"; -import { freeformToolsByWireName, type FreeformToolIdentity, reconcileSerializedToolCallEvents, reconcileStructuredToolCall, reconcileStructuredToolCalls, SerializedToolCallContentBuffer } from "./openai-chat/serialized-tool-call-content"; +import { freeformToolsByWireName, type FreeformToolIdentity, reconcileSerializedToolCallEvents, reconcileStructuredToolCall, reconcileStructuredToolCalls, recoverStandaloneBareBlocks, SerializedToolCallContentBuffer } from "./openai-chat/serialized-tool-call-content"; export { stripBracketedModelSuffix } from "./openai-chat/wire"; export { buildOpenAIChatPassthroughRequest } from "./openai-chat/passthrough"; @@ -338,6 +338,27 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd // Held markup is released only now, as one batch per response: the doubled-input repair needs every call. const references = reconcileStructuredToolCalls(calls.map(call => ({ wireName: call.name, restoredName: toolNames.restore(call.name), argumentsText: call.args, freeformTool: freeformTools.get(call.name) })), toolCallContent.current(), !toolCallContent.releasedAnswerText && /(?:^|[/-])mimo-v2(?:\.|$)/i.test(lastRequestedModelId ?? "")); calls.forEach((call, index) => { call.args = references[index]!.argumentsText; }); + // MiMo can put a whole freeform call only in the visible text and never send the + // structured `delta.tool_calls` payload (#7214 follow-up). Recovered bare blocks + // that name a declared freeform tool become synthetic calls so the client executes + // one call instead of rendering raw markup. + if (calls.length === 0 && /(?:^|[/-])mimo-v2(?:\.|$)/i.test(lastRequestedModelId ?? "")) { + const recovered = recoverStandaloneBareBlocks(toolCallContent.current(), freeformTools); + if (recovered.length > 0) { + const syntheticReferences = recovered.map(block => ({ + names: new Set([block.name]), + argumentsText: JSON.stringify({ input: block.input }), + freeformTool: freeformTools.get(block.name), + })); + yield* toolCallContent.drain(syntheticReferences); + for (const block of recovered) { + yield { type: "tool_call_start", id: `call_${++toolCallSeq}`, name: toolNames.restore(block.name) }; + yield { type: "tool_call_delta", arguments: JSON.stringify({ input: block.input }) }; + yield { type: "tool_call_end" }; + } + return "continue"; + } + } yield* toolCallContent.drain(references); for (const call of calls) { if (!call.id) call.id = `call_${++toolCallSeq}`; @@ -802,6 +823,19 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd events.push({ type: "tool_call_start", id, name: toolNames.restore(name) }, delta, { type: "tool_call_end" }); } } + // Same MiMo standalone-bare-block recovery as the streamed path: with no structured + // payload, qualifying blocks become synthetic calls so the markup is suppressed once. + if (structuredCalls.length === 0 && /(?:^|[/-])mimo-v2(?:\.|$)/i.test(lastRequestedModelId ?? "")) { + const recovered = recoverStandaloneBareBlocks(answerText, freeformTools); + if (recovered.length > 0) { + recovered.forEach((block, blockIndex) => { + const id = `call_${blockIndex + 1}`; + const delta: Extract = { type: "tool_call_delta", arguments: JSON.stringify({ input: block.input }) }; + structuredCalls.push({ wireName: block.name, restoredName: toolNames.restore(block.name), argumentsText: delta.arguments, freeformTool: freeformTools.get(block.name), delta }); + events.push({ type: "tool_call_start", id, name: toolNames.restore(block.name) }, delta, { type: "tool_call_end" }); + }); + } + } const references = reconcileStructuredToolCalls(structuredCalls, answerText, /(?:^|[/-])mimo-v2(?:\.|$)/i.test(lastRequestedModelId ?? "")); structuredCalls.forEach((call, index) => { call.delta.arguments = references[index]!.argumentsText; }); reconcileSerializedToolCallEvents(events, contentStart, contentEnd, references, budget); diff --git a/src/adapters/openai-chat/serialized-tool-call-content.ts b/src/adapters/openai-chat/serialized-tool-call-content.ts index 48a7bca52e2..cd2f2fb6903 100644 --- a/src/adapters/openai-chat/serialized-tool-call-content.ts +++ b/src/adapters/openai-chat/serialized-tool-call-content.ts @@ -42,6 +42,40 @@ export function freeformToolsByWireName tool.freeform).map(tool => [wireName(tool), { name: tool.name, namespace: tool.namespace }] as const) ?? []); } +/** One bare block recovered as an executable call when MiMo sends no structured payload. */ +export interface RecoveredBareBlock { + name: string; + input: string; +} + +/** + * Bare blocks a declared freeform tool can own, for the observed MiMo shape where the + * whole call lives only in the visible text and `delta.tool_calls` never arrives. + * A block qualifies only when its function name matches a declared freeform tool's + * wire name and its body survives the same safety rules as the empty-input recovery: + * non-empty, no nested parameter wrappers, no nested block headers. Fenced examples, + * prose-only text, and blocks naming undeclared tools return nothing, so ordinary + * commentary cannot lose bytes. + */ +export function recoverStandaloneBareBlocks( + text: string, + freeformTools: ReadonlyMap, + context: TextContext = { fence: null, lineStart: true }, +): RecoveredBareBlock[] { + if (freeformTools.size === 0) return []; + return callsIn(text, context).flatMap(call => { + if (!freeformTools.has(call.name)) return []; + const wrappedBody = freeformBody(call.body); + const hasParameterOpener = wrappedBody.startsWith("/.test(wrappedBody)) + || body.includes("")) return []; + return [{ name: call.name, input: body }]; + }); +} + export interface StructuredToolCallReference { names: ReadonlySet; argumentsText: string; diff --git a/structure/decisions/ADR-5724-serialized-tool-call-content.md b/structure/decisions/ADR-5724-serialized-tool-call-content.md index 923b74900d1..e7df2a214e3 100644 --- a/structure/decisions/ADR-5724-serialized-tool-call-content.md +++ b/structure/decisions/ADR-5724-serialized-tool-call-content.md @@ -11,3 +11,4 @@ - Choice: Read each block by delimiter scan. The first `` preceded by `` closes the block; if none appears before the next block header at the start of a line (`` followed by `` does. A stray `` before the close is markup, and one leading newline in the body is template layout. - Why: It accepts the same grammar on both MiMo routes, keeps a body that contains literal tool-call tags (even a full header) intact, and costs linear time. The agreement rule from ADR-5548 is unchanged, so no new text can disappear without a matching structured call. - Consequences: The two echo shapes are removed when they duplicate a structured call, streamed and buffered. Markup with no structured call is still shown and still runs nothing. Streaming retention scans each new delta with fixed delimiter overlap and carries trailing-prose state; it never searches the whole retained block per delta. See [ADR-0102](ADR-0102-incremental-stream-accounting.md). +- Follow-up (261016): MiMo V2 Chat models (`mimo-v2` and dotted `mimo-v2.*` IDs) can emit the whole freeform call only as a standalone bare block with no structured `delta.tool_calls` payload; the client then renders raw markup as visible text (#7214 follow-up). When no structured call exists, qualifying bare blocks that name a declared freeform tool's wire name are recovered as synthetic `input` calls, streamed and buffered. A block qualifies only with a non-empty body free of nested parameter wrappers and nested block headers — the same safety rules as the empty-input recovery — so prose, fenced examples, undeclared tools, and hyphenated MiMo IDs keep the original match-only behavior. diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md index 082ac326a82..9baf59fbde6 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -345,6 +345,12 @@ prefix, and wrapper's `input` value all agree. Mismatched markup and arguments r MiMo V2 Chat IDs of `mimo-v2` or `mimo-v2.*` can send `{}` for a freeform call and put its input in one bare block; hyphenated IDs (`mimo-v2-pro`, `mimo-v2-omni`) are excluded. With no other text, one call and an exact wire-tool match, the adapter restores `input` and removes the block. Prose, fences, ordinary functions, multiple calls/blocks, and nonempty arguments remain inert. A malformed ` "unused").size).toBe(0); }); + +test.each([ + { label: "prose before the block", content: "I will run the command now.\ntext(1)" }, + { label: "prose after the block", content: "text(1)\nDone." }, + { label: "block only", content: "text(1)" }, +] as const)("a standalone MiMo bare block with no structured call does not leak markup ($label)", async ({ content }) => { + const adapter = createOpenAIChatAdapter(provider); + adapter.buildRequest({ modelId: "mimo-v2.6-pro", stream: false, options: {}, context: { + messages: [{ role: "user", content: "run it", timestamp: 0 }], tools: [execTool], + } }); + const events = await adapter.parseResponse!(Response.json({ + choices: [{ message: { content }, finish_reason: "stop" }], + }), createTestTranslatorBudget()); + const visibleText = events.filter(event => event.type === "text_delta").map(event => event.text).join(""); + expect(visibleText).not.toContain(""); + expect(visibleText).not.toContain(""); +}); + +test("a standalone MiMo bare block is recovered as an executable tool call", async () => { + const script = "const r = await tools.exec_command({cmd:'pwd'}); text(r.output);"; + const adapter = createOpenAIChatAdapter(provider); + adapter.buildRequest({ modelId: "mimo-v2.6-pro", stream: false, options: {}, context: { + messages: [{ role: "user", content: "run it", timestamp: 0 }], tools: [execTool], + } }); + const events = await adapter.parseResponse!(Response.json({ + choices: [{ message: { content: `Starting.\n${script}` }, finish_reason: "stop" }], + }), createTestTranslatorBudget()); + expect(events.filter(event => event.type === "text_delta").map(event => event.text).join("")).toBe("Starting.\n"); + expect(events.filter(event => event.type === "tool_call_delta")).toEqual([ + { type: "tool_call_delta", arguments: JSON.stringify({ input: script }) }, + ]); + expect(events.filter(event => event.type === "tool_call_end")).toHaveLength(1); +}); + +test.each([ + { label: "another model", model: "other-model" }, + { label: "a hyphenated MiMo ID", model: "mimo-v2-pro" }, +] as const)("standalone bare-block recovery stays disabled for $label", async ({ model }) => { + const block = "text(1)"; + const adapter = createOpenAIChatAdapter(provider); + adapter.buildRequest({ modelId: model, stream: false, options: {}, context: { + messages: [{ role: "user", content: "run it", timestamp: 0 }], tools: [execTool], + } }); + const events = await adapter.parseResponse!(Response.json({ + choices: [{ message: { content: block }, finish_reason: "stop" }], + }), createTestTranslatorBudget()); + expect(events.filter(event => event.type === "text_delta").map(event => event.text).join("")).toBe(block); + expect(events.filter(event => event.type === "tool_call_start")).toEqual([]); +}); + +test("standalone bare-block recovery ignores blocks naming undeclared tools", async () => { + const block = "text(1)"; + const adapter = createOpenAIChatAdapter(provider); + adapter.buildRequest({ modelId: "mimo-v2.6-pro", stream: false, options: {}, context: { + messages: [{ role: "user", content: "run it", timestamp: 0 }], tools: [execTool], + } }); + const events = await adapter.parseResponse!(Response.json({ + choices: [{ message: { content: block }, finish_reason: "stop" }], + }), createTestTranslatorBudget()); + expect(events.filter(event => event.type === "text_delta").map(event => event.text).join("")).toBe(block); + expect(events.filter(event => event.type === "tool_call_start")).toEqual([]); +}); From cf502f268c7219542df6fbf001d0bef9218833b1 Mon Sep 17 00:00:00 2001 From: shirruy Date: Fri, 16 Oct 2026 21:24:30 -0400 Subject: [PATCH 02/12] fix(chat): suppress MiMo raw-text echo with stray invoke closer --- .../serialized-tool-call-content.ts | 23 +++++++- ...-chat-serialized-tool-call-content.test.ts | 54 +++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/adapters/openai-chat/serialized-tool-call-content.ts b/src/adapters/openai-chat/serialized-tool-call-content.ts index cd2f2fb6903..508151f0b2a 100644 --- a/src/adapters/openai-chat/serialized-tool-call-content.ts +++ b/src/adapters/openai-chat/serialized-tool-call-content.ts @@ -88,6 +88,7 @@ const BLOCK_HEADER = /\s*\r\n]+)>/y; const NEXT_BLOCK_HEADER = /\n\s*\r\n]+>/g; const FUNCTION_CLOSE = ""; const PARAMETER_CLOSE = ""; +const INVOKE_CLOSE = ""; function trimmedEnd(text: string, from: number, to: number): number { while (to > from && /\s/.test(text[to - 1]!)) to--; @@ -121,6 +122,13 @@ function blockAt(text: string, offset: number): SerializedToolCall | undefined { let bodyEnd = trimmedEnd(text, bodyStart, close); const closed = endsWithAt(text, bodyStart, bodyEnd, FUNCTION_CLOSE); if (closed) bodyEnd = trimmedEnd(text, bodyStart, bodyEnd - FUNCTION_CLOSE.length); + // MiMo's echo can also carry a stray `` close (the Command Code grammar's + // alternate function close) between `` and ``. Strip tail + // closers from the end inward — first, then — so the body + // comparison sees the same text the structured arguments carried (#7260 follow-up). + while (endsWithAt(text, bodyStart, trimmedEnd(text, bodyStart, bodyEnd), INVOKE_CLOSE)) { + bodyEnd = trimmedEnd(text, bodyStart, bodyEnd) - INVOKE_CLOSE.length; + } if (endsWithAt(text, bodyStart, bodyEnd, PARAMETER_CLOSE)) bodyEnd -= PARAMETER_CLOSE.length; const call = { name: header[1]!.trim(), @@ -517,8 +525,19 @@ function duplicatedSerializedToolCallRanges( const body = freeformBody(call.body); return structuredCalls.some(structured => { const input = structured.names.has(call.name) ? inputFromArguments(structured.argumentsText, structured.freeformTool) : undefined; - if (input === undefined) return false; - const normalized = freeformBody(input); + // MiMo can send a function tool's arguments as raw JS text instead of JSON and echo + // the same body as a bare block (#7260 follow-up). A raw-arguments string that equals + // the block body is the strongest possible agreement between the two representations, + // so it authorizes suppression exactly like a parsed `input` match. + let normalized: string | undefined = input !== undefined ? freeformBody(input) : undefined; + if (normalized === undefined && structured.freeformTool !== undefined && structured.argumentsText !== "") { + // JSON-parsable arguments keep the original bridge-unwrap contract; only a non-JSON + // payload whose entire text equals the block body authorizes the raw-text match. + try { JSON.parse(structured.argumentsText); } catch { + if (freeformBody(structured.argumentsText) === body) normalized = body; + } + } + if (normalized === undefined) return false; return normalized === body || (structured.recoveredParameterWrapper === true && body.startsWith(" event.type === "text_delta").map(event => event.text).join("")).toBe(block); expect(events.filter(event => event.type === "tool_call_start")).toEqual([]); }); + +test("a raw-text structured call plus its bare echo suppress the markup", async () => { + const script = "const r = await tools.exec_command({cmd:'node --version'}); text(r.output);"; + const block = `${script}`; + const adapter = createOpenAIChatAdapter(provider); + adapter.buildRequest({ modelId: "mimo-v2.6-pro", stream: false, options: {}, context: { + messages: [{ role: "user", content: "check node", timestamp: 0 }], tools: [execTool], + } }); + const events = await adapter.parseResponse!(Response.json({ + choices: [{ message: { content: `Running now.\n${block}`, tool_calls: [ + { id: "call_exec", function: { name: "exec", arguments: script } }, + ] }, finish_reason: "tool_calls" }], + }), createTestTranslatorBudget()); + expect(events.filter(event => event.type === "text_delta").map(event => event.text).join("")).toBe("Running now.\n"); + expect(events.filter(event => event.type === "tool_call_delta")).toEqual([ + { type: "tool_call_delta", arguments: script }, + ]); +}); + +test("a raw-text structured call with a different body keeps the markup visible", async () => { + const callBody = "text('first command');"; + const blockBody = "text('different command');"; + const block = `${blockBody}`; + const adapter = createOpenAIChatAdapter(provider); + adapter.buildRequest({ modelId: "mimo-v2.6-pro", stream: false, options: {}, context: { + messages: [{ role: "user", content: "check", timestamp: 0 }], tools: [execTool], + } }); + const events = await adapter.parseResponse!(Response.json({ + choices: [{ message: { content: block, tool_calls: [ + { id: "call_exec", function: { name: "exec", arguments: callBody } }, + ] }, finish_reason: "tool_calls" }], + }), createTestTranslatorBudget()); + expect(events.filter(event => event.type === "text_delta").map(event => event.text).join("")).toBe(block); + expect(events.find(event => event.type === "tool_call_delta")).toEqual({ type: "tool_call_delta", arguments: callBody }); +}); + +test("a raw-text call plus its bare echo with a stray close suppresses the markup", async () => { + const script = "const r = await tools.exec_command({cmd:'node --version'}); text(r.output);"; + // The exact shape observed live: the echo carries \\n. + const block = `${script}\n`; + const adapter = createOpenAIChatAdapter(provider); + adapter.buildRequest({ modelId: "mimo-v2.6-pro", stream: false, options: {}, context: { + messages: [{ role: "user", content: "check node", timestamp: 0 }], tools: [execTool], + } }); + const events = await adapter.parseResponse!(Response.json({ + choices: [{ message: { content: `Running.\n${block}`, tool_calls: [ + { id: "call_exec", function: { name: "exec", arguments: script } }, + ] }, finish_reason: "tool_calls" }], + }), createTestTranslatorBudget()); + expect(events.filter(event => event.type === "text_delta").map(event => event.text).join("")).toBe("Running.\n"); + expect(events.filter(event => event.type === "tool_call_delta")).toEqual([ + { type: "tool_call_delta", arguments: script }, + ]); +}); From 124ec136b8d591aec872a3a1d84eff93a5263f44 Mon Sep 17 00:00:00 2001 From: shirruy Date: Fri, 16 Oct 2026 21:43:16 -0400 Subject: [PATCH 03/12] fix(chat): suppress MiMo echo on error path and non-freeform raw-text calls Two follow-ups from live 20-run adversarial testing: 1. The raw-text echo match required freeformTool to be declared. Live runs showed MiMo sends raw JS arguments for plain JSON function tools too; the match now fires for any tool whose arguments are non-JSON raw text equal to the block body. Text that merely looks like JSON (leading { or [) stays protected so truncated JSON payloads keep the preservation contract. 2. terminateWithError released held text without reconciliation. An echo block that exactly matched one of the undelivered calls was still duplicate markup; the error path now reconciles against the closed calls before releasing, same as the success path. --- src/adapters/openai-chat.ts | 7 ++++++- src/adapters/openai-chat/serialized-tool-call-content.ts | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/adapters/openai-chat.ts b/src/adapters/openai-chat.ts index 92b28a6fb25..6d1385789b4 100644 --- a/src/adapters/openai-chat.ts +++ b/src/adapters/openai-chat.ts @@ -372,7 +372,12 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd event: Extract, ): Generator { closeToolCalls(); - yield* heldText(); // Pending tools are not dispatched, so held text stays visible. + // Pending tools are not dispatched, so held text stays visible — but a MiMo echo + // block that exactly matches one of those undelivered calls is still duplicate + // markup, not answer text. Reconcile against the closed calls before releasing. + const closedCalls = [...pendingToolCalls]; + const references = reconcileStructuredToolCalls(closedCalls.map(call => ({ wireName: call.name, restoredName: toolNames.restore(call.name), argumentsText: call.args, freeformTool: freeformTools.get(call.name) })), toolCallContent.current(), false); + yield* toolCallContent.drain(references); yield event; return "terminate"; }; diff --git a/src/adapters/openai-chat/serialized-tool-call-content.ts b/src/adapters/openai-chat/serialized-tool-call-content.ts index 508151f0b2a..df9b9861c88 100644 --- a/src/adapters/openai-chat/serialized-tool-call-content.ts +++ b/src/adapters/openai-chat/serialized-tool-call-content.ts @@ -530,11 +530,14 @@ function duplicatedSerializedToolCallRanges( // the block body is the strongest possible agreement between the two representations, // so it authorizes suppression exactly like a parsed `input` match. let normalized: string | undefined = input !== undefined ? freeformBody(input) : undefined; - if (normalized === undefined && structured.freeformTool !== undefined && structured.argumentsText !== "") { + if (normalized === undefined && structured.argumentsText !== "") { // JSON-parsable arguments keep the original bridge-unwrap contract; only a non-JSON // payload whose entire text equals the block body authorizes the raw-text match. + // A text that merely looks like JSON (starts with { or [) stays protected: it may + // be a truncated JSON payload, not the raw-text shape MiMo sends. + const looksLikeJson = /^\s*[[{]/.test(structured.argumentsText); try { JSON.parse(structured.argumentsText); } catch { - if (freeformBody(structured.argumentsText) === body) normalized = body; + if (!looksLikeJson && freeformBody(structured.argumentsText) === body) normalized = body; } } if (normalized === undefined) return false; From cfc8d82a90d14e749e502ff9fd0fdad6d39522f8 Mon Sep 17 00:00:00 2001 From: shirruy Date: Fri, 16 Oct 2026 21:51:30 -0400 Subject: [PATCH 04/12] fix(chat): reconcile MiMo echo blocks on the adapter error path terminateWithError released held text without reconciliation, so an echo block that exactly matched one of the undelivered calls leaked as visible markup when the turn failed. The error path now runs the same reconcile-and-drain the success path uses. --- src/adapters/openai-chat/serialized-tool-call-content.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/adapters/openai-chat/serialized-tool-call-content.ts b/src/adapters/openai-chat/serialized-tool-call-content.ts index df9b9861c88..60ef299925b 100644 --- a/src/adapters/openai-chat/serialized-tool-call-content.ts +++ b/src/adapters/openai-chat/serialized-tool-call-content.ts @@ -5,6 +5,7 @@ import { repairFreeformToolInput } from "../../responses/apply-patch-envelope"; const OPEN_TAG = ""; const CLOSE_TAG = ""; const FUNCTION_TAG = " Date: Fri, 16 Oct 2026 22:11:25 -0400 Subject: [PATCH 05/12] fix(chat): accept MiMo's alternate block close Live 20-run testing surfaced a third close shape: the echo block ends \n with no at all. blockAt now treats as a completed close only when a known function closer ( or ) immediately precedes it, so prose that merely mentions the tag cannot terminate a block. --- .../serialized-tool-call-content.ts | 43 ++++++++++++++++--- ...-chat-serialized-tool-call-content.test.ts | 36 ++++++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/adapters/openai-chat/serialized-tool-call-content.ts b/src/adapters/openai-chat/serialized-tool-call-content.ts index 60ef299925b..9eafa40967b 100644 --- a/src/adapters/openai-chat/serialized-tool-call-content.ts +++ b/src/adapters/openai-chat/serialized-tool-call-content.ts @@ -118,11 +118,36 @@ function blockAt(text: string, offset: number): SerializedToolCall | undefined { const next = NEXT_BLOCK_HEADER.exec(text); const limit = next ? next.index + 1 : text.length; let unclosed: SerializedToolCall | undefined; - for (let close = text.indexOf(CLOSE_TAG, bodyStart); close >= 0 && (close < limit || !unclosed); - close = text.indexOf(CLOSE_TAG, close + CLOSE_TAG.length)) { - let bodyEnd = trimmedEnd(text, bodyStart, close); + for ( + let close = nextBlockClose(text, bodyStart); + close.index >= 0 && (close.index < limit || !unclosed); + close = nextBlockClose(text, close.end) + ) { + const { index: closeIndex, end: closeEnd, tag: closeTag } = close; + let bodyEnd = trimmedEnd(text, bodyStart, closeIndex); const closed = endsWithAt(text, bodyStart, bodyEnd, FUNCTION_CLOSE); if (closed) bodyEnd = trimmedEnd(text, bodyStart, bodyEnd - FUNCTION_CLOSE.length); + // MiMo's echo can also end with the Command Code alternate closer + // ... or ... instead of + // . Treat the alternate close as a completed block only when + // a known function closer immediately precedes it, so prose containing the tag alone + // cannot terminate a block. + if (closeTag === FUNCTION_CALLS_CLOSE) { + const inner = trimmedEnd(text, bodyStart, closeIndex); + const hasFn = endsWithAt(text, bodyStart, inner, FUNCTION_CLOSE); + const hasInvoke = endsWithAt(text, bodyStart, inner, INVOKE_CLOSE); + if (!hasFn && !hasInvoke) continue; + let altEnd = inner; + if (hasFn) altEnd = trimmedEnd(text, bodyStart, altEnd - FUNCTION_CLOSE.length); + else if (hasInvoke) altEnd = trimmedEnd(text, bodyStart, altEnd - INVOKE_CLOSE.length); + if (endsWithAt(text, bodyStart, altEnd, PARAMETER_CLOSE)) altEnd -= PARAMETER_CLOSE.length; + return { + name: header[1]!.trim(), + body: text.slice(bodyStart, altEnd), + start: offset, + end: closeEnd, + }; + } // MiMo's echo can also carry a stray `` close (the Command Code grammar's // alternate function close) between `` and ``. Strip tail // closers from the end inward — first, then — so the body @@ -135,14 +160,22 @@ function blockAt(text: string, offset: number): SerializedToolCall | undefined { name: header[1]!.trim(), body: text.slice(bodyStart, bodyEnd), start: offset, - end: close + CLOSE_TAG.length, + end: closeEnd, }; if (closed) return call; - if (close < limit) unclosed ??= call; + if (closeIndex < limit) unclosed ??= call; } return unclosed; } +/** The next candidate block close: `` or MiMo's alternate ``. */ +function nextBlockClose(text: string, from: number): { index: number; end: number; tag: string } { + const a = text.indexOf(CLOSE_TAG, from); + const b = text.indexOf(FUNCTION_CALLS_CLOSE, from); + if (b >= 0 && (a < 0 || b < a)) return { index: b, end: b + FUNCTION_CALLS_CLOSE.length, tag: FUNCTION_CALLS_CLOSE }; + return { index: a, end: a >= 0 ? a + CLOSE_TAG.length : -1, tag: CLOSE_TAG }; +} + /** Finds complete bare blocks outside literal Markdown; ambiguous outer blocks stop the scan. */ function callsIn(text: string, context: TextContext = { fence: null, lineStart: true }): SerializedToolCall[] { const calls: SerializedToolCall[] = []; diff --git a/tests/adapters/openai/openai-chat-serialized-tool-call-content.test.ts b/tests/adapters/openai/openai-chat-serialized-tool-call-content.test.ts index 3ca66d41545..a7e8f4fad47 100644 --- a/tests/adapters/openai/openai-chat-serialized-tool-call-content.test.ts +++ b/tests/adapters/openai/openai-chat-serialized-tool-call-content.test.ts @@ -1013,3 +1013,39 @@ test("a raw-text call plus its bare echo with a stray close suppresses { type: "tool_call_delta", arguments: script }, ]); }); + +test("a bare echo closed by with a raw-text call suppresses the markup", async () => { + const script = "const r = await tools.exec_command({cmd:'node --version'}); text(r.output);"; + // The live shape: the block ends \\n with no . + const block = `${script}\n`; + const adapter = createOpenAIChatAdapter(provider); + adapter.buildRequest({ modelId: "mimo-v2.6-pro", stream: false, options: {}, context: { + messages: [{ role: "user", content: "check node", timestamp: 0 }], tools: [execTool], + } }); + const events = await adapter.parseResponse!(Response.json({ + choices: [{ message: { content: `Running.\n${block}`, tool_calls: [ + { id: "call_exec", function: { name: "exec", arguments: script } }, + ] }, finish_reason: "tool_calls" }], + }), createTestTranslatorBudget()); + expect(events.filter(event => event.type === "text_delta").map(event => event.text).join("")).toBe("Running.\n"); + expect(events.filter(event => event.type === "tool_call_delta")).toEqual([ + { type: "tool_call_delta", arguments: script }, + ]); +}); + +test("prose containing alone does not close a block", async () => { + const script = "text('start');"; + const content = `${script}\nOrdinary text mentioning in prose.`; + const adapter = createOpenAIChatAdapter(provider); + adapter.buildRequest({ modelId: "mimo-v2.6-pro", stream: false, options: {}, context: { + messages: [{ role: "user", content: "ping", timestamp: 0 }], tools: [execTool], + } }); + const events = await adapter.parseResponse!(Response.json({ + choices: [{ message: { content }, finish_reason: "stop" }], + }), createTestTranslatorBudget()); + // The standalone bare block is recovered as a synthetic exec call (declared freeform + // tool, dotted MiMo ID, empty body rules pass). The prose mentioning + // survives byte-exact: the alternate close cannot swallow text that follows a block. + expect(events.filter(event => event.type === "text_delta").map(event => event.text).join("")).toBe("\nOrdinary text mentioning in prose."); + expect(events.filter(event => event.type === "tool_call_start")).toHaveLength(1); +}); From 8d5535376cd73d51686b31f6e7979de26e5efc7b Mon Sep 17 00:00:00 2001 From: shirruy Date: Fri, 16 Oct 2026 22:24:14 -0400 Subject: [PATCH 06/12] fix(chat): reduce MiMo raw-text doubled arguments inside ambiguity gate Live testing surfaced args sent as body+body raw text with no JSON wrapper. The reduction now runs inside doubledInputReduction, which reduceUnambiguousDoubledInput already guards with the single-explanation rule, so competing calls keep their arguments unchanged. --- .../serialized-tool-call-content.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/adapters/openai-chat/serialized-tool-call-content.ts b/src/adapters/openai-chat/serialized-tool-call-content.ts index 9eafa40967b..bb55a22a96c 100644 --- a/src/adapters/openai-chat/serialized-tool-call-content.ts +++ b/src/adapters/openai-chat/serialized-tool-call-content.ts @@ -608,6 +608,10 @@ function doubledInputReduction( ): string | undefined { if (!functionNames.has(repeated.name)) return undefined; const body = freeformBody(repeated.body); + // Raw-text doubled arguments (no JSON wrapper) reduce to the single body. + if (body !== "" && (argumentsText === body + body || argumentsText === body + "\n" + body)) { + return body; + } try { const parsed = JSON.parse(argumentsText) as unknown; if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) @@ -622,6 +626,23 @@ function doubledInputReduction( return undefined; } +/** + * The raw-text counterpart of the doubled-input reduction: MiMo can send a plain + * function tool's arguments as `body + body` raw text (no JSON wrapper). When the + * first half equals a bare block's body exactly, the call meant that body once. + */ +function rawTextDoubledReduction( + argumentsText: string, + functionNames: ReadonlySet, + repeated: SerializedToolCall, +): string | undefined { + if (!functionNames.has(repeated.name)) return undefined; + const body = freeformBody(repeated.body); + if (body === "" || argumentsText === body) return undefined; + return argumentsText === body + body || argumentsText === body + "\n" + body + ? body : undefined; +} + /** Removes a malformed argument prefix only when a bare block and the JSON suffix prove identical input. */ export function repairArgumentsDuplicatedBesideSerializedCall( argumentsText: string, From 3eb61368a9f9fc7101eb8bccd76619c7660d98a2 Mon Sep 17 00:00:00 2001 From: shirruy Date: Fri, 16 Oct 2026 22:53:49 -0400 Subject: [PATCH 07/12] fix(chat): recover mid-line MiMo echo blocks and wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The live screenshot showed the block starting mid-line directly after a prose sentence with no newline — the most common emission position. The line-start streaming splitter deliberately keeps those visible, so suppression now also scans mid-line blocks at reconciliation time, with the same function-name + exact-body agreement rule. Also handles the wrapper MiMo puts inside the function body (same family as the malformed /.test(wrappedBody)) || body.includes("")) return []; - return [{ name: call.name, input: body }]; + return [{ name: call.name, input: unwrappedBody }]; }); } @@ -555,8 +559,32 @@ function duplicatedSerializedToolCallRanges( const doubled = structuredCalls.some(structured => structured !== matching[0] && hasDoubledInput(structured, repeated)); return matching.length === 1 && !doubled ? [{ start: repeated.start, end: repeated.end }] : []; } - return callsIn(text, context).filter(call => { - const body = freeformBody(call.body); + // MiMo also emits the block mid-line, immediately after a prose sentence with no + // newline. The line-start splitter deliberately keeps those visible while streaming, + // but at reconciliation time — with the structured calls in hand — a mid-line block + // whose function name and exact body match a dispatched call is the same echo. + const lineStartCalls = callsIn(text, context); + const lineStartSpans = new Set(lineStartCalls.map(call => `${call.start}:${call.end}`)); + const midLineCalls: SerializedToolCall[] = []; + const MID_LINE_HEADER = /\s*\r\n]+>/g; + MID_LINE_HEADER.lastIndex = 0; + for (let header = MID_LINE_HEADER.exec(text); header !== null; header = MID_LINE_HEADER.exec(text)) { + const at = header.index; + if (context?.fence) { + // Skip blocks that sit inside a fence; the splitter's fence tracking already covers + // line-start cases, so only a cheap before-text check is needed here. + const before = text.slice(0, at); + const fences = (before.match(/^ {0,3}(`{3,}|~{3,})/gm) ?? []).length; + if (fences % 2 === 1) continue; + } + const call = blockAt(text, at); + if (call && !lineStartSpans.has(`${call.start}:${call.end}`)) midLineCalls.push(call); + } + return [...lineStartCalls, ...midLineCalls].filter(call => { + let body = freeformBody(call.body); + // MiMo can wrap the body in a stray opener (the same family as the + // malformed { const input = structured.names.has(call.name) ? inputFromArguments(structured.argumentsText, structured.freeformTool) : undefined; // MiMo can send a function tool's arguments as raw JS text instead of JSON and echo diff --git a/tests/adapters/openai/openai-chat-serialized-tool-call-content.test.ts b/tests/adapters/openai/openai-chat-serialized-tool-call-content.test.ts index a7e8f4fad47..491952ce513 100644 --- a/tests/adapters/openai/openai-chat-serialized-tool-call-content.test.ts +++ b/tests/adapters/openai/openai-chat-serialized-tool-call-content.test.ts @@ -1049,3 +1049,38 @@ test("prose containing alone does not close a block", async () expect(events.filter(event => event.type === "text_delta").map(event => event.text).join("")).toBe("\nOrdinary text mentioning in prose."); expect(events.filter(event => event.type === "tool_call_start")).toHaveLength(1); }); + +test("a bare block with a wrapper and a raw-text call suppresses the markup", async () => { + const script = "const patch = `*** Begin Patch`; text('applied');"; + // The live screenshot shape: the block body carries a stray opener. + const block = `${script}`; + const adapter = createOpenAIChatAdapter(provider); + adapter.buildRequest({ modelId: "mimo-v2.6-pro", stream: false, options: {}, context: { + messages: [{ role: "user", content: "apply the patch", timestamp: 0 }], tools: [execTool], + } }); + const events = await adapter.parseResponse!(Response.json({ + choices: [{ message: { content: `Updating the matrix now.\n${block}`, tool_calls: [ + { id: "call_exec", function: { name: "exec", arguments: script } }, + ] }, finish_reason: "tool_calls" }], + }), createTestTranslatorBudget()); + expect(events.filter(event => event.type === "text_delta").map(event => event.text).join("")).toBe("Updating the matrix now.\n"); + expect(events.filter(event => event.type === "tool_call_delta")).toEqual([ + { type: "tool_call_delta", arguments: script }, + ]); +}); + +test("a standalone bare block with a wrapper is recovered as a call", async () => { + const script = "const r = await tools.exec_command({cmd:'dir'}); text(r.output);"; + const block = `${script}`; + const adapter = createOpenAIChatAdapter(provider); + adapter.buildRequest({ modelId: "mimo-v2.6-pro", stream: false, options: {}, context: { + messages: [{ role: "user", content: "run it", timestamp: 0 }], tools: [execTool], + } }); + const events = await adapter.parseResponse!(Response.json({ + choices: [{ message: { content: `Starting.\n${block}`, finish_reason: "stop" } }], + }), createTestTranslatorBudget()); + expect(events.filter(event => event.type === "text_delta").map(event => event.text).join("")).toBe("Starting.\n"); + expect(events.filter(event => event.type === "tool_call_delta")).toEqual([ + { type: "tool_call_delta", arguments: JSON.stringify({ input: script }) }, + ]); +}); From 52dea95984b60e14c28c5517116a50a34ef0a81c Mon Sep 17 00:00:00 2001 From: shirruy Date: Fri, 16 Oct 2026 23:02:35 -0400 Subject: [PATCH 08/12] fix(chat): accept the alternate block close Live 20-run testing surfaced a seventh close shape: the echo block ends \n (no or ). nextBlockClose now also considers , and the alternate-close rule accepts it only when a known function closer precedes it, matching the treatment. --- .../openai-chat/serialized-tool-call-content.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/adapters/openai-chat/serialized-tool-call-content.ts b/src/adapters/openai-chat/serialized-tool-call-content.ts index 7b46a30cf0e..a629de84bfc 100644 --- a/src/adapters/openai-chat/serialized-tool-call-content.ts +++ b/src/adapters/openai-chat/serialized-tool-call-content.ts @@ -6,6 +6,7 @@ const OPEN_TAG = ""; const CLOSE_TAG = ""; const FUNCTION_TAG = "= 0 && (a < 0 || b < a)) return { index: b, end: b + FUNCTION_CALLS_CLOSE.length, tag: FUNCTION_CALLS_CLOSE }; - return { index: a, end: a >= 0 ? a + CLOSE_TAG.length : -1, tag: CLOSE_TAG }; + const c = text.indexOf(FUNCTION_NAME_CLOSE, from); + const candidates = [ + a >= 0 ? { index: a, end: a + CLOSE_TAG.length, tag: CLOSE_TAG } : undefined, + b >= 0 ? { index: b, end: b + FUNCTION_CALLS_CLOSE.length, tag: FUNCTION_CALLS_CLOSE } : undefined, + c >= 0 ? { index: c, end: c + FUNCTION_NAME_CLOSE.length, tag: FUNCTION_NAME_CLOSE } : undefined, + ].filter((x): x is { index: number; end: number; tag: string } => x !== undefined) + .sort((x, y) => x.index - y.index); + return candidates[0] ?? { index: -1, end: -1, tag: CLOSE_TAG }; } /** Finds complete bare blocks outside literal Markdown; ambiguous outer blocks stop the scan. */ From 1622c6e16bcd3548c1ba4217489a3746ce842952 Mon Sep 17 00:00:00 2001 From: shirruy Date: Sat, 17 Oct 2026 00:00:45 -0400 Subject: [PATCH 09/12] fix(chat): generalize the wrapper strip to any variable name Live testing from the actual Codex Desktop harness session showed the wrapper is (a variable declaration fragment), not only the literal . The strip regex now accepts any or ?/; /** * Streaming bounds (ingestStreaming only; buffered reconciliation already knows its structured calls). * MAX_HELD_BYTES is a runaway guard on held text plus queued events, far below the translator budget, @@ -70,11 +71,12 @@ export function recoverStandaloneBareBlocks( if (!freeformTools.has(call.name)) return []; const wrappedBody = freeformBody(call.body); const hasParameterOpener = wrappedBody.startsWith("/.test(wrappedBody)) || body.includes("")) return []; @@ -593,7 +595,8 @@ function duplicatedSerializedToolCallRanges( let body = freeformBody(call.body); // MiMo can wrap the body in a stray opener (the same family as the // malformed { const input = structured.names.has(call.name) ? inputFromArguments(structured.argumentsText, structured.freeformTool) : undefined; // MiMo can send a function tool's arguments as raw JS text instead of JSON and echo diff --git a/tests/adapters/openai/openai-chat-serialized-tool-call-content.test.ts b/tests/adapters/openai/openai-chat-serialized-tool-call-content.test.ts index 491952ce513..17289044762 100644 --- a/tests/adapters/openai/openai-chat-serialized-tool-call-content.test.ts +++ b/tests/adapters/openai/openai-chat-serialized-tool-call-content.test.ts @@ -1084,3 +1084,24 @@ test("a standalone bare block with a wrapper is recovered as a cal { type: "tool_call_delta", arguments: JSON.stringify({ input: script }) }, ]); }); + +test.each([ + { label: "variable with = (live " }, + { label: "input name (live shape)", wrapper: "" }, +] as const)("a bare block wrapped in $label suppresses the markup", async ({ wrapper }) => { + const script = "const r = await tools.exec_command({cmd:'node -v'}); text(r.output);"; + const block = `${wrapper}${script}`; + const adapter = createOpenAIChatAdapter(provider); + adapter.buildRequest({ modelId: "mimo-v2.6-pro", stream: false, options: {}, context: { + messages: [{ role: "user", content: "check", timestamp: 0 }], tools: [execTool], + } }); + const events = await adapter.parseResponse!(Response.json({ + choices: [{ message: { content: `Running.\n${block}`, tool_calls: [ + { id: "call_exec", function: { name: "exec", arguments: script } }, + ] }, finish_reason: "tool_calls" }], + }), createTestTranslatorBudget()); + expect(events.filter(event => event.type === "text_delta").map(event => event.text).join("")).toBe("Running.\n"); + expect(events.filter(event => event.type === "tool_call_delta")).toEqual([ + { type: "tool_call_delta", arguments: script }, + ]); +}); From 0caee6423ff1313a4167755698888dc6df1ff816 Mon Sep 17 00:00:00 2001 From: shirruy Date: Sat, 17 Oct 2026 00:18:39 -0400 Subject: [PATCH 10/12] fix(chat): distinguish tag wrappers from stray-bracket const declarations Two live shapes exist and need opposite strips: - const r=await... : the tag is a complete wrapper; the script restarts with its own const. Strip the whole tag. - { let body = freeformBody(call.body); - // MiMo can wrap the body in a stray opener (the same family as the - // malformed script — a complete tag; strip the whole tag. + // B) const r=await...): the script restarts with its own + // `const` declaration after the tag — the tag is a complete wrapper; strip it whole. + // Shape B ( { const input = structured.names.has(call.name) ? inputFromArguments(structured.argumentsText, structured.freeformTool) : undefined; // MiMo can send a function tool's arguments as raw JS text instead of JSON and echo From 3011832cc15ee23564b9bde0501f3f1346203a8b Mon Sep 17 00:00:00 2001 From: shirruy Date: Sat, 17 Oct 2026 01:03:33 -0400 Subject: [PATCH 11/12] fix(chat): hide bare MiMo blocks when no tools are declared (Codex Desktop harness path) The Codex Desktop harness plugin executes bare blocks client-side and sends no tool declarations to the proxy. The standalone recovery required declared freeform tools, so these sessions never triggered any suppression and the raw markup rendered as the assistant's visible answer. When the routed model is a dotted MiMo V2 ID, no structured call exists, and no freeform tools are declared, the flush paths now strip bare blocks from the visible text events. The harness still executes the block from its own client-side parse; the proxy only keeps the markup out of the answer. --- src/adapters/openai-chat.ts | 34 ++++++++- .../serialized-tool-call-content.ts | 69 +++++++++++++------ ...-chat-serialized-tool-call-content.test.ts | 34 +++++++++ 3 files changed, 116 insertions(+), 21 deletions(-) diff --git a/src/adapters/openai-chat.ts b/src/adapters/openai-chat.ts index 6d1385789b4..e3239f96482 100644 --- a/src/adapters/openai-chat.ts +++ b/src/adapters/openai-chat.ts @@ -47,7 +47,7 @@ import { messagesToChatFormat } from "./openai-chat/messages"; import { withOpenAIChatToolNames } from "./openai-chat/tool-name-registry"; import { openAIChatTransport, stripBracketedModelSuffix } from "./openai-chat/wire"; import { toolChoiceToChatFormat, toolsToChatFormatForProvider } from "./openai-chat/tool-schema"; -import { freeformToolsByWireName, type FreeformToolIdentity, reconcileSerializedToolCallEvents, reconcileStructuredToolCall, reconcileStructuredToolCalls, recoverStandaloneBareBlocks, SerializedToolCallContentBuffer } from "./openai-chat/serialized-tool-call-content"; +import { freeformToolsByWireName, type FreeformToolIdentity, reconcileSerializedToolCallEvents, reconcileStructuredToolCall, reconcileStructuredToolCalls, recoverStandaloneBareBlocks, SerializedToolCallContentBuffer, stripStandaloneBareBlocks } from "./openai-chat/serialized-tool-call-content"; export { stripBracketedModelSuffix } from "./openai-chat/wire"; export { buildOpenAIChatPassthroughRequest } from "./openai-chat/passthrough"; @@ -358,6 +358,22 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd } return "continue"; } + // Codex Desktop harness sessions declare no tools and send no structured + // payload; the client-side harness executes these blocks. The proxy's job is + // only to keep the markup out of the visible answer text. + if (freeformTools.size === 0) { + const stripped = stripStandaloneBareBlocks(toolCallContent.current()); + if (stripped !== toolCallContent.current()) { + const events = toolCallContent.drain([]); + for (const event of events) { + if (event.type === "text_delta") { + const clean = stripStandaloneBareBlocks(event.text); + if (clean.length > 0) yield { type: "text_delta", text: clean }; + } else yield event; + } + return "continue"; + } + } } yield* toolCallContent.drain(references); for (const call of calls) { @@ -840,6 +856,22 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd events.push({ type: "tool_call_start", id, name: toolNames.restore(block.name) }, delta, { type: "tool_call_end" }); }); } + // Same no-tools harness path as the streamed adapter: strip the bare markup + // from the visible text events when nothing else claimed it. + if (freeformTools.size === 0) { + const stripped = stripStandaloneBareBlocks(answerText); + if (stripped !== answerText) { + const replacement: AdapterEvent[] = []; + for (let i = contentStart; i < contentEnd; i++) { + const event = events[i]!; + if (event.type === "text_delta") { + const clean = stripStandaloneBareBlocks(event.text); + if (clean.length > 0) replacement.push({ type: "text_delta", text: clean }); + } else replacement.push(event); + } + events.splice(contentStart, contentEnd - contentStart, ...replacement); + } + } } const references = reconcileStructuredToolCalls(structuredCalls, answerText, /(?:^|[/-])mimo-v2(?:\.|$)/i.test(lastRequestedModelId ?? "")); structuredCalls.forEach((call, index) => { call.delta.arguments = references[index]!.argumentsText; }); diff --git a/src/adapters/openai-chat/serialized-tool-call-content.ts b/src/adapters/openai-chat/serialized-tool-call-content.ts index 78ce1a07c73..ed31e8a2f40 100644 --- a/src/adapters/openai-chat/serialized-tool-call-content.ts +++ b/src/adapters/openai-chat/serialized-tool-call-content.ts @@ -84,6 +84,35 @@ export function recoverStandaloneBareBlocks( }); } +/** + * The ranges of bare serialized blocks to hide from visible text when no structured + * payload and no declared freeform tools exist (the Codex Desktop harness path: the + * client-side harness parses and executes these blocks, so the proxy must only keep + * them out of the assistant's visible answer). Used by the adapter's flush paths when + * the routed model is a dotted MiMo V2 ID and nothing else claimed the markup. + */ +export function standaloneBareBlockRanges( + text: string, + context: TextContext = { fence: null, lineStart: true }, +): { start: number; end: number }[] { + return [...callsIn(text, context), ...midLineCallsIn(text, context)] + .map(call => ({ start: call.start, end: call.end })); +} + +/** Removes bare serialized blocks from visible text when no structured call claims them. */ +export function stripStandaloneBareBlocks( + text: string, + context: TextContext = { fence: null, lineStart: true }, +): string { + let result = ""; + let cursor = 0; + for (const range of standaloneBareBlockRanges(text, context)) { + result += text.slice(cursor, range.start); + cursor = range.end; + } + return result + text.slice(cursor); +} + export interface StructuredToolCallReference { names: ReadonlySet; argumentsText: string; @@ -556,27 +585,10 @@ function hasDoubledInput(structured: StructuredToolCallReference, repeated: Seri } /** The `[start, end)` ranges of blocks whose function identity and freeform input match a dispatched call. */ -function duplicatedSerializedToolCallRanges( - text: string, - structuredCalls: readonly StructuredToolCallReference[], - context?: TextContext, -): { start: number; end: number }[] { - if (structuredCalls.length === 0) return []; - const repeated = repeatedCallIn(text, context); - if (repeated) { - // A doubled call beside an agreeing call leaves the pair ambiguous. If reduction was - // refused, keep the markup too: otherwise the visible text and executable call disagree. - const matching = structuredCalls.filter(structured => agreesWithRepeatedBlock(structured, repeated)); - const doubled = structuredCalls.some(structured => structured !== matching[0] && hasDoubledInput(structured, repeated)); - return matching.length === 1 && !doubled ? [{ start: repeated.start, end: repeated.end }] : []; - } - // MiMo also emits the block mid-line, immediately after a prose sentence with no - // newline. The line-start splitter deliberately keeps those visible while streaming, - // but at reconciliation time — with the structured calls in hand — a mid-line block - // whose function name and exact body match a dispatched call is the same echo. - const lineStartCalls = callsIn(text, context); - const lineStartSpans = new Set(lineStartCalls.map(call => `${call.start}:${call.end}`)); +/** Mid-line bare blocks (MiMo emits them right after a prose sentence with no newline). */ +function midLineCallsIn(text: string, context?: TextContext): SerializedToolCall[] { const midLineCalls: SerializedToolCall[] = []; + const lineStartSpans = new Set(callsIn(text, context ?? { fence: null, lineStart: true }).map(call => `${call.start}:${call.end}`)); const MID_LINE_HEADER = /\s*\r\n]+>/g; MID_LINE_HEADER.lastIndex = 0; for (let header = MID_LINE_HEADER.exec(text); header !== null; header = MID_LINE_HEADER.exec(text)) { @@ -591,6 +603,23 @@ function duplicatedSerializedToolCallRanges( const call = blockAt(text, at); if (call && !lineStartSpans.has(`${call.start}:${call.end}`)) midLineCalls.push(call); } + return midLineCalls; +} + +function duplicatedSerializedToolCallRanges( + text: string, + structuredCalls: readonly StructuredToolCallReference[], + context?: TextContext, +): { start: number; end: number }[] { + if (structuredCalls.length === 0) return []; + const repeated = repeatedCallIn(text, context); + if (repeated) { + const matching = structuredCalls.filter(structured => agreesWithRepeatedBlock(structured, repeated)); + const doubled = structuredCalls.some(structured => structured !== matching[0] && hasDoubledInput(structured, repeated)); + return matching.length === 1 && !doubled ? [{ start: repeated.start, end: repeated.end }] : []; + } + const lineStartCalls = callsIn(text, context); + const midLineCalls = midLineCallsIn(text, context); return [...lineStartCalls, ...midLineCalls].filter(call => { let body = freeformBody(call.body); // Two wrapper families: diff --git a/tests/adapters/openai/openai-chat-serialized-tool-call-content.test.ts b/tests/adapters/openai/openai-chat-serialized-tool-call-content.test.ts index 17289044762..09bd3182c60 100644 --- a/tests/adapters/openai/openai-chat-serialized-tool-call-content.test.ts +++ b/tests/adapters/openai/openai-chat-serialized-tool-call-content.test.ts @@ -381,6 +381,40 @@ test("buffered Chat responses suppress a repeated echo beside an unrelated struc ]); }); +test("a bare block in a no-tools harness session (Codex Desktop path) is hidden from visible text", async () => { + const script = "const r = await tools.exec_command({cmd:'node -v'}); text(r.output);"; + const block = `${script}`; + // No tools declared — the harness executes the block client-side. + const adapter = createOpenAIChatAdapter(provider); + adapter.buildRequest({ modelId: "mimo-v2.6-pro", stream: false, options: {}, context: { + messages: [{ role: "user", content: "check version", timestamp: 0 }], + } }); + const events = await adapter.parseResponse!(Response.json({ + choices: [{ message: { content: `Checking now.\n${block}\nDone setting up.`, finish_reason: "stop" } }], + }), createTestTranslatorBudget()); + const visible = events.filter(event => event.type === "text_delta").map(event => event.text).join(""); + expect(visible).toBe("Checking now.\n\nDone setting up."); + expect(visible).not.toContain(""); + expect(visible).not.toContain("