Seguimiento de costos
ZeroClaw registra cada llamada a la API con tarificación en un libro contable de solo anexado, atribuye el gasto al agente de origen, aplica presupuestos diarios/mensuales y muestra el resumen en la pestaña Cost del panel. Las reglas de tarificación residen en la configuración para que los operadores puedan editarlas sin necesidad de una recompilación.
Esta página describe el esquema, el flujo de búsqueda y las superficies del operador. El código se encuentra en crates/zeroclaw-config/src/cost/ y crates/zeroclaw-runtime/src/agent/cost.rs.
Esquema de configuración
Dos secciones relacionadas controlan la superficie. cost cubre la aplicación del presupuesto y el comportamiento de registro. cost.rates.* es la tabla de tarifas gestionada por el operador; la ruta con puntos de cada subsección refleja la ruta providers.* correspondiente, con el segmento final <alias> reemplazado por el recurso ascendente al que se le asigna precio.
Por qué la clave es un id de recurso, no un alias
Una entrada [providers.models.anthropic.<alias>] está identificada por un alias elegido por el operador (glados, production) que cumple con el validador de alias: ASCII en minúsculas, guiones bajos simples, sin guiones. Una entrada [cost.rates.providers.models.anthropic.<resource>] está identificada por el id del modelo upstream tal como aparece en la telemetría de uso (claude-opus-4-7, gpt-4o-mini, whisper-1): esas cadenas de id provienen del espacio de nombres del proveedor y casi siempre contienen guiones.
El esquema marca cada HashMap de hoja de tarifas con #[resource_key] (en crates/zeroclaw-macros/src/lib.rs). Ese atributo excluye el campo de validate_alias_key en create_map_key / rename_map_key, de modo que el POST /api/config/map-key de la puerta de enlace acepta ids con guiones. Sin él, create_map_key rechaza todo id de modelo realista y la interfaz de usuario de la hoja de tarifas se viene abajo. Los alias y los ids de recursos comparten la estructura en disco (HashMap<String, T>), pero son sistemas de nomenclatura diferentes con validadores diferentes.
Las listas de slots son la única fuente de verdad
Los espacios por tipo de proveedor bajo [cost.rates.providers.models.<type>], [cost.rates.providers.tts.<type>] y [cost.rates.providers.transcription.<type>] se expanden a partir de las mismas macros que controlan los contenedores de espacios [providers.*]:
#![allow(unused)]
fn main() {
// crates/zeroclaw-config/src/providers.rs
for_each_model_provider_slot!(emit_model_cost_rates_struct);
for_each_tts_provider_slot!(emit_tts_cost_rates_struct, super::schema::TtsCostRates);
for_each_transcription_provider_slot!(emit_transcription_cost_rates_struct, super::schema::TranscriptionCostRates);
}
Agregar un nuevo tipo de proveedor de modelos es una fila en for_each_model_provider_slot!; el slot de la hoja de tarifas, el slot de configuración del proveedor y los menús desplegables del dashboard se expanden todos a partir de ella. Sin tablas de dispatch escritas a mano, sin listas de cadenas paralelas en el frontend.
Precios al momento de la solicitud
El flujo desde [cost.rates.*] hasta un valor cost_usd registrado es:
-
El arranque del orquestador construye el mapa de precios. Cuando el supervisor de canales instancia un contexto de tiempo de ejecución para un agente, recorre
config.cost.rates.providers.models.iter_entries()y fusiona las tarifas en unHashMap<provider_type, HashMap<key, f64>>dondekeyes"<model_id>.input","<model_id>.output"o"<model_id>.cached_input". La tabla heredada por alias[providers.models.<type>.<alias>].pricingtambién se fusiona;[cost.rates.*]prevalece en caso de conflicto porque es la superficie orientada al futuro. (Véasecrates/zeroclaw-channels/src/orchestrator/mod.rs, la clausura bajocost_tracking: CostTracker::get_or_init_global(...).map(|tracker| ...).) -
Registro dentro del bucle del agente. Cada respuesta exitosa del LLM llega a
record_tool_loop_cost_usage(provider_name, model, usage)encrates/zeroclaw-runtime/src/agent/cost.rs. La función obtiene la ranura del mapa de precios paraprovider_name, llama aresolve_rates(map, model), multiplica por los recuentos de tokens y almacena unCostRecordmediante elCostTrackerglobal. -
resolve_rates_opt intenta primero con el ID del modelo, luego con la forma de sufijo de ruta para cadenas
provider/model(asíanthropic/claude-opus-4-7degrada aclaude-opus-4-7si el operador guardó solo la forma corta). Devuelve unOptionpor dimensión, de modo que cualquier dimensión que el operador haya dejado sin configurar pueda completarse desde el fallback de precios en vivo (ver abajo) antes de facturar. Solo cuando ambos la configuración y el fallback en vivo dejan la entrada y la salida en0.0se dispara la advertencia única demissing_pricing, por lo que los registros genuinos de “no pudimos poner precio a esto” siguen apareciendo en los logs. -
CostTracker es un singleton de proceso global (
OnceLockencrates/zeroclaw-config/src/cost/tracker.rs). La recarga aplica laCostConfigmás reciente al tracker existente, y si el seguimiento de costos estaba deshabilitado al arrancar, una recarga posterior concost.enabled = trueconstruye el tracker bajo demanda. El mapa de precios del orquestador también se reconstruye en cada recarga del daemon a partir de la configuración activa, por lo que las ediciones de tarifas surten efecto en la siguiente solicitud después de la recarga.
Precios en vivo desde pasarelas
Los operadores no tienen que mantener manualmente una tarifa para cada modelo. Un proveedor puede optar por obtener los precios de los tokens directamente desde su propia pasarela configurando live_pricing = true en ese bloque del proveedor (junto con su api_key y la configuración de modelos existentes); los precios provienen del listado /models de la propia pasarela.
Comportamiento:
- Gateway es la fuente principal. El endpoint existente
/modelsdel proveedor (el mismo que usa la incorporación para listar modelos) se analiza para obtener el precio por modelo. Los gateways que publican precios allí los informan como cadenas decimales por token (laspricing{prompt,completion,...}de OpenRouter y Kilo), que se escalan a USD por 1M de tokens. Un gateway cuyo listado de/modelsno incluye ningún precio en absoluto (como opencode zen, que solo enumera ids de modelo) queda cubierto por la alternativa de models.dev que se indica abajo. No hay una segunda copia de la URL del endpoint ni de las credenciales: se leen de la configuración existente del proveedor. - models.dev fallback. Un modelo que la pasarela no tarifica (o un proveedor sin listado HTTP
/modelsen absoluto, como una pasarela de subproceso comokilocli) recurre al catálogo público de models.dev (api.json), indexado por el nombre de models.dev de la familia (véasecatalog_source_forencrates/zeroclaw-providers/src/catalog.rs). El catálogo de respaldo se obtiene de nuevo en cada ciclo de actualización, por lo que ambas fuentes siguen los cambios de precios upstream con la misma cadencia horaria. - La configuración siempre prevalece. Los precios en vivo rellenan solo las dimensiones para las que un modelo no tiene una entrada de
[cost.rates]/pricing. Una tarifa configurada (incluido un0.0deliberado) nunca se sobrescribe. Esto es un relleno de huecos, no un reemplazo. - Una llamada por gateway, solo modelos marcados. Los alias que comparten un gateway se deduplican a una sola recuperación de
/models; de esa respuesta, solo se completa elmodelconfigurado de cada alias que se haya inscrito, no todos los modelos que enumera el gateway. - Actualización en segundo plano, nunca bloquea. Una sola tarea actualiza una instantánea de precios a nivel de proceso cada hora. La ruta de registro de costos lee la instantánea en caché de forma sincrónica y nunca realiza una llamada de red en línea, por lo que una pasarela lenta no puede bloquear la contabilidad de solicitudes.
- Desactivado de forma predeterminada. Sin ninguna configuración de proveedor con
live_pricing = trueno hay tarea de actualización ni tráfico de red; el comportamiento es idéntico al de una compilación sin la funcionalidad. Al desactivar en tiempo de ejecución el último proveedor marcado (recarga de configuración), se limpia la instantánea en el siguiente ciclo de actualización, por lo que los precios en vivo dejan de rellenarse sin reiniciar. La instantánea vive solo enzeroclaw_providers::pricing(véasecrates/zeroclaw-providers/src/pricing.rs); la leerecord_tool_loop_cost_usagey se inicia una vez desde el supervisor de canales y el arranque del gateway.
Como [cost.rates], un precio en vivo solo afecta a las solicitudes realizadas después de que se haya completado la instantánea; no hay recálculo retroactivo de registros anteriores.
Persistencia
CostTracker::record_usage_with_agent añade un CostRecord por cada respuesta con coste a <workspace>/state/costs.jsonl, un objeto JSON por línea. El libro mayor se lee al iniciar para que la agregación por agente del mes actual del panel se conserve tras los reinicios.
cost_usd se calcula en el momento del registro a partir de la tabla de tarifas vigente en ese momento. Los registros son inmutables: si el operador agrega tarifas después de que algunas solicitudes ya hayan sido registradas, esos registros existentes conservan cost_usd = 0. Solo las solicitudes realizadas después de que se configure la tarifa (y se recargue el daemon para que se reconstruya el mapa de precios del orquestador) llevan un costo distinto de cero.
Esta es la sorpresa más común después de habilitar la hoja de tarifas por primera vez. La solución es esperar nuevas solicitudes; no hay retarificación retroactiva.
Aplicación de presupuesto
CostConfig::enforcement.mode decide qué sucede cuando un costo proyectado superaría daily_total o monthly_total por encima del límite configurado:
warn: el valor predeterminado; registra el evento con un log de nivel warn y deja pasar la solicitud.block: rechaza la solicitud con un errorBudgetExceeded.route_down: sustituyeroute_down_model(una alternativa más económica) por el modelo original. La sustitución ocurre antes de que se envíe la solicitud.
allow_override = true permite que una solicitud omita block pasando un token de anulación en la CLI (zeroclaw --override). El valor predeterminado es false. warn_at_percent controla cuándo la puerta de enlace muestra un banner de advertencia antes del límite estricto; el valor predeterminado es 80%.
Atribución por agente
Cuando cost.track_per_agent es true (valor predeterminado), cada CostRecord registrado incluye el alias del agente de origen. El panel Spend by agent del dashboard y GET /api/cost?agent=<alias> consumen este campo. Establecer track_per_agent = false es una optimización para instalaciones de alto volumen donde la agregación adicional de HashMap aparece en los perfiles; la contrapartida es perder la dimensión por agente en todas partes.
Superficies de operador
Interfaz de configuración
/config/cost→ pestaña Limits: todos los campos planos[cost].*(enabled, limits, enforcement, track_per_agent). Las filas de la tabla de tarifas no se editan aquí, están vinculadas al proveedor propietario del modelo, por lo que se encuentran un nivel más abajo./config/providers.<category>/<type>→ pestaña Costs: editor de tarifas para ese tipo de proveedor. La entrada+ Addsugiere ids de recursos upstream extraídos deproviders.<category>.<type>.*.modelentre los alias configurados, de modo que el operador puede agregar con un clic una fila de tarifa para cada modelo que haya vinculado realmente. Este es el único punto de entrada para editar[cost.rates.providers.<category>.<type>.*].
Panel de control
La pestaña Cost del panel muestra tres paneles más un selector de Ventana (hoy / últimos 7 días / últimos 30 días / este mes / todo el tiempo):
- Totales de gasto: totales diarios y mensuales de
costs.jsonl. - Gasto por agente ·
<window>: resumen acumulado por agente durante la ventana seleccionada. Visible cuandotrack_per_agentes true. - Gasto por modelo ·
<window>: resumen acumulado por modelo. El id de modelo de cada fila es clicable; el clic resuelve el tipo de proveedor propietario a partir de los alias configurados y navega a la pestaña Costs de ese proveedor. Cuando el id de modelo no está vinculado a ningún proveedor configurado, el clic no tiene efecto (no existe una ruta de tarifario calificada para un modelo huérfano).
Gateway
GET /api/cost:CostSummaryactual (coincide con la estructura de la vista general de costos del panel de control). Agregue?agent=<alias>para una vista de un solo agente.GET /api/config/templates: cada sección con claves de tipo map que registra el esquema, utilizada por los menús desplegables de categoría × tipo de proveedor de la pestaña Rates.POST /api/config/map-key?path=cost.rates.providers.<category>.<type>&key=<resource>crea una nueva fila de tarifa. La ruta se rechaza si no existe tal sección de mapa; la clave del recurso pasa por#[resource_key]en lugar devalidate_alias_key.
Solución de problemas
El panel muestra $0.0000 para todos los agentes después de configurar las tarifas. Los registros antiguos son inmutables, se registraron con cost_usd = 0 porque no había ninguna tarifa configurada cuando ocurrieron. Realiza una nueva solicitud de chat después de recargar el daemon y verifica Cost overview > Session junto con Spend by model; ambos deberían completarse para la nueva solicitud.
Se detectó deriva en las rutas cost.rates.* después de guardar. Un daemon anterior a v0.8.0 corrompía las claves con guiones de HashMap en la ruta de guardado de cambios pendientes (dirty-save), descartando silenciosamente cada escritura en la hoja de tarifas. Si ve esto en v0.8.0 o posterior, es un bug real: la resolución de rutas con cambios pendientes se encuentra en crates/zeroclaw-config/src/schema.rs::apply_dirty_path; abra un issue con la versión del daemon y la ruta que presentó la deriva.
Las advertencias missing_pricing saturan el log. Se emite una vez por cada par (provider_type, model) cuando resolve_rates devuelve (0.0, 0.0). O bien la tarifa no está configurada para ese modelo, o el upstream devolvió un id de modelo distinto al que aparece en la hoja de tarifas (algunos proveedores devuelven ids versionados como claude-3-5-sonnet-20241022 incluso cuando configuraste claude-3-5-sonnet). Agrega el id exacto que indica la advertencia, o configura el id sin versión y confía en la ruta de coincidencia por sufijo de resolve_rates.