Skip to content

[Provider compatibility] ollama-native rejects Codex exec histories with more than one output per call, breaking routed compaction #6574

Closed

Description

@thisisjun786

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 each notify() call adds another output for the same call_id.
  • A still-running cell can record its notify() output after later items, for example after a wait function call and its function_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:

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 toolResult for 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 toolResult whose 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

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

  1. Ingwannu commented on Oct 4, 2026

    @Ingwannu
    Owner

    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.

  2. Ingwannu commented on Oct 4, 2026

    @Ingwannu
    Owner

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    providerProvider adapters, OpenAI-compat presets, upstream API quirksprovider-compatibilityProvider compatibility reports

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions