Saltar al contenido

Modelos

Nombrar un modelo

Agent("a", model="openai-compatible:qwen3-8b")

El prefijo dice quién normaliza la respuesta, no solo qué modelo se quiere. Es nuestro y no viaja por el cable: al proveedor le llega qwen3-8b a secas.

Las tres puertas

El agente no sabe cuál tiene, y eso es la propiedad, no comodidad.

Sin inferencia — el doble

Por defecto. Ver Probar.

Transporte HTTP directo

from synaptum import HttpModel, LocalGateway

modelo = HttpModel("http://localhost:8080/v1", api_key=None)
gateway = LocalGateway(model=modelo, stream=modelo.stream, tools=[...])

Solo stdlib — urllib en un hilo, porque el núcleo no tiene dependencias y no las va a tener por esto. Vale para cualquier endpoint OpenAI-compatible: vLLM, Ollama, LM Studio, llama.cpp o la API de OpenAI.

Es transporte de desarrollo. En el camino gobernado la llamada al modelo no ocurre en este proceso: sale por la costura hacia quien tiene las credenciales y puede denegar. Un transporte dentro del proceso tiene la clave en su memoria y llama a quien le digan.

Un SDK de plataforma

Cuando la inferencia está gobernada y un SDK es la única puerta legítima —credenciales, catálogo, cuotas y facturación viven ahí— tiene prioridad. Ir al endpoint por detrás se salta todo eso.

from synaptum.providers.axonium import AxoniumModel   # pip install synaptum[axonium]

puente = AxoniumModel()
gateway = LocalGateway(model=puente.complete, stream=puente.stream, tools=[...])

Esto no es un Provider: un Provider normaliza y no transporta, y un SDK así hace las dos cosas. Lo que falta es traducir entre dos vocabularios que ya están normalizados.

Escribir un adaptador

Provider es un Protocol de tres funciones puras: to_wire, from_wire, stream_from_wire. No abren conexiones ni leen credenciales.

Que la normalización sea separable del transporte es lo que permite ejercitarla contra un fichero —sin servidor, sin red, sin gastar— y es la razón de que el corpus dorado exista.

Los adaptadores se descubren por entry points, así que el núcleo no conoce a ninguno.

Usage tiene tres estados

Usage(input=100, output=20, cache_read=80, estimated=True)
#     reasoning=None, cache_write=None   ← nadie los midió
None Nadie lo midió.
0 Se midió y fue cero.
estimated=True Derivado, no reportado.

Confundir None con 0 es lo que hace creer que escribir en caché es gratis, y el bucle decide mal en cada compactación. Lo desconocido se propaga al sumar: si un tramo no midió un contador, el total de ese contador es None — sumar solo lo conocido daría una cota inferior con aspecto de cifra exacta.

input es inclusivo: contiene los tokens servidos desde caché, y cache_read dice cuántos. Confirmado contra grabaciones reales, donde prompt_tokens == prompt_n + cache_n.

El prefijo estable

Los proveedores cachean prefijos exactos. Medido contra un despliegue real, con un prefijo de 2.477 tokens:

1ª llamada         input=2477   cache_read=0       ← nada cacheado todavía
mismo prefijo      input=2477   cache_read=2473    ← 99,8 % servido de caché
prefijo cambiado   input=2479   cache_read=0       ← tres palabras al principio

Cambiar tres palabras al inicio tiró la caché entera: un prefijo que cambia en el token 10 invalida los 10.000 siguientes.

Lo que no debe cambiar dentro de un run, en este orden: el modelo, las instrucciones, el catálogo de herramientas —nombres, descripciones y esquemas, en su orden— y el formato de salida. Los mensajes van después y crecen; crecer al final no invalida nada.

Reanudar con otra configuración está prohibido

ConfigurationError: El run 'r1' se creó con otra configuración y reanudarlo con esta
mezclaría dos agentes en un mismo diario (herramientas: ['leer']  ['leer', 'borrar']).
Reanudar es continuar ese run; una configuración distinta es otro run  usa un run_id nuevo.

Y el motivo de fondo no es el dinero. Que se pierda la caché es la consecuencia visible. La que hace daño es que la primera mitad del run la ejecutó una configuración y la segunda otra, y el journal lo registra como uno solo: una auditoría de «qué hizo el agente» devuelve una historia que ninguna configuración produjo nunca.

