Pricing Auto-Refresh
Architecture
Section titled “Architecture”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.
Merge strategy
Section titled “Merge strategy”The refresh_pricing() function:
- Loads existing
pricing.yaml - Fetches all sources
- Never overwrites manual entries — detects them via
_meta.auto_models(a set of model IDs that were auto-fetched in a previous cycle) - Updates auto-fetched models when prices change
- 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.
Estimated-price models
Section titled “Estimated-price models”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.
Sentinel file
Section titled “Sentinel file”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.
Atomic write (issue #105/#106)
Section titled “Atomic write (issue #105/#106)”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.