On 22 October 2026, at AGNTCon + MCPCon North America in San Jose, the Linux Foundation’s Agentic AI Foundation (AAIF) took in four projects: Agent Skills from Anthropic, Open Responses from OpenAI, OpenEnv from Hugging Face and OpenSandbox from Alibaba. Of the four, Open Responses is the one that changes the client code you write today. It’s an open spec, derived from OpenAI’s Responses API, for calling any model provider with the same items, streaming events and tool calls.
I cloned the spec repo (commit 8907bc3) and read the 2026-04-24 release, the OpenAPI file and the compliance runner. On 28 Oct 2026 I checked every provider’s docs, read Ollama’s and vLLM’s source where the docs were silent, and checked SDK signatures in openai 3.40.0 (PyPI) and 7.45.1 (npm). I didn’t call any API or run the suite: there’s no local model server on this machine.
1. The Problem: Every Provider Speaks a Slightly Different Chat API
Chat Completions became a de facto standard because everyone cloned it, and every clone drifts at the edges: how tool calls are shaped, where reasoning goes, which chunks a stream emits, what an error looks like. Anthropic still defines its own Messages API and Google its Interactions API, as InfoQ’s AGNTCon report points out. Your agent loop ends up encoding one provider’s assumptions, and switching means rewriting it.
Open Responses works at the wire level. It standardizes items, semantic streaming events and the tool-call loop, and it lets vendors add features under a prefixed namespace without breaking the core. If a server implements it, you swap providers by changing a base URL, a key and a model ID. The rest of this post checks how true that is today.
AAIF projects
10
Up from 3 at the December 2025 launch; 4 added on 22 Oct 2026
Latest spec
2026-04-24
Second dated release after 2026-01-15. Dated releases are immutable
Licenses
Apache-2.0 + CC BY 4.0
Code and docs, per the charter adopted on 21 Oct 2026
GitHub
~1.2K stars
openresponses/openresponses on 28 Oct 2026. 9 maintainers from 7 companies
The honest caveat
It’s an open standard built on one vendor’s API. Simon Willison called it the standardization effort he most wanted, while noting he’d have preferred Chat Completions as the base, and a developer quoted by InfoQ called it “slightly ironic, but practical”. The governance answer is the charter: technical decisions sit with a TSC whose nine initial maintainers come from OpenAI, Databricks, Amazon, Hugging Face, Ollama, OpenRouter and Ramp.
Where the other three new AAIF projects fit
Agent Skills
Anthropic
An open format for packaging reusable capabilities an agent loads on demand. It sits above the API: what your agent can do, not how it talks to a model.
OpenEnv
Hugging Face
A framework and community hub for shareable reinforcement learning environments, used to train and evaluate agents before production.
OpenSandbox
Alibaba
A general-purpose sandbox platform for AI apps, with multi-language SDKs and Docker and Kubernetes runtimes: where your agent’s tools run code.
The other six AAIF projects are A2A, Agent Router, agentgateway, AGENTS.md, goose and MCP. Rule of thumb: Open Responses is the wire format between your agent loop and the model; MCP sits between your agent and its tools.
2. The Minimal Concepts (Only What the Spec Defines)
The spec is short once you skip the examples. Here are the six ideas the tutorial needs, as the 2026-04-24 release and its OpenAPI file define them.
Items
The atomic unit of context, in both directions. The standard types, keyed by type, are message, function_call, function_call_output and reasoning (plus compaction). Every item has an id, a type and a status, and output items move from in_progress to completed or incomplete. Vendor items must carry a slug prefix, such as openai:web_search_call.
The response object
POST /v1/responses returns a response resource: id, status, model, the output array of items, usage, and an echo of your settings, including store and previous_response_id. A string input counts as one user message. Since 2026-04-24, assistant messages can carry an optional phase (commentary or final_answer).
Streaming events
With stream: true the server sends SSE whose event name matches the JSON type. Events are semantic: state transitions (response.created, response.in_progress, response.completed) and deltas (response.output_item.added, response.output_text.delta, response.function_call_arguments.delta). Every stream must end with response.completed, response.failed or response.incomplete, and a trailing [DONE] is optional.
Tools
The only tool type in the core schema is function. Externally hosted tools (your functions) hand control back to you. Internally hosted ones (search, file search) run inside the provider and show up as slug-prefixed tools and items. tool_choice takes auto, required, none, a specific function or an allowed_tools subset.
previous_response_id and store
previous_response_id makes the server load the earlier response’s input and output and append your new input, in that order. store is a boolean described as “whether to store the response so it can be retrieved later”, yet the spec defines no endpoint to retrieve it. Over WebSocket, a store=false chain lives only in connection-local memory.
Reasoning items
A reasoning item may carry raw content, an opaque encrypted_content blob that only its provider can read back, and a summary that’s safe to show users. All three are optional. To round-trip encrypted reasoning, the spec’s include enum offers reasoning.encrypted_content.
What the spec does not define
There are exactly two HTTP paths: POST /responses and POST /responses/compact. There’s no GET or DELETE by ID, no conversations API and no tool type beyond function in the core. The 2026-04-24 release added an optional WebSocket transport on the same /v1/responses resource, with one in-flight response per connection and a 60-minute connection limit. Anything else a provider offers is an extension, so treat it as non-portable.
3. Step 1: The Same Request to Seven Providers
You don’t need a new SDK. The official openai SDK already speaks this wire format, so you point base_url at another server. The base URLs and model IDs below come from each provider’s docs. The request sends only the two items the compliance suite’s own system-prompt test sends, so any server that passes that test should accept it.
0. Start the local servers (skip the ones you don’t use)
# Ollama (Responses support since v0.13.3) listens on port 11434
ollama pull qwen3:8b
# LM Studio (Responses support since 0.3.29): load openai/gpt-oss-20b in the app, then
lms server start --port 1234
# vLLM: flags from its Responses tool-calling example, plus an API key (needs a GPU)
vllm serve Qwen/Qwen3-1.7B --reasoning-parser qwen3 \
--enable-auto-tool-choice --tool-call-parser hermes \
--api-key token-abc1231. One client, seven providers (Python)
# pip install --upgrade openai (signatures checked against openai 3.40.0)
import os
import openai
from openai import OpenAI
# Base URLs and model IDs from each provider's docs (Oct 2026).
# The SDK appends /responses, so every base_url stops at /v1.
PROVIDERS = {
"openai": {"base_url": "https://api.openai.com/v1", "key_env": "OPENAI_API_KEY",
"model": "gpt-6-astra", "extra": {"store": False}}, # OpenAI stores by default
"openrouter": {"base_url": "https://openrouter.ai/api/v1", "key_env": "OPENROUTER_API_KEY",
"model": "openai/gpt-6.1-terra"},
"huggingface": {"base_url": "https://router.huggingface.co/v1", "key_env": "HF_TOKEN",
"model": "openai/gpt-oss-120b:groq"},
"vercel": {"base_url": "https://ai-gateway.vercel.sh/v1", "key_env": "AI_GATEWAY_API_KEY",
"model": "anthropic/claude-sonnet-5.5"},
"ollama": {"base_url": "http://localhost:11434/v1", "key": "ollama", "model": "qwen3:8b"},
"lmstudio": {"base_url": "http://localhost:1234/v1", "key": "lm-studio",
"model": "openai/gpt-oss-20b"},
"vllm": {"base_url": "http://localhost:8000/v1", "key": "token-abc123",
"model": "Qwen/Qwen3-1.7B"},
}
def client_for(name):
p = PROVIDERS[name]
# Never pass api_key=None: the SDK falls back to OPENAI_API_KEY
# and would send your OpenAI key to whatever base_url you set.
key = p.get("key") or os.environ[p["key_env"]]
return OpenAI(base_url=p["base_url"], api_key=key), p["model"], p.get("extra", {})
# The same two items the compliance suite's system-prompt test sends
INPUT = [
{"type": "message", "role": "system", "content": "Answer in one short sentence."},
{"type": "message", "role": "user", "content": "What does HTTP status 429 mean?"},
]
for name in PROVIDERS:
try:
client, model, extra = client_for(name)
r = client.responses.create(model=model, input=INPUT, **extra)
print(f"{name:12} {r.status} {r.output_text!r}")
except (KeyError, openai.APIError) as e: # missing key, server down, 4xx/5xx
print(f"{name:12} skipped: {type(e).__name__}: {e}")The same call in TypeScript
// npm i openai (types checked against openai 7.45.1)
import OpenAI from "openai";
// One provider per run: set BASE_URL, API_KEY and MODEL
const client = new OpenAI({
baseURL: process.env.BASE_URL ?? "http://localhost:11434/v1",
apiKey: process.env.API_KEY ?? "ollama", // never undefined: it falls back to OPENAI_API_KEY
});
const response = await client.responses.create({
model: process.env.MODEL ?? "qwen3:8b",
input: [
{ type: "message", role: "system", content: "Answer in one short sentence." },
{ type: "message", role: "user", content: "What does HTTP status 429 mean?" },
],
});
console.log(response.status, response.output_text);Keep model IDs in config, not in the request
Each provider has its own namespace: gpt-6-astra on OpenAI, provider/model on OpenRouter and Vercel, repo:provider on Hugging Face (or a policy suffix like :fastest or :cheapest), Ollama tags like qwen3:8b, and on vLLM whatever repo you served.
Never pass api_key=None to another provider
In openai 3.40.0, a missing api_key falls back to the OPENAI_API_KEY environment variable whatever base_url you set, and the TypeScript SDK does the same with an undefined apiKey. A helper that does api_key=os.getenv("OPENROUTER_API_KEY") with that variable unset will quietly send your OpenAI key to OpenRouter. That’s why client_for reads os.environ[...] and fails loudly instead. Local servers still need some string: Ollama’s docs call the key “required but ignored”.
4. Step 2: Streaming Without Provider-Specific Parsers
Streaming is where multi-provider clients usually break, because each vendor’s chunks look different. With Open Responses you switch on event.type and ignore what you don’t recognize, as the spec requires clients to do for vendor events.
2. Stream the text and keep the terminal event
client, model, extra = client_for("ollama") # any provider from step 1
stream = client.responses.create(
model=model,
input="Count from 1 to 5, one number per line.",
stream=True,
**extra,
)
final = None
for event in stream:
t = event.type
if t == "response.output_text.delta":
print(event.delta, end="", flush=True)
elif t in ("response.reasoning_summary_text.delta", # summaries: same name everywhere
"response.reasoning.delta", # raw reasoning, spec name
"response.reasoning_text.delta"): # raw reasoning, OpenAI SDK name
pass # route to a "thinking" panel
elif t == "response.output_item.added" and event.item.type == "function_call":
print(f"\n[tool call started: {event.item.name}]")
elif t in ("response.completed", "response.incomplete", "response.failed"):
final = event.response # terminal event: the full response
elif t == "error":
raise RuntimeError(f"stream error: {event}")
# anything else, including vendor events such as acme:trace_event: ignore it
print("\nstatus:", final.status if final else "no terminal event received")The events you actually need
- response.created / response.in_progress
- State transitions. Good for a spinner, not for content.
- response.output_item.added / .done
- An item opens and closes. For a function_call, added already carries the name.
- response.output_text.delta / .done
- Text deltas for one content part, then the full text.
- response.function_call_arguments.delta / .done
- Tool arguments as they stream. The .done event has the final JSON string.
- response.reasoning_summary_text.delta
- Reasoning summary text that’s safe to show users. Same name in the spec and in OpenAI’s SDK.
- response.completed / .failed / .incomplete
- Terminal events that carry the final response object. Every stream must end with one of them.
One event name differs from OpenAI’s SDK
For raw reasoning text, the spec (both the 2026-01-15 and 2026-04-24 releases) defines response.reasoning.delta and response.reasoning.done, while openai 3.40.0 types the OpenAI events as response.reasoning_text.delta and .done. Ollama’s source emits only the reasoning summary events. If you build a “thinking” panel, match all three names and treat raw reasoning as optional. In the Python SDK, unknown event types keep their original type string, so matching on event.type still works.
5. Step 3: One Function-Call Round Trip
This is the spec’s agentic loop at its smallest. The model emits a function_call item, you run the function, and you send back a function_call_output with the same call_id. I keep the conversation on the client, because that’s the only pattern every provider in this post supports.
3. Stateless round trip (the portable version)
import json
client, model, extra = client_for("vllm") # swap the name to test another provider
tools = [{
"type": "function",
"name": "get_weather",
"description": "Current temperature in Celsius for a city.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
"additionalProperties": False,
},
}]
def get_weather(city: str) -> str:
return json.dumps({"city": city, "celsius": 18}) # stub: call a real API here
question = {"type": "message", "role": "user", "content": "What's the weather in Montevideo?"}
history = [question]
first = client.responses.create(model=model, input=history, tools=tools, **extra)
# Keep EVERY output item (reasoning included), then answer each function_call
history += first.output
for item in first.output:
if item.type == "function_call":
args = json.loads(item.arguments)
history.append({
"type": "function_call_output",
"call_id": item.call_id, # must match the call it answers
"output": get_weather(args["city"]),
})
second = client.responses.create(model=model, input=history, tools=tools, **extra)
print(second.output_text)The stateful shortcut, only where the server keeps state
# Only where the server keeps state: OpenAI (store is on by default), LM Studio,
# or vLLM started with VLLM_ENABLE_RESPONSES_API_STORE=1. Not OpenRouter, not Ollama.
first = client.responses.create(model=model, input=[question], tools=tools)
calls = [i for i in first.output if i.type == "function_call"]
second = client.responses.create(
model=model,
previous_response_id=first.id, # the server replays first's input + output
input=[{"type": "function_call_output", "call_id": c.call_id,
"output": get_weather(json.loads(c.arguments)["city"])} for c in calls],
tools=tools,
)Why I append every output item
OpenAI’s reasoning guide recommends passing back all reasoning, function call and function call output items, and its conversation-state guide appends the whole output array. vLLM’s example appends the function_call item the same way, and Ollama’s input parser accepts reasoning, function_call and function_call_output items. Replaying the full output to the same provider is the most portable loop I found.
Send store=False only where it changes something
OpenAI saves response objects for 30 days by default, so the OpenAI entry in PROVIDERS sends store=False. In that stateless mode, its reasoning items now carry encrypted_content by default. OpenRouter rejects store: true with a 400, vLLM silently downgrades it, and Ollama’s request struct has no store field at all. That’s why the flag lives in per-provider config and not in the shared request.
6. Step 4: Run the Compliance Suite Against Your Endpoint
The repo ships one suite with two front ends: the browser tester at openresponses.org/compliance and a Bun CLI in bin/compliance-test.ts. Both run the same 17 test templates from src/lib/compliance-tests.ts. Run it against your own server or gateway first.
4. Clone, install, run (commands from the README and the CLI’s --help)
# Requires Bun (the repo pins bun 1.4.2 in .tool-versions)
git clone https://github.com/openresponses/openresponses.git
cd openresponses
bun install
# All 17 tests against a local Ollama. --api-key is mandatory; Ollama ignores it
bun run test:compliance --base-url http://localhost:11434/v1 --api-key ollama --model qwen3:8b
# Only the HTTP basics against your gateway (e.g. a LiteLLM proxy), verbose on failure
bun run test:compliance -u http://localhost:4000/v1 -k "$LITELLM_KEY" -m "$MODEL" \
--filter basic-response,system-prompt,multi-turn,streaming-response,tool-calling -v
# CI: JSON summary on stdout, exit code 1 if any test fails
bun run test:compliance -u "$API_URL" -k "$API_KEY" -m "$MODEL" --json > results.jsonWhat the 17 tests cover
10 HTTP tests (browser and CLI)
- • basic-response: one user message, validated against the response schema
- • system-prompt and multi-turn: a system role and assistant history in input
- • streaming-response: SSE events, their schema and a completed terminal event
- • tool-calling: a function tool must produce a function_call item
- • image-input: a user image, so it needs a vision model
- • assistant-phase: the server accepts commentary and final_answer phases
- • compact-response and compact-missing-model: POST /responses/compact and its 400/422 rule
- • response-output-phase-schema: a local schema fixture that sends no request
7 WebSocket tests (CLI only)
- • websocket-response and websocket-sequential-responses: events over one socket, one turn at a time
- • websocket-continuation: store=false chaining with previous_response_id on the same socket
- • websocket-previous-response-not-found and websocket-reconnect-store-false-recovery: the previous_response_not_found error after a missing or lost response
- • websocket-failed-continuation-evicts-cache: a failed turn evicts the response it referenced
- • websocket-compact-new-chain: a new chain built from /responses/compact output
Before you use the browser tester
The page calls your endpoint straight from the browser, so the server must allow CORS (LM Studio has lms server start --cors), and you type your API key into a web page. Use a throwaway key with a spend limit, or use the CLI. The browser skips the seven WebSocket tests by design, because browsers can’t set the authorization header on a WebSocket.
What a green run doesn’t prove
The HTTP tests never send previous_response_id, never replay reasoning items and never return a function_call_output: tool-calling only checks that a function_call comes back. store semantics aren’t tested over HTTP either. A pass means your server speaks the core shapes, not that your agent loop will work, which is why step 3 matters. Simon Willison’s other point still stands: there’s no conformance suite for clients.
Use it as a CI gate
The CLI exits with code 1 if any test fails. --json prints a summary with passed, failed and skipped counts, and --filter takes comma-separated test IDs. The key can come from OPENRESPONSES_API_KEY instead of --api-key. Tests run in parallel, and each HTTP request has a 30-second deadline. The default model is gpt-4o-mini, so pass --model for any non-OpenAI endpoint.
7. Provider Differences, According to Their Own Docs
The logo wall on openresponses.org lists 12 organizations: NVIDIA, Vercel, OpenRouter, Hugging Face, LM Studio, Databricks, Red Hat, AWS, Ollama, OpenAI, vLLM and Llama Stack. The January launch coverage named six adopters. A logo isn’t a compliance result, so this table shows what each provider documents today (checked 28 Oct 2026), plus source code where the docs were silent.
| Provider | Base URL | Model IDs | State | Streaming | Tools |
|---|---|---|---|---|---|
| OpenAI | https://api.openai.com/v1 | gpt-6-astra | Stored 30 days by default; previous_response_id; store=false for stateless | SSE; WebSocket mode also documented | Functions plus hosted tools (vendor extensions) |
| OpenRouter | https://openrouter.ai/api/v1 | provider/model, e.g. openai/gpt-6.1-terra | Stateless only: store: true or previous_response_id returns 400 | Yes (stream: true) | Functions, web search |
| Hugging Face (beta) | https://router.huggingface.co/v1 | repo:provider, e.g. openai/gpt-oss-120b:groq | Not documented: send the full history | Yes | Functions, remote MCP, structured outputs |
| Vercel AI Gateway | https://ai-gateway.vercel.sh/v1 | provider/model, e.g. anthropic/claude-sonnet-5.5 | Not documented: send the full history | Yes | Functions, structured outputs; fallbacks via providerOptions |
| Ollama (v0.13.3+) | http://localhost:11434/v1 | Local tags, e.g. qwen3:8b | Stateless only; previous_response_id, conversation and truncation unsupported | Yes | Functions; tool_choice isn’t in the supported list |
| LM Studio (0.3.29+) | http://localhost:1234/v1 | e.g. openai/gpt-oss-20b | previous_response_id supported | Yes (SSE) | Custom tools, remote MCP (opt-in) |
| vLLM | http://localhost:8000/v1 | The repo you served | store ignored; previous_response_id returns 404 unless VLLM_ENABLE_RESPONSES_API_STORE=1 (in memory) | Yes | Functions with --enable-auto-tool-choice and a tool-call parser |
“Not documented” means I couldn’t find it in that provider’s Responses docs, not that it fails. None of the pages I read publishes Open Responses compliance results, so run the suite against the endpoints you depend on.
8. Common Errors (and the Fix for Each)
The failure modes to expect when one loop meets different servers, each traced to a doc or a line of source.
Unsupported fields: rejected here, ignored there
Symptom: A field that works on OpenAI gets a 400 on one server and does nothing on another. OpenRouter rejects store: true, vLLM turns store=True into False without telling you, and Ollama’s request struct has no store or previous_response_id field, so its JSON decoder simply drops them.
Fix: Keep the shared request to fields in both the spec and the provider’s supported list, then check the echo: every response object must include store and previous_response_id, and Ollama always echoes false and null, which exposes the drop.
Stateless vs stored responses
Symptom: vLLM answers a previous_response_id with 404 “Response with id … not found” unless you started it with VLLM_ENABLE_RESPONSES_API_STORE=1, which keeps every response in memory and, per its own source comments, never frees them. On Ollama the same request loses context without any error.
Fix: Make client-side history the default and previous_response_id an optimization you turn on per provider. On OpenAI it doesn’t save money either: all previous input tokens in the chain are billed as input.
Reasoning items
Symptom: Tool loops on reasoning models degrade when you drop reasoning items between turns, and an encrypted_content blob from one provider means nothing to another: the spec says its format is provider-specific and opaque.
Fix: Replay every output item to the same provider, untouched. If you switch providers mid-conversation, my rule is to drop reasoning items and keep messages, function calls and their outputs. That’s my inference from the spec, not documented behavior.
Model-name mapping
Symptom: model_not_found or a 404 right after you swap base_url, because gpt-6-astra, openai/gpt-6.1-terra, openai/gpt-oss-120b:groq and qwen3:8b live in different namespaces.
Fix: Store the model ID next to the base URL and the key, as PROVIDERS does. For vLLM, read it from client.models.list(). For Hugging Face, decide whether you want a pinned :provider suffix or a routing policy like :fastest.
Streaming event differences
Symptom: A “thinking” panel stays empty on one provider, or a strict parser crashes on an event it doesn’t know, such as a slug-prefixed vendor event or response.reasoning.delta where it expected response.reasoning_text.delta.
Fix: Switch on event.type, ignore unknown types and take the final state from the terminal event’s response object, not from accumulated deltas. Accept streams that end with or without [DONE]; the spec allows both.
Base URL paths
Symptom: 404s on /v1/v1/responses or on /responses, because tools disagree on what “URL” means. The openai SDK and the compliance CLI both append /responses to a base URL that ends in /v1, while the AI SDK’s @ai-sdk/open-responses provider wants the full …/v1/responses endpoint.
Fix: Store the base URL ending in /v1 and derive the rest. Check it once with the compliance CLI and --filter basic-response before you wire it into an agent.
My rule of thumb
Write the loop against the spec, not against a vendor: stateless history, only the function tool type, events matched by name with a default branch, model IDs in config. Add previous_response_id, hosted tools and vendor events as opt-in extras per provider. That gets you the portability Open Responses promises without betting on the parts each server still does differently.
Conclusion
Moving Open Responses under AAIF matters less for the logo than for the process: a charter, a TSC with maintainers from seven companies, dated immutable spec releases and a test suite anyone can run. The core is small enough to learn in an afternoon: items, semantic events, function tools and an optional previous_response_id.
For the basics, portability is real today: the same request reaches all seven providers here through the SDK you already use. State, reasoning replay and event naming still differ, so keep your loop stateless, run the compliance CLI against every endpoint you depend on, and treat a green run as the floor, not the finish line.
Verification stamp
AAIF: its 22 Oct 2026 announcement and InfoQ’s 26 Oct report. Spec: openresponses/openresponses at commit 8907bc3 (~1.2K stars on 28 Oct 2026). Providers: each one’s docs on 28 Oct 2026, plus Ollama’s openai/responses.go and vLLM’s responses/serving.py and envs.py on main. SDK: openai 3.40.0 (PyPI) and 7.45.1 (npm). I didn’t run any of the code or call any API.
Sources
- AAIF: Agentic AI Foundation announces four new projects (22 Oct 2026)
- InfoQ: AGNTCon + MCPCon 2026, AAIF adds four projects (26 Oct 2026)
- Open Responses: specification (2026-04-24 release)
- openresponses/openresponses on GitHub (spec source and compliance runner)
- Open Responses: technical charter (adopted 21 Oct 2026)
- Open Responses: browser acceptance tests
- Simon Willison: Open Responses (15 Jan 2026)
- InfoQ: Open Responses launch coverage (2 Feb 2026)
- OpenRouter: Responses API (stateless only)
- Hugging Face Inference Providers: Responses API (beta)
- Vercel AI Gateway: OpenResponses API
- Ollama: OpenAI compatibility, /v1/responses
- LM Studio: Responses endpoint
- vLLM: OpenAI-compatible server, Responses API
- OpenAI: conversation state (store, previous_response_id, retention)
- OpenAI: reasoning guide (stateless mode, encrypted reasoning)



