Open Responses: one contract instead of N×M adapters

A shared spec built on OpenAI's Responses API replaces a pile of provider adapters. The interesting part is not the field names but the state machine, the semantic events, and the MUST behind allowed_tools, truncation and previous_response_id — those decide who gets to make which call.

中文版是这篇文章的默认版本。 阅读中文版 →
This English page stays available at this URL; the Chinese version is the one we maintain first.

In one line: it swaps “messages” for bidirectional items, swaps token streaming for state-machine events, and gives extensions a namespaced side channel — so the endpoint and model name become configuration, and the cost moves from adapters to the semantics of individual fields.

Read from three places: the specification itself (both the 2026-01-15 and 2026-04-24 versions), its TypeSpec schema and repository, and the docs of two live implementations at Databricks and Vercel. Numbers are labelled by origin: counted in the repo, or read from the GitHub API.

1. What it actually defines#

Built on OpenAI’s Responses API, it fixes the shape of requests and responses for endpoints like /v1/responses. The scope is deliberately narrow: the API layer only. No model change, no retraining.

What it really defines is three things, not a pile of JSON fields:

PieceWhat the spec saysWhy
Itemsthe atomic unit of context, bidirectional: input to the model, or output from ita turn produces atomic events, not a string
Semantic eventsstreaming is a series of meaningful transitions, not text deltasclients need to rebuild structure, not concatenate
Extensionsany implementer may be a proper superset; new types carry their own slugproviders need room to ship their own features without collisions

Figure 1 — N×M edges on the left, N+M on the right
tap to enlarge

It also defines compliance: an API is compliant if it implements the spec directly or is a proper superset of it. That sentence matters — you may add, you may not change what a standard field means.

2. Three parts: items, events, state machines#

Figure 2 — an item from added to done, with extensions on a side channel
tap to enlarge

Items carry four properties, each solving a specific problem:

  • Polymorphic: discriminated by type. A message and a function_call have different shapes; the client dispatches instead of guessing.
  • State machines: in_progress / incomplete / completed. One hard rule: an item that ends incomplete must be the last item, and the containing response must also be incomplete. No “half sentence, then another one” ambiguity.
  • Streamable: the first event must be response.output_item.added, carrying every field known at that moment (at least role for a message, at least name for a function call), the rest as zero values. The client can draw the skeleton before filling it.
  • Extensible: non-standard item types must be prefixed with the implementer slug, e.g. openai:web_search_call.

Content is split asym­metrically into UserContent and ModelContent. User input may be text, images, audio, other modalities; model output is guaranteed text in the base protocol. The asymmetry is deliberate: a provider can later add output_image without over-generalising the user side.

Reasoning items are the most interesting detail: content (raw reasoning), encrypted_content (opaque, provider-specific), and summary (safe to show a user) are all optional. The spec does not require a provider to expose its chain of thought — only to be explicit about which of the three it hands you.

3. Four fields, four prices#

Figure 3 — the MUST behind each field
tap to enlarge

FieldWhat the spec requiresWho pays
previous_response_idthe server MUST load the prior input and output and treat the new request as input → output → new input, order preservedthe server keeps state; zero-retention deployments need another plan
allowed_toolsthe full tool list stays visible, but only the listed subset may actually run; servers MUST enforce it as a hard constraintthe server; the win is that prompt and schema caches survive
truncationwith disabled, no truncation: over-long context must fail, not quietly shrinkthe client, which must handle the error path
service_tiera hint about priority, multiple tiers allowedthe implementer maps it to queues and pricing

allowed_tools is the best piece of design in the spec. The old way to narrow tool permissions was to edit the tools array — which invalidates every prompt and schema cache you had. Here the list stays and the executable set narrows, decoupling governance from cache economics.

4. Streaming and transport#

The SSE rules are few and hard: Content-Type: text/event-stream, each data object a JSON string, the event field must match the body’s type, and the stream must end on a terminal event (response.completed, response.failed or response.incomplete). [DONE] is optional and clients must accept its absence. Servers “SHOULD NOT” use SSE’s own id; ordering comes from sequence_number.

