Skip to content

fix: preserve prompt cache tokens across providers and salvage streams on terminal finish_reason - #293

Open
longlixiao-ong wants to merge 5 commits into
caigee-cmd:mainfrom
longlixiao-ong:fix/cache-and-updater-issues
Open

longlixiao-ong wants to merge 5 commits into
caigee-cmd:mainfrom
longlixiao-ong:fix/cache-and-updater-issues

Conversation

@longlixiao-ong

Copy link
Copy Markdown

Summary

  • Fix prompt cache token extraction and passthrough for Trae, Qoder, and WorkBuddy providers in both streaming and non-streaming modes.
  • Support \cache_read_tokens, \prompt_cache_hit_tokens\ (DeepSeek format), and \prompt_tokens_details.cached_tokens\ across provider adapters and the OpenAI stream relay.
  • Salvage upstream streams that cleanly finish with a terminal \ inish_reason\ (\stop, \length, \ ool_calls) before EOF without a literal [DONE]\ frame, preventing downstream proxies and clients from experiencing artificial 502 incomplete stream errors.

Test plan

  • \go test ./...\
  • \go vet ./...\
  • \cd worker && npm test\
  • Added \changelog/unreleased/prompt-cache-and-stream-salvage.md\
  • No tokens, auth blobs, raw captures, or host details in the diff

@caigee-cmd caigee-cmd left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

审查结论:Approved(我已替你修掉 3 处会阻塞或影响发布的问题,见下)

这是四个 PR 里内容最好的一个。salvage 逻辑和缓存 token 回退链都写得很扎实,我先说为什么认可,再说我改了什么。

认可的部分

salvage 的判定是安全的。 classifyStreamSSEError 在帧级先拦错误帧,只有干净终态才补 [DONE];条件要求 stats.FinishReason != "" 且已有 token 或事件,不会把空流误判为成功。这消掉的是一整类虚假 502 —— 上游发了 finish_reason 就直接 EOF、而严格的 [DONE] 检查会判流不完整。

stats.FinishReason 的赋值顺序是对的。 在 ParseStreamUsageLine 用 stats 覆盖局部变量之前先写入,且覆盖时又回传 usage.FinishReason = stats.FinishReason,不会丢。这个细节容易写错。

devin 的 auth 分类扩展有真实依据。 isDevinAuthError 覆盖的是 Codeium Connect-RPC 和 seat management 的真实错误形状(permission_denied + an internal error occurred 是过期 session token 的表现),并且 Classify 的 case 顺序保证 isDevinMCPConfigDenial 仍然最先命中 —— MCP 工具转储造成的 permission_denied 不会被误升级成 auth、不会把账号错误地冷却。测试还带了一个负向 case(bare permission_denied 仍归 invalid_request),这是对的。

normalizeTraeUsage 合成 total_tokens 解决的是 Trae 不回总 token 导致用量统计缺一项的问题,方向正确。

我修掉的 3 处

1. deploy/docker-compose.yml — 宿主特定拓扑,对公共用户是破坏性的(最重要)

原 diff:

-    build:
-      context: ..
-      dockerfile: deploy/Dockerfile
+    networks:
+      - default
+      - new-api-net
+  new-api-net:
+    external: true
+    name: new-api-deploy_default

删掉本地 build: 段意味着别人 docker compose up 时不会构建镜像;而引入 external: true 的 new-api-net(name: new-api-deploy_default)更严重 —— 该网络不存在时 docker compose up 直接失败。这是你自己宿主机上配合 new-api 网关的拓扑,PR 描述里对此只字未提。

我已把 deploy/docker-compose.yml 恢复成 main 的原样。你自己的部署覆盖请放到 deploy/docker-compose.override.yml(该文件已被 .gitignore,正是为本地覆盖准备的),不要进公共仓库。

2. internal/providers/trae/client.go — gofmt 不干净

usage 结构体的 tag 对齐不符合 gofmt(CI 不跑 gofmt -l 所以没拦住)。已 gofmt -w 修好,现在 gofmt -l 只剩 internal/store/migrations.go(main 既有基线,与本 PR 无关)。

3. changelog fragment — 20 个反斜杠会原样渲染

changelog/unreleased/prompt-cache-and-stream-salvage.md 里 \cache_read_tokens\、\[DONE]\、\finish_reason\ 这种写法,反斜杠会在生成的 release notes 里原样显示。显然是想用反引号包裹代码标识符,已全部改为 `...`。release-notes.py validate 仍 exit 0。

