feat(code): refresh price catalog hourly in the background - #5264
Merged
Mason Daugherty (mdrxy) merged 5 commits intoAug 3, 2026
Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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=0or[update].prices_auto_update = falseinconfig.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).