Repository navigation
[Provider compatibility] ollama-native rejects Codex exec histories with more than one output per call, breaking routed compaction #6574
Description
Client or integration
Codex App
Provider or upstream service
ollama-cloud (provider adapter: "ollama-native"), observed with ollama-cloud/deepseek-v4.1-flash. The failing function serves every routed Ollama model.
OpenCodex version
Observed on 2.75.0. Reproduced offline on dev 33185c2 (2.78.0).
Endpoint or capability
POST /v1/responses: remote compaction turns routed by compactionRouting, and any ordinary routed turn that replays Codex code-mode exec history.
Current behaviour
One Codex code-mode exec call can leave more than one custom_tool_call_output with the same call_id in the history:
- When the cell outlives
yield_time_ms, Codex records the yield marker (Script running with cell ID N ...), and eachnotify()call adds another output for the samecall_id. - A still-running cell can record its
notify()output after later items, for example after awaitfunction call and itsfunction_call_output.
buildNativeMessages() rejects both shapes before anything is sent upstream (400, failureStage: headers-only):
- adjacent outputs:
ollama-native duplicate tool result for <id> - a late output after another call's batch:
ollama-native tool result <id> has no originating call
The damage is largest with compactionRouting. When compaction for every session goes to an Ollama Cloud model, a session whose history contains such an exec call can no longer compact. Codex reports Error running remote compact task, and a retry fails the same way because the history does not change. On one installation, 3 of 49 routed compaction attempts over two days failed like this. The other 46 succeeded, with inputs of 340k to 590k tokens.
The anthropic, google and openai-chat adapters build a request from the same parsed history. Anthropic and Google pair the first result with its call and carry the extra outputs as explicit orphan-result text.
Expected behaviour
ollama-native should not fail a request just because Codex recorded more than one output for one exec call. Two acceptable behaviours:
- Merge additional outputs for a call into its result, as fix(kiro): coalesce adjacent outputs without losing tool identity #3750 did for Kiro in the adjacent case.
- Keep the first result paired and carry extra or late outputs as explicit user-side text, which is what the anthropic and google adapters already do.
A result whose call id never appeared in the history can stay an error if that is the intended policy. A late or additional output for a call that did appear earlier should not fail the request.
Minimal redacted request or reproduction
Run from the repository root on dev (no network, no credentials):
import { parseRequest } from "./src/responses/parser";
import { createOllamaNativeAdapter } from "./src/adapters/ollama-native";
import { createAnthropicAdapter } from "./src/adapters/anthropic";
const user = (text: string) => ({ type: "message", role: "user", content: [{ type: "input_text", text }] });
const execCall = (id: string) => ({ type: "custom_tool_call", call_id: id, name: "exec", input: "notify('progress')" });
const execOut = (id: string, output: string) => ({ type: "custom_tool_call_output", call_id: id, output });
const waitCall = (id: string) => ({ type: "function_call", call_id: id, name: "wait", arguments: JSON.stringify({ cell_id: "31" }) });
const waitOut = (id: string, output: string) => ({ type: "function_call_output", call_id: id, output });
const tools = [
{ type: "custom", name: "exec", description: "Run JavaScript" },
{ type: "function", name: "wait", description: "Wait on a yielded exec cell",
parameters: { type: "object", properties: { cell_id: { type: "string" } }, required: ["cell_id"] } },
];
const yieldMarker = "Script running with cell ID 31\nWall time 31.0 seconds\nOutput:\n";
const shapes: Record<string, unknown[]> = {
control: [user("start"), execCall("call_A"), execOut("call_A", "done"), user("continue")],
adjacent: [user("start"), execCall("call_A"), execOut("call_A", yieldMarker), execOut("call_A", "progress 1"), user("continue")],
late: [user("start"), execCall("call_A"), execOut("call_A", yieldMarker),
waitCall("call_B"), waitOut("call_B", "Script completed\nOutput:\n"), execOut("call_A", "progress 2"), user("continue")],
};
const base = { authMode: "key", apiKey: "test-key", liveModels: false, models: ["deepseek-v4.1-flash"] } as any;
for (const [name, input] of Object.entries(shapes)) {
const parsed = parseRequest({ model: "deepseek-v4.1-flash", stream: true, input, tools });
for (const [label, adapter] of [
["ollama-native", createOllamaNativeAdapter({ ...base, adapter: "ollama-native", baseUrl: "https://ollama.com/v1" })],
["anthropic", createAnthropicAdapter({ ...base, adapter: "anthropic", baseUrl: "https://api.anthropic.com" })],
] as const) {
try { await adapter.buildRequest(structuredClone(parsed)); console.log(name, label, "ok"); }
catch (e: any) { console.log(name, label, "THROWS", e.message); }
}
}Output on 33185c2. The google and openai-chat adapters also return ok for every shape.
control ollama-native ok
control anthropic ok
adjacent ollama-native THROWS ollama-native duplicate tool result for call_A
adjacent anthropic ok
late ollama-native THROWS ollama-native tool result call_A has no originating call
late anthropic ok
The real history that failed had both shapes in one call: yield marker, notify() output, a wait call and result, then another notify() output for the first call.
Actual response or error
Error running remote compact task: {"error":{"message":"ollama-native duplicate tool result for toolu_<redacted>","type":"invalid_request_error","code":"invalid_request_error"}}
Usage log for the routed attempt: status 400, adapter ollama-native, failureStage headers-only, failureCause payload-rejected, sendCount 0.
Upstream documentation
No public Ollama specification covers more than one tool message per tool_call_id. The concrete client requirement is the Codex code-mode exec output sequence (notify() and yielded cells), the same requirement as #3734.
Suggested mapping or implementation notes
In buildNativeMessages():
- A
toolResultfor a call in the open batch that already has a result: append its content to that result in input order and keep error status if any part is an error (the fix(kiro): coalesce adjacent outputs without losing tool identity #3750 rule). - A
toolResultwhose call id was issued in an earlier, already settled batch: emit it as explicit user-side text with a marker, as the anthropic adapter does ([tool_result without adjacent tool_use: ...]), instead of throwing. - Keep the error for ids that never appeared in the history, if that policy should stay.
The existing tests a duplicate result for the same call is still refused, duplicate results remain rejected while commentary holds an unresolved batch and a result cannot cross a subsequent tool-call batch encode the current refusal, so this is a deliberate policy change for those cases.
Additional context and attachments
- [Kiro] Multiple outputs for one Codex exec call produce duplicate toolResult entries #3731 / [Kiro] Multiple outputs for one Codex exec call produce duplicate toolResult entries #3734: the same Codex shape on Kiro. fix(kiro): coalesce adjacent outputs without losing tool identity #3750 fixed adjacent outputs there; a late output after another call's result is still rejected by that rule.
- [Provider compatibility] ollama-native rejects a Codex replay whose tool result arrives after an injected mid-turn developer message #4842: an ollama-native replay-ordering fix for injected developer messages.
- Workaround in use: avoid
notify()in code-mode cells, or route compaction to a provider whose adapter accepts this history.
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
Ingwannu commentedon Oct 4, 2026
Confirmed offline on current dev 33185c2 using the real Responses parser and native builder: the single-output control passes, adjacent yield/notify outputs throw duplicate tool result, and a notification after wait throws no originating call, before any upstream send.
Draft fix: #6576, head 3d74c4d. Open-batch output fragments join once in arrival order; known late output uses an explicit attributed conversation-text carrier behind any pending native batch. Unknown IDs, mismatched tool identities, and reused call IDs still fail; images/error evidence and immutable source history are covered. The three existing rejection-policy tests were intentionally updated rather than left as contradictory contracts.
Regression proof: new file was 2 pass/5 fail before the implementation; related six-file set is now 101 pass/0 fail (336 assertions), plus structure/ratchet/layout gates. All runs were resource-capped with private homes. Full typecheck hit the 180-second cap, so it is explicitly incomplete; full suite, docs build, live provider/compaction verification, and independent review remain unclaimed. This is not a released fix, and the issue stays open.
@lidge-jun please review the late-output carrier/adjacency tradeoff when the remaining exact-head validation is available. No automatic merge or review waiver is being requested.
Ingwannu commentedon Oct 4, 2026
Fixed on dev by #6576, merged by lidge-jun at 9096934. Closing the local pre-dispatch rejection reported here: open-batch outputs are joined once in arrival order with images/error evidence, and valid late outputs from a known settled call are preserved as attributed user text without fabricating another invocation. Unknown IDs/wrong identities/reused call IDs remain refused.
Exact-head CI37209802493 passed for 0f896f5. Independent RED/GREEN history (2 pass/5 fail before; 101 pass/336 assertions after at3d74...) and the final repeated-result/settled-commentary test follow-up are documented in #6576. No actual Ollama/provider call or installed-client/routed-compaction acceptance was performed by me. This is a dev mapping fix, not an announced release or universal upstream behavior certification. Thanks @thisisjun786 for the concrete replay cases.