Uso opencode, con T3 Code como interfaz, sobre los modelos abiertos que hospeda RunInfra. Las sesiones largas de agente se morían siempre igual: un error, y de ahí en adelante fallaba todo. opencode reenvía el historial completo en cada turno, así que lo que rompe una petición la rompe para siempre y la sesión se queda atorada.
Fueron tres errores:
Hosted inference accepts at most 8 images per request. Cuenta las imágenes de toda la petición, turnos anteriores incluidos: el noveno screenshot de la sesión la tumba.Payload too large. The maximum request body is 3.5 MB. Un límite en bytes, aparte de la ventana de contexto. Una sesión real lo topó con unos 623k tokens, en modelos que anuncian 1M de contexto. Los clientes compactan por tokens, no por bytes, así que nada se activa antes del choque.messages[N].tool_calls[0].function.arguments must be a JSON object encoded as a string; received string.
Para ser justo con RunInfra, los dos primeros están documentados: en los límites de salida de Chat Completions, en la página de errores, y /v1/models hasta publica max_request_bytes. El problema es que nadie los maneja para los clientes de agentes.
Mi lectura, y es solo eso: límites duros como estos muy probablemente son lo que hay detrás del precio de RunInfra, que en mi experiencia anda en la mitad de lo que cobran otros. Me parece un buen trato. Pero alguien tiene que absorber esos límites, y con un agente, ese alguien es el cliente.
El tercer error probablemente era mío
Ese no lo reproduje; lo deduje de los logs. Dos servidores de opencode estaban usando la misma base de datos de sesiones: el de T3 Code y el servicio en segundo plano de opencode, que se levanta solo con cualquier comando de la CLI. Los errores empezaron justo cuando arrancó ese servicio. Parte del arreglo es opencode service stop. Por si acaso, el plugin además convierte en objeto cualquier input de tool call que no lo sea.
Qué hace el plugin
Es un plugin para opencode 2.x, limitado al proveedor runinfra. Solo reescribe la petición que sale; la sesión guardada no se toca.
Cada imagen recibe un número fijo según su primera aparición. Las que se quedan llevan una etiqueta antes; las descartadas se vuelven texto:
[imagen 9 · /tmp/shot-menu.png · <one-line description>]
[imagen 2 omitida · <path> · <description> · vuelve a leer ese archivo con read si la necesitas]
Así el modelo sabe qué perdió y puede releer justo el archivo que necesita. Las imágenes sin archivo (capturas pegadas, data URIs) se guardan una vez en un directorio de caché para poder releerlas.
La descripción se genera una sola vez por imagen (con caché por hash) con el modelo de RunInfra más barato que acepte imágenes, elegido de GET /v1/models por input_modalities y pricing: hoy glm-5-3-flash, a unos $0.0001 por imagen.
Para el tamaño: si la petición pasa de 3 MB, recorta la salida de herramientas más vieja hasta 2 MB y, si no alcanza, los strings largos en inputs de tool calls viejos. Nunca toca los mensajes de sistema, tu último mensaje ni los 10 más recientes. Aparte, a cada modelo de RunInfra le pongo limit.context en unos 400000 para que opencode compacte antes del límite de bytes.
El arreglo que rompió la factura
El caché de prompts de RunInfra reutiliza el prefijo más largo que sea idéntico byte por byte. Todo lo que va después del primer cambio se cobra sin caché, y el input en caché cuesta más o menos la décima parte.
Mi primera versión recortaba lo justo para caber, en cada turno. Suena ordenado. En la práctica el corte se mueve cada turno, el prefijo cambia cerca del inicio y casi toda la sesión se cobra sin caché en cada petición. Arreglé un límite y descompuse la economía de la sesión.
El arreglo es aburrido: recortar por lotes, con histéresis. Las imágenes bajan de 8 a 4 de un jalón; el historial, de 3 MB a 2 MB. El plugin guarda esas decisiones por sesión, así que los turnos siguientes repiten exactamente los mismos cortes y el prefijo sigue idéntico hasta el siguiente lote.
Lo medí contra la API de RunInfra. En las 10 peticiones después de cruzar el límite de tamaño, los tokens de prompt en caché pasaron de 57% a 98%: de 443k tokens sin caché a 17.6k, unas 25 veces menos. En los turnos con imágenes, de 75% a 91%. Es una sola corrida por versión y RunInfra no garantiza cuánto retiene el caché, así que tómalo como referencia.
A esto me refería con presupuestos de contexto en La costura es el producto. El presupuesto no es solo de tokens: son bytes, número de imágenes y prefijos de caché, y mover uno cambia la factura de los demás.
Lo que le pedí a RunInfra
El plugin es un parche. Con esto, casi todo sobraría:
- Contar el tope de imágenes solo sobre los turnos recientes, o subirlo.
- Aceptar cuerpos de petición con gzip, o subir los 3.5 MB.
- Mencionar los límites en la guía de OpenCode y poner ahí un límite de contexto que quepa en 3.5 MB.
- Tratar los argumentos
""como{}.
Instálalo
Pégale esto a tu agente de código:
Install this opencode plugin: https://github.com/OmarAlex24/opencode-runinfra-sanitize
El agente sigue el README del repo y lo deja listo, pero te enseña el cambio al límite de contexto antes de aplicarlo. A mano es un solo curl:
curl -fsSL --create-dirs -o ~/.config/opencode/plugins/runinfra-sanitize.ts https://raw.githubusercontent.com/OmarAlex24/opencode-runinfra-sanitize/main/plugins/runinfra-sanitize.ts
Aburrido pero depurable: cada corte queda en un JSON por sesión, y el log de depuración te dice dónde cambió el prefijo.