Análisis con LLM
Compatible con la forma de OpenAI. Lo que cambia es qué campos se atienden.
POST /v1/chat/completions. Scope: llm:invoke.
La forma de petición y respuesta es la de OpenAI, así que el SDK oficial funciona
apuntando base_url a https://api.callsist.com/v1. Lo que cambia es qué campos se
atienden.
Petición
{
"model": "callsist-llm-1-small", // o "callsist-llm-1-large"
"messages": [
{ "role": "system", "content": "Eres un analista de calidad de un BPO." },
{ "role": "user", "content": "…" }
],
"temperature": 0, // 0 a 2
"max_tokens": 4000, // entero positivo, máximo 32000
"stream": false,
"response_format": { "type": "json_object" },
"reasoning_effort": "none" // "none" | "low" | "medium" | "high"
}role solo admite system, user y assistant. content solo admite cadena:
el formato multimodal de OpenAI —array de partes— se rechaza con 400.
Todo campo que no esté en esa lista se descarta en silencio. No da error: se ignora.
Eso incluye tools, tool_choice, top_p, seed, stop, n, presence_penalty,
frequency_penalty y logprobs.
No hay tool calling
tools y tool_choice no existen en el esquema y se descartan sin avisar. Un mensaje
con role: "tool" se rechaza con 400.
Esto rompe los frameworks de agentes que dan el tool calling por hecho: el modelo contesta en prosa, el framework no encuentra la llamada a herramienta que espera y falla con un mensaje que no apunta a la causa.
Para un RAG, el patrón correcto es recuperar antes y meter el contexto en el prompt:
- embebe la consulta con
POST /v1/embeddings; - busca en tu propio almacén de vectores;
- mete los fragmentos recuperados en el mensaje
userosystem; - llama a
/v1/chat/completionsconresponse_format: { "type": "json_object" }.
Es lo mismo que haría el tool calling con una vuelta menos.
reasoning_effort — el parámetro que hay que entender
Los modelos de chat razonan por defecto y emiten unos 420 tokens de razonamiento oculto
por llamada, aunque devuelvan JSON limpio. Callsist manda reasoning_effort: "none"
automáticamente salvo que pidas otra cosa, y eso es lo correcto para clasificación y
extracción: dejarlo activo duplica el coste del módulo y multiplica por 35 los tokens
de salida.
"low" no ahorra: gasta lo mismo que el defecto. Si quieres razonamiento, pide
"medium" o "high" a sabiendas; si no, no mandes el campo.
Respuesta
{
"id": "chatcmpl_9xKp2mQvRt4L",
"object": "chat.completion",
"model": "callsist-llm-1-small",
"choices": [
{ "index": 0, "message": { "role": "assistant", "content": "{\"resultado\":…}" }, "finish_reason": "stop" }
],
"usage": { "prompt_tokens": 24318, "completion_tokens": 892, "total_tokens": 25210 }
}finish_reason es "stop" o "length". Con "length" el JSON viene truncado:
compruébalo antes de parsear, o tendrás un error de sintaxis que parece otra cosa.
JSON estructurado
response_format: { "type": "json_object" } empuja al modelo a devolver JSON, pero no
lo garantiza a nivel de gramática. En producción:
- pide el esquema exacto en el prompt, campo por campo;
temperature: 0;- valida el resultado contra tu esquema y reintenta una vez si no valida;
- acepta que un campo opcional venga con otra forma sin tirar el resto de la respuesta.
Streaming
Con "stream": true la respuesta es SSE con trozos chat.completion.chunk y termina con
data: [DONE]. Tres cosas que hay que saber:
- El stream nunca trae
usage. No hace falta mandarstream_options(se descarta): Callsist ya lo pide por dentro y la llamada sí se factura. Pero el bloque de uso no llega al cliente, así que si necesitas contar tokens, no uses streaming. - Un fallo a mitad de stream se cierra con
[DONE], no con un error HTTP, porque la cabecera ya salió. Un cliente que solo mire[DONE]no distingue una respuesta completa de una cortada: comprueba que llegó un trozo confinish_reason. - Nunca mandes
Idempotency-Keyconstream: true. El middleware de idempotencia consume la respuesta entera para memorizarla: el streaming deja de serlo.
Para análisis por lotes, no uses streaming. No aporta nada, no da tokens y complica el manejo de errores.
Timeout
El cliente HTTP interno tiene 120 s y hasta 3 reintentos con espera creciente. En el peor caso una petición tarda varios minutos en devolver un error, y no hay forma de acotarlo desde la petición. Pon tu propio timeout —60 a 120 s para lotes— y trátalo como reintentable.