Skip to content

feat(anthropic): detect actual model from thinking signature in stream#1223

Merged
ding113 merged 4 commits into
devfrom
feat/anthropic-thinking-signature-model-detection
May 28, 2026
Merged

feat(anthropic): detect actual model from thinking signature in stream#1223
ding113 merged 4 commits into
devfrom
feat/anthropic-thinking-signature-model-detection

Conversation

@ding113

@ding113 ding113 commented May 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • 在 Anthropic 流式响应里解析 signature_delta 内嵌的 protobuf payload(字段路径 [2, 1, 6]),把实际用于思考的模型名作为 actualResponseModel 优先存储 —— 比 message_start 明文 model 更准确。
  • 触发条件:provider 类型 ∈ {claude, claude-auth}requestedModelclaude- 开头。三态 source:signature(签名命中)/fallback_no_signature_with_thinking(开了思考却没拿到签名 → 详情页面亮"无思考签名"badge)/fallback_no_thinking(未开思考,保持原行为)。
  • 新增 ThinkingSignatureModelDetectionSpecialSetting 类型用于审计留痕;UI 仅在详情面板 SummaryTab 的 modelAudit 区块加 badge(列表不动以保持简洁);5 个 dashboard.json(zh-CN/zh-TW/en/ja/ru)各加 noSignatureBadge / noSignatureTooltip 翻译。

Related PRs

关键设计

  • 手写 protobuf 解码:不引第三方库,按字段路径递归;字段路径作为可配参数便于未来 schema 变更。任何异常(损坏 base64 / 截断 / wire-type 错位 / utf-8 解码失败)一律返回 null,绝不抛。
  • 复用现有 SSE/NDJSON 解析:actual-response-model.tsextractJsonChunks 改为 export,新模块共用同一份处理多 data: 行合并、[DONE]event: 边界的能力。
  • 集成点最小侵入:仅 response-handler.ts:2553 流式分支前插入决策代码,非流式分支(line 1450)与错误兜底路径不动(signature_delta 仅存在于流式)。
  • 签名解码失败合并归类(用户决策):无 signature_delta 事件 / 有事件但 base64 损坏 / protobuf 路径解不出,三种情况在 thinkingEnabled 时统一归到 fallback_no_signature_with_thinking,共用同一个 badge 文案。

Test plan

  • bun run typecheck 全绿
  • bun run lint 全绿
  • bun run test 全绿(6119 passed / 13 skipped / 0 failed)
  • bun run test:v1 全绿(覆盖率 88%+)
  • bun run build 生产构建成功(含 standalone copy)
  • 新增 37 个测试用例(protobuf 解码 19 + 业务层 18),包含真实样例 claude-opus-4-7、损坏 base64、字段路径不存在、wire-type 错位、SSE 多事件解析、三态决策树边界
  • 端到端验证(可选):构造含 signature_delta 的 mock SSE,检查 DB message_requests.actual_response_modelspecial_settings

Description enhanced by Claude AI

Greptile Summary

This PR extends the actualResponseModel infrastructure for Anthropic streaming responses by parsing the protobuf payload embedded in signature_delta SSE events (field path [2, 1, 6]) to extract the model name used during thinking — a more reliable source than the plain model field in message_start. A three-state source value (signature / fallback_no_signature_with_thinking / fallback_no_thinking) drives both DB audit persistence and a new "No thinking signature" badge in the SummaryTab detail panel.

  • New modules: thinking-signature-model.ts (zero-dependency, all-null-safe hand-rolled protobuf + base64 decoder) and anthropic-actual-response-model.ts (three-way decision logic, reuses existing extractJsonChunks).
  • Integration: a single insertion point in response-handler.ts (streaming branch only) populates ThinkingSignatureModelDetectionSpecialSetting and overrides finalActualResponseModel; non-streaming and error paths are untouched.
  • i18n + UI: noSignatureBadge / noSignatureTooltip added to all five locale files; badge is rendered in SummaryTab only on fallback_no_signature_with_thinking.

Confidence Score: 5/5

Safe to merge: the change is additive, all new paths are guarded by provider-type and model-prefix checks, and any protobuf/base64 decoding failure silently falls back to the existing message_start model field.

