Files
deepagents/libs/code/PRICING.md
T
Mason Daugherty 89bcaf2c31 feat(code): local pricing overrides as a fallback when genai-prices misses (#5304)
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.
2026-08-04 15:40:14 -04:00

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.