Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Suivi des coûts

ZeroClaw enregistre chaque appel de modèle portant des jetons dans un registre en ajout seul, y compris les appels dont la tarification n’est que partiellement disponible. Il attribue les dépenses à l’agent d’origine, applique des budgets quotidiens et mensuels sur la partie tarifée, et présente à l’opérateur à la fois les dépenses et l’exposition due à une tarification manquante. Les règles de tarification sont définies dans la configuration, afin que les opérateurs puissent les modifier sans recompilation.

Cette page décrit le schéma, le pipeline de recherche et les surfaces opérateur. Le code se trouve dans crates/zeroclaw-config/src/cost/ et crates/zeroclaw-runtime/src/agent/cost.rs.

Schéma de configuration

Deux sections connexes possèdent la surface. cost couvre l’application du budget et le comportement d’enregistrement. cost.rates.* est la grille tarifaire gérée par l’opérateur ; le chemin en notation pointée de chaque sous-section reflète le chemin providers.* correspondant, le segment final <alias> étant remplacé par la ressource amont tarifée.

Pourquoi la clé est un identifiant de ressource, et non un alias

Une entrée [providers.models.anthropic.<alias>] est indexée par un alias choisi par l’opérateur (glados, production) qui respecte le validateur d’alias : ASCII en minuscules, tirets bas simples, pas de traits d’union. Une entrée [cost.rates.providers.models.anthropic.<resource>] est indexée par l’identifiant de modèle amont tel qu’il apparaît dans la télémétrie d’utilisation (claude-opus-4-7, gpt-4o-mini, whisper-1) : ces chaînes d’identifiants proviennent de l’espace de noms du fournisseur et contiennent presque toujours des traits d’union.

Le schéma marque chaque HashMap de grille tarifaire avec #[resource_key] (dans crates/zeroclaw-macros/src/lib.rs). Cet attribut exclut le champ de validate_alias_key dans create_map_key / rename_map_key, de sorte que le POST /api/config/map-key de la passerelle accepte les ids comportant des traits d’union. Sans cela, create_map_key rejette tout id de modèle réaliste et l’interface de la grille tarifaire échoue. Les alias et les ids de ressource partagent la même structure sur disque (HashMap<String, T>), mais il s’agit de systèmes de nommage différents avec des validateurs différents.

Les listes de slots sont l’unique source de vérité

Les emplacements par type de fournisseur sous [cost.rates.providers.models.<type>], [cost.rates.providers.tts.<type>] et [cost.rates.providers.transcription.<type>] se développent à partir des mêmes macros qui pilotent les wrappers d’emplacement [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);
}

L’ajout d’un nouveau type de fournisseur de modèles correspond à une ligne dans for_each_model_provider_slot! ; l’emplacement de grille tarifaire, l’emplacement de configuration du fournisseur et les menus déroulants du tableau de bord en découlent tous. Aucune table de répartition saisie à la main, aucune liste de chaînes parallèle sur le frontend.

Tarification au moment de la requête

