Skip to content

fix(tui): DeepSeek V4 tiered peak/off-peak pricing resolved per turn - #5470

Merged
Hmbown merged 2 commits into
mainfrom
codex/v099-deepseek-tiered-pricing
Aug 17, 2026
Merged

fix(tui): DeepSeek V4 tiered peak/off-peak pricing resolved per turn#5470
Hmbown merged 2 commits into
mainfrom
codex/v099-deepseek-tiered-pricing

Conversation

@Hmbown

@Hmbown Hmbown commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Summary

First-party DeepSeek V4 pricing is tiered by UTC hour, but crates/tui/src/pricing.rs still carried a single flat rate per model (0.003625 / 0.435 / 0.87 for V4-Pro, 0.0028 / 0.14 / 0.28 for V4-Flash). This PR replaces those rows with peak/off-peak tiers resolved from each turn's recorded time, in both USD and CNY.

Published rates (per 1M tokens: cache-hit / cache-miss / output)

Verified live on https://api-docs.deepseek.com/quick_start/pricing and the /zh-cn page on 2026-08-17. Off-peak is exactly half of peak.

Model Currency Off-peak Peak
deepseek-v4-flash USD 0.007 / 0.22 / 0.66 0.014 / 0.44 / 1.32
deepseek-v4-flash CNY 0.05 / 1.5 / 4.5 0.10 / 3.0 / 9.0
deepseek-v4-pro USD 0.022 / 0.66 / 1.98 0.044 / 1.32 / 3.96
deepseek-v4-pro CNY 0.15 / 4.5 / 13.5 0.30 / 9.0 / 27.0

Hour rule: peak windows are 01:00–04:00 and 06:00–10:00 UTC, half-open — a turn is peak iff its recorded UTC hour is in {1,2,3,6,7,8,9}; every other hour is off-peak.

What changed

  • deepseek_peak_hour(hour_utc) / deepseek_is_peak(now) helpers (chrono::Timelike::hour() on the recorded DateTime<Utc>).
  • deepseek_v4_pro_pricing(now) and deepseek_v4_flash_pricing(now) now take the recorded time and return the tier's USD and CNY rows; cache-write policy stays DocumentedAsInputRate(DEEPSEEK_CACHE_WRITE_IS_FREE).
  • pricing_for_model_at passes now through, so the route audit (audit_turn_cost_for_provider_on_endpoint_atprovider_owned_hand_pricing_at) prices each turn at its own recorded tier, mirroring the claude_sonnet_5_pricing recorded-time precedent.
  • Hosted Fireworks / OpenCode Zen DeepSeek rows are provider-owned flat rates and are unchanged.
  • The bundled crates/config/assets/models_dev.bundled.json already omits DeepSeek-native pricing (its _meta.honesty note says a flat catalog row would be misleading), so no catalog row needed updating; a flat cost row cannot express the tiers, which is why the time-aware table lives in pricing.rs.

Tests

Removed (pinned the stale flat rates):

  • v4_pro_uses_limited_time_discount_before_expiry
  • v4_pro_keeps_adjusted_rates_after_discount_window
  • v4_pro_discount_still_applies_just_before_old_may5_expiry
  • v4_flash_keeps_current_published_rates

Added:

  • deepseek_peak_window_is_half_open_on_utc_hours — all 24 hours against the {1,2,3,6,7,8,9} set
  • deepseek_v4_tiers_flip_at_each_published_utc_boundary — 00:59/01:00, 03:59/04:00, 05:59/06:00, 09:59/10:00 UTC, both models, exact USD + CNY values
  • deepseek_v4_pro_off_peak_and_peak_rates_match_published_table (also asserts the retired 0.003625/0.435/0.87 rates never resurface)
  • deepseek_v4_flash_off_peak_and_peak_rates_match_published_table (same for 0.0028/0.14/0.28)
  • deepseek_v4_off_peak_is_exactly_half_of_peak
  • deepseek_audit_uses_recorded_time_tier_not_now — route audit on Deepseek / DeepseekCN / DeepseekAnthropic prices from the RECORDED time, USD and CNY

