fix(deepseek): Responses reasoning placeholder, 128K cap at max effort, exec finish_reason; localize /statusline hints

DeepSeek harness follow-ups from CW-7450, plus one localization gap.

- Responses route (deepseek-flash default): replaying a tool call with no
  stored DeepSeek reasoning (an interrupted turn, a thinking-off turn, or
  history from another provider) returned `400 The reasoning_text in the
  thinking mode must be passed back`. convert_messages_to_responses_input now
  emits a reasoning item with the existing REASONING_REPLAY_PLACEHOLDER before
  the first function_call of an assistant message that has none. Live probes
  on 2026-10-17 against api.deepseek.com/responses: function_call without a
  reasoning item -> HTTP 400; with the placeholder item -> HTTP 200.
  On Chat, probes showed DeepSeek enforces reasoning_content only on tool-call
  assistant messages (text-only turns without it returned 200 on
  deepseek-flash and deepseek-v4-pro, in both a fresh and an in-tool-loop
  shape; a tool-call turn without it returned 400), which the existing
  placeholder already covers, so the proposed Chat change was not applied.
- Output cap: at reasoning effort max (or ultra, which DeepSeek maps to max)
  the first-party DeepSeek route now gets an automatic 131,072-token cap,
  DeepSeek's documented max-effort default, instead of 65,536.
  effective_max_output_tokens_for_turn takes the resolved effort and the
  outbound clamp in client.rs passes the request's own effort, so the larger
  cap reaches the wire. Every other provider and effort keeps 65,536; explicit
  overrides and known route limits intersect as before. The stale provenance
  comment now cites DeepSeek's published limits (64K thinking default, 128K at
  max, range up to 393216). The free helper effective_max_output_tokens is now
  test-only.
- exec stream-json `turn_usage` records carry the provider's raw per-step
  `finish_reason` (omitted when unreported). Event::TurnUsage gains the field;
  the host protocol wire, Runtime turn.usage and TUI metrics are unchanged.
- /statusline item hints resolve through 12 new MessageIds in all 15 locale
  packs; English is byte-identical.
