API y Streaming del Asesor de IA
📡 API y Streaming del Asesor de IA
Sección titulada «📡 API y Streaming del Asesor de IA»Todas las rutas cuelgan de /api/advisor y forman parte de AppType, por lo que el frontend llama a los endpoints de configuración mediante el cliente tipado hc<AppType> (ver Backend Hono y RPC). El endpoint de streaming se consume con fetch y un ReadableStream. Se montan exactamente seis endpoints del asesor y no existe ninguna ruta comodín ni de servidor de agentes (advisorMounting.spec.ts).
| Método | Ruta | Propósito |
|---|---|---|
POST |
/api/advisor/ask |
Sin streaming: se ejecuta hasta el final y devuelve una respuesta. |
POST |
/api/advisor/stream |
Ejecución con streaming SSE. |
GET |
/api/advisor/config |
Cadena almacenada y estado de credenciales por proveedor. |
PUT |
/api/advisor/config/model-chain |
Reemplaza la cadena de modelos. |
GET |
/api/advisor/config/execution-profiles |
Perfiles almacenados, o valores por defecto. |
PUT |
/api/advisor/config/execution-profiles |
Reemplaza los perfiles. |
Fallo común: un cuerpo de petición inválido devuelve HTTP 400 desde zValidator antes de que empiece ninguna ejecución.
💬 POST /api/advisor/ask y POST /api/advisor/stream
Sección titulada «💬 POST /api/advisor/ask y POST /api/advisor/stream»Cuerpo de la petición (Content-Type: application/json):
{ "message": "What should I review first in my tax report?", "threadId": "0f8a2b3c-5d6e-4f70-8a91-b2c3d4e5f607"}| Campo | Tipo | Reglas |
|---|---|---|
message |
string | De 1 a 4000 caracteres. |
threadId |
string, opcional | UUID. Omítelo para iniciar una conversación; envía el threadId de un frame terminal para continuarla. |
/ask siempre devuelve 200 con un cuerpo discriminado por outcome, también para ejecuciones rechazadas y fallidas:
{ "outcome": "completed", "text": "Start with the two critical data-quality flags ...", "receipt": { "runId": "7c1e0a52-3f4b-4f08-9d8e-2b6a1c9d0e11", "threadId": "0f8a2b3c-5d6e-4f70-8a91-b2c3d4e5f607", "startedAt": "2026-05-01T09:30:00.000Z", "finishedAt": "2026-05-01T09:30:07.412Z", "providerId": "ollama", "modelId": "qwen3:8b", "toolsCalled": ["fiscal_integrity"], "usage": { "inputTokens": 1420, "outputTokens": 260 }, "executionProfile": "local", "stepsUsed": 3, "maxSteps": 15 }}{ "outcome": "refused", "reason": "This request asks how to evade ...", "processorId": "grounding-directive-detector" }{ "outcome": "failed", "code": "NO_MODEL_AVAILABLE" }La respuesta de /ask no tiene los campos disclaimer, figuresIncomplete ni cause; esos pertenecen al contrato de streaming. /stream devuelve 200 con Content-Type: text/event-stream; los problemas a nivel de ejecución son frames, no errores HTTP.
⚙️ GET /api/advisor/config
Sección titulada «⚙️ GET /api/advisor/config»Devuelve 200:
{ "chain": [ { "providerId": "ollama", "modelId": "qwen3:8b", "contextWindow": 8192 }, { "providerId": "anthropic", "modelId": "claude-sonnet-4-5" } ], "providers": [ { "id": "openai", "category": { "kind": "ai-model" }, "credential": { "kind": "absent" } }, { "id": "anthropic", "category": { "kind": "ai-model" }, "credential": { "kind": "locked" } }, { "id": "ollama", "category": { "kind": "ai-model" }, "credential": { "kind": "present" } } ]}chain es [] cuando no está definida o no se puede parsear. providers siempre lista los seis ids. credential.kind es present (almacenada y bóveda desbloqueada, o ollama), absent (nada almacenado) o locked (almacenada, bóveda bloqueada).
🔗 PUT /api/advisor/config/model-chain
Sección titulada «🔗 PUT /api/advisor/config/model-chain»Cuerpo: un array de entradas de cadena, al menos una (ver Cadena de Modelos y Fallback). Devuelve 200 { "success": true }, o 400 para un array vacío o una entrada que viole las reglas de forma (por ejemplo, una entrada ollama local sin contextWindow).
curl -X PUT http://localhost:3001/api/advisor/config/model-chain \ -H 'content-type: application/json' \ -d '[{"providerId":"ollama","modelId":"qwen3:8b","contextWindow":8192}]'⚖️ GET y PUT /api/advisor/config/execution-profiles
Sección titulada «⚖️ GET y PUT /api/advisor/config/execution-profiles»GET devuelve 200 con los perfiles almacenados o defaultExecutionProfiles(). PUT valida contra los techos y devuelve 200 con los perfiles almacenados, o 400 ante una violación. El bloque metered exige una entrada toolBudgets para cada herramienta; local no tiene presupuestos. Una fila almacenada antes de que existiera una herramienta sigue cargando: al leer, sus toolBudgets se fusionan sobre los valores por defecto, de modo que las herramientas ausentes toman su presupuesto por defecto. El PUT sigue siendo estricto y exige las 25 claves.
{ "metered": { "maxSteps": 5, "lastMessages": 20, "topNHoldings": 15, "lotsPageSize": 20, "rowsPageSize": 25, "toolBudgets": { "portfolio_summary": 4000, "fiscal_integrity": 6000, "token_history": 6000, "asset_allocation": 3000, "risk_metrics": 1500, "drawdown_curve": 4000, "performance_history": 4000, "kpis": 3000, "volatility_heatmap": 4000, "spanish_tax_report": 5000, "live_prices": 2000, "fiscal_integrity_rows": 4000, "token_lots": 4000, "holding_detail": 3000, "account_holdings": 5000, "tax_year_comparison": 4000, "data_gaps": 4000, "scenario_position_value": 1500, "breakeven_price": 1500, "explain_metric": 2000, "derivatives_pnl": 4000, "custody_locations": 4000, "scenario_portfolio_shock": 4000, "concentration_risk": 2000, "tx_search": 5000 } }, "local": { "maxSteps": 15, "lastMessages": 50, "topNHoldings": 50, "lotsPageSize": 100, "rowsPageSize": 100 }}[!WARNING] A diferencia de la cadena (un valor incorrecto degrada a
NO_MODEL_AVAILABLE), un valor almacenado deai_advisor_execution_profilesque no se pueda parsear no se captura:GETresponde HTTP 500 y una ejecución terminafailedconINTERNAL_ERROR. Recupérate escribiendo un valor válido conPUTo eliminando el ajuste.
🌊 Contrato de Frames del Stream (SSE)
Sección titulada «🌊 Contrato de Frames del Stream (SSE)»Cada mensaje SSE usa el kind del frame como nombre de event SSE y un payload JSON en data. Se escribe un comentario : keep-alive cada 15 segundos. Todos los frames llevan runId. Una ejecución termina con exactamente uno de done, refused o failed.
kind |
Campos además de runId |
|---|---|
token |
text |
tool-start |
callId, tool |
tool-result |
callId, tool |
tool-error |
callId, tool, code (INVALID_TOOL_INPUT o USE_CASE_FAILED) |
done |
threadId, providerId, modelId, usage (inputTokens, outputTokens), toolsCalled, executionProfile (local, metered, mixed), stepsUsed, maxSteps, disclaimer, figuresIncomplete |
refused |
threadId, reason, processorId |
failed |
code, threadId opcional; cause (kind, providerId, modelId) obligatorio en ALL_PROVIDERS_FAILED y prohibido en cualquier otro código |
Ambos miembros de failed son .strict(), por lo que una causa en un código sin causa se rechaza en lugar de descartarse. threadId en failed es opcional solo porque un error inesperado en una conversación nueva puede ocurrir antes de que exista un id de hilo.
Deliberadamente no existe un frame transport-lost: un stream que se cierra sin frame terminal lo sintetiza el cliente, de modo que una conexión caída nunca puede confundirse con un rechazo de guardarraíl.
Ejemplo: una Respuesta Fundamentada
Sección titulada «Ejemplo: una Respuesta Fundamentada»event: tool-startdata: {"kind":"tool-start","runId":"7c1e0a52-3f4b-4f08-9d8e-2b6a1c9d0e11","callId":"call_01","tool":"portfolio_summary"}
event: tool-resultdata: {"kind":"tool-result","runId":"7c1e0a52-3f4b-4f08-9d8e-2b6a1c9d0e11","callId":"call_01","tool":"portfolio_summary"}
event: tokendata: {"kind":"token","runId":"7c1e0a52-3f4b-4f08-9d8e-2b6a1c9d0e11","text":"Your largest position is "}
event: tokendata: {"kind":"token","runId":"7c1e0a52-3f4b-4f08-9d8e-2b6a1c9d0e11","text":"BTC at `15000` USD."}
event: donedata: {"kind":"done","runId":"7c1e0a52-3f4b-4f08-9d8e-2b6a1c9d0e11","threadId":"0f8a2b3c-5d6e-4f70-8a91-b2c3d4e5f607","providerId":"ollama","modelId":"qwen3:8b","usage":{"inputTokens":1834,"outputTokens":212},"toolsCalled":["portfolio_summary"],"executionProfile":"local","stepsUsed":3,"maxSteps":15,"disclaimer":false,"figuresIncomplete":false}Ejemplo: un Rechazo por Guardarraíl
Sección titulada «Ejemplo: un Rechazo por Guardarraíl»event: refuseddata: {"kind":"refused","runId":"9a3b...","threadId":"0f8a2b3c-5d6e-4f70-8a91-b2c3d4e5f607","reason":"This request asks how to evade or hide taxable gains, which this advisor never assists with.","processorId":"grounding-directive-detector"}Ejemplo: Fallaron Todos los Proveedores
Sección titulada «Ejemplo: Fallaron Todos los Proveedores»event: faileddata: {"kind":"failed","runId":"4d2c...","threadId":"0f8a2b3c-5d6e-4f70-8a91-b2c3d4e5f607","code":"ALL_PROVIDERS_FAILED","cause":{"kind":"auth-rejected","providerId":"anthropic","modelId":"claude-sonnet-4-5"}}Ejemplo: Ningún Modelo Configurado
Sección titulada «Ejemplo: Ningún Modelo Configurado»event: faileddata: {"kind":"failed","runId":"b17e...","threadId":"3e5a...","code":"NO_MODEL_AVAILABLE"}[!NOTE] Si la propia ruta lanza una excepción, emite
{"kind":"failed","runId":"unknown","code":"INTERNAL_ERROR"}(sinthreadId) antes de cerrar.