Skip to content

release v0.8.3#1225

Merged
ding113 merged 5 commits into
mainfrom
dev
May 28, 2026
Merged

release v0.8.3#1225
ding113 merged 5 commits into
mainfrom
dev

Conversation

@ding113

@ding113 ding113 commented May 28, 2026

Copy link
Copy Markdown
Owner

Summary

Release v0.8.3 - merges the dev branch into main. This release includes 5 PRs covering Anthropic stream model detection, OpenAI Responses API compatibility fixes, dashboard user list crash fix, public status page performance overhaul, and model reference updates.

Included Changes

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

Adds a new three-state model detection mechanism for Anthropic streaming responses that extracts the actual model name from the signature_delta protobuf payload embedded in SSE events. This is more accurate than the message_start plain text model field when thinking is enabled.

  • New modules: thinking-signature-model.ts (hand-rolled protobuf decoder) and anthropic-actual-response-model.ts (three-way decision logic)
  • UI: New "No thinking signature" badge in the SummaryTab detail panel when thinking is enabled but no signature was received; i18n added for all 5 locales
  • Audit: Adds ThinkingSignatureModelDetectionSpecialSetting type for audit persistence
  • Trigger: Only applies to providerType in {claude, claude-auth} with requestedModel starting with claude- or anthropic/

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

Fixes the TypeError: 'NoneType' object is not iterable crash in the official openai-python SDK when using CCH as an OpenAI-compatible proxy for /v1/responses. Some upstream providers return null for fields where the SDK expects arrays, strings, or objects.

  • New module: response-output-normalizer.ts -- converts null fields to schema-compatible defaults (output: null -> [], text: null -> "", function_call.arguments: null -> "{}", etc.)
  • Scoped to non-SSE, originalFormat === "response", 2xx JSON responses only
  • Gracefully falls back to the original response on any parse error

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

Fixes a dashboard crash for non-admin users who open the users page. The /api/v1/users:self endpoint previously returned a user detail shape without a keys array, but the dashboard unconditionally reads user.keys.length, causing the global error page.

  • Extracts buildUserDisplays() from getUsers() as a shared helper
  • Adds getCurrentUserDisplay() that returns the same UserDisplay list-item shape (including keys) without triggering a full admin user-list scan
  • Fixes Date object corruption in redactUserKeys so Hono JSON serialization emits ISO strings