The integration point is minimal (a single insertion before the existing streaming persistence call), all error paths return null and never throw, the old non-Anthropic and non-streaming code paths are untouched, and 37 new tests including a real Anthropic signature fixture confirm the decoder behaves correctly. The two flagged items are design trade-offs explicitly documented in code comments and do not affect correctness of the happy path.

No files require special attention beyond the noted design trade-offs in thinking-signature-model.ts and anthropic-actual-response-model.ts.

Important Files Changed

Filename Overview
src/app/v1/_lib/proxy/thinking-signature-model.ts New module: hand-rolled protobuf decoder + SSE scanner for thinking-signature model extraction. Logic is sound for the current Anthropic proto3 schema; findFirstField aborts on unrecognized wire types rather than skipping, which could silently degrade on future schema changes.
src/app/v1/_lib/proxy/anthropic-actual-response-model.ts Three-way source decision logic is clean; the intentional choice to return source: "fallback_no_thinking" when fallbackModel===null (even for thinking-enabled requests with truncated streams) creates a contradictory audit record but is documented.
src/app/v1/_lib/proxy/response-handler.ts Integration is minimal and correct: thinkingActuallyEnabled comes from isThinkingEnabled(session.request.message) directly, special setting is only added when source is non-null, and finalActualResponseModel correctly falls back to the old path for non-Anthropic providers.
src/types/special-settings.ts New ThinkingSignatureModelDetectionSpecialSetting type added with all required fields; buildSettingKey in special-settings.ts has a matching case.
src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx Badge rendering for "no signature" is correctly gated on source === "fallback_no_signature_with_thinking"; tooltip uses i18n keys that are defined across all 5 locales.
tests/unit/proxy/thinking-signature-model.test.ts 19 unit tests covering real Anthropic signature, truncated input, bad base64, missing fields, wire-type mismatches, and SSE stream parsing; real-world example decodes claude-opus-4-7 correctly.
tests/unit/proxy/anthropic-actual-response-model.test.ts 18 business-layer tests covering all three source states, trigger condition boundaries, and fallback paths.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Streaming response complete] --> B{providerType in claude/claude-auth AND requestedModel starts with claude- or anthropic/}
    B -- No --> C[Old path: extractActualResponseModelForProvider]
    B -- Yes --> D[extractThinkingSignatureModelFromStream scan SSE for signature_delta events]
    D --> E{signature found and decoded?}
    E -- Yes --> F[source = signature, actualResponseModel = decoded model]
    E -- No --> G[extractActualResponseModelForProvider fallback to message_start model]
    G --> H{fallbackModel == null? stream truncated}
    H -- Yes --> I[source = fallback_no_thinking, actualResponseModel = null]
    H -- No --> J{thinkingEnabled?}
    J -- Yes --> K[source = fallback_no_signature_with_thinking, UI badge shown]
    J -- No --> L[source = fallback_no_thinking, no badge]
    F & I & K & L --> M[addSpecialSetting: ThinkingSignatureModelDetection]
    M --> N[updateMessageRequestDetails, actualResponseModel persisted]
    C --> N
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/app/v1/_lib/proxy/thinking-signature-model.ts:142-151
**`findFirstField` aborts on unrecognized wire types instead of skipping**

When `readNextField` returns `null` — which happens for wire types 3/4 (deprecated proto3 groups) as well as any truncated field — `findFirstField` exits the entire loop immediately (`if (!field) return null`). A standards-compliant protobuf parser must skip unknown fields rather than abort, so any protobuf field with an unrecognized wire type appearing *before* field 2 in the outer message would silently prevent model extraction and fall back to `message_start`. Wire types 3/4 don't appear in proto3, so current Anthropic responses are unaffected, but if Anthropic ever adds fields with new wire types before field 2 in a future schema revision the parser will silently degrade with no observable signal.

### Issue 2 of 2
src/app/v1/_lib/proxy/anthropic-actual-response-model.ts:84-88
**`source: "fallback_no_thinking"` when thinking was enabled but stream is empty**

When `fallbackModel === null` (stream so truncated that `message_start` was never delivered), the function returns `source: "fallback_no_thinking"` regardless of the `thinkingEnabled` parameter. This path is reachable when thinking IS enabled — the comment documents the intent (suppress badge to avoid false alerts on empty streams). However, the resulting special-settings audit record will have `source: "fallback_no_thinking"` paired with `thinkingEnabled: true` (set by the caller in `response-handler.ts` line 2560), which is contradictory: consumers reading `source` to infer whether thinking was requested will get a wrong answer for this edge case. A third explicit source value (e.g., `"fallback_unknown"`) or returning `source: null` here would be less ambiguous without changing the badge behaviour.