Le pipeline depuis [cost.rates.*] vers une valeur cost_usd enregistrée est :

  1. Le démarrage de l’orchestrateur construit la table de tarification. Lorsque le superviseur de canaux instancie un contexte d’exécution pour un agent, il parcourt config.cost.rates.providers.models.iter_entries() et fusionne les tarifs dans une HashMap<provider_type, HashMap<key, f64>> où key est "<model_id>.input", "<model_id>.output" ou "<model_id>.cached_input". La table héritée par alias [providers.models.<type>.<alias>].pricing est également fusionnée ; [cost.rates.*] l’emporte en cas de conflit car c’est la surface orientée vers l’avenir. (Voir crates/zeroclaw-channels/src/orchestrator/mod.rs, la closure sous cost_tracking: CostTracker::get_or_init_global(...).map(|tracker| ...).)

  2. Enregistrement à l’intérieur de la boucle de l’agent. Chaque réponse LLM réussie atteint record_tool_loop_cost_usage(provider_name, model, usage) dans crates/zeroclaw-runtime/src/agent/cost.rs. La fonction récupère l’emplacement de la table de tarification pour provider_name, appelle resolve_rates(map, model), multiplie par le nombre de tokens, et stocke un CostRecord via le CostTracker global.

  3. Résolution et validation par dimension. resolve_rates_opt essaie d’abord l’identifiant du modèle, puis la forme à suffixe de chemin pour les chaînes provider/model (ainsi anthropic/claude-opus-4-7 se replie sur claude-opus-4-7 si l’opérateur n’a stocké que la forme courte). Il conserve un Option par dimension, de sorte que la tarification en direct et le catalogue global ne comblent que les lacunes. Un tarif d’entrée ou de sortie explicitement fixé à 0.0 reste configuré et gratuit. Un tarif d’entrée en cache fixé à 0.0 est également configuré, mais il n’est pas gratuit : le constructeur de coût existant le traite comme « pas de remise sur le cache » et facture les tokens en cache au tarif d’entrée standard. Les tarifs négatifs, non finis ou implausiblement élevés (au-dessus de la borne de sécurité partagée de $1,000,000 par unité configurée) sont considérés comme indisponibles à la frontière d’enregistrement, même si un chemin de chargement de configuration plus ancien ou plus permissif les a laissé passer.

    Les dimensions d’entrée, d’entrée en cache et de sortie manquantes ne sont évaluées que lorsque cette dimension contient des tokens. L’avertissement d’exécution émis une seule fois identifie les dimensions incomplètes. Un appel partiellement tarifé enregistre donc son coût connu et enregistre séparément uniquement le sous-ensemble de tokens qui n’a pas pu être tarifé.

  4. CostTracker est un singleton global au processus (OnceLock dans crates/zeroclaw-config/src/cost/tracker.rs). Le rechargement applique la dernière CostConfig au tracker existant, et si la surveillance des coûts était désactivée au démarrage, un rechargement ultérieur avec cost.enabled = true construit le tracker à la demande. La carte tarifaire de l’orchestrateur est également reconstruite à chaque rechargement du daemon à partir de la configuration active, de sorte que les modifications de taux prennent effet lors de la prochaine requête après rechargement.

Tarification en temps réel depuis les passerelles

Les opérateurs n’ont pas à maintenir manuellement un tarif pour chaque modèle. Un fournisseur peut opter pour la récupération des prix des tokens directement depuis sa propre passerelle en définissant live_pricing = true sur ce bloc de fournisseur (en plus de ses paramètres api_key et de modèle existants) ; les prix proviennent de la liste /models de la passerelle elle-même.

Comportement :

  • La passerelle est la source principale. L’endpoint /models existant du fournisseur (le même utilisé par l’onboarding pour lister les modèles) est analysé afin d’en extraire les tarifs par modèle. Les passerelles qui publient les prix y indiquent ces derniers sous forme de chaînes décimales par token (les pricing{prompt,completion,...} d’OpenRouter et de Kilo), qui sont ensuite rééchelonnées en USD pour 1M de tokens. Une passerelle dont le listing /models ne contient aucun prix (comme opencode zen, qui ne liste que les identifiants de modèles) est couverte par le fallback models.dev ci-dessous. Aucune seconde copie de l’URL de l’endpoint ou des identifiants n’est requise : ils sont lus depuis la configuration existante du fournisseur.
  • Fallback models.dev. Un modèle que la passerelle ne tarifie pas (ou un fournisseur ne disposant pas de liste HTTP /models du tout, comme une passerelle de sous-processus telle que kilocli) se rabat sur le catalogue public models.dev (api.json), indexé par le nom models.dev de la famille (voir catalog_source_for dans crates/zeroclaw-providers/src/catalog.rs). Le catalogue de fallback est récupéré à jour à chaque cycle de rafraîchissement, afin que les deux sources suivent les variations de prix en amont avec la même cadence horaire.
  • La configuration prime toujours. Les prix en direct ne remplissent que les dimensions pour lesquelles un modèle n’a pas d’entrée [cost.rates] / pricing. Un taux configuré (y compris un 0.0 volontaire) n’est jamais écrasé. Il s’agit d’un palliatif, pas d’un remplacement.
  • Une requête par gateway, uniquement pour les modèles marqués. Les alias partageant un gateway sont dédupliqués en une seule requête /models ; à partir de cette réponse, seul le model configuré pour chaque alias activé est renseigné, et non pas chaque modèle listé par le gateway.
  • Actualisation en arrière-plan, sans blocage. Une tâche unique actualise un instantané des prix à l’échelle du processus toutes les heures. Le chemin d’enregistrement des coûts lit l’instantané mis en cache de manière synchrone et n’effectue jamais d’appel réseau en ligne, ce qui empêche une passerelle lente de bloquer la comptabilité des requêtes.
  • Désactivé par défaut. Aucun fournisseur n’active live_pricing = true, donc aucune tâche de rafraîchissement et aucun trafic réseau ; le comportement est identique à celui d’une compilation sans cette fonctionnalité. Désactiver le dernier fournisseur configuré à l’exécution (rechargement de la configuration) efface l’instantané lors du prochain cycle de rafraîchissement, ce qui fait cesser l’actualisation des prix en direct sans redémarrage. L’instantané réside uniquement dans zeroclaw_providers::pricing (voir crates/zeroclaw-providers/src/pricing.rs) ; il est lu par record_tool_loop_cost_usage et démarré une seule fois à partir du superviseur de canaux et du démarrage de la passerelle.

Comme pour [cost.rates], un prix en direct n’affecte que les requêtes effectuées après le remplissage de l’instantané ; aucun recalcul rétroactif des prix n’est appliqué aux enregistrements passés.

Persistance

CostTracker::record_usage_with_agent ajoute un CostRecord par réponse contenant des jetons à <workspace>/state/costs.jsonl, un objet JSON par ligne. Le registre est lu au démarrage afin que le récapitulatif par agent du mois en cours du tableau de bord survive aux redémarrages.

Chaque enregistrement TokenUsage contient deux champs de provenance des tarifs :

  • unpriced_tokens : le sous-ensemble de jetons d’entrée, d’entrée en cache ou de sortie dont le tarif effectif n’était pas disponible. Il vaut 0 pour les appels entièrement tarifés, y compris les appels facturés à un tarif d’entrée ou de sortie 0.0 volontairement gratuit, et les appels dont le tarif d’entrée en cache 0.0 a été remplacé par le tarif d’entrée standard.
  • pricing_available : résumé de compatibilité de cette provenance. Les nouveaux enregistrements le définissent à false lorsque unpriced_tokens > 0.

Les lignes du registre écrites avant l’existence de ces champs sont désérialisées avec pricing_available = true et unpriced_tokens = 0. Une dépense nulle sur une ligne héritée ne prouve donc pas que la tarification historique était disponible ; la provenance manquante ne peut pas être reconstruite de manière sûre a posteriori.

cost_usd est calculé au moment de l’enregistrement à partir de la grille tarifaire en vigueur à cet instant. Les enregistrements sont immuables : si l’opérateur ajoute des tarifs après que certaines requêtes ont déjà été enregistrées, ces enregistrements existants conservent cost_usd = 0. Seules les requêtes effectuées après la configuration du tarif (et le rechargement du daemon afin que la table de tarification de l’orchestrateur soit reconstruite) portent un coût non nul.

Il s’agit de la surprise la plus courante après l’activation initiale de la grille tarifaire. La solution consiste à attendre de nouvelles requêtes ; il n’y a pas de retarification rétroactive.

Application du budget

CostConfig::enforcement.mode détermine ce qui se passe lorsqu’un coût projeté dépasserait la limite configurée pour daily_total ou monthly_total :

  • warn : la valeur par défaut ; enregistre l’événement avec un journal de niveau warn et laisse passer la requête.
  • block : refuser la requête avec une erreur BudgetExceeded.
  • route_down : remplace le modèle d’origine par route_down_model (une alternative moins coûteuse). La substitution a lieu avant l’envoi de la requête.

allow_override = true permet à une requête de contourner block en passant un jeton de remplacement via la CLI (zeroclaw --override). La valeur par défaut est false. warn_at_percent contrôle le moment où la passerelle affiche une bannière d’avertissement avant la limite stricte ; la valeur par défaut est 80 %.

Les comparaisons de budget utilisent le cost_usd enregistré, elles ne peuvent donc pas tenir compte du sous-ensemble unpriced_tokens. Un total quotidien ou mensuel inférieur à son plafond n’est pas une garantie de sécurité tant que le mois en cours contient des unpriced_tokens. Configurez les dimensions de tarif manquantes et générez une nouvelle requête avant de vous fier à l’application des plafonds ; les lignes existantes du registre ne sont jamais retarifées rétroactivement.

Attribution par agent

Lorsque cost.track_per_agent est défini sur true (valeur par défaut), chaque CostRecord enregistré porte l’alias de l’agent d’origine. Le panneau Spend by agent du tableau de bord et GET /api/cost?agent=<alias> consomment ce champ. Définir track_per_agent = false est une optimisation pour les installations à fort volume où l’agrégation HashMap supplémentaire apparaît dans les profils ; le compromis est la perte de la dimension par agent partout.

Surfaces de l’opérateur

État du CLI

zeroclaw status affiche les dépenses du jour et du mois en cours à partir du registre. Lorsqu’un modèle a enregistré unpriced_tokens > 0 à n’importe quel moment du mois UTC en cours, la commande affiche également un avertissement d’indisponibilité de la tarification, listant les modèles concernés et le nombre total de tokens non facturés pour l’ensemble de ces modèles. L’avertissement s’appuie sur un agrégat par modèle à l’échelle du mois (CostTracker::get_current_month_model_stats), et non sur la ventilation quotidienne by_model utilisée par le tableau de bord ; ainsi, l’usage non tarifé d’un jour antérieur du mois reste visible après le passage au jour UTC suivant et après un redémarrage. L’avertissement est volontairement indépendant du total monétaire agrégé : les lignes mixtes tarifées/non tarifées restent visibles, tandis qu’un modèle gratuit entièrement configuré reste silencieux.

Cet avertissement signifie que le montant dépensé affiché est une borne inférieure et que la marge restante avant le plafond est une borne supérieure, et non un décompte complet. Les anciennes lignes ne peuvent pas déclencher cet avertissement, car leur provenance historique de tarification est inconnue.

Interface de configuration

  • /config/cost → onglet Limits : chaque champ plat [cost].* (enabled, limits, enforcement, track_per_agent). Les lignes de la grille tarifaire ne sont pas modifiées ici, elles sont liées au fournisseur propriétaire du modèle, elles se trouvent donc un niveau en dessous.
  • /config/providers.<category>/<type> → onglet Costs : éditeur de grille tarifaire pour ce type de fournisseur. Le champ + Add suggère des identifiants de ressources en amont issus de providers.<category>.<type>.*.model parmi les alias configurés, afin que l’opérateur puisse ajouter en un clic une ligne de tarif pour chaque modèle qu’il a réellement lié. Il s’agit du seul point d’entrée pour modifier [cost.rates.providers.<category>.<type>.*].

Tableau de bord