The 2026-04-24 version added WebSocket transport on the same /v1/responses resource, each turn opening with {"type":"response.create"} and using the same event objects. The limits are spelled out:

  • one in-flight response per connection; parallelism means more connections;
  • a 60-minute connection cap, then websocket_connection_limit_reached;
  • continuation via previous_response_id, which the server may hold in connection-local memory — which is exactly what makes store=false (zero data retention) deployments workable; if it cannot find it, previous_response_not_found, never a silent downgrade;
  • a separate /responses/compact returns a compacted input window, not a response id; after compaction you drop previous_response_id or set it to null.

Where state lives becomes a choice. In a zero-retention deployment the context travels with the connection, so dropping the connection loses it.

5. Extensions: a proper superset counts#

Three layers, increasingly permissive:

  1. Hosted tools: an implementer’s own search, retrieval or orchestration tool, named implementor_slug:tool_name, executed inside the implementer’s environment. The spec requires a matching item type as a receipt — how the model invoked it, the status, the result — and that receipt must round-trip losslessly on a follow-up request.
  2. Extended items and events: slug-prefixed, each event carrying at least type and a monotonic sequence_number, and clients must be able to ignore what they do not recognise.
  3. Extended schemas: extra fields are allowed, but must not change the meaning of standard ones, and should be optional — the docs say plainly that this reduces portability.

Then there is the governance path: a feature that frontier models broadly adopt gets proposed to the TSC for standardisation. Spec, implementation and governance finally form one loop.

6. Where the ecosystem stands#

ItemValueSource
Spec versions2026-01-15, 2026-04-24the site’s version index
Licence / stars / forksApache-2.0 / 1,197 / 70GitHub API
Repository created2026-01-15GitHub API
Open issues47GitHub API
Languages (bytes)TypeScript 269K, MDX 92K, Python 90K, TypeSpec 61KGitHub languages API
Schema sources9 .tsp files (items, events, tools, responses, content, configuration, routes, helpers, main)repo schema/
Conformancebin/compliance-test.ts and friends; a compliance page on the siterepo bin/
Live implementationsDatabricks Model Serving (Claude, Gemini, hosted open models), Vercel AI Gatewaytheir docs
Governancea TSC under AAIFspec site

The spec is not a hand-written OpenAPI document but a typed source, written in TypeSpec, that generates clients and schemas. That keeps clients and prose from drifting; the price is a higher contribution bar — a field proposal means writing the DSL.

7. What actually changed, and what did not#

Changed:

  • Adapters become configuration. Endpoint plus model name is a setting, not an if (provider === …) branch; gateways (Vercel) and hosting platforms (Databricks) accept the same body and translate downward for you.
  • Evals and routing get a common floor: one request body against four vendors, with logs and latency aligned under one schema.
  • Tool calls and streaming get a semantic layer: a call is an item, and execution authority is either yours (external tools) or explicitly the platform’s (hosted tools).

Unchanged:

  • Compliant is not equivalent. The spec guarantees a request is understood and a stream reconstructable; it does not promise comparable reasoning, identical truncation policy, or the same hosted toolset.
  • Extension fields do not travel. A client built on acme:telemetry_chunk must handle its absence elsewhere.
  • The MUSTs only matter if implementers mean them. allowed_tools says the server must enforce — whether it does is something you test.

8. Four checks you can run#

  1. curl a streaming request and see whether the last event is a terminal one, rather than the socket just closing.
  2. Narrow allowed_tools to one tool, then steer the model at another. The server should reject or suppress it, not run it.
  3. With truncation: "disabled", send an over-long input. It should fail, not silently truncate.
  4. Feed the client an unknown acme:* event. It should ignore it and finish the standard stream intact.

9. Open questions#

  • One response per WebSocket conflicts with the connection reuse and multiplexing that gateways like to do. For low-latency multi-turn agents, is it one socket per session, or a connection pool inside the gateway? The spec does not say.
  • “A proper superset counts as compliant” gives implementers room, but the value of the phrase “Open Responses compatible” now depends on their discipline. Should there be a per-field compatibility matrix instead of a boolean?

References#

Search 40 essays

Browse all writing