Skip to content

feat(code): refresh price catalog hourly in the background - #5264

Merged
Mason Daugherty (mdrxy) merged 5 commits into
mainfrom
mdrxy/code/automatic-pricing-updates
Aug 3, 2026
Merged

feat(code): refresh price catalog hourly in the background#5264
Mason Daugherty (mdrxy) merged 5 commits into
mainfrom
mdrxy/code/automatic-pricing-updates

Conversation

@mdrxy

Copy link
Copy Markdown
Member

Cost estimates can now pick up new model prices without waiting for a dcode release. Once cost estimates are first used, dcode checks for updated pricing data in the background every hour.

To use only the pricing data bundled with dcode, set DEEPAGENTS_CODE_PRICES_AUTO_UPDATE=0 or [update].prices_auto_update = false in config.toml. Offline mode also disables these downloads.


New model prices can be unavailable until a dcode release updates its bundled pricing data. This change refreshes that data in the background after pricing is first needed, so it does not add work at startup.

If an update fails or appears incomplete, dcode keeps using the pricing data it already has.

Supersedes #5262 (same commits; branch renamed to match repo naming convention).

The bundled pricing catalog only refreshes when the genai-prices pin
is bumped, while upstream merges new model pricing continuously. Start
genai-prices' UpdatePrices daemon on the first successful pricing import
(never at CLI startup) so calc_price uses the fresher upstream catalog
within the hour. DEEPAGENTS_CODE_PRICES_AUTO_UPDATE opts out; a failed
updater start logs once and pricing falls back to the bundled data.
An explicitly empty DEEPAGENTS_CODE_PRICES_AUTO_UPDATE disables the
updater at runtime via is_env_truthy, but manifest resolution skipped
empty env values, so 'dcode config get update.prices_auto_update'
reported the default true while the updater was actually off.
Refuse an upstream catalog listing fewer providers than the bundled data.
genai-prices validates only that the payload is an array of well-formed
providers, so an empty or mid-publish data.json installs cleanly and makes
every calc_price raise LookupError -- which this module reports as "no
published rates for this model", leaving costs to stop accruing with no
visible cause. The guard raises rather than returning None, because
_update_prices installs whatever fetch returns and None would discard a
healthy earlier fetch.

Quiet the genai-prices logger. Its background thread logs a failed hourly
refresh at ERROR, and with no handler attached that clears
logging.lastResort's WARNING threshold and prints over the alternate-screen
TUI once an hour for any offline or proxied session. _start_price_updater
also claims the logger itself, for the server graph and embedded hosts that
never run _quiet_sdk_logging.

Honor DEEPAGENTS_CODE_OFFLINE alongside the dedicated opt-out, and record the
new unpinned egress as T11b in the threat model.

Serialize the start attempt under a lock. estimate_cost runs both on the event
loop and on the executor threads that price drained records, so an unguarded
check-then-set let two threads reach start(); the loser tripped genai-prices'
process-wide singleton guard and its RuntimeError was reported as a failed
start while the winner's updater ran fine. The warning now names the exception
type and message, which differ in what the user should do about them.

Make update.prices_auto_update settable in config.toml, and tie the manifest
default to the one _start_price_updater applies.

Tests stand UpdatePrices in with an autospec, so a renamed keyword in a patch
release fails the suite instead of silently killing the feature in production.
TestPriceUpdater moves out of the middle of TestEstimateCost, where it had
re-parented 21 pricing tests into itself.
The runtime gate for the background price updater read
DEEPAGENTS_CODE_PRICES_AUTO_UPDATE directly, so a user who set
[update].prices_auto_update = false in config.toml saw 'config get'
report false while the hourly network fetch still started. Route the
gate through the manifest resolver so env, TOML, and default resolve
with the same precedence the config surface reports.
@mdrxy Mason Daugherty (mdrxy) added feature New feature/enhancement or request for one internal User is a member of the `langchain-ai` GitHub organization dcode Related to `deepagents-code` labels Aug 3, 2026
@github-actions github-actions Bot added the size: L 500-999 LOC label Aug 3, 2026
@mdrxy
Mason Daugherty (mdrxy) merged commit 2454afa into main Aug 3, 2026
55 checks passed
@mdrxy
Mason Daugherty (mdrxy) deleted the mdrxy/code/automatic-pricing-updates branch August 3, 2026 20:53
Johannes du Plessis (johannes117) pushed a commit that referenced this pull request Aug 4, 2026
> [!CAUTION]
> Merging this PR will automatically publish to **PyPI** and create a
**GitHub release**.

For the full release process, see
[`.github/RELEASING.md`](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md).

---

_Release notes preview: keep this section in sync with the package
`CHANGELOG.md`. Publish reads the merged CHANGELOG via `release.yml`,
not this PR description — keep them aligned anyway so the PR stays an
accurate historical record for reviewers and anyone returning later._

---


##
[0.1.52](deepagents-code==0.1.51...deepagents-code==0.1.52)
(2026-08-04)

### Features

- Hooks v2 is now generally available, with support for loading hooks
from installed plugins.
([#5307](#5307),
[#5198](#5198))
- Auto approval classifier configuration now supports selecting the
classifier model and setting a review timeout.
([#5205](#5205),
[#5302](#5302))
- HITL rejection reasons are now framed for the model, and the approval
menu makes reject-with-feedback easier to discover.
([#5259](#5259),
[#5260](#5260))
- Added a tri-state `DEEPAGENTS_CODE_ONBOARDING` environment variable.
([#5301](#5301))
- The `/model` footer Ctrl+N hint now follows the current display mode.
([#5247](#5247))
- The price catalog now refreshes hourly in the background.
([#5264](#5264))
- Updated recommendations to include DeepSeek V4 Flash 0731.
([#5244](#5244))

### Bug Fixes

- Fixed several Hooks v2 lifecycle issues: session-end teardown is now
bounded, hooks refresh after cwd switches, malformed hook resumes are
handled, hook stops surface without agent errors, and unused
`SessionEndCause` members were removed.
([#5248](#5248),
[#5249](#5249),
[#5233](#5233),
[#5276](#5276),
[#5240](#5240))
- `PreCompact` now fires before auto-compaction.
([#5277](#5277))

_End release notes preview._

---

> [!NOTE]
> A **New Contributors** section is appended to the GitHub release notes
automatically at publish time (see [Release
Pipeline](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md#release-pipeline),
step 2).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: langchain-oss-automated-triage[bot] <248757908+langchain-oss-automated-triage[bot]@users.noreply.github.com>
Mason Daugherty (mdrxy) added a commit that referenced this pull request Aug 4, 2026
…isses (#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dcode Related to `deepagents-code` feature New feature/enhancement or request for one internal User is a member of the `langchain-ai` GitHub organization size: L 500-999 LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant