Skip to content

Pricing Auto-Refresh

PricingSource is an ABC with a single fetch() -> dict[str, dict] method. Two concrete sources ship:

Source Mechanism Frequency
OpenRouterSource HTTP GET to openrouter.ai/api/v1/models (no auth) Every 24h
GoogleAISource Constant table in code (LAST_VERIFIED = "2026-06-05") Manual quarterly

GoogleAISource is a constant because Google has no structured pricing API. The LAST_VERIFIED date is an explicit reminder to bump it quarterly.

The refresh_pricing() function:

  1. Loads existing pricing.yaml
  2. Fetches all sources
  3. Never overwrites manual entries — detects them via _meta.auto_models (a set of model IDs that were auto-fetched in a previous cycle)
  4. Updates auto-fetched models when prices change
  5. Writes _meta.auto_models, _meta.estimated_price_models, _meta.last_refresh

Manual entry detection: if a model is in models: but NOT in _meta.auto_models, it’s considered manual — preserved and logged as an override warning if the remote price differs.

OpenRouter represents models without fixed pricing (auto-routing, experimental) with negative prices in their API. The plugin normalizes these to $0.00 and flags them with _estimated_price: true. The budget engine checks for these models via estimated_price_share() and degrades hard verdicts under warn_only.

The 24h refresh cadence is tracked via a sentinel file at ~/.hermes/telemetry/.pricing_refresh. Touch it to force a refresh on next plugin load. Delete it to trigger auto-refresh.

Under Hermes multiplex, one gateway process runs N plugin instances (one per served profile), all sharing the same pricing.yaml and all running their own auto-refresh cycle. pricing_refresh._atomic_write_text() writes to a temp file in the same directory, flushes + fsyncs, then commits via os.replace (POSIX-atomic rename) — a concurrent reader in another instance only ever sees the old content or the new content in full, never a truncated/partial file. _save_yaml() (the auto-refresh writer) builds on it and keeps its existing fail-open contract (logs and returns on error, never raises). pricing_drift.py’s --apply writer and setup_wizard.py’s first-run writer both route through the same primitive — the former still propagates a write failure to the CLI (unchanged), the latter still propagates to /setup’s caller (unchanged).

Symlinks are followed, not replaced. _atomic_write_text() resolves path with os.path.realpath first and uses the resolved target for the mode lookup, the temp file’s directory and the os.replace. Without that, a symlinked pricing.yaml (e.g. into a dotfiles repo) would be swapped for a regular file and the real target left untouched.

Permissions are preserved, not reset to mkstemp’s default. tempfile.mkstemp() creates its file 0600 — if that mode rode along into the replace it would silently tighten pricing.yaml from open(path, "w")’s usual 0o666 & ~umask (typically 0644), breaking read access for e.g. a dashboard running as another user. _atomic_write_text() chmods the temp file (os.fchmod on the open fd, done inside the with os.fdopen(fd, "w") block so the fd is guaranteed closed even if fchmod itself raises — doing it before the with would leak the fd on that path): to the existing target’s mode when one is already there, otherwise to 0o666 masked by the umask captured once at module import (_DEFAULT_FILE_MODE). That umask is read via _read_process_umask(), which parses the Umask: field from /proc/self/status (falling back to 0o022 when unavailable) rather than calling os.umask() — os.umask() has no read-only form, it always sets the process-wide umask while returning the old value, which would be unsafe to call at arbitrary import time in a long-running multithreaded gateway process (import can happen mid-uptime, racing another thread’s file creation).

hermes-telemetry is an independent community plugin for Hermes Agent. It is not affiliated with or endorsed by Nous Research.