Updated to pin a deterministic off-peak/peak time instead of Utc::now():

  • unpublished_cache_write_fails_closed_but_documented_same_rate_prices
  • cost_estimate_calculates_usd_and_cny
  • hosted_flash_and_pro_routes_price_from_bundled_docs_rates

No-Issue: #5241 follow-up; pricing verified against the live published page 2026-08-17

DeepSeek's published V4 pricing (api-docs.deepseek.com/quick_start/pricing,
verified 2026-08-17) is tiered: peak hours are 01:00-04:00 and 06:00-10:00
UTC (half-open), off-peak is exactly half of peak. Replace the flat
single-tier V4-Pro/V4-Flash rows with tier-aware rows resolved from each
turn's recorded time (same contract as claude_sonnet_5_pricing), in both
USD and CNY, and replace the stale flat-rate tests with boundary tests.

Signed-off-by: Hunter Bown <hmbown@gmail.com>
@cursor

cursor Bot commented Aug 17, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@claude

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 3m 7s —— View job


Code review: DeepSeek V4 tiered peak/off-peak pricing

  • Gather context (PR body, diff, custom review instructions)
  • Review crates/tui/src/pricing.rs diff for correctness, provider/route safety, reuse, tests, security
  • Post final review

Summary: Solid change. The peak-window helper, tier tables, and time-threading through pricing_for_model_atprovider_owned_hand_pricing_ataudit_turn_cost_for_provider_at are correct and well tested (half-open boundary tests at all four published cutoffs, off-peak-is-exactly-half assertion, recorded-time-not-wall-clock regression test). No provider/route-safety issue — this PR doesn't infer provider/model from prefixes or prompt text, it only changes the rate table resolved for an already-selected DeepSeek route.

Findings

  1. Stale doc comments after this change (nit, not a functional bug): three comments still describe the retired flat first-party rate $0.0028 as if it's current:

    • crates/tui/src/pricing.rs:1696 — "Hosted Fireworks / OpenCode Zen rates are provider-owned docs rows, not first-party DeepSeek's $0.0028 cache-hit card and not Models.dev."
    • crates/tui/src/pricing.rs:1785 — "Do not inherit first-party DeepSeek's $0.0028 cache-hit card — Fireworks publishes $0.028."
    • crates/tui/src/pricing.rs:1809 — "Flash cache-read is $0.028, not first-party DeepSeek's $0.0028."

    First-party DeepSeek V4-Flash cache-hit is now tiered (0.007 off-peak / 0.014 peak), not a flat 0.0028, so these comments are now inaccurate about what value the hosted routes must not inherit. The updated test (hosted_flash_and_pro_routes_price_from_bundled_docs_rates) was correctly updated to check both [0.007, 0.014], but the surrounding comments weren't. Worth a follow-up sentence like "not first-party DeepSeek's tiered 0.007/0.014 cache-hit rate."

    Fix this →

Good choices worth calling out

  • deepseek_peak_hour/deepseek_is_peak cleanly separated from the pricing tables, and the boundary test (deepseek_v4_tiers_flip_at_each_published_utc_boundary) exercises all four published cutoffs on both models in both currencies.
  • deepseek_audit_uses_recorded_time_tier_not_now correctly asserts pricing is pinned to the recorded turn time rather than wall-clock, mirroring the claude_sonnet_5_pricing precedent noted in AGENTS.md.
  • Regression guards (assert_ne! against the old 0.003625/0.435/0.87 and 0.0028/0.14/0.28 rates) prevent silent reversion.
  • Removing the flat-discount tests (v4_pro_uses_limited_time_discount_before_expiry, etc.) is correct — they pinned rates this PR intentionally retires, not incidental deletions.
    · Branch: codex/v099-deepseek-tiered-pricing

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a57b06ef43

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/tui/src/pricing.rs
}

fn deepseek_v4_pro_pricing(now: DateTime<Utc>) -> ModelPricing {
let peak = deepseek_is_peak(now);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail closed when a tiered DeepSeek turn lacks its timestamp

When an offline or legacy RecordedTurn has no created_at, this tier selection is not necessarily based on the turn's recorded time: scorecard.rs lines 344-352 only marks deepseek-chat and deepseek-reasoner as requiring a timestamp, so explicit deepseek-v4-pro/deepseek-v4-flash IDs and compact aliases fall through to Utc::now(). Consequently, the same saved turn can be charged at the peak or off-peak rate depending on when the scorecard is regenerated, instead of failing closed as the existing time-window contract requires.

Useful? React with 👍 / 👎.

Comment thread crates/tui/src/pricing.rs
Comment on lines +830 to +831
fn deepseek_v4_pro_pricing(now: DateTime<Utc>) -> ModelPricing {
let peak = deepseek_is_peak(now);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve pre-tier rates for earlier recorded turns

For any DeepSeek V4 turn recorded before this peak/off-peak schedule took effect, this unconditional hour-only selection retroactively applies the new August rates. The removed tests and prior rows establish that earlier V4-Pro and V4-Flash calls used the flat 0.003625/0.435/0.87 and 0.0028/0.14/0.28 USD schedules, but a persisted turn from that period is now recalculated at one of the new tiers merely according to its hour, corrupting historical scorecard totals. Retain the prior rows behind the schedule's effective-date cutoff before applying the hourly tier.

Useful? React with 👍 / 👎.

…me-tiered rates)

Every direct DeepSeek first-party rate is time-windowed now (V4 flash/pro
peak/off-peak tiers; the retired chat/reasoner aliases price through them),
so an undated turn cannot be resolved to one price — treat it like an
undated claude-sonnet-5 turn and fail closed with `missing_recorded_time`
instead of pricing it at whatever tier `now` happens to be. Windows CI
caught exactly that: `direct_deepseek_compatibility_aliases_use_the_flash_route`
compared a 15:58Z (off-peak) alias against an undated canonical turn priced
at 07:14Z (peak). Tests pin their recorded times; a new test asserts the
fail-closed behavior.
@Hmbown

Hmbown commented Aug 17, 2026

Copy link
Copy Markdown
Owner Author

Follow-up commit 5042de6: Windows CI caught scorecard::tests::direct_deepseek_compatibility_aliases_use_the_flash_route comparing a dated (15:58Z, off-peak) alias turn against an undated canonical turn priced at Utc::now() (07:14Z, peak). Since every direct DeepSeek first-party rate is now time-windowed, the scorecard treats an undated direct-DeepSeek turn like an undated claude-sonnet-5 turn: fail closed with missing_recorded_time rather than guess a tier. Tests pin their recorded times; new test undated_direct_deepseek_v4_turns_fail_closed_on_the_time_window. Behavior change worth a CHANGELOG line (release branch carries it).

@claude

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 2m 39s —— View job


Code review: DeepSeek V4 tiered peak/off-peak pricing (update)

  • Gather context (PR diff, custom review instructions, prior Codex comments)
  • Review crates/tui/src/pricing.rs and crates/tui/src/scorecard.rs diff for correctness, provider/route safety, reuse, tests, security
  • Verify the two Codex P1/P2 findings against actual code
  • Post final review

Scope: diffed d03260e..HEAD (the PR's two commits, a57b06e + 5042de6) against just the two changed files — matches the PR's reported +324/-87 across pricing.rs and scorecard.rs. Could not run cargo test/cargo fmt in this sandbox (Bash execution was denied); review is source-level only.

Verifying the two Codex findings from this thread:

  1. P2 — "fail closed on undated tiered DeepSeek turns": fixed by 5042de6. scorecard.rs:338-350 now sets needs_recorded_time = direct_deepseek || …, i.e. any first-party DeepSeek provider (Deepseek/DeepseekCN/DeepseekAnthropic) requires a recorded time, not just the retired deepseek-chat/deepseek-reasoner ids. So deepseek-v4-pro/deepseek-v4-flash (and compact aliases) now correctly fail closed when created_at is None, instead of silently pricing at Utc::now()'s tier. Covered by the new undated_direct_deepseek_v4_turns_fail_closed_on_the_time_window test. Good fix.

  2. P1 — "preserve pre-tier rates for earlier recorded turns": still open, and I agree with it. deepseek_v4_pro_pricing(now)/deepseek_v4_flash_pricing(now) (pricing.rs:826-869) select a rate purely from now.hour() via deepseek_is_peak, with no calendar/effective-date gate. Since provider_scoped_cost/audit_turn_cost_for_provider_at resolve cost from each turn's own recorded timestamp (not wall-clock), any DeepSeek V4 turn recorded before this tiered schedule existed will be recomputed under the new table whenever the scorecard is regenerated — e.g. a V4-Pro turn from several months ago would jump from the old flat 0.003625/0.435/0.87 to whatever the new hour happens to map to (up to 0.044/1.32/3.96 at peak, a ~12x change), silently rewriting historical totals. This file already has the right pattern to copy: claude_sonnet_5_pricing (pricing.rs:799-809) gates on a calendar cutoff (intro_ends) before switching rate regimes, falling back to the prior rate for turns recorded before the change. The DeepSeek functions should do the same — gate the new tiers behind the schedule's effective date and keep returning the old flat rate for turns recorded before it — rather than applying the hour-only tier unconditionally across all of history.

    Fix this →

Carried over from my earlier pass on this branch, still unaddressed (nit): the doc comments at pricing.rs:1696, 1785, and 1809 still describe first-party DeepSeek's cache-hit rate as a flat "$0.0028 cache-hit card," which is now stale — it's tiered (0.007 off-peak / 0.014 peak). Worth a follow-up wording pass so the "hosted routes must not inherit first-party DeepSeek's rate" comments name the current tiered values.

Other observations:

  • No provider/route-safety issue — this PR only changes the rate table resolved for an already-selected DeepSeek route; it doesn't infer provider/model from prefixes or prompt text.
  • deepseek_peak_hour/deepseek_is_peak and the half-open boundary math ({1,2,3,6,7,8,9}) check out against the published windows, and are well covered (deepseek_peak_window_is_half_open_on_utc_hours, deepseek_v4_tiers_flip_at_each_published_utc_boundary, deepseek_v4_off_peak_is_exactly_half_of_peak).
  • Regression guards (assert_ne! against the retired 0.003625/0.435/0.87 and 0.0028/0.14/0.28 rates) are a good touch to prevent silent reversion.
    · Branch: codex/v099-deepseek-tiered-pricing

@Hmbown
Hmbown merged commit fb3b840 into main Aug 17, 2026
25 checks passed
@Hmbown
Hmbown deleted the codex/v099-deepseek-tiered-pricing branch August 17, 2026 08:43
birhantprkc pushed a commit to birhantprkc/CodeWhale that referenced this pull request Aug 17, 2026
…026-08-17

Catalog currency sweep against official provider pages (all re-verified
2026-08-17):

- OpenAI gpt-5.6-terra 0.20/2.00/12.00 and gpt-5.6-luna 0.02/0.20/1.20
  (cache-read/input/output per 1M) replace the retired launch cards.
- Anthropic claude-sonnet-5 stays 2.00/10.00 (cache 0.20 / write 2.50):
  the scheduled 2026-09-01 increase to 3/15 was cancelled upstream, so
  the time-windowed flip is gone; claude-opus-5 (GA 2026-07-24,
  5.00/25.00, 0.50/6.25, 1M/128K) is added to models.rs, the registry,
  the pricing table, both bundled catalogs and the agent model list.
- Moonshot kimi-k3 0.30/3.00/15.00 and kimi-k2.7-code-highspeed
  0.38/1.90/8.00 get first-party rows (membership `k3` stays unpriced).
- MiniMax-M2.7-highspeed 0.06/0.60/2.40 (write 0.375).
- xAI grok-4.5 (0.30/2.00/6.00) and grok-4.3 (0.20/1.25/2.50) join
  grok-4.6 on the usage-aware 2x-above-200K tier; bundled xai rows carry
  limits only (same honesty rule as grok-4.6).
- Mistral medium/large/small/code first-party rows (cache-write
  unpublished, kept unpriced).
- Google Gemini API context/max-output (1,048,576 / 65,536) for the
  seeded ids plus gemini-3.7-flash; OpenRouter
  dots-studio/dots-3-note-preview:free limits (512K) as the only hosted
  RedNote/Dots route.

Tests pin every number (DeepSeek V4 tier-test style, Hmbown#5470); the Sonnet 5
dispatch-boundary aggregation test now rides the DeepSeek off-peak/peak
boundary since Sonnet 5 no longer has one.

No-Issue: model catalog currency sweep for v0.9.9/v0.9.10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant