Ir al contenido

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.


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).

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).

Ventana de terminal
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 de ai_advisor_execution_profiles que no se pueda parsear no se captura: GET responde HTTP 500 y una ejecución termina failed con INTERNAL_ERROR. Recupérate escribiendo un valor válido con PUT o eliminando el ajuste.


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.

event: tool-start
data: {"kind":"tool-start","runId":"7c1e0a52-3f4b-4f08-9d8e-2b6a1c9d0e11","callId":"call_01","tool":"portfolio_summary"}
event: tool-result
data: {"kind":"tool-result","runId":"7c1e0a52-3f4b-4f08-9d8e-2b6a1c9d0e11","callId":"call_01","tool":"portfolio_summary"}
event: token
data: {"kind":"token","runId":"7c1e0a52-3f4b-4f08-9d8e-2b6a1c9d0e11","text":"Your largest position is "}
event: token
data: {"kind":"token","runId":"7c1e0a52-3f4b-4f08-9d8e-2b6a1c9d0e11","text":"BTC at `15000` USD."}
event: done
data: {"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}
event: refused
data: {"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"}
event: failed
data: {"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"}}
event: failed
data: {"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"} (sin threadId) antes de cerrar.