{"name":"AI SDK API","version":"2.1.0","authentication":{"description":"Depende del provider seleccionado","openrouter":{"headers":{"x-openrouter-api-key":"Tu API key de OpenRouter","Authorization":"Alternativa: Bearer <tu-api-key>"}},"bedrock":{"headers":{"x-aws-region":"Región AWS (recomendado para sobreescribir por request)","x-aws-access-key-id":"Access key AWS (opcional)","x-aws-secret-access-key":"Secret key AWS (opcional)","x-aws-session-token":"Session token AWS (opcional)","x-aws-bearer-token-bedrock":"Bearer token Bedrock (opcional)","x-bedrock-api-key":"Alias de x-aws-bearer-token-bedrock (opcional)","Authorization":"Alternativa para bearer: Bearer <token>"},"environment":{"AWS_REGION":"Región AWS (requerido)","AWS_ACCESS_KEY_ID":"Opcional si usas provider chain","AWS_SECRET_ACCESS_KEY":"Opcional si usas provider chain","AWS_SESSION_TOKEN":"Opcional","AWS_BEARER_TOKEN_BEDROCK":"Opcional (Bearer auth alternativa)"}}},"environment":{"REQUEST_TIMEOUT":"number (opcional, segundos) - Timeout total de POST /generate. Default: 1200 (20 minutos)","API_BASE_URL":"string (opcional) - URL base pública para webhooks de respuesta async de tools","REDIS_URL":"string (opcional) - Redis para coordinar esperas async y stops entre réplicas. Ver toolAsyncGuide.redis","DEBUG_MODEL_RESPONSE":"boolean (opcional) - 'true' loguea la respuesta cruda del modelo","LOG_LEVEL":"string (opcional) - debug | info | warn | error. Default: info. Controla verbosidad de logs estructurados"},"endpoints":{"POST /generate":{"description":"Genera texto usando AI SDK con OpenRouter o Bedrock","headers":{"x-openrouter-api-key":"string (requerido solo para provider=openrouter)","x-aws-region":"string (requerido para bedrock si no hay AWS_REGION)","x-aws-access-key-id":"string (opcional, bedrock)","x-aws-secret-access-key":"string (opcional, bedrock)","x-aws-session-token":"string (opcional, bedrock)","x-aws-bearer-token-bedrock":"string (opcional, bedrock)"},"body":{"provider":"string (opcional) - 'openrouter' (default) | 'bedrock'","model":"string | string[] (requerido) - Modelo principal. Array = fallbacks OpenRouter. Con fusion, actua como juez salvo fusion.judge_model","prompt":"string (opcional) - Prompt simple","system":"string (opcional) - Mensaje de sistema","messages":"array (opcional) - Historial de mensajes","config":{"temperature":"number (0-2)","maxTokens":"number","topP":"number (0-1)","frequencyPenalty":"number","presencePenalty":"number","stopSequences":"string[]"},"tools":"array (opcional) - Tools disponibles [{name, description, parameters}]","toolsWebhook":"string (requerido si tools) - URL del webhook para ejecutar tools","messagesWebhook":"string (opcional) - URL que recibe mensajes intermedios de cada step (assistant, tool-calls, tool results)","toolContext":"object (opcional) - Contexto extra que recibirán toolsWebhook y messagesWebhook","maxToolSteps":"number (default: 5) - Máximo de iteraciones de tools","stopId":"string (opcional) - ID para detener esta generación con POST /generate-stop/:stopId. Si se omite y hay messagesWebhook, se genera automáticamente","providerOptions":"object (opcional) - Ej: { openrouter: { reasoning: { effort: 'low' | 'none' }, parallel_tool_calls: false } }","output":{"type":"string - text | object | array | choice | json","name":"string (opcional)","description":"string (opcional)","schema":"object (requerido para object/array) - JSON Schema","options":"string[] (requerido para choice, mínimo 2)"},"fusion":{"analysis_models":"string[] (opcional, 1-8) - Panel que debate en paralelo. Omítelo para usar el preset Quality de OpenRouter","judge_model":"string (opcional, avanzado) - Juez distinto al model principal. Casi nunca hace falta","max_tool_calls":"number (opcional, 1-16) - Pasos de tool-calling internos","max_completion_tokens":"number (opcional) - Tokens máximos por llamada interna","tool_choice":"string (opcional) - 'required' fuerza la invocación de fusion en cada request"}}},"GET /health":{"description":"Verifica el estado de la API"},"POST /webhook-response/:id":{"description":"Recibe la respuesta async de una tool cuando su webhook devolvió _wait_response: true. También puede interrumpir la espera con _interrupted_mode","body":{"normal":"any - Resultado de la tool (mismo formato que respuesta síncrona del webhook)","interrupt":{"_interrupted_mode":"boolean - true detiene /generate sin que el asistente procese la respuesta","_fallback":"object (opcional) - tool-result a guardar en messages; si se omite, usa el _fallback de la respuesta inicial"}},"errors":{"410":"La espera ya expiró o fue interrumpida"}},"POST /generate-stop/:stopId":{"description":"Detiene una generación activa registrada con stopId en POST /generate. Durante step solo de mensaje: queued hasta la próxima tool. En espera async (_wait_response): interrumpe al instante con _fallback. Con webhook síncrono en curso: applied pero la tool termina con su resultado real; /generate se detiene al cerrar el step (messages = resultados reales + fallbacks de async canceladas)","body":{"_fallback":"object (requerido) - tool-result para tools en espera async; ignorado si la tool ya está ejecutando el webhook síncrono (se conserva el resultado real)"},"response":{"ok":"boolean","stop_id":"string","applied":"boolean - true si había tool en curso o espera async (el efecto depende de la fase)","queued":"boolean - true si el stop quedó pendiente (step de mensaje en curso)"},"errors":{"404":"No hay generación activa con ese stopId"}}},"examples":{"simple":{"description":"Prompt simple → texto final en text y messages","request":{"provider":"openrouter","model":"openai/gpt-4o-mini","prompt":"Explica qué es TypeScript en una oración"},"response":{"text":"TypeScript es JavaScript con tipado estático opcional.","finishReason":"stop","messages":[{"role":"assistant","content":[{"type":"text","text":"TypeScript es JavaScript con tipado estático opcional."}],"usage":{"inputTokens":24,"outputTokens":14,"totalTokens":38},"provider_data":{"id":"gen-abc","modelId":"openai/gpt-4o-mini"}}]}},"withReasoning":{"description":"Con reasoning del proveedor → parte type reasoning antes de text/tool-call (effort none desactiva solo si el modelo lo soporta)","request":{"provider":"openrouter","model":"anthropic/claude-sonnet-4","messages":[{"role":"user","content":"Busca viajes de auroras boreales"}],"providerOptions":{"openrouter":{"reasoning":{"effort":"low"}}},"tools":[{"name":"buscar_viajes","description":"Busca paquetes de viaje por keywords","parameters":{"type":"object","properties":{"keywords":{"type":"string"}},"required":["keywords"]}}],"toolsWebhook":"https://tu-servidor.com/webhook/tools"},"response":{"text":"Encontré varios paquetes para ver auroras boreales.","finishReason":"stop","toolCalls":[{"toolName":"buscar_viajes","args":{"keywords":"auroras boreales"}}],"toolResults":[{"toolName":"buscar_viajes","args":{"keywords":"auroras boreales"},"result":{"paquetes":[{"nombre":"Tromsø Aurora","dias":5}]}}],"messages":[{"role":"assistant","content":[{"type":"reasoning","text":"El usuario pide auroras; primero busco paquetes con buscar_viajes.","providerOptions":{"openrouter":{"reasoning_details":[{"type":"reasoning.text","text":"El usuario pide auroras; primero busco paquetes con buscar_viajes.","format":"unknown","index":0}]}}},{"type":"tool-call","toolCallId":"call_R0MYcTWECUDothbx199E3U00","toolName":"buscar_viajes","input":{"keywords":"auroras boreales"},"providerOptions":{"openrouter":{"reasoning_details":[{"type":"reasoning.text","text":"El usuario pide auroras; primero busco paquetes con buscar_viajes.","format":"unknown","index":0}]}}}],"usage":{"inputTokens":420,"outputTokens":86,"totalTokens":506,"reasoningTokens":48},"provider_data":{"id":"gen-reason-1","modelId":"anthropic/claude-sonnet-4"}},{"role":"tool","content":[{"type":"tool-result","toolCallId":"call_R0MYcTWECUDothbx199E3U00","toolName":"buscar_viajes","output":{"type":"json","value":{"paquetes":[{"nombre":"Tromsø Aurora","dias":5}]}}}]},{"role":"assistant","content":[{"type":"text","text":"Encontré varios paquetes para ver auroras boreales."}],"usage":{"inputTokens":510,"outputTokens":22,"totalTokens":532},"provider_data":{"id":"gen-reason-2","modelId":"anthropic/claude-sonnet-4"}}]}},"withTools":{"description":"Tools síncronas → messages delta con tool-call + tool-result + texto final","request":{"provider":"openrouter","model":"openai/gpt-4o","system":"Eres un asistente que puede consultar el clima y buscar información.","messages":[{"role":"user","content":"¿Qué clima hace en Madrid?"}],"tools":[{"name":"get_weather","description":"Obtiene el clima actual de una ciudad","parameters":{"type":"object","properties":{"city":{"type":"string","description":"Nombre de la ciudad"},"unit":{"type":"string","enum":["celsius","fahrenheit"],"description":"Unidad de temperatura"}},"required":["city"]}}],"toolsWebhook":"https://tu-servidor.com/webhook/tools","maxToolSteps":3},"response":{"text":"En Madrid hace 22°C y está soleado.","finishReason":"stop","toolCalls":[{"toolName":"get_weather","args":{"city":"Madrid","unit":"celsius"}}],"toolResults":[{"toolName":"get_weather","args":{"city":"Madrid","unit":"celsius"},"result":{"temp":22,"condition":"soleado"}}],"messages":[{"role":"assistant","content":[{"type":"tool-call","toolCallId":"call_weather_1","toolName":"get_weather","input":{"city":"Madrid","unit":"celsius"}}],"usage":{"inputTokens":180,"outputTokens":28,"totalTokens":208},"provider_data":{"id":"gen-tools-1","modelId":"openai/gpt-4o"}},{"role":"tool","content":[{"type":"tool-result","toolCallId":"call_weather_1","toolName":"get_weather","output":{"type":"json","value":{"temp":22,"condition":"soleado"}}}]},{"role":"assistant","content":[{"type":"text","text":"En Madrid hace 22°C y está soleado."}],"usage":{"inputTokens":230,"outputTokens":18,"totalTokens":248},"provider_data":{"id":"gen-tools-2","modelId":"openai/gpt-4o"}}]}},"withStructuredOutput":{"description":"output.type object → campo output parseado además de text/messages","request":{"model":"openai/gpt-4o","prompt":"Genera una receta de lasaña","output":{"type":"object","name":"Recipe","schema":{"type":"object","properties":{"name":{"type":"string"},"ingredients":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"amount":{"type":"string"}},"required":["name","amount"]}}},"required":["name","ingredients"]}}},"response":{"text":"{\"name\":\"Lasaña\",\"ingredients\":[{\"name\":\"pasta\",\"amount\":\"500g\"}]}","finishReason":"stop","output":{"name":"Lasaña","ingredients":[{"name":"pasta","amount":"500g"}]},"messages":[{"role":"assistant","content":[{"type":"text","text":"{\"name\":\"Lasaña\",\"ingredients\":[{\"name\":\"pasta\",\"amount\":\"500g\"}]}"}],"usage":{"inputTokens":90,"outputTokens":40,"totalTokens":130},"provider_data":{"id":"gen-struct-1","modelId":"openai/gpt-4o"}}]}},"interrupted":{"description":"Tool async interrumpida → interrupted true; messages traen el tool-result con _fallback; el asistente no lo procesó","request":{"model":"openai/gpt-4o","messages":[{"role":"user","content":"Consulta el clima"}],"tools":[{"name":"get_weather","description":"Obtiene el clima","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}],"toolsWebhook":"https://tu-servidor.com/webhook/tools","stopId":"sess_abc"},"response":{"interrupted":true,"finishReason":"tool-wait-timeout","text":"","stop_id":"sess_abc","messages":[{"role":"assistant","content":[{"type":"tool-call","toolCallId":"call_abc123","toolName":"get_weather","input":{"city":"Madrid"}}],"usage":{"inputTokens":120,"outputTokens":20,"totalTokens":140},"provider_data":{"id":"gen-int-1","modelId":"openai/gpt-4o"}},{"role":"tool","content":[{"type":"tool-result","toolCallId":"call_abc123","toolName":"get_weather","output":{"type":"json","value":{"error":"timeout","city":"Madrid"}}}]}]}},"withModelFallbacks":{"description":"Array en model → mismo shape de respuesta; OpenRouter elige el primero disponible","request":{"provider":"openrouter","model":["anthropic/claude-sonnet-4","openai/gpt-4o","google/gemini-2.5-pro-preview"],"prompt":"Explica qué es TypeScript en una oración"},"response":{"text":"TypeScript es JavaScript con tipado estático opcional.","finishReason":"stop","messages":[{"role":"assistant","content":[{"type":"text","text":"TypeScript es JavaScript con tipado estático opcional."}],"usage":{"inputTokens":24,"outputTokens":14,"totalTokens":38},"provider_data":{"id":"gen-fb-1","modelId":"openai/gpt-4o"}}]}},"withFusion":{"description":"Fusion Router con panel custom → respuesta de texto como un generate normal","request":{"provider":"openrouter","model":"anthropic/claude-sonnet-4","fusion":{"analysis_models":["anthropic/claude-opus-4","openai/gpt-4o","google/gemini-2.5-pro-preview"]},"prompt":"Compara los argumentos a favor y en contra de un impuesto al carbono. ¿Dónde discrepan los expertos?"},"response":{"text":"Los expertos coinciden en el objetivo climático, pero discrepan en el impacto distributivo y en alternativas como subsidios verdes.","finishReason":"stop","messages":[{"role":"assistant","content":[{"type":"text","text":"Los expertos coinciden en el objetivo climático, pero discrepan en el impacto distributivo y en alternativas como subsidios verdes."}],"usage":{"inputTokens":800,"outputTokens":120,"totalTokens":920},"provider_data":{"id":"gen-fusion-1","modelId":"anthropic/claude-sonnet-4"}}]}},"withFusionAlias":{"description":"Alias openrouter/fusion → mismo contrato de respuesta que withFusion","request":{"provider":"openrouter","model":"openrouter/fusion","prompt":"Compara los argumentos a favor y en contra de un impuesto al carbono. ¿Dónde discrepan los expertos?"},"response":{"text":"Hay consenso en reducir emisiones; el debate está en quién asume el costo y si el impuesto es el instrumento óptimo.","finishReason":"stop","messages":[{"role":"assistant","content":[{"type":"text","text":"Hay consenso en reducir emisiones; el debate está en quién asume el costo y si el impuesto es el instrumento óptimo."}],"usage":{"inputTokens":640,"outputTokens":90,"totalTokens":730},"provider_data":{"id":"gen-fusion-alias-1","modelId":"openrouter/fusion"}}]}},"withBedrock":{"description":"Provider bedrock → mismo shape; autenticación por headers/env AWS","request":{"provider":"bedrock","model":"us.anthropic.claude-3-5-sonnet-20241022-v2:0","prompt":"Explica qué es TypeScript en una oración"},"response":{"text":"TypeScript es un superconjunto tipado de JavaScript que compila a JS estándar.","finishReason":"stop","messages":[{"role":"assistant","content":[{"type":"text","text":"TypeScript es un superconjunto tipado de JavaScript que compila a JS estándar."}],"usage":{"inputTokens":30,"outputTokens":20,"totalTokens":50},"provider_data":{"id":"gen-bedrock-1","modelId":"us.anthropic.claude-3-5-sonnet-20241022-v2:0"}}]}},"multipleTools":{"description":"Varias tools en un turno → varios tool-call/tool-result en messages","request":{"provider":"openrouter","model":"anthropic/claude-3.5-sonnet","messages":[{"role":"user","content":"Busca hoteles en París y dime el clima"}],"tools":[{"name":"search_hotels","description":"Busca hoteles en una ciudad","parameters":{"type":"object","properties":{"city":{"type":"string"},"checkIn":{"type":"string","description":"Fecha YYYY-MM-DD"},"checkOut":{"type":"string","description":"Fecha YYYY-MM-DD"}},"required":["city"]}},{"name":"get_weather","description":"Obtiene el clima","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}],"toolsWebhook":"https://tu-servidor.com/webhook/tools"},"response":{"text":"En París hace 18°C. Hoteles sugeridos: Hôtel Le Marais y Hotel Rivoli.","finishReason":"stop","toolCalls":[{"toolName":"search_hotels","args":{"city":"París"}},{"toolName":"get_weather","args":{"city":"París"}}],"toolResults":[{"toolName":"search_hotels","args":{"city":"París"},"result":{"hotels":["Hôtel Le Marais","Hotel Rivoli"]}},{"toolName":"get_weather","args":{"city":"París"},"result":{"temp":18,"unit":"celsius"}}],"messages":[{"role":"assistant","content":[{"type":"tool-call","toolCallId":"call_hotels_1","toolName":"search_hotels","input":{"city":"París"}},{"type":"tool-call","toolCallId":"call_weather_2","toolName":"get_weather","input":{"city":"París"}}],"usage":{"inputTokens":260,"outputTokens":55,"totalTokens":315},"provider_data":{"id":"gen-multi-1","modelId":"anthropic/claude-3.5-sonnet"}},{"role":"tool","content":[{"type":"tool-result","toolCallId":"call_hotels_1","toolName":"search_hotels","output":{"type":"json","value":{"hotels":["Hôtel Le Marais","Hotel Rivoli"]}}}]},{"role":"tool","content":[{"type":"tool-result","toolCallId":"call_weather_2","toolName":"get_weather","output":{"type":"json","value":{"temp":18,"unit":"celsius"}}}]},{"role":"assistant","content":[{"type":"text","text":"En París hace 18°C. Hoteles sugeridos: Hôtel Le Marais y Hotel Rivoli."}],"usage":{"inputTokens":340,"outputTokens":30,"totalTokens":370},"provider_data":{"id":"gen-multi-2","modelId":"anthropic/claude-3.5-sonnet"}}]}}},"messagesWebhookFormat":{"description":"POST antes de ejecutar toolsWebhook. Recibe cada step del asistente; si incluye tool-calls, la API espera la respuesta del webhook antes de invocar las tools. Errores del webhook no bloquean la generación.","timing":"Se dispara justo después de que el modelo responde y antes de ejecutar las tools de ese step","body":{"step":"number - Índice del step (0-based)","messages":"array - Mensaje assistant del step (texto intermedio + tool-calls, con usage y provider_data)","finish_reason":"string - Razón de fin del step (ej: tool-calls, stop)","stop_id":"string - stopId del request o generado automáticamente si se omitió; usar con POST /generate-stop/:stopId","context":"object (opcional) - Mismo toolContext del request","timestamp":"string - ISO timestamp"},"response":"any - Respuesta del webhook (se consume pero no altera el flujo). Si falla o timeout, se continúa igual","example":{"step":0,"finish_reason":"tool-calls","stop_id":"sess_abc","context":{"sessionId":"sess_abc"},"timestamp":"2024-01-15T10:30:00.000Z","messages":[{"role":"assistant","content":[{"type":"reasoning","text":"El usuario elige una opción; muestro el menú con la tool.","providerOptions":{"openrouter":{"reasoning_details":[{"type":"reasoning.text","text":"El usuario elige una opción; muestro el menú con la tool.","format":"unknown","index":0}]}}},{"type":"text","text":"Encontré varias opciones..."},{"type":"tool-call","toolCallId":"call_abc123","toolName":"mostrar_opciones","input":{"body":"¿Qué presentación prefiere?","opciones":["a","b"]}}],"usage":{"inputTokens":1200,"outputTokens":180,"totalTokens":1380,"reasoningTokens":40},"provider_data":{"id":"gen-1","modelId":"minimax/minimax-m3"}}]}},"webhookFormat":{"request":{"description":"Lo que tu webhook recibirá","body":{"tool":"string - Nombre de la tool a ejecutar","arguments":"object - Argumentos de la tool","context":"object - Contexto extra enviado en toolContext","timestamp":"string - ISO timestamp","response_id":"string - ID único de la llamada (toolCallId); usar al responder","response_webhook":"string - URL POST de esta API donde enviar el resultado async"}},"asyncResponse":{"description":"Respuesta inmediata del webhook para operaciones que tardan. La API espera el resultado real en response_webhook.","body":{"_wait_response":"boolean - true para indicar que la respuesta llegará después","_timeout":"number (opcional, default 30) - Segundos máximos de espera","_fallback":"object (opcional) - Resultado si expira el timeout. Con _interrupted_mode es obligatorio","_interrupted_mode":"boolean (opcional) - Si true, al expirar el timeout detiene /generate sin que el asistente procese la respuesta. El _fallback se guarda como tool-result en messages. Para continuar, el cliente reenvía messages + un nuevo mensaje user"},"callback":{"method":"POST","url":"{API_BASE_URL}/webhook-response/{response_id}","body":"any - Resultado normal, o { _interrupted_mode: true, _fallback?: {...} } para interrumpir con otro fallback. Rechazado (410) si la espera ya expiró"},"interruptCallback":{"_interrupted_mode":true,"_fallback":{"status":"cancelled","reason":"usuario canceló"}}},"response":{"description":"Lo que tu webhook debe retornar","body":"any - El resultado será pasado al modelo. Opcionalmente puede incluir _metadata (objeto) que no verá el agente pero se devolverá en tools_metadata del endpoint /generate"},"metadata":{"description":"Campo opcional _metadata en la respuesta del webhook","behavior":"Se elimina antes de enviar el resultado al modelo. Si está presente, aparece en tools_metadata de la respuesta de /generate","format":{"tool_name":"string - Nombre de la tool ejecutada","tool_id":"string - ID de la llamada a la tool (toolCallId)","metadata":"object - Valor del campo _metadata devuelto por el webhook"}},"example":{"request":{"tool":"get_weather","arguments":{"city":"Madrid","unit":"celsius"},"context":{"userId":"user_123","sessionId":"sess_abc"},"timestamp":"2024-01-15T10:30:00.000Z","response_id":"call_abc123","response_webhook":"https://tu-api.com/webhook-response/call_abc123"},"immediateResponse":{"_wait_response":true,"_timeout":30,"_interrupted_mode":true,"_fallback":{"status":"pending","timed_out":true}},"interruptedGenerateResponse":{"interrupted":true,"finishReason":"tool-wait-timeout","messages":"array - Solo mensajes nuevos de este /generate (sin el historial enviado). En interrupción: assistant + tool-results del turno; el último suele ser tool","resume":"Concatenar historial previo + messages de la respuesta + nuevo { role: 'user', content: '...' } en el siguiente /generate"},"asyncCallback":{"temperature":15,"condition":"soleado","humidity":45},"response":{"temperature":15,"condition":"soleado","humidity":45,"_metadata":{"source":"openweather","requestId":"req_abc123"}}}},"toolAsyncGuide":{"overview":"Las tools pueden responder de forma síncrona o pedir espera async con _wait_response. En modo async la API bloquea /generate hasta recibir POST /webhook-response/:id o hasta que expire _timeout. El modo interrumpido detiene /generate sin que el asistente procese el resultado, pero guarda el historial completo (incluido tool-result) para reanudar después.","environment":{"API_BASE_URL":"Requerido para async: genera response_webhook en el POST al toolsWebhook","REDIS_URL":"Recomendado con varias instancias o load balancer. Sin esto el estado vive en memoria del proceso (una sola réplica). Ej: redis://localhost:6379"},"redis":{"overview":"Redis guarda el estado efímero de tools async e interrupciones para que cualquier réplica de la API pueda resolver callbacks y stops, no solo la que atendió POST /generate.","stores":["Esperas de POST /webhook-response/:id — abierto/cerrado, respuesta early y resultado del callback","Stops de POST /generate-stop/:stopId — registro activo, fase tool y _fallback pendiente"],"whenRequired":"Obligatorio si despliegas más de una instancia detrás de un balanceador. Si el callback o el stop llegan a otra réplica que la que inició /generate, sin Redis fallan (404/410 o stop not_found).","withoutRedis":"Una sola instancia: funciona igual con memoria local. No hace falta Redis en desarrollo salvo que quieras probar el flujo distribuido.","localDev":"docker compose up levanta redis + api con REDIS_URL=redis://redis:6379. Solo Redis: docker compose up redis -d y REDIS_URL=redis://localhost:6379 en .env"},"fallbackRule":"Un resultado real nunca se descarta. _fallback solo reemplaza tools detenidas antes de tener resultado real: espera async cancelada (stop, timeout interrumpido, callback con _interrupted_mode) o stop en cola aplicado antes de ejecutar el webhook. Webhook síncrono que ya respondió siempre conserva su resultado en messages.","controlFields":{"_wait_response":"true — activa espera async","_timeout":"Segundos máximos de espera (default 30)","_fallback":"Resultado si expira el timeout. Obligatorio si usas _interrupted_mode en la respuesta inicial","_interrupted_mode":"En respuesta inicial: interrumpe al expirar timeout. En POST /webhook-response/:id: interrumpe manualmente"},"fallbackPriorityOnInterrupt":["_fallback del POST /webhook-response/:id (si se envía)","_fallback de la respuesta inicial del toolsWebhook","Resto del body del callback sin campos de control"],"closedWebhook":"Tras timeout, interrupción o callback exitoso, el response_id queda cerrado. POST tardíos reciben HTTP 410.","useCases":[{"id":"sync","title":"1. Respuesta síncrona","description":"El toolsWebhook devuelve el resultado directamente. El agente lo recibe y continúa en el mismo /generate.","webhookResponse":{"temperature":15,"condition":"soleado"}},{"id":"async-normal","title":"2. Async con callback normal","description":"El toolsWebhook responde con _wait_response. Tu backend procesa en background y envía el resultado real a POST /webhook-response/{response_id}. El agente continúa con ese resultado.","webhookResponse":{"_wait_response":true,"_timeout":120},"callback":{"temperature":15,"condition":"soleado"}},{"id":"timeout-fallback","title":"3. Timeout con _fallback (el agente sigue)","description":"Si no llega el callback a tiempo, se usa _fallback como resultado de la tool y el asistente sigue procesándolo en el mismo /generate.","webhookResponse":{"_wait_response":true,"_timeout":30,"_fallback":{"status":"pending","timed_out":true}}},{"id":"timeout-interrupted","title":"4. Timeout interrumpido (_interrupted_mode en respuesta inicial)","description":"Al expirar _timeout, /generate termina con interrupted: true. El _fallback se guarda como tool-result en messages pero el asistente NO lo procesa. El webhook de espera queda cerrado (410).","webhookResponse":{"_wait_response":true,"_timeout":30,"_interrupted_mode":true,"_fallback":{"status":"pending","timed_out":true}},"generateResponse":{"interrupted":true,"finishReason":"tool-wait-timeout","text":"","messages":[{"role":"user","content":"¿Qué clima hace en Madrid?"},{"role":"assistant","content":[{"type":"text","text":"Voy a consultar el clima."},{"type":"tool-call","toolCallId":"call_abc123","toolName":"get_weather","input":{"city":"Madrid"}}]},{"role":"tool","content":[{"type":"tool-result","toolCallId":"call_abc123","toolName":"get_weather","output":{"status":"pending","timed_out":true}}]}]}},{"id":"callback-interrupt","title":"5. Interrupción manual vía POST /webhook-response/:id","description":"Mientras la tool espera, puedes interrumpir enviando _interrupted_mode al callback. Puedes pasar un _fallback distinto al de la respuesta inicial. Mismo efecto que timeout interrumpido: historial completo, asistente detenido, id cerrado.","webhookResponse":{"_wait_response":true,"_timeout":120,"_fallback":{"status":"pending"}},"callback":{"_interrupted_mode":true,"_fallback":{"status":"cancelled","reason":"usuario canceló"}}},{"id":"generate-stop","title":"6. Stop general vía POST /generate-stop/:stopId","description":"Incluye stopId en POST /generate. Step solo de mensaje: queued. Espera async: _fallback al instante. Webhook síncrono en curso: deja terminar con resultado real y detiene /generate al cerrar el step (sin otro turno del asistente).","generateRequest":{"stopId":"sess_abc","model":"openai/gpt-4o","messages":[{"role":"user","content":"Consulta el clima"}],"tools":[{"name":"get_weather","description":"...","parameters":{}}],"toolsWebhook":"https://tu-servidor.com/webhook/tools"},"stopCallback":{"_fallback":{"status":"stopped","reason":"usuario canceló"}},"generateResponse":{"interrupted":true,"finishReason":"generation-stopped","messages":"Historial del step: tool-results reales de webhooks síncronos + _fallback en async canceladas"}},{"id":"resume","title":"7. Reanudar conversación","description":"Tras una interrupción, concatena tu historial previo + messages de la respuesta + un nuevo mensaje user. Puedes incluir usage y provider_data tal cual: la API los quita al llamar al modelo y normaliza tool-result.","generateRequest":{"model":"openai/gpt-4o","messages":[{"role":"user","content":"¿Qué clima hace en Madrid?"},{"role":"assistant","content":[{"type":"tool-call","toolCallId":"call_abc123","toolName":"get_weather","input":{"city":"Madrid"}}]},{"role":"tool","content":[{"type":"tool-result","toolCallId":"call_abc123","toolName":"get_weather","output":{"status":"pending","timed_out":true}}]},{"role":"user","content":"Ya tengo el dato, continúa"}],"tools":[{"name":"get_weather","description":"...","parameters":{}}],"toolsWebhook":"https://tu-servidor.com/webhook/tools"}}]}}