Cost estimates now fall back to a local pricing catalog when genai-prices has no rates for a model. Two sources are consulted, in precedence order: a user-supplied `prices.json` in the user config directory (`~/.deepagents`) for models upstream lacks entirely, and a maintainer-curated `bundled_prices.json` shipped inside the package as a stopgap while an upstream addition is pending. The fallback only fires on a primary-catalog miss, so published upstream rates always win. --- `estimate_cost` previously returned `None` for any model the genai-prices catalog — bundled or hourly auto-updated (#5264) — did not cover, leaving those requests out of the session total with no recourse short of waiting on a genai-prices release. This adds an escape hatch without giving up upstream as the source of truth. Design points: - **Fallback-on-miss, upstream always wins.** Overrides are consulted only from the existing `except LookupError` path in `estimate_cost`; they are never installed via `set_custom_snapshot`. The hourly auto-updater wholesale-replaces the custom snapshot, so anything installed there would be clobbered every refresh — keeping overrides outside the snapshot mechanism is what makes the two features compose. A successful primary lookup never touches the overrides, which means a built-in entry automatically becomes dead weight the day a released genai-prices ships the model, with no migration needed. <details> <summary>Worked examples</summary> > **Scenario 1 — why overrides can't live in the snapshot.** You start `dcode` after upgrading to a release whose `bundled_prices.json` ships a stopgap for `new-model-v2`. The updater fetches the upstream catalog in the background and installs it via `set_custom_snapshot`, which replaces the snapshot as a whole rather than merging into it. If the stopgap had been registered into the snapshot at startup, that first refresh would have deleted it — from then on `new-model-v2` would be priced by neither catalog and silently drop out of the session total, exactly the gap the stopgap shipped to close, with no warning and no way for the user to restore it. Because the stopgap lives in the separate override catalog instead, the refresh doesn't touch it: the primary lookup still raises `LookupError` for `new-model-v2`, and the fallback prices it as before. > > **Scenario 2 — why a stale entry needs no migration.** A week later the hourly fetch picks up an upstream catalog that now prices `new-model-v2`. From the next request on, the primary lookup succeeds, so the `except LookupError` path — the only place overrides are read — is never reached for that model. The stopgap entry is now dead weight: it costs nothing at runtime, and its removal is a housekeeping PR, not a correctness fix. There is no data to migrate, no flag to flip, and no version check, because "upstream now covers it" and "the override is unreachable" are the same event. </details> - **Same schema as upstream.** Both files use the raw provider-array schema of genai-prices' `prices/new_data/v2/data.json` - **User file wins on conflict.** On a conflicting `(provider id, model id)` pair, the user's `prices.json` entry replaces the built-in one (per-model). The user path resolves through the existing `model_config.DEFAULT_CONFIG_DIR` constant - **Never breaks a model turn.** A missing user file is the normal case and stays silent. Malformed JSON, a non-array payload, an unreadable file, or a schema validation failure each log a warning once and disable only that source; `pricing_data_available()` and the contract-broken guard are untouched, so a bad override file never masquerades as a broken pricing install. Successful override pricing logs at DEBUG — that's the signal to pursue the upstream addition. - **Built-in ships empty.** `bundled_prices.json` is a structurally valid empty provider array until the first stopgap is needed. The maintenance policy lives in the sibling `bundled_prices.README.md` (JSON has no comments): every entry must link the upstream genai-prices PR/issue, and must be removed once a released genai-prices covers it.
4.6 KiB
Cost estimates and local pricing overrides
/cost reports an estimate for the current thread. Rates come from
genai-prices, which ships an offline
catalog and refreshes it hourly in the background (opt out with
DEEPAGENTS_CODE_PRICES_AUTO_UPDATE=0, or [update].prices_auto_update = false
in config.toml).
Estimates are display-only. Nothing in dcode caps spend or gates execution on them, so a wrong rate costs you an inaccurate number and nothing else.
When a model has no rates
A model the catalog does not cover is left out of the total. /cost distinguishes
that from a genuinely free request: it reports how many of the recorded requests
are included in the figure.
That happens with a newly released model, a self-hosted or proxied endpoint, or a provider name that does not line up with what genai-prices calls it. If you would rather see an estimate than a gap, write your own rates to:
~/.deepagents/prices.json
The file is read once, on the first request that needs it. Edits take effect on the next dcode start, not mid-session.
File format
prices.json uses the same provider-array schema as genai-prices' own
prices/new_data/v2/data.json, so an entry can be contributed upstream as-is.
It is a JSON array of providers:
[
{
"id": "my-proxy",
"name": "My proxy",
"api_pattern": "gateway\\.example\\.internal",
"models": [
{
"id": "house-model-v2",
"match": { "equals": "house-model-v2" },
"prices": { "input_mtok": 2.5, "output_mtok": 10.0 }
}
]
}
]
Required, and easy to omit:
| Field | Where | Notes |
|---|---|---|
id |
provider | Must be the provider id dcode reports for the request — see below. |
name |
provider | Any display string. |
api_pattern |
provider | Regex against the API URL. Unused by dcode, but the schema requires it. |
id |
model | Any identifier; used in log messages. |
match |
model | How to recognize the model: {"equals": ...}, {"starts_with": ...}, {"contains": ...}, or {"regex": ...}. |
prices |
model | At least one rate. |
Rates are per million tokens: input_mtok, output_mtok, and optionally
cache_read_mtok, cache_write_mtok, output_reasoning_mtok,
input_audio_mtok. Only list a bucket you have a real rate for — tokens in an
omitted bucket are billed at the ordinary input or output rate rather than being
dropped.
Getting the provider id right
This is where a hand-written file usually goes wrong. dcode resolves the LangChain provider name through its own alias table before looking anything up, so the id it searches on is not always the string you would expect.
If no provider in your file claims that id, dcode falls back to searching every
provider in the file by model id alone. That keeps your entry reachable, but it
also means an entry can price a request that ran against a different provider —
llama-3.3-70b costs very different amounts on Bedrock, Together, and Groq. When
that happens you get a warning naming both providers; pin the entry by using the
id from the warning.
Diagnosing a file that is not working
Everything below is logged to the Debug Console (Ctrl+\), each message once per
session:
| What you see | What it means |
|---|---|
| Nothing at all, cost still missing | The file is not where dcode looks, or is empty. Both are silent by design. |
| "No pricing override matched model=… though … contributed N model entries" | The file loaded, but nothing matched. The provider_id in the message is the post-alias id your entry must use. |
| "Could not parse … as JSON" / "must be an array of providers" | Syntax or shape problem. The payload must be a bare array, not {"providers": [...]}. |
| "the entries failed validation against the genai-prices provider schema" | A required field is missing or mistyped. |
| "the request ran against provider …" | Priced by a sweep, possibly from the wrong provider's entry. See above. |
| "its entry matched but its rates were rejected" | A rate is unusable — negative, or large enough to overflow. |
A malformed prices.json never breaks a model turn and never disables ordinary
pricing: the bad source is dropped and everything else still prices.
Built-in stopgaps
dcode also ships a small maintainer-curated catalog for models upstream has not
priced yet. Your file wins over it on a conflicting (provider id, model id)
pair, and both are consulted only when genai-prices itself has no rates. See
deepagents_code/bundled_prices.README.md
for that policy.