Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` —
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**

<sub>`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.</sub>
<sub>`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.</sub>

</div>

Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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": {
Expand Down
4 changes: 3 additions & 1 deletion docs/guide/SETUP_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://z.ai/>.

Expand Down
7 changes: 6 additions & 1 deletion src/context_system/context_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
51 changes: 44 additions & 7 deletions src/models/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 ``<vendor>/`` segment so OpenRouter ids
resolve to their bare row. ``get_pricing`` (services/pricing.py) does
Expand All @@ -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
Expand Down
29 changes: 18 additions & 11 deletions src/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
Expand Down
37 changes: 23 additions & 14 deletions src/providers/deepseek_provider.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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``
Expand All @@ -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:
Expand Down Expand Up @@ -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",
]
Loading
Loading