diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff671fa2d..51f6f5268 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+- **DeepSeek-V4.1-Flash (`deepseek-flash`), and the DeepSeek line collapses
+ onto it.** 1M context, 384K max output, thinking on by default — and the
+ first DeepSeek model that accepts an image, folding in the retired
+ `deepseek-v4-flash-vision-exp`, so a screenshot no longer needs a
+ fusion model on this provider. It is now the `deepseek`
+ provider's default and its whole subagent tier table (opus/sonnet/haiku),
+ because DeepSeek says V4.1 Flash "has comprehensively surpassed V4 Pro in
+ performance, cost, speed, and total time" and is retiring Pro onto it.
+ `deepseek-v4-pro`, `deepseek-v4-flash`, `deepseek-chat` and
+ `deepseek-reasoner` all still resolve, so a pinned session keeps working.
+- **`/cost` follows DeepSeek's re-card and its V4 Pro retirement.** V4.1 Flash
+ is cheaper than the V4 flash line it replaces — **$0.15 / $0.60 per MTok
+ off-peak and $0.003 cache-hit**, against $0.22 / $0.66 / $0.007 — and the
+ retired flash ids are billed at that same card, since DeepSeek serves them
+ from V4.1 Flash. `deepseek-v4-pro` keeps its own card until 2026-09-14
+ 12:00 Beijing (04:00 UTC) and prices as Flash from that instant, on top of
+ the existing peak/off-peak schedule. Both axes read the request's
+ timestamp, so re-opening an August session still shows what it actually
+ cost rather than restating it 4.4× low.
+
- **Web: the reference's session stats strip.** One centred line under the
composer — `2 turns · 106 steps | LLM 6m28s · Tool call 23.7s | TTFT avg
1.3s · 258 tok/s | Cache hit 99% | Input 11.5M tok · Output 65.9K tok` —
diff --git a/README.md b/README.md
index a7d8a2a4b..37bd65ab5 100644
--- a/README.md
+++ b/README.md
@@ -64,14 +64,14 @@ prefix nearly free, `/eco` shrinks the fresh suffix every turn actually pays for
# Run long agentic coding sessions for *pennies*
-### Cache-hit input bills at **`$0.022` / 1M tokens** — about **450× cheaper** than Claude Fable 5 (`$10` / 1M).
+### Cache-hit input bills at **`$0.003` / 1M tokens** — about **3,300× cheaper** than Claude Fable 5 (`$10` / 1M).
ClawCodex keeps your request prefix **byte-stable**, so DeepSeek's prompt cache covers your whole
`system + tools + history` span across turns. **The longer you code, the more you save.**
-`deepseek-v4-pro` off-peak, checked 2026-08-25. DeepSeek doubles every rate during peak hours
-(01:00–04:00 and 06:00–10:00 UTC, Mon–Fri) — still ~227× cheaper there. `/cost` follows the
-schedule.
+`deepseek-flash` (DeepSeek-V4.1-Flash) off-peak, checked 2026-09-10. DeepSeek doubles every
+rate during peak hours (01:00–04:00 and 06:00–10:00 UTC, Mon–Fri) — still ~1,600× cheaper there.
+`/cost` follows the schedule.
@@ -151,7 +151,7 @@ The configuration file is saved at `~/.clawcodex/config.json`. Minimal example:
"deepseek": {
"api_key": "xxx-xxx",
"base_url": "https://api.deepseek.com",
- "default_model": "deepseek-v4-pro"
+ "default_model": "deepseek-flash"
}
},
"env": {
@@ -728,7 +728,7 @@ The configuration file is saved in `~/.clawcodex/config.json`. Example structure
"deepseek": {
"api_key": "your-api-key",
"base_url": "https://api.deepseek.com",
- "default_model": "deepseek-v4-pro"
+ "default_model": "deepseek-flash"
}
},
"session": {
diff --git a/docs/guide/SETUP_GUIDE.md b/docs/guide/SETUP_GUIDE.md
index 5b2cac0f1..b0ce2e05f 100644
--- a/docs/guide/SETUP_GUIDE.md
+++ b/docs/guide/SETUP_GUIDE.md
@@ -97,7 +97,9 @@ Create `~/.clawcodex/config.json` (only the providers you actually use are requi
| `zai` | `https://api.z.ai/api/coding/paas/v4` | `glm-5.2` (also `glm-5.1`) |
| `minimax` | `https://api.minimaxi.com/anthropic` | `MiniMax-M2.7` |
| `openrouter` | `https://openrouter.ai/api/v1` | `deepseek/deepseek-v4-pro` |
-| `deepseek` | `https://api.deepseek.com` | `deepseek-v4-pro` |
+| `deepseek` | `https://api.deepseek.com` | `deepseek-flash` |
+
+> **DeepSeek:** `deepseek-flash` is DeepSeek-V4.1-Flash — 1M context, 384K max output, thinking on by default, and the first DeepSeek model that accepts images. `deepseek-v4-pro` is being retired: the id still works, but from 2026-09-14 its requests run V4.1 Flash and bill at the Flash price. The older spellings (`deepseek-v4-flash`, `deepseek-chat`, `deepseek-reasoner`) still resolve.
> **Z.ai (GLM):** clawcodex uses Z.ai's OpenAI-compatible GLM Coding Plan at `https://api.z.ai/api/coding/paas/v4`, serving `GLM-5.1` (stable) and `GLM-5.2` (preview). The legacy provider name `glm` is still accepted as an alias for `zai`. Get a key at .
diff --git a/src/context_system/context_analyzer.py b/src/context_system/context_analyzer.py
index 117b32dc3..713d2b716 100644
--- a/src/context_system/context_analyzer.py
+++ b/src/context_system/context_analyzer.py
@@ -33,8 +33,13 @@
"glm-5.2": 1_000_000,
"glm-5.1": 202_752,
"glm-4": 128_000, # legacy GLM-4.x fallback
- # DeepSeek V4 ships a 1M context window, like glm-5.2 (legacy deepseek-chat/
+ # DeepSeek ships a 1M context window, like glm-5.2 (legacy deepseek-chat/
# -reasoner are intentionally NOT matched here — see src/models/configs.py).
+ # Two rows, not a bare "deepseek": this table matches by SUBSTRING, so the
+ # family name alone would promote the legacy ids the exclusion above is
+ # about. ``deepseek-flash`` is DeepSeek-V4.1-Flash, which does not contain
+ # the ``deepseek-v4`` substring.
+ "deepseek-flash": 1_000_000,
"deepseek-v4": 1_000_000,
# MiniMax model-specific windows must precede the family fallback.
"minimax-m3": 1_000_000,
diff --git a/src/models/configs.py b/src/models/configs.py
index 26f0fe4b1..063dc02bf 100644
--- a/src/models/configs.py
+++ b/src/models/configs.py
@@ -23,6 +23,15 @@ class ModelConfig:
cost_output_per_mtok: float = 15.0
cost_cache_create_per_mtok: float = 3.75
cost_cache_read_per_mtok: float = 0.30
+ #: Whether this row may be reached by ``get_model_config``'s prefix
+ #: fallback (which claims ``key.rsplit("-", 1)[0]``). Set ``False`` on a
+ #: row whose id has too few segments for that base to be specific — e.g.
+ #: ``deepseek-flash`` claims bare ``deepseek`` and would otherwise swallow
+ #: ``deepseek-chat``, ``deepseek-reasoner`` and OpenRouter's
+ #: ``deepseek/…`` ids. Exact-match lookup ignores this flag, so an
+ #: opted-out row still describes its own id fully; it just stops
+ #: describing everyone else's.
+ claims_prefix: bool = True
MODEL_CONFIGS: dict[str, ModelConfig] = {
@@ -252,14 +261,16 @@ class ModelConfig:
cost_cache_read_per_mtok=0.03,
),
- # DeepSeek V4 series (OpenAI-compatible; api.deepseek.com). Registered so
+ # DeepSeek (OpenAI-compatible; api.deepseek.com). Registered so
# context-window-aware logic (compaction triggers, token warnings) uses
# DeepSeek's real ~1M window instead of the 200K default. Keys are the
# bare model ids used ONLY by the ``deepseek`` provider; OpenRouter's
# ``deepseek/…`` ids do not prefix-match ``deepseek-v4``, so OpenRouter is
# intentionally unaffected. Legacy ``deepseek-chat`` / ``deepseek-reasoner``
# are deliberately NOT registered: their prefix-match base would be the
- # broad ``deepseek`` and could capture other ids.
+ # broad ``deepseek`` and could capture other ids — which is exactly why the
+ # ``deepseek-flash`` row below carries ``claims_prefix=False`` rather than
+ # being left out.
#
# NOTE: ``get_model_config``'s prefix fallback bases these on
# ``deepseek-v4`` and ``pro`` precedes ``flash``, so a FUTURE
@@ -288,14 +299,36 @@ class ModelConfig:
# was briefly suspected of truncating long ``effort=max`` responses on
# terminal-bench 2.1, and it cannot, because it never reaches the wire.
#
- # ``supports_vision=False``: the DeepSeek API rejects any non-text
- # content block outright —
+ # ``supports_vision=False`` on the V4 rows: the DeepSeek API rejected any
+ # non-text content block outright —
# 400 unknown variant `image_url`, expected `text`
# — so a pasted screenshot, an ``@image.png`` mention, or a ``Read`` of
- # an image kills the turn. Probed against api.deepseek.com 2026-07-30.
+ # an image killed the turn. Probed against api.deepseek.com 2026-07-30.
# A fusion model (``/fusion``, ``providers/fusion_models.py``) is the
- # way to use images with these: it borrows vision from a second model
+ # way to use images with those: it borrows vision from a second model
# and hands the base model a text description.
+ #
+ # ``deepseek-flash`` (DeepSeek-V4.1-Flash) is the exception and the reason
+ # to reach for the canonical id: the vendor's capability table marks it
+ # Vision ✓, folding in the separate ``deepseek-v4-flash-vision-exp`` line
+ # it retired. The claim is made for THAT id only — the retired flash ids
+ # are served by the same model, but the table says nothing about them, so
+ # they keep the conservative ``False`` and a user hitting it is told to
+ # switch rather than handed a 400 mid-turn.
+ "deepseek-flash": ModelConfig(
+ model_id="deepseek-flash",
+ display_name="DeepSeek V4.1 Flash",
+ context_window=1_000_000,
+ max_output_tokens=384_000,
+ supports_cache=True,
+ supports_vision=True,
+ # Bare ``deepseek`` would be this row's claimed prefix. See the field's
+ # docstring: without the opt-out it captures ``deepseek-chat``,
+ # ``deepseek-reasoner`` and every ``deepseek/…`` OpenRouter id, the
+ # last of which ``tests/test_deepseek_prefix_cache.py`` pins at the
+ # 200K default.
+ claims_prefix=False,
+ ),
"deepseek-v4-pro": ModelConfig(
model_id="deepseek-v4-pro",
display_name="DeepSeek V4 Pro",
@@ -813,7 +846,9 @@ def get_model_config(model_id: str) -> ModelConfig | None:
"""Get config for a model, or None if unknown.
Exact match, then a prefix fallback for date-variant ids (a row's claimed
- prefix is its key minus the last ``-``-segment).
+ prefix is its key minus the last ``-``-segment). A row with
+ ``claims_prefix=False`` is skipped by that fallback and reachable only by
+ its exact id.
NOT attempted: stripping a leading ``/`` segment so OpenRouter ids
resolve to their bare row. ``get_pricing`` (services/pricing.py) does
@@ -833,6 +868,8 @@ def get_model_config(model_id: str) -> ModelConfig | None:
return MODEL_CONFIGS[model_id]
# Try prefix match (for date-variant models)
for key, config in MODEL_CONFIGS.items():
+ if not config.claims_prefix:
+ continue
base = key.rsplit("-", 1)[0]
if model_id.startswith(base):
return config
diff --git a/src/providers/__init__.py b/src/providers/__init__.py
index 4ad0f4597..71385aced 100644
--- a/src/providers/__init__.py
+++ b/src/providers/__init__.py
@@ -152,22 +152,29 @@ class ProviderInfo(_ProviderInfoOptional):
"deepseek": {
"label": "DeepSeek",
"default_base_url": "https://api.deepseek.com",
- "default_model": "deepseek-v4-pro",
- # Subagent defaults: v4-flash is DeepSeek's fast/cheap line, the
- # equivalent of the sonnet/haiku work tiers; v4-pro (the session
- # default above) stays the opus-tier target. Catalog verified live
- # 2026-08-12 (GET /models returns exactly v4-pro and v4-flash).
- "subagent_model": "deepseek-v4-flash",
+ "default_model": "deepseek-flash",
+ # Every tier is deepseek-flash (DeepSeek-V4.1-Flash), and that is the
+ # vendor's own arrangement rather than a simplification: DeepSeek says
+ # V4.1 Flash "has comprehensively surpassed V4 Pro in performance,
+ # cost, speed, and total time", and from 2026-09-14 routes every
+ # deepseek-v4-pro request to it, billed at the Flash price. So an
+ # opus-tier row naming v4-pro buys nothing after that date and costs
+ # up to 4.4x more before it. v4-pro stays listed below — it is still
+ # accepted, and a user who pins it keeps working — it just is not what
+ # anything defaults to.
+ "subagent_model": "deepseek-flash",
"subagent_tier_models": {
- "opus": "deepseek-v4-pro",
- "sonnet": "deepseek-v4-flash",
- "haiku": "deepseek-v4-flash",
+ "opus": "deepseek-flash",
+ "sonnet": "deepseek-flash",
+ "haiku": "deepseek-flash",
},
"available_models": [
- # V4 series (current)
+ # Current line
+ "deepseek-flash",
+ # Retiring: routed to V4.1 Flash from 2026-09-14 (still accepted)
"deepseek-v4-pro",
+ # Legacy aliases (retired models; served by V4.1 Flash)
"deepseek-v4-flash",
- # Legacy aliases (being deprecated; map to v4-flash modes)
"deepseek-chat",
"deepseek-reasoner",
],
diff --git a/src/providers/deepseek_provider.py b/src/providers/deepseek_provider.py
index 54abe5baf..61a810fbd 100644
--- a/src/providers/deepseek_provider.py
+++ b/src/providers/deepseek_provider.py
@@ -1,10 +1,13 @@
"""DeepSeek provider implementation.
DeepSeek exposes an OpenAI-compatible API at https://api.deepseek.com.
-Current production models are ``deepseek-v4-pro`` and ``deepseek-v4-flash``;
-the legacy aliases ``deepseek-chat`` / ``deepseek-reasoner`` are being
-deprecated and resolve to the non-thinking / thinking modes of
-``deepseek-v4-flash`` respectively.
+The current line is ``deepseek-flash`` (DeepSeek-V4.1-Flash): 1M context,
+384K max output, thinking on by default, and — unlike every DeepSeek model
+before it — image input. ``deepseek-v4-pro`` is being retired; from
+2026-09-14 the id still resolves but its requests run V4.1 Flash at the Flash
+price. ``deepseek-v4-flash``, ``deepseek-v4-flash-vision-exp``,
+``deepseek-chat`` and ``deepseek-reasoner`` are legacy spellings the API still
+accepts and serves from V4.1 Flash.
"""
from __future__ import annotations
@@ -36,8 +39,8 @@ class DeepSeekProvider(OpenAICompatibleProvider):
#: (api-docs.deepseek.com/guides/thinking_mode). Thinking is ON by
#: default at ``high``.
#:
- #: The API does not VALIDATE this field — probed 2026-08-03 against
- #: ``deepseek-v4-flash``, every one of ``low / medium / high / xhigh /
+ #: The API does not VALIDATE this field — probed 2026-08-03 against the
+ #: then-current flash model, every one of ``low / medium / high / xhigh /
#: max / minimal`` returned 200, and so did a value the docs never list.
#: So an unsupported level is not an error, it is silently discarded and
#: the default (``high``) applies. Without the mapping below, ``xhigh``
@@ -59,12 +62,12 @@ def __init__(
Args:
api_key: DeepSeek API key (sk-...)
base_url: Base URL (optional, defaults to https://api.deepseek.com)
- model: Default model (default: deepseek-v4-pro)
+ model: Default model (default: deepseek-flash)
"""
super().__init__(
api_key,
base_url or self.DEFAULT_BASE_URL,
- model or "deepseek-v4-pro",
+ model or "deepseek-flash",
)
def _create_client(self) -> Any:
@@ -168,16 +171,22 @@ def _field(name: str) -> int:
return result
def get_available_models(self) -> list[str]:
- """Return DeepSeek's current production models.
-
- ``deepseek-chat`` and ``deepseek-reasoner`` are kept for backward
- compatibility but DeepSeek has announced they will be deprecated.
+ """Return the DeepSeek model ids the API accepts, current first.
+
+ Only ``deepseek-flash`` names a live model. The rest are ids DeepSeek
+ still routes: ``deepseek-v4-pro`` is being retired onto V4.1 Flash,
+ and the remaining spellings name models that are already gone. They
+ stay listed because a session pinned to one keeps working and the
+ subagent resolver degrades a model missing from this list to inherit —
+ dropping them would silently re-point existing sessions.
"""
return [
- # V4 series (current)
+ # Current line
+ "deepseek-flash",
+ # Retiring: routed to V4.1 Flash from 2026-09-14 (still accepted)
"deepseek-v4-pro",
+ # Legacy spellings (retired models; served by V4.1 Flash)
"deepseek-v4-flash",
- # Legacy aliases (being deprecated; map to v4-flash modes)
"deepseek-chat",
"deepseek-reasoner",
]
diff --git a/src/services/pricing.py b/src/services/pricing.py
index 55fc2d7be..8aa4bd4fb 100644
--- a/src/services/pricing.py
+++ b/src/services/pricing.py
@@ -5,11 +5,12 @@
``add_to_total_cost_state`` and friends); this module just computes the
dollar cost of a usage record.
-One exception to "pure", added for DeepSeek's peak/off-peak card: when a
-caller omits ``request_time``, ``get_pricing`` reads the wall clock to decide
-which side of that schedule a request falls on. Every caller that knows the
-real request time should pass it; "now" is only correct because the live path
-prices a response the moment it arrives.
+One exception to "pure", added for DeepSeek's peak/off-peak card and reused
+for its V4 Pro retirement: when a caller omits ``request_time``,
+``get_pricing`` reads the wall clock to decide which side of those schedules a
+request falls on. Every caller that knows the real request time should pass
+it; "now" is only correct because the live path prices a response the moment
+it arrives.
Pricing mirrors ``typescript/src/utils/modelCost.ts``: published Anthropic
list prices per million tokens for first-party direct calls. Proxies
@@ -83,7 +84,7 @@
"cache_creation": 0.30 / 1_000_000,
"cache_read": 0.03 / 1_000_000,
}
-# DeepSeek V4 (USD per million tokens) — checked 2026-08-25 against
+# DeepSeek (USD per million tokens) — checked 2026-09-10 against
# https://api-docs.deepseek.com/quick_start/pricing/
#
# DeepSeek's automatic prefix cache bills cache HITS at the low ``cache_read``
@@ -111,20 +112,25 @@
# likely to move again, and a promo card that rots into silent
# under-reporting is the same failure the Luna row below was written about.
#
-# Not registered: ``deepseek-v4-flash-vision-exp``, which shares the flash
-# card on the vendor's page but has no row in ``models/configs.py`` and is
-# unreachable through the provider (``supports_vision=False``).
+# RE-CARDED 2026-09-10: DeepSeek-V4.1-Flash (``deepseek-flash``) replaced the
+# V4 flash line at a CHEAPER card — 0.15/0.6/0.003 off-peak against the
+# 0.22/0.66/0.007 below it — and the retired ``deepseek-v4-flash`` /
+# ``deepseek-v4-flash-vision-exp`` ids are served by V4.1-Flash and billed at
+# its price, so they share this card rather than keeping the V4 one. The old
+# flash rates are GONE from the vendor page, not merely superseded for new
+# ids; leaving them on the legacy ids would over-report every legacy-id
+# session by ~1.5x on input and 2.3x on cache read.
_TIER_DEEPSEEK_FLASH_OFF_PEAK = {
- "input": 0.22 / 1_000_000,
- "output": 0.66 / 1_000_000,
- "cache_creation": 0.22 / 1_000_000,
- "cache_read": 0.007 / 1_000_000,
+ "input": 0.15 / 1_000_000,
+ "output": 0.6 / 1_000_000,
+ "cache_creation": 0.15 / 1_000_000,
+ "cache_read": 0.003 / 1_000_000,
}
_TIER_DEEPSEEK_FLASH_PEAK = {
- "input": 0.44 / 1_000_000,
- "output": 1.32 / 1_000_000,
- "cache_creation": 0.44 / 1_000_000,
- "cache_read": 0.014 / 1_000_000,
+ "input": 0.3 / 1_000_000,
+ "output": 1.2 / 1_000_000,
+ "cache_creation": 0.3 / 1_000_000,
+ "cache_read": 0.006 / 1_000_000,
}
_TIER_DEEPSEEK_PRO_OFF_PEAK = {
"input": 0.66 / 1_000_000,
@@ -144,12 +150,36 @@
# states the windows to the hour and says nothing finer, so hour granularity
# is exact rather than a rounding.
_DEEPSEEK_PEAK_WINDOWS_UTC: tuple[tuple[int, int], ...] = ((1, 4), (6, 10))
+# When ``deepseek-v4-pro`` stops being its own model. The vendor: "From 12:00
+# Beijing Time on September 14, 2026, and until V4.1 Pro is released in the
+# future, requests to deepseek-v4-pro will all be routed to V4.1 Flash and
+# billed at the V4.1 Flash price." Beijing is UTC+8 year-round (no DST), so
+# 12:00 there is 04:00 UTC.
+#
+# A DATE axis rather than a card edit, because both sides are live: a session
+# priced before the cutover really did run V4 Pro at the pro card, and
+# ``compute_cost`` already carries the request's timestamp for the peak
+# schedule, so re-pricing a stored usage record from last week still gets the
+# card that was in force when it was billed. Flattening pro onto the flash
+# card outright would restate that history 4.4x low; leaving it on the pro
+# card would over-report every request after the cutover by the same factor.
+_DEEPSEEK_V4_PRO_ROUTES_TO_FLASH_AT = datetime(
+ 2026, 9, 14, 4, 0, tzinfo=timezone.utc
+).timestamp()
# Canonical model id -> (off-peak card, peak card).
+#
+# ``deepseek-flash`` is DeepSeek's current line (DeepSeek-V4.1-Flash). The two
+# retired flash ids are listed because the vendor still ACCEPTS them and bills
+# them at the flash price — an unpriced id shows no cost at all, which is the
+# one outcome worse than a stale one.
+_DEEPSEEK_FLASH_CARDS = (
+ _TIER_DEEPSEEK_FLASH_OFF_PEAK,
+ _TIER_DEEPSEEK_FLASH_PEAK,
+)
_DEEPSEEK_TIERS: dict[str, tuple[dict[str, float], dict[str, float]]] = {
- "deepseek-v4-flash": (
- _TIER_DEEPSEEK_FLASH_OFF_PEAK,
- _TIER_DEEPSEEK_FLASH_PEAK,
- ),
+ "deepseek-flash": _DEEPSEEK_FLASH_CARDS,
+ "deepseek-v4-flash": _DEEPSEEK_FLASH_CARDS,
+ "deepseek-v4-flash-vision-exp": _DEEPSEEK_FLASH_CARDS,
"deepseek-v4-pro": (
_TIER_DEEPSEEK_PRO_OFF_PEAK,
_TIER_DEEPSEEK_PRO_PEAK,
@@ -321,17 +351,21 @@
"claude-opus-4-5": _TIER_5_25,
"claude-opus-4-1": _TIER_15_75,
"claude-opus-4-20250514": _TIER_15_75,
- # DeepSeek V4 (api.deepseek.com). OpenRouter's ``deepseek/…`` ids resolve
+ # DeepSeek (api.deepseek.com). OpenRouter's ``deepseek/…`` ids resolve
# here too via get_pricing's vendor-prefix strip — consistent with how
# every proxied model is priced at its upstream rate.
- # VALUES UNUSED, same as the gpt-5.6-luna rows below: these two entries
- # are membership gates for ``get_pricing``'s ``model in PRICING`` checks,
+ # VALUES UNUSED, same as the gpt-5.6-luna rows below: these entries are
+ # membership gates for ``get_pricing``'s ``model in PRICING`` checks,
# and the live card is picked by request time in ``_get_exact_pricing``,
# which returns before reaching ``PRICING.get(model)``. They point at the
# off-peak card so that anything reading the table directly (the legacy
# ``services.cost_tracker`` fallback path) gets the rate that covers 133
- # of every 168 hours rather than a number picked for tidiness.
+ # of every 168 hours rather than a number picked for tidiness. Keep this
+ # key set identical to ``_DEEPSEEK_TIERS`` — a row present in one and not
+ # the other either prices at the wrong card or does not price at all.
+ "deepseek-flash": _TIER_DEEPSEEK_FLASH_OFF_PEAK,
"deepseek-v4-flash": _TIER_DEEPSEEK_FLASH_OFF_PEAK,
+ "deepseek-v4-flash-vision-exp": _TIER_DEEPSEEK_FLASH_OFF_PEAK,
"deepseek-v4-pro": _TIER_DEEPSEEK_PRO_OFF_PEAK,
"MiniMax-M3": _TIER_MINIMAX_M3_STANDARD,
"MiniMax-M2.7": _TIER_MINIMAX_M27,
@@ -414,6 +448,22 @@ def is_deepseek_peak(request_time: float | None = None) -> bool:
)
+def deepseek_v4_pro_is_routed_to_flash(request_time: float | None = None) -> bool:
+ """True once ``deepseek-v4-pro`` is served and billed as V4.1 Flash.
+
+ ``request_time`` is POSIX epoch seconds; ``None`` means now. DeepSeek is
+ retiring V4 Pro: from 2026-09-14 12:00 Beijing (04:00 UTC) the id stays
+ accepted but every request behind it runs V4.1 Flash at the Flash price.
+
+ Public for the same reason ``is_deepseek_peak`` is — it is the only way to
+ explain why a ``deepseek-v4-pro`` line costs a quarter of what an older
+ one did, without re-deriving the vendor's retirement schedule. It says
+ nothing about ``deepseek-flash``, which was never on the pro card.
+ """
+ ts = time.time() if request_time is None else request_time
+ return ts >= _DEEPSEEK_V4_PRO_ROUTES_TO_FLASH_AT
+
+
def _get_exact_pricing(
model: str,
*,
@@ -437,6 +487,10 @@ def _get_exact_pricing(
# always resolves to "standard").
deepseek = _DEEPSEEK_TIERS.get(model)
if deepseek is not None:
+ if model == "deepseek-v4-pro" and deepseek_v4_pro_is_routed_to_flash(
+ request_time
+ ):
+ deepseek = _DEEPSEEK_FLASH_CARDS
off_peak, peak = deepseek
return peak if is_deepseek_peak(request_time) else off_peak
if model != "MiniMax-M3":
@@ -653,6 +707,7 @@ def compute_session_cost(
"DEFAULT_PRICING",
"get_pricing",
"is_deepseek_peak",
+ "deepseek_v4_pro_is_routed_to_flash",
"is_known_pricing",
"compute_cost",
"compute_session_cost",
diff --git a/tests/test_ch08_subagents_round4.py b/tests/test_ch08_subagents_round4.py
index 27f14f253..67d9b191d 100644
--- a/tests/test_ch08_subagents_round4.py
+++ b/tests/test_ch08_subagents_round4.py
@@ -148,7 +148,7 @@ def _anthropic(model="claude-fable-5", **kwargs):
return AnthropicProvider(api_key="test-key", model=model, **kwargs)
@staticmethod
- def _deepseek(model="deepseek-v4-pro"):
+ def _deepseek(model="deepseek-flash"):
from src.providers.deepseek_provider import DeepSeekProvider
return DeepSeekProvider(api_key="test-key", model=model)
@@ -170,19 +170,25 @@ def test_anthropic_haiku_tier_resolves_to_live_haiku(self):
)
def test_deepseek_unspecified_uses_flash(self):
- # Goal ask #2: deepseek-v4-flash is the subagent default.
+ # Goal ask #2: the flash line is the subagent default.
p = self._deepseek()
- self.assertEqual(get_agent_model(None, None, p), "deepseek-v4-flash")
+ self.assertEqual(get_agent_model(None, None, p), "deepseek-flash")
def test_deepseek_haiku_tier_uses_flash(self):
# Previously 'haiku' fell back to inherit → every Explore fan-out
- # ran (and billed) the v4-pro session model.
- p = self._deepseek()
- self.assertEqual(get_agent_model(None, "haiku", p), "deepseek-v4-flash")
-
- def test_deepseek_opus_tier_uses_pro(self):
- p = self._deepseek(model="deepseek-v4-flash")
- self.assertEqual(get_agent_model("opus", None, p), "deepseek-v4-pro")
+ # ran (and billed) the pro session model.
+ p = self._deepseek(model="deepseek-v4-pro")
+ self.assertEqual(get_agent_model(None, "haiku", p), "deepseek-flash")
+
+ def test_deepseek_opus_tier_also_uses_flash(self):
+ # DeepSeek is a one-model line again: V4.1 Flash beats V4 Pro on the
+ # vendor's own numbers, and from 2026-09-14 a v4-pro request IS a
+ # flash request. So the opus tier must NOT climb back to v4-pro —
+ # that spends up to 4.4x for the same or worse model. The assertion
+ # is load-bearing precisely because it looks like a no-op: it is what
+ # a re-added pro row in ``subagent_tier_models`` would trip.
+ p = self._deepseek(model="deepseek-v4-pro")
+ self.assertEqual(get_agent_model("opus", None, p), "deepseek-flash")
def test_explicit_inherit_still_forces_session_model(self):
# The Plan/fork agents pin 'inherit' — the provider default must
@@ -193,7 +199,7 @@ def test_explicit_inherit_still_forces_session_model(self):
)
self.assertEqual(
get_agent_model("inherit", None, self._deepseek()),
- "deepseek-v4-pro",
+ "deepseek-flash",
)
def test_custom_anthropic_endpoint_inherits(self):
@@ -330,7 +336,7 @@ def test_tier_env_pin_does_not_leak_to_other_providers(self):
# api.deepseek.com on every Explore spawn (hard 400).
os.environ["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = "my-bedrock-haiku"
d = self._deepseek()
- self.assertEqual(get_agent_model(None, "haiku", d), "deepseek-v4-flash")
+ self.assertEqual(get_agent_model(None, "haiku", d), "deepseek-flash")
def test_known_alias_spellings_never_ship_raw(self):
# critic r3 — trust_literal must only trust ids NO alias table
@@ -366,7 +372,7 @@ def test_alias_unservable_on_provider_inherits(self):
# A known alias whose canonical target the session provider does
# not serve degrades to inherit (never the raw spelling, never a
# foreign id that would 400 louder).
- d = self._deepseek()
+ d = self._deepseek(model="deepseek-v4-pro")
self.assertEqual(
get_agent_model("claude-haiku", None, d), "deepseek-v4-pro",
)
@@ -405,10 +411,10 @@ def test_general_purpose_and_explore_defs_route_as_designed(self):
)
self.assertEqual(
get_agent_model(None, GENERAL_PURPOSE_AGENT.model, d),
- "deepseek-v4-flash",
+ "deepseek-flash",
)
self.assertEqual(
- get_agent_model(None, EXPLORE_AGENT.model, d), "deepseek-v4-flash",
+ get_agent_model(None, EXPLORE_AGENT.model, d), "deepseek-flash",
)
diff --git a/tests/test_deepseek_peak_pricing.py b/tests/test_deepseek_peak_pricing.py
index 5337fc03c..4ed2388bc 100644
--- a/tests/test_deepseek_peak_pricing.py
+++ b/tests/test_deepseek_peak_pricing.py
@@ -1,4 +1,5 @@
-"""DeepSeek V4's peak/off-peak rate schedule (issue #904).
+"""DeepSeek's peak/off-peak rate schedule (issue #904), and V4 Pro's
+retirement onto V4.1 Flash.
Since 2026-08-16 DeepSeek publishes a peak and an off-peak card: every rate
doubles during 01:00-04:00 and 06:00-10:00 UTC, Monday through Friday. That
@@ -17,6 +18,12 @@
cache_creation) and still 3x low. Only an external number catches that,
which is the same lesson the gpt-5.6-luna row in ``services/pricing.py``
records.
+
+A second time axis joined them on 2026-09-10: DeepSeek is retiring
+``deepseek-v4-pro`` onto DeepSeek-V4.1-Flash, so from a fixed instant that id
+prices at the Flash card. It composes with the schedule rather than replacing
+it, and — like the schedule — it reads the REQUEST's timestamp, so a session
+that really did run V4 Pro is never restated at the cheaper card.
"""
from __future__ import annotations
@@ -26,13 +33,22 @@
import pytest
from src.services.pricing import (
+ _TIER_DEEPSEEK_FLASH_OFF_PEAK,
+ _TIER_DEEPSEEK_FLASH_PEAK,
+ _TIER_DEEPSEEK_PRO_OFF_PEAK,
+ _TIER_DEEPSEEK_PRO_PEAK,
compute_cost,
+ deepseek_v4_pro_is_routed_to_flash,
get_pricing,
is_deepseek_peak,
)
-MODELS = ("deepseek-v4-flash", "deepseek-v4-pro")
+MODELS = ("deepseek-flash", "deepseek-v4-pro")
+
+# Ids DeepSeek retired but still accepts, all served by — and billed as —
+# DeepSeek-V4.1-Flash.
+LEGACY_FLASH_IDS = ("deepseek-v4-flash", "deepseek-v4-flash-vision-exp")
def _utc(year: int, month: int, day: int, hour: int, minute: int = 0,
@@ -47,10 +63,20 @@ def _utc(year: int, month: int, day: int, hour: int, minute: int = 0,
MON, FRI, SAT, SUN = 24, 28, 29, 30
# One instant on each side of the schedule, for the tests that care which
-# card is in force rather than where the boundaries are.
+# card is in force rather than where the boundaries are. Both are in August
+# 2026, BEFORE the 2026-09-14 cutover below, so ``deepseek-v4-pro`` is still
+# priced as its own model here — the schedule tests and the retirement tests
+# stay independent.
OFF_PEAK_TS = _utc(2026, 8, MON, 12)
PEAK_TS = _utc(2026, 8, MON, 2)
+# DeepSeek retires V4 Pro onto V4.1 Flash at 12:00 Beijing (UTC+8) on
+# 2026-09-14 — 04:00 UTC, which is itself off-peak (the 01:00-04:00 window is
+# half-open). 2026-09-14 is a Monday and 2026-09-15 a Tuesday.
+CUTOVER_TS = _utc(2026, 9, 14, 4)
+AFTER_CUTOVER_OFF_PEAK_TS = _utc(2026, 9, 15, 12)
+AFTER_CUTOVER_PEAK_TS = _utc(2026, 9, 15, 2)
+
# --------------------------------------------------------------------------- #
# The schedule
@@ -148,12 +174,15 @@ def test_omitting_request_time_reads_the_clock(monkeypatch) -> None:
# The card
# --------------------------------------------------------------------------- #
-# Published USD per 1M tokens, read 2026-08-25 from
-# https://api-docs.deepseek.com/quick_start/pricing/
+# Published USD per 1M tokens, read 2026-09-10 from
+# https://api-docs.deepseek.com/quick_start/pricing/. The flash column is
+# DeepSeek-V4.1-Flash's, which is CHEAPER than the V4 flash card it replaced
+# (0.22 / 0.66 / 0.007) — a re-card in the user's favour is exactly as silent
+# as one against them, and only an external number catches either.
PUBLISHED = {
- "deepseek-v4-flash": {
- "off_peak": {"input": 0.22, "output": 0.66, "cache_read": 0.007},
- "peak": {"input": 0.44, "output": 1.32, "cache_read": 0.014},
+ "deepseek-flash": {
+ "off_peak": {"input": 0.15, "output": 0.6, "cache_read": 0.003},
+ "peak": {"input": 0.3, "output": 1.2, "cache_read": 0.006},
},
"deepseek-v4-pro": {
"off_peak": {"input": 0.66, "output": 1.98, "cache_read": 0.022},
@@ -182,25 +211,120 @@ def test_peak_is_exactly_double_off_peak(model: str) -> None:
assert peak[field] == pytest.approx(2 * off[field], rel=1e-12), field
-def test_pro_is_three_times_flash_except_on_cache_read() -> None:
- """The vendor prices pro at exactly 3x flash on input and output — but NOT
- on cache read, where $0.022 against $0.007 is 22/7, not 3.
+def test_no_field_of_the_pro_card_derives_from_the_flash_one() -> None:
+ """Every field has to be read off the page.
- Pinned because that is exactly the kind of near-ratio that invites
- deriving one row from the other. The cache-read rate has to be read off
- the page, and at ~96% of agentic input tokens it is the field that moves
- the bill most.
+ The V4 cards were pro = 3x flash on input and output but 22/7 on cache
+ read, which was already enough to make "derive one row from the other"
+ wrong. Against V4.1 Flash the ratios are 4.4x, 3.3x and 7.33x — no two
+ alike. Pinned as "no single multiplier fits" rather than as three magic
+ numbers, because the claim worth defending is that the rows are
+ independent, not what today's quotients happen to be.
"""
for ts in (OFF_PEAK_TS, PEAK_TS):
- flash = get_pricing("deepseek-v4-flash", request_time=ts)
+ flash = get_pricing("deepseek-flash", request_time=ts)
pro = get_pricing("deepseek-v4-pro", request_time=ts)
- for field in ("input", "output", "cache_creation"):
- assert pro[field] == pytest.approx(3 * flash[field], rel=1e-9), field
- assert pro["cache_read"] == pytest.approx(
- flash["cache_read"] * 22 / 7, rel=1e-9
- )
- assert pro["cache_read"] != pytest.approx(
- 3 * flash["cache_read"], rel=1e-9
+ ratios = [pro[f] / flash[f] for f in ("input", "output", "cache_read")]
+ assert len(set(round(r, 9) for r in ratios)) == 3, ratios
+ # And the one that moves an agentic bill most is the odd one out.
+ assert ratios[2] > max(ratios[0], ratios[1])
+
+
+def test_retired_flash_ids_are_billed_at_the_flash_card() -> None:
+ """DeepSeek still accepts ``deepseek-v4-flash`` and
+ ``deepseek-v4-flash-vision-exp``; both are served by V4.1 Flash "and
+ billed at the Flash price".
+
+ So they must carry V4.1 Flash's card, not the V4 one they shipped with.
+ Keeping the old rates on them would over-report a legacy-id session by
+ 1.5x on input and 2.3x on cache read — the same silent-drift failure
+ issue #904 was about, pointing the other way.
+ """
+ for model in LEGACY_FLASH_IDS:
+ for ts in (OFF_PEAK_TS, PEAK_TS, AFTER_CUTOVER_OFF_PEAK_TS):
+ assert get_pricing(model, request_time=ts) == get_pricing(
+ "deepseek-flash", request_time=ts
+ ), model
+
+
+# --------------------------------------------------------------------------- #
+# V4 Pro's retirement onto V4.1 Flash
+# --------------------------------------------------------------------------- #
+
+def _card(tiers: tuple[dict[str, float], dict[str, float]], ts: float) -> dict:
+ """The peak-schedule half of the expectation, so the retirement tests can
+ state the other half without re-deriving which window ``ts`` is in."""
+ off_peak, peak = tiers
+ return peak if is_deepseek_peak(ts) else off_peak
+
+
+PRO_TIERS = (_TIER_DEEPSEEK_PRO_OFF_PEAK, _TIER_DEEPSEEK_PRO_PEAK)
+FLASH_TIERS = (_TIER_DEEPSEEK_FLASH_OFF_PEAK, _TIER_DEEPSEEK_FLASH_PEAK)
+
+
+def test_pro_prices_as_itself_before_the_cutover() -> None:
+ # One second before, which is 03:59:59 UTC — inside a peak window, so this
+ # also pins that the two axes compose rather than one overriding the other.
+ ts = CUTOVER_TS - 1
+ assert deepseek_v4_pro_is_routed_to_flash(ts) is False
+ assert get_pricing("deepseek-v4-pro", request_time=ts) == _card(PRO_TIERS, ts)
+
+
+def test_pro_prices_as_flash_from_the_cutover_instant() -> None:
+ """Half-open, like the peak windows: the stated instant is already the new
+ regime."""
+ assert deepseek_v4_pro_is_routed_to_flash(CUTOVER_TS) is True
+ assert get_pricing("deepseek-v4-pro", request_time=CUTOVER_TS) == get_pricing(
+ "deepseek-flash", request_time=CUTOVER_TS
+ )
+
+
+def test_cutover_is_1200_beijing_not_1200_utc() -> None:
+ """Beijing is UTC+8 with no DST, so the vendor's noon is 04:00 UTC. An
+ off-by-eight-hours here mis-prices a whole business day."""
+ assert deepseek_v4_pro_is_routed_to_flash(_utc(2026, 9, 14, 3, 59, 59)) is False
+ assert deepseek_v4_pro_is_routed_to_flash(_utc(2026, 9, 14, 4, 0, 0)) is True
+
+
+def test_pro_still_follows_the_peak_schedule_after_the_cutover() -> None:
+ """The retirement swaps which CARD applies; it does not exempt the id from
+ the clock. Both axes have to compose, and the peak one is applied last."""
+ for ts, window in (
+ (AFTER_CUTOVER_OFF_PEAK_TS, "off_peak"),
+ (AFTER_CUTOVER_PEAK_TS, "peak"),
+ ):
+ card = PUBLISHED["deepseek-flash"][window]
+ pricing = get_pricing("deepseek-v4-pro", request_time=ts)
+ for field, dollars in card.items():
+ assert pricing[field] == dollars / 1_000_000, (window, field)
+
+
+def test_cutover_does_not_restate_history_at_the_new_card() -> None:
+ """A request that really did run V4 Pro is priced by ITS timestamp, not by
+ the clock at display time. ``compute_cost`` already carries the timestamp
+ for the peak schedule; the retirement rides the same value, so re-reading
+ an August session from October must still show what it cost.
+ """
+ usage = _agent_mix()
+ before = compute_cost("deepseek-v4-pro", usage, request_time=OFF_PEAK_TS)
+ after = compute_cost(
+ "deepseek-v4-pro", usage, request_time=AFTER_CUTOVER_OFF_PEAK_TS
+ )
+ assert before == pytest.approx(0.0536, abs=5e-5)
+ assert after == pytest.approx(
+ compute_cost("deepseek-flash", usage,
+ request_time=AFTER_CUTOVER_OFF_PEAK_TS)
+ )
+ assert before > 4 * after
+
+
+def test_flash_is_untouched_by_the_pro_cutover() -> None:
+ """Scope gate: ``deepseek-flash`` was never on the pro card, so no instant
+ of the retirement may move it."""
+ for ts in (CUTOVER_TS - 1, CUTOVER_TS, AFTER_CUTOVER_OFF_PEAK_TS,
+ AFTER_CUTOVER_PEAK_TS):
+ assert get_pricing("deepseek-flash", request_time=ts) == _card(
+ FLASH_TIERS, ts
)
diff --git a/tests/test_deepseek_prefix_cache.py b/tests/test_deepseek_prefix_cache.py
index e1d9ab2f7..3388f8f3a 100644
--- a/tests/test_deepseek_prefix_cache.py
+++ b/tests/test_deepseek_prefix_cache.py
@@ -50,7 +50,8 @@ def test_is_deepseek_flag_scoped_to_deepseek_provider():
# Context-window registry
# --------------------------------------------------------------------------- #
-def test_deepseek_v4_context_windows_registered():
+def test_deepseek_context_windows_registered():
+ assert get_context_window_for_model("deepseek-flash") == 1_000_000
assert get_context_window_for_model("deepseek-v4-pro") == 1_000_000
assert get_context_window_for_model("deepseek-v4-flash") == 1_000_000
# DeepSeek's documented ceiling. Was 8_192 — a placeholder that
@@ -64,6 +65,7 @@ def test_deepseek_v4_context_windows_registered():
# (20_000). Effective input therefore moves 991_808 -> 980_000 only.
# Pinned so nobody "fixes" a timeout by editing this number: it cannot
# truncate a response, because it never reaches the request.
+ assert get_model_max_output_tokens("deepseek-flash") == 384_000
assert get_model_max_output_tokens("deepseek-v4-pro") == 384_000
assert get_model_max_output_tokens("deepseek-v4-flash") == 384_000
@@ -77,6 +79,34 @@ def test_other_providers_context_window_unchanged():
assert get_context_window_for_model("some-unknown-model") == 200_000
# Legacy aliases intentionally NOT registered (broad prefix-match risk).
assert get_context_window_for_model("deepseek-chat") == 200_000
+ assert get_context_window_for_model("deepseek-reasoner") == 200_000
+
+
+def test_deepseek_flash_row_does_not_claim_the_family_prefix():
+ """``deepseek-flash``'s prefix base is the bare family name, so the row
+ opts out of ``get_model_config``'s prefix fallback (``claims_prefix``).
+ Without that, every id above would inherit its 1M window — including the
+ legacy aliases the previous test pins at the default, and OpenRouter's
+ ``deepseek/…`` ids, which decision #1 keeps out of scope.
+ """
+ from src.models.configs import MODEL_CONFIGS, get_model_config
+
+ assert MODEL_CONFIGS["deepseek-flash"].claims_prefix is False
+ assert get_model_config("deepseek-flash").model_id == "deepseek-flash"
+ for other in ("deepseek-chat", "deepseek-reasoner",
+ "deepseek/deepseek-flash", "deepseek-anything-else"):
+ assert get_model_config(other) is None, other
+
+
+def test_deepseek_flash_is_the_one_deepseek_model_that_sees():
+ """DeepSeek-V4.1-Flash folds in the retired ``*-vision-exp`` line; every
+ other DeepSeek id still 400s on an image content block, which is what the
+ fusion-model path exists for."""
+ from src.models.capabilities import supports_vision
+
+ assert supports_vision("deepseek-flash") is True
+ assert supports_vision("deepseek-v4-pro") is False
+ assert supports_vision("deepseek-v4-flash") is False
# --------------------------------------------------------------------------- #
@@ -181,28 +211,32 @@ def test_other_provider_usage_unchanged_by_cache_fields():
def test_deepseek_pricing_registered():
"""Published rates, pinned as absolutes on both sides of the schedule.
- Checked 2026-08-25 against api-docs.deepseek.com/quick_start/pricing/.
+ Checked 2026-09-10 against api-docs.deepseek.com/quick_start/pricing/.
Pinning absolutes (rather than ratios) is what catches a stale card —
the pre-2026-08-16 values in issue #904 were internally consistent and
still 3x low.
+
+ Both instants predate the 2026-09-14 retirement of ``deepseek-v4-pro``
+ onto V4.1 Flash, so the pro card here is still V4 Pro's own; that axis is
+ pinned in ``tests/test_deepseek_peak_pricing.py``.
"""
from src.services.pricing import get_pricing
- flash = get_pricing("deepseek-v4-flash", request_time=_OFF_PEAK)
+ flash = get_pricing("deepseek-flash", request_time=_OFF_PEAK)
pro = get_pricing("deepseek-v4-pro", request_time=_OFF_PEAK)
assert flash is not None and pro is not None
- assert flash["input"] == 0.22 / 1_000_000
- assert flash["output"] == 0.66 / 1_000_000
- assert flash["cache_read"] == 0.007 / 1_000_000
+ assert flash["input"] == 0.15 / 1_000_000
+ assert flash["output"] == 0.6 / 1_000_000
+ assert flash["cache_read"] == 0.003 / 1_000_000
assert pro["input"] == 0.66 / 1_000_000
assert pro["output"] == 1.98 / 1_000_000
assert pro["cache_read"] == 0.022 / 1_000_000
- flash_peak = get_pricing("deepseek-v4-flash", request_time=_PEAK)
+ flash_peak = get_pricing("deepseek-flash", request_time=_PEAK)
pro_peak = get_pricing("deepseek-v4-pro", request_time=_PEAK)
- assert flash_peak["input"] == 0.44 / 1_000_000
- assert flash_peak["output"] == 1.32 / 1_000_000
- assert flash_peak["cache_read"] == 0.014 / 1_000_000
+ assert flash_peak["input"] == 0.3 / 1_000_000
+ assert flash_peak["output"] == 1.2 / 1_000_000
+ assert flash_peak["cache_read"] == 0.006 / 1_000_000
assert pro_peak["input"] == 1.32 / 1_000_000
assert pro_peak["output"] == 3.96 / 1_000_000
assert pro_peak["cache_read"] == 0.044 / 1_000_000
@@ -215,7 +249,7 @@ def test_deepseek_cache_creation_mirrors_input_on_both_cards():
from src.services.pricing import get_pricing
for ts in (_OFF_PEAK, _PEAK):
- for model in ("deepseek-v4-flash", "deepseek-v4-pro"):
+ for model in ("deepseek-flash", "deepseek-v4-flash", "deepseek-v4-pro"):
p = get_pricing(model, request_time=ts)
assert p["cache_creation"] == p["input"]
@@ -244,14 +278,14 @@ def test_deepseek_cost_credits_cache_hit_end_to_end():
prompt_tokens=1_000_000, completion_tokens=0, total_tokens=1_000_000,
prompt_cache_hit_tokens=900_000, prompt_cache_miss_tokens=100_000,
))
- cost = compute_cost("deepseek-v4-flash", usage, request_time=_OFF_PEAK)
- expected = 100_000 * 0.22 / 1_000_000 + 900_000 * 0.007 / 1_000_000
+ cost = compute_cost("deepseek-flash", usage, request_time=_OFF_PEAK)
+ expected = 100_000 * 0.15 / 1_000_000 + 900_000 * 0.003 / 1_000_000
assert abs(cost - expected) < 1e-12
# ~9x cheaper than pricing the whole prompt as uncached input.
- full = 1_000_000 * 0.22 / 1_000_000
+ full = 1_000_000 * 0.15 / 1_000_000
assert cost < full / 5
# The same response costs exactly twice as much inside a peak window.
- peak = compute_cost("deepseek-v4-flash", usage, request_time=_PEAK)
+ peak = compute_cost("deepseek-flash", usage, request_time=_PEAK)
assert abs(peak - 2 * expected) < 1e-12