Reordenar herramientas cuenta como cambio, porque por el cable lo es.

Qué costó un run, y por qué

Usage dice cuánto. El informe de economía dice por qué, que es lo único accionable:

from synaptum import economy

informe = economy(await store.load("run-1"))
print(informe.report())
Run run-1 · 2 turnos
  caché      43% de la entrada servida de caché
  crecimiento +57 tokens de entrada por turno
  prefijo    estable durante todo el run
  total      entrada=411 salida=378 caché=177

Se calcula del journal, así que funciona sobre un run terminado, sobre uno reanudado y sin tener el agente delante. Un informe que solo se pudiera sacar mientras el run corre no serviría para lo único que hace falta: mirar ayer.

Lo que cada cifra responde

El acierto de caché se calcula sobre los totales, no como media de los turnos: una media pesaría igual un turno de 50 tokens que uno de 50.000. Y si nadie lo midió dice sin medir, no 0 % — confundirlas manda a alguien a arreglar lo que no está roto.

Las reescrituras de prefijo deberían ser cero. Reanudar con otra configuración ya está prohibido, pero dentro de un run el prefijo puede romperse solo: unas instrucciones con la fecha dentro lo reescriben en cada turno, y el síntoma es una caché que nunca arranca.

  ⚠ prefijo  reescrito 1 vez — cada una tira toda la caché posterior
      · 000001-model: instrucciones de sistema: cambiaron

El crecimiento dice cuánto sube la entrada por turno. Es la pendiente entre el primero y el último, no una regresión: con cinco puntos, una regresión da una cifra más precisa y no más cierta.

Lo que no hace

No exporta nada. Los nombres de atributo, las unidades y el transporte son de la instrumentación (SYN-37), y esa forma la fija la plataforma de observabilidad que los consuma.

No habla de dinero. Los tokens se saben; los precios no, y convertirlos con una tarifa inventada daría una cifra con aspecto de exacta.

Streaming

async for evento in agente.stream(tarea, session=sesion):
    if evento.kind == "text_delta":
        print(evento.text, end="", flush=True)

Es el mismo bucle y el mismo journal que run(); lo único que cambia es que los fragmentos del modelo se ceden intercalados entre la intención del paso y su resultado.

Va aparte de run() porque cambia el tipo de lo que se cede: quien consume run() hace match sobre pasos sin una rama para lo que nunca va a llegar.

Cancelar es dejar de iterar

No hay evento de cancelación, y no lo habrá: un canal que se está cerrando no es sitio para mandar el aviso de que se cierra. Cerrar el iterador cierra el cuerpo de la respuesta, y eso es lo que para la generación arriba.

flujo = agente.stream(tarea, session=sesion)
async for evento in flujo:
    if suficiente(evento):
        break
await flujo.aclose()          # esto es la señal

Reintentos

Un error trae en su tipo si es reintentable — la decisión no es del bucle, la sabe quien habló con el proveedor. Cualquier 4xx salvo 429 no lo es; 429, 5xx, timeouts y fallos de red sí.

El bucle espera entre intentos, con espera creciente y jitter, y respeta el Retry-After del otro extremo con un techo. Reintentar al instante no es reintentar: es repetir contra el mismo estado roto. Y sin jitter, N agentes que caen por la misma razón reintentan a la vez y reconstruyen el pico que los tiró.

Limits(max_retries=2, retry_base=0.5, max_retry_wait=30.0)

Salida estructurada

@dataclass
class Diagnostico:
    causa: str
    confianza: float

Agent("a", model=..., output=Diagnostico)

Una dataclass de stdlib basta; Pydantic es un extra para quien ya lo use.

El esquema viaja por dos caminos a la vez: en response_format y en las instrucciones. No todo gateway admite el campo —algunos lo descartan avisando por warning— y entonces la restricción no viaja, el modelo contesta en prosa y el error culpa al JSON. Decirlo también en el prompt cuesta unos cientos de tokens y funciona con cualquier proveedor.

La validación ocurre dentro del reintento: un objeto mal formado no es un fallo del run, es una muestra mala, y el muestreo es estocástico.