fix(client): translate DeepSeek 429 into a concurrency-limit hint#1526
Merged
Conversation
DeepSeek's actual rate limit is concurrency-based, not RPM (500 for
v4-pro, 2500 for v4-flash, account-wide across API keys). When the
account exceeds it the server returns HTTP 429, which currently
surfaces as a raw `DeepSeek 429: {...body}` string — fine for a
maintainer to read, useless for a user trying to figure out why.
Add a friendly message that names the actual cap, identifies the
likely cause (another reasonix process on the same key, or a
parallel subagent fan-out that overshot), and points at the
remediation (wait + retry, reduce parallelism, or request expansion).
Same shape as the existing 401/402/422/400 cases.
Also clarify the `rateLimit.rpm` config docstring: it's a client-side
self-throttle for politeness on shared infra, not a DeepSeek-enforced
limit. DeepSeek has never had a published RPM cap.
Closes #1522
esengine
added a commit
that referenced
this pull request
May 22, 2026
…se (#1565) * chore(release): 0.49.0 — static-history TUI, queued steers, Bing default, lifecycle plans Headline themes: - TUI: Static-history renderer is the only path; virtual-viewport layers removed (#1529 stages 1-4) - Chat: queued mid-turn steer handling so input mid-render doesn't drop or fight the live frame (#1501) - Web search: default switches to Bing; dashboard engine switcher; Mojeek dropped (#1558) - Plans: lifecycle evidence summaries surface why a plan is ready to accept (#1500) - Desktop: native OS notifications for approvals + completion (#1519) - i18n: CLI command output (/mcp /sessions /prune /theme) + approval-prompt labels translated (#1524, #1560) - Security: SSRF block in web_fetch (#1544), edit-snapshot path containment (#1454), shell redirect sandbox (#1457), Task integrity guardrail (#1516) - Tools: per-turn dispatch-rate limit (#1356); run_command discourages shell-based edits (#1514) - Client: DeepSeek 429 → concurrency-limit hint (#1526); timeoutMs honored with AbortSignal (#1535); --no-proxy opt-out for direct route (#1507) - Files: read/edit/restore preserves source encoding (GB18030 / UTF-8 BOM) (#1518) - Context: pinned constraints survive folds + full tail capture (#1515, #1552) - Refactor: lifecycle risk policy extracted into its own module (#1557) See CHANGELOG for the full list. * fix(context): align fold summary prefix with main agent for cache reuse The summarizer call was sending a bespoke "You compress conversation history" system prompt and no tools, guaranteeing a 0% cache hit against the main agent's just-cached prefix. Reshape the request so system + tools + head bytes mirror the live agent's last call — the only novel bytes are the trailing summarize instruction. Skill-pin handling now collects bodies read-only instead of stubbing mid-head, so the cache prefix stays unbroken. The summarize instruction names pinned skills so the model knows not to paraphrase their bodies (which we append verbatim regardless). Measured on a real session at 48.7K prompt tokens: OLD shape: 0.0% cache hit → $0.145 per fold NEW shape: 99.6% cache hit → $0.015 per fold saving: 89.6% per fold * tools: add fold-cache shape + live benchmarks bench-fold-cache-shape.mjs replays real session jsonls, simulates OLD vs NEW summary-call shapes at the fold point, and reports byte-level shared-prefix with the main agent's preceding request. Pure local — no API required. bench-fold-cache-live.mjs sends one priming + two summary calls to DeepSeek and reports prompt_cache_hit_tokens / cost for each shape. Used to confirm the shape change actually translates to API-side cache hits. --------- Co-authored-by: reasonix <reasonix@deepseek.com>
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.
Summary
Translate
DeepSeek 429into a user-friendly concurrency-limit hint, and clarify that therateLimit.rpmconfig is a client-side self-throttle (not a DeepSeek-enforced cap). Closes #1522.Scope decision (intentionally narrow)
The "new rate limit" docs the issue references are concurrency-based: 500 in-flight requests per account for
deepseek-v4-pro, 2500 fordeepseek-v4-flash, summed across all API keys on the account. A single-user CLI doing one turn at a time can never approach this — only really hit when:reasonixprocesses sharing one key, orspawn_subagentparallel fan-out + nested subagents pile up beyond 500 in flightSo this PR is only about telling the user what's happening when 429 does fire. Explicitly not in scope:
user_idparameter support — useful for multi-tenant scenarios (dashboard, cc-connect bridge), but per RFC RFC: Chat-platform bridge (Feishu / WeWork / WeChat) #410 chat-bridge work lives outside core. Reasonix-core stays single-user.client.ts:117-126comment confirms SSE comments and non-stream empty lines are invisible to our parsers).What this PR does
1. Friendly 429 error message
src/loop/errors.ts: add429 → t("errors.concurrency429", { inner })alongside the existing 401/402/422/400 cases.src/i18n/{EN,zh-CN}.ts+types.ts: newerrors.concurrency429key. Names the actual caps (500 pro / 2500 flash), identifies the likely cause (another reasonix process on the same key, or a fan-out that overshot), and points at the remediation (wait + retry, reduce parallelism, or request expansion at platform.deepseek.com).Before:
After:
2. Clarify
rateLimit.rpmdocstringsrc/config.ts:RateLimitConfig.rpmis a client-side self-throttle paced by a min-interval timer — it never mapped to a DeepSeek-enforced cap (DeepSeek has never published an RPM ceiling). Docstring now spells that out so users don't think settingrpm: 60does anything DeepSeek-related.Test Plan
tests/loop-error.test.ts— new case:DeepSeek 429: {...}→ contains "concurrency limit" + "500" + "2500" + the inner reason + the platform URLnpx vitest run tests/loop-error.test.ts tests/comment-policy.test.ts— 38 passnpx tsc --noEmit— cleannpx biome checkon touched files — cleannpm run verifyvia pre-push hook — greenTest Plan (manual)
Not run — no easy way to actually trigger 429 from a single CLI process against DeepSeek's real concurrency cap without spinning up 500+ pro requests. The unit test covers the error-formatting path which is the only thing this PR changes.