Optimize public status page Redis aggregation and polling performance (#1211)

Major redesign of the public status page data layer to reduce Redis memory growth and DB back-scan costs. Replaces per-request DB back-scanning with an incremental v2 Redis rollup architecture.

  • New v2 Redis namespace: public-status:v2 -- 5-minute HINCRBYFLOAT rollup buckets written at request finalization
  • Background worker: Materializes snapshots from rollup buckets instead of querying DB
  • Dual-namespace fallback: v2 primary with v1 legacy fallback during coverage warm-up window
  • Frontend optimization: Polling uses include=meta,defaults,groups (no timeline); timeline is reused client-side; single shared hover summary replaces per-cell Radix Tooltip instances
  • No database migration required: Docker/standard upgrades need no manual steps
  • Behavioral change: TTFB/TPS metric changes from DB median to rollup sum/count mean

chore: upgrade Codex test models to GPT-5.5 (#1221)

Bulk-update all Codex/OpenAI model references from older GPT-5.x versions (5.2, 5.3, non-mini 5.4) to GPT-5.5 across all 5 locales, test fixtures, provider-testing defaults, and documentation. gpt-5.4-mini references are preserved as the current latest mini model.

Purely cosmetic/reference update -- no runtime behavior or API changes.

Related Issues

Breaking Changes

None. The public status page metric change (TTFB/TPS from median to mean) is a behavioral change but not a breaking API change.

Testing

All included PRs were individually verified:

  • bun run typecheck -- passes
  • bun run lint -- passes
  • bun run test -- passes
  • bun run build -- passes
  • bun run test:v1 -- passes

Verification

bun run typecheck
bun run lint
bun run test
bun run build

Description enhanced by Claude AI

Greptile Summary

Release v0.8.3 merges five independently reviewed PRs: Anthropic thinking-signature model detection (hand-rolled protobuf decoder + three-state decision logic), OpenAI Responses API null-field normalizer, a /api/v1/users:self crash fix for non-admin users, a major public status data-layer overhaul from DB back-scans to a v2 Redis HINCRBYFLOAT rollup, and a bulk GPT-5.5 model reference update.

  • Anthropic stream model detection: new hand-rolled protobuf decoder extracts the actual model from signature_delta payloads; falls back to message_start plain-text model; audited via ThinkingSignatureModelDetectionSpecialSetting.
  • Responses API normalizer: converts null output/content/arguments fields to schema-compatible defaults, scoped to non-SSE 2xx originalFormat === \"response\" responses only.
  • Users self-endpoint fix: buildUserDisplays extracted as a shared helper; getCurrentUserDisplay returns the full UserDisplay shape (including keys); redactUserKeys now correctly passes Date objects through.
  • Public status v2 Redis rollup: 5-minute HINCRBYFLOAT bucket writes replace per-rebuild DB back-scans; dual-namespace read path (v2 primary, v1 legacy fallback) handles warm-up coverage gaps.

Confidence Score: 4/5

Safe to merge; changes are well-scoped and tested, with no regressions expected in existing behaviour.

The five sub-PRs are each independently tested and touch distinct subsystems. The protobuf decoder fails gracefully to null on every bad-input path. The response normalizer only activates for Responses-API non-streaming 2xx JSON. The users fix properly restores the full UserDisplay shape. The public-status rollup architecture includes a dual-namespace warm-up fallback. The only open questions are minor defensive-programming gaps (array-destructure null guard, bodyUsed check) that have no realistic failure path in production.

src/actions/users.ts (getCurrentUserDisplay array destructure) and src/app/v1/_lib/proxy/response-output-normalizer.ts (bodyUsed guard) would benefit from small hardening touches noted in the inline comments.

Important Files Changed

Filename Overview
src/app/v1/_lib/proxy/thinking-signature-model.ts New hand-rolled protobuf decoder; handles all edge cases with null returns, never throws.
src/app/v1/_lib/proxy/anthropic-actual-response-model.ts Three-state decision module scoped to claude/claude-auth + claude-/anthropic/ prefixes with graceful fallback.
src/app/v1/_lib/proxy/response-output-normalizer.ts Normalizes Responses API null fields; correctly scoped to non-SSE 2xx JSON responses.
src/app/v1/_lib/proxy/response-handler.ts Integrates model detection and normalizer into the proxy pipeline with proper fallbacks.
src/actions/users.ts Adds getCurrentUserDisplay(); array destructure lacks a runtime guard for the empty-array case.
src/app/api/v1/resources/users/handlers.ts Switches :self to getCurrentUserDisplay(); adds Date instanceof guard in redactUserKeys fixing empty-object serialization.
src/lib/public-status/rollup-store.ts Core v2 HINCRBYFLOAT bucket write/read; pipeline and non-pipeline paths covered; NX coverage-start key correct.
src/lib/public-status/rebuild-worker.ts Replaces DB back-scan with rollup reads; adds rollupCoverageComplete/rollupSampleCount to manifest.
src/lib/public-status/read-store.ts Dual-namespace fallback; strict rollupCoverageComplete === false prevents unnecessary legacy reads.
src/lib/public-status/redis-contract.ts Upgrades default prefix to v2, exports legacy prefix, adds rollup key builders.
src/app/[locale]/status/_components/public-status-timeline.tsx Replaces per-cell Radix Tooltip with a single shared hover-summary panel.

Sequence Diagram

sequenceDiagram
    participant RH as ResponseHandler
    participant AARM as AnthropicActualResponseModel
    participant TSM as ThinkingSignatureModel
    participant EAR as extractActualResponseModel
    RH->>AARM: resolveAnthropicStreamActualResponseModel
    alt not claude provider OR non-Anthropic model prefix
        AARM-->>RH: source null
        RH->>EAR: extractActualResponseModelForProvider
        EAR-->>RH: model from message_start
    else Anthropic model family
        AARM->>TSM: extractThinkingSignatureModelFromStream
        TSM->>TSM: find signature_delta in SSE chunks
        TSM->>TSM: decodeBase64Strict
        TSM->>TSM: walkLengthDelimitedPath buf [2,1,6]
        TSM-->>AARM: model or null
        alt signature valid
            AARM-->>RH: source signature
        else fallback
            AARM->>EAR: extractActualResponseModelForProvider
            EAR-->>AARM: fallbackModel
            alt thinkingEnabled and fallbackModel
                AARM-->>RH: source fallback_no_signature_with_thinking
            else
                AARM-->>RH: source fallback_no_thinking
            end
        end
        RH->>RH: addSpecialSetting + persist finalActualResponseModel
    end
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/actions/users.ts:523-524
Array destructuring without a null guard — if `buildUserDisplays` somehow returns an empty array (the guard at the top of the function prevents it, but nothing enforces it at the type level without `noUncheckedIndexedAccess`), `displayUser` is `undefined` at runtime while typed as `UserDisplay`. Adding an explicit check makes the failure branch explicit and prevents a silent `{ ok: true, data: undefined }` response.

```suggestion
    const [displayUser] = await buildUserDisplays([user], session, session.user.role === "admin");
    if (!displayUser) {
      return { ok: false, error: tError("USER_NOT_FOUND"), errorCode: ERROR_CODES.NOT_FOUND };
    }
    return { ok: true, data: displayUser };
```

### Issue 2 of 2
src/app/v1/_lib/proxy/response-output-normalizer.ts:156-159
The function bypasses SSE/streaming via `isJsonContentType` but does not guard against a already-consumed body before calling `response.clone().text()`. If `ResponseFixer` or any earlier middleware has drained the body (`response.bodyUsed === true`), `clone()` on a consumed body throws in some runtimes. Adding a `bodyUsed` guard mirrors the defensive pattern used elsewhere in the proxy and keeps the fallback path clean.

```suggestion
  if (response.bodyUsed) return response;

  let rawText: string;
  try {
    rawText = await response.clone().text();
  } catch (error) {
```

Reviews (1): Last reviewed commit: "优化公开状态页 Redis 聚合与轮询性能 (#1211)" | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

tesgth032 and others added 5 commits May 28, 2026 18:55
Co-authored-by: tesgth032 <tesgth032@users.noreply.github.com>
#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): 规范化 Responses 输出空值

* fix(proxy): 完善 Responses 输出归一化边界
* fix: return full self user list shape

* fix: target self user display loading

* chore: remove unreachable self user guard
* 优化公开状态页 Redis 聚合与轮询性能

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

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

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

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

---------

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

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

统一将默认/示例模型更新为 gpt-5.5;引入 Anthropic 流“thinking signature”实际模型检测与“无签名”UI提示;新增 Responses 输出归一化;Public Status 升级至 v2 rollup:新键空间、读取/重建/聚合与视图更新;用户 self 接口改为 getCurrentUserDisplay;全面测试更新。

Changes

GPT-5.5 与 Public Status v2 综合更新

Layer / File(s) Summary
模型与文档/i18n更新
CHANGELOG.md, messages/*, src/app/[locale]/usage-doc/page.tsx, messages/*/usage.json, tests/usage-doc
将默认/示例模型替换为 gpt-5.5,更新多语言文案与用例断言。
Provider 与 Proxy 行为
src/actions/providers.ts, src/app/v1/_lib/proxy/*
OpenAI 测试默认模型改为 gpt-5.5;新增 Responses 输出归一化;导出 extractJsonChunks;加入 Anthropic thinking signature 实际模型推断与“无签名”来源标识。
Dashboard/Logs UI 提示
SummaryTab.tsx, messages/*/dashboard.json
当开启思考且缺失签名时显示“no signature”徽章与说明 tooltip。
Public Status v2(rollup) 链路
src/lib/public-status/*, src/app/[locale]/status/*
新增 v2 前缀与 rollup 键、读取/投影/重建/覆盖判定、rollup 存取与聚合;前端视图轮询与时间线复用/汇总计算更新。
用户 self 展示链路
src/actions/users.ts, src/app/api/v1/resources/users/handlers.ts, tests/users*
抽取 buildUserDisplays,新增 getCurrentUserDisplay;self 接口改用该动作并细化返回结构与脱敏处理。
广泛测试更新
tests/**/*
同步 gpt-5.5 断言;覆盖输出归一化、Anthropic 实际模型推断、Public Status v2 rollup、调度与键合约等。

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Possibly related PRs

✨ 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 dev

@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 upgrades default models from GPT-5.4 to GPT-5.5, introduces Anthropic streaming thinking signature model detection, and implements a response output normalizer for Responses API payloads. It also optimizes public status aggregation by transitioning to a 5-minute rollup store and refactors user display logic to load only the current session user. The review feedback highlights a few areas for improvement: using loose equality (== null) to safely handle potential undefined values when formatting metrics, safely casting event.relatedTarget to Node to avoid TypeScript compilation errors, and applying optional chaining to previousModel?.timeline?.length to prevent runtime errors if the timeline is null or undefined.

Comment on lines +90 to +93
availability:
activeBucket.availabilityPct === null ? "—" : `${activeBucket.availabilityPct.toFixed(2)}%`,
ttfb: formatTtfb(activeBucket.ttfbMs),
tps: activeBucket.tps === null ? "—" : activeBucket.tps.toFixed(1),

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

Using strict equality === null does not guard against undefined values. If availabilityPct or tps is undefined (which is common for optional or missing API payload fields), the strict check will evaluate to false, and calling .toFixed() on undefined will throw a runtime TypeError and crash the page. Using loose equality == null safely handles both null and undefined.

Suggested change
availability:
activeBucket.availabilityPct === null ? "—" : `${activeBucket.availabilityPct.toFixed(2)}%`,
ttfb: formatTtfb(activeBucket.ttfbMs),
tps: activeBucket.tps === null ? "—" : activeBucket.tps.toFixed(1),
availability:
activeBucket.availabilityPct == null ? "—" : `${activeBucket.availabilityPct.toFixed(2)}%`,
ttfb: formatTtfb(activeBucket.ttfbMs),
tps: activeBucket.tps == null ? "—" : activeBucket.tps.toFixed(1),

Comment on lines +101 to +105
onBlur={(event) => {
if (!event.currentTarget.contains(event.relatedTarget)) {
setActiveIndex(null);
}
}}

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

In TypeScript, event.relatedTarget is of type EventTarget | null. Since EventTarget is not assignable to Node | null, passing it directly to contains will cause a compilation error under strict type checking. Additionally, if event.relatedTarget is null (e.g., when focus leaves the window), we should still clear the active index. Checking for relatedTarget nullability and casting/checking type is safer.

Suggested change
onBlur={(event) => {
if (!event.currentTarget.contains(event.relatedTarget)) {
setActiveIndex(null);
}
}}
onBlur={(event) => {
if (!event.relatedTarget || !event.currentTarget.contains(event.relatedTarget as Node)) {
setActiveIndex(null);
}
}}

return {
...model,
timeline: previousModel?.timeline ?? [],
timelineReusedFromPrevious: Boolean(previousModel?.timeline.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

To ensure robust defensive programming, use optional chaining when accessing the length property of timeline. If timeline is ever null or undefined, previousModel?.timeline.length will throw a runtime error. Using previousModel?.timeline?.length is safer.

Suggested change
timelineReusedFromPrevious: Boolean(previousModel?.timeline.length),
timelineReusedFromPrevious: Boolean(previousModel?.timeline?.length),

@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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

@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: 3

🧹 Nitpick comments (2)
src/lib/public-status/rollup-store.ts (1)

103-108: 💤 Low value

模块级缓存需注意内存与并发

cachedConfiguredGroups 是模块级可变状态。在长时间运行的 Node.js 进程中:

  1. 缓存过期逻辑正确(基于 expiresAt 时间戳)
  2. 但在高并发场景下,多个请求可能同时触发缓存刷新

当前实现可接受,但如果未来出现竞态问题,可考虑使用 Promise 缓存模式避免重复读取。

🤖 Prompt for 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.

In `@src/lib/public-status/rollup-store.ts` around lines 103 - 108,
cachedConfiguredGroups is a mutable module-level cache that can cause duplicate
concurrent refreshes under high load; change the refresh logic to use a
Promise-based in-flight marker (e.g., store a Promise in a companion variable
like cachedConfiguredGroupsFetch or convert cachedConfiguredGroups to hold a
pending Promise) so concurrent callers await the same fetch instead of
triggering multiple reads, and ensure the resolved value replaces
cachedConfiguredGroups with the final object (including configVersion, groups,
retryable, expiresAt) and that errors clear the in-flight marker so subsequent
attempts can retry.
src/lib/public-status/read-store.ts (1)

346-354: 💤 Low value

多次调用 triggerRebuildHint 可考虑合并

当 legacy 读取成功时,连续调用了三次 triggerRebuildHint(rollup-coverage-incomplete、legacy-generation、config-version-mismatch)。如果 triggerRebuildHint 内部有 Redis 写入或其他开销,可考虑批量传递 reason 数组以减少 I/O 次数。

不过若当前实现已有去重或短路逻辑(如 rebuild-hints.ts 中的 TTL 检查),则当前写法可接受。

🤖 Prompt for 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.

In `@src/lib/public-status/read-store.ts` around lines 346 - 354, When
legacyRead.ok is true the code calls input.triggerRebuildHint three times
(rollup-coverage-incomplete, legacy-generation, and conditionally
config-version-mismatch) which may incur repeated I/O; refactor the block around
legacyRead.ok to collect reasons into an array (always push
"rollup-coverage-incomplete" and "legacy-generation", and push
"config-version-mismatch" only when input.configVersion !==
legacyRead.projection.selectedManifest.configVersion) and call
input.triggerRebuildHint once with that array (or introduce a helper like
batchTriggerRebuildHints that accepts string[]), keeping the existing
conditional logic but reducing redundant calls to triggerRebuildHint.
🤖 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/actions/users.ts`:
- Around line 526-529: The current code logs the full error via logger.error but
returns error.message to the client; change it to always return a generic public
message while keeping the full error detail in logs. Specifically, in the block
that calls logger.error(...) and builds the message variable, remove returning
error.message and instead set the returned error string to a generic text like
"Internal server error" (or the module's standard message) while leaving the log
call unchanged (or augment it with context). Ensure the return still uses
ERROR_CODES.INTERNAL_ERROR and the same shape { ok: false, error: <generic>,
errorCode: ERROR_CODES.INTERNAL_ERROR } so the internal exception details are
not exposed to clients.

In `@src/app/v1/_lib/proxy/response-output-normalizer.ts`:
- Line 2: 将相对导入改为仓库约定的 `@/` 别名导入:在文件 response-output-normalizer.ts 中把 import type
{ ProxySession } from "./session"; 替换为使用 "`@/`..." 路径别名导入对应的 session 模块(例如 import
type { ProxySession } from "`@/app/v1/_lib/proxy/session`";),确保与 tsconfig/paths
映射一致并运行构建/类型检查以验证路径正确性。

In `@src/lib/model-vendor-icons.test.ts`:
- Line 137: Update the dynamic import in the test so it uses the project path
alias instead of a relative path: replace the import("./model-vendor-icons")
call in model-vendor-icons.test.ts with the alias-based import (e.g.
import("`@/lib/model-vendor-icons`")) so getModelVendor is loaded via the '`@/`...'
mapping; ensure the symbol name getModelVendor remains the same and the dynamic
import target matches your tsconfig/webpack alias configuration.

---

Nitpick comments:
In `@src/lib/public-status/read-store.ts`:
- Around line 346-354: When legacyRead.ok is true the code calls
input.triggerRebuildHint three times (rollup-coverage-incomplete,
legacy-generation, and conditionally config-version-mismatch) which may incur
repeated I/O; refactor the block around legacyRead.ok to collect reasons into an
array (always push "rollup-coverage-incomplete" and "legacy-generation", and
push "config-version-mismatch" only when input.configVersion !==
legacyRead.projection.selectedManifest.configVersion) and call
input.triggerRebuildHint once with that array (or introduce a helper like
batchTriggerRebuildHints that accepts string[]), keeping the existing
conditional logic but reducing redundant calls to triggerRebuildHint.

In `@src/lib/public-status/rollup-store.ts`:
- Around line 103-108: cachedConfiguredGroups is a mutable module-level cache
that can cause duplicate concurrent refreshes under high load; change the
refresh logic to use a Promise-based in-flight marker (e.g., store a Promise in
a companion variable like cachedConfiguredGroupsFetch or convert
cachedConfiguredGroups to hold a pending Promise) so concurrent callers await
the same fetch instead of triggering multiple reads, and ensure the resolved
value replaces cachedConfiguredGroups with the final object (including
configVersion, groups, retryable, expiresAt) and that errors clear the in-flight
marker so subsequent attempts can retry.
🪄 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: c4e0d4e0-828d-4ead-bfc4-9780ba4d9b1f

📥 Commits

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

📒 Files selected for processing (110)
  • CHANGELOG.md
  • messages/en/dashboard.json
  • messages/en/settings/prices.json
  • messages/en/settings/providers/form/modelSelect.json
  • messages/en/settings/providers/form/strings.json
  • messages/en/usage.json
  • messages/ja/dashboard.json
  • messages/ja/settings/prices.json
  • messages/ja/settings/providers/form/modelSelect.json
  • messages/ja/settings/providers/form/strings.json
  • messages/ja/usage.json
  • messages/providers-i18n-additions.json
  • messages/ru/dashboard.json
  • messages/ru/settings/prices.json
  • messages/ru/settings/providers/form/modelSelect.json
  • messages/ru/settings/providers/form/strings.json
  • messages/ru/usage.json
  • messages/zh-CN/dashboard.json
  • messages/zh-CN/settings/prices.json
  • messages/zh-CN/settings/providers/form/modelSelect.json
  • messages/zh-CN/settings/providers/form/strings.json
  • messages/zh-CN/usage.json
  • messages/zh-TW/dashboard.json
  • messages/zh-TW/settings/prices.json
  • messages/zh-TW/settings/providers/form/modelSelect.json
  • messages/zh-TW/settings/providers/form/strings.json
  • messages/zh-TW/usage.json
  • src/actions/providers.ts
  • src/actions/users.ts
  • src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx
  • src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx
  • src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/session-messages-client-actions.test.tsx
  • src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/session-messages-client.test.tsx
  • src/app/[locale]/settings/providers/_components/forms/api-test-button.tsx
  • src/app/[locale]/status/[slug]/page.tsx
  • src/app/[locale]/status/_components/public-status-timeline.tsx
  • src/app/[locale]/status/_components/public-status-view.tsx
  • src/app/[locale]/usage-doc/page.tsx
  • src/app/api/v1/resources/users/handlers.ts
  • 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-fixer/index.ts
  • src/app/v1/_lib/proxy/response-fixer/response-fixer.test.ts
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/app/v1/_lib/proxy/response-output-normalizer.ts
  • src/app/v1/_lib/proxy/thinking-signature-model.ts
  • src/app/v1/_lib/responses-ws/__tests__/server-helpers.test.ts
  • src/app/v1/_lib/responses-ws/__tests__/upstream-adapter.test.ts
  • src/lib/model-vendor-icons.test.ts
  • src/lib/model-vendor-icons.tsx
  • src/lib/model-vendor-rules.ts
  • src/lib/provider-testing/data/cx_base.json
  • src/lib/provider-testing/data/cx_codex_basic.json
  • src/lib/provider-testing/data/cx_gpt_basic.json
  • src/lib/provider-testing/presets.ts
  • src/lib/provider-testing/test-service.test.ts
  • src/lib/provider-testing/utils/test-prompts.ts
  • src/lib/public-status/aggregation-core.ts
  • src/lib/public-status/aggregation.ts
  • src/lib/public-status/config-publisher.ts
  • src/lib/public-status/config-snapshot.ts
  • src/lib/public-status/read-store.ts
  • src/lib/public-status/rebuild-hints.ts
  • src/lib/public-status/rebuild-worker.ts
  • src/lib/public-status/redis-contract.ts
  • src/lib/public-status/rollup-store.ts
  • src/lib/public-status/scheduler.ts
  • src/lib/public-status/vendor-icon-key.ts
  • src/lib/session-manager-detail-snapshots.test.ts
  • src/lib/utils/special-settings.ts
  • src/repository/message.ts
  • src/types/model-price.ts
  • src/types/special-settings.ts
  • tests/api/v1/model-prices/model-prices.test.ts
  • tests/api/v1/providers/providers.read.test.ts
  • tests/api/v1/users/users.test.ts
  • tests/e2e/responses-ws-codex-cli-transport.test.ts
  • tests/integration/billing-model-source.test.ts
  • tests/integration/non-chat-endpoint-fallback-observability.test.ts
  • tests/unit/actions/active-sessions-detail-snapshots.test.ts
  • tests/unit/actions/model-prices.test.ts
  • tests/unit/api/v1/api-client-actions.test.ts
  • tests/unit/codex/session-completer.test.ts
  • tests/unit/lib/utils/pricing-resolution.test.ts
  • tests/unit/proxy/actual-response-model.test.ts
  • tests/unit/proxy/anthropic-actual-response-model.test.ts
  • tests/unit/proxy/codex-provider-overrides.test.ts
  • tests/unit/proxy/non-chat-endpoint-fallback.test.ts
  • tests/unit/proxy/non-chat-endpoint-session-context.test.ts
  • tests/unit/proxy/proxy-forwarder-large-chunked-response.test.ts
  • tests/unit/proxy/proxy-forwarder-nonok-body-hang.test.ts
  • tests/unit/proxy/proxy-forwarder-raw-passthrough-regression.test.ts
  • tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts
  • tests/unit/proxy/response-output-normalizer.test.ts
  • tests/unit/proxy/thinking-signature-model.test.ts
  • tests/unit/public-status/aggregation.test.ts
  • tests/unit/public-status/config-publisher.test.ts
  • tests/unit/public-status/config-snapshot.test.ts
  • tests/unit/public-status/no-db-import-guard.test.ts
  • tests/unit/public-status/public-status-view.test.tsx
  • tests/unit/public-status/read-store.test.ts
  • tests/unit/public-status/rebuild-worker.test.ts
  • tests/unit/public-status/redis-contract.test.ts
  • tests/unit/public-status/rollup-store.test.ts
  • tests/unit/repository/message-public-status-rollup.test.ts
  • tests/unit/server-ws-close-handshake.test.ts
  • tests/unit/settings/prices/price-list-multi-provider-ui.test.tsx
  • tests/unit/settings/providers/api-test-button.test.tsx
  • tests/unit/usage-doc/opencode-usage-doc.test.tsx
  • tests/unit/users-action-get-users-compat.test.ts

Comment thread src/actions/users.ts
Comment on lines +526 to +529
logger.error("Failed to fetch current user display data:", error);
const message =
error instanceof Error ? error.message : "Failed to fetch current user display data";
return { ok: false, error: message, errorCode: ERROR_CODES.INTERNAL_ERROR };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

避免向客户端返回原始异常信息。

这里直接返回 error.message,会把内部错误细节暴露给前端调用方。建议只记录详细日志,对外统一返回通用错误文案与错误码。

建议修改
   } catch (error) {
     logger.error("Failed to fetch current user display data:", error);
-    const message =
-      error instanceof Error ? error.message : "Failed to fetch current user display data";
-    return { ok: false, error: message, errorCode: ERROR_CODES.INTERNAL_ERROR };
+    const tError = await getTranslations("errors");
+    return {
+      ok: false,
+      error: tError("OPERATION_FAILED"),
+      errorCode: ERROR_CODES.INTERNAL_ERROR,
+    };
   }
 }
🤖 Prompt for 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.

In `@src/actions/users.ts` around lines 526 - 529, The current code logs the full
error via logger.error but returns error.message to the client; change it to
always return a generic public message while keeping the full error detail in
logs. Specifically, in the block that calls logger.error(...) and builds the
message variable, remove returning error.message and instead set the returned
error string to a generic text like "Internal server error" (or the module's
standard message) while leaving the log call unchanged (or augment it with
context). Ensure the return still uses ERROR_CODES.INTERNAL_ERROR and the same
shape { ok: false, error: <generic>, errorCode: ERROR_CODES.INTERNAL_ERROR } so
the internal exception details are not exposed to clients.

@@ -0,0 +1,189 @@
import { logger } from "@/lib/logger";
import type { ProxySession } from "./session";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

请改为 @/ 路径别名导入

这里使用了相对导入,和仓库 TS 导入规范不一致,建议改为 @/ 别名路径。

建议修改
-import type { ProxySession } from "./session";
+import type { ProxySession } from "`@/app/v1/_lib/proxy/session`";

As per coding guidelines **/*.{ts,tsx,js,jsx}: Use path alias @/ to map to ./src/ for imports.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { ProxySession } from "./session";
import type { ProxySession } from "`@/app/v1/_lib/proxy/session`";
🤖 Prompt for 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.

In `@src/app/v1/_lib/proxy/response-output-normalizer.ts` at line 2, 将相对导入改为仓库约定的
`@/` 别名导入:在文件 response-output-normalizer.ts 中把 import type { ProxySession } from
"./session"; 替换为使用 "`@/`..." 路径别名导入对应的 session 模块(例如 import type { ProxySession }
from "`@/app/v1/_lib/proxy/session`";),确保与 tsconfig/paths 映射一致并运行构建/类型检查以验证路径正确性。

vi.stubEnv("NODE_ENV", "development");

try {
const { getModelVendor: getMockedModelVendor } = await import("./model-vendor-icons");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

动态导入请统一改为 @/ 别名路径

这里是新增导入点,建议按仓库规则改为 @/ 别名,避免相对路径继续扩散。

建议修改
-      const { getModelVendor: getMockedModelVendor } = await import("./model-vendor-icons");
+      const { getModelVendor: getMockedModelVendor } = await import("`@/lib/model-vendor-icons`");

As per coding guidelines **/*.{ts,tsx,js,jsx}: Use path alias @/ to map to ./src/ for imports.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { getModelVendor: getMockedModelVendor } = await import("./model-vendor-icons");
const { getModelVendor: getMockedModelVendor } = await import("`@/lib/model-vendor-icons`");
🤖 Prompt for 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.

In `@src/lib/model-vendor-icons.test.ts` at line 137, Update the dynamic import in
the test so it uses the project path alias instead of a relative path: replace
the import("./model-vendor-icons") call in model-vendor-icons.test.ts with the
alias-based import (e.g. import("`@/lib/model-vendor-icons`")) so getModelVendor
is loaded via the '`@/`...' mapping; ensure the symbol name getModelVendor remains
the same and the dynamic import target matches your tsconfig/webpack alias
configuration.

@github-actions github-actions Bot added the size/XL Extra Large PR (> 1000 lines) label 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.

PR #1225 Code Review Summary (release v0.8.3)

Reviewer: Automated Multi-Perspective Code Review
Confidence Threshold: 80
Files Reviewed: 110 | +5,621 / -838 lines
Size Label Applied: size/XL


Issues by Category

# Category Severity Confidence File Description
1 ERROR-SILENT Medium 85 response-output-normalizer.ts:171 Silent JSON.parse failure returns original response with no logging
2 ERROR-SILENT Low-Medium 80 rebuild-worker.ts:308 Empty catch block on config bootstrap failure (intentional but unlogged)

Critical Issues

None identified.

High Issues

None identified.

Medium Issues

#1 - Silent JSON.parse failure in response-output-normalizer.ts (Confidence: 85)

In normalizeResponseOutput(), the catch block after JSON.parse(rawText) silently returns the original response without any logging. If upstream returns malformed JSON for a Responses API call, this error is swallowed entirely. The normalizer already has a logger used for successful normalization - it should also log parse failures.

Suggested fix:

} catch (error) {
  logger.warn("[ResponseOutputNormalizer] Failed to parse response JSON", {
    error: error instanceof Error ? error.message : String(error),
    sessionId: session.sessionId ?? null,
    requestSequence: session.requestSequence ?? null,
  });
  return response;
}

Low-Medium Issues

#2 - Empty catch block in rebuild-worker.ts bootstrap (Confidence: 80)

The catch block when publishCurrentPublicStatusConfigProjection fails during config bootstrap contains only a comment. While the behavior is intentional (fall through to missing-config semantics), the lack of logging means persistent bootstrap failures are invisible. Adding a warn-level log would help ops detect repeated failures without changing the control flow.

Suggested fix:

} catch (error) {
  logger.warn("[RebuildWorker] Bootstrap config publish failed", {
    reason: error instanceof Error ? error.message : String(error),
  });
  // Intentional: let caller continue with missing-config semantics.
}

Review Coverage Checklist

Perspective Status Details
Comment Analyzer Done No misleading comments found; comments are accurate and purposeful
Test Analyzer Done 4 new test files with good coverage (thinking-signature, anthropic-model, response-normalizer, rollup-store). Edge cases covered. Mock boundaries appropriate.
Silent Failure Hunter Done 2 issues found (see above). All other catch blocks are either intentional null-return patterns (protobuf decoder) or have proper logging.
Type Design Auditor Done Discriminated unions used correctly (ActionResult, rebuild status types). No stringly-typed state where enums would be better.
General Code Reviewer Done No security issues. Base64 validation is thorough (standard + URL-safe). Redis pipeline usage is correct. Distributed locking uses proper Lua compare-and-delete.
Code Simplifier Done buildUserDisplays() extraction avoids duplication. No significant over-engineering detected. Fire-and-forget rollup pattern is appropriate for the use case.

Key Sub-PRs Reviewed

  1. Anthropic thinking signature detection - Hand-rolled protobuf decoder, appropriate null-return-on-error design, well-tested.
  2. OpenAI Responses API normalization - Null-to-default conversion is correct and scoped properly. Silent parse failure noted above.
  3. User list crash fix - Defense-in-depth with Date instance check and self-lookup guard. buildUserDisplays() extraction is clean.
  4. Public status Redis rollup v2 - HINCRBYFLOAT bucket design is sound. FIFO cache eviction is correct. Dual-namespace fallback (v2/v1) is well-structured.
  5. Model reference updates - Straightforward, no concerns.

Verdict: The PR is well-structured with good test coverage. Two medium/low silent failure issues should be addressed before or shortly after merge. No blocking issues found.

@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.

Inline Review Comments

See summary review above for full context. Adding targeted comments on the two silent failure issues.

let parsed: unknown;
try {
parsed = JSON.parse(rawText);
} catch {

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.

[ERROR-SILENT] Silent JSON.parse failure (Confidence: 85)

The catch after JSON.parse(rawText) returns response with no logging. If upstream returns malformed JSON for a Responses API call, this silently swallows the error and returns the original (potentially broken) response.

Suggestion:

} catch (error) {
  logger.warn("[ResponseOutputNormalizer] Failed to parse response JSON", {
    error: error instanceof Error ? error.message : String(error),
    sessionId: session.sessionId ?? null,
    requestSequence: session.requestSequence ?? null,
  });
  return response;
}

@@ -280,19 +308,25 @@ export async function rebuildPublicStatusProjection(input: {
| { status: "skipped"; reason: "distributed-lock-held"; sourceGeneration: string }

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.

[ERROR-SILENT] Empty catch block on bootstrap failure (Confidence: 80)

The catch when publishCurrentPublicStatusConfigProjection fails is empty (only a comment). While the behavior is intentional (fall through to missing-config), the lack of logging means persistent bootstrap failure is invisible.

Suggestion:

} catch (error) {
  logger.warn("[RebuildWorker] Bootstrap config publish failed", {
    reason: error instanceof Error ? error.message : String(error),
  });
  // Intentional: let caller continue with missing-config semantics.
}

@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.

Inline Review Comments

See summary review above for full context. Adding targeted comments on the two silent failure issues.

let parsed: unknown;
try {
parsed = JSON.parse(rawText);
} catch {

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.

[ERROR-SILENT] Silent JSON.parse failure (Confidence: 85)

The catch after JSON.parse(rawText) returns response with no logging. If upstream returns malformed JSON for a Responses API call, this silently swallows the error and returns the original (potentially broken) response.

Suggestion:

} catch (error) {
  logger.warn("[ResponseOutputNormalizer] Failed to parse response JSON", {
    error: error instanceof Error ? error.message : String(error),
    sessionId: session.sessionId ?? null,
    requestSequence: session.requestSequence ?? null,
  });
  return response;
}

@@ -280,19 +308,25 @@ export async function rebuildPublicStatusProjection(input: {
| { status: "skipped"; reason: "distributed-lock-held"; sourceGeneration: string }

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.

[ERROR-SILENT] Empty catch block on bootstrap failure (Confidence: 80)

The catch when publishCurrentPublicStatusConfigProjection fails is empty (only a comment). While the behavior is intentional (fall through to missing-config), the lack of logging means persistent bootstrap failure is invisible.

Suggestion:

} catch (error) {
  logger.warn("[RebuildWorker] Bootstrap config publish failed", {
    reason: error instanceof Error ? error.message : String(error),
  });
  // Intentional: let caller continue with missing-config semantics.
}

Comment thread src/actions/users.ts
Comment on lines +523 to +524
const [displayUser] = await buildUserDisplays([user], session, session.user.role === "admin");
return { ok: true, data: displayUser };

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 Array destructuring without a null guard — if buildUserDisplays somehow returns an empty array (the guard at the top of the function prevents it, but nothing enforces it at the type level without noUncheckedIndexedAccess), displayUser is undefined at runtime while typed as UserDisplay. Adding an explicit check makes the failure branch explicit and prevents a silent { ok: true, data: undefined } response.

Suggested change
const [displayUser] = await buildUserDisplays([user], session, session.user.role === "admin");
return { ok: true, data: displayUser };
const [displayUser] = await buildUserDisplays([user], session, session.user.role === "admin");
if (!displayUser) {
return { ok: false, error: tError("USER_NOT_FOUND"), errorCode: ERROR_CODES.NOT_FOUND };
}
return { ok: true, data: displayUser };
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/users.ts
Line: 523-524

Comment:
Array destructuring without a null guard — if `buildUserDisplays` somehow returns an empty array (the guard at the top of the function prevents it, but nothing enforces it at the type level without `noUncheckedIndexedAccess`), `displayUser` is `undefined` at runtime while typed as `UserDisplay`. Adding an explicit check makes the failure branch explicit and prevents a silent `{ ok: true, data: undefined }` response.

```suggestion
    const [displayUser] = await buildUserDisplays([user], session, session.user.role === "admin");
    if (!displayUser) {
      return { ok: false, error: tError("USER_NOT_FOUND"), errorCode: ERROR_CODES.NOT_FOUND };
    }
    return { ok: true, data: displayUser };
```

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

Comment on lines +156 to +159
let rawText: string;
try {
rawText = await response.clone().text();
} catch (error) {

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 function bypasses SSE/streaming via isJsonContentType but does not guard against a already-consumed body before calling response.clone().text(). If ResponseFixer or any earlier middleware has drained the body (response.bodyUsed === true), clone() on a consumed body throws in some runtimes. Adding a bodyUsed guard mirrors the defensive pattern used elsewhere in the proxy and keeps the fallback path clean.

Suggested change
let rawText: string;
try {
rawText = await response.clone().text();
} catch (error) {
if (response.bodyUsed) return response;
let rawText: string;
try {
rawText = await response.clone().text();
} catch (error) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/response-output-normalizer.ts
Line: 156-159

Comment:
The function bypasses SSE/streaming via `isJsonContentType` but does not guard against a already-consumed body before calling `response.clone().text()`. If `ResponseFixer` or any earlier middleware has drained the body (`response.bodyUsed === true`), `clone()` on a consumed body throws in some runtimes. Adding a `bodyUsed` guard mirrors the defensive pattern used elsewhere in the proxy and keeps the fallback path clean.

```suggestion
  if (response.bodyUsed) return response;

  let rawText: string;
  try {
    rawText = await response.clone().text();
  } catch (error) {
```

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

@ding113 ding113 merged commit c92778a into main May 28, 2026
31 of 32 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Claude Code Hub Roadmap May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core area:provider area:UX 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.

OpenAI-compatible /v1/responses returns 200 but openai-python parser fails with NoneType object is not iterable

4 participants