另外两点(不阻塞,供参考)

  • 这个分支落后 main 一个提交(f0f05a8,v0.6.21 changelog 归档)。它自带的 checkin-row-align.md 和 devin-rate-limit-reset.md 两个 fragment 已被 main 归档进 v0.6.21,合并后会自动清掉,不会重复发布。它带的 frontend 侧改动则会被 main 的 #294/#290/#291 完全取代 —— 我核对过 merge 后的 blob 与 main 一致,所以保留 main 的 frontend 源码和 internal/webui/static 静态资源即可,不需要额外处理。
  • head commit 的 CI 从未验证过(最后一次成功 CI 停在 ce173cb,head 已是 27585ba7)。我已代为批准了 fork 的 pending workflow run,CI 正在跑。合并前我会等它出结果。

验证

我打上上述修正后完整跑过:go build ./...、go test ./...(全量通过)、go vet ./...、gofmt -l(仅 main 基线)、release-notes.py validate(exit 0),以及 -race 覆盖 gateway / executor / providers/... / runtime / store,全部通过。

CI 通过后我就地合并。

@caigee-cmd caigee-cmd left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for merging main in — CI is now fully green (all 6 jobs). But the three fixes from my earlier review are not in this head yet. Re-checking 27585ba7:

Fix Status on 27585ba7
deploy/docker-compose.yml ❌ still diverges from main — build: block deleted, new-api-net / new-api-deploy_default hardcoded as external: true
gofmt on internal/providers/trae/client.go ❌ still unformatted
changelog \token\ → `token` ❌ still backslash-wrapped

I know these are annoying to hand-apply, so here they are as suggestions you can commit in one click each.

1. deploy/docker-compose.yml — please revert to main's version

This is the one that actually breaks people. Your version deletes the build: stanza, so docker compose build no longer works from this file — it only pulls a prebuilt image. And it declares:

networks:
  default:
  new-api-net:
    external: true
    name: new-api-deploy_default

external: true means Docker will refuse to start the container unless a network literally named new-api-deploy_default already exists on the host. That is a host-specific artifact from whoever ran this, not something that belongs in a repo others clone. Every other user gets network new-api-net declared as external, but could not be found.

Please take main's file byte-for-byte:

    build:
      context: ..
      dockerfile: deploy/Dockerfile

and drop the networks: key on the service plus the whole networks: block at the bottom, and drop the trailing newline difference.

2. internal/providers/trae/client.go — run gofmt -w

CI does not gate on gofmt (only go vet / go test), so this slipped through, but the repo's convention is gofmt-clean. One-line fix:

gofmt -w internal/providers/trae/client.go

It's a struct-tag alignment in the usage struct. No behavior change.

3. changelog/unreleased/prompt-cache-and-stream-salvage.md — use backticks, not \token\

The fragment currently reads \cache_read_tokens\, \prompt_cache_hit_tokens\, \prompt_tokens_details\, \inish_reason\, \[DONE]\. Two problems:

  • The repo convention is backticks. Every archived fragment in CHANGELOG.md uses `finish_reason`, `tool_calls`, `codex`. Zero fragments use the backslash style.
  • \inish_reason\ is actually mangled — the f got eaten, so the released note would literally read inish_reason. Same class of bug as the earlier \token\ round.
- Preserve and normalize prompt cache token metrics (`cache_read_tokens`, `prompt_cache_hit_tokens`, and `prompt_tokens_details`) across Trae, Qoder, and WorkBuddy providers in both streaming and non-streaming responses.
- Gracefully salvage upstream streams that finish with a terminal `finish_reason` before EOF without emitting literal `[DONE]`, preventing artificial 502 incomplete stream errors.
- 修复并标准化 Trae、Qoder 与 WorkBuddy 提供商在流式与非流式响应中的 Prompt 缓存 Token 指标(统一解析并透传 `cache_read_tokens`、`prompt_cache_hit_tokens` 与 `prompt_tokens_details`)。
- 容错处理上游流式响应在带有终态 `finish_reason` 正常结束但未显式发送 `[DONE]` 的边界情况,自动补齐结束帧,避免下游误判为 502 流异常中断。

scripts/release-notes.py validate passes either way, so this is a convention fix rather than a gate failure — but the \inish_reason\ typo means the text is currently wrong, which is why I'm asking for it.


Once those three are in I'll merge. Nothing else is blocking — the substantive change (prompt-cache normalization + stream salvage + Devin error classification) reviewed clean and CI is green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants