# Provider adaptation and protocol correctness

A provider adapter translates between the harness's conversation model and a model service's request and response format. Its hardest job is preserving meaning: which tool was requested, whether its arguments are complete, which result answers it, and which metadata must accompany the next request.

Suppose two tool calls stream simultaneously. One receives the fragment `{"path":"src/`; the other receives `{"query":"parser"}`. A single shared argument buffer can mix their inputs. Even separate buffers are insufficient if the connection closes before the first call finishes. A UI can display partial progress, but execution needs a stricter completion contract.

This page examines selected paths in the four pinned snapshots. It does not certify every provider integration. Sidekick's links point to private source and require repository access.

## Three representations, three responsibilities

| Representation | What it needs to preserve |
| --- | --- |
| Provider stream | Fragment identity, ordering, completion markers, errors, usage, and provider metadata. |
| Harness conversation | Typed assistant content, tool calls and matching results, stop reasons, provenance, and replay information. |
| Next provider request | A valid projection of that conversation for the selected API and model. |

The display transcript is another view. A readable reasoning summary is not interchangeable with an opaque reasoning signature: a provider may require the latter for replay even though the UI has nothing useful to show. Similarly, a failed tool call still needs a coherent representation in the next request.

## Four adaptation boundaries

| Harness | Inspected boundary | Consequence for builders |
| --- | --- | --- |
| Codex | Responses events become typed `ResponseEvent` and `ResponseItem` values; completed items feed turn handling. [Events][C1], [turn handling][C2] | Item completion and whole-response completion are separate milestones. |
| pi | `Models` normalizes context, resolves authentication, and dispatches through provider streaming; API adapters emit common assistant events. [Dispatch][P1], [Responses adapter][P2] | The reusable agent loop can consume one event contract while adapters retain API-specific rules. |
| Goose | `Provider::stream` receives common messages and tools and returns message/usage events; format modules translate specific wire protocols. [Interface][G1], [OpenAI-compatible stream][G2] | Common types still need provider-specific serialization and repair. |
| Sidekick | The native OpenRouter adapter emits LLM chunks; `stream_llm` translates them into unified messages for session dispatch. [Request adapter][S1], [translator][S2] | A second translation boundary can lose information unless its contract carries it explicitly. |

These boundaries describe different scopes. Goose's provider interface also permits integrations that own their context; Sidekick's external Outpost runtimes have their own model protocol handling. Native adapter behavior should not automatically be attributed to those wrapped runtimes.

## Completing a tool call is more than parsing partial JSON

Codex's inspected Responses parser forwards `output_item.done` as a complete typed item. It deliberately ignores function-argument delta events rather than reconstructing a function call from those fragments; custom-tool input deltas have a separate event. Turn handling consumes the completed item. The SSE reader reports an error if the stream ends before a completion event, and handles incomplete responses according to their reason. An interrupted response can become a completion with `end_turn: false`. [Event mapping][C1], [completion handling][C3], [stream termination][C4]

This is not an assertion that Codex rejects every malformed frame. The SSE reader logs and skips JSON it cannot deserialize, and item conversion can ignore an unrecognized payload. The completion requirement is a specific invariant, not universal strictness.

pi's Responses adapter instead keeps slots keyed by output index, appends argument fragments to each slot, and parses partial JSON for progress events. At item completion it finalizes arguments and removes temporary buffers. It rejects a missing terminal response event; when the final stop reason is `toolUse`, it also rejects calls whose unfinished buffers remain. This makes “looks parseable so far” different from “the provider finished this call.” [Assembly and finalization][P2]

Goose's OpenAI-compatible path accumulates arguments by tool index. Metadata-only frames are skipped without prematurely ending accumulation. On finalization, output-token exhaustion produces an error-bearing tool request; non-object JSON and unparseable arguments also become errors. Its argument parser can remove wrappers or repair some non-truncated malformation, but refuses inputs that look structurally truncated. That is a deliberate compatibility policy rather than permission to invent missing arguments. [Accumulation][G2], [tool errors][G3], [argument parser][G4]

Sidekick accumulates native tool fragments by index and parses their accumulated argument strings after streaming ends. Invalid JSON is retained as a distinguished raw-input value. The agent loop recognizes that value, refuses execution, and supplies a correlated error result the model can correct. Partial field extraction used for UI updates does not authorize execution of the repaired preview. The translator handles exceptions and explicit provider errors, but does not itself require a terminal marker before building assistant content after normal iterator exhaustion. Its completion contract therefore differs from the inspected pi and Codex paths. [Translator][S2], [refusal boundary][S3]

## Keep the next request valid after failure

Consider this illustrative trace:

| Step | Required interpretation |
| --- | --- |
| Assistant requests `read`, call ID `a1`. | Record the request identity and arguments. |
| Execution is cancelled or arguments are rejected. | Do not represent the operation as successful. |
| Another user message arrives. | Close or repair the earlier tool exchange before serializing the new turn. |
| The user switches providers. | Adapt call IDs and provider-bound metadata consistently on both sides. |

Codex's history normalization inserts missing call outputs, removes orphaned outputs unless they name external tool events, and strips unsupported image/audio content. This is a model-context invariant, not evidence that an interrupted external action was rolled back. [History normalization][C5]

pi's transformation pass supplies missing results as errors saying no result was provided. It holds intervening system messages until the tool exchange closes, skips errored or aborted assistant messages, and remaps result IDs when call IDs change for the destination provider. [Conversation transformation][P3]

Goose solves a particularly instructive failure: an unparseable tool request still needs an assistant-side call when replayed. Its OpenAI formatter emits a placeholder call named `unparseable_tool_call` with the original ID, allowing the following error result to remain paired. This placeholder describes failed history; it is not a new operation to execute. [Failed-call serialization][G5]

## Reasoning metadata belongs to its protocol

Codex's typed reasoning item contains summary, optional content, and encrypted content separately. Collapsing that item into its visible summary would discard fields the protocol can retain. [Reasoning representation][C6]

pi makes replay compatibility explicit: reasoning survives only when provider, API, and model match. Signed or redacted blocks can survive even with empty visible text; foreign reasoning is dropped, and foreign tool thought signatures are removed. Its Responses adapter also backfills encrypted reasoning from the terminal response when an Azure response omitted it from the earlier completed item. [Compatibility transformation][P3], [signature backfill][P4]

Goose's OpenAI formatter conditionally preserves thinking context, carries it into tool-call messages, and can translate it into configured content formats. Its common stream contract does not make every reasoning representation universally portable. [Thinking serialization][G6]

Sidekick's lower OpenRouter adapter exposes structured `reasoning_details`, but the inspected unified-message translator accumulates textual `reasoning_delta` and creates a text thinking block without carrying those structured details into the assistant message. This review therefore does not establish lossless replay of opaque reasoning through the native loop. Displaying thinking and preserving provider replay state are different capabilities. [Adapter details][S4], [unified output][S2]

## Make capability fallbacks visible

An adapter can reject an unsupported feature, omit it, transform it, or delegate it elsewhere. Those choices change behavior. Codex declares provider capabilities as an upper bound that model and turn settings can restrict. pi replaces images with explicit placeholders for models without image input. Goose's interface exposes ownership of context, so local context management can defer to an external provider. [Codex capabilities][C7], [pi modality conversion][P3], [Goose ownership][G1]

Sidekick illustrates another limitation: when its native adapter supplies a provider-only routing restriction, it sets `require_parameters` to false. Constructing a request parameter therefore does not establish that every routed endpoint will honor it. See [Tools](tools.md) for capability negotiation and [prompt caching](prompt-caching.md) for the separate cost of changing request structure. [Routing configuration][S1]

## Builder exercise: replay an adversarial stream

Build a fake provider that interleaves two tool calls, inserts a metadata frame, and closes before one call completes. Then repeat with valid completion, malformed arguments, a missing result, and a provider switch carrying signed reasoning.

Assert that fragments never cross call identities, unfinished arguments never become executable merely because they parse, and failures produce valid subsequent requests. Preserve original evidence separately from normalized context; identify synthesized errors as such. Test destination-specific reasoning and modality rules instead of assuming a universal transcript format.

Finally, record item completion separately from response completion. Some harnesses start completed tools before the entire model response finishes. A later stream failure can therefore coexist with already-started work; protocol repair alone cannot make replay safe. Continue with [retries and failure semantics](retries.md), [testing and evaluation](testing.md), and [persistence and recovery](persistence.md) to make those boundaries testable.

[C1]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/codex-api/src/sse/responses.rs#L342-L520
[C2]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/session/turn.rs#L2804-L2913
[C3]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/codex-api/src/sse/responses.rs#L417-L479
[C4]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/codex-api/src/sse/responses.rs#L562-L604
[C5]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/context_manager/history.rs#L964-L981
[C6]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/protocol/src/models.rs#L1095-L1104
[C7]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/model-provider/src/capabilities.rs#L7-L51
[P1]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/ai/src/models.ts#L883-L922
[P2]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/ai/src/api/openai-responses-shared.ts#L624-L801
[P3]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/ai/src/api/transform-messages.ts#L63-L256
[P4]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/ai/src/api/openai-responses-shared.ts#L559-L581
[G1]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-provider-types/src/base.rs#L520-L697
[G2]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-provider-types/src/formats/openai.rs#L1347-L1448
[G3]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-provider-types/src/formats/openai.rs#L1518-L1567
[G4]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-provider-types/src/json.rs#L266-L297
[G5]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-provider-types/src/formats/openai.rs#L334-L379
[G6]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-provider-types/src/formats/openai.rs#L510-L558
[S1]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/llm_providers/openrouter_native.py#L232-L384
[S2]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/llm_stream_translator.py#L193-L368
[S3]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/services/agent_loop.py#L643-L697
[S4]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/llm_providers/openrouter_native.py#L873-L951