- CHANGELOG: entries for the above, and the 0.10.2 catalog bullet no longer
  claims model resolve/set, the app-server registry and route admission read
  catalog facts (per the #6396 audit).

Worker provenance: the output-cap, finish_reason and localization slices were
drafted by DeepSeek-Flash (V4.1-Flash-1017) via Codewhale exec in scratch
exports, reviewed line by line; the client.rs clamp, the Responses fix and the
dead-code fix are mine.

Checks (local, frozen batch on 626d7e0142+b327a4d0ef):
- cargo fmt --check OK
- CI-form clippy -p codewhale-tui -p codewhale-localization --all-targets
  --all-features -D warnings: clean
- codewhale-localization tests: 54 passed
- dev-test tui (route_budget status_picker responses exec_stream
  protocol_parity terminal_mode turn_usage chat:: client::): 847 passed
- dev-test tui (conformance:: max_output output_cap preparation model_step
  TurnUsage reasoning_replay rlm): 190 passed
- check-tui-locale-parity PASS (15 x 2568 keys), product vocabulary,
  sync-changelog --check, check-versions, check-feature-release-notes,
  check-facts, runtime-contract budget (55 metrics at budget),
  blocking-calls budget, check-contributor-credit: all pass

Refs CW-7450

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-17 11:23:39 -07:00
1 parent b327a4d0ef
commit 25966bca59
34 files changed
+416 -65

No files matched your search

+24 -3
View File
@@ -244,6 +244,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`/new` refused. Thanks @luestr for reporting the refusal in #6981; this is
one way to reach it, and the report stays open until the blocker that
0.10.2 names confirms its cause (Refs #6981).
- DeepSeek on the Responses route (the `deepseek-flash` default) no longer
fails with `400 reasoning_text must be passed back` when a session replays a
tool call that has no stored DeepSeek reasoning: a turn that was interrupted,
ran with thinking off, or came from another provider. Codewhale now sends the
same `(reasoning omitted)` placeholder the Chat route already uses, before
that turn's tool calls. A live probe on 2026-10-17 returned 400 without it and
200 with it. On Chat, DeepSeek enforces this only for tool-call turns, which
were already covered (Refs CW-7450).
- At reasoning effort `max`, the first-party DeepSeek route now asks for up to
131,072 output tokens, DeepSeek's documented default at that effort, instead
of 65,536. Every other provider and effort keeps 65,536, and explicit
overrides and route limits still apply (Refs CW-7450).
- `codewhale exec --output-format stream-json` adds the provider's
`finish_reason` to each `turn_usage` record, so a tool-call stop can be told
apart from an output-limit stop or a provider abort. The field is omitted
when the provider reports none (Refs CW-7450).
- The `/statusline` picker's item hints are localized in all 15 UI languages
instead of always showing English.
## [0.10.2] - 2026-10-13
@@ -302,9 +320,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
remain in place. Windows Terminal IME preedit is still controlled by the
terminal; native IME retesting remains open (#2323).
- Model lists, resolution, picker controls and Runtime facts use the same
endpoint-scoped catalog facts. Cost estimates preserve complete price tiers,
calculate exact decimal amounts per request, and freeze rates at dispatch.
- `codewhale model list`, the model picker, sub-agent model validation and
Runtime facts read the same endpoint-scoped catalog facts. `model resolve`
and `model set`, the app-server model registry and route admission still
read the static registry; moving them is tracked in #6396. Cost estimates
preserve complete price tiers, calculate exact decimal amounts per request,
and freeze rates at dispatch.
Missing rates or required usage breakdowns stay unpriced; public prices do
not establish account access. The offline seed records its source snapshot
and keeps absent default wire IDs as dated compatibility fallbacks. Refs #6396.
+12
View File
@@ -240,6 +240,18 @@
"StatusPickerActionNone": "cap ",
"StatusPickerActionSave": "desar ",
"StatusPickerActionCancel": "cancel·lar ",
"StatusPickerModeHint": "plan · work · operate",
"StatusPickerModelHint": "l'ID del model al qual envies",
"StatusPickerCostHint": "total acumulat d'aquesta sessió",
"StatusPickerCacheHint": "% del prompt servit des de la memòria cau",
"StatusPickerContextPercentHint": "tokens usats / finestra de context del model",
"StatusPickerTokensHint": "tokens de sortida del torn actual o anterior",
"StatusPickerBalanceHint": "crèdit prepagat restant del proveïdor actiu",
"StatusPickerSessionMetricsHint": "temps fins al primer token i taxa de sortida",
"StatusPickerTtftHint": "espera mitjana del primer token",
"StatusPickerOutputRateHint": "mitjana de tok/s, inclosa l'espera del primer token",
"StatusPickerWorkspaceHint": "directori on escriu aquesta sessió",
"StatusPickerGitBranchHint": "branca on cau el pròxim commit",
"HotbarSetupTitle": " Configuració de Hotbar ",
"HotbarSetupSourceApp": "app",
"HotbarSetupSourceSlash": "slash",
+12
View File
@@ -240,6 +240,18 @@
"StatusPickerActionNone": "keine ",
"StatusPickerActionSave": "speichern ",
"StatusPickerActionCancel": "abbrechen ",
"StatusPickerModeHint": "plan · work · operate",
"StatusPickerModelHint": "die Modell-ID, an die du sendest",
"StatusPickerCostHint": "laufende Summe dieser Sitzung",
"StatusPickerCacheHint": "% des Prompts aus dem Cache",
"StatusPickerContextPercentHint": "verbrauchte Tokens / Kontextfenster des Modells",
"StatusPickerTokensHint": "Ausgabe-Tokens des laufenden oder letzten Turns",
"StatusPickerBalanceHint": "verbleibendes Prepaid-Guthaben des aktiven Anbieters",
"StatusPickerSessionMetricsHint": "Zeit bis zum ersten Token und Ausgaberate",
"StatusPickerTtftHint": "durchschnittliche Wartezeit auf das erste Token",
"StatusPickerOutputRateHint": "Ø tok/s, einschließlich Wartezeit auf das erste Token",
"StatusPickerWorkspaceHint": "Verzeichnis, in das diese Sitzung schreibt",
"StatusPickerGitBranchHint": "Branch, auf dem der nächste Commit landet",
"HotbarSetupTitle": " Hotbar-Setup ",
"HotbarSetupSourceApp": "App",
"HotbarSetupSourceSlash": "Slash",
+12
View File
@@ -240,6 +240,18 @@
"StatusPickerActionNone": "none ",
"StatusPickerActionSave": "save ",
"StatusPickerActionCancel": "cancel ",
"StatusPickerModeHint": "plan · work · operate",
"StatusPickerModelHint": "the model id you'll send to",
"StatusPickerCostHint": "running total for this session",
"StatusPickerCacheHint": "% of prompt served from cache",
"StatusPickerContextPercentHint": "tokens used / model context window",
"StatusPickerTokensHint": "output tokens of the live or last turn",
"StatusPickerBalanceHint": "remaining prepaid credit from the active provider",
"StatusPickerSessionMetricsHint": "time to first token and output rate",
"StatusPickerTtftHint": "average wait for the first token",
"StatusPickerOutputRateHint": "average tok/s, including first-token wait",
"StatusPickerWorkspaceHint": "directory this session writes to",
"StatusPickerGitBranchHint": "branch the next commit lands on",
"HotbarSetupTitle": " Hotbar setup ",
"HotbarSetupSourceApp": "app",
"HotbarSetupSourceSlash": "slash",
+12
View File
@@ -240,6 +240,18 @@
"StatusPickerActionNone": "ninguno ",
"StatusPickerActionSave": "guardar ",
"StatusPickerActionCancel": "cancelar ",
"StatusPickerModeHint": "plan · work · operate",
"StatusPickerModelHint": "el ID del modelo al que envías",
"StatusPickerCostHint": "total acumulado de esta sesión",
"StatusPickerCacheHint": "% del prompt servido desde el caché",
"StatusPickerContextPercentHint": "tokens usados / ventana de contexto del modelo",
"StatusPickerTokensHint": "tokens de salida del turno actual o anterior",
"StatusPickerBalanceHint": "crédito prepago restante del proveedor activo",
"StatusPickerSessionMetricsHint": "tiempo hasta el primer token y tasa de salida",
"StatusPickerTtftHint": "espera promedio del primer token",
"StatusPickerOutputRateHint": "promedio de tok/s, incluida la espera del primer token",
"StatusPickerWorkspaceHint": "directorio donde escribe esta sesión",
"StatusPickerGitBranchHint": "rama donde cae el próximo commit",
"HotbarSetupTitle": " Configuración de Hotbar ",
"HotbarSetupSourceApp": "app",
"HotbarSetupSourceSlash": "comando",
+12
View File
@@ -240,6 +240,18 @@
"StatusPickerActionNone": "aucun ",
"StatusPickerActionSave": "enregistrer ",
"StatusPickerActionCancel": "annuler ",
"StatusPickerModeHint": "plan · work · operate",
"StatusPickerModelHint": "l'ID du modèle vers lequel vous envoyez",
"StatusPickerCostHint": "total cumulé de cette session",
"StatusPickerCacheHint": "% du prompt servi depuis le cache",
"StatusPickerContextPercentHint": "tokens utilisés / fenêtre de contexte du modèle",
"StatusPickerTokensHint": "tokens de sortie du tour en cours ou précédent",
"StatusPickerBalanceHint": "crédit prépayé restant du fournisseur actif",
"StatusPickerSessionMetricsHint": "temps jusqu'au premier token et débit de sortie",
"StatusPickerTtftHint": "attente moyenne du premier token",
"StatusPickerOutputRateHint": "moyenne de tok/s, attente du premier token incluse",
"StatusPickerWorkspaceHint": "répertoire où cette session écrit",
"StatusPickerGitBranchHint": "branche sur laquelle atterrit le prochain commit",
"HotbarSetupTitle": " Configuration Hotbar ",
"HotbarSetupSourceApp": "app",
"HotbarSetupSourceSlash": "slash",
+12
View File
@@ -240,6 +240,18 @@
"StatusPickerActionNone": "कोई नहीं ",
"StatusPickerActionSave": "सहेजें ",
"StatusPickerActionCancel": "रद्द करें ",
"StatusPickerModeHint": "plan · work · operate",
"StatusPickerModelHint": "वह मॉडल ID जिस पर आप भेजेंगे",
"StatusPickerCostHint": "इस सत्र का चालू कुल",
"StatusPickerCacheHint": "प्रॉम्प्ट का कैश से दिया गया %",
"StatusPickerContextPercentHint": "उपयोग किए गए टोकन / मॉडल संदर्भ विंडो",
"StatusPickerTokensHint": "मौजूदा या पिछले टर्न के आउटपुट टोकन",
"StatusPickerBalanceHint": "सक्रिय प्रदाता से बचा प्रीपेड क्रेडिट",
"StatusPickerSessionMetricsHint": "पहले टोकन तक समय और आउटपुट दर",
"StatusPickerTtftHint": "पहले टोकन के लिए औसत प्रतीक्षा",
"StatusPickerOutputRateHint": "औसत tok/s, पहले टोकन की प्रतीक्षा सहित",
"StatusPickerWorkspaceHint": "यह सत्र जिस निर्देशिका में लिखता है",
"StatusPickerGitBranchHint": "अगला कमिट जिस ब्रांच पर जाएगा",
"HotbarSetupTitle": " Hotbar सेटअप ",
"HotbarSetupSourceApp": "ऐप",
"HotbarSetupSourceSlash": "स्लैश",
+12
View File
@@ -240,6 +240,18 @@
"StatusPickerActionNone": "tidak ada ",
"StatusPickerActionSave": "simpan ",
"StatusPickerActionCancel": "batal ",
"StatusPickerModeHint": "plan · work · operate",
"StatusPickerModelHint": "ID model yang Anda kirimi",
"StatusPickerCostHint": "total berjalan sesi ini",
"StatusPickerCacheHint": "% prompt yang dilayani dari cache",
"StatusPickerContextPercentHint": "token terpakai / jendela konteks model",
"StatusPickerTokensHint": "token keluaran dari giliran aktif atau terakhir",
"StatusPickerBalanceHint": "sisa kredit prabayar dari penyedia aktif",
"StatusPickerSessionMetricsHint": "waktu ke token pertama dan laju keluaran",
"StatusPickerTtftHint": "rata-rata tunggu token pertama",
"StatusPickerOutputRateHint": "rata-rata tok/s, termasuk tunggu token pertama",
"StatusPickerWorkspaceHint": "direktori tempat sesi ini menulis",
"StatusPickerGitBranchHint": "cabang tempat commit berikutnya mendarat",
"HotbarSetupTitle": " Penyiapan Hotbar ",
"HotbarSetupSourceApp": "app",
"HotbarSetupSourceSlash": "slash",
+12
View File
@@ -240,6 +240,18 @@
"StatusPickerActionNone": "なし ",
"StatusPickerActionSave": "保存 ",
"StatusPickerActionCancel": "キャンセル ",
"StatusPickerModeHint": "plan · work · operate",
"StatusPickerModelHint": "送信先モデルの ID",
"StatusPickerCostHint": "このセッションの累計コスト",
"StatusPickerCacheHint": "プロンプトがキャッシュから返る割合",
"StatusPickerContextPercentHint": "使用トークン / モデルのコンテキストウィンドウ",
"StatusPickerTokensHint": "現在または前ターンの出力トークン",
"StatusPickerBalanceHint": "アクティブなプロバイダーの残り前払いクレジット",
"StatusPickerSessionMetricsHint": "最初のトークンまでの時間と出力レート",
"StatusPickerTtftHint": "最初のトークンまでの平均待ち時間",
"StatusPickerOutputRateHint": "平均 tok/s(最初のトークン待ちを含む)",
"StatusPickerWorkspaceHint": "このセッションが書き込むディレクトリ",
"StatusPickerGitBranchHint": "次のコミットが乗るブランチ",
"HotbarSetupTitle": " Hotbar設定 ",
"HotbarSetupSourceApp": "アプリ",
"HotbarSetupSourceSlash": "コマンド",
+12
View File
@@ -240,6 +240,18 @@
"StatusPickerActionNone": "없음 ",
"StatusPickerActionSave": "저장 ",
"StatusPickerActionCancel": "취소 ",
"StatusPickerModeHint": "계획 · 실행 · 운영",
"StatusPickerModelHint": "요청을 보낼 모델 ID",
"StatusPickerCostHint": "이 세션의 누적 비용",
"StatusPickerCacheHint": "캐시에서 제공된 프롬프트 비율",
"StatusPickerContextPercentHint": "사용 토큰 / 모델 컨텍스트 창",
"StatusPickerTokensHint": "현재 또는 마지막 턴의 출력 토큰",
"StatusPickerBalanceHint": "활성 제공자의 남은 선불 크레딧",
"StatusPickerSessionMetricsHint": "첫 토큰까지 걸리는 시간과 출력 속도",
"StatusPickerTtftHint": "첫 토큰까지의 평균 대기 시간",
"StatusPickerOutputRateHint": "평균 tok/s (첫 토큰 대기 포함)",
"StatusPickerWorkspaceHint": "이 세션이 쓰는 디렉터리",
"StatusPickerGitBranchHint": "다음 커밋이 올라갈 브랜치",
"HotbarSetupTitle": " 핫바 설정 ",
"HotbarSetupSourceApp": "앱",
"HotbarSetupSourceSlash": "슬래시",
+12
View File
@@ -240,6 +240,18 @@
"StatusPickerActionNone": "nenhum ",
"StatusPickerActionSave": "salvar ",
"StatusPickerActionCancel": "cancelar ",
"StatusPickerModeHint": "plan · work · operate",
"StatusPickerModelHint": "o ID do modelo para onde você envia",
"StatusPickerCostHint": "total acumulado desta sessão",
"StatusPickerCacheHint": "% do prompt servido pelo cache",
"StatusPickerContextPercentHint": "tokens usados / janela de contexto do modelo",
"StatusPickerTokensHint": "tokens de saída do turno atual ou anterior",
"StatusPickerBalanceHint": "crédito pré-pago restante do provedor ativo",
"StatusPickerSessionMetricsHint": "tempo até o primeiro token e taxa de saída",
"StatusPickerTtftHint": "espera média pelo primeiro token",
"StatusPickerOutputRateHint": "média de tok/s, incluindo a espera pelo primeiro token",
"StatusPickerWorkspaceHint": "diretório em que esta sessão grava",
"StatusPickerGitBranchHint": "branch onde o próximo commit cai",
"HotbarSetupTitle": " Configuração da Hotbar ",
"HotbarSetupSourceApp": "app",
"HotbarSetupSourceSlash": "comando",
+12
View File
@@ -240,6 +240,18 @@
"StatusPickerActionNone": "ничего ",
"StatusPickerActionSave": "сохранить ",
"StatusPickerActionCancel": "отмена ",
"StatusPickerModeHint": "plan · work · operate",
"StatusPickerModelHint": "ID модели, на который вы отправляете",
"StatusPickerCostHint": "текущая сумма за этот сеанс",
"StatusPickerCacheHint": "% промпта, отданного из кэша",
"StatusPickerContextPercentHint": "использованные токены / контекстное окно модели",
"StatusPickerTokensHint": "выходные токены текущего или последнего хода",
"StatusPickerBalanceHint": "остаток предоплаченного кредита активного провайдера",
"StatusPickerSessionMetricsHint": "время до первого токена и скорость вывода",
"StatusPickerTtftHint": "среднее ожидание первого токена",
"StatusPickerOutputRateHint": "средняя скорость tok/s с ожиданием первого токена",
"StatusPickerWorkspaceHint": "каталог, в который пишет этот сеанс",
"StatusPickerGitBranchHint": "ветка, куда попадёт следующий коммит",
"HotbarSetupTitle": " Настройка Hotbar ",
"HotbarSetupSourceApp": "прил.",
"HotbarSetupSourceSlash": "слеш",
+12
View File
@@ -240,6 +240,18 @@
"StatusPickerActionNone": "нічого ",
"StatusPickerActionSave": "зберегти ",
"StatusPickerActionCancel": "скасувати ",
"StatusPickerModeHint": "plan · work · operate",
"StatusPickerModelHint": "ID моделі, на яку ви надсилаєте",
"StatusPickerCostHint": "поточна сума за цей сеанс",
"StatusPickerCacheHint": "% промпту, відданого з кешу",
"StatusPickerContextPercentHint": "використані токени / контекстне вікно моделі",
"StatusPickerTokensHint": "вихідні токени поточного або останнього ходу",
"StatusPickerBalanceHint": "залишок передплаченого кредиту активного провайдера",
"StatusPickerSessionMetricsHint": "час до першого токена та швидкість виводу",
"StatusPickerTtftHint": "середнє очікування першого токена",
"StatusPickerOutputRateHint": "середня швидкість tok/s з очікуванням першого токена",
"StatusPickerWorkspaceHint": "каталог, у який пише цей сеанс",
"StatusPickerGitBranchHint": "гілка, куди потрапить наступний коміт",
"HotbarSetupTitle": " Налаштування Hotbar ",
"HotbarSetupSourceApp": "app",
"HotbarSetupSourceSlash": "slash",
+12
View File
@@ -240,6 +240,18 @@
"StatusPickerActionNone": "không ",
"StatusPickerActionSave": "lưu ",
"StatusPickerActionCancel": "huỷ ",
"StatusPickerModeHint": "plan · work · operate",
"StatusPickerModelHint": "ID model bạn sẽ gửi tới",
"StatusPickerCostHint": "tổng tích lũy của phiên này",
"StatusPickerCacheHint": "% prompt được phục vụ từ bộ nhớ đệm",
"StatusPickerContextPercentHint": "token đã dùng / cửa sổ ngữ cảnh model",
"StatusPickerTokensHint": "token đầu ra của lượt hiện tại hoặc gần nhất",
"StatusPickerBalanceHint": "tín dụng trả trước còn lại của nhà cung cấp đang dùng",
"StatusPickerSessionMetricsHint": "thời gian đến token đầu tiên và tốc độ đầu ra",
"StatusPickerTtftHint": "thời gian chờ trung bình cho token đầu tiên",
"StatusPickerOutputRateHint": "tok/s trung bình, gồm cả chờ token đầu tiên",
"StatusPickerWorkspaceHint": "thư mục phiên này ghi vào",
"StatusPickerGitBranchHint": "nhánh mà commit tiếp theo sẽ vào",
"HotbarSetupTitle": " Thiết lập Hotbar ",
"HotbarSetupSourceApp": "ứng dụng",
"HotbarSetupSourceSlash": "lệnh",
+12
View File
@@ -240,6 +240,18 @@
"StatusPickerActionNone": "无 ",
"StatusPickerActionSave": "保存 ",
"StatusPickerActionCancel": "取消 ",
"StatusPickerModeHint": "plan · work · operate",
"StatusPickerModelHint": "发送请求所用的模型 ID",
"StatusPickerCostHint": "本会话的累计费用",
"StatusPickerCacheHint": "提示词由缓存提供的比例",
"StatusPickerContextPercentHint": "已用 token / 模型上下文窗口",
"StatusPickerTokensHint": "当前或上一回合的输出 token",
"StatusPickerBalanceHint": "当前提供商剩余的预付费额度",
"StatusPickerSessionMetricsHint": "首 token 时间与输出速率",
"StatusPickerTtftHint": "首个 token 的平均等待时间",
"StatusPickerOutputRateHint": "平均 tok/s,含首 token 等待",
"StatusPickerWorkspaceHint": "本会话写入的目录",
"StatusPickerGitBranchHint": "下次提交所在的分支",
"HotbarSetupTitle": " Hotbar 设置 ",
"HotbarSetupSourceApp": "应用",
"HotbarSetupSourceSlash": "命令",
+12
View File
@@ -238,6 +238,18 @@
"StatusPickerActionNone": "無 ",
"StatusPickerActionSave": "儲存 ",
"StatusPickerActionCancel": "取消 ",
"StatusPickerModeHint": "plan · work · operate",
"StatusPickerModelHint": "傳送請求所用的模型 ID",
"StatusPickerCostHint": "本工作階段的累計費用",
"StatusPickerCacheHint": "提示詞由快取提供的比例",
"StatusPickerContextPercentHint": "已用 token / 模型上下文視窗",
"StatusPickerTokensHint": "目前或上一回合的輸出 token",
"StatusPickerBalanceHint": "目前供應商剩餘的預付額度",
"StatusPickerSessionMetricsHint": "首 token 時間與輸出速率",
"StatusPickerTtftHint": "首個 token 的平均等待時間",
"StatusPickerOutputRateHint": "平均 tok/s,含首 token 等待",
"StatusPickerWorkspaceHint": "本工作階段寫入的目錄",
"StatusPickerGitBranchHint": "下次提交所在的分支",
"HotbarSetupTitle": " Hotbar 設定 ",
"HotbarSetupSourceApp": "應用",
"HotbarSetupSourceSlash": "斜線指令",
+24
View File
@@ -220,6 +220,18 @@ pub enum MessageId {
StatusPickerActionNone,
StatusPickerActionSave,
StatusPickerActionCancel,
StatusPickerModeHint,
StatusPickerModelHint,
StatusPickerCostHint,
StatusPickerCacheHint,
StatusPickerContextPercentHint,
StatusPickerTokensHint,
StatusPickerBalanceHint,
StatusPickerSessionMetricsHint,
StatusPickerTtftHint,
StatusPickerOutputRateHint,
StatusPickerWorkspaceHint,
StatusPickerGitBranchHint,
// Hotbar setup wizard chrome and validation.
HotbarSetupTitle,
HotbarSetupSourceApp,
@@ -2897,6 +2909,18 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::StatusPickerActionNone,
MessageId::StatusPickerActionSave,
MessageId::StatusPickerActionCancel,
MessageId::StatusPickerModeHint,
MessageId::StatusPickerModelHint,
MessageId::StatusPickerCostHint,
MessageId::StatusPickerCacheHint,
MessageId::StatusPickerContextPercentHint,
MessageId::StatusPickerTokensHint,
MessageId::StatusPickerBalanceHint,
MessageId::StatusPickerSessionMetricsHint,
MessageId::StatusPickerTtftHint,
MessageId::StatusPickerOutputRateHint,
MessageId::StatusPickerWorkspaceHint,
MessageId::StatusPickerGitBranchHint,
MessageId::HotbarSetupTitle,
MessageId::HotbarSetupSourceApp,
MessageId::HotbarSetupSourceSlash,
+24 -3
View File
@@ -244,6 +244,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`/new` refused. Thanks @luestr for reporting the refusal in #6981; this is
one way to reach it, and the report stays open until the blocker that
0.10.2 names confirms its cause (Refs #6981).
- DeepSeek on the Responses route (the `deepseek-flash` default) no longer
fails with `400 reasoning_text must be passed back` when a session replays a
tool call that has no stored DeepSeek reasoning: a turn that was interrupted,
ran with thinking off, or came from another provider. Codewhale now sends the
same `(reasoning omitted)` placeholder the Chat route already uses, before
that turn's tool calls. A live probe on 2026-10-17 returned 400 without it and
200 with it. On Chat, DeepSeek enforces this only for tool-call turns, which
were already covered (Refs CW-7450).
- At reasoning effort `max`, the first-party DeepSeek route now asks for up to
131,072 output tokens, DeepSeek's documented default at that effort, instead
of 65,536. Every other provider and effort keeps 65,536, and explicit
overrides and route limits still apply (Refs CW-7450).
- `codewhale exec --output-format stream-json` adds the provider's
`finish_reason` to each `turn_usage` record, so a tool-call stop can be told
apart from an output-limit stop or a provider abort. The field is omitted
when the provider reports none (Refs CW-7450).
- The `/statusline` picker's item hints are localized in all 15 UI languages
instead of always showing English.
## [0.10.2] - 2026-10-13
@@ -302,9 +320,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
remain in place. Windows Terminal IME preedit is still controlled by the
terminal; native IME retesting remains open (#2323).
- Model lists, resolution, picker controls and Runtime facts use the same
endpoint-scoped catalog facts. Cost estimates preserve complete price tiers,
calculate exact decimal amounts per request, and freeze rates at dispatch.
- `codewhale model list`, the model picker, sub-agent model validation and
Runtime facts read the same endpoint-scoped catalog facts. `model resolve`
and `model set`, the app-server model registry and route admission still
read the static registry; moving them is tracked in #6396. Cost estimates
preserve complete price tiers, calculate exact decimal amounts per request,
and freeze rates at dispatch.
Missing rates or required usage breakdowns stay unpriced; public prices do
not establish account access. The offline seed records its source snapshot
and keeps absent default wire IDs as dated compatibility fallbacks. Refs #6396.
+10 -4
View File
@@ -2928,8 +2928,11 @@ impl CodewhaleClient {
let outbound_dialect = WireDialect::from_wire_format(self.wire_format);
role_placement::reject_unsupported_roles(&request.messages, outbound_dialect)?;
let clamp_output_cap = |mut request: MessageRequest, route_limits: Option<RouteLimits>| {
let route_cap =
self.effective_max_output_tokens_with_limits(&request.model, route_limits);
let route_cap = self.effective_max_output_tokens_with_limits(
&request.model,
route_limits,
request.reasoning_effort.as_deref(),
);
if request.max_tokens > route_cap {
tracing::debug!(
requested_max_tokens = request.max_tokens,
@@ -3200,7 +3203,7 @@ impl CodewhaleClient {
.ok()
.and_then(|candidate| crate::route_budget::known_route_limits(candidate.limits()))
};
self.effective_max_output_tokens_with_limits(requested_model, route_limits)
self.effective_max_output_tokens_with_limits(requested_model, route_limits, None)
}
#[must_use]
@@ -3208,12 +3211,15 @@ impl CodewhaleClient {
&self,
requested_model: &str,
route_limits: Option<RouteLimits>,
reasoning_effort: Option<&str>,
) -> u32 {
let wire_model = self.wire_model_for_route(requested_model);
crate::route_budget::effective_max_output_tokens_for_route(
crate::route_budget::effective_max_output_tokens_for_turn(
self.api_provider,
&wire_model,
route_limits,
None,
reasoning_effort,
)
}
+14
View File
@@ -19,6 +19,7 @@ use codewhale_models::{
OpaqueReasoningState, StreamEvent, Tool, Usage,
};
use super::chat::REASONING_REPLAY_PLACEHOLDER;
use super::prepared::WireDialect;
use super::role_placement::{RolePlacement, role_placement};
use super::wire::{extract_sse_data_value, next_sse_line, push_sse_event_data};
@@ -874,6 +875,7 @@ pub(super) fn convert_messages_to_responses_input(
}
}
RolePlacement::Assistant | RolePlacement::InterruptedAssistant => {
let mut reasoning_emitted = false;
for block in &msg.content {
match block {
ContentBlock::Text { text, .. } => {
@@ -898,6 +900,16 @@ pub(super) fn convert_messages_to_responses_input(
ContentBlock::ToolUse {
id, name, input, ..
} => {
if is_deepseek && !reasoning_emitted {
items.push(json!({
"type": "reasoning",
"content": [{
"type": "reasoning_text",
"text": REASONING_REPLAY_PLACEHOLDER,
}],
}));
reasoning_emitted = true;
}
let (call_id, _item_id) = parse_tool_use_id(id);
let mut item = json!({
"type": "function_call",
@@ -931,6 +943,7 @@ pub(super) fn convert_messages_to_responses_input(
item["id"] = json!(id);
}
items.push(item);
reasoning_emitted = true;
}
} else if is_deepseek {
items.push(json!({
@@ -940,6 +953,7 @@ pub(super) fn convert_messages_to_responses_input(
"text": thinking,
}],
}));
reasoning_emitted = true;
}
}
_ => {}
+20 -15
View File
@@ -1,5 +1,6 @@
//! Configuration loading and defaults for codewhale.
use std::borrow::Cow;
use std::collections::{BTreeMap, HashMap};
use std::fs;
use std::path::{Path, PathBuf};
@@ -8,6 +9,7 @@ use std::time::SystemTime;
use anyhow::{Context, Result};
use codewhale_execpolicy::ExecPolicyEngine;
use codewhale_localization::{Locale, MessageId, tr};
use serde::{Deserialize, Serialize};
use serde_json::json;
#[cfg(unix)]
@@ -1580,21 +1582,24 @@ impl StatusItem {
/// One-line hint shown beside the label so the user knows what each item
/// surfaces without having to toggle it on first.
#[must_use]
pub fn hint(self) -> &'static str {
match self {
StatusItem::Mode => "plan · work · operate",
StatusItem::Model => "the model id you'll send to",
StatusItem::Cost => "running total for this session",
StatusItem::Cache => "% of prompt served from cache",
StatusItem::ContextPercent => "tokens used / model context window",
StatusItem::Tokens => "output tokens of the live or last turn",
StatusItem::Balance => "remaining prepaid credit from the active provider",
StatusItem::SessionMetrics => "time to first token and output rate",
StatusItem::Ttft => "average wait for the first token",
StatusItem::OutputRate => "average tok/s, including first-token wait",
StatusItem::Workspace => "directory this session writes to",
StatusItem::GitBranch => "branch the next commit lands on",
}
pub fn hint(self, locale: Locale) -> Cow<'static, str> {
tr(
locale,
match self {
StatusItem::Mode => MessageId::StatusPickerModeHint,
StatusItem::Model => MessageId::StatusPickerModelHint,
StatusItem::Cost => MessageId::StatusPickerCostHint,
StatusItem::Cache => MessageId::StatusPickerCacheHint,
StatusItem::ContextPercent => MessageId::StatusPickerContextPercentHint,
StatusItem::Tokens => MessageId::StatusPickerTokensHint,
StatusItem::Balance => MessageId::StatusPickerBalanceHint,
StatusItem::SessionMetrics => MessageId::StatusPickerSessionMetricsHint,
StatusItem::Ttft => MessageId::StatusPickerTtftHint,
StatusItem::OutputRate => MessageId::StatusPickerOutputRateHint,
StatusItem::Workspace => MessageId::StatusPickerWorkspaceHint,
StatusItem::GitBranch => MessageId::StatusPickerGitBranchHint,
},
)
}
/// Editable items in display order. Legacy combined metrics parse but
@@ -392,6 +392,7 @@ impl Engine {
// fail by name when there is none) instead of passing for a finished
// answer or being re-requested unchanged. The diagnostics above keep
// the reason the provider sent.
let provider_finish_reason = stop_reason.clone();
let stop_reason = if stop_contradicts_output_ceiling(
stop_reason.as_deref(),
usage_reported.then_some(usage.output_tokens),
@@ -448,6 +449,7 @@ impl Engine {
u64::try_from(request_dispatched_at.elapsed().as_millis())
.unwrap_or(u64::MAX),
),
finish_reason: provider_finish_reason,
})
.await;
}
@@ -245,6 +245,14 @@ impl Engine {
}
}
// Resolve `auto` reasoning_effort to a concrete tier (#663).
let effective_reasoning_effort = resolve_auto_effort(
self.session.reasoning_effort.as_deref(),
self.api_provider,
&self.api_config.active_route_base_url(),
&self.config.model,
);
// The guard measures what the compaction gate measures: the honest
// estimate, lifted to the provider's last bill plus the growth
// since it. The ×1.5-inflated overflow estimate, compared against
@@ -293,6 +301,7 @@ impl Engine {
&self.session.model,
self.active_route_limits,
turn.max_output_tokens,
effective_reasoning_effort.as_deref(),
),
reserved_response_headroom_tokens = budget.output_cap_tokens,
safety_headroom_tokens = crate::context_budget::CONTEXT_HEADROOM_TOKENS,
@@ -386,18 +395,6 @@ impl Engine {
// model sees compile errors before its next reasoning step.
self.flush_pending_lsp_diagnostics().await;
// Build the request. Tool selection goes through the same
// helper that seeded this turn and that `/preview-request`
// reports, so a deferred tool activated mid-turn is reflected
// identically in both places.
// Resolve `auto` reasoning_effort to a concrete tier (#663).
let effective_reasoning_effort = resolve_auto_effort(
self.session.reasoning_effort.as_deref(),
self.api_provider,
&self.api_config.active_route_base_url(),
&self.config.model,
);
// Check prefix-cache stability before building the request.
// This detects system-prompt or tool-set drift that would
// invalidate DeepSeek's KV prefix cache for this turn.
@@ -549,6 +546,10 @@ impl Engine {
// `take` is what keeps this request-scoped: the nudge is spent
// here and never reaches `self.session.messages`.
let request_nudge = progress.reasoning_only_nudge.take();
// Build the request. Tool selection goes through the same
// helper that seeded this turn and that `/preview-request`
// reports, so a deferred tool activated mid-turn is reflected
// identically in both places.
let mut request = prepare_primary_turn_request(PrimaryTurnRequest {
model: self.session.model.clone(),
messages: {
@@ -566,6 +567,7 @@ impl Engine {
&self.session.model,
self.active_route_limits,
turn.max_output_tokens,
effective_reasoning_effort.as_deref(),
),
system: self.session.system_prompt.clone(),
tools: active_tools.clone(),
@@ -774,6 +776,12 @@ impl Engine {
{
return PhaseResult::Return((TurnOutcomeStatus::Failed, Some("RLM whole round history exceeds the captured route context budget; history was not truncated".into())));
}
let reasoning_effort = resolve_auto_effort(
self.session.reasoning_effort.as_deref(),
self.api_provider,
&self.api_config.active_route_base_url(),
&self.config.model,
);
let mut request = prepare_primary_turn_request(PrimaryTurnRequest {
model: self.session.model.clone(),
messages: self.messages_with_turn_metadata(),
@@ -782,16 +790,12 @@ impl Engine {
&self.session.model,
self.active_route_limits,
turn.max_output_tokens,
reasoning_effort.as_deref(),
),
system: self.session.system_prompt.clone(),
tools: None,
tool_choice: None,
reasoning_effort: resolve_auto_effort(
self.session.reasoning_effort.as_deref(),
self.api_provider,
&self.api_config.active_route_base_url(),
&self.config.model,
),
reasoning_effort,
});
request.max_tokens = request
.max_tokens
+1
View File
@@ -377,6 +377,7 @@ pub enum Event {
/// measured (for example an aggregate REPL child receipt). This is
/// the denominator for effective session-average throughput.
request_ms: Option<u64>,
finish_reason: Option<String>,
},
/// Usage telemetry for a programmatic provider call whose cost is carried
+3
View File
@@ -636,6 +636,7 @@ pub fn event_to_protocol(event: &Event, ids: &ProtocolIds) -> wire::EventMsg {
duration_ms,
first_token_ms,
request_ms,
finish_reason: _,
} => wire::EventMsg::TurnUsage {
max_output_tokens: *max_output_tokens,
thread_id,
@@ -1436,6 +1437,7 @@ mod tests {
duration_ms: 12,
first_token_ms: Some(2),
request_ms: Some(10),
finish_reason: None,
},
"turn_usage",
),
@@ -1539,6 +1541,7 @@ mod tests {
duration_ms: 12,
first_token_ms: Some(3),
request_ms: None,
finish_reason: None,
},
Event::Error {
envelope: ErrorEnvelope {
+5 -1
View File
@@ -1206,7 +1206,10 @@ pub(crate) async fn run_exec_agent(
}
}
Event::TurnUsage {
usage, duration_ms, ..
usage,
duration_ms,
finish_reason,
..
} => {
if output_format == ExecOutputFormat::StreamJson {
turn_usage_seq = turn_usage_seq.saturating_add(1);
@@ -1220,6 +1223,7 @@ pub(crate) async fn run_exec_agent(
prompt_cache_write_tokens: usage.prompt_cache_write_tokens,
reasoning_replay_tokens: usage.reasoning_replay_tokens,
duration_ms,
finish_reason,
})?;
}
}
+2
View File
@@ -13998,6 +13998,8 @@ enum ExecStreamEvent {
#[serde(skip_serializing_if = "Option::is_none")]
reasoning_replay_tokens: Option<u32>,
duration_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
finish_reason: Option<String>,
},
#[serde(rename = "metadata")]
Metadata { meta: Box<ExecStreamMeta> },
@@ -456,6 +456,7 @@
prompt_cache_write_tokens: Some(0),
reasoning_replay_tokens: Some(40),
duration_ms: 1834,
finish_reason: None,
};
let value = exec_stream_value(&event).expect("serializes");
@@ -491,6 +492,7 @@
prompt_cache_write_tokens: None,
reasoning_replay_tokens: None,
duration_ms: 250,
finish_reason: None,
};
let value = exec_stream_value(&event).expect("serializes");
+54 -12
View File
@@ -2,11 +2,17 @@ use codewhale_config::route::RouteLimits;
use crate::config::{ProviderKind, provider_capability};
use crate::context_budget::ContextBudget;
use crate::reasoning_preference::ReasoningEffort;
use codewhale_models::{DEFAULT_COMPACTION_TOKEN_THRESHOLD, context_window_for_model};
/// Safe ordinary API request cap across provider routes.
const API_MAX_OUTPUT_TOKENS: u32 = 65_536;
/// Automatic request cap on a first-party DeepSeek route at max reasoning
/// effort, where DeepSeek documents 128K instead of the 64K thinking-mode
/// default (allowed range 1..=393216).
const DEEPSEEK_MAX_EFFORT_MAX_OUTPUT_TOKENS: u32 = 131_072;
/// Preserve only route limits that came from a concrete offering.
#[must_use]
pub(crate) fn known_route_limits(limits: RouteLimits) -> Option<RouteLimits> {
@@ -28,11 +34,29 @@ pub(crate) fn effective_max_output_tokens_for_turn(
model: &str,
route_limits: Option<RouteLimits>,
allowance: Option<std::num::NonZeroU32>,
reasoning_effort: Option<&str>,
) -> u32 {
let ceiling = effective_max_output_tokens_for_route(provider, model, route_limits);
let ceiling = effective_max_output_tokens_for_route_with_cap(
provider,
model,
route_limits,
automatic_max_output_tokens(provider, reasoning_effort),
);
allowance.map_or(ceiling, |allowance| ceiling.min(allowance.get()))
}
fn automatic_max_output_tokens(provider: ProviderKind, reasoning_effort: Option<&str>) -> u32 {
let max_effort = reasoning_effort.is_some_and(|raw| {
ReasoningEffort::parse_strict(raw)
.is_ok_and(|effort| matches!(effort, ReasoningEffort::Max | ReasoningEffort::Ultra))
});
if provider == ProviderKind::Deepseek && max_effort {
DEEPSEEK_MAX_EFFORT_MAX_OUTPUT_TOKENS
} else {
API_MAX_OUTPUT_TOKENS
}
}
/// Context window for a resolved runtime route.
///
/// Route/offering facts win when known; otherwise this falls back to the
@@ -151,8 +175,13 @@ fn explicit_max_output_tokens_override() -> Option<u32> {
}
/// Effective `max_tokens` for a model before provider/route caps are applied.
#[cfg(test)]
#[must_use]
pub(crate) fn effective_max_output_tokens(model: &str) -> u32 {
effective_max_output_tokens_with_cap(model, API_MAX_OUTPUT_TOKENS)
}
fn effective_max_output_tokens_with_cap(model: &str, automatic_cap: u32) -> u32 {
if let Some(tokens) = explicit_max_output_tokens_override() {
return tokens;
}
@@ -163,25 +192,24 @@ pub(crate) fn effective_max_output_tokens(model: &str) -> u32 {
// 262K/327K self-hosted route for almost its whole context as output before
// it had counted a single input token (#5516/#5518). Keep documented
// ceilings through the normal compatibility intersection, but automatic
// requests start at the ordinary 64K cap unless the operator explicitly
// overrides it. A maximum describes what a provider may allow, not what
// every response should reserve by default.
// requests start at the ordinary cap — 64K, or DeepSeek's documented 128K
// at max effort — unless the operator explicitly overrides it. A maximum
// describes what a provider may allow, not what every response should
// reserve by default.
//
// Provenance for the ceiling (deepseek-v4-flash/pro: 384_000 output):
// - models_dev.bundled.json documents limit.output = 384000.
// - The DS4 provider contract corroborates 384K
// (crates/config/src/model_reference.rs pins max_output 384_000 / "384K").
// - Official DeepSeek API docs confirm the model ids (deepseek-v4-flash ->
// V4-Flash-0731, deepseek-v4-pro -> V4-Pro-0813) but do not publish the
// output ceiling in a machine-readable form; that number remains a
// catalogue-sourced value to re-verify against official docs when they
// publish one (#5373).
// - Official DeepSeek API docs publish the output allowance: 64K by
// default in thinking mode, 128K at `reasoning_effort: max`, in the
// range 1..=393216 (#5373).
if let Some(documented) = codewhale_models::max_output_tokens_for_model(model) {
return documented.min(API_MAX_OUTPUT_TOKENS);
return documented.min(automatic_cap);
}
let window = context_window_for_model(model).unwrap_or(128_000);
(window / 2).min(API_MAX_OUTPUT_TOKENS)
(window / 2).min(automatic_cap)
}
/// Automatic allowance when an exact remote model has no output metadata.
@@ -304,6 +332,20 @@ pub(crate) fn effective_max_output_tokens_for_route(
provider: ProviderKind,
model: &str,
route_limits: Option<RouteLimits>,
) -> u32 {
effective_max_output_tokens_for_route_with_cap(
provider,
model,
route_limits,
API_MAX_OUTPUT_TOKENS,
)
}
fn effective_max_output_tokens_for_route_with_cap(
provider: ProviderKind,
model: &str,
route_limits: Option<RouteLimits>,
automatic_cap: u32,
) -> u32 {
let window = route_context_window_tokens(provider, model, route_limits);
let compatibility_source = output_ceiling_source(provider, model);
@@ -319,7 +361,7 @@ pub(crate) fn effective_max_output_tokens_for_route(
// model-only request larger than half of whatever window is in force also
// yields to the window-relative reservation.
// Explicit requests and documented ceilings retain their existing rules.
let model_only_cap = effective_max_output_tokens(model);
let model_only_cap = effective_max_output_tokens_with_cap(model, automatic_cap);
let window_known = route_limits
.and_then(|limits| limits.context_tokens)
.is_some_and(|tokens| (1..=u64::from(u32::MAX)).contains(&tokens));
+6 -1
View File
@@ -15121,7 +15121,11 @@ impl RuntimeThreadManager {
let route_limits = known_route_limits(route.candidate.limits());
let max_output_tokens = req.max_output_tokens.and_then(|requested| {
std::num::NonZeroU32::new(crate::route_budget::effective_max_output_tokens_for_turn(
provider, &model, route_limits, Some(requested),
provider,
&model,
route_limits,
Some(requested),
reasoning_effort.as_deref(),
))
});
let settings = crate::settings::Settings::load().unwrap_or_default();
@@ -18877,6 +18881,7 @@ impl RuntimeThreadManager {
duration_ms,
first_token_ms,
request_ms,
finish_reason: _,
} => {
self.emit_event(
&thread_id,
+1
View File
@@ -4435,6 +4435,7 @@ pub(crate) async fn run_event_loop(
duration_ms,
first_token_ms,
request_ms,
finish_reason: _,
} => {
// Per-step usage receipt. The session metrics strip
// folds each model call's timing (stream time, TTFT,
+19 -8
View File
@@ -290,15 +290,16 @@ impl ModalView for StatusPickerView {
if is_cursor {
let selected_style = menu_style::selected_row_style();
let line = status_row_text(pointer, mark, item, content.width as usize);
let line =
status_row_text(pointer, mark, item, self.locale, content.width as usize);
lines.push(Line::from(Span::styled(line, selected_style)));
} else {
let label = item.label();
let hint = item.hint();
let hint = item.hint(self.locale);
let prefix = format!(" {pointer} {mark} {label} (");
let truncated_hint = crate::tui::ui_text::semantic_truncate_between_affixes(
&prefix,
hint,
hint.as_ref(),
")",
usize::from(content.width),
);
@@ -328,10 +329,20 @@ fn visible_row_start(total_rows: usize, cursor: usize, visible_rows: usize) -> u
.min(max_start)
}
fn status_row_text(pointer: &str, mark: &str, item: &StatusItem, width: usize) -> String {
fn status_row_text(
pointer: &str,
mark: &str,
item: &StatusItem,
locale: Locale,
width: usize,
) -> String {
let prefix = format!(" {pointer} {mark} {} (", item.label());
let mut text =
crate::tui::ui_text::semantic_truncate_with_affixes(&prefix, item.hint(), ")", width);
let mut text = crate::tui::ui_text::semantic_truncate_with_affixes(
&prefix,
item.hint(locale).as_ref(),
")",
width,
);
let current_width = text.width();
if current_width < width {
text.push_str(&" ".repeat(width - current_width));
@@ -494,14 +505,14 @@ mod tests {
#[test]
fn selected_row_text_fills_available_width() {
let text = status_row_text("▸", "[ ]", &StatusItem::Cache, 40);
let text = status_row_text("▸", "[ ]", &StatusItem::Cache, Locale::En, 40);
assert_eq!(text.width(), 40);
assert!(text.starts_with(" ▸ [ ] Prompt cache hit rate"));
}
#[test]
fn selected_row_text_semantically_truncates_hint_at_narrow_width() {
let text = status_row_text("▸", "[ ]", &StatusItem::Cache, 44);
let text = status_row_text("▸", "[ ]", &StatusItem::Cache, Locale::En, 44);
assert_eq!(text.width(), 44);
// Cut at a word boundary, never mid-word.
assert!(text.contains("% of…"), "{text:?}");
+2
View File
@@ -213,6 +213,8 @@ request (turn-step) when the provider reported usage for that call:
`completion_tokens_details` / Responses `output_tokens_details`; Anthropic
does not report a thinking-token count). `reasoning_replay_tokens` is a
client-side estimate for DeepSeek V4 interleaved-thinking replays.
- `finish_reason` is the provider's stop reason for that step (for example
`stop`, `tool_calls`, or `length`), omitted when the provider reported none.
- When a provider reports no usage at all for a call, the whole event is
skipped for that call. Latency/convergence analysis should sum
`turn_usage` events instead of inferring per-step tokens from wall time;
+1
View File
@@ -104,6 +104,7 @@ Runtime 执行账本持久化的是 worker 自己的事件流,而不是另一
- `turn` 是这次 exec 运行内该模型调用的序号,从 1 开始;`input_tokens`、`output_tokens` 和 `duration_ms` 始终存在。
- 提供商没有报告的可选 token 字段会被**省略**——绝不输出为 null,也绝不用 0 回填。字段名与终态 `metadata` 回执保持一致:`prompt_cache_hit_tokens` 是提供商的缓存读取计数(Anthropic 的 `cache_read_input_tokens`),`prompt_cache_write_tokens` 是缓存创建计数(`cache_creation_input_tokens`)。`reasoning_tokens` 只出现在会报告它的提供商路径上(OpenAI 兼容的 `completion_tokens_details` / Responses 的 `output_tokens_details`;Anthropic 不报告思考 token 计数)。`reasoning_replay_tokens` 是对 DeepSeek V4 交错思考(interleaved-thinking)重放的客户端估算值。
- `finish_reason` 是该步骤提供商报告的停止原因(例如 `stop`、`tool_calls` 或 `length`),提供商未报告时省略。
- 当提供商对某次调用完全没有报告用量时,该次调用的整个事件都会被跳过。做延迟/收敛分析时,应当对 `turn_usage` 事件求和,而不是根据墙钟时间去推断每一步的 token;终态 `metadata` 回执仍然携带累计总数。
## 与 Claude Code 的收敛(#2972)