Reviews (4): Last reviewed commit: "fix(proxy): skip thinking-signature dete..." | Re-trigger Greptile

Use the thinking signature protobuf embedded in Anthropic stream
content_block_delta events to extract the actual model name. This is
more accurate than the message_start plain text when thinking is on.

Three-state detection:
- signature found → use model from signature
- no signature with thinking on → fall back to plain model, warn via UI
- no signature without thinking → plain model (normal)

Add a UI badge and i18n labels for five locales when thinking was
enabled but no signature arrived.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces Anthropic thinking signature model detection, which extracts the actual response model from the protobuf payload inside the signature_delta stream event. It includes fallback mechanisms when the signature is missing or thinking is disabled, adds a 'No thinking signature' warning badge in the dashboard UI, and provides comprehensive unit tests and localization updates. The reviewer feedback suggests relaxing the strict Base64 length validation in thinking-signature-model.ts to support unpadded Base64 strings, preventing potential decoding failures if padding is stripped by proxies.

Comment on lines +95 to +96
// base64 长度必须是 4 的倍数(去 padding 后允许 2/3 位余数,标准 base64 一般要 padding)
if (trimmed.length % 4 !== 0) throw new Error("invalid base64 length");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

强制要求 trimmed.length % 4 === 0 过于严格。在实际应用中,Base64 字符串有时会丢失末尾的填充字符 =(例如被某些中转代理或客户端剥离)。强制要求 4 的倍数会导致这些合法但未填充的 Base64 签名解码失败。

我们可以通过检查去除填充字符后的实际长度来同时支持有填充和无填充的 Base64(在 Base64 编码中,有效字符长度除以 4 的余数不能为 1)。

Suggested change
// base64 长度必须是 4 的倍数(去 padding 后允许 2/3 位余数,标准 base64 一般要 padding)
if (trimmed.length % 4 !== 0) throw new Error("invalid base64 length");
// 支持有填充和无填充的 base64(有效长度除以 4 的余数不能为 1)
const unpaddedLength = trimmed.replace(/=+$/, "");
if (unpaddedLength.length % 4 === 1) throw new Error("invalid base64 length");

@github-actions github-actions Bot added the size/M Medium PR (< 500 lines) label May 28, 2026
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

新增 Anthropic 流式 thinking signature 的严格 protobuf 解码与流中提取,按签名/启用思考/未启用思考三态确定实际响应模型,将检测结果写入特殊设置,并在错误详情 UI 显示“无签名”徽标;同时补充类型、i18n 文案与测试覆盖。

变更详情

思考签名检测与展示完整流程

Layer / File(s) Summary
类型与特殊设置基础设施
src/types/special-settings.ts, src/lib/utils/special-settings.ts
扩展 SpecialSetting 联合类型,新增 ThinkingSignatureModelDetectionSpecialSetting;在 buildSettingKey 中加入该类型的 key 分支,并新增 getThinkingSignatureModelDetectionSpecialSetting 查询函数。
思考签名 protobuf 解析实现
src/app/v1/_lib/proxy/thinking-signature-model.ts
实现 decodeThinkingSignatureModel 从 base64 protobuf 按字段路径逐层遍历并严格 UTF-8 解码终点 bytes;实现 extractThinkingSignatureModelFromStream 从 SSE/NDJSON 文本块提取 signature_delta 并解码首个成功签名;含严格 base64 校验、varint/field/tag 解析与边界保护。
JSON 块解析导出
src/app/v1/_lib/proxy/actual-response-model.ts
extractJsonChunks 导出为公共生成器以支持流文本分块重用。
Anthropic 流式响应模型解析
src/app/v1/_lib/proxy/anthropic-actual-response-model.ts
新增 resolveAnthropicStreamActualResponseModel:优先尝试从 thinking signature 解出模型(并校验合理性),签名无效时回退到 message_start 解析并根据 thinkingEnabled 返回对应 source;新增 isThinkingEnabled 判断函数。
流式响应处理集成
src/app/v1/_lib/proxy/response-handler.ts
在流式处理链路中集成 Anthropic 检测:读取当前请求模型、调用解析器、在命中时写入 thinking_signature_model_detection 特殊设置,并计算最终 actualResponseModel 写入请求详情。
前端错误详情 UI 展示
src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx
读取 thinking signature 特殊设置并计算 showNoSignatureBadge(当 source 为 fallback_no_signature_with_thinking 时);扩展模型审计标题渲染并在命中时显示带 tooltip 的“无签名” Badge,tooltip 文案来自 i18n。
多语言文案更新
messages/{en,ja,ru,zh-CN,zh-TW}/dashboard.json
为仪表板的 modelAudit 区块新增 noSignatureBadgenoSignatureTooltip 文案键;在若干语言中将 arrowPrefix 的表示由转义改为直接字符。
单元测试覆盖
tests/unit/proxy/thinking-signature-model.test.ts, tests/unit/proxy/anthropic-actual-response-model.test.ts
新增测试覆盖 decode/extract 流与 protobuf 解码边界、isThinkingEnabled 与 resolveAnthropicStreamActualResponseModel 的多场景行为与回退路径。

