Skip to content

release v0.5.4#731

Merged
ding113 merged 15 commits intomainfrom
dev
Feb 7, 2026
Merged

release v0.5.4#731
ding113 merged 15 commits intomainfrom
dev

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Feb 7, 2026

Summary

Release v0.5.4 consolidates 13 merged PRs from the dev branch, delivering critical bug fixes, new features, and infrastructure improvements. This release focuses on provider endpoint management, session tracking stability, UI enhancements, and hot-reload capabilities.

Related Issues & PRs

Fixes:

Related:

Key Changes

Critical Fixes

Session Tracker Memory Leak (#718)

  • Aligned SessionTracker with env SESSION_TTL (was hardcoded to 5 minutes)
  • Added probabilistic write-path cleanup (1%) to prevent ZSet growth
  • Fixed TTL=-1 keys by ensuring EXPIRE on hot write path
  • Prevents Redis memory exhaustion from unbounded session tracking

Provider Endpoint Race Conditions (#730)

  • Wrapped provider create/update in database transactions
  • Added syncProviderEndpointOnProviderEdit() for atomic endpoint migration
  • Handles URL changes with soft-delete + create/revive/in-place update logic
  • Eliminates orphaned endpoint records and routing to stale endpoints

Hot-Reload Cache Invalidation (#710)

  • Fixed Request Filters and Sensitive Words requiring server restart after UI changes
  • Implemented globalThis singleton pattern for cross-worker consistency
  • Hardened Redis pub/sub with async connection, reconnect handling, and proper error signaling
  • Dual-channel propagation (EventEmitter + Redis pub/sub)

Authentication Session Propagation (#720)

  • Fixed getSession() returning null in /api/actions endpoints
  • Introduced AsyncLocalStorage for request-scoped session context
  • Resolves empty data returns for regular user API access

New Features

Gemini Google Search Control (#721)

  • Provider-level preference for Google Search tool injection
  • Three modes: inherit (default), enabled (force inject), disabled (force remove)
  • Full audit trail support in logs
  • Database migration: adds gemini_google_search_preference column

Vendor Endpoint Pool UI (#719)

  • Exposes endpoint pools in provider settings with visual status indicators
  • Hover tooltips showing per-endpoint health, probe state, and circuit breaker status
  • Reusable CRUD table component for endpoint management
  • Helps diagnose failover issues and circuit breaker states

Cost Column Toggle (#715)

  • Cost column now toggleable in logs table (previously always visible)
  • Addresses flat-rate subscription users where cost is always $0
  • Maintains visibility by default for backward compatibility
  • Improved null handling for zero-cost scenarios

Other Improvements

OpenAI Format Support (#716)

  • Added prompt_tokens/completion_tokens extraction to usage metrics
  • Priority-based format detection: Claude > Gemini > OpenAI
  • Fixes token billing for OpenAI-compatible providers

Thinking Signature Error Detection

  • Added detection for "cannot be modified" errors in thinking blocks
  • Improved error handling for Anthropic thinking mode edge cases

Dependency Updates

  • Bumped jspdf from 3.0.4 to 4.1.0 (security update)

Database Migration

Migration 0062_aromatic_taskmaster.sql adds:

ALTER TABLE "providers" ADD COLUMN "gemini_google_search_preference" varchar(20);

Migration is backward compatible - column is nullable, existing providers unaffected.

Breaking Changes

None. All changes are backward compatible:

  • New database column is nullable with sensible defaults
  • Session tracking improvements are transparent to users
  • UI changes maintain default visibility states
  • API contracts unchanged

Testing

Automated Tests

  • All existing tests pass (1800+ tests)
  • New test coverage:
    • Session tracker cleanup and TTL validation tests
    • Provider endpoint sync transaction tests
    • Hot-reload singleton pattern tests
    • Gemini override tests (22 new tests)
    • OpenAI format extraction tests
    • Cost column toggle tests

Pre-commit Checklist

  • bun run build - Production build succeeds
  • bun run lint - Biome check passes
  • bun run typecheck - TypeScript check passes
  • bun run test - All tests pass

Merged PRs (13 total)

PR Type Description
#730 fix Wrap endpoint sync in transactions to prevent race conditions
#721 feat Add Google Search web access preference for Gemini providers
#720 fix Propagate /api/actions auth session to avoid getUsers returning empty data
#719 feat Expose vendor endpoint pools in settings UI
#718 fix Bound SessionTracker active_sessions zsets by env TTL
#717 fix Respect system currencyDisplay setting in UI
#716 fix Add OpenAI chat completion format support in usage extraction
#715 feat Make cost column toggleable with improved type safety
#711 chore Bump jspdf from 3.0.4 to 4.1.0 (security)
#710 fix Hot-reload cache invalidation for Request Filters and Sensitive Words
+ 3 more various Thinking signature detection, format code, minor fixes

Deployment Notes

  1. Database Migration: Auto-applies on startup if AUTO_MIGRATE=true (default)
  2. Redis: Session tracking improvements take effect immediately, no action needed
  3. Environment Variables: Ensure SESSION_TTL is set appropriately (default: 300 seconds)
  4. No Downtime Required: All changes are hot-reloadable or backward compatible

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests pass locally
  • Database migration generated and reviewed
  • i18n translations complete (5 languages)
  • CHANGELOG.md updated

Description enhanced by Claude AI

Greptile Overview

Greptile Summary

  • Cuts the v0.5.4 release by consolidating provider endpoint pool management, session propagation, hot-reload cache invalidation, and Gemini override plumbing.
  • Adds a DB migration for providers.gemini_google_search_preference and wires it through provider CRUD and proxy request overrides.
  • Introduces/extends Redis pub/sub based cache invalidation to propagate settings changes across workers.
  • Expands proxy forwarding logic to prefer vendor endpoint pools (latency-ordered) with stricter fallback behavior for certain standard endpoints.

Confidence Score: 3/5

  • This PR is mergeable after addressing a few correctness issues in edge-case control flow and pub/sub state management.
  • Main risk comes from a confirmed crash path in proxy forwarding when strict endpoint policy yields zero candidates, and a concrete in-memory/Redis subscription state desync in pubsub cleanup; other changes look consistent and covered by tests/migrations.
  • src/app/v1/_lib/proxy/forwarder.ts, src/lib/redis/pubsub.ts

Important Files Changed

Filename Overview
drizzle/0062_aromatic_taskmaster.sql Adds nullable gemini_google_search_preference column; file currently lacks trailing newline.
src/lib/redis/pubsub.ts Refactors Redis pubsub subscriber lifecycle; cleanup unsubscribe path can desync subscribedChannels state due to non-awaited unsubscribe and races.
src/lib/gemini/provider-overrides.ts Implements Gemini googleSearch tool override + audit; unknown preference still returns null audit making misconfig hard to detect.
src/app/v1/_lib/proxy/forwarder.ts Adds endpoint pool selection and stricter fallback behavior; strict-policy branches can fall through with empty endpointCandidates leading to runtime crash (already flagged in prior thread).

Sequence Diagram

sequenceDiagram
  autonumber
  participant UI as Settings UI
  participant API as Actions/API
  participant Repo as Repository
  participant DB as Postgres
  participant Cache as In-process Cache
  participant Redis as Redis PubSub
  participant Proxy as ProxyForwarder
  participant Sel as Endpoint Selector

  UI->>API: Update provider settings (url/type/google_search_pref)
  API->>Repo: updateProvider(id, data)
  Repo->>DB: BEGIN
  Repo->>Repo: getOrCreateProviderVendorIdFromUrls(...)
  Repo->>DB: UPDATE providers ... RETURNING
  Repo->>Repo: syncProviderEndpointOnProviderEdit(...)
  Repo->>DB: INSERT/UPDATE provider_endpoints (atomic in tx)
  Repo->>DB: COMMIT
  Repo-->>Cache: invalidate provider cache (emit)
  Cache-->>Redis: publishCacheInvalidation(channel)
  Redis-->>Cache: subscriber emits message -> callbacks run

  Proxy->>Sel: getPreferredProviderEndpoints(vendorId, providerType)
  Sel->>DB: Read endpoints + health ordering
  Sel-->>Proxy: endpointCandidates[]
  Proxy->>Proxy: applyGeminiGoogleSearchOverrideWithAudit()
  Proxy->>Proxy: forward request to selected endpoint

Loading

ding113 and others added 13 commits February 3, 2026 16:59
…ature rectifier

Extend the thinking signature rectifier to detect and handle the
Anthropic API error when thinking/redacted_thinking blocks have been
modified from their original response. This error occurs when clients
inadvertently modify these blocks in multi-turn conversations.

The rectifier will now remove these blocks and retry the request,
similar to how it handles other thinking-related signature errors.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Bumps the npm_and_yarn group with 1 update in the / directory: [jspdf](https://github.com/parallax/jsPDF).


Updates `jspdf` from 3.0.4 to 4.1.0
- [Release notes](https://github.com/parallax/jsPDF/releases)
- [Changelog](https://github.com/parallax/jsPDF/blob/master/RELEASE.md)
- [Commits](parallax/jsPDF@v3.0.4...v4.1.0)

---
updated-dependencies:
- dependency-name: jspdf
  dependency-version: 4.1.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
…Words (#710)

* fix: hot-reload request filters via globalThis singleton pattern

EventEmitter and RequestFilterEngine now use globalThis caching to ensure
the same instance is shared across different Next.js worker contexts.
This fixes the issue where filter changes required Docker restart.

Added diagnostic logging for event subscription and propagation.

* fix(redis): wait for subscriber connection ready before subscribe

- ensureSubscriber now returns Promise<Redis>, waits for 'ready' event
- subscribeCacheInvalidation returns null on failure instead of noop
- RequestFilterEngine checks cleanup !== null before logging success
- Fixes false "Subscribed" log when Redis connection fails

* feat(sensitive-words): add hot-reload via Redis pub/sub

Enable real-time cache invalidation for sensitive words detector,
matching the pattern used by request-filter-engine and error-rule-detector.

* fix(redis): harden cache invalidation subscriptions

Ensure sensitive-words CRUD emits update events so hot-reload propagates across workers. Roll back failed pub/sub subscriptions, add retry/timeout coverage, and avoid sticky provider-cache subscription state.

* fix(codex): bump default User-Agent fallback

Update the hardcoded Codex UA used when requests lack an effective user-agent (e.g. filtered out). Keep unit tests in sync with the new default.

* fix(redis): resubscribe cache invalidation after reconnect

Clear cached subscription state on disconnect and resubscribe on ready so cross-worker cache invalidation survives transient Redis reconnects. Add unit coverage, avoid misleading publish logs, track detector cleanup handlers, and translate leftover Russian comments to English.

* fix(sensitive-words): use globalThis singleton detector

Align SensitiveWordDetector with existing __CCH_* singleton pattern to avoid duplicate instances across module reloads. Extend singleton unit tests to cover the detector.

* chore: format code (req-fix-dda97fd)

* fix: address PR review comments

- pubsub.ts: use `once` instead of `on` for ready event to prevent
  duplicate resubscription handlers on reconnect
- forwarder.ts: extract DEFAULT_CODEX_USER_AGENT constant
- provider-cache.ts: wrap subscribeCacheInvalidation in try/catch
- tests: use exported constant instead of hardcoded UA string

* fix(redis): resubscribe across repeated reconnects

Ensure pub/sub resubscribe runs on every reconnect, extend unit coverage, and keep emitRequestFiltersUpdated resilient when logger import fails.

---------

Co-authored-by: John Doe <johndoe@example.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…tion (#705) (#716)

The `extractUsageMetrics` function was missing support for OpenAI chat
completion format fields (`prompt_tokens`/`completion_tokens`), causing
token statistics to not be recorded for OpenAI-compatible providers.

Changes:
- Add `prompt_tokens` -> `input_tokens` mapping
- Add `completion_tokens` -> `output_tokens` mapping
- Preserve priority: Claude > Gemini > OpenAI format
- Add 5 unit tests for OpenAI format handling

Closes #705

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Fixes #678 - Currency display unit configuration was not applied.

Root cause:
- `users-page-client.tsx` hardcoded `currencyCode="USD"`
- `UserLimitBadge` and `LimitStatusIndicator` had hardcoded `unit="$"` default
- `big-screen/page.tsx` used hardcoded "$" in multiple places

Changes:
- Add `getCurrencySymbol()` helper function to currency.ts
- Fetch system settings in `users-page-client.tsx` and pass to table
- Pass `currencySymbol` from `user-key-table-row.tsx` to limit badges
- Remove hardcoded "$" defaults from badge components
- Update big-screen page to fetch settings and use dynamic symbol
- Add unit tests for `getCurrencySymbol`

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…iders (#721)

* feat(gemini): add Google Search web access preference for Gemini providers

Add provider-level preference for Gemini API type providers to control
Google Search (web access) tool injection:

- inherit: Follow client request (default)
- enabled: Force inject googleSearch tool into request
- disabled: Force remove googleSearch tool from request

Changes:
- Add geminiGoogleSearchPreference field to provider schema
- Add GeminiGoogleSearchPreference type and validation
- Implement applyGeminiGoogleSearchOverride with audit trail
- Add UI controls in provider form (Gemini Overrides section)
- Add i18n translations for 5 languages (en, zh-CN, zh-TW, ja, ru)
- Integrate override logic in proxy forwarder for Gemini requests
- Add 22 unit tests for the override logic

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(gemini): address code review feedback

- Use explicit else-if for disabled preference check (gemini-code-assist)
- Use i18n for SelectValue placeholder instead of hardcoded string (coderabbitai)
- Sync overridden body back to session.request.message for log consistency (coderabbitai)
- Persist Gemini special settings immediately, matching Anthropic pattern (coderabbitai)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(gemini): use strict types for provider config and audit

- Narrow preference type to "enabled" | "disabled" (exclude unreachable "inherit")
- Use ProviderType and GeminiGoogleSearchPreference types instead of string

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(api): 透传 /api/actions 认证会话以避免 getUsers 返回空数据

* fix(auth): 让 scoped session 继承 allowReadOnlyAccess 语义并支持内部降权校验
* fix(session): bound active_sessions zsets by env ttl

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(rate-limit): pass session ttl to lua cleanup

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(session): validate SESSION_TTL env and prevent ZSET leak on invalid values

- Add input validation for SESSION_TTL (reject NaN, 0, negative; default 300)
- Guard against invalid TTL in Lua script to prevent clearing all sessions
- Use dynamic EXPIRE based on SESSION_TTL instead of hardcoded 3600s
- Add unit tests for TTL validation and dynamic expiry behavior

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* feat(providers): add endpoint status mapping

* feat(providers): add endpoint pool hover

* feat(providers): show vendor endpoints in list rows

* feat(providers): extract vendor endpoint CRUD table

* chore(i18n): add provider endpoint UI strings

* fix(providers): integrate endpoint pool into provider form
…ditions (#730)

* fix(provider): wrap provider create/update endpoint sync in transactions

Provider create and update operations now run vendor resolution and
endpoint sync inside database transactions to prevent race conditions
that could leave orphaned or inconsistent endpoint rows.

Key changes:
- createProvider: wrap vendor + insert + endpoint seed in a single tx
- updateProvider: wrap vendor + update + endpoint sync in a single tx
- Add syncProviderEndpointOnProviderEdit for atomic URL/type/vendor
  migration with in-place update, soft-delete, and conflict handling
- Vendor cleanup failures degrade to warnings instead of propagating
- Add comprehensive unit and integration tests for sync edge cases

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(provider): defer endpoint circuit reset until transaction commit

Avoid running endpoint circuit reset side effects inside DB transactions to prevent rollback inconsistency. Run resets only after commit and add regression tests for deferred reset behavior in helper and provider update flows.

* fix(provider): distinguish noop from created-next in endpoint sync action label

When ensureNextEndpointActive() returns "noop" (concurrent transaction
already created an active next endpoint), the action was incorrectly
labelled "kept-previous-and-created-next". Add a new
"kept-previous-and-kept-next" action to ProviderEndpointSyncAction and
use a three-way branch so callers and logs reflect the true outcome.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

📝 Walkthrough

Walkthrough

引入 Gemini Google Search 偏好与端点池/端点管理 UI,扩展数据库/schema 与验证,增加 Scoped Auth 会话支持、Redis pub/sub 与敏感词事件联动、会话 TTL 可配置化、成本列可见性、货币符号工具及大量相关单元与集成测试。

Changes

Cohort / File(s) Summary
数据库架构与迁移
drizzle/0062_aromatic_taskmaster.sql, drizzle/meta/0062_snapshot.json, drizzle/meta/_journal.json, src/drizzle/schema.ts
新增 providers.gemini_google_search_preference 字段,添加对应 Drizzle 模式与元数据快照。
本地化 (i18n)
messages/*/settings/providers/form/errors.json, messages/*/settings/providers/form/sections.json, messages/*/settings/providers/strings.json
为多语言添加 endpointPoolgeminiOverridesendpointStatussortOrder 等翻译与表单校验文案(EN/JA/RU/ZH-CN/ZH-TW)。
提供商端点管理 UI
src/app/[locale]/settings/providers/_components/endpoint-status.ts, src/app/[locale]/settings/providers/_components/provider-endpoint-hover.tsx, src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx, src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
新增端点状态模型、端点悬停组件与完整端点管理表格(ProviderEndpointsSection),并将原地实现替换为集中化组件。
提供商表单与状态管理
src/app/[locale]/settings/providers/_components/forms/provider-form/... (index.tsx, provider-form-context.tsx, provider-form-types.ts, sections/basic-info-section.tsx, sections/routing-section.tsx)
在 Provider 表单中集成端点池、条件化 URL 输入、gemini_google_search_preference 字段、相应 reducer/action/类型与提交/校验逻辑。
后端逻辑与仓库
src/repository/provider-endpoints.ts, src/repository/provider.ts, src/repository/_shared/transformers.ts
为 provider-endpoints 引入可选事务上下文、添加 syncProviderEndpointOnProviderEdit 编排,provider 创建/更新改为事务化并返回新字段。
代理层与响应处理
src/app/v1/_lib/proxy/forwarder.ts, src/app/v1/_lib/proxy/response-handler.ts, src/app/v1/_lib/proxy/thinking-signature-rectifier.ts
导出 DEFAULT_CODEX_USER_AGENT,加入严格端点池策略、Gemini Google Search 覆盖路径、以及对 OpenAI 风格 usage 字段(prompt/completion → input/output)的映射与 thinking-block 错误识别。
Scoped Auth 会话支持
src/lib/auth.ts, src/lib/auth-session-storage.node.ts, src/lib/api/action-adapter-openapi.ts, src/app/api/actions/[...route]/route.ts
增加基于 AsyncLocalStorage 的作用域会话存储、runWithAuthSession/getScopedAuthSession API,并在 OpenAPI action 适配器中使用该会话;路由引入 node-only 模块。
Redis pub/sub 与事件单例
src/lib/redis/pubsub.ts, src/lib/cache/provider-cache.ts, src/lib/emit-event.ts, src/lib/event-emitter.ts, src/lib/request-filter-engine.ts, src/lib/sensitive-word-detector.ts
重构订阅初始化为 Promise 驱动、添加 CHANNEL_SENSITIVE_WORDS_UPDATED、引入全局 singletons、改进重连/重订阅、并将敏感词检测器订阅联动到 pub/sub,提供销毁接口。
会话追踪 / 速率限制 / Lua 脚本
src/lib/session-tracker.ts, src/lib/redis/lua-scripts.ts, src/lib/rate-limit/service.ts
将 SESSION_TTL 配置化(env 驱动),在写入路径增加概率性清理,向 Lua 脚本传入 TTL 参数并调整脚本内清理逻辑,扩展 CostLimit 类型。
日志表格与成本列
src/lib/column-visibility.ts, src/app/[locale]/dashboard/logs/_components/... (column-visibility-dropdown.tsx, usage-logs-table.tsx, virtualized-logs-table.tsx)
"cost" 加入可切换列与默认可见列,改为 null 检查以显示 0 值,重构虚拟表格成本列渲染并加入 context1m 徽章与丰富 Tooltip。
货币显示工具
src/lib/utils/currency.ts, src/app/.../user-limit-badge.tsx, .../limit-status-indicator.tsx, .../user-key-table-row.tsx, .../users-page-client.tsx, .../big-screen/page.tsx
新增 getCurrencySymbol(),将若干组件默认单位从 "$" 改为空串,使用 systemSettings.currencyDisplay 获取货币并向子组件传递符号。
Gemini 覆盖与特殊设置
src/lib/gemini/provider-overrides.ts, src/lib/utils/special-settings.ts, src/types/special-settings.ts
新增 Gemini Google Search 注入/移除工具逻辑与审计路径,并在特殊设置序列化中加入 preferencehadGoogleSearchInRequest 字段。
敏感词/事件与仓库集成
src/repository/sensitive-words.ts, src/actions/sensitive-words.ts
在敏感词的 create/update/delete 成功后触发更新事件,移除部分显式 sensitiveWordDetector.reload() 调用,采用事件驱动重载。
工具依赖与配置变更
.gitignore, biome.json, package.json
新增 .ace-tool/.gitignore;调整 biome.json 的 $schema 引用;将 jspdf 依赖从 ^3 升级到 ^4。
测试套件与新增测试
tests/unit/**, tests/integration/provider-endpoint-sync-race.test.ts, vitest.integration.config.ts
新增/扩展大量单元与集成测试,覆盖端点同步竞态、Gemini 覆盖、pubsub、SESSION_TTL、货币工具、端点 UI 等关键路径。

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~140 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.48% 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 标题"release v0.5.4"准确反映了此PR的主要目的,即发布版本v0.5.4。标题简洁清晰,符合发布版本的命名规范。
Description check ✅ Passed PR描述详细涵盖了该版本的关键修复、新功能、改进项以及部署说明,与变更集密切相关且信息完整。
Linked Issues check ✅ Passed 代码变更完整实现了所有关联问题的需求:#714(SessionTracker TTL对齐+概率清理)、#713(成本列可切换)、#722(端点同步事务处理)、#705(OpenAI token提取)。
Out of Scope Changes check ✅ Passed 所有变更均在范围内:数据库迁移(gemini_google_search_preference)、会话追踪修复、端点池UI、热重载缓存失效、认证会话传播和OpenAI格式支持,均与关联问题或版本目标相符。

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

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.

@github-actions github-actions bot added enhancement New feature or request area:provider area:UI dependencies Pull requests that update a dependency file labels Feb 7, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @ding113, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This release focuses on strengthening the application's infrastructure for managing AI providers and their endpoints. It introduces a robust system for handling multiple endpoints per vendor, enabling more granular control and improved resilience. Key changes include new UI elements for endpoint configuration, the ability to override specific Gemini API parameters, and significant under-the-hood improvements to ensure data consistency during provider lifecycle events. Additionally, it refines cache invalidation, session management, and introduces dynamic currency display for a better user experience.

Highlights

  • Provider Endpoint Management: Introduced a new 'Endpoint Pool' system allowing multiple API endpoints per provider vendor and type. This includes dedicated UI components for viewing, adding, editing, and probing endpoints, enhancing flexibility and reliability.
  • Gemini Parameter Overrides: Added support for overriding Gemini API request parameters, specifically for Google Search (web access). Providers can now force-enable or force-disable Google Search at the provider level, with audit trails for these modifications.
  • Transactional Data Consistency: Refactored provider creation and update operations to be fully transactional. This ensures that related data, such as provider vendors and endpoints, are created, updated, or cleaned up atomically, preventing data inconsistencies.
  • Improved Cache Invalidation & Session Management: Enhanced the Redis Pub/Sub mechanism for cache invalidation, ensuring more robust and reliable updates across instances. Session tracking now uses configurable TTLs from environment variables and includes probabilistic cleanup for active sessions.
  • Dynamic Currency Display: Implemented dynamic display of currency symbols in the dashboard and user key management, based on system settings, improving localization and user experience.
  • Strict Endpoint Policy for Standard APIs: For standard API endpoints (e.g., /v1/messages), the system now enforces a strict endpoint pool policy. If no valid endpoints are found or selected for a provider, requests will fail instead of silently falling back to a potentially misconfigured legacy URL.
Changelog
  • .gitignore
    • Added '.ace-tool/' to the ignore list.
  • CHANGELOG.md
    • Added new changelog entry for v0.5.3, detailing new features, optimizations, fixes, and other changes.
  • biome.json
    • Downgraded Biome.js schema version from 2.3.11 to 2.3.10.
  • drizzle/0062_aromatic_taskmaster.sql
    • Added gemini_google_search_preference column to the providers table.
  • drizzle/meta/0062_snapshot.json
    • Updated Drizzle ORM snapshot to reflect the new gemini_google_search_preference column.
  • drizzle/meta/_journal.json
    • Updated Drizzle migration journal to include 0062_aromatic_taskmaster.
  • messages/en/settings/providers/form/errors.json
    • Added new error messages for required fields: keyRequired, nameRequired, urlRequired.
  • messages/en/settings/providers/form/sections.json
    • Added new sections: endpointPool and geminiOverrides.
  • messages/en/settings/providers/strings.json
    • Added sortOrder string.
    • Added endpointStatus object with various status messages.
  • messages/ja/settings/providers/form/errors.json
    • Added new error messages for required fields: keyRequired, nameRequired, urlRequired.
  • messages/ja/settings/providers/form/sections.json
    • Added new sections: endpointPool and geminiOverrides.
  • messages/ja/settings/providers/strings.json
    • Added sortOrder string.
    • Added endpointStatus object with various status messages.
  • messages/ru/settings/providers/form/errors.json
    • Added new error messages for required fields: keyRequired, nameRequired, urlRequired.
  • messages/ru/settings/providers/form/sections.json
    • Added new sections: endpointPool and geminiOverrides.
  • messages/ru/settings/providers/strings.json
    • Added sortOrder string.
    • Added endpointStatus object with various status messages.
  • messages/zh-CN/settings/providers/form/errors.json
    • Added new error messages for required fields: nameRequired, urlRequired, keyRequired.
  • messages/zh-CN/settings/providers/form/sections.json
    • Added new sections: endpointPool and geminiOverrides.
  • messages/zh-CN/settings/providers/strings.json
    • Added sortOrder string.
    • Added endpointStatus object with various status messages.
  • messages/zh-TW/settings/providers/form/errors.json
    • Added new error messages for required fields: keyRequired, nameRequired, urlRequired.
  • messages/zh-TW/settings/providers/form/sections.json
    • Added new sections: endpointPool and geminiOverrides.
  • messages/zh-TW/settings/providers/strings.json
    • Added sortOrder string.
    • Added endpointStatus object with various status messages.
  • package.json
    • Updated jspdf dependency from ^3 to ^4.
  • src/actions/provider-endpoints.ts
    • Added error handling around tryDeleteProviderVendorIfEmpty calls in removeProviderEndpoint.
  • src/actions/providers.ts
    • Included geminiGoogleSearchPreference in getProviders data mapping.
    • Added error handling around tryDeleteProviderVendorIfEmpty calls in removeProvider and reclusterProviderVendors.
    • Passed tx option to getOrCreateProviderVendorIdFromUrls in reclusterProviderVendors.
  • src/actions/sensitive-words.ts
    • Removed explicit sensitiveWordDetector.reload() calls, relying on event-driven updates.
  • src/app/[locale]/dashboard/_components/user/limit-status-indicator.tsx
    • Changed default unit prop from "$" to "".
  • src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx
    • Imported and utilized getCurrencySymbol for displaying currency units in limit indicators.
  • src/app/[locale]/dashboard/_components/user/user-limit-badge.tsx
    • Changed default unit prop from "$" to "".
  • src/app/[locale]/dashboard/logs/_components/column-visibility-dropdown.tsx
    • Added cost column to COLUMN_LABEL_KEYS for logs table.
  • src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
    • Modified cost display condition from log.costUsd ? to log.costUsd != null ? for accurate null handling.
  • src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
    • Updated VirtualizedLogsTableColumn to LogsTableColumn.
    • Implemented hideCostColumn logic to conditionally render the cost column.
  • src/app/[locale]/dashboard/users/users-page-client.tsx
    • Integrated fetching of system settings for currencyDisplay to dynamically show currency symbols.
  • src/app/[locale]/internal/dashboard/big-screen/page.tsx
    • Used currencySymbol for displaying costs in user and provider rankings.
  • src/app/[locale]/settings/providers/_components/endpoint-status.ts
    • New file: Defines types and a utility function getEndpointStatusModel for determining and displaying provider endpoint health status in the UI.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
    • Integrated useQuery and useQueryClient for fetching vendor and endpoint data.
    • Implemented normalizeWebsiteDomainFromUrl to derive domain from URL.
    • Introduced logic to display an 'Endpoint Pool' section and conditionally hide the traditional URL input if a vendor is resolved and has active endpoints.
    • Added gemini_google_search_preference to provider form data.
    • Invalidated relevant queries on successful provider add.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
    • Added geminiGoogleSearchPreference to the initial state and reducer actions for provider forms.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts
    • Defined GeminiGoogleSearchPreference type and added geminiGoogleSearchPreference to RoutingState and ProviderFormAction.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/basic-info-section.tsx
    • Reordered sections to place Website URL before API Endpoint.
    • Conditionally renders ProviderEndpointsSection or the traditional URL input based on endpoint pool status.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
    • Added a new section for 'Gemini Parameter Overrides' to configure Google Search preference for Gemini providers.
  • src/app/[locale]/settings/providers/_components/provider-endpoint-hover.tsx
    • New file: Component to display detailed endpoint status information on hover, including circuit breaker state and latency.
  • src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx
    • New file: Comprehensive set of components (ProviderEndpointsTable, AddEndpointButton, EditEndpointDialog, ProviderEndpointsSection) for managing provider endpoints, including CRUD operations, probing, and status display.
  • src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx
    • Integrated ProviderEndpointHover to display endpoint pool status for providers with associated vendors.
    • Displays vendor name instead of raw URL when a vendor is linked.
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
    • Refactored to use the new ProviderEndpointsSection component for displaying and managing vendor endpoints.
  • src/app/v1/_lib/proxy/forwarder.ts
    • Defined DEFAULT_CODEX_USER_AGENT for Codex CLI requests.
    • Introduced STRICT_STANDARD_ENDPOINTS to enforce endpoint pool usage for specific API paths.
    • Implemented strict endpoint selection logic, preventing fallback to provider.url if endpoint selection fails for standard endpoints.
    • Added applyGeminiGoogleSearchOverrideWithAudit to modify Gemini API requests based on geminiGoogleSearchPreference and record audit trails.
  • src/app/v1/_lib/proxy/response-handler.ts
    • Enhanced extractUsageMetrics to correctly parse prompt_tokens and completion_tokens from OpenAI-compatible responses.
  • src/app/v1/_lib/proxy/thinking-signature-rectifier.test.ts
    • Added test cases to detect 'cannot be modified' errors in Anthropic thinking blocks.
  • src/app/v1/_lib/proxy/thinking-signature-rectifier.ts
    • Extended detectThinkingSignatureRectifierTrigger to identify errors related to modified thinking/redacted_thinking blocks.
  • src/drizzle/schema.ts
    • Added gemini_google_search_preference column to the providers table schema.
  • src/lib/api/action-adapter-openapi.ts
    • Integrated runWithAuthSession to ensure proper authentication session context for API actions.
  • src/lib/auth-session-storage.node.ts
    • New file: Implements AsyncLocalStorage for managing authentication session context in Node.js environments.
  • src/lib/auth.ts
    • Introduced ScopedAuthContext and AuthSessionStorage types.
    • Added runWithAuthSession, getScopedAuthSession, and getScopedAuthContext for explicit session management.
    • Modified getSession to prioritize the scoped session, improving compatibility with various runtimes.
  • src/lib/cache/provider-cache.ts
    • Refined Redis pub/sub subscription initialization with subscriptionInitPromise to prevent race conditions and ensure robust setup.
  • src/lib/column-visibility.test.ts
    • Added unit tests for toggling the visibility of the new cost column.
  • src/lib/column-visibility.ts
    • Added cost to LogsTableColumn and removed it from ALWAYS_VISIBLE_COLUMNS, allowing users to toggle its visibility.
  • src/lib/emit-event.ts
    • Added Redis pub/sub publishing for emitSensitiveWordsUpdated.
    • Enhanced logging for emitRequestFiltersUpdated events.
  • src/lib/event-emitter.ts
    • Ensured GlobalEventEmitter is a true singleton across worker contexts using globalThis.
  • src/lib/gemini/provider-overrides.ts
    • New file: Contains logic for applyGeminiGoogleSearchOverride and applyGeminiGoogleSearchOverrideWithAudit to programmatically modify Gemini API requests based on provider preferences.
  • src/lib/rate-limit/service.ts
    • Configured session TTL (SESSION_TTL_SECONDS, SESSION_TTL_MS) to be configurable via environment variables.
    • Updated the CHECK_AND_TRACK_SESSION Lua script call to pass the configured session TTL.
  • src/lib/redis/tests/pubsub.test.ts
    • Expanded test suite to cover Redis pub/sub resubscription on reconnect, connection error handling, and timeout scenarios.
  • src/lib/redis/lua-scripts.ts
    • Updated CHECK_AND_TRACK_SESSION Lua script to dynamically use the session TTL passed as an argument and added a guard for invalid TTL values.
  • src/lib/request-filter-engine.ts
    • Added detailed logging for event listener setup and filter reloading.
    • Ensured requestFilterEngine is a true singleton across worker contexts using globalThis.
  • src/lib/sensitive-word-detector.ts
    • Implemented event listener setup and destruction for SensitiveWordCache, subscribing to local and Redis pub/sub events for hot reloading.
    • Ensured sensitiveWordDetector is a true singleton across worker contexts using globalThis.
  • src/lib/session-tracker.ts
    • Made session TTL configurable via the SESSION_TTL environment variable.
    • Introduced a probabilistic cleanup mechanism for provider active session ZSETs during refreshSession.
    • Ensured session binding keys (session:${sessionId}:provider, session:${sessionId}:key, session:${sessionId}:last_seen) have their TTL refreshed dynamically.
  • src/lib/utils/currency.ts
    • Added getCurrencySymbol utility function to retrieve currency symbols based on currency codes.
  • src/lib/utils/special-settings.ts
    • Extended buildSettingKey to include gemini_google_search_override for consistent special setting key generation.
  • src/lib/validation/schemas.ts
    • Defined GEMINI_GOOGLE_SEARCH_PREFERENCE Zod schema.
    • Integrated gemini_google_search_preference into CreateProviderSchema and UpdateProviderSchema.
  • src/repository/_shared/transformers.ts
    • Included geminiGoogleSearchPreference in the toProvider transformation logic.
  • src/repository/provider-endpoints.ts
    • Added isUniqueViolationError helper for database error handling.
    • Modified getOrCreateProviderVendorIdFromUrls and tryDeleteProviderVendorIfEmpty to accept an optional tx parameter for transactional consistency.
    • Updated ensureProviderEndpointExistsForUrl to throw specific errors for invalid URLs instead of returning false.
    • Introduced syncProviderEndpointOnProviderEdit to manage provider endpoint lifecycle during provider updates, handling in-place updates, soft deletes, and circuit breaker resets within a transaction.
  • src/repository/provider.ts
    • Refactored createProvider to perform vendor creation and endpoint seeding within a single database transaction.
    • Refactored updateProvider to leverage syncProviderEndpointOnProviderEdit for robust endpoint management and tryDeleteProviderVendorIfEmpty for vendor cleanup, all within a transaction.
    • Updated provider queries (findProviderList, findAllProvidersFresh, findProviderById) to include geminiGoogleSearchPreference.
  • src/repository/sensitive-words.ts
    • Integrated emitSensitiveWordsUpdated calls after createSensitiveWord, updateSensitiveWord, and deleteSensitiveWord operations to trigger cache invalidation.
  • src/types/provider.ts
    • Defined GeminiGoogleSearchPreference type.
    • Added geminiGoogleSearchPreference to Provider, ProviderDisplay, CreateProviderData, and UpdateProviderData interfaces.
  • src/types/special-settings.ts
    • Defined GeminiGoogleSearchOverrideSpecialSetting type for auditing Gemini Google Search overrides.
  • tests/api/action-adapter-auth-session.unit.test.ts
    • New test file: Verifies that authentication sessions are correctly propagated and accessible within action functions when using the OpenAPI action adapter.
  • tests/integration/provider-endpoint-sync-race.test.ts
    • New test file: Integration tests to ensure syncProviderEndpointOnProviderEdit handles concurrent updates and race conditions correctly.
  • tests/unit/lib/gemini/provider-overrides.test.ts
    • New test file: Unit tests for the applyGeminiGoogleSearchOverride and applyGeminiGoogleSearchOverrideWithAudit functions, covering various scenarios for Gemini API request modification.
  • tests/unit/lib/hot-reload-singleton.test.ts
    • New test file: Unit tests for the globalThis singleton pattern for event emitters and request filter engine.
  • tests/unit/lib/session-tracker-cleanup.test.ts
    • New test file: Unit tests for SessionTracker TTL and probabilistic cleanup.
  • tests/unit/lib/session-ttl-validation.test.ts
    • New test file: Unit tests for SESSION_TTL environment variable validation.
  • tests/unit/lib/utils/currency.test.ts
    • New test file: Unit tests for the getCurrencySymbol utility function.
  • tests/unit/repository/provider-create-transaction.test.ts
    • New test file: Unit tests for provider creation transaction.
  • tests/unit/repository/provider-endpoint-sync-helper.test.ts
    • New test file: Unit tests for provider endpoint sync helper.
  • tests/unit/repository/provider-endpoint-sync-on-edit.test.ts
    • New test file: Unit tests for provider endpoint sync on edit.
  • tests/unit/repository/sensitive-words-events.test.ts
    • New test file: Unit tests for sensitive words repository events.
  • tests/unit/settings/providers/endpoint-status.test.ts
    • New test file: Unit tests for endpoint status model.
  • tests/unit/settings/providers/provider-endpoint-hover.test.ts
    • New test file: Unit tests for provider endpoint hover component.
  • tests/unit/settings/providers/provider-endpoints-table.test.ts
    • New test file: Unit tests for provider endpoints table component.
  • tests/unit/settings/providers/provider-form-endpoint-pool.test.ts
    • New test file: Unit tests for provider form endpoint pool integration.
  • tests/unit/settings/providers/provider-rich-list-item-endpoints.test.ts
    • New test file: Unit tests for provider rich list item endpoint display.
Activity
  • The pull request is titled 'release v0.5.4', indicating a new version release.
  • The changelog for v0.5.3 is added, detailing new features, optimizations, and fixes.
  • New files related to provider endpoint management (endpoint-status.ts, provider-endpoint-hover.tsx, provider-endpoints-table.tsx) and Gemini overrides (gemini/provider-overrides.ts) have been introduced.
  • Database schema changes are reflected in drizzle migration files, adding gemini_google_search_preference to the providers table.
  • Core logic for provider and endpoint handling in src/actions/providers.ts, src/repository/provider.ts, and src/repository/provider-endpoints.ts has been significantly refactored for transactional consistency and new features.
  • Internationalization files (messages/*/settings/providers/*) have been updated to support new UI elements and features.
  • Extensive unit and integration tests have been added for new features and refactored logic, covering areas like endpoint synchronization, session management, and UI components.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions bot added the size/XL Extra Large PR (> 1000 lines) label Feb 7, 2026
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

9 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 252 to +255
});
endpointCandidates.push(...preferred.map((e) => ({ endpointId: e.id, baseUrl: e.url })));
} catch (error) {
endpointSelectionError =
Copy link

Choose a reason for hiding this comment

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

Strict policy doesn’t exit

When shouldEnforceStrictEndpointPool is true and endpointCandidates.length === 0, the code sets attemptCount = maxAttemptsPerProvider but does not continue/break. The function then continues and can proceed with an empty endpointCandidates array, leading to undefined endpoint selection / request base URL downstream.

Suggested change
});
endpointCandidates.push(...preferred.map((e) => ({ endpointId: e.id, baseUrl: e.url })));
} catch (error) {
endpointSelectionError =
failedProviderIds.push(currentProvider.id);
attemptCount = maxAttemptsPerProvider;
continue;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 252:255

Comment:
**Strict policy doesn’t exit**

When `shouldEnforceStrictEndpointPool` is true and `endpointCandidates.length === 0`, the code sets `attemptCount = maxAttemptsPerProvider` but does not `continue`/`break`. The function then continues and can proceed with an empty `endpointCandidates` array, leading to undefined endpoint selection / request base URL downstream.

```suggestion
          failedProviderIds.push(currentProvider.id);
          attemptCount = maxAttemptsPerProvider;
          continue;
```

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

Comment on lines 162 to 170
// 优先读取 adapter 注入的请求级会话(适配 /api/actions 等非 Next 原生上下文场景)
const scoped = getScopedAuthContext();
if (scoped) {
// 关键:scoped 会话必须遵循其“创建时语义”,并允许内部显式降权校验
const effectiveAllowReadOnlyAccess = options?.allowReadOnlyAccess ?? scoped.allowReadOnlyAccess;
if (!effectiveAllowReadOnlyAccess && !scoped.session.key.canLoginWebUi) {
return null;
}
return scoped.session;
Copy link

Choose a reason for hiding this comment

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

Caller can widen access

In the scoped-session path, effectiveAllowReadOnlyAccess uses options?.allowReadOnlyAccess ?? scoped.allowReadOnlyAccess, which lets an inner caller override the adapter’s original allowReadOnlyAccess decision. That can effectively “upgrade” a scoped read-only session to allow usage where the adapter meant to enforce stricter semantics. Consider making the scoped context authoritative (or only allow further restriction).

Suggested change
// 优先读取 adapter 注入的请求级会话(适配 /api/actions 等非 Next 原生上下文场景)
const scoped = getScopedAuthContext();
if (scoped) {
// 关键:scoped 会话必须遵循其“创建时语义”,并允许内部显式降权校验
const effectiveAllowReadOnlyAccess = options?.allowReadOnlyAccess ?? scoped.allowReadOnlyAccess;
if (!effectiveAllowReadOnlyAccess && !scoped.session.key.canLoginWebUi) {
return null;
}
return scoped.session;
const effectiveAllowReadOnlyAccess = scoped.allowReadOnlyAccess && (options?.allowReadOnlyAccess ?? true);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/auth.ts
Line: 162:170

Comment:
**Caller can widen access**

In the scoped-session path, `effectiveAllowReadOnlyAccess` uses `options?.allowReadOnlyAccess ?? scoped.allowReadOnlyAccess`, which lets an inner caller override the adapter’s original `allowReadOnlyAccess` decision. That can effectively “upgrade” a scoped read-only session to allow usage where the adapter meant to enforce stricter semantics. Consider making the scoped context authoritative (or only allow further restriction).

```suggestion
    const effectiveAllowReadOnlyAccess = scoped.allowReadOnlyAccess && (options?.allowReadOnlyAccess ?? true);
```

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

Comment on lines +86 to +89
}

function onReady(): void {
cleanup();
Copy link

Choose a reason for hiding this comment

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

Clearing subscriptions breaks resubscribe

On reconnect onReady() does subscribedChannels.clear() and then triggers resubscribeAll(sub), but resubscribeAll only subscribes channels not in subscribedChannels. Because subscribedChannels is cleared, this is fine; the real issue is subscribedChannels is also cleared on close/end without awaiting/serializing against any in-flight subscribeCacheInvalidation() calls, so you can end up with subscribedChannels missing a channel while Redis is subscribed (or vice versa), causing missed resubscribe / duplicate subscribe attempts.

A safer pattern is to (a) avoid using subscribedChannels as the source of truth, or (b) guard mutations with a single reconnect/subscription mutex and always derive desired channels from subscriptions.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/redis/pubsub.ts
Line: 86:89

Comment:
**Clearing subscriptions breaks resubscribe**

On reconnect `onReady()` does `subscribedChannels.clear()` and then triggers `resubscribeAll(sub)`, but `resubscribeAll` only subscribes channels *not* in `subscribedChannels`. Because `subscribedChannels` is cleared, this is fine; the real issue is `subscribedChannels` is also cleared on `close`/`end` without awaiting/serializing against any in-flight `subscribeCacheInvalidation()` calls, so you can end up with `subscribedChannels` missing a channel while Redis is subscribed (or vice versa), causing missed resubscribe / duplicate subscribe attempts.

A safer pattern is to (a) avoid using `subscribedChannels` as the source of truth, or (b) guard mutations with a single reconnect/subscription mutex and always derive desired channels from `subscriptions`.


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

Comment on lines +93 to +104
* Apply Gemini Google Search override with audit trail
*/
export function applyGeminiGoogleSearchOverrideWithAudit(
provider: GeminiProviderOverrideConfig,
request: Record<string, unknown>
): { request: Record<string, unknown>; audit: GeminiGoogleSearchOverrideSpecialSetting | null } {
if (provider.providerType !== "gemini" && provider.providerType !== "gemini-cli") {
return { request, audit: null };
}

const preference = provider.geminiGoogleSearchPreference;

Copy link

Choose a reason for hiding this comment

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

Unknown preference is silently ignored

If geminiGoogleSearchPreference is set to an unexpected string, applyGeminiGoogleSearchOverrideWithAudit returns {audit: null} and performs no override, which makes misconfigurations hard to detect. Since this preference comes from persisted provider settings, it’s better to treat unknown values as an error/audit event (or coerce to inherit) so operators can spot bad data.

Suggested change
* Apply Gemini Google Search override with audit trail
*/
export function applyGeminiGoogleSearchOverrideWithAudit(
provider: GeminiProviderOverrideConfig,
request: Record<string, unknown>
): { request: Record<string, unknown>; audit: GeminiGoogleSearchOverrideSpecialSetting | null } {
if (provider.providerType !== "gemini" && provider.providerType !== "gemini-cli") {
return { request, audit: null };
}
const preference = provider.geminiGoogleSearchPreference;
} else {
return { request, audit: {
type: "gemini_google_search_override",
scope: "request",
hit: true,
providerId: provider.id ?? null,
providerName: provider.name ?? null,
action: "passthrough",
preference: "disabled",
hadGoogleSearchInRequest: hadGoogleSearch,
}};
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/gemini/provider-overrides.ts
Line: 93:104

Comment:
**Unknown preference is silently ignored**

If `geminiGoogleSearchPreference` is set to an unexpected string, `applyGeminiGoogleSearchOverrideWithAudit` returns `{audit: null}` and performs no override, which makes misconfigurations hard to detect. Since this preference comes from persisted provider settings, it’s better to treat unknown values as an error/audit event (or coerce to `inherit`) so operators can spot bad data.

```suggestion
  } else {
    return { request, audit: {
      type: "gemini_google_search_override",
      scope: "request",
      hit: true,
      providerId: provider.id ?? null,
      providerName: provider.name ?? null,
      action: "passthrough",
      preference: "disabled",
      hadGoogleSearchInRequest: hadGoogleSearch,
    }};
  }
```

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

@greptile-apps
Copy link

greptile-apps bot commented Feb 7, 2026

Additional Comments (1)

src/repository/provider-endpoints.ts
Behavior change: now throws

ensureProviderEndpointExistsForUrl previously returned false on empty/invalid URLs; it now throws. Callers that didn’t already wrap this (or rely on a best-effort seed) will now fail hard inside transactions (e.g., provider create/update). If the intent is still “best effort”, convert these back to a non-throwing result or ensure all call sites handle the exception and return a user-facing validation error.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/repository/provider-endpoints.ts
Line: 736:742

Comment:
**Behavior change: now throws**

`ensureProviderEndpointExistsForUrl` previously returned `false` on empty/invalid URLs; it now throws. Callers that didn’t already wrap this (or rely on a best-effort seed) will now fail hard inside transactions (e.g., provider create/update). If the intent is still “best effort”, convert these back to a non-throwing result or ensure *all* call sites handle the exception and return a user-facing validation error.


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

Copy link
Contributor

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

Choose a reason for hiding this comment

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

Code Review

这个 PR 主要是为 v0.5.4 版本发布做准备,包含了大量的新功能、优化和修复。

主要变更包括:

  • 供应商端点管理:引入了供应商端点池的概念,允许为每个供应商配置多个 API 端点,并实现了健康探测和故障转移。这是本次更新的核心功能,涉及前后端大量改动。
  • Gemini 参数覆写:新增了在供应商层面覆写 Gemini googleSearch 参数的功能。
  • 架构改进
    • 对供应商和端点的创建、更新操作实现了事务性,保证了数据一致性。
    • 使用 AsyncLocalStorage 改造了认证会话的传递方式,解决了在某些环境下 getSession 无法获取会话的问题。
    • 增强了 Redis Pub/Sub 机制的可靠性,包括处理重连、连接错误和超时。
    • 将多个核心服务(如 eventEmitter, requestFilterEngine)改造为使用 globalThis 的单例模式,以确保在多 worker 环境下的一致性。
  • UI/UX 优化
    • 供应商表单和列表页重构,以支持新的端点管理功能。
    • 在仪表盘、用户管理等多个页面增加了多币种显示支持。
  • 可靠性增强
    • 对多个模块(如 Redis Pub/Sub、Session 追踪、端点同步逻辑)增加了更健壮的错误处理和日志记录。
    • 增加了大量的单元测试和集成测试,覆盖了新功能和关键的重构部分。

整体来看,这是一个高质量的 PR,代码结构清晰,考虑周全,特别是对系统稳定性和数据一致性的改进非常出色。

我提出了一些关于版本号、依赖项变更的疑问,请确认。


---

## v0.5.3 (2026-02-03)
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

PR 标题是 release v0.5.4,但 CHANGELOG.md 中添加的是 v0.5.3 的更新日志。请确认版本号是否正确,以避免发布时产生混淆。

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

注意到 Biome 的 schema URL 从 2.3.11 降级到了 2.3.10。通常依赖会进行升级,请问这次降级是出于什么考虑?是为了解决特定版本的回归问题或兼容性问题吗?

"html2canvas": "^1",
"ioredis": "^5",
"jspdf": "^3",
"jspdf": "^4",
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

jspdf^3 升级到了 ^4,这是一个主版本更新,通常意味着可能存在破坏性变更。请确认项目中所有使用 jspdf 的功能(例如导出 PDF)是否已经过充分测试,以确保升级没有引入回归问题。

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
package.json (1)

3-3: ⚠️ Potential issue | 🟡 Minor

package.json 版本号与 PR 标题不一致。

PR 标题为 "release v0.5.4",但 version 字段仍为 "0.5.0"。如果此 PR 用于发布 v0.5.4,请更新版本号。

src/lib/redis/lua-scripts.ts (1)

84-112: ⚠️ Potential issue | 🟡 Minor

BATCH_CHECK_SESSION_LIMITS 定义中硬编码的 TTL 与 CHECK_AND_TRACK_SESSION 的可配置 TTL 不一致,但此脚本当前未在代码中使用。

BATCH_CHECK_SESSION_LIMITS(第 87 行)硬编码 ttl = 300000,而 CHECK_AND_TRACK_SESSION 接受自定义 ttlMs 参数(ARGV[4]),可通过 SESSION_TTL_MS 配置。若未来需要使用此脚本,当 SESSION_TTL_MS 被修改时,两个脚本的清理行为将不一致。

建议:若 BATCH_CHECK_SESSION_LIMITS 计划使用,应改为接受 TTL 参数;否则考虑移除此未使用的脚本定义。

src/lib/session-tracker.ts (1)

504-525: ⚠️ Potential issue | 🟡 Minor

JSDoc 和行内注释中的 "5 分钟" 描述已过时。

countFromZSet 的文档注释(Line 508)和行内注释(Line 524)仍写着 "5 分钟前",但实际清理窗口现在由 SESSION_TTL 环境变量驱动。建议更新为准确描述(如 "SESSION_TTL 秒前")。类似地,文件顶部 Line 9 的 "5 分钟无活动" 也已不再准确。

建议修改注释
   * 实现步骤:
-  * 1. ZREMRANGEBYSCORE 清理过期 session(5 分钟前)
+  * 1. ZREMRANGEBYSCORE 清理过期 session(SESSION_TTL 秒前)
-    // 1. 清理过期 session(5 分钟前)
+    // 1. 清理过期 session(基于 SESSION_TTL)
tests/unit/settings/providers/provider-form-total-limit-ui.test.tsx (1)

141-191: ⚠️ Potential issue | 🟠 Major

添加缺失的 geminiGoogleSearchPreference 字段。

ProviderDisplay 类型(src/types/provider.ts 第 212 行)要求该字段为 geminiGoogleSearchPreference: GeminiGoogleSearchPreference | null,但测试对象中缺少此字段,会导致 TypeScript 编译错误。

修复建议
      anthropicMaxTokensPreference: null,
      anthropicThinkingBudgetPreference: null,
+     geminiGoogleSearchPreference: null,
      tpm: null,
🤖 Fix all issues with AI agents
In `@CHANGELOG.md`:
- Line 7: The changelog currently only contains the heading "## v0.5.3
(2026-02-03)" while the PR is titled "release v0.5.4"; either add a new "##
v0.5.4 (YYYY-MM-DD)" section to CHANGELOG.md with the release date and a short
bullet list of changes introduced in this PR, or if the PR title is incorrect,
rename the PR to match the existing "v0.5.3" entry; locate the header "## v0.5.3
(2026-02-03)" in CHANGELOG.md and add the new v0.5.4 header and entries (or
update the PR title) accordingly.

In `@messages/ru/settings/providers/strings.json`:
- Around line 114-115: The values for the keys "circuitOpen" and
"circuitHalfOpen" use an inconsistent English/Russian mix; update their
translations to match the existing Russian terminology (e.g., replace "Circuit
открыт" with "Цепь открыта" and "Circuit полуоткрыт" with "Цепь полуоткрыта") so
they are consistent with other entries like "Цепь разомкнута" and "Состояние
цепи"; ensure you only change the values for the keys "circuitOpen" and
"circuitHalfOpen" and keep JSON syntax intact.

In `@messages/zh-CN/settings/providers/form/errors.json`:
- Around line 5-7: The new keys nameRequired and urlRequired use the term "供应商"
while existing keys addFailed, updateFailed, deleteFailed use "服务商"; update the
messages for nameRequired and urlRequired to use the same term "服务商" so
terminology is consistent across the file (leave keyRequired as-is), locating
the strings by the JSON keys nameRequired and urlRequired and changing their
values to mirror the wording style of addFailed/updateFailed/deleteFailed.

In
`@src/app/`[locale]/settings/providers/_components/provider-endpoints-table.tsx:
- Around line 304-307: The time string uses formatDistanceToNow without a
locale, causing English output for non-English users; update the component that
renders endpoint.lastProbedAt to pass a date-fns locale via the options (e.g.,
formatDistanceToNow(..., { addSuffix: true, locale: dateFnsLocale })). Add or
reuse a small mapping or helper (e.g., getDateFnsLocale or a switch keyed by the
current locale variable used in the route like locale) that returns the correct
date-fns locale object for zh-CN, zh-TW, en, ja, ru, import the needed locales
from date-fns/locale, and pass that mapped value to formatDistanceToNow so the
displayed relative time is localized.

In `@src/lib/redis/pubsub.ts`:
- Around line 60-133: Update error-rule-detector.ts to check the return value of
subscribeCacheInvalidation (same as in sensitive-word-detector.ts,
request-filter-engine.ts, provider-cache.ts): after awaiting
subscribeCacheInvalidation(CHANNEL_ERROR_RULES_UPDATED, handleUpdated) verify it
is not null and handle the null case (log and/or return) so callers know
subscription failed. Also harden ensureSubscriber: when the duplicated client
emits "close" or "end" mark subscriberReady = null and subscriberClient = null
(in the handlers already wired in ensureSubscriber) so a permanently lost
connection doesn't leave subscriberReady resolved with a dead sub; ensure fail()
and onError() also clear subscriberClient/subscriberReady (they already clear
subscriberReady but add subscriberClient = null) so subsequent calls to
ensureSubscriber will recreate the subscriber and trigger resubscribeAll as
intended (references: ensureSubscriber, subscriberReady, subscriberClient,
resubscribeAll, subscribeCacheInvalidation, CHANNEL_ERROR_RULES_UPDATED,
handleUpdated).

In `@tests/unit/settings/providers/provider-endpoint-hover.test.tsx`:
- Around line 210-229: The test "sorts endpoints correctly: Healthy > Unknown >
Unhealthy" asserts display names but the component (ProviderEndpointHover)
actually renders endpoint URLs; update the assertion in that test (the
expect(labels).toEqual(...)) to assert the endpoint URLs in the correct sorted
order (matching mockEndpoints returned by
providerEndpointsActionMocks.getProviderEndpointsByVendor) — i.e., replace the
human-readable labels with the corresponding URL strings for Healthy, Unknown,
and Unhealthy endpoints so the expected array matches the rendered content.
🧹 Nitpick comments (28)
src/app/v1/_lib/proxy/thinking-signature-rectifier.ts (1)

73-82: lower.includes("redacted_thinking") 条件是冗余的。

"redacted_thinking" 包含子串 "thinking",所以 lower.includes("thinking") 已经覆盖了 lower.includes("redacted_thinking") 的情况,|| 右侧永远不会被求值到。

当前代码功能正确,保留也无害(可视为自文档化),仅提醒注意。

src/app/v1/_lib/proxy/thinking-signature-rectifier.test.ts (1)

92-106: 测试描述语言不一致:此处使用英文,其余均为中文。

文件中其他所有测试用例(如 "应命中:...", "不应命中:..." 等)均使用中文描述,建议保持一致。

建议修改
-    test("should detect 'cannot be modified' error", () => {
+    test("应命中:thinking/redacted_thinking blocks cannot be modified(thinking 块被修改)", () => {
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx (1)

610-654: Cost 列的 tooltip 内容与 usage-logs-table.tsx 存在差异。

虚拟化表格中的 Cost tooltip(Lines 639-646)仅显示 input/output tokens,而 usage-logs-table.tsx(Lines 380-428)还包含 cache write 5m/1h、cache read、>200k 上下文加价提示和 multiplier 信息。如果这是有意简化可以忽略,否则建议保持两处一致,避免用户在不同视图下看到不同程度的计费明细。

tests/unit/actions/providers-recluster.test.ts (1)

214-239: 事务上下文传播测试看起来正确。

tx 对象的 mock 链(update → set → where)以及在 getOrCreateProviderVendorIdFromUrlsMock 断言中验证 { tx } 传播是合理的,确保了事务性操作的正确性。

注意到第 261-269 行的 "publishes cache invalidation after apply" 测试中仍使用内联 tx 对象且未验证 { tx } 传播。如果后续需要统一事务上下文验证,可以考虑在该测试中也提取 tx 并添加类似断言。

tests/unit/lib/utils/currency.test.ts (1)

28-35: codes 数组可以从 CURRENCY_CONFIG 的键派生,避免手动维护。

如果将来新增货币类型,这个硬编码列表可能遗忘更新。可以用 Object.keys(CURRENCY_CONFIG) 代替。

♻️ 建议的改进
  test("all CURRENCY_CONFIG entries have valid symbols", () => {
-    const codes: CurrencyCode[] = ["USD", "CNY", "EUR", "JPY", "GBP", "HKD", "TWD", "KRW", "SGD"];
+    const codes = Object.keys(CURRENCY_CONFIG) as CurrencyCode[];
    for (const code of codes) {
tests/unit/lib/session-tracker-cleanup.test.ts (1)

1-53: 测试辅助代码与 session-ttl-validation.test.ts 存在大量重复。

makePipeline 工厂函数、pipelineCalls 数组、logger mock 和 redis mock 在两个测试文件中几乎完全相同。可以考虑提取为共享的测试辅助模块(如 tests/unit/lib/__helpers__/session-tracker-mocks.ts),减少维护成本。

src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx (2)

339-345: 删除确认使用了浏览器原生 confirm(),与应用其他地方的 UX 不一致。

同文件中的 DeleteVendorDialog(在 provider-vendor-view.tsx 中)使用了 AlertDialog 组件实现两步确认。此处删除 endpoint 的操作直接调用 confirm(),体验不一致且无法被主题样式覆盖。建议改用 AlertDialog 或至少使用自定义确认对话框。


389-391: 类型过滤使用硬编码排除列表。

["claude-auth", "gemini-cli"] 被硬编码为不可选类型。如果后续新增类似的非用户可选类型,这里容易被遗漏。考虑在 getProviderTypeConfig 中增加 selectableuserCreatable 属性来驱动过滤逻辑。

src/app/[locale]/dashboard/users/users-page-client.tsx (1)

195-204: 系统设置的货币获取逻辑合理,有一个小冗余。

staleTime: 30_000 与外层 QueryClient 默认配置(第 38 行 staleTime: 30000)重复,可以省略。不影响功能,仅为冗余配置。

src/lib/emit-event.ts (1)

57-82: emitRequestFiltersUpdated 新增日志可观测性,但三个 emit* 函数之间的日志一致性值得考虑。

emitRequestFiltersUpdated 添加了详细的 info/warn 日志,而 emitErrorRulesUpdatedemitSensitiveWordsUpdated 仍然是静默的。如果这是有意为之(例如 request filters 调试需求更高),可以忽略;否则建议统一三个函数的日志策略。

src/lib/auth-session-storage.node.ts (1)

11-13: 在断言处添加注释解释类型不兼容的原因。

AsyncLocalStorage<ScopedAuthContext>run 方法签名接受 callback: (...args: unknown[]) => T,而 AuthSessionStorage 要求 callback: () => T。类型不兼容导致 TypeScript 无法直接转换,双重断言是必要的。建议添加代码注释说明:

// AsyncLocalStorage.run 的 callback 接受可变参数,而 AuthSessionStorage 不接受参数
// 因此需要通过 as unknown 规避类型检查
new AsyncLocalStorage<ScopedAuthContext>() as unknown as AuthSessionStorage;
src/lib/auth.ts (1)

37-45: runWithAuthSession 在 storage 不可用时静默降级,建议确认是否需要日志。

globalThis.__cchAuthSessionStorage 未初始化时,fn() 会在无 scoped 上下文的情况下执行。这在非 Node.js 环境(如 Edge Runtime)下是合理的降级,但在本应有 storage 却因初始化顺序问题缺失的场景下,可能导致难以排查的 session 丢失问题。

tests/unit/settings/providers/provider-rich-list-item-endpoints.test.tsx (2)

61-68: 清理多余的注释。

第 61-63 行的注释看起来是开发过程中的思考记录,建议精简:

-// Mock ProviderEndpointHover to avoid complex children rendering if needed,
-// but we want to check if it's rendered.
-// Actually, let's NOT mock it fully, or mock it to render a simple test id.
+// Mock ProviderEndpointHover with a simple stub to verify rendering

143-165: renderWithProviders 未在 beforeEach 中自动清理,依赖手动调用 unmount()

当前模式下,如果测试中途抛异常导致 unmount() 未执行,DOM 容器会残留。beforeEach 中的 document.body 清理(第 183-185 行)可以兜底,但建议考虑使用 afterEach 自动清理或 @testing-library/reactrender 来简化。

当前实现可以正常工作,仅作为改进建议。

src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx (1)

100-104: 冗余的 await

queryFnasync () => await getProviderVendors()await 是多余的,直接返回 promise 即可。

建议简化
   const { data: vendors = [] } = useQuery({
     queryKey: ["provider-vendors"],
-    queryFn: async () => await getProviderVendors(),
+    queryFn: () => getProviderVendors(),
     staleTime: 60000,
   });
src/app/[locale]/settings/providers/_components/provider-endpoint-hover.tsx (1)

132-135: labelKey 前缀剥离方式脆弱,考虑改用已限定作用域的 key。

statusModel.labelKey 是全限定 key(如 "settings.providers.endpointStatus.healthy"),而 useTranslations("settings.providers") 返回的 t 已经限定了命名空间作用域,因此这里用 .replace("settings.providers.", "") 来剥离前缀。如果 endpoint-status.ts 中的 labelKey 前缀发生变化,此处会静默失败并显示 fallback 文本。

建议让 EndpointStatusModel 直接提供相对 key(如 "endpointStatus.healthy"),或在 endpoint-status.ts 中导出命名空间常量以消除硬编码耦合。

tests/unit/settings/providers/provider-form-endpoint-pool.test.tsx (1)

11-14: src/ 目录下的文件应使用 @/ 路径别名。

第 11、12、14 行使用了相对路径引用 src/ 目录下的文件,而同文件中的 vi.mock 调用(第 33、38、43、57 行)已经正确使用了 @/ 路径别名。建议统一使用 @/ 以保持一致性。

建议修改
-import { ProviderForm } from "../../../../src/app/[locale]/settings/providers/_components/forms/provider-form";
-import { Dialog } from "../../../../src/components/ui/dialog";
+import { ProviderForm } from "@/app/[locale]/settings/providers/_components/forms/provider-form";
+import { Dialog } from "@/components/ui/dialog";
 import enMessages from "../../../../messages/en";
-import type { ProviderEndpoint, ProviderVendor } from "../../../../src/types/provider";
+import type { ProviderEndpoint, ProviderVendor } from "@/types/provider";

As per coding guidelines: "Use path alias @/ to reference files in ./src/ directory"

src/lib/sensitive-word-detector.ts (1)

36-38: 构造函数中触发异步初始化,建议添加注释说明。

setupEventListener() 返回的 Promise 在构造函数中被隐式丢弃。虽然内部已有完善的 try-catch 保护(不会产生未处理的 rejection),但建议加一行注释或显式 void 标记,使 fire-and-forget 的意图更明确。

建议修改
  constructor() {
-   this.setupEventListener();
+   // Fire-and-forget: errors are caught internally
+   void this.setupEventListener();
  }
src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx (3)

702-702: 已有命名导出,默认导出冗余且不符合编码规范。

第 624 行已经有 export function ProviderForm,此处的 export default 多余。根据编码规范应优先使用命名导出。

建议修改
-export default ProviderForm;

As per coding guidelines, "Prefer named exports over default exports".


127-137: queryKey 回退值在 enabled: false 时不会触发请求,但可读性欠佳。

endpointPoolQueryKeynull 时,enabledfalse,此时 queryKey 回退到 ["provider-endpoints", "unresolved", "provider-form"]。虽然功能上不会发起请求,但这个"占位" key 可能让维护者困惑。可以考虑添加注释说明意图。


94-98: Vendor 查询缺少 staleTime 配置。

["provider-vendors"] 查询没有设置 staleTime,在组件重渲染时可能频繁重新请求。考虑根据 vendor 数据变更频率设置合理的 staleTime

src/lib/redis/pubsub.ts (1)

20-58: resubscribeAll 的逐通道订阅可优化为批量操作。

当前逐个通道调用 sub.subscribe(channel) 进行重新订阅。如果通道数量较多,可以考虑使用 ioredis 的批量订阅(sub.subscribe(channel1, channel2, ...))来减少往返次数。鉴于当前通道数量不多,这只是一个优化建议。

tests/unit/settings/providers/provider-endpoints-table.test.tsx (1)

106-127: 使用 createRoot + 手动 DOM 管理替代 @testing-library/react

当前通过 createRoot + act 手动管理渲染和卸载。如果项目已使用 @testing-library/react,考虑使用 render + cleanup 以获得更好的测试工具支持(如 screen.getByTextwaitFor 等),可减少手动 flushTicks 的需要。

不过作为集成度较低的单元测试,当前方式也可以接受。

src/lib/gemini/provider-overrides.ts (1)

95-138: applyGeminiGoogleSearchOverrideWithAudit 内部重复了类型检查和偏好判断逻辑(可选优化)。

WithAudit 版本在 Lines 99-108 重复了与 applyGeminiGoogleSearchOverride 相同的 provider type 和 preference 前置检查,然后在 Line 124 又调用了后者(后者会再次执行这些检查)。当前这种方式正确且易读,但如果未来前置逻辑发生变化,需要同步修改两处。

可以考虑提取一个共享的 resolvePreference 内部函数来去重,但鉴于逻辑简单且当前可维护,不阻塞合并。

src/repository/provider.ts (1)

86-145: returning() 列选择在文件中重复了 5 次,建议提取为共享常量。

createProviderfindProviderListfindAllProvidersFreshfindProviderByIdupdateProvider 中的 .returning() / .select() 列定义几乎完全相同(约 50 个字段)。每次新增字段(如本次的 geminiGoogleSearchPreference)都需要同步修改 5 处,容易遗漏。

可以提取一个 providerColumns 常量对象:

const providerColumns = {
  id: providers.id,
  name: providers.name,
  // ... 其余字段
  geminiGoogleSearchPreference: providers.geminiGoogleSearchPreference,
  createdAt: providers.createdAt,
  updatedAt: providers.updatedAt,
  deletedAt: providers.deletedAt,
} as const;

然后在各处复用:.select(providerColumns) / .returning(providerColumns)

Also applies to: 168-222, 245-299, 326-380, 531-588

tests/unit/settings/providers/provider-form-total-limit-ui.test.tsx (2)

18-23: hasOwn 辅助函数实现过于迂回。

当前通过 (Object as unknown as { hasOwn: ... }).hasOwn(obj, prop) 绕了一层不安全的类型断言来调用 Object.hasOwn。如果测试环境不支持 Object.hasOwn(ES2022),这段代码在运行时一样会抛异常。

建议使用更可靠的方式:

推荐替代方案
-function hasOwn(obj: object, prop: PropertyKey): boolean {
-  return (Object as unknown as { hasOwn: (obj: object, prop: PropertyKey) => boolean }).hasOwn(
-    obj,
-    prop
-  );
-}
+function hasOwn(obj: object, prop: PropertyKey): boolean {
+  return Object.prototype.hasOwnProperty.call(obj, prop);
+}

11-14: 导入路径混用相对路径和 @/ 别名。

Lines 11-14 使用 ../../../../src/... 相对路径导入组件和类型,而 Lines 42、47、52、59 的 vi.mock() 调用则使用 @/ 别名。如果测试环境已配置 @/ 别名(从 mock 调用来看是支持的),建议统一使用 @/ 前缀。As per coding guidelines, "Use path alias @/ to reference files in ./src/ directory".

src/repository/provider-endpoints.ts (1)

1049-1068: Savepoint 检测的类型断言存在脆弱性,建议添加注释说明适用范围。

Line 1049-1051 将 tx 断言为可能包含 transaction 方法的类型来检测 savepoint 支持。实际上,当通过 db.transaction() 或外部 options.tx 传入时,Drizzle 的 TransactionExecutor 确实支持 .transaction()(作为 savepoint)。else 分支(Line 1064-1067)理论上不会被触发。

QueryExecutor 类型定义(Line 22-25)中刻意没有包含 transaction 方法,导致这里需要运行时检测。如果未来有人传入一个真正不支持 savepoint 的 executor,Line 1066 的 unique violation 会直接抛出到调用方,中断整个事务。

建议在 QueryExecutor 类型或此处添加简短注释说明这一设计取舍。


---

## v0.5.3 (2026-02-03)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

PR 标题为 v0.5.4,但 CHANGELOG 仅新增了 v0.5.3 条目。

当前 PR 标题为 "release v0.5.4",但 CHANGELOG 中未见 v0.5.4 的变更记录。请确认是否需要补充 v0.5.4 的条目,或者 PR 标题有误。

🤖 Prompt for AI Agents
In `@CHANGELOG.md` at line 7, The changelog currently only contains the heading
"## v0.5.3 (2026-02-03)" while the PR is titled "release v0.5.4"; either add a
new "## v0.5.4 (YYYY-MM-DD)" section to CHANGELOG.md with the release date and a
short bullet list of changes introduced in this PR, or if the PR title is
incorrect, rename the PR to match the existing "v0.5.3" entry; locate the header
"## v0.5.3 (2026-02-03)" in CHANGELOG.md and add the new v0.5.4 header and
entries (or update the PR title) accordingly.

Comment on lines +60 to 133
function ensureSubscriber(baseClient: Redis): Promise<Redis> {
if (subscriberReady) return subscriberReady;

subscriberReady = new Promise<Redis>((resolve, reject) => {
const sub = baseClient.duplicate();
let timeoutId: ReturnType<typeof setTimeout> | null = null;

function cleanup(): void {
sub.off("ready", onReady);
sub.off("error", onError);

if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
}

function fail(error: Error): void {
cleanup();
subscriberReady = null;
try {
sub.disconnect();
} catch {
// ignore
}
reject(error);
}

function onReady(): void {
cleanup();
subscriberClient = sub;
subscribedChannels.clear();

sub.on("error", (error) =>
logger.warn("[RedisPubSub] Subscriber connection error", { error })
);
sub.on("close", () => subscribedChannels.clear());
sub.on("end", () => subscribedChannels.clear());
sub.on("ready", () => void resubscribeAll(sub));

sub.on("message", (channel: string) => {
const callbacks = subscriptions.get(channel);
if (!callbacks || callbacks.size === 0) return;
for (const cb of callbacks) {
try {
cb();
} catch (error) {
logger.error("[RedisPubSub] Callback error", { channel, error });
}
}
});

logger.info("[RedisPubSub] Subscriber connection ready");
resolve(sub);
}

function onError(error: Error): void {
logger.warn("[RedisPubSub] Subscriber connection error", { error });
fail(error);
}

sub.once("ready", onReady);
sub.once("error", onError);

// Timeout 10 seconds
timeoutId = setTimeout(() => {
if (sub.status !== "ready") {
fail(new Error("Redis subscriber connection timeout"));
}
}, 10000);
});

return subscriberClient;
return subscriberReady;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/lib/redis/pubsub.ts

Repository: ding113/claude-code-hub

Length of output: 6980


🏁 Script executed:

rg -n 'subscribeCacheInvalidation' --type=ts --type=tsx --type=js --type=jsx -C3

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

rg 'subscribeCacheInvalidation' --type ts -C3

Repository: ding113/claude-code-hub

Length of output: 11572


🏁 Script executed:

# Check context around subscribeCacheInvalidation calls in error-rule-detector.ts
sed -n '1,100p' src/lib/error-rule-detector.ts | tail -50

Repository: ding113/claude-code-hub

Length of output: 1393


🏁 Script executed:

# Check sensitive-word-detector.ts around the subscribeCacheInvalidation call
sed -n '1,150p' src/lib/sensitive-word-detector.ts | tail -80

Repository: ding113/claude-code-hub

Length of output: 2061


🏁 Script executed:

# Check request-filter-engine.ts around the subscribeCacheInvalidation call
sed -n '1,150p' src/lib/request-filter-engine.ts | tail -80

Repository: ding113/claude-code-hub

Length of output: 2806


🏁 Script executed:

# Check provider-cache.ts around the subscribeCacheInvalidation call
sed -n '1,150p' src/lib/cache/provider-cache.ts | tail -80

Repository: ding113/claude-code-hub

Length of output: 1941


🏁 Script executed:

# Get the exact context around subscribeCacheInvalidation in error-rule-detector.ts
rg -n 'subscribeCacheInvalidation' src/lib/error-rule-detector.ts -B5 -A5

Repository: ding113/claude-code-hub

Length of output: 545


🏁 Script executed:

# Get the exact context in sensitive-word-detector.ts
rg -n 'subscribeCacheInvalidation' src/lib/sensitive-word-detector.ts -B5 -A10

Repository: ding113/claude-code-hub

Length of output: 834


🏁 Script executed:

# Get the exact context in request-filter-engine.ts
rg -n 'subscribeCacheInvalidation' src/lib/request-filter-engine.ts -B5 -A10

Repository: ding113/claude-code-hub

Length of output: 899


🏁 Script executed:

# Get the exact context in provider-cache.ts
rg -n 'subscribeCacheInvalidation' src/lib/cache/provider-cache.ts -B5 -A10

Repository: ding113/claude-code-hub

Length of output: 1077


需要确认 error-rule-detector.ts 中未捕获返回值的问题。

src/lib/error-rule-detector.ts 第 116 行,subscribeCacheInvalidation 的返回值未被捕获:

await subscribeCacheInvalidation(CHANNEL_ERROR_RULES_UPDATED, handleUpdated);

而其他三个调用者(sensitive-word-detector.tsrequest-filter-engine.tsprovider-cache.ts)都正确地检查了返回值是否为 null。虽然 subscribeCacheInvalidation 内部会记录失败日志,但这里的实现方式隐匿了错误信息,使得调用者无法感知订阅是否失败。

此外,还需要注意一个潜在的架构问题:如果 ensureSubscriber 成功建立连接后,Redis 连接意外断开且无法自动重连(如 Redis 服务停止),subscriberReady 将保持为已解析状态并持有一个失效的 sub 对象。后续对 ensureSubscriber 的调用会直接返回这个已解析的 Promise,导致调用者拿到失效连接并在 sub.subscribe() 时失败。虽然 resubscribeAll 机制在重连成功时会被触发,但对于永久性连接失败的情况并无重试机制。

🤖 Prompt for AI Agents
In `@src/lib/redis/pubsub.ts` around lines 60 - 133, Update error-rule-detector.ts
to check the return value of subscribeCacheInvalidation (same as in
sensitive-word-detector.ts, request-filter-engine.ts, provider-cache.ts): after
awaiting subscribeCacheInvalidation(CHANNEL_ERROR_RULES_UPDATED, handleUpdated)
verify it is not null and handle the null case (log and/or return) so callers
know subscription failed. Also harden ensureSubscriber: when the duplicated
client emits "close" or "end" mark subscriberReady = null and subscriberClient =
null (in the handlers already wired in ensureSubscriber) so a permanently lost
connection doesn't leave subscriberReady resolved with a dead sub; ensure fail()
and onError() also clear subscriberClient/subscriberReady (they already clear
subscriberReady but add subscriberClient = null) so subsequent calls to
ensureSubscriber will recreate the subscriber and trigger resubscribeAll as
intended (references: ensureSubscriber, subscriberReady, subscriberClient,
resubscribeAll, subscribeCacheInvalidation, CHANNEL_ERROR_RULES_UPDATED,
handleUpdated).

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

This is a large release PR consolidating 13 merged PRs with critical bug fixes and new features. After comprehensive analysis through 6 review perspectives, I identified 1 Critical issue that must be addressed.

PR Size: XL

  • Lines changed: 11,326 (10,249 additions + 1,077 deletions)
  • Files changed: 103

Recommendation for future releases: Consider splitting XL PRs into smaller batches:

  • Batch 1: Critical bug fixes (session tracker, provider endpoints, hot-reload)
  • Batch 2: New features (Gemini Google Search, endpoint pool UI, cost column toggle)
  • Batch 3: Minor improvements and dependency updates

This improves reviewability and reduces risk of introducing regressions.


Issues Found

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

Critical Issues (Must Fix)

1. [ERROR-SILENT] Empty catch blocks in cache invalidation events

File: src/lib/emit-event.ts
Lines: 16-18, 25-27, 39-41, 53-55, 62-64
Confidence: 95/100

Problem:
All five functions (emitErrorRulesUpdated, emitSensitiveWordsUpdated, emitRequestFiltersUpdated) contain empty catch blocks that swallow errors without logging. This violates the "No Silent Failures" principle.

Current code:

try {
  const { eventEmitter } = await import("@/lib/event-emitter");
  eventEmitter.emitErrorRulesUpdated();
} catch {
  // 忽略导入错误  <-- NO LOGGING!
}

Why this is critical:

  • Cache invalidation failures go unnoticed in production
  • Multi-instance deployments can have stale caches indefinitely
  • Debugging cache inconsistency becomes impossible without logs
  • Operators have zero visibility into event propagation failures

Suggested fix:

try {
  const { eventEmitter } = await import("@/lib/event-emitter");
  eventEmitter.emitErrorRulesUpdated();
} catch (error) {
  logger.warn("[emit-event] Failed to emit local event (graceful degradation)", {
    event: "errorRulesUpdated",
    error: error instanceof Error ? error.message : String(error),
  });
}

try {
  const { CHANNEL_ERROR_RULES_UPDATED, publishCacheInvalidation } = await import(
    "@/lib/redis/pubsub"
  );
  await publishCacheInvalidation(CHANNEL_ERROR_RULES_UPDATED);
} catch (error) {
  logger.error("[emit-event] Failed to publish cache invalidation to Redis", {
    channel: "error_rules",
    error: error instanceof Error ? error.message : String(error),
  });
}

Note: Use logger.warn for local EventEmitter failures (non-critical) and logger.error for Redis pub/sub failures (critical for multi-instance consistency).


Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - 1 Critical issue found
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Excellent (29 test files added/modified)
  • Code clarity - Good

Positive Observations

Strong test coverage:

  • 29 test files covering new features
  • Integration tests for race conditions (provider-endpoint-sync-race.test.ts)
  • Unit tests for session cleanup, TTL validation, and hot-reload patterns

Good architectural decisions:

  • Transaction wrapping for provider endpoint sync prevents race conditions
  • AsyncLocalStorage for session propagation is the correct pattern
  • Redis pub/sub + EventEmitter dual-channel approach is solid

Code quality:

  • Comprehensive error handling in most areas (except emit-event.ts)
  • Good use of TypeScript types
  • Clear comments explaining complex logic

Recommendation

APPROVE with required changes:

  1. Add logging to all catch blocks in src/lib/emit-event.ts (Critical)
  2. After fix, this PR is ready to merge

The core functionality is solid, test coverage is excellent, and the architectural decisions are sound. The single critical issue is straightforward to fix and does not require code restructuring.


Automated review by Claude AI

pipeline.zadd(`provider:${providerId}:active_sessions`, now, sessionId);
pipeline.zadd(providerZSetKey, now, sessionId);
// Use dynamic TTL based on session TTL (at least 1h to cover active sessions)
pipeline.expire(providerZSetKey, Math.max(3600, ttlSeconds));
Copy link
Contributor

Choose a reason for hiding this comment

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

[HIGH] [LOGIC-BUG] refreshSession() only refreshes TTL for provider ZSET; global/key/user ZSETs can expire or lose TTL under long-lived sessions

Evidence: src/lib/session-tracker.ts:189 only sets TTL for provider:${providerId}:active_sessions:

pipeline.zadd("global:active_sessions", now, sessionId);
pipeline.zadd(`key:${keyId}:active_sessions`, now, sessionId);
pipeline.zadd(providerZSetKey, now, sessionId);
// Use dynamic TTL based on session TTL (at least 1h to cover active sessions)
pipeline.expire(providerZSetKey, Math.max(3600, ttlSeconds));
if (userId !== undefined) {
  pipeline.zadd(`user:${userId}:active_sessions`, now, sessionId);
}

Why this is a problem: With SESSION_TTL > 3600 (or when a key sees no new sessions for > 1h), global:active_sessions / key:*:active_sessions / user:*:active_sessions can still expire at their previous TTL and then be recreated by ZADD without an EXPIRE, causing inaccurate session counts and potential Redis key leakage (no TTL).

Suggested fix:

const ttlSeconds = SessionTracker.SESSION_TTL_SECONDS;
const zsetTtlSeconds = Math.max(3600, ttlSeconds);

pipeline.zadd("global:active_sessions", now, sessionId);
pipeline.expire("global:active_sessions", zsetTtlSeconds);

pipeline.zadd(`key:${keyId}:active_sessions`, now, sessionId);
pipeline.expire(`key:${keyId}:active_sessions`, zsetTtlSeconds);

pipeline.zadd(providerZSetKey, now, sessionId);
pipeline.expire(providerZSetKey, zsetTtlSeconds);

if (userId !== undefined) {
  pipeline.zadd(`user:${userId}:active_sessions`, now, sessionId);
  pipeline.expire(`user:${userId}:active_sessions`, zsetTtlSeconds);
}

(Optional) Extend tests/unit/lib/session-tracker-cleanup.test.ts to assert expire is called for global:/key:/user: when SESSION_TTL=7200.

<EndpointLatencySparkline endpointId={endpoint.id} limit={12} />
{endpoint.lastProbedAt ? (
<span className="text-muted-foreground text-[10px] whitespace-nowrap">
{formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true })}
Copy link
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] [STANDARD-VIOLATION] Relative time display uses date-fns default (English), bypassing i18n

Evidence: src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx:306

{formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true })}

Why this is a problem: formatDistanceToNow defaults to en-US without an explicit locale, so non-English UIs will render English strings (for example, "about 1 hour ago"), violating CLAUDE.md:
i18n Required - All user-facing strings must use i18n (5 languages supported). Never hardcode display text

Suggested fix (use existing locale-aware helper):

import { useLocale } from "next-intl";
import { formatDateDistance } from "@/lib/utils/date-format";

const locale = useLocale();

{formatDateDistance(endpoint.lastProbedAt, new Date(), locale, { addSuffix: true })}

(Alternative: pass locale: getDateFnsLocale(locale) to formatDistanceToNow.)

Copy link
Contributor

@github-actions github-actions bot left a comment

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 is an XL release bundle that spans provider endpoint pooling (DB + UI), Redis hot-reload/pubsub, session TTL/rate-limit behavior, Gemini request overrides, and a dependency bump. The breadth makes regression risk high and makes it hard to reason about behavior changes (especially around Redis/session lifecycle) as a single unit.

PR Size: XL

  • Lines changed: 11326
  • Files changed: 103

Split suggestions (recommended for reviewability and safer rollback):

  • Provider endpoint pools UI + i18n message updates + UI tests
  • Provider endpoint sync/transaction changes + migration + repository tests
  • Redis pub/sub hot-reload + globalThis singleton refactors + unit tests
  • Session TTL + Lua/rate-limit/session-tracker adjustments + unit tests
  • Gemini Google Search override feature (schema/types/forwarder/tests)
  • Dependency bump(s) (jspdf) isolated

Issues Found

Category Critical High Medium Low
Logic/Bugs 1 0 1 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

Critical Issues (Must Fix)

  • src/lib/session-tracker.ts:189refreshSession() refreshes provider:*:active_sessions TTL but not global:/key:/user: ZSET TTLs; under long-lived sessions (notably when SESSION_TTL > 3600) those keys can expire early and then be recreated without TTL, leading to inaccurate counts and potential Redis key leakage.

High Priority Issues (Should Fix)

  • src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx:306formatDistanceToNow(...) is used without an explicit locale, so relative time strings will render in English in non-English locales (violates i18n requirement).

Review Coverage

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

Automated review by Codex AI

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

  • PR detected: #731 (release v0.5.4); size labeled size/XL.
  • Inline review comments posted:
    • src/lib/session-tracker.ts:189 — TTL refresh only applied to provider:*:active_sessions, not global:/key:/user: ZSETs (risk: early expiry + recreate-without-TTL leak under long-lived sessions).
    • src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx:306formatDistanceToNow(...) used without locale (relative time strings will render in English; i18n violation).
  • Summary review submitted via gh pr review --comment.

- fix(auth): prevent scoped session access widening via ?? -> && guard
- fix(i18n): standardize zh-CN provider terminology to "服务商"
- fix(i18n): use consistent Russian translations for circuit status
- fix(i18n): replace raw formatDistanceToNow with locale-aware RelativeTime
- fix(gemini): log warning for unknown google search preference values
- fix(error-rules): check subscribeCacheInvalidation return value
- fix(test): correct endpoint hover sort test to assert URLs not labels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Contributor

github-actions bot commented Feb 7, 2026

🧪 测试结果

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

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

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

24 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link

greptile-apps bot commented Feb 7, 2026

Additional Comments (1)

src/lib/auth-session-storage.node.ts
Missing module export

This module only initializes globalThis.__cchAuthSessionStorage but exports nothing, so importing it for side effects is the only way it can be used. If any code expects to import { authSessionStorage } ... (or similar) this will be undefined at runtime. Either export the initialized storage, or ensure all call sites only ever side-effect import this file and read via globalThis.__cchAuthSessionStorage (and remove any typed imports expecting a value).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/auth-session-storage.node.ts
Line: 11:15

Comment:
**Missing module export**

This module only initializes `globalThis.__cchAuthSessionStorage` but exports nothing, so importing it for side effects is the only way it can be used. If any code expects to `import { authSessionStorage } ...` (or similar) this will be `undefined` at runtime. Either export the initialized storage, or ensure all call sites only ever side-effect import this file and read via `globalThis.__cchAuthSessionStorage` (and remove any typed imports expecting a value).

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tests/unit/settings/providers/provider-endpoint-hover.test.tsx`:
- Around line 107-193: The mockEndpoints array contains fields that don't exist
on ProviderEndpoint (remove lastOk and lastLatencyMs) and is missing required
fields (add lastProbeStatusCode, lastProbeErrorType, lastProbeErrorMessage) and
uses strings for createdAt/updatedAt instead of Date objects; update the
mockEndpoints entries (the objects in the mockEndpoints constant) to remove
lastOk/lastLatencyMs, add the three missing lastProbe* properties with
appropriate values (null or numbers/strings matching the type), and convert
createdAt and updatedAt to Date instances (e.g., new Date("2024-01-01")),
applying the same fixes to all five endpoint objects so they match the
ProviderEndpoint type.
🧹 Nitpick comments (7)
tests/unit/settings/providers/provider-endpoint-hover.test.tsx (1)

62-84: renderWithProviders 中容器未在测试失败时自动清理

renderWithProviders 返回的 unmount 需要手动调用。如果测试中途抛出异常,unmount() 不会执行,容器会残留在 DOM 中。虽然 beforeEach 中有 DOM 清理逻辑(Line 102-104),但建议考虑使用 afterEach 统一清理,或配合 vitest 的 cleanup 机制,确保测试隔离性。

src/lib/auth.ts (2)

37-45: runWithAuthSession 降级逻辑合理,但静默降级可能掩盖配置问题。

globalThis.__cchAuthSessionStorageundefined 时,函数直接调用 fn() 而不注入会话上下文。这意味着后续 getScopedAuthSession() 会返回 nullgetSession() 将回退到 cookie/header 路径。

这个降级行为在客户端或 Edge Runtime 中是合理的,但如果在 Node.js 服务端(预期 storage 已初始化)出现 undefined,则属于配置遗漏,静默跳过可能导致难以排查的鉴权问题。建议至少在 process.env.NODE_ENV === "development" 时打印一条 console.warn

可选:在开发环境添加降级警告
 export function runWithAuthSession<T>(
   session: AuthSession,
   fn: () => T,
   options?: { allowReadOnlyAccess?: boolean }
 ): T {
   const storage = globalThis.__cchAuthSessionStorage;
-  if (!storage) return fn();
+  if (!storage) {
+    if (process.env.NODE_ENV === "development") {
+      console.warn("[auth] __cchAuthSessionStorage is not initialized; scoped session will not be available.");
+    }
+    return fn();
+  }
   return storage.run({ session, allowReadOnlyAccess: options?.allowReadOnlyAccess ?? false }, fn);
 }

162-172: options?.allowReadOnlyAccess ?? true 的默认值与 validateKey 中的 ?? false 不对称,逻辑正确但容易误读。

第 167 行 options?.allowReadOnlyAccess ?? true 使得 getSession() 无参数调用时,scoped 路径默认"继承 scoped context 的 allowReadOnlyAccess",而非 scoped 路径(validateKey 第 66 行)默认为 false(要求完全权限)。

经过分析,AND 语义保证了"不允许提权"——两方必须同时同意才放行,逻辑正确。但这个不对称的默认值在维护时容易引起混淆。建议补充一行注释说明 ?? true 的语义意图。

建议:添加注释说明默认值意图
     // 关键:scoped 会话必须遵循其"创建时语义",仅允许内部显式降权(不允许提权)
+    // options?.allowReadOnlyAccess 未传时默认 true,表示"不额外收紧",以继承 scoped 创建时的权限;
+    // 与 validateKey 中默认 false 不同,因为 scoped 路径已在入口处完成了首次鉴权。
     const effectiveAllowReadOnlyAccess =
       scoped.allowReadOnlyAccess && (options?.allowReadOnlyAccess ?? true);
src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx (4)

116-128: useMemo 回调中变量名 t 遮蔽了外层的 useTranslations 返回值。

Line 118 的 typeOrder.map((t, i) => ...)t 与 Line 95 的 const t = useTranslations(...) 同名,虽然作用域不同不会导致 bug,但降低了可读性且容易在后续维护中引发混淆。

建议重命名 map 参数
-    const typeIndexMap = new Map(typeOrder.map((t, i) => [t, i]));
+    const typeIndexMap = new Map(typeOrder.map((type, i) => [type, i]));

338-344: 删除确认使用了浏览器原生 confirm() 对话框。

原生 confirm() 会阻塞主线程,且样式无法定制、不符合项目 UI 规范。项目已引入 Dialog / AlertDialog 组件,建议使用 AlertDialog 替代以保持 UI 一致性并提升可访问性。

当前功能可用,可作为后续优化。


496-504: url 输入框采用了混合受控/非受控模式。

Line 503 通过 onChange 更新 url state(用于 UrlPreview 预览),但未设置 value={url},表单提交时又从 FormData 读取值。虽然功能正确(两者值始终一致),但这种半受控模式在维护时容易产生困惑。建议统一为完全受控(设置 value={url})或在提交时也从 state 读取。

统一为受控模式
             <Input
               id="url"
               name="url"
               placeholder={t("endpointUrlPlaceholder")}
               required
+              value={url}
               onChange={(e) => setUrl(e.target.value)}
             />

88-169: ProviderEndpointsTable 整体结构清晰。

查询键构建、条件数据获取、排序逻辑和加载/空状态处理均合理。一个小建议:useQuery 未设置 staleTime,默认为 0,每次组件挂载或窗口聚焦都会重新请求。如果端点数据变更频率不高,可考虑设置适当的 staleTime(如 EndpointLatencySparkline 中使用的 30_000)以减少不必要的请求。

- Export authSessionStorage from auth-session-storage.node.ts to prevent
  undefined on named imports; remove duplicate declare global block
- Fix mockEndpoints in provider-endpoint-hover test: remove nonexistent
  lastOk/lastLatencyMs fields, add missing lastProbe* fields, use Date
  objects for createdAt/updatedAt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

@@ -0,0 +1 @@
ALTER TABLE "providers" ADD COLUMN "gemini_google_search_preference" varchar(20); No newline at end of file
Copy link

Choose a reason for hiding this comment

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

Missing trailing newline

This migration file is committed without a trailing newline (\ No newline at end of file). Some tooling (formatters, certain migration runners / diffs) assumes POSIX newlines and can produce noisy diffs or fail strict checks. Add a final newline to the file to avoid these issues.

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!

Prompt To Fix With AI
This is a comment left during a code review.
Path: drizzle/0062_aromatic_taskmaster.sql
Line: 1:1

Comment:
**Missing trailing newline**

This migration file is committed without a trailing newline (`\ No newline at end of file`). Some tooling (formatters, certain migration runners / diffs) assumes POSIX newlines and can produce noisy diffs or fail strict checks. Add a final newline to the file to avoid these issues.

<sub>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!</sub>

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

Comment on lines 187 to 193
if (cbs.size === 0) {
subscriptions.delete(channel);
subscribedChannels.delete(channel);
if (subscriberClient) {
void subscriberClient.unsubscribe(channel);
}
}
Copy link

Choose a reason for hiding this comment

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

Unsubscribe can desync state

In the cleanup function, subscribedChannels.delete(channel) happens before unsubscribe(channel) completes (and unsubscribe is not awaited). If the unsubscribe fails or is delayed while another subscribeCacheInvalidation() races in, the in-memory subscribedChannels can become out-of-sync with the actual Redis subscription state, leading to missed resubscribeAll() behavior or duplicate subscribe attempts. Consider serializing subscribe/unsubscribe mutations (or making subscriptions the single source of truth and deriving desired channels from it).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/redis/pubsub.ts
Line: 187:193

Comment:
**Unsubscribe can desync state**

In the cleanup function, `subscribedChannels.delete(channel)` happens before `unsubscribe(channel)` completes (and `unsubscribe` is not awaited). If the unsubscribe fails or is delayed while another `subscribeCacheInvalidation()` races in, the in-memory `subscribedChannels` can become out-of-sync with the actual Redis subscription state, leading to missed `resubscribeAll()` behavior or duplicate subscribe attempts. Consider serializing subscribe/unsubscribe mutations (or making `subscriptions` the single source of truth and deriving desired channels from it).

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

Comment on lines +120 to +126
} else {
logger.warn("applyGeminiGoogleSearchOverrideWithAudit: unknown preference value", {
preference,
providerId: provider.id,
});
return { request, audit: null };
}
Copy link

Choose a reason for hiding this comment

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

Bad preference loses audit

When geminiGoogleSearchPreference contains an unexpected persisted value, this path logs a warning but returns { audit: null } and leaves the request unchanged. That makes misconfigurations hard to diagnose because downstream logs/audits won’t show that the override was skipped due to invalid data. Prefer emitting an audit event (or coercing to inherit) so operators can spot and remediate bad stored values.

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!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/gemini/provider-overrides.ts
Line: 120:126

Comment:
**Bad preference loses audit**

When `geminiGoogleSearchPreference` contains an unexpected persisted value, this path logs a warning but returns `{ audit: null }` and leaves the request unchanged. That makes misconfigurations hard to diagnose because downstream logs/audits won’t show that the override was skipped due to invalid data. Prefer emitting an audit event (or coercing to `inherit`) so operators can spot and remediate bad stored values.

<sub>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!</sub>

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

Comment on lines +270 to 291
if (endpointCandidates.length === 0) {
if (shouldEnforceStrictEndpointPool) {
logger.warn(
"[ProxyForwarder] Failed to load provider endpoints, fallback to provider.url",
"ProxyForwarder: Strict endpoint policy blocked legacy provider.url fallback",
{
providerId: currentProvider.id,
vendorId: currentProvider.providerVendorId,
vendorId: providerVendorId,
providerType: currentProvider.providerType,
error: error instanceof Error ? error.message : String(error),
requestPath,
reason: "strict_blocked_legacy_fallback",
strictBlockCause: endpointSelectionError
? "selector_error"
: "no_endpoint_candidates",
selectorError: endpointSelectionError?.message,
}
);
failedProviderIds.push(currentProvider.id);
attemptCount = maxAttemptsPerProvider;
} else {
endpointCandidates.push({ endpointId: null, baseUrl: currentProvider.url });
}
}
Copy link

Choose a reason for hiding this comment

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

Strict branch still falls through

In the strict-endpoint-policy case (shouldEnforceStrictEndpointPool and endpointCandidates.length === 0), the code sets attemptCount = maxAttemptsPerProvider but then continues executing the provider attempt logic. This leaves endpointCandidates empty and later accesses activeEndpoint.endpointId/baseUrl, which will crash. The strict branch should continue to the next provider (or otherwise short-circuit) after marking this provider as failed.

Also appears in the vendor-type circuit-open branch where attemptCount is set to maxAttemptsPerProvider but execution still continues with normal attempt loop setup.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 270:291

Comment:
**Strict branch still falls through**

In the strict-endpoint-policy case (`shouldEnforceStrictEndpointPool` and `endpointCandidates.length === 0`), the code sets `attemptCount = maxAttemptsPerProvider` but then continues executing the provider attempt logic. This leaves `endpointCandidates` empty and later accesses `activeEndpoint.endpointId/baseUrl`, which will crash. The strict branch should `continue` to the next provider (or otherwise short-circuit) after marking this provider as failed.

Also appears in the vendor-type circuit-open branch where `attemptCount` is set to `maxAttemptsPerProvider` but execution still continues with normal attempt loop setup.

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

@github-actions
Copy link
Contributor

github-actions bot commented Feb 7, 2026

🧪 测试结果

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

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tests/unit/settings/providers/provider-endpoint-hover.test.tsx`:
- Around line 37-58: The test's loadMessages function currently hardcodes
endpointStatus strings (including an incorrect "View Details" without the
{count} placeholder) causing mismatch with the app messages; update loadMessages
to reuse the real translations by assigning endpointStatus =
enMessages.settings.providers.endpointStatus (or spread that object into
settings.providers) so viewDetails and other keys come from enMessages and
include the correct "{count}" placeholder used by the component when calling
t("endpointStatus.viewDetails", { count }).
🧹 Nitpick comments (2)
src/lib/auth-session-storage.node.ts (1)

6-9: 在双重断言处添加注释说明其必要性。

AsyncLocalStorage<ScopedAuthContext>AuthSessionStorage 接口在运行时结构兼容(两者都拥有匹配的 rungetStore 方法签名),但 TypeScript 无法直接推断这种结构兼容性,因此需要 as unknown as 来桥接类型。建议在此处添加一行简短注释(如 // AsyncLocalStorage 与 AuthSessionStorage 结构兼容但 TS 无法直接推断),方便后续维护者理解该断言的原因。

已确认 globalThis.__cchAuthSessionStorage 的类型声明已在 src/lib/auth.ts 的全局类型扩展中正确定义。

tests/unit/settings/providers/provider-endpoint-hover.test.tsx (1)

227-235: 排序断言依赖内部 CSS 类名选择器 span.truncate,存在脆弱性风险。

如果 EndpointRow 组件内部重构(例如更改类名或 DOM 结构),此选择器会静默返回空数组,断言 toEqual([]) 将失败但错误信息不够直观。建议为端点行添加 data-testid 属性,或至少在断言前验证 labels.length > 0

建议:增加长度断言作为前置校验
     const labels = Array.from(
       document.querySelectorAll("[data-testid='tooltip-content'] span.truncate")
     ).map((el) => el.textContent);
 
+    expect(labels).toHaveLength(3);
     expect(labels).toEqual([
       "https://api.anthropic.com/v1",
       "https://api.anthropic.com/v3",
       "https://api.anthropic.com/v2",
     ]);

Comment on lines +37 to +58
function loadMessages() {
const endpointStatus = {
viewDetails: "View Details",
activeEndpoints: "Active Endpoints",
noEndpoints: "No Endpoints",
healthy: "Healthy",
unhealthy: "Unhealthy",
unknown: "Unknown",
circuitOpen: "Circuit Open",
circuitHalfOpen: "Circuit Half-Open",
};

return {
settings: {
...enMessages.settings,
providers: {
...(enMessages.settings.providers || {}),
endpointStatus,
},
},
};
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find messages directory and check for en.json
fd -t f "en\.json" --exec cat {} \; 2>/dev/null | head -200

Repository: ding113/claude-code-hub

Length of output: 4528


🏁 Script executed:

#!/bin/bash
# Search for en.json more broadly and inspect structure
find . -name "en.json" -o -name "*en*" -type f | grep -i messages | head -20

Repository: ding113/claude-code-hub

Length of output: 1026


🏁 Script executed:

#!/bin/bash
# Check the test file to understand the structure
cat tests/unit/settings/providers/provider-endpoint-hover.test.tsx | head -50

Repository: ding113/claude-code-hub

Length of output: 1753


🏁 Script executed:

#!/bin/bash
# Find how messages are exported
find messages -type f \( -name "index.*" -o -name "*.json" \) | sort

Repository: ding113/claude-code-hub

Length of output: 13544


🏁 Script executed:

#!/bin/bash
# Search for endpointStatus usage in the codebase
rg "endpointStatus" --type ts --type tsx --type json -A 2 -B 2

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

#!/bin/bash
# Check the actual messages/en structure
ls -la messages/en/ 2>/dev/null && echo "---" && cat messages/en/*.json | head -100

Repository: ding113/claude-code-hub

Length of output: 4454


🏁 Script executed:

#!/bin/bash
# Search for endpointStatus in JSON files
rg "endpointStatus" --type json -A 5 -B 2

Repository: ding113/claude-code-hub

Length of output: 3207


🏁 Script executed:

#!/bin/bash
# Check the messages/en/settings/providers structure
find messages/en/settings/providers -type f -name "*.json" | xargs -I {} bash -c 'echo "=== {} ===" && cat {} | jq . 2>/dev/null || cat {}'

Repository: ding113/claude-code-hub

Length of output: 50379


🏁 Script executed:

#!/bin/bash
# Check how the component uses the messages
cat src/app/\[locale\]/settings/providers/_components/provider-endpoint-hover.tsx 2>/dev/null | head -100

Repository: ding113/claude-code-hub

Length of output: 3874


🏁 Script executed:

#!/bin/bash
# Check exact viewDetails string in messages file
cat messages/en/settings/providers/strings.json | jq '.endpointStatus.viewDetails'

Repository: ding113/claude-code-hub

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Check how the component uses the viewDetails translation
rg "endpointStatus.viewDetails" src/

Repository: ding113/claude-code-hub

Length of output: 210


直接复用 enMessages 中的 endpointStatus 翻译,并修正 viewDetails 中缺失的 {count} 占位符。

endpointStatus 的定义已存在于 messages/en/settings/providers/strings.json 中。当前测试中硬编码这些字符串会导致维护负担。更重要的是,test mock 中 viewDetails 的值为 "View Details",但实际消息文件中为 "View Details ({count})",而组件通过 t("endpointStatus.viewDetails", { count }) 传递 count 参数进行插值。这个不匹配会导致测试消息与实际组件翻译不同步。

建议:直接从 enMessages.settings.providers.endpointStatus 复用,确保测试消息与生产消息一致。

🤖 Prompt for AI Agents
In `@tests/unit/settings/providers/provider-endpoint-hover.test.tsx` around lines
37 - 58, The test's loadMessages function currently hardcodes endpointStatus
strings (including an incorrect "View Details" without the {count} placeholder)
causing mismatch with the app messages; update loadMessages to reuse the real
translations by assigning endpointStatus =
enMessages.settings.providers.endpointStatus (or spread that object into
settings.providers) so viewDetails and other keys come from enMessages and
include the correct "{count}" placeholder used by the component when calling
t("endpointStatus.viewDetails", { count }).

@ding113 ding113 merged commit 9ee6452 into main Feb 7, 2026
22 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Feb 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider area:UI dependencies Pull requests that update a dependency file enhancement New feature or request size/XL Extra Large PR (> 1000 lines)

Projects

Status: Done

4 participants