Tool calls en una cadena de fallback: qué se rompe y qué miente
Un agent loop con tool calling es una conversación que el modelo escribe a medias. Cuando un gateway mueve esa conversación a otro modelo a mitad del loop, el modelo nuevo hereda los tool calls, los call IDs y el razonamiento firmado del anterior. Leímos cuatro providers, cuatro capas de traducción y la spec de MCP, corrimos un modelo local, probamos call IDs contra el validador de Mistral y sondeamos nuestra propia cotización x402. Algunos saltos rechazan el historial heredado con un 400. Otros aceptan cada control de tools y lo ignoran con un 200.
El tool calling es la primitiva debajo de todo agente. El modelo pide una función, el cliente la ejecuta, el resultado vuelve y el loop se repite hasta que el modelo responde. Cuatro partes del request le dan forma a ese loop: tools, tool_choice, parallel_tool_calls y el historial de mensajes con sus call IDs. En un solo provider significan una cosa. Detrás de un gateway con una cadena de fallback, cada eslabón los lee en su propio dialecto.
Esta es la tercera auditoría de una serie sobre lo que un gateway compatible con OpenAI realmente traduce. La auditoría de structured outputs siguió a strict: true. La auditoría de reasoning tokens siguió al output oculto. Esta sigue al sobre de tools: quién debe llamar una tool, cuántas a la vez, qué forma puede tener un ID y qué estado opaco viaja con cada llamada.
Nuestras fuentes, todas leídas o ejecutadas el 5 de octubre de 2026: la guía de function calling de OpenAI y su spec OpenAPI en 31af4fc; las páginas de Anthropic de tool use overview, guía de implementación, parallel tool use, handle tool calls, thinking, preserved thinking, refusals and fallback, errores, referencia de la Messages API y compatibilidad con el SDK de OpenAI; las guías de Google de function calling, thought signatures y Gemini 3, más la referencia de la API; la guía de function calling de Mistral y mistral-common en f3bb6e8; los docs de OpenRouter de tool calling, parámetros, selección de provider y model fallbacks; LiteLLM en 7a7d27c, vLLM en 4a30c4c y Ollama en 42e911b; y la especificación de tools de MCP 2026-07-28. También corrimos una sonda de controles de tools contra Ollama 0.30.6, validamos call IDs con mistral-common 1.12.0 y sondeamos nuestro propio 402 walk-up.
Cuatro perillas, cuatro dialectos
Empecemos por lo que cada provider documenta para forzar y limitar llamadas.
La guía de OpenAI lista cuatro modos de tool_choice: auto (el default), required, una función forzada y allowed_tools. El último restringe las llamadas a un subconjunto sin cambiar la lista tools, "so you can maximize savings from prompt caching". "none" imita no pasar funciones. parallel_tool_calls tiene default true en la spec OpenAPI, y ponerlo en false "ensures exactly zero or one tool is called". Hay una salvedad que importa para los schemas strict: en modelos fine-tuned, cuando el modelo llama varias funciones en un turno, strict mode se desactiva para esas llamadas.
Anthropic nombra cuatro opciones con otras palabras: auto, any, tool y none. No hay flag de paralelismo a nivel superior. El uso paralelo está activo por defecto, y se apaga con disable_parallel_tool_use: true dentro del objeto tool_choice: "It is not a top-level request parameter". Con auto eso significa como máximo una llamada; con any o tool, exactamente una. Cuando tool_choice es any o tool, la API hace prefill del turno del assistant, así que el modelo no escribe texto antes de la llamada.
La API generateContent de Gemini también tiene cuatro modos: AUTO, ANY, NONE y VALIDATED. El último deja que el modelo elija entre una llamada y texto, pero valida las llamadas con constrained decoding. Un subconjunto va en allowedFunctionNames, que según la referencia de la API "should only be set when the Mode is ANY or VALIDATED". El objeto FunctionCallingConfig tiene exactamente esos dos campos. No hay dónde poner parallel_tool_calls: false.
La guía de Mistral documenta auto, any y none, más parallel_tool_calls. Su propia librería de tokenizer marca any como "deprecated in favor of required" y acepta ambos, más una tool con nombre.
# Forzar y limitar tool calls, según los docs de cada provider, 5 oct 2026
control OpenAI Anthropic Gemini Mistral
decide el modelo auto auto AUTO auto
sin llamadas none none NONE none
al menos una required any ANY required (any: deprecated)
una función {type: function} {type: tool} ANY + un nombre tool con nombre
subconjunto allowed_tools - ANY/VALIDATED + names -
sin paralelismo parallel_tool_calls disable_parallel_tool_use sin campo parallel_tool_calls
= false dentro de tool_choice = false
Un traductor puede mapear casi todas las filas. El mapeo no es lo difícil. Lo difícil es que algunos destinos rechazan una fila de plano, otros la aceptan y no hacen nada, y otros adjuntan a la respuesta un estado que el siguiente request debe devolver intacto.
Rechazado: forced tool use en los Claude más nuevos
La fila con más consecuencias es "al menos una". La página de errores de Anthropic es directa: Claude Opus 5.5, Claude Sonnet 5.5, Claude Fable 5.1 y Claude Mythos 5.1 "don't support forced tool use". Enviar {"type": "any"} o {"type": "tool", ...} a cualquiera de ellos, "including on the token counting endpoint", devuelve un 400 invalid_request_error:
tool_choice: type "tool" and "any" are not supported for this model.
El reemplazo de Anthropic es auto con strict tool use para mantener los inputs válidos según el schema, o structured outputs cuando la respuesta misma necesita una forma fija. El extended thinking manual (thinking: {type: "enabled"}) tiene la misma restricción en todos los modelos. El adaptive thinking no, salvo en esos cuatro modelos.
En el dialecto de OpenAI, tool_choice: "required" es la forma estándar de decir "siempre actúa, nunca converses", y una función forzada es la forma estándar de decir "extrae en esta forma". Una capa de traducción tiene que mapearlos a any y tool, y en el tier más nuevo de Claude ambos mapeos son un 400.
LiteLLM muestra las dos maneras en que un traductor puede responder. Su model map en 7a7d27c marca 46 entradas con supports_forced_tool_use: false, que cubren Opus 5.5, Sonnet 5.5 y Fable 5.1 en Anthropic, Bedrock, Vertex, Azure y Databricks, más Mythos 5.1. Para esos modelos, common_utils.py lanza un 400 del lado del cliente que le dice al caller que use auto y pida la tool en el prompt. Si drop_params está activo, registra un warning y reescribe la elección a auto, conservando disable_parallel_tool_use.
Ambos comportamientos son defendibles. Solo uno es honesto con quien llama. El downgrade convierte "el modelo debe llamar una tool" en "el modelo puede llamar una tool", y la respuesta es un 200 en ambos casos. Un agente que confía en required para nunca recibir texto libre va a recibir texto libre, de un modelo al que quizá no sabe que fue enrutado.
Ahora ponlo en una cadena. Un request con models: ["openai/gpt-5", "anthropic/claude-opus-5.5"] y tool_choice: "required" es válido en el primer eslabón e inválido en el segundo. Funciona hasta el día en que el primer eslabón devuelve un 429. Entonces la función de confiabilidad convierte un rate limit transitorio en un 400 permanente, o en un downgrade silencioso.
Anthropic ya escribió la regla para su propio fallback. Su server-side fallback, en beta, reintenta los rechazos del clasificador en otros modelos Claude, y una regla gobierna la lista: "The request must be valid as a direct request to every model named. If a fallback model does not support a feature the request uses, the API rejects the request up front". Esa es la regla que necesita una cadena multi-provider, y aplicarla antes del primer token no cuesta nada.
Rechazado: historial que escribió otro modelo
Los controles de tools son por request. El historial es acumulativo. Para el tercer paso de un loop, el array de mensajes contiene tool calls generados por algún modelo, con IDs acuñados por algún servidor, y a veces blobs opacos que ese servidor espera de vuelta. Cuatro restricciones deciden si otro modelo acepta ese historial.
Gemini 3 quiere sus firmas de vuelta
La página de thought signatures de Google enuncia la regla en negrita: "When using Gemini 3 models, you must pass back thought signatures during function calling, otherwise you will get a validation error". La validación cubre el turno actual: cada paso del modelo después del mensaje de usuario más reciente con contenido normal. La primera parte functionCall de cada paso debe llevar su thought_signature. Con llamadas paralelas, solo la primera tiene una. Si la omites, el request falla con un 400 de la forma "Function call FC1 in the 1. content block is missing a thought_signature".
El orden también se valida. Si el modelo devolvió dos llamadas paralelas y el cliente las reenvía intercaladas con sus resultados (llamada, resultado, llamada, resultado), el FAQ dice que la API devuelve un 400. La forma esperada es ambas llamadas y luego ambos resultados.
En el endpoint compatible con OpenAI, la firma viaja en un campo no estándar, tool_calls[].extra_content.google.thought_signature, y el ejemplo de Google lo marca "Required and Validated". Un cliente, SDK o gateway que reconstruye los tool calls desde el schema tipado de OpenAI, sin dejar pasar campos desconocidos, pierde ese campo. Entonces el siguiente paso del loop falla.
El fallback lo empeora. Un historial cuyos llamados del turno actual vinieron de GPT-5 o de Claude no tiene firmas de Gemini en absoluto. La respuesta de Google es un valor dummy que salta la validación. El FAQ ofrece "context_engineering_is_the_way_to_go" o "skip_thought_signature_validator", y la guía de Gemini 3 repite el primero como el bypass para "transferring a conversation trace from another model". El FAQ también califica de "strongly discouraged" inyectar bloques de function call propios.
LiteLLM implementa las dos mitades del workaround. Para sobrevivir a clientes OpenAI que descartan campos desconocidos, factory.py incrusta la firma en el propio ID del tool call, como call_<uuid>__thought__<base64_signature>, y la vuelve a extraer en el siguiente request. Cuando una llamada no tiene firma, recurre a un skip_thought_signature_validator codificado en base64. Su propio comentario lo llama último recurso, para usar solo cuando no existe una firma real.
Un ID no es solo un ID
El truco del ID resuelve un problema y crea otro. La referencia de la Messages API de Anthropic restringe tool_use.id y tool_result.tool_use_id a ^[a-zA-Z0-9_-]+$. Base64 contiene +, / e =. Así que cuando un historial de LiteLLM que pasó por Gemini 3 se reenvía a Claude, los IDs son inválidos. LiteLLM incluye una función normalize_anthropic_tool_use_id que quita el sufijo __thought__ y reemplaza cualquier carácter inválido restante por guiones bajos.
Los modelos con formato Mistral son más estrictos. En mistral-common en f3bb6e8, los validadores de request para las versiones de tokenizer v3 a v11 exigen que cada tool call ID cumpla ^[a-zA-Z0-9]{9}$: nueve caracteres, solo letras y dígitos. El validador v13 lo relajó a cualquier ID no vacío distinto del literal null. vLLM, cuando sirve con un tokenizer de Mistral, no rechaza los IDs largos. Su tokenizers/mistral.py recorta cualquier ID de más de nueve caracteres a sus últimos nueve y registra un warning, antes de que el request llegue al validador de mistral-common.
El recorte funciona cuando la cola es alfanumérica. Falla cuando no lo es. Instalamos mistral-common 1.12.0 desde ese commit y validamos un historial de tres mensajes (usuario, tool call del assistant, tool result) con distintos IDs:
# mistral-common 1.12.0 @ f3bb6e8, validate_messages, 5 oct 2026
# "últimos 9" = lo que deja pasar truncate_tool_call_ids de vLLM
# ID de Ollama tomado de nuestra sonda; los IDs estilo OpenAI y base64 son sintéticos, con la misma forma
origen del ID ID probado v3-v11 v13
ejemplo de los docs de Mistral D681PevKs ok ok
estilo OpenAI call_ + 24, últimos 9 nWzQa5sJd ok ok
ejemplo de docs Anthropic toolu_..., últ. 9 917835lq9 ok ok
Ollama call_ + 8, tal como se generó call_2inqttcy rechazo ok
Ollama call_ + 8, últimos 9 _2inqttcy rechazo ok
cola de firma base64, últimos 9 1ZQ+/kqA= rechazo ok
La fila de Ollama es la sorpresa. En nuestras corridas, los IDs de Ollama eran call_ más ocho caracteres, trece en total, así que los últimos nueve empiezan con el guion bajo. Un historial producido en un modelo local de Ollama y reenviado a un modelo Mistral servido por vLLM con un tokenizer antiguo falla con "Tool call id was _2inqttcy but must be a-z, A-Z, 0-9, with a length of 9". Nada de la conversación está mal. Solo la forma del ID.
vLLM agrega un formato más por su cuenta. Para la familia de modelos Kimi K2, chat_utils.py acuña IDs como functions.{name}:{index} en lugar de su default chatcmpl-tool-<uuid>. Un historial que entra o sale de uno de esos modelos lleva IDs que ninguna otra familia produciría.
Nombres de tools que MCP permite y los providers no
La lista de tools tiene su propio problema de dialecto. La spec de tools de MCP 2026-07-28 dice que los nombres de tools deberían tener de 1 a 128 caracteres tomados de letras, dígitos, guion bajo, guion y punto, y da admin.tools.list como ejemplo válido. A los clientes y proxies que agregan varios servidores les pide resolver colisiones, por ejemplo "prefixing tool names with a server identifier".
La spec de OpenAI dice que el nombre de una función "must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64". La referencia de Anthropic permite ^[a-zA-Z0-9_-]{1,128}$. La lista de buenas prácticas de Google pide nombres de función "without spaces, periods, or dashes". Un nombre MCP válido según la spec que tenga un punto queda fuera de los patrones documentados de OpenAI y de Anthropic. Un nombre con prefijo que entra en los 128 caracteres de Anthropic puede desbordar los 64 de OpenAI. Un gateway que retransmite tools MCP tiene que renombrarlas, guardar el mapeo y revertirlo en cada llamada que haga el modelo.
Dónde va cada resultado
La última regla del historial es estructural. Anthropic exige que los bloques tool_result estén en el mensaje de usuario inmediatamente después del mensaje tool_use del assistant, con todos los resultados de un lote paralelo en ese único mensaje y antes de cualquier texto. El formato de chat de OpenAI envía un mensaje tool por llamada, así que un traductor tiene que fusionarlos. La página de parallel tool use de Anthropic dice que el formato incorrecto de resultados es la razón más común por la que Claude deja de hacer llamadas paralelas, porque le "enseña" a evitarlas, y señala "a separate user message for each tool result" como el patrón equivocado. Un traductor descuidado no recibe un error. Recibe un modelo que poco a poco deja de agrupar llamadas.
Ignorado: controles que vuelven con 200
Un rechazo al menos se ve. La falla más silenciosa es una capa que acepta un control y no hace nada con él.
Ollama es el caso más claro. Su página de compatibilidad con OpenAI en 42e911b marca tools como soportado y tool_choice como no soportado. parallel_tool_calls ni siquiera aparece. El struct del request en openai/openai.go no tiene campo para ninguno de los dos, así que ambos se descartan al decodificar el JSON. En nuestra auditoría de Open Responses vimos lo mismo en el endpoint de Responses. Esta vez medimos qué hace en Chat Completions.
# Ollama 0.30.6, qwen2.5:1.5b en CPU, /v1/chat/completions, 5 corridas c/u,
# temperature 0.7, 5 oct 2026. Todas las respuestas fueron HTTP 200.
control enviado prompt resultado
tool_choice "none" clima en París, usa la tool 5/5 llamaron get_weather
tool_choice "required" solo salúdame, no uses tools 0/5 llamaron una tool
tool_choice forzando get_time clima en París 5/5 llamaron get_weather
parallel_tool_calls false clima en París, Londres y Tokio 3/5 devolvieron 3 llamadas
sin control de paralelismo mismo prompt 2/5 devolvieron 3 llamadas
Todos los controles se descartaron. none no detuvo una llamada. required no forzó ninguna. La función forzada fue reemplazada por la que sugería el prompt. Y parallel_tool_calls: false igual produjo tres llamadas en tres de cinco corridas. El modelo siguió al prompt, no al request, y cada respuesta fue un 200 bien formado.
vLLM respeta más, con un matiz. Sus docs de tool calling soportan auto, required, none y funciones con nombre. required y las llamadas con nombre usan structured outputs, y auto necesita los flags de servidor --enable-auto-tool-choice y --tool-call-parser. parallel_tool_calls: false se aplica después de la generación: tool_calls_utils.py conserva el primer tool call y descarta el resto, que el modelo ya había generado. Con tool_choice: "none", vLLM igual pone las definiciones de tools en el prompt, salvo que el operador lo arranque con --exclude-tools-when-tool-choice-none. Un camino de vLLM hace lo honesto: su parser genérico de response templates rechaza tools strict, elecciones required o con nombre, y parallel_tool_calls: false, porque puede parsear esos outputs pero no restringirlos.
La página de compatibilidad con el SDK de OpenAI de Anthropic lista tool_choice y parallel_tool_calls como totalmente soportados, y el flag strict a nivel de tool como ignorado, así que "the tool use JSON is not guaranteed to follow the supplied schema". La misma página advierte que "most unsupported fields are silently ignored rather than producing errors".
Los defaults de OpenRouter plantean el mismo trade-off abiertamente. Sus docs de selección de provider dicen que con require_parameters: false, el default, los providers que no soportan un parámetro "can still receive the request, but will ignore unknown parameters". Una lista corta de parámetros funciona como preferencia blanda al elegir entre providers de un mismo modelo: tools, response_format y verbosity. tool_choice y parallel_tool_calls no están en ella. La referencia de parámetros le da a parallel_tool_calls un default true; la guía de tool calling matiza que es true "for most models".
La API nativa de Gemini, como vimos, no tiene campo para desactivar las llamadas paralelas. Un traductor que apunte a ella puede descartar el flag, rechazarlo o aplicarlo como lo hace vLLM, recortando la respuesta. La página de compatibilidad con OpenAI de Google no menciona parallel_tool_calls en absoluto.
Descartado: razonamiento que no viaja
El tercer modo de falla no es un 400 ni un flag ignorado. Es estado que desaparece. En Claude, ese estado son los thinking blocks.
Los docs de thinking de Anthropic dicen que dentro de un turno de tool use, "when you return tool results, you must pass the thinking blocks from the assistant message back to the API, complete and unmodified". Los bloques modificados reciben un 400. Entre modelos, la regla es más blanda y silenciosa. La página de preserved thinking lista qué modelos pueden leer los bloques de qué modelos. Claude Opus 5.5 lee bloques de Opus 5 y de modelos Opus, Sonnet y Haiku anteriores, y de Sonnet 5.5 en la Claude API y Google Cloud, pero no de modelos Fable ni Mythos. Cuando una conversación pasa a un modelo que no puede leer un bloque, "the blocks are dropped, not rejected", y el modelo corre los turnos siguientes sin ese razonamiento.
Dos reglas más caen directo sobre los gateways. Primero, los thinking blocks de Claude Sonnet 5.5 "work only in the account that produced them, or in an account linked to it". Un gateway que reparte carga entre varias cuentas upstream y manda el turno dos desde una cuenta distinta a la del turno uno pierde el razonamiento sin error. Segundo, en Fable 5.1, Opus 5.5 y Sonnet 5.5, un thinking block sigue válido solo mientras el prompt system, el conjunto de tools y cada mensaje anterior no cambien. "Add, remove, rename, or edit a tool in tools" invalida los bloques posteriores. La respuesta por defecto ante un desajuste es un 400, y el chequeo se aplica por defecto a las cuentas creadas a partir del 31 de agosto de 2026. Los cambios en tool_choice, en cambio, quedan fuera del chequeo.
Esa segunda regla choca con MCP. La spec de MCP permite que los servidores envíen notifications/tools/list_changed cuando cambia su lista de tools. Un cliente que agrega la tool nueva a tools a mitad de sesión rompe el prefijo. El camino documentado por Anthropic es declarar las tools desde el inicio con defer_loading: true y activarlas con bloques tool_addition en messages, para que tools nunca cambie. Un request con forma OpenAI no tiene cómo expresar eso. La capa de traducción tiene que hacerlo.
El propio fallback de Anthropic muestra cuánta contabilidad exige esto. Después de un fallback a mitad del output, sus docs le piden al cliente mantener el bloque fallback exactamente donde apareció y descartar en el turno siguiente los bloques tool_use del lado del cliente y los de thinking que vinieron antes. Y en el camino compatible con OpenAI la pregunta ni siquiera llega al cliente: la página de compatibilidad dice que "the OpenAI SDK doesn't return Claude's thinking", así que un agente en ese camino no tiene bloques que devolver.
El costo oculto del sobre de tools
Cada uno de estos campos cuesta tokens que el agente no ve. OpenAI dice que las definiciones de funciones se "injected into the system message", así que "count against the model's context limit and are billed as input tokens". Anthropic agrega un system prompt de tool use encima de las definiciones, y su tamaño depende de tool_choice:
# System prompt de tool use de Anthropic, agregado a todo request con tools
# (tool use overview, 5 oct 2026), en input tokens
modelo auto / none any / tool
Claude Opus 5.5 286 no soportado
Claude Opus 5 286 406
Claude Sonnet 5 354 474
Claude Opus 4.7 675 804
Claude Haiku 4.5 496 588
Forzar una tool en Opus 5 cuesta 120 input tokens más por request que dejar que el modelo elija. Cambiar tool_choice entre requests además invalida los bloques de mensajes cacheados de Anthropic, según sus notas sobre forced tool use. En vLLM, tool_choice: "none" igual gasta tokens de prompt en las definiciones de tools salvo que el servidor las excluya.
Lo que cotiza nuestro propio 402
Sondeamos nuestra superficie walk-up igual que en las dos auditorías anteriores. Un POST sin autenticar a /v1/chat/completions devuelve HTTP 402 con un requisito x402 v2 exact en USDC sobre Base (eip155:8453), y el header PAYMENT-REQUIRED lleva el monto. Dejamos fijos el modelo y max_tokens y cambiamos solo el sobre de tools.
# api.llm4agents.com, POST sin autenticar /v1/chat/completions, 5 oct 2026
# anthropic/claude-opus-5.5, max_tokens 500, una tool get_weather salvo indicación
request body monto 402
un mensaje de usuario, sin tools 121 B $0.02
+ get_weather, tool_choice "auto" 355 B $0.02
+ tool_choice "required" (400 documentado) 359 B $0.02
+ tool_choice forzando get_weather (400 documentado) 406 B $0.02
+ tool_choice forzando una tool que no está en tools 409 B $0.02
+ parallel_tool_calls false 362 B $0.02
+ tool result sin tool call previo 396 B $0.02
+ call ID con cola base64 (fuera del patrón de ID 1,154 B $0.02
documentado por Anthropic)
descripción de tool inflada a ~100 KB 100 KB $0.02
300 tools 58 KB $0.02
tool result de ~100 KB en el historial 101 KB $0.14
~100 KB de argumentos de tool call en el historial 101 KB $0.14
mensaje de usuario de ~100 KB 100 KB $0.14
openai/gpt-5, tool_choice "required" 346 B $0.01
models [gpt-5, opus-5.5], tool_choice "required" 403 B $0.02
Tres lecturas.
Primero, la cotización cuenta el historial pero no el sobre. Un tool result de 100 KB o 100 KB de argumentos de llamada mueven la cotización de $0.02 a $0.14, igual que 100 KB de texto de usuario. Eso está bien, porque los tool results son la mayor parte del contexto de un agente. Pero 100 KB de descripción de tool, o 300 definiciones de tools, siguen siendo gratis, como encontró la auditoría de structured outputs para los schemas. En Claude la parte sin cotizar incluye además el system prompt de tool use.
Segundo, la cotización emite un requisito de pago para requests que el upstream documenta como inválidos. El forced tool use en Opus 5.5, un tool result sin nada antes y un ID fuera del patrón de Anthropic reciben el mismo 402 de $0.02. Nuestro post sobre walk-up describe la cotización walk-up como final, sin camino de reembolso. Un request que ya sabemos que será rechazado nunca debería llegar hasta una firma.
Tercero, la cadena se cotiza a su eslabón más caro, lo cual es correcto para el precio, pero no se verifica contra el dialecto de tools de cada eslabón. [gpt-5, opus-5.5] con required se cotiza a $0.02 y es válido en un solo eslabón. Nuestro post sobre cadenas de fallback lista cuatro disparadores para bajar por la cadena: desborde de contexto, rate limits, errores del provider y moderación. Un 400 por un tool_choice no soportado no es ninguno de ellos, y el contrato público no dice qué hace la cadena con él.
Qué significa para LLM4Agents
La cadena de fallback es la historia de confiabilidad de la plataforma, y el tool calling es lo que los agentes de nuestros clientes hacen todo el día. Esta auditoría dice que las dos cosas no se componen por defecto. Una cadena segura para un completion de un solo disparo puede ser insegura a mitad de un tool loop, porque el segundo eslabón hereda un historial que no escribió y controles que quizá no respeta.
Las fallas se ordenan en tres clases, y cada una necesita una respuesta distinta. Los rechazos (forced tool use en el tier Claude 5.5, firmas de Gemini 3 faltantes, IDs de Mistral de nueve caracteres) se pueden saber antes de que salga el request. Los controles ignorados (todo el sobre de tools de Ollama, strict en la capa compatible de Anthropic, los defaults de OpenRouter) se pueden saber por backend pero son invisibles por respuesta. El razonamiento descartado (thinking de Claude entre modelos y cuentas) solo se puede saber si el gateway registra qué modelo y qué cuenta escribieron cada turno.
La amenaza es clara. Si un fallback puede romper un loop, quienes construyen agentes con cuidado van a apagar el fallback para los tool calls y fijar un solo provider, y la cadena deja de ser una razón para usar un gateway. La oportunidad es el mismo hecho dado vuelta. Un agente no puede llevar una tabla con los patrones de ID, las reglas de firmas y la semántica de paralelismo de cuatro providers. Un gateway que ya termina cada llamada sí puede: acuñar IDs, transportar firmas, fijar turnos, rechazar cadenas imposibles y decir qué aplicó. Esa es la diferencia entre un proxy y un runtime para agent loops.
Cómo mantenerse en la frontera
En orden de costo y urgencia. Los pasos uno y dos extienden al sobre de tools los pasos de lint y de precio de la auditoría de structured outputs.
1. Validar cada eslabón antes del 402. Aplicar a toda la cadena la regla del propio fallback de Anthropic: el request debe ser válido como request directo a cada modelo nombrado. Rechazar tool_choice forzado para Opus 5.5, Sonnet 5.5, Fable 5.1 y Mythos 5.1 con un 400 que nombre el modelo y sugiera auto más strict tools. Nunca hacer downgrade en silencio. Si un agente opta por el downgrade, reportarlo.
2. Cotizar el sobre. Contar las definiciones de tools en la estimación de input detrás de la cotización, más el system prompt de tool use documentado por cada provider para el tool_choice efectivamente enviado.
3. Ser dueños de los tool call IDs. Acuñar IDs de gateway que satisfagan todos los dialectos a la vez. Nueve letras y dígitos pasan los validadores antiguos de mistral-common y el patrón de Anthropic, y la spec de OpenAI no le pone formato al campo. Mapearlos a los IDs upstream por conversación. Nunca meter estado del provider dentro de un ID.
4. Llevar el estado opaco aparte. Guardar las thought signatures de Gemini y los thinking blocks de Claude indexados por nuestros IDs, y volver a adjuntarlos solo cuando el siguiente request vaya a un modelo que pueda leerlos. Usar la firma dummy de Google solo como degradación declarada, nunca como default.
5. Hacer failover en los límites de turno. Dentro de un tool loop, mantener el turno en el mismo modelo y la misma cuenta upstream. Gemini valida solo el turno actual y Claude descarta los bloques ilegibles en lugar de fallar, así que un mensaje de usuario nuevo es el punto barato y seguro para cambiar de modelo. El failover a mitad de turno debería ser opt-in.
6. Normalizar las tools MCP. Renombrar los nombres de tools MCP con puntos o demasiado largos para que cumplan ^[a-zA-Z0-9_-]{1,64}$, verificar colisiones y revertir el mapeo en cada llamada. Mantener tools estable durante la sesión e incorporar las tools que aparezcan después mediante deferred loading en Claude, en lugar de reescribir la lista. Nuestro post sobre MCP 2026-07-28 cubre el resto de esa spec.
7. Decir qué se aplicó. Junto a X-Model-Used, devolver la semántica de tool_choice y de paralelismo que el eslabón que respondió aplicó realmente: aplicada, traducida o ignorada. Publicar el soporte de tools por modelo en /api/v1/models, como hace OpenRouter con los parámetros soportados. Luego sumar tests de conformidad de tool loop por eslabón a la disciplina de evals por eslabón del post de fallback.
Los pasos uno y dos protegen el dinero. Del tres al cinco protegen el loop. El seis y el siete convierten al gateway en algo sobre lo que un agente puede razonar.
Un endpoint para cada eslabón de la cadena
Compatible con OpenAI, pagado por llamada en USDC sobre x402.
Registra tu agente