El 22 de octubre de 2026, en AGNTCon + MCPCon North America en San José, la Agentic AI Foundation (AAIF) de la Linux Foundation sumó cuatro proyectos: Agent Skills de Anthropic, Open Responses de OpenAI, OpenEnv de Hugging Face y OpenSandbox de Alibaba. De los cuatro, Open Responses es el que cambia el código de cliente que escribís hoy. Es una especificación abierta, derivada de la Responses API de OpenAI, para llamar a cualquier proveedor de modelos con los mismos items, eventos de streaming y llamadas a tools.
Cloné el repo de la spec (commit 8907bc3) y leí la versión 2026-04-24, el archivo OpenAPI y el runner de compliance. El 28 de octubre de 2026 revisé la documentación de cada proveedor, leí el código de Ollama y de vLLM donde la documentación no decía nada, y contrasté las firmas del SDK en openai 3.40.0 (PyPI) y 7.45.1 (npm). No llamé a ninguna API ni corrí la suite: en esta computadora no hay un servidor de modelos local.
1. El Problema: Cada Proveedor Habla una API de Chat Ligeramente Distinta
Chat Completions se volvió un estándar de facto porque todos lo clonaron, y cada clon se desvía en los bordes: la forma de las tool calls, dónde va el razonamiento, qué chunks emite un stream, cómo se ve un error. Anthropic sigue definiendo su propia Messages API y Google su Interactions API, como señala la nota de InfoQ sobre AGNTCon. Tu loop de agente termina codificando los supuestos de un proveedor, y cambiar implica reescribirlo.
Open Responses trabaja a nivel del protocolo. Estandariza los items, los eventos semánticos de streaming y el loop de tool calls, y deja que cada vendor sume funciones bajo un namespace con prefijo sin romper el núcleo. Si un servidor la implementa, cambiás de proveedor cambiando una base URL, una key y un ID de modelo. El resto del post comprueba qué tan cierto es eso hoy.
Proyectos en AAIF
10
Eran 3 en el lanzamiento de diciembre de 2025; 4 se sumaron el 22 oct 2026
Última spec
2026-04-24
Segunda versión fechada después de 2026-01-15. Las versiones fechadas son inmutables
Licencias
Apache-2.0 + CC BY 4.0
Código y documentación, según el charter adoptado el 21 oct 2026
GitHub
~1.2K stars
openresponses/openresponses al 28 oct 2026. 9 maintainers de 7 empresas
La advertencia honesta
Es un estándar abierto construido sobre la API de un solo vendor. Simon Willison lo llamó el esfuerzo de estandarización que más quería, aunque aclaró que habría preferido Chat Completions como base, y un desarrollador citado por InfoQ lo describió como “un poco irónico, pero práctico”. La respuesta en gobernanza es el charter: las decisiones técnicas quedan en un TSC cuyos nueve maintainers iniciales vienen de OpenAI, Databricks, Amazon, Hugging Face, Ollama, OpenRouter y Ramp.
Dónde encajan los otros tres proyectos nuevos de AAIF
Agent Skills
Anthropic
Un formato abierto para empaquetar capacidades reutilizables que un agente carga cuando las necesita. Vive por encima de la API: qué puede hacer tu agente, no cómo habla con un modelo.
OpenEnv
Hugging Face
Un framework y un hub comunitario de entornos de reinforcement learning compartibles, para entrenar y evaluar agentes antes de producción.
OpenSandbox
Alibaba
Una plataforma de sandbox de uso general para apps de IA, con SDKs en varios lenguajes y runtimes de Docker y Kubernetes: donde las tools de tu agente ejecutan código.
Los otros seis proyectos de AAIF son A2A, Agent Router, agentgateway, AGENTS.md, goose y MCP. Regla práctica: Open Responses es el formato de protocolo entre tu loop de agente y el modelo; MCP va entre tu agente y sus tools.
2. Los Conceptos Mínimos (Solo lo que Define la Spec)
La spec es corta si te salteás los ejemplos. Acá tenés las seis ideas que necesita el tutorial, tal como las definen la versión 2026-04-24 y su archivo OpenAPI.
Items
La unidad atómica de contexto, en las dos direcciones. Los tipos estándar, que se distinguen por type, son message, function_call, function_call_output y reasoning (más compaction). Todo item tiene id, type y status, y los items de salida pasan de in_progress a completed o incomplete. Los items propios de un vendor llevan un prefijo con su slug, como openai:web_search_call.
El objeto response
POST /v1/responses devuelve un recurso response: id, status, model, el array output de items, usage y un eco de tu configuración, incluidos store y previous_response_id. Un input de tipo string cuenta como un mensaje de usuario. Desde 2026-04-24, los mensajes del asistente pueden llevar un phase opcional (commentary o final_answer).
Eventos de streaming
Con stream: true el servidor manda SSE cuyo nombre de evento coincide con el type del JSON. Los eventos son semánticos: transiciones de estado (response.created, response.in_progress, response.completed) y deltas (response.output_item.added, response.output_text.delta, response.function_call_arguments.delta). Todo stream tiene que terminar con response.completed, response.failed o response.incomplete, y el [DONE] final es opcional.
Tools
El único tipo de tool del schema central es function. Las tools alojadas afuera (tus funciones) te devuelven el control. Las alojadas adentro (búsqueda, file search) corren dentro del proveedor y aparecen como tools e items con prefijo de slug. tool_choice acepta auto, required, none, una función específica o un subconjunto allowed_tools.
previous_response_id y store
previous_response_id hace que el servidor cargue el input y el output de la respuesta anterior y agregue tu input nuevo, en ese orden. store es un booleano descrito como “si guardar la respuesta para recuperarla después”, pero la spec no define ningún endpoint para recuperarla. Por WebSocket, una cadena con store=false vive solo en la memoria local de la conexión.
Items de razonamiento
Un item reasoning puede traer el contenido crudo, un blob opaco encrypted_content que solo su proveedor puede volver a leer, y un summary que se puede mostrar a usuarios. Los tres son opcionales. Para reenviar el razonamiento cifrado, el enum include de la spec ofrece reasoning.encrypted_content.
Lo que la spec no define
Hay exactamente dos rutas HTTP: POST /responses y POST /responses/compact. No hay GET ni DELETE por ID, ni API de conversaciones, ni otro tipo de tool aparte de function en el núcleo. La versión 2026-04-24 agregó un transporte WebSocket opcional sobre el mismo recurso /v1/responses, con una sola respuesta en curso por conexión y un límite de 60 minutos por conexión. Cualquier otra cosa que ofrezca un proveedor es una extensión, así que tratala como no portable.
3. Paso 1: El Mismo Request a Siete Proveedores
No necesitás un SDK nuevo. El SDK oficial openai ya habla este protocolo, así que apuntás base_url a otro servidor. Las base URLs y los IDs de modelo de abajo salen de la documentación de cada proveedor. El request manda solo los dos items que manda el propio test system-prompt de la suite de compliance, así que cualquier servidor que pase ese test debería aceptarlo.
0. Levantá los servidores locales (salteá los que no uses)
# 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. Un cliente, siete proveedores (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}")La misma llamada en 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);Los IDs de modelo van en la config, no en el request
Cada proveedor tiene su propio namespace: gpt-6-astra en OpenAI, proveedor/modelo en OpenRouter y Vercel, repo:proveedor en Hugging Face (o un sufijo de política como :fastest o :cheapest), tags de Ollama como qwen3:8b, y en vLLM el repo que hayas servido.
Nunca pases api_key=None a otro proveedor
En openai 3.40.0, si falta api_key el SDK usa la variable de entorno OPENAI_API_KEY sin importar qué base_url hayas puesto, y el SDK de TypeScript hace lo mismo con un apiKey undefined. Un helper que hace api_key=os.getenv("OPENROUTER_API_KEY") con esa variable vacía le manda en silencio tu key de OpenAI a OpenRouter. Por eso client_for lee os.environ[...] y falla de forma explícita. Los servidores locales igual necesitan algún string: la documentación de Ollama dice que la key es “obligatoria pero se ignora”.
4. Paso 2: Streaming sin Parsers por Proveedor
El streaming es donde suelen romperse los clientes multi-proveedor, porque los chunks de cada vendor se ven distintos. Con Open Responses hacés un switch sobre event.type e ignorás lo que no reconocés, como la spec les exige a los clientes con los eventos de vendors.
2. Streameá el texto y quedate con el evento terminal
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")Los eventos que realmente necesitás
- response.created / response.in_progress
- Transiciones de estado. Sirven para un spinner, no para el contenido.
- response.output_item.added / .done
- Un item se abre y se cierra. En un function_call, added ya trae el name.
- response.output_text.delta / .done
- Deltas de texto de una content part, y después el texto completo.
- response.function_call_arguments.delta / .done
- Los argumentos de la tool mientras llegan. El evento .done trae el string JSON final.
- response.reasoning_summary_text.delta
- Texto del resumen de razonamiento, que se puede mostrar a usuarios. Mismo nombre en la spec y en el SDK de OpenAI.
- response.completed / .failed / .incomplete
- Eventos terminales que traen el objeto response final. Todo stream tiene que terminar con uno de ellos.
Un nombre de evento difiere del SDK de OpenAI
Para el texto de razonamiento crudo, la spec (tanto la versión 2026-01-15 como la 2026-04-24) define response.reasoning.delta y response.reasoning.done, mientras que openai 3.40.0 tipa los eventos de OpenAI como response.reasoning_text.delta y .done. El código de Ollama emite solo los eventos de resumen de razonamiento. Si armás un panel de “pensamiento”, matcheá los tres nombres y tratá el razonamiento crudo como opcional. En el SDK de Python, los tipos de evento desconocidos conservan su string type original, así que matchear sobre event.type sigue funcionando.
5. Paso 3: Un Ida y Vuelta de Function Calling
Este es el loop agéntico de la spec en su mínima expresión. El modelo emite un item function_call, vos ejecutás la función y devolvés un function_call_output con el mismo call_id. Mantengo la conversación del lado del cliente, porque es el único patrón que soportan todos los proveedores de este post.
3. Ida y vuelta stateless (la versión portable)
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)El atajo stateful, solo donde el servidor guarda estado
# 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,
)Por qué agrego todos los items de salida
La guía de razonamiento de OpenAI recomienda reenviar todos los items de reasoning, function call y function call output, y su guía de estado de conversación agrega el array output completo. El ejemplo de vLLM agrega el item function_call de la misma forma, y el parser de input de Ollama acepta items reasoning, function_call y function_call_output. Reenviar el output completo al mismo proveedor es el loop más portable que encontré.
Mandá store=False solo donde cambia algo
OpenAI guarda los objetos response durante 30 días por defecto, así que la entrada de OpenAI en PROVIDERS manda store=False. En ese modo stateless, sus items de razonamiento ahora traen encrypted_content por defecto. OpenRouter rechaza store: true con un 400, vLLM lo baja a False en silencio y el struct de request de Ollama ni siquiera tiene un campo store. Por eso el flag vive en la config de cada proveedor y no en el request compartido.
6. Paso 4: Corré la Suite de Compliance Contra tu Endpoint
El repo trae una sola suite con dos frontends: el tester de navegador en openresponses.org/compliance y un CLI de Bun en bin/compliance-test.ts. Los dos corren las mismas 17 plantillas de test de src/lib/compliance-tests.ts. Correla primero contra tu propio servidor o gateway.
4. Cloná, instalá, corré (comandos del README y del --help del CLI)
# 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.jsonQué cubren los 17 tests
10 tests HTTP (navegador y CLI)
- • basic-response: un mensaje de usuario, validado contra el schema de response
- • system-prompt y multi-turn: un rol system e historial del asistente en el input
- • streaming-response: eventos SSE, su schema y un evento terminal completed
- • tool-calling: una function tool tiene que producir un item function_call
- • image-input: una imagen del usuario, así que necesita un modelo con visión
- • assistant-phase: el servidor acepta las fases commentary y final_answer
- • compact-response y compact-missing-model: POST /responses/compact y su regla de 400/422
- • response-output-phase-schema: un fixture de schema local que no manda ningún request
7 tests WebSocket (solo CLI)
- • websocket-response y websocket-sequential-responses: eventos por un mismo socket, un turno a la vez
- • websocket-continuation: encadenamiento con store=false y previous_response_id en el mismo socket
- • websocket-previous-response-not-found y websocket-reconnect-store-false-recovery: el error previous_response_not_found después de una respuesta faltante o perdida
- • websocket-failed-continuation-evicts-cache: un turno fallido saca de la caché la respuesta que referenciaba
- • websocket-compact-new-chain: una cadena nueva armada con el output de /responses/compact
Antes de usar el tester de navegador
La página llama a tu endpoint directo desde el navegador, así que el servidor tiene que permitir CORS (LM Studio tiene lms server start --cors), y escribís tu API key en una página web. Usá una key descartable con límite de gasto, o usá el CLI. El navegador se saltea los siete tests WebSocket a propósito, porque los navegadores no pueden setear el header de autorización en un WebSocket.
Lo que una corrida en verde no demuestra
Los tests HTTP nunca mandan previous_response_id, nunca reenvían items de razonamiento y nunca devuelven un function_call_output: tool-calling solo verifica que vuelva un function_call. La semántica de store tampoco se testea por HTTP. Pasar significa que tu servidor habla las formas básicas, no que tu loop de agente vaya a funcionar, y por eso importa el paso 3. El otro punto de Simon Willison sigue en pie: no hay una suite de conformidad para clientes.
Usala como gate de CI
El CLI sale con código 1 si falla cualquier test. --json imprime un resumen con los conteos de passed, failed y skipped, y --filter acepta IDs de test separados por comas. La key puede venir de OPENRESPONSES_API_KEY en lugar de --api-key. Los tests corren en paralelo y cada request HTTP tiene un límite de 30 segundos. El modelo por defecto es gpt-4o-mini, así que pasá --model para cualquier endpoint que no sea OpenAI.
7. Diferencias entre Proveedores, Según su Propia Documentación
La pared de logos de openresponses.org lista 12 organizaciones: NVIDIA, Vercel, OpenRouter, Hugging Face, LM Studio, Databricks, Red Hat, AWS, Ollama, OpenAI, vLLM y Llama Stack. La cobertura del lanzamiento en enero nombraba seis adoptantes. Un logo no es un resultado de compliance, así que esta tabla muestra lo que cada proveedor documenta hoy (revisado el 28 oct 2026), más el código fuente donde la documentación no decía nada.
| Proveedor | Base URL | IDs de modelo | Estado | Streaming | Tools |
|---|---|---|---|---|---|
| OpenAI | https://api.openai.com/v1 | gpt-6-astra | Se guarda 30 días por defecto; previous_response_id; store=false para stateless | SSE; también documenta modo WebSocket | Funciones más tools alojadas (extensiones del vendor) |
| OpenRouter | https://openrouter.ai/api/v1 | proveedor/modelo, p. ej. openai/gpt-6.1-terra | Solo stateless: store: true o previous_response_id devuelven 400 | Sí (stream: true) | Funciones, búsqueda web |
| Hugging Face (beta) | https://router.huggingface.co/v1 | repo:proveedor, p. ej. openai/gpt-oss-120b:groq | No documentado: mandá el historial completo | Sí | Funciones, MCP remoto, structured outputs |
| Vercel AI Gateway | https://ai-gateway.vercel.sh/v1 | proveedor/modelo, p. ej. anthropic/claude-sonnet-5.5 | No documentado: mandá el historial completo | Sí | Funciones, structured outputs; fallbacks con providerOptions |
| Ollama (v0.13.3+) | http://localhost:11434/v1 | Tags locales, p. ej. qwen3:8b | Solo stateless; previous_response_id, conversation y truncation no soportados | Sí | Funciones; tool_choice no está en la lista de soportados |
| LM Studio (0.3.29+) | http://localhost:1234/v1 | p. ej. openai/gpt-oss-20b | Soporta previous_response_id | Sí (SSE) | Tools custom, MCP remoto (opt-in) |
| vLLM | http://localhost:8000/v1 | El repo que serviste | store se ignora; previous_response_id devuelve 404 salvo con VLLM_ENABLE_RESPONSES_API_STORE=1 (en memoria) | Sí | Funciones con --enable-auto-tool-choice y un parser de tool calls |
“No documentado” significa que no lo encontré en la documentación de Responses de ese proveedor, no que falle. Ninguna de las páginas que leí publica resultados de compliance de Open Responses, así que corré la suite contra los endpoints de los que dependés.
8. Errores Comunes (y la Solución de Cada Uno)
Los modos de falla que podés esperar cuando un mismo loop se cruza con servidores distintos, cada uno respaldado por una documentación o una línea de código.
Campos no soportados: rechazados acá, ignorados allá
Síntoma: Un campo que funciona en OpenAI recibe un 400 en un servidor y no hace nada en otro. OpenRouter rechaza store: true, vLLM convierte store=True en False sin avisarte, y el struct de request de Ollama no tiene campos store ni previous_response_id, así que su decoder de JSON directamente los descarta.
Solución: Limitá el request compartido a campos que estén en la spec y en la lista de soportados del proveedor, y después revisá el eco: todo objeto response tiene que incluir store y previous_response_id, y Ollama siempre devuelve false y null, lo que deja al descubierto el descarte.
Respuestas stateless vs guardadas
Síntoma: vLLM responde a un previous_response_id con un 404 “Response with id … not found” salvo que lo hayas levantado con VLLM_ENABLE_RESPONSES_API_STORE=1, que guarda cada respuesta en memoria y, según los propios comentarios de su código, nunca la libera. En Ollama el mismo request pierde el contexto sin ningún error.
Solución: Hacé que el historial del lado del cliente sea el default y que previous_response_id sea una optimización que activás por proveedor. En OpenAI tampoco ahorra plata: todos los tokens de input anteriores de la cadena se cobran como input.
Items de razonamiento
Síntoma: Los loops de tools con modelos de razonamiento se degradan cuando descartás los items de razonamiento entre turnos, y un blob encrypted_content de un proveedor no significa nada para otro: la spec dice que su formato es opaco y específico de cada proveedor.
Solución: Reenviá cada item de salida al mismo proveedor, sin tocarlo. Si cambiás de proveedor a mitad de conversación, mi regla es descartar los items de razonamiento y conservar mensajes, function calls y sus outputs. Es una inferencia mía a partir de la spec, no un comportamiento documentado.
Mapeo de nombres de modelo
Síntoma: model_not_found o un 404 justo después de cambiar base_url, porque gpt-6-astra, openai/gpt-6.1-terra, openai/gpt-oss-120b:groq y qwen3:8b viven en namespaces distintos.
Solución: Guardá el ID de modelo junto a la base URL y la key, como hace PROVIDERS. En vLLM, leelo de client.models.list(). En Hugging Face, decidí si querés un sufijo :proveedor fijo o una política de ruteo como :fastest.
Diferencias en los eventos de streaming
Síntoma: Un panel de “pensamiento” queda vacío con un proveedor, o un parser estricto se cae con un evento que no conoce, como un evento de vendor con prefijo de slug o response.reasoning.delta donde esperaba response.reasoning_text.delta.
Solución: Hacé switch sobre event.type, ignorá los tipos desconocidos y tomá el estado final del objeto response del evento terminal, no de los deltas acumulados. Aceptá streams que terminen con o sin [DONE]; la spec permite las dos cosas.
Rutas de la base URL
Síntoma: 404 en /v1/v1/responses o en /responses, porque las herramientas no se ponen de acuerdo en qué significa “URL”. El SDK openai y el CLI de compliance agregan /responses a una base URL que termina en /v1, mientras que el provider @ai-sdk/open-responses del AI SDK quiere el endpoint completo …/v1/responses.
Solución: Guardá la base URL terminada en /v1 y derivá el resto. Comprobala una vez con el CLI de compliance y --filter basic-response antes de conectarla a un agente.
Mi regla práctica
Escribí el loop contra la spec, no contra un vendor: historial stateless, solo el tipo de tool function, eventos matcheados por nombre con una rama por defecto y los IDs de modelo en la config. Sumá previous_response_id, tools alojadas y eventos de vendor como extras opcionales por proveedor. Así obtenés la portabilidad que promete Open Responses sin apostar a las partes que cada servidor todavía resuelve distinto.
Conclusión
Que Open Responses pase a la AAIF importa menos por el logo que por el proceso: un charter, un TSC con maintainers de siete empresas, versiones de la spec fechadas e inmutables y una suite de tests que cualquiera puede correr. El núcleo es lo bastante chico para aprenderlo en una tarde: items, eventos semánticos, function tools y un previous_response_id opcional.
Para lo básico, la portabilidad ya es real: el mismo request llega a los siete proveedores de este post con el SDK que ya usás. El estado, el reenvío del razonamiento y los nombres de eventos todavía difieren, así que mantené tu loop stateless, corré el CLI de compliance contra cada endpoint del que dependés y tomá una corrida en verde como el piso, no como la meta.
Sello de verificación
AAIF: su anuncio del 22 de octubre de 2026 y la nota de InfoQ del 26 de octubre. Spec: openresponses/openresponses en el commit 8907bc3 (~1.2K stars al 28 de octubre de 2026). Proveedores: la documentación de cada uno al 28 de octubre de 2026, más openai/responses.go de Ollama y responses/serving.py y envs.py de vLLM en main. SDK: openai 3.40.0 (PyPI) y 7.45.1 (npm). No ejecuté nada del código ni llamé a ninguna API.
Fuentes
- AAIF: la Agentic AI Foundation anuncia cuatro proyectos nuevos (22 oct 2026)
- InfoQ: AGNTCon + MCPCon 2026, AAIF suma cuatro proyectos (26 oct 2026)
- Open Responses: especificación (versión 2026-04-24)
- openresponses/openresponses en GitHub (fuente de la spec y runner de compliance)
- Open Responses: charter técnico (adoptado el 21 oct 2026)
- Open Responses: tests de aceptación en el navegador
- Simon Willison: Open Responses (15 ene 2026)
- InfoQ: cobertura del lanzamiento de Open Responses (2 feb 2026)
- OpenRouter: Responses API (solo stateless)
- Hugging Face Inference Providers: Responses API (beta)
- Vercel AI Gateway: OpenResponses API
- Ollama: compatibilidad con OpenAI, /v1/responses
- LM Studio: endpoint de Responses
- vLLM: servidor compatible con OpenAI, Responses API
- OpenAI: estado de conversación (store, previous_response_id, retención)
- OpenAI: guía de razonamiento (modo stateless, razonamiento cifrado)