代码评审工作量估算

🎯 3 (中等) | ⏱️ ~25 分钟

可能相关的 PR

  • ding113/claude-code-hub#654: 修改与 error-details-dialog/components/SummaryTab.tsx 相关,可能在相同组件区域存在重叠变更。
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective of detecting the actual model from Anthropic thinking signature in stream responses.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed PR description comprehensively details the changes: protobuf parsing from signature_delta, three-state source logic, new special setting type, i18n additions, and test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/anthropic-thinking-signature-model-detection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: af56afc808

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const anthropicModelDetection = resolveAnthropicStreamActualResponseModel({
providerType: provider.providerType,
requestedModel: currentRequestedModel,
thinkingEnabled: isThinkingEnabled(session.request.message),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the forwarded body for thinking detection

When a final-phase body request filter changes thinking (for example an advanced remove or set op on the thinking path), this reads the stale session.request.message instead of the body that was actually serialized to the upstream; request-filter-engine.applyFinal explicitly mutates the cloned body passed from the forwarder, not the session. In that configuration an Anthropic request can be sent with thinking disabled/removed, but this branch still records fallback_no_signature_with_thinking and shows the “No thinking signature” badge even though the upstream correctly omitted a signature.

Useful? React with 👍 / 👎.

Comment on lines +95 to +96
// base64 长度必须是 4 的倍数(去 padding 后允许 2/3 位余数,标准 base64 一般要 padding)
if (trimmed.length % 4 !== 0) throw new Error("invalid base64 length");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The % 4 !== 0 check is too strict — it rejects valid unpadded base64 strings (lengths ≡ 2 or 3 mod 4). Standard base64 without = padding is common in API responses; e.g., a 6-char signature like "dGVzdA" (encoding 4 bytes) is well-formed but would be silently rejected here and silently fall back to the message_start model. Only a length ≡ 1 mod 4 is always invalid (6 bits cannot encode a full byte).

Suggested change
// base64 长度必须是 4 的倍数(去 padding 后允许 2/3 位余数,标准 base64 一般要 padding)
if (trimmed.length % 4 !== 0) throw new Error("invalid base64 length");
// length ≡ 1 (mod 4) is always invalid (6 bits < 8 bits needed for one byte);
// lengths ≡ 0/2/3 are all valid padded or unpadded base64.
if (trimmed.length % 4 === 1) throw new Error("invalid base64 length");
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/thinking-signature-model.ts
Line: 95-96

Comment:
The `% 4 !== 0` check is too strict — it rejects valid *unpadded* base64 strings (lengths ≡ 2 or 3 mod 4). Standard base64 without `=` padding is common in API responses; e.g., a 6-char signature like `"dGVzdA"` (encoding 4 bytes) is well-formed but would be silently rejected here and silently fall back to the `message_start` model. Only a length ≡ 1 mod 4 is always invalid (6 bits cannot encode a full byte).

```suggestion
  // length ≡ 1 (mod 4) is always invalid (6 bits < 8 bits needed for one byte);
  // lengths ≡ 0/2/3 are all valid padded or unpadded base64.
  if (trimmed.length % 4 === 1) throw new Error("invalid base64 length");
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

source: anthropicModelDetection.source,
extractedModel: anthropicModelDetection.actualResponseModel,
signatureFound: anthropicModelDetection.source === "signature",
thinkingEnabled: anthropicModelDetection.source !== "fallback_no_thinking",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 thinkingEnabled stored in the special setting is derived from source !== "fallback_no_thinking" rather than from the actual request state. This diverges when isThinkingEnabled(session.request.message) = false but a signature_delta is present in the stream (source becomes "signature"), causing the stored thinkingEnabled to be true while the request never enabled thinking — the field's doc comment says it should be "由 isThinkingEnabled(session.request.message) 派生". Future consumers reading this audit field to determine whether thinking was requested would see incorrect data in that edge case. Consider using isThinkingEnabled(session.request.message) directly here.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/response-handler.ts
Line: 2559

Comment:
`thinkingEnabled` stored in the special setting is derived from `source !== "fallback_no_thinking"` rather than from the actual request state. This diverges when `isThinkingEnabled(session.request.message) = false` but a `signature_delta` is present in the stream (source becomes `"signature"`), causing the stored `thinkingEnabled` to be `true` while the request never enabled thinking — the field's doc comment says it should be "由 `isThinkingEnabled(session.request.message)` 派生". Future consumers reading this audit field to determine whether thinking was *requested* would see incorrect data in that edge case. Consider using `isThinkingEnabled(session.request.message)` directly here.

How can I resolve this? If you propose a fix, please make it concise.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

@github-actions github-actions Bot added size/L Large PR (< 1000 lines) and removed size/M Medium PR (< 500 lines) labels May 28, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/v1/_lib/proxy/anthropic-actual-response-model.ts`:
- Around line 22-23: Update the two imports in
anthropic-actual-response-model.ts to use the project path alias instead of
relative paths: replace the relative imports for extractActualResponseModel and
extractThinkingSignatureModelFromStream with alias imports (e.g., starting with
`@/`) so they import from the proxy module via the '`@/app/v1/_lib/proxy/`...'
alias; ensure the symbol names extractActualResponseModel and
extractThinkingSignatureModelFromStream remain unchanged and that import paths
match the project's tsconfig/jsconfig alias mapping.

In `@src/app/v1/_lib/proxy/response-handler.ts`:
- Around line 45-48: The import in response-handler.ts uses a relative path for
the module providing isThinkingEnabled and
resolveAnthropicStreamActualResponseModel; update the import to use the project
path alias so it reads from
"`@/app/v1/_lib/proxy/anthropic-actual-response-model`" instead of the relative
"./anthropic-actual-response-model" thereby following the repository import
guideline and keeping references to isThinkingEnabled and
resolveAnthropicStreamActualResponseModel intact.
- Around line 2551-2560: The persisted thinkingEnabled value is computed
incorrectly from anthropicModelDetection.source (source !==
"fallback_no_thinking"), which can mark thinking as enabled when the source is
"signature" even if the request did not enable thinking; update the
session.addSpecialSetting call in response-handler.ts to use the original
runtime boolean that indicates whether thinking was enabled (the same flag
passed to or used when invoking the detection logic) instead of deriving it from
anthropicModelDetection.source (i.e., replace the computed expression for
thinkingEnabled with the actual isThinking/enabledThinking variable used at the
call site).

In `@src/app/v1/_lib/proxy/thinking-signature-model.ts`:
- Line 14: Replace the relative import of extractJsonChunks from
"./actual-response-model" with the repository path-alias import
"`@/app/v1/_lib/proxy/actual-response-model`"; update the import statement that
references extractJsonChunks so it uses the `@/` alias (keeping the same exported
symbol name) to comply with the TS import convention.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 31c896c6-c67e-4037-aabb-2a30a899daa0

📥 Commits

Reviewing files that changed from the base of the PR and between e0d0978 and af56afc.

📒 Files selected for processing (14)
  • messages/en/dashboard.json
  • messages/ja/dashboard.json
  • messages/ru/dashboard.json
  • messages/zh-CN/dashboard.json
  • messages/zh-TW/dashboard.json
  • src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx
  • src/app/v1/_lib/proxy/actual-response-model.ts
  • src/app/v1/_lib/proxy/anthropic-actual-response-model.ts
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/app/v1/_lib/proxy/thinking-signature-model.ts
  • src/lib/utils/special-settings.ts
  • src/types/special-settings.ts
  • tests/unit/proxy/anthropic-actual-response-model.test.ts
  • tests/unit/proxy/thinking-signature-model.test.ts

Comment thread src/app/v1/_lib/proxy/anthropic-actual-response-model.ts Outdated
Comment thread src/app/v1/_lib/proxy/response-handler.ts Outdated
Comment thread src/app/v1/_lib/proxy/response-handler.ts
Comment thread src/app/v1/_lib/proxy/thinking-signature-model.ts Outdated
throw new Error("invalid base64 alphabet");
}
// base64 长度必须是 4 的倍数(去 padding 后允许 2/3 位余数,标准 base64 一般要 padding)
if (trimmed.length % 4 !== 0) throw new Error("invalid base64 length");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[High] [LOGIC-BUG] base64 length check rejects valid unpadded base64

Why this is a problem: The check trimmed.length % 4 \!== 0 rejects valid unpadded base64 strings (lengths that are 2 or 3 mod 4). Standard base64 without trailing = padding is common in API responses - for example, a 6-char signature like "dGVzdA" (encoding 4 bytes of "test") is well-formed but would be silently rejected here, causing the decoder to fall back to the message_start model name instead of using the signature.

Only a length congruent to 1 mod 4 is always invalid (6 bits cannot encode a full byte).

The inline comment also contradicts the code: it says the decoder allows 2/3 remainder after removing padding, but the code does NOT allow those remainders.

Suggested fix:

// Only length === 1 (mod 4) is always invalid; lengths 0/2/3 are valid padded or unpadded base64.
if (trimmed.length % 4 === 1) throw new Error("invalid base64 length");

source: anthropicModelDetection.source,
extractedModel: anthropicModelDetection.actualResponseModel,
signatureFound: anthropicModelDetection.source === "signature",
thinkingEnabled: anthropicModelDetection.source !== "fallback_no_thinking",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[High] [LOGIC-BUG] thinkingEnabled audit field derived from source instead of request state

Why this is a problem: The thinkingEnabled field stored in the special setting is derived from anthropicModelDetection.source \!== "fallback_no_thinking" (i.e., it is true whenever source is "signature" or "fallback_no_signature_with_thinking"). However, the type doc comment on ThinkingSignatureModelDetectionSpecialSetting says this field should record whether thinking was requested: "由 isThinkingEnabled(session.request.message) 派生". This diverges in the edge case where a signature_deltais present in the stream but the request never enabled thinking (source becomes"signature", so thinkingEnabledis recorded astrue`). Future consumers reading this audit field to determine whether thinking was requested would see incorrect data.

The correct value is already computed 11 lines above as isThinkingEnabled(session.request.message) (passed into resolveAnthropicStreamActualResponseModel).

Suggested fix:

thinkingEnabled: isThinkingEnabled(session.request.message),

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review Summary

This PR adds a three-state model detection pipeline for Anthropic streaming responses via protobuf parsing of signature_delta events. The implementation is well-structured with clean separation of concerns, thorough null-safety, and good test coverage (37 new tests). Two logic issues were identified that should be addressed before merge.

PR Size: L

  • Lines changed: 895 (881 additions, 14 deletions)
  • Files changed: 14

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 2 0 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

High Priority Issues (Should Fix)

  1. base64 length validation too strict (thinking-signature-model.ts:96)
    trimmed.length % 4 \!== 0 rejects valid unpadded base64 (lengths 2 or 3 mod 4). Should be trimmed.length % 4 === 1 to only reject truly invalid lengths. The adjacent comment correctly states "2/3 remainder allowed" but the code contradicts this.

  2. thinkingEnabled audit field derived from source instead of request (response-handler.ts:2559)
    The field records source \!== "fallback_no_thinking" instead of the actual isThinkingEnabled(session.request.message) value. When a signature is present but thinking was not requested, the audit field incorrectly reports thinkingEnabled: true. The correct value is already computed on line 2548 and should be reused directly.

Review Coverage

  • Logic and correctness
  • Security (OWASP Top 10)
  • Error handling
  • Type safety
  • Documentation accuracy
  • Test coverage
  • Code clarity

Automated review by Claude AI

…model

Remove the requestedModel prefix check so the detection works with
model-redirect, Bedrock, and Vertex providers. Validate signature-decoded
model names for length (≤128 chars) and presence of "claude" to reject
spoofed payloads.

Also fix several related issues:
- Accept both standard and URL-safe base64 alphabets, and correct the
  length validation to only reject remainder 1.
- Show the no-signature badge in the UI even when no model field is
  present.
- Return fallback_no_thinking for empty streams to avoid false
  no-signature alerts.
- Record the truthful thinkingEnabled value in audit logs instead of a
  source-derived heuristic.
- Fix SSE chunk joining in tests to ensure proper event boundary blank
  lines.
- Correct full-width punctuation in zh-CN and zh-TW tooltip strings.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/proxy/anthropic-actual-response-model.test.ts`:
- Around line 37-43: The helper function buildSignatureBase64ForModel constructs
length bytes using single-byte values which breaks protobuf varint encoding for
lengths >=128; change it to encode utf8.length, terminal.length, and
middle.length using proper protobuf varint encoding (e.g., a small
encodeVarint(n) that emits one or more bytes with 7-bit payload and MSB
continuation) and use those varint buffers when building terminal/middle/outer
before base64-encoding; update references in buildSignatureBase64ForModel to
call the varint encoder for each length field so decoded protobuf parsing
succeeds for large payloads.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a463a8ea-4386-4bd1-8d48-f46949c24023

📥 Commits

Reviewing files that changed from the base of the PR and between af56afc and a03db97.

📒 Files selected for processing (7)
  • messages/zh-CN/dashboard.json
  • messages/zh-TW/dashboard.json
  • src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx
  • src/app/v1/_lib/proxy/anthropic-actual-response-model.ts
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/app/v1/_lib/proxy/thinking-signature-model.ts
  • tests/unit/proxy/anthropic-actual-response-model.test.ts
✅ Files skipped from review due to trivial changes (1)
  • messages/zh-TW/dashboard.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx
  • messages/zh-CN/dashboard.json
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/app/v1/_lib/proxy/thinking-signature-model.ts

Comment thread tests/unit/proxy/anthropic-actual-response-model.test.ts
@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

- Strip trailing padding from base64 input before checking invalid
  length, so that padded strings like "xxxxx=" are correctly rejected.
- Convert relative imports to @/ path aliases for consistency.
- Update test fixture to encode protobuf length fields with varint.
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Actionable comments posted: 0

Restores a guard on the requested model family (claude- or anthropic/
prefix) so that providers using the Anthropic-compatible API path but
serving non-Anthropic models (e.g. GLM) are not incorrectly classified.
Without this gate, those providers could produce a
fallback_no_signature_with_thinking source even though they never emit
thinking signatures.

Drops the requirement that the signature-decoded model name contain
"claude", leaving only the length check (<= 128 chars). This allows
future Anthropic model families that may drop the "claude-" prefix.

Fixes import order in response-handler.ts after lint:fix.
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Actionable comments posted: 0

@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

@github-actions github-actions Bot added size/XL Extra Large PR (> 1000 lines) and removed size/L Large PR (< 1000 lines) labels May 28, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review Summary

This PR adds Anthropic streaming actual-response-model detection by parsing the protobuf payload embedded in events, recording audit special settings and showing a dashboard warning badge when thinking was enabled but no signature was present.

PR Size: XL

  • Lines changed: 1028 (1013 additions, 15 deletions)
  • Files changed: 14
  • Split suggestions: Consider splitting into (1) core proxy detection + types, (2) dashboard i18n/UI badge, (3) unit tests, to ease review and rollback.

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 0 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Review Coverage

  • Logic and correctness
  • Security (OWASP Top 10)
  • Error handling
  • Type safety
  • Documentation accuracy
  • Test coverage
  • Code clarity

Automated review by Codex AI

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review Summary

No significant issues identified in this PR.

PR Size: XL

  • Lines changed: 1028 (1013 additions, 15 deletions)
  • Files changed: 14
  • Split suggestions: Consider splitting into (1) core proxy detection + types, (2) dashboard i18n/UI badge, (3) unit tests, to ease review and rollback.

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Adequate
  • Code clarity - Good

Automated review by Codex AI

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  • Synced PR #1223 to current head 1e9c4451fc3592bfe1886ee64a445035e4ffc3b9 and re-validated the previously-reported issues; both are now fixed in the diff:
    • src/app/v1/_lib/proxy/thinking-signature-model.ts now supports unpadded/base64url and rejects only len % 4 === 1.
    • src/app/v1/_lib/proxy/response-handler.ts now records thinkingEnabled from isThinkingEnabled(session.request.message) via thinkingActuallyEnabled.
  • Updated size label to match current PR stats: 1013 additions + 15 deletions = 1028 lines ⇒ applied size/XL (removed size/L).
  • Submitted the required PR review summary via gh pr review 1223 --comment (no >=80-confidence issues remaining in changed lines).

Note: bun isn’t available in this runner (bun: command not found), so I couldn’t execute the repo’s bun run test/lint/typecheck locally here.

@ding113
ding113 merged commit d0fe34d into dev May 28, 2026
10 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Claude Code Hub Roadmap May 28, 2026
@github-actions github-actions Bot mentioned this pull request May 28, 2026
ding113 added a commit that referenced this pull request May 28, 2026
* chore: 将 Codex 测试模型升级到 GPT-5.5 (#1221)

Co-authored-by: tesgth032 <tesgth032@users.noreply.github.com>

* feat(anthropic): detect actual model from thinking signature in stream (#1223)

* feat(anthropic): detect model from thinking signature in stream

Use the thinking signature protobuf embedded in Anthropic stream
content_block_delta events to extract the actual model name. This is
more accurate than the message_start plain text when thinking is on.

Three-state detection:
- signature found → use model from signature
- no signature with thinking on → fall back to plain model, warn via UI
- no signature without thinking → plain model (normal)

Add a UI badge and i18n labels for five locales when thinking was
enabled but no signature arrived.

* fix(proxy): drop claude- prefix gate and validate thinking signature model

Remove the requestedModel prefix check so the detection works with
model-redirect, Bedrock, and Vertex providers. Validate signature-decoded
model names for length (≤128 chars) and presence of "claude" to reject
spoofed payloads.

Also fix several related issues:
- Accept both standard and URL-safe base64 alphabets, and correct the
  length validation to only reject remainder 1.
- Show the no-signature badge in the UI even when no model field is
  present.
- Return fallback_no_thinking for empty streams to avoid false
  no-signature alerts.
- Record the truthful thinkingEnabled value in audit logs instead of a
  source-derived heuristic.
- Fix SSE chunk joining in tests to ensure proper event boundary blank
  lines.
- Correct full-width punctuation in zh-CN and zh-TW tooltip strings.

* fix(proxy): strip base64 padding and use @/ imports

- Strip trailing padding from base64 input before checking invalid
  length, so that padded strings like "xxxxx=" are correctly rejected.
- Convert relative imports to @/ path aliases for consistency.
- Update test fixture to encode protobuf length fields with varint.

* fix(proxy): skip thinking-signature detection for non-Anthropic models

Restores a guard on the requested model family (claude- or anthropic/
prefix) so that providers using the Anthropic-compatible API path but
serving non-Anthropic models (e.g. GLM) are not incorrectly classified.
Without this gate, those providers could produce a
fallback_no_signature_with_thinking source even though they never emit
thinking signatures.

Drops the requirement that the signature-decoded model name contain
"claude", leaving only the length check (<= 128 chars). This allows
future Anthropic model families that may drop the "claude-" prefix.

Fixes import order in response-handler.ts after lint:fix.

* fix(proxy): normalize nullable Responses output fields (#1220)

* fix(proxy): 规范化 Responses 输出空值

* fix(proxy): 完善 Responses 输出归一化边界

* fix: return full self user list shape (#1213)

* fix: return full self user list shape

* fix: target self user display loading

* chore: remove unreachable self user guard

* 优化公开状态页 Redis 聚合与轮询性能 (#1211)

* 优化公开状态页 Redis 聚合与轮询性能

* 修复公开状态页 rollup 边界问题

* 完善公开状态页 rollup 可靠性

* 完善公开状态页轮询测试清理

* 完善公开状态页轮询测试清理

---------

Co-authored-by: tesgth032 <tesgth032@users.noreply.github.com>

---------

Co-authored-by: tesgth032 <tesgth032@hotmail.com>
Co-authored-by: tesgth032 <tesgth032@users.noreply.github.com>
Co-authored-by: puyujian <46592377+puyujian@users.noreply.github.com>
Co-authored-by: Clouder <Clouder0@outlook.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Anthropic area:provider enhancement New feature or request size/XL Extra Large PR (> 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant