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 d’API facturé dans un journal en ajout seul, attribue les dépenses à l’agent à l’origine de l’appel, applique des budgets quotidiens/mensuels et affiche le récapitulatif dans l’onglet Cost du tableau de bord. Les règles de tarification résident dans la configuration, ce qui permet aux opérateurs de 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>>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. resolve_rates_opt tente d’abord l’identifiant du modèle, puis la forme par suffixe de chemin pour les chaînes provider/model (de sorte que anthropic/claude-opus-4-7 soit simplifié en claude-opus-4-7 si l’opérateur n’a enregistré que la forme courte). Il renvoie une Option par dimension afin que toute dimension non configurée par l’opérateur puisse être complétée à partir de la solution de repli de tarification en temps réel (voir ci-dessous) avant la facturation. Ce n’est que lorsque la configuration et la solution de repli en temps réel laissent l’entrée et la sortie à 0.0 que l’avertissement unique missing_pricing se déclenche, de sorte que les enregistrements réels « nous n’avons pas pu tarifier cet élément » apparaissent toujours dans les journaux.

  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 pour chaque réponse facturée à <workspace>/state/costs.jsonl, avec 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 persiste après les redémarrages.

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 %.

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

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.

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. Émis une fois par paire (provider_type, model) lorsque resolve_rates renvoie (0.0, 0.0). Soit le tarif n’est pas configuré pour ce modèle, soit l’amont a renvoyé un id de modèle différent de celui figurant dans la grille tarifaire (certains fournisseurs renvoient des id versionnés comme claude-3-5-sonnet-20241022 même lorsque vous avez configuré claude-3-5-sonnet). Ajoutez l’id exact mentionné par l’avertissement, ou définissez l’id non versionné et appuyez-vous sur le mécanisme de correspondance par suffixe de resolve_rates.