L’onglet Cost du tableau de bord affiche trois panneaux ainsi qu’un sélecteur de période (aujourd’hui / 7 derniers jours / 30 derniers jours / ce mois-ci / depuis le début) :

  • Totaux de dépenses : totaux quotidiens et mensuels issus de costs.jsonl.
  • Dépenses par agent · <window> : agrégation par agent sur la fenêtre sélectionnée. Visible lorsque track_per_agent est à true.
  • Dépenses par modèle · <window> : agrégation par modèle. L’identifiant de modèle de chaque ligne est cliquable ; le clic résout le type de fournisseur propriétaire à partir des alias configurés et navigue vers l’onglet Costs de ce fournisseur. Lorsque l’identifiant de modèle n’est lié à aucun fournisseur configuré, le clic est sans effet (il n’existe pas de route de grille tarifaire qualifiée pour un modèle orphelin).

Passerelle

  • GET /api/cost : CostSummary actuel (correspond à la structure de l’aperçu des coûts du tableau de bord). Ajoutez ?agent=<alias> pour une vue par agent unique.
  • GET /api/config/templates : chaque section indexée par map enregistrée par le schéma, utilisée par les listes déroulantes catégorie × type de fournisseur de l’onglet Rates.
  • POST /api/config/map-key?path=cost.rates.providers.<category>.<type>&key=<resource> crée une nouvelle ligne de tarif. Le chemin est rejeté si aucune section de map de ce type n’existe ; la clé de ressource passe par #[resource_key] au lieu de validate_alias_key.

Dépannage

Le tableau de bord affiche 0,0000 $ pour tous les agents après la configuration des tarifs. Les anciens enregistrements sont immuables, ils ont été enregistrés avec cost_usd = 0 car aucun tarif n’était défini au moment où ils ont eu lieu. Effectuez une nouvelle requête de chat après le rechargement du démon et vérifiez Cost overview > Session ainsi que Spend by model ; les deux devraient se remplir pour la nouvelle requête.

zeroclaw status indique que la tarification est indisponible alors que certains tarifs sont configurés. La tarification est résolue pour chaque dimension portant des jetons. Par exemple, un tarif d’entrée ne tarifie pas les jetons de sortie, et un tarif d’entrée en cache ne tarifie pas les entrées non mises en cache. Ajoutez les dimensions indiquées par l’avertissement d’exécution. Un tarif d’entrée ou de sortie configuré à 0.0 est un tarif gratuit valide et ne déclenche pas l’avertissement d’état ; un tarif d’entrée en cache configuré à 0.0 facture les jetons en cache au tarif d’entrée standard et ne le déclenche pas non plus.

Dérive détectée sur les chemins cost.rates.* après l’enregistrement. Un daemon antérieur à la v0.8.0 corrompait les clés de HashMap contenant des traits d’union dans le chemin de sauvegarde des modifications (dirty-save), supprimant silencieusement chaque écriture dans la grille tarifaire. Si vous constatez ce problème sur la v0.8.0 ou une version ultérieure, il s’agit d’un vrai bug : la résolution du chemin des modifications (dirty-path) se trouve dans crates/zeroclaw-config/src/schema.rs::apply_dirty_path ; ouvrez un ticket en indiquant la version du daemon et le chemin ayant dérivé.

Les avertissements missing_pricing saturent le journal. Le runtime émet cet avertissement une fois par couple (provider_type, model) lorsqu’une dimension avec jetons (entrée, entrée en cache ou sortie) reste sans tarif après que resolve_rates_opt a fusionné les tarifs configurés, en direct et du catalogue global. L’avertissement indique les dimensions incomplètes ; un modèle partiellement configuré peut donc déclencher un avertissement alors qu’une autre dimension est déjà tarifée. Ajoutez uniquement les dimensions manquantes pour l’identifiant de modèle exact indiqué dans l’avertissement. Certains fournisseurs renvoient des identifiants versionnés, comme claude-3-5-sonnet-20241022, alors que l’identifiant configuré est claude-3-5-sonnet ; le résolveur essaie aussi son candidat de suffixe de chemin documenté avant de signaler l’écart.