Skip to content

release v0.6.8#1014

Merged
ding113 merged 8 commits into
mainfrom
dev
Apr 13, 2026
Merged

release v0.6.8#1014
ding113 merged 8 commits into
mainfrom
dev

Conversation

@ding113

@ding113 ding113 commented Apr 13, 2026

Copy link
Copy Markdown
Owner

Summary

Release v0.6.8 consolidating five merged PRs: relaxed provider routing rule limits, improved provider detection templates, fixed error rule reload race conditions, prevented agent pool premature eviction of streaming connections, and corrected case-sensitive model ID handling.

Changes Included

1. Relax provider routing rule limits (PR #1000)

  • Raised model allowlist and redirect rule caps from 100 to 100,000 entries
  • Increased per-rule text length cap from 255 to 4,096 characters
  • Centralized limits via PROVIDER_RULE_LIMITS constant
  • Updated i18n strings across all 5 languages to use dynamic {max} interpolation

2. Improve provider detection template selection (PR #1003)

  • Split provider detection into independent Claude, Codex, OpenAI Compatible, and Gemini template suites
  • Added dedicated OpenAI Compatible chat completion templates (oa_chat_basic, oa_chat_stream) instead of reusing Codex Responses templates
  • Added Gemini generateContent templates (non-streaming, thinkingBudget: 0)
  • Implemented automatic template fallback: retries next template on 400/404/405/415/422 errors
  • Added parser-based response extraction with full rawResponse passthrough
  • Simplified provider detection UI to only require model input (removed API format, timeout, preset/custom config, and success keyword fields)
  • Switched Gemini API test from streamGenerateContent?alt=sse to generateContent for reliability

3. Fix error rule reload race condition (PR #1006)

  • Replaced silent-skip reload behavior with a queued reload mechanism (queueIfRunning)
  • Added activeReloadPromise coalescing so concurrent callers await the same reload
  • Ensured ensureInitialized() waits for queued reruns to complete
  • Added safety: DB failures during queued reload break the loop to avoid hot-retry storms

4. Prevent STREAM_PROCESSING_ERROR from premature agent pool eviction (PR #1009)

  • Added activeRequests reference counting to the agent pool (getAgent increments, releaseAgent decrements)
  • Agents with in-flight requests are no longer evicted by TTL cleanup
  • Added dispatcherId generation token to prevent cross-generation release miscount
  • Added 30-minute hard upper bound for agent lifetime as a safety net
  • Integrated releaseAgent calls throughout forwarder fetch paths and response-handler finally blocks
  • Recognized UND_ERR_DESTROYED, UND_ERR_CLOSED, ClientDestroyedError, ClientClosedError, and ERR_HTTP2_STREAM_ERROR as transport errors

5. Respect case-sensitive provider model IDs (PR #1011)

  • Removed .toLowerCase() from duplicate detection in allowlist and redirect rule editors/schemas
  • Runtime matching was already case-sensitive; this aligns UI dedup logic to match

Related Issues

Breaking Changes

None. All changes are backward-compatible:

  • Rule limits are raised (not lowered)
  • Transport error detection is expanded (not narrowed)
  • API test UI is simplified but functionally equivalent

Testing

Automated Tests

  • Unit tests for ErrorRuleDetector reload queue race conditions (230 lines)
  • Unit tests for AgentPool reference counting (307 lines)
  • Unit tests for provider testing presets and test-service (296 lines)
  • Unit tests for providers API test actions (185 lines)
  • Unit tests for provider allowed model schema and redirect schema (106 lines)
  • UI tests for AllowedModelRuleEditor and ModelRedirectEditor case sensitivity
  • UI tests for ApiTestButton simplified component
  • Transport error detection tests for new error types
  • Hedge forwarder tests with matched rule metadata logging

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests pass locally
  • i18n updated (5 languages)

Description enhanced by Claude AI

Greptile Summary

This release consolidates five merged PRs: provider routing rule limit relaxation (100 → 100,000), reworked provider detection templates with per-suite fallback retry, error rule reload race-condition fix (queued-reload pattern), agent pool premature eviction prevention via activeRequests reference counting with dispatcherId generation tokens, and case-sensitive model ID dedup alignment.

The core changes are well-implemented and comprehensively tested (1,200+ new test lines across the five areas).

Confidence Score: 5/5

Safe to merge — all five sub-PRs are backward-compatible and well-tested; no new P0/P1 issues found.

All core changes (reference counting, reload queue, template fallback, case-sensitive dedup, limit relaxation) are correctly implemented with comprehensive test coverage (1,200+ new test lines). The three agent-release code paths in forwarder.ts are properly isolated with no double-release. The error-rule reload do/while queue handles the acknowledged narrow race window. Previous thread concerns are P2 style/documentation items that do not block merge.

No files require special attention. The most complex change is forwarder.ts with its three-path agent release, but the logic is correct and tested.

Important Files Changed

Filename Overview
src/lib/proxy-agent/agent-pool.ts Adds activeRequests reference counting and dispatcherId generation tokens to prevent premature TTL eviction of in-flight streaming agents; logic is correct and the hard-expiry 30-min safety net is in place.
src/app/v1/_lib/proxy/forwarder.ts Integrates releaseAgent across three non-overlapping code paths (fetch failure at catch-top, HTTP error in !response.ok finally, streaming success via session callback); also extracts buildClientErrorChainEntry / buildMatchedRuleDetails helpers and adds matched-rule logging to hedge paths.
src/app/v1/_lib/proxy/response-handler.ts Adds idempotent releaseSessionAgent helper that clears the callback after first call, placed in all major early-exit and finally blocks; guards against double-release.
src/lib/error-rule-detector.ts Replaces silent-skip reload with do/while queued-reload loop and activeReloadPromise coalescing; handles the final-block micro-race window explicitly; DB failures break the loop to avoid hot-retry storms.
src/lib/provider-testing/test-service.ts Refactored to build AttemptPlan list from scored preset candidates, retrying on 400/404/405/415/422 or content-mismatch; total timeout split across attempts via deadline arithmetic.
src/lib/provider-testing/presets.ts Splits presets into Claude, Codex, OpenAI-Compatible, and Gemini suites; adds scorePreset + getExecutionPresetCandidates for context-aware template ranking.
src/actions/providers.ts Exports ProviderApiTestSuccessDetails/ProviderApiTestFailureDetails types, adds rawResponse passthrough, switches Gemini test from streamGenerateContent?alt=sse to generateContent with thinkingBudget:0.
src/app/[locale]/settings/providers/_components/forms/api-test-button.tsx Removes API-format selector, preset/custom config, timeout, and success-keyword fields; simplifies to model-only input with auto-detected defaults; adds rawResponse, usage, and stream info to result card.
src/lib/provider-allowed-model-schema.ts Raises limits to 100,000 items / 4,096 chars via PROVIDER_RULE_LIMITS; removes .toLowerCase() from duplicate key to align with case-sensitive runtime matching.
src/lib/provider-model-redirect-schema.ts Same limit relaxation and case-sensitive dedup fix as allowed-model schema; consistent with runtime redirect behavior.
src/app/v1/_lib/proxy/errors.ts Adds UND_ERR_DESTROYED, UND_ERR_CLOSED, ERR_HTTP2_STREAM_ERROR to transport error codes and ClientDestroyedError/ClientClosedError to name checks; broadens transport detection for undici agent lifecycle errors.

Sequence Diagram

sequenceDiagram
    participant F as ProxyForwarder
    participant AP as AgentPool
    participant RH as ResponseHandler

    F->>AP: getAgent(endpoint, http2=true)
    AP-->>F: {agent, cacheKey, dispatcherId} activeRequests++

    alt fetch network failure
        F->>AP: releaseAgent(cacheKey, dispatcherId)
        Note over AP: activeRequests-- (guard >0)
    else HTTP 4xx/5xx response
        F->>AP: releaseAgent in !response.ok finally
        Note over AP: activeRequests-- (guard >0)
    else HTTP 200 streaming success
        F->>RH: response + session.releaseAgent callback
        Note over F,RH: isExpired returns false while activeRequests > 0
        RH->>AP: releaseSessionAgent idempotent clears callback
        Note over AP: activeRequests-- (guard >0)
    end

    Note over AP: cleanup evicts only when activeRequests==0 AND TTL exceeded hard cap 30 min
Loading

Comments Outside Diff (1)

  1. src/lib/proxy-agent/agent-pool.ts, line 504-519 (link)

    P1 LRU eviction ignores activeRequests, bypassing the streaming-protection fix

    enforceMaxSize calls evictByKey directly without checking activeRequests, so when the pool grows past maxTotalAgents (100 by default), the oldest agents — including ones serving live streams — are evicted. This reintroduces STREAM_PROCESSING_ERROR in any deployment with >100 distinct (endpoint × proxy × protocol) combos, which the rest of this PR was designed to prevent.

    isExpired and cleanup respect activeRequests > 0, but enforceMaxSize bypasses that guard entirely. The 30-minute hard cap in isExpired is not applied here either.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/lib/proxy-agent/agent-pool.ts
    Line: 504-519
    
    Comment:
    **LRU eviction ignores `activeRequests`, bypassing the streaming-protection fix**
    
    `enforceMaxSize` calls `evictByKey` directly without checking `activeRequests`, so when the pool grows past `maxTotalAgents` (100 by default), the oldest agents — including ones serving live streams — are evicted. This reintroduces `STREAM_PROCESSING_ERROR` in any deployment with >100 distinct (endpoint × proxy × protocol) combos, which the rest of this PR was designed to prevent.
    
    `isExpired` and `cleanup` respect `activeRequests > 0`, but `enforceMaxSize` bypasses that guard entirely. The 30-minute hard cap in `isExpired` is not applied here either.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

Reviews (3): Last reviewed commit: "chore: format code (dev-56698a2)" | Re-trigger Greptile

ding113 and others added 5 commits April 8, 2026 21:27
* feat: improve provider detection templates

* fix: address provider detection review feedback

* chore: format code (feat-provider-detect-templates-02e4d1f)

* test: cover provider detection fallback retries

* chore: format code (feat-provider-detect-templates-4dc1701)

* fix: tighten provider detection validation

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* fix: honor disabled error rules during queued reloads

* fix: propagate matched rule metadata in hedge errors

* fix: avoid hot retries on error rule reload failure

* fix: avoid redundant error-rule reload passes

* fix: await queued error rule reload completion
…on (#1009)

* fix: prevent STREAM_PROCESSING_ERROR from premature agent pool eviction

The agent pool cleanup timer was destroying HTTP agents after 5min TTL
with no awareness of in-flight streaming requests, causing
ClientDestroyedError and ERR_HTTP2_STREAM_ERROR for long-running
responses. These errors were also misclassified because isTransportError
did not recognize them.

Three fixes:
1. Add UND_ERR_DESTROYED, UND_ERR_CLOSED, and HTTP/2 errors to
   isTransportError() so they classify as STREAM_UPSTREAM_ABORTED
2. Add reference counting (activeRequests) to AgentPool to prevent
   eviction of agents with in-flight requests (30min hard upper bound)
3. Wire up releaseAgent lifecycle in forwarder/response-handler to
   decrement refcount when streams complete

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

* fix: harden agent pool refcount against cross-generation misattribution

Addresses bot review feedback on #1009 covering four root causes that
still let STREAM_PROCESSING_ERROR leak through under concurrency:

- Track dispatcher generation ids so releaseAgent can ignore stale
  releases from old dispatchers after markUnhealthy / 30-min hard
  expire / LRU eviction rebuilds the same cacheKey
- Increment activeRequests for pending-creation waiters, not just the
  creator, so concurrent bursts don't prematurely evict the shared
  agent once the first request completes
- Move agent acquisition inside forwarder's fetch try block so any
  exception between acquire and fetch still releases the refcount
- Clear proxyConfig after a successful proxy-to-direct fallback to
  prevent session.releaseAgent double-decrementing the shared agent
- Add ERR_HTTP2_STREAM_ERROR to transport error codes so wrapped
  HTTP/2 errors on the cause chain are detected by isTransportError

Adds regression coverage for concurrent pending-creation waiter
counting (Promise.all with blocked createAgent), stale dispatcher
generation release safety, and HTTP/2 cause-chain detection.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: respect case-sensitive provider model ids

* fix: keep model picker ids case-sensitive
@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

此PR将多语言消息参数化为动态 {max} 占位符;引入并使用 PROVIDER_RULE_LIMITS 替代硬编码上限;调整允许/重定向规则的大小写去重为区分大小写;重构提供商测试/预设执行以保留 rawResponse 并增加多模板重试;并对代理池、转发与错误检测做引用计数、dispatcherId 与并发/释放改进,同时补充大量测试与修复。

Changes

Cohort / File(s) Summary
多语言消息参数化
messages/{en,ja,ru,zh-CN,zh-TW}/settings/providers/form/{allowedModelRules,modelRedirect}.json
将硬编码数值(如“100”/固定字符数)替换为参数化占位符 {max} / {max, number}
规则常量与 Schema 验证
src/lib/constants/provider.constants.ts, src/lib/{provider-allowed-model-schema,provider-model-redirect-schema}.ts
新增 PROVIDER_RULE_LIMITSMAX_ITEMS/MAX_TEXT_LENGTH)并在 Zod 验证中替换硬编码上限,错误消息改为接收 { max }
编辑器与去重行为
src/app/[locale]/settings/providers/_components/{allowed-model-rule-editor,model-redirect-editor}.tsx, src/app/[locale]/settings/providers/_components/model-multi-select.tsx
移除 .toLowerCase() 归一化(改为仅 trim()),使去重/键值大小写敏感;使用 PROVIDER_RULE_LIMITS 并将 { max } 传入 i18n。
提供商 API 测试类型与执行
src/actions/providers.ts, src/app/[locale]/settings/providers/_components/forms/api-test-button.tsx
新增 ProviderApiTestSuccessDetails/FailureDetails、在结果中携带 rawResponse,调整 Gemini 路径与请求体(:generateContentcontents.role、确定性配置),并简化 UI 测试流程以按 providerType 驱动。
provider-testing 预设与执行引擎
src/lib/provider-testing/{presets.ts,test-service.ts,types.ts,utils/test-prompts.ts}
重做预设集合与评分/选取逻辑,新增 per-preset path/userAgent/extraHeaders,使用候选计划 + 多模板重试执行请求;更新各 provider 默认测试体与 Gemini 行为,扩展类型。
测试夹具与单元测试
src/lib/provider-testing/data/*, src/lib/provider-testing/{presets.test.ts,test-service.test.ts}, 多个 tests/unit/...
新增大量 provider 测试 fixture(Claude/Codex/OpenAI/Gemini)并补充/更新单元测试以覆盖 rawResponse、预设选择、schema 验证、错误检测、代理池引用计数与并发 reload 等场景。
代理池与代理调度器
src/lib/proxy-agent/agent-pool.ts, src/lib/proxy-agent.ts
为缓存 agent 添加 dispatcherIdactiveRequests 引用计数,新增 releaseAgent(cacheKey, dispatcherId) API,getAgent 返回包含 dispatcherId 的结果并更新 pool 统计。
转发器与响应处理改进
src/app/v1/_lib/proxy/{forwarder,response-handler}.ts
提取 matched-rule / chain-entry 构建 helpers,统一非重试客户端错误链条构造;在 doForward 中改进 agent 获取/释放与 dispatcher 跟踪;在响应处理处添加幂等释放 helper,避免重复释放或泄漏。
传输错误检测增强
src/app/v1/_lib/proxy/errors.ts, tests/unit/transport-error-detection.test.ts
扩展 isTransportErrorUND_ERR_DESTROYED/UND_ERR_CLOSED/ERR_HTTP2_STREAM_ERROR 等识别并加入 HTTP/2 错误检测;新增对应测试覆盖。
ErrorRuleDetector 并发/排队重载
src/lib/error-rule-detector.ts, tests/unit/lib/error-rule-detector-reload-queue.test.ts
reload() 引入单飞+排队合并(activeReloadPromisereloadRequestedWhileLoading),reload(options?: { queueIfRunning?: boolean }) 支持排队重跑,事件处理使用排队选项;新增并发行为测试。
请求过滤与向后兼容清理
src/lib/request-filter-engine.ts, 相关契约测试
移除了私有兼容访问器(globalFilters/providerFilters),并更新测试以使用新的 PROVIDER_RULE_LIMITS 边界。

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • #1003 — 与本PR在 provider 测试类型、rawResponse 传播及 Gemini 端点/请求形态(:generateContent、generationConfig 字段)上的改动高度重叠。
  • #1000 — 共同引入/使用 PROVIDER_RULE_LIMITS 并参数化 i18n 与 schema 验证,代码级别直接相关。
  • #1006 — 在 ErrorRuleDetector 的 reload 合并逻辑与 forwarder 的 matched-rule 构建/日志方面存在直接重叠改动。
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 标题 'release v0.6.8' 清晰地总结了这个版本发布的主要变化,与 PR 涵盖的五个合并功能和发布内容完全相关。
Description check ✅ Passed PR 描述详细说明了五个合并的 PR 所包含的具体变化,涵盖提供商路由规则限制放宽、提供商检测模板改进、错误规则重新加载竞态条件修复、代理池流处理优化和大小写敏感模型 ID 处理等内容,与代码变更高度相关。

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 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 area:i18n size/XL Extra Large PR (> 1000 lines) labels Apr 13, 2026

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request refactors provider rule management and API testing, increasing rule limits to 100,000 and introducing case-sensitive model matching. It enhances the proxy agent pool with reference counting to prevent premature connection eviction and updates the API test service to support automatic template probing. Review feedback identifies a missing parsers.ts file that will cause build failures and recommends using the nullish coalescing operator (??) instead of logical OR (||) when handling parsed response content to ensure empty strings are processed correctly and do not trigger structural fallbacks.


import { createProxyAgentForProvider, type ProviderProxyConfig } from "@/lib/proxy-agent";
import { getPreset, getPresetPayload } from "./presets";
import { parseResponse } from "./parsers";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

The file src/lib/provider-testing/parsers/index.ts (or parsers.ts) appears to be missing from this pull request. The code adds an import for parseResponse from this path on line 8 and uses it on line 198, but the file itself is not present in the provided patches. This will lead to build failures.

// Tier 1: HTTP Status validation
const contentType = response.headers.get("content-type") || undefined;
const parsed = parseResponse(config.providerType, responseBody, contentType);
const validationInput = parsed.content || responseBody;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using the logical OR operator (||) for validationInput causes a fallback to the full responseBody if parsed.content is an empty string. This can lead to false positives in content validation if the successContains keyword matches a key or value in the raw JSON structure (e.g., matching the string "content" against the JSON key "content": ""). It is recommended to use the nullish coalescing operator (??) to ensure that an empty string is treated as valid content and doesn't trigger the fallback.

Suggested change
const validationInput = parsed.content || responseBody;
const validationInput = parsed.content ?? responseBody;

content: responseBody.slice(0, 500), // Preview for quick view
rawResponse: responseBody.slice(0, 5000), // Full response for detailed inspection
model: parsed.model,
content: parsed.content || responseBody,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Similar to the validation input, using || here will cause the UI to display the entire raw JSON response if the model returns an empty string. Using the nullish coalescing operator (??) ensures that an empty string is preserved as the content, while still falling back to the raw response if parsing failed to extract any content field at all.

Suggested change
content: parsed.content || responseBody,
content: parsed.content ?? responseBody,

@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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


export const PROVIDER_RULE_LIMITS = {
// 模型白名单 / 重定向规则保存在 JSONB 中,这里统一放宽到支持大规模路由表
MAX_ITEMS: 100_000,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Unformatted large number in user-facing messages

MAX_ITEMS = 100_000 will render as the raw string "100000" in all five locales — e.g. "You can add up to 100000 allowlist rules" and "Reached the 100000-rule limit while importing upstream models". The hyphenated form in the latter is especially jarring. Consider using next-intl's {max, number} ICU format token so the locale's digit grouping is applied automatically, or format the value before passing it to t():

In the call sites, switch to locale-aware formatting:

// In allowed-model-rule-editor.tsx / model-redirect-editor.tsx
t("maxRules", { max: Intl.NumberFormat().format(MAX_ALLOWED_MODEL_RULES) })

Or update the i18n messages to use ICU number format: "You can add up to {max, number} allowlist rules".

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/constants/provider.constants.ts
Line: 21

Comment:
**Unformatted large number in user-facing messages**

`MAX_ITEMS = 100_000` will render as the raw string `"100000"` in all five locales — e.g. `"You can add up to 100000 allowlist rules"` and `"Reached the 100000-rule limit while importing upstream models"`. The hyphenated form in the latter is especially jarring. Consider using `next-intl`'s `{max, number}` ICU format token so the locale's digit grouping is applied automatically, or format the value before passing it to `t()`:

In the call sites, switch to locale-aware formatting:

```ts
// In allowed-model-rule-editor.tsx / model-redirect-editor.tsx
t("maxRules", { max: Intl.NumberFormat().format(MAX_ALLOWED_MODEL_RULES) })
```

Or update the i18n messages to use ICU number format: `"You can add up to {max, number} allowlist rules"`.

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

Comment on lines 244 to 258
if (pending) {
// Wait for the pending creation and return its result
const result = await pending;
// ⚠️ 关键:等待 pending 创建的调用者也必须计入 activeRequests。
// 创建者在 createAgentWithCache 里把 activeRequests 初始化为 1(只代表它自己),
// 这里的每个等待者都要再 +1,否则一旦创建者完成并减到 0,cleanup/LRU 会在其它
// 仍在飞行中的请求头上把共享 agent 驱逐掉,又会把 STREAM_PROCESSING_ERROR 带回来。
const currentCached = this.cache.get(cacheKey);
if (currentCached && currentCached.id === result.dispatcherId) {
currentCached.activeRequests++;
currentCached.lastUsedAt = Date.now();
}
// Count as cache hit - we're reusing the pending result, not creating a new agent
// Note: Don't decrement cacheMisses here since we never incremented it for this request
this.stats.cacheHits++;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Pending-creation waiter increments activeRequests after resolving, but returns the original result

When multiple callers race and the second caller awaits the pending promise, it increments activeRequests on the cached entry only if currentCached.id === result.dispatcherId. The subtle risk: if the 30-minute hard-expire elapses between the moment await pending resolves and the moment the waiter's increment runs, the cache entry would have been replaced with a new id. The condition would be false, activeRequests would not be incremented, and the subsequent releaseAgent call would also be a no-op — leaving the new agent's activeRequests permanently at 1 until its own hard-expire fires. Consider adding a comment noting this invariant and the assumption that the pending creation window is always well under 30 minutes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/proxy-agent/agent-pool.ts
Line: 244-258

Comment:
**Pending-creation waiter increments `activeRequests` after resolving, but returns the original `result`**

When multiple callers race and the second caller awaits the `pending` promise, it increments `activeRequests` on the cached entry only if `currentCached.id === result.dispatcherId`. The subtle risk: if the 30-minute hard-expire elapses between the moment `await pending` resolves and the moment the waiter's increment runs, the cache entry would have been replaced with a new `id`. The condition would be false, `activeRequests` would not be incremented, and the subsequent `releaseAgent` call would also be a no-op — leaving the new agent's `activeRequests` permanently at `1` until its own hard-expire fires. Consider adding a comment noting this invariant and the assumption that the pending creation window is always well under 30 minutes.

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

@@ -2900,6 +3001,7 @@ export class ProxyForwarder {
const sessionWithTimeout = session as ProxySession & {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 releaseAgent attached as an ad-hoc property on ProxySession

The releaseAgent callback is grafted onto the session object via a local type cast (session as ProxySession & { releaseAgent?: () => void }). This pattern is repeated both here and in response-handler.ts. If future callers access session without the cast, the callback is invisible and could be silently skipped. Consider adding releaseAgent?: () => void to the actual ProxySession interface (or a dedicated StreamingContext type) so the contract is explicit and compiler-checked.

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

Comment:
**`releaseAgent` attached as an ad-hoc property on `ProxySession`**

The `releaseAgent` callback is grafted onto the session object via a local type cast (`session as ProxySession & { releaseAgent?: () => void }`). This pattern is repeated both here and in `response-handler.ts`. If future callers access `session` without the cast, the callback is invisible and could be silently skipped. Consider adding `releaseAgent?: () => void` to the actual `ProxySession` interface (or a dedicated `StreamingContext` type) so the contract is explicit and compiler-checked.

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d2e71b8254

ℹ️ About Codex in GitHub

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

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

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

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

Comment on lines +3025 to +3028
if (agentCacheKeyToRelease && agentDispatcherIdToRelease) {
const pool = getGlobalAgentPool();
sessionWithTimeout.releaseAgent = () => {
pool.releaseAgent(agentCacheKeyToRelease, agentDispatcherIdToRelease);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Release pooled agent on stream fast-path exits

This callback introduces in-flight ref counting, but it assumes response handling will always run a finalizer. In handleStream, requests with no messageContext return immediately (which is expected on raw passthrough routes like /v1/responses/compact), so releaseAgent is never called. In proxy/HTTP2 pooled paths that leaks activeRequests per request, blocking normal TTL cleanup and eventually causing avoidable agent eviction/churn under load.

Useful? React with 👍 / 👎.

// Tier 1: HTTP Status validation
const contentType = response.headers.get("content-type") || undefined;
const parsed = parseResponse(config.providerType, responseBody, contentType);
const validationInput = parsed.content || responseBody;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate content against full response body

validationInput currently prefers parsed.content, but parser fallback paths return only a 500-character preview when JSON parsing fails. That means long plain-text/malformed JSON responses can contain the success marker after char 500 and still be reported as content_mismatch, producing false negatives and unnecessary template retries even when the upstream response is actually valid.

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

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

⚠️ Outside diff range comments (1)
src/app/v1/_lib/proxy/forwarder.ts (1)

3001-3029: ⚠️ Potential issue | 🔴 Critical

Hedge 路径会泄漏 agent 的引用计数。

这里把 releaseAgent 只挂到了 attempt session 上,默认等 response-handler 去调用;但 hedge 的 loser、首块读取失败分支都在 sendStreamingWithHedge() 内部消费响应,根本不会经过 response-handler。影子 session 获胜时,syncWinningAttemptSession() 也没有把这个回调同步回主 session。结果是每次 hedge 都可能把 activeRequests 留高,TTL 清理和池容量判断都会失真。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/forwarder.ts` around lines 3001 - 3029, The hedge path
leaks agent reference counts because releaseAgent is only attached to the
attempt session (sessionWithTimeout) and relied on the response-handler, while
sendStreamingWithHedge() consumes loser/failed attempt responses internally (and
syncWinningAttemptSession() doesn't copy the callback back to the main session);
fix by ensuring the agent release callback is registered on every session that
may hold the agent: attach the same releaseAgent function to the primary session
object and to any shadow/attempt sessions created by sendStreamingWithHedge(),
and update syncWinningAttemptSession(session, winningAttempt) to copy
releaseAgent (and responseController/clearResponseTimeout if relevant) from the
winning attempt to the main session; use the existing symbols releaseAgent,
sessionWithTimeout, sendStreamingWithHedge, syncWinningAttemptSession,
getGlobalAgentPool, agentCacheKeyToRelease and agentDispatcherIdToRelease to
locate where to add the synchronization so all code paths (hedge losers,
failures, and the winning sync) call pool.releaseAgent exactly once.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/app/`[locale]/settings/providers/_components/allowed-model-rule-editor.tsx:
- Line 42: The UI currently uses MAX_ALLOWED_MODEL_RULES (set from
PROVIDER_RULE_LIMITS.MAX_ITEMS) in AllowedModelRuleEditor and renders the entire
rules table (around lines ~360-503), which will freeze the browser at tens of
thousands of items; change the approach by splitting the backend storage limit
from the UI interaction limit: introduce a separate UI cap (e.g.,
MAX_ALLOWED_MODEL_RULES_UI) used by AllowedModelRuleEditor and any functions
that render or iterate the full rule set, keep PROVIDER_RULE_LIMITS.MAX_ITEMS as
the storage ceiling, and do NOT increase the UI cap until you implement
pagination or a virtualized list for the rules table (or server-side pagination)
so additions/edits/removals operate on a paged/virtualized subset rather than
scanning the entire array.

In `@src/app/`[locale]/settings/providers/_components/forms/api-test-button.tsx:
- Around line 9-12: 将 testProviderGemini 的返回结构对齐到与 testProviderUnified 相同:不要把
details 限制为“仅成功时保留”,在失败分支也要回传 details.error 和 details.rawResponse(保持与
ProviderApiTestSuccessDetails/返回结构一致);同时计算最终 status 时除了 fallback 还要考虑
validationDetails.latencyPassed(若 latencyPassed 为 false 则应把状态降为 "yellow" 而不是保持
"green");检查并调整 testProviderGemini 的类型签名/返回类型以允许在失败时包含 details,确保调用方(UI 分支)能获取到原始
body、error、rawResponse 和 validationDetails 字段。

In `@src/app/v1/_lib/proxy/response-handler.ts`:
- Around line 43-57: The early-return branches in handleNonStream (the !provider
path) and handleStream (the !messageContext || !provider || !response.body
paths) skip releasing the session agent, leaving activeRequests held; before
each of those early returns call releaseSessionAgent(session) to decrement/clear
the pool reference and also clear any per-session state (same behavior as the
finally-backed path), ensuring releaseSessionAgent is invoked wherever you
return early; apply the same change at the other similar spots noted (around the
other occurrences referenced).

In `@src/lib/constants/provider.constants.ts`:
- Around line 19-23: The constant PROVIDER_RULE_LIMITS currently sets MAX_ITEMS
= 100_000 which mismatches the front-end editor (see model-redirect-editor.tsx
where a direct map at line ~232 renders the full list) and will cause UI
freezes; update the design by splitting the limit into separate backend and
frontend caps (e.g., BACKEND_MAX_ITEMS and FRONTEND_SAFE_MAX_ITEMS) and use
FRONTEND_SAFE_MAX_ITEMS where the editor rendering logic
(model-redirect-editor.tsx map) relies on full-list rendering, or alternatively
implement virtualization/virtual-scrolling in model-redirect-editor.tsx to
safely handle large lists; ensure all references to
PROVIDER_RULE_LIMITS.MAX_ITEMS that affect UI are replaced to use the new
FRONTEND_SAFE_MAX_ITEMS and keep the larger backend cap for storage/validation.

In `@src/lib/error-rule-detector.ts`:
- Around line 195-214: The failure branch currently clears isInitialized and
dbLoadedSuccessfully before calling reload(), causing ensureInitialized() to hit
the DB again on every request if the event-driven reload fails; change the logic
so you either (A) do not clear isInitialized/dbLoadedSuccessfully before calling
reload() from the event handler (leave the old usable cache/state in place until
reload succeeds), or (B) if you must clear them up-front, restore the previous
values when the catch branch runs (i.e., revert isInitialized and
dbLoadedSuccessfully to their prior values) and still set
lastReloadTime/reloadRequestedWhileLoading as needed; alternatively implement a
separate stale flag (e.g., isStale) to mark expiration without touching
isInitialized/dbLoadedSuccessfully so ensureInitialized() won’t retry DB loads
on every request.
- Around line 172-179: The current reload() treats callers that simply call
reload() as if they wanted to join the in-flight reload (returning
activeReloadPromise), which downgrades an explicit reload into a no-op and can
leave old rules in memory; change reload so its default behavior is to queue
another run when a reload is already in progress (i.e. treat
options.queueIfRunning === undefined as true), by setting the default sematics
in reload() to queue (and keep the existing reloadRequestedWhileLoading flag
behavior), and update callers that truly want to only join the current pass
(such as ensureInitialized()) to call reload({ queueIfRunning: false }) so they
retain the join-only behavior; refer to reload, activeReloadPromise,
reloadRequestedWhileLoading and ensureInitialized to locate and adjust call
sites and the method signature accordingly.

In `@src/lib/provider-testing/test-service.test.ts`:
- Around line 12-19: Replace the direct global assignment of fetch in beforeEach
with vitest's stub mechanism: instead of setting global.fetch = fetchMock in the
beforeEach block, call vi.stubGlobal("fetch", fetchMock) so vitest tracks and
can restore it via vi.unstubAllGlobals() in afterEach; keep vi.clearAllMocks()
in beforeEach and leave afterEach calling vi.unstubAllGlobals() unchanged, and
update references to fetchMock, beforeEach, afterEach, vi.clearAllMocks,
vi.stubGlobal, and vi.unstubAllGlobals accordingly.

In `@src/lib/provider-testing/test-service.ts`:
- Around line 195-205: The current code calls parseResponse(...) before
classifyHttpStatus(...), so parse errors can bubble out and turn valid 4xx/5xx
HTTP results into network errors; change this so the original HTTP result is
preserved and parsing is best-effort: first compute httpResult =
classifyHttpStatus(response.status, latencyMs, slowThresholdMs) using the raw
response/responseBody, then attempt parseResponse(config.providerType,
responseBody, contentType) inside a try/catch; on parse failure fall back to
using the raw responseBody (e.g. set parsed = { content: responseBody } or
similar) so evaluateContentValidation(httpResult.status, httpResult.subStatus,
validationInput, plan.successContains) always runs with a valid httpResult and
non-throwing parsing logic (refer to parseResponse, classifyHttpStatus,
evaluateContentValidation, responseBody, validationInput).
- Around line 65-72: The code currently checks only that
getPreset(config.preset) returns a value but does not verify the preset's
providerType; update the block that assigns presets (the code using presets,
config.preset and getPreset) to also assert that preset.providerType ===
config.providerType (or equivalent field on the returned preset) and throw a
clear error like `Preset X does not match providerType Y` if it doesn't match,
so we fail fast when a caller passes a preset for a different provider type.

In `@tests/unit/actions/providers-api-test.test.ts`:
- Around line 93-105: The beforeEach replaces global.fetch with fetchMock (in
the beforeEach block) but there is no afterEach to restore the original
global.fetch which can leak into other tests; add an afterEach that restores the
global.fetch (either call vi.restoreAllMocks() or explicitly save the original
fetch before overwriting and reassign it in afterEach), or replace the manual
assignment with vi.stubGlobal('fetch', fetchMock) in the beforeEach so the test
runner will automatically clean it up; update the beforeEach/afterEach
surrounding the global.fetch mock accordingly.

In `@tests/unit/lib/proxy-agent/agent-pool.test.ts`:
- Around line 529-532: Remove the emoji from the added test comments (replace "⭐
回归用例:" and the other occurrence at the later comment) so they comply with the
"Never use emoji" rule; edit the comment lines surrounding vi.useRealTimers()
and the similar comment around lines 599-600 to use plain-text wording (e.g.,
"Regression test: simulate concurrent requests during initial creation") while
preserving the existing explanation about using vi.useRealTimers() and spying to
block createAgent and asserting activeRequests increments; keep other logic
unchanged.
- Around line 532-595: The test creates concurrentPool under real timers but
only calls concurrentPool.shutdown() and createSpy.mockRestore() on the
successful path; move cleanup into the finally block so shutdown and spy restore
always run: ensure the finally block calls createSpy?.mockRestore() (guarding if
spy variable is null) and awaits concurrentPool?.shutdown() (guarding if pool
was created) before switching back to vi.useFakeTimers(), so no real timers or
background tasks leak to other tests; reference the variables concurrentPool,
createSpy, releaseCreate and the methods getAgent, releaseAgent, getPoolStats,
and shutdown to locate where to perform the cleanup.

---

Outside diff comments:
In `@src/app/v1/_lib/proxy/forwarder.ts`:
- Around line 3001-3029: The hedge path leaks agent reference counts because
releaseAgent is only attached to the attempt session (sessionWithTimeout) and
relied on the response-handler, while sendStreamingWithHedge() consumes
loser/failed attempt responses internally (and syncWinningAttemptSession()
doesn't copy the callback back to the main session); fix by ensuring the agent
release callback is registered on every session that may hold the agent: attach
the same releaseAgent function to the primary session object and to any
shadow/attempt sessions created by sendStreamingWithHedge(), and update
syncWinningAttemptSession(session, winningAttempt) to copy releaseAgent (and
responseController/clearResponseTimeout if relevant) from the winning attempt to
the main session; use the existing symbols releaseAgent, sessionWithTimeout,
sendStreamingWithHedge, syncWinningAttemptSession, getGlobalAgentPool,
agentCacheKeyToRelease and agentDispatcherIdToRelease to locate where to add the
synchronization so all code paths (hedge losers, failures, and the winning sync)
call pool.releaseAgent exactly once.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 456b2bd9-b2d8-448b-9bd4-9a1f07ab83bb

📥 Commits

Reviewing files that changed from the base of the PR and between 3d860b3 and d2e71b8.

📒 Files selected for processing (52)
  • messages/en/settings/providers/form/allowedModelRules.json
  • messages/en/settings/providers/form/modelRedirect.json
  • messages/ja/settings/providers/form/allowedModelRules.json
  • messages/ja/settings/providers/form/modelRedirect.json
  • messages/ru/settings/providers/form/allowedModelRules.json
  • messages/ru/settings/providers/form/modelRedirect.json
  • messages/zh-CN/settings/providers/form/allowedModelRules.json
  • messages/zh-CN/settings/providers/form/modelRedirect.json
  • messages/zh-TW/settings/providers/form/allowedModelRules.json
  • messages/zh-TW/settings/providers/form/modelRedirect.json
  • src/actions/providers.ts
  • src/app/[locale]/settings/providers/_components/allowed-model-rule-editor.tsx
  • src/app/[locale]/settings/providers/_components/forms/api-test-button.tsx
  • src/app/[locale]/settings/providers/_components/model-multi-select.tsx
  • src/app/[locale]/settings/providers/_components/model-redirect-editor.tsx
  • src/app/v1/_lib/proxy/errors.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/lib/constants/provider.constants.ts
  • src/lib/error-rule-detector.ts
  • src/lib/provider-allowed-model-schema.ts
  • src/lib/provider-model-redirect-schema.ts
  • src/lib/provider-testing/data/cc_beta_cli.json
  • src/lib/provider-testing/data/cc_haiku_basic.json
  • src/lib/provider-testing/data/cx_codex_basic.json
  • src/lib/provider-testing/data/cx_gpt_basic.json
  • src/lib/provider-testing/data/gm_flash_basic.json
  • src/lib/provider-testing/data/gm_pro_basic.json
  • src/lib/provider-testing/data/oa_chat_basic.json
  • src/lib/provider-testing/data/oa_chat_stream.json
  • src/lib/provider-testing/presets.test.ts
  • src/lib/provider-testing/presets.ts
  • src/lib/provider-testing/test-service.test.ts
  • src/lib/provider-testing/test-service.ts
  • src/lib/provider-testing/types.ts
  • src/lib/provider-testing/utils/test-prompts.ts
  • src/lib/proxy-agent.ts
  • src/lib/proxy-agent/agent-pool.ts
  • src/lib/request-filter-engine.ts
  • tests/unit/actions/providers-api-test.test.ts
  • tests/unit/actions/providers-patch-contract.test.ts
  • tests/unit/lib/error-rule-detector-reload-queue.test.ts
  • tests/unit/lib/provider-allowed-model-schema.test.ts
  • tests/unit/lib/provider-model-redirect-schema.test.ts
  • tests/unit/lib/proxy-agent/agent-pool.test.ts
  • tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts
  • tests/unit/proxy/proxy-forwarder-retry-limit.test.ts
  • tests/unit/settings/providers/allowed-model-rule-editor.test.tsx
  • tests/unit/settings/providers/api-test-button.test.tsx
  • tests/unit/settings/providers/model-multi-select.test.tsx
  • tests/unit/settings/providers/model-redirect-editor.test.tsx
  • tests/unit/transport-error-detection.test.ts
💤 Files with no reviewable changes (1)
  • src/lib/request-filter-engine.ts

pattern: "",
};
const MAX_ALLOWED_MODEL_RULES = 100;
const MAX_ALLOWED_MODEL_RULES = PROVIDER_RULE_LIMITS.MAX_ITEMS;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

把前端上限直接放到 100_000 会让编辑器失去可用性。

Line 42 现在跟随后端常量放开到 100_000,但下面 Lines 360-503 仍然会一次性渲染整张规则表,增删改也都依赖整表线性扫描。实际加载几万到 10 万条规则时,这个页面基本会卡死,用户很容易保存出之后再也打不开、编辑不了的配置。建议把“存储上限”和“交互上限”拆开,至少补分页或虚拟列表后再放开 UI 上限。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/app/`[locale]/settings/providers/_components/allowed-model-rule-editor.tsx
at line 42, The UI currently uses MAX_ALLOWED_MODEL_RULES (set from
PROVIDER_RULE_LIMITS.MAX_ITEMS) in AllowedModelRuleEditor and renders the entire
rules table (around lines ~360-503), which will freeze the browser at tens of
thousands of items; change the approach by splitting the backend storage limit
from the UI interaction limit: introduce a separate UI cap (e.g.,
MAX_ALLOWED_MODEL_RULES_UI) used by AllowedModelRuleEditor and any functions
that render or iterate the full rule set, keep PROVIDER_RULE_LIMITS.MAX_ITEMS as
the storage ceiling, and do NOT increase the UI cap until you implement
pagination or a virtualized list for the rules table (or server-side pagination)
so additions/edits/removals operate on a paged/virtualized subset rather than
scanning the entire array.

Comment on lines +9 to 12
type ProviderApiTestSuccessDetails,
testProviderGemini,
testProviderUnified,
} from "@/actions/providers";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

让 Gemini 分支返回与统一测试一致的结果结构。

Line 189 这里把 details 限制成“仅成功时才保留”,所以失败场景下服务端已经返回的 details.errordetails.rawResponse 会被直接丢掉;同时 Line 240 只根据 fallback 决定 status,Line 257 的延迟校验失败不会把结果降成 yellow。这样会同时出现“失败时前端拿不到原始 body”和“状态是 green 但 validationDetails.latencyPassed 为 false”的矛盾结果。

参考修改
 import {
   getUnmaskedProviderKey,
+  type ProviderApiTestFailureDetails,
   type ProviderApiTestSuccessDetails,
   testProviderGemini,
   testProviderUnified,
 } from "@/actions/providers";
@@
-        const isSuccess = response.data.success === true;
-        const details = (isSuccess ? response.data.details : undefined) as
-          | ProviderApiTestSuccessDetails
-          | undefined;
+        const isSuccess = response.data.success === true;
+        const details = response.data.details;
+        const successDetails = isSuccess
+          ? (details as ProviderApiTestSuccessDetails | undefined)
+          : undefined;
+        const failureDetails = !isSuccess
+          ? (details as ProviderApiTestFailureDetails | undefined)
+          : undefined;
         const latencyMs = response.data.details?.responseTime ?? 0;
+        const latencyPassed = isSuccess && latencyMs <= 5000;
@@
         testResultData = {
           success: isSuccess,
-          status: isSuccess ? (usedFallback ? "yellow" : "green") : "red",
+          status: !isSuccess ? "red" : usedFallback || !latencyPassed ? "yellow" : "green",
           subStatus: inferSubStatus(),
           message: cleanMessage,
           latencyMs,
-          model: details?.model,
-          content: details?.content,
+          model: successDetails?.model,
+          content: successDetails?.content,
           rawResponse: details?.rawResponse,
-          usage: normalizeUsage(details?.usage),
-          streamInfo: details?.streamInfo
+          usage: normalizeUsage(successDetails?.usage),
+          streamInfo: successDetails?.streamInfo
             ? {
                 isStreaming: true,
-                chunksReceived: details.streamInfo.chunksReceived,
+                chunksReceived: successDetails.streamInfo.chunksReceived,
               }
             : undefined,
+          errorMessage: failureDetails?.error,
           testedAt: new Date().toISOString(),
           validationDetails: {
             httpPassed: isSuccess,
-            latencyPassed: isSuccess && latencyMs < 5000,
+            latencyPassed,
             latencyMs,
             contentPassed: isSuccess,
             contentTarget: "pong",

Also applies to: 188-261

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/providers/_components/forms/api-test-button.tsx
around lines 9 - 12, 将 testProviderGemini 的返回结构对齐到与 testProviderUnified 相同:不要把
details 限制为“仅成功时保留”,在失败分支也要回传 details.error 和 details.rawResponse(保持与
ProviderApiTestSuccessDetails/返回结构一致);同时计算最终 status 时除了 fallback 还要考虑
validationDetails.latencyPassed(若 latencyPassed 为 false 则应把状态降为 "yellow" 而不是保持
"green");检查并调整 testProviderGemini 的类型签名/返回类型以允许在失败时包含 details,确保调用方(UI 分支)能获取到原始
body、error、rawResponse 和 validationDetails 字段。

Comment thread src/app/v1/_lib/proxy/response-handler.ts
Comment on lines +19 to +23
export const PROVIDER_RULE_LIMITS = {
// 模型白名单 / 重定向规则保存在 JSONB 中,这里统一放宽到支持大规模路由表
MAX_ITEMS: 100_000,
MAX_TEXT_LENGTH: 4_096,
} as const;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

MAX_ITEMS = 100_000 与当前前端编辑器渲染模型不匹配,会产生明显可用性风险。

Line 21 将规则数上限放大到 100k,但当前编辑器仍是全量列表渲染与客户端内存处理(如 model-redirect-editor.tsx Line 232 的直接 map)。在大规则集下会导致页面卡顿甚至不可操作。建议拆分“后端硬上限”和“前端安全编辑上限”,或在放宽上限前先引入虚拟化。

可选修正方向(示例)
 export const PROVIDER_RULE_LIMITS = {
-  // 模型白名单 / 重定向规则保存在 JSONB 中,这里统一放宽到支持大规模路由表
-  MAX_ITEMS: 100_000,
+  // 存储/接口硬上限
+  MAX_ITEMS_HARD: 100_000,
+  // 前端编辑器安全上限(避免一次渲染超大列表)
+  MAX_ITEMS_UI: 2_000,
   MAX_TEXT_LENGTH: 4_096,
 } as const;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/constants/provider.constants.ts` around lines 19 - 23, The constant
PROVIDER_RULE_LIMITS currently sets MAX_ITEMS = 100_000 which mismatches the
front-end editor (see model-redirect-editor.tsx where a direct map at line ~232
renders the full list) and will cause UI freezes; update the design by splitting
the limit into separate backend and frontend caps (e.g., BACKEND_MAX_ITEMS and
FRONTEND_SAFE_MAX_ITEMS) and use FRONTEND_SAFE_MAX_ITEMS where the editor
rendering logic (model-redirect-editor.tsx map) relies on full-list rendering,
or alternatively implement virtualization/virtual-scrolling in
model-redirect-editor.tsx to safely handle large lists; ensure all references to
PROVIDER_RULE_LIMITS.MAX_ITEMS that affect UI are replaced to use the new
FRONTEND_SAFE_MAX_ITEMS and keep the larger backend cap for storage/validation.

Comment on lines +172 to +179
async reload(options: { queueIfRunning?: boolean } = {}): Promise<void> {
if (this.activeReloadPromise) {
if (options.queueIfRunning) {
this.reloadRequestedWhileLoading = true;
logger.info("[ErrorRuleDetector] Reload already in progress, queueing another pass");
}
return this.activeReloadPromise;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

reload() 在并发场景下会把“显式刷新”降级成“仅等待当前刷新”。

如果调用方是在写库之后再 await errorRuleDetector.reload(),而当前 in-flight pass 已经读过旧快照,这里直接返回 activeReloadPromise 不会补跑下一轮,最终仍可能把旧规则留在内存里。建议把显式 reload() 的默认语义改成排队补跑,只给 ensureInitialized() 这类普通等待者保留“仅 join 当前 reload”的行为。

一个最小改法
-  async reload(options: { queueIfRunning?: boolean } = {}): Promise<void> {
+  async reload(options: { queueIfRunning?: boolean } = { queueIfRunning: true }): Promise<void> {
+    const queueIfRunning = options.queueIfRunning ?? true;
     if (this.activeReloadPromise) {
-      if (options.queueIfRunning) {
+      if (queueIfRunning) {
         this.reloadRequestedWhileLoading = true;
         logger.info("[ErrorRuleDetector] Reload already in progress, queueing another pass");
       }
       return this.activeReloadPromise;
     }

如果某些调用点确实只想复用当前 reload,再显式传 queueIfRunning: false

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/error-rule-detector.ts` around lines 172 - 179, The current reload()
treats callers that simply call reload() as if they wanted to join the in-flight
reload (returning activeReloadPromise), which downgrades an explicit reload into
a no-op and can leave old rules in memory; change reload so its default behavior
is to queue another run when a reload is already in progress (i.e. treat
options.queueIfRunning === undefined as true), by setting the default sematics
in reload() to queue (and keep the existing reloadRequestedWhileLoading flag
behavior), and update callers that truly want to only join the current pass
(such as ensureInitialized()) to call reload({ queueIfRunning: false }) so they
retain the join-only behavior; refer to reload, activeReloadPromise,
reloadRequestedWhileLoading and ensureInitialized to locate and adjust call
sites and the method signature accordingly.

Comment on lines +65 to 72
let presets: PresetConfig[];
if (config.preset) {
const preset = getPreset(config.preset);
if (!preset) {
throw new Error(`Preset not found: ${config.preset}`);
}
presets = [preset];
} else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

拒绝与 providerType 不匹配的显式 preset。

Line 67-71 现在只校验 preset 是否存在,没有校验它是否属于当前 providerType。如果调用方传入 providerType: "codex" + preset: "gm_pro_basic",这里会把 Gemini 的 path/body 和 Codex 的 headers/parser 混在一起,最后得到非常难诊断的假错误。这里应该直接 fail fast。

参考修改
 import {
   getExecutionPresetCandidates,
   getPreset,
   getPresetPayload,
+  isPresetCompatible,
   type PresetConfig,
 } from "./presets";
@@
   let presets: PresetConfig[];
   if (config.preset) {
+    if (!isPresetCompatible(config.preset, config.providerType)) {
+      throw new Error(
+        `Preset ${config.preset} is not compatible with provider type ${config.providerType}`
+      );
+    }
     const preset = getPreset(config.preset);
     if (!preset) {
       throw new Error(`Preset not found: ${config.preset}`);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/provider-testing/test-service.ts` around lines 65 - 72, The code
currently checks only that getPreset(config.preset) returns a value but does not
verify the preset's providerType; update the block that assigns presets (the
code using presets, config.preset and getPreset) to also assert that
preset.providerType === config.providerType (or equivalent field on the returned
preset) and throw a clear error like `Preset X does not match providerType Y` if
it doesn't match, so we fail fast when a caller passes a preset for a different
provider type.

Comment thread src/lib/provider-testing/test-service.ts
Comment on lines +93 to +105
beforeEach(() => {
vi.clearAllMocks();
getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
validateProviderUrlForConnectivityMock.mockImplementation((providerUrl: string) => ({
valid: true,
normalizedUrl: providerUrl,
}));
createProxyAgentForProviderMock.mockReturnValue(null);
getAccessTokenMock.mockImplementation(async (apiKey: string) => apiKey);
isJsonMock.mockReturnValue(false);
getPresetsForProviderMock.mockReturnValue([]);
global.fetch = fetchMock as typeof fetch;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's get the full test file to see the complete structure
fd -t f "providers-api-test.test.ts" --exec cat -n {} \;

Repository: ding113/claude-code-hub

Length of output: 6661


afterEach 中恢复 global.fetch,避免测试间污染。

第 104 行直接覆写了 global.fetch,但该文件缺少对应的 afterEach 钩子来恢复原始值。vi.clearAllMocks() 只清空 mock 调用历史,不会还原全局对象的直接赋值,可能导致后续测试继承这里的 mock,引发不稳定的结果。建议添加 afterEach(() => { vi.restoreAllMocks(); }) 或改用 vi.stubGlobal('fetch', fetchMock) 自动处理清理。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/actions/providers-api-test.test.ts` around lines 93 - 105, The
beforeEach replaces global.fetch with fetchMock (in the beforeEach block) but
there is no afterEach to restore the original global.fetch which can leak into
other tests; add an afterEach that restores the global.fetch (either call
vi.restoreAllMocks() or explicitly save the original fetch before overwriting
and reassign it in afterEach), or replace the manual assignment with
vi.stubGlobal('fetch', fetchMock) in the beforeEach so the test runner will
automatically clean it up; update the beforeEach/afterEach surrounding the
global.fetch mock accordingly.

Comment thread tests/unit/lib/proxy-agent/agent-pool.test.ts Outdated
Comment on lines +532 to +595
vi.useRealTimers();
try {
const concurrentPool = new AgentPoolImpl({
...defaultConfig,
agentTtlMs: 60_000,
});

const params = {
endpointUrl: "https://api.anthropic.com/v1/messages",
proxyUrl: null,
enableHttp2: false,
};

// 用 deferred 控制 createAgent 的完成时机,保证后续 getAgent 都落入 pendingCreations
let releaseCreate: (() => void) | null = null;
const createBlocker = new Promise<void>((resolve) => {
releaseCreate = resolve;
});

// biome-ignore lint/suspicious/noExplicitAny: private 方法 spy
const createSpy = vi.spyOn(concurrentPool as any, "createAgent");
createSpy.mockImplementationOnce(async () => {
await createBlocker;
return {
close: vi.fn().mockResolvedValue(undefined),
destroy: vi.fn().mockResolvedValue(undefined),
options: {},
};
});

// 同时发起 3 个请求:首个进入 createAgentWithCache;后两个必须走 pendingCreations
const p1 = concurrentPool.getAgent(params);
const p2 = concurrentPool.getAgent(params);
const p3 = concurrentPool.getAgent(params);

// 稍微等一个 microtask,确保 p2/p3 已经进入 await pending 分支
await Promise.resolve();
await Promise.resolve();

// 放行创建
releaseCreate?.();

const [r1, r2, r3] = await Promise.all([p1, p2, p3]);

// 三个调用应拿到相同的 cacheKey 与 dispatcherId
expect(r2.cacheKey).toBe(r1.cacheKey);
expect(r3.cacheKey).toBe(r1.cacheKey);
expect(r2.dispatcherId).toBe(r1.dispatcherId);
expect(r3.dispatcherId).toBe(r1.dispatcherId);

// 关键断言:所有 3 个并发 waiter 都应计入 activeRequests
expect(concurrentPool.getPoolStats().activeRequests).toBe(3);

// 释放 3 次后归零,且仍能再多释一次(no-op)
concurrentPool.releaseAgent(r1.cacheKey, r1.dispatcherId);
concurrentPool.releaseAgent(r2.cacheKey, r2.dispatcherId);
concurrentPool.releaseAgent(r3.cacheKey, r3.dispatcherId);
expect(concurrentPool.getPoolStats().activeRequests).toBe(0);

createSpy.mockRestore();
await concurrentPool.shutdown();
} finally {
vi.useFakeTimers();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

把额外 pool 的清理移到 finally

这个用例在 vi.useRealTimers() 下新建了 concurrentPool,但 shutdown() 只会在 try 正常走到底时执行。前面任一断言失败都会跳过清理,把真实计时器/后台任务留给后续用例。

建议修复
     vi.useRealTimers();
+    let concurrentPool: AgentPoolImpl | null = null;
+    let createSpy: { mockRestore: () => void } | null = null;
     try {
-      const concurrentPool = new AgentPoolImpl({
+      concurrentPool = new AgentPoolImpl({
         ...defaultConfig,
         agentTtlMs: 60_000,
       });
@@
-      const createSpy = vi.spyOn(concurrentPool as any, "createAgent");
+      createSpy = vi.spyOn(concurrentPool as any, "createAgent");
@@
-      createSpy.mockRestore();
-      await concurrentPool.shutdown();
     } finally {
+      createSpy?.mockRestore();
+      await concurrentPool?.shutdown();
       vi.useFakeTimers();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/lib/proxy-agent/agent-pool.test.ts` around lines 532 - 595, The
test creates concurrentPool under real timers but only calls
concurrentPool.shutdown() and createSpy.mockRestore() on the successful path;
move cleanup into the finally block so shutdown and spy restore always run:
ensure the finally block calls createSpy?.mockRestore() (guarding if spy
variable is null) and awaits concurrentPool?.shutdown() (guarding if pool was
created) before switching back to vi.useFakeTimers(), so no real timers or
background tasks leak to other tests; reference the variables concurrentPool,
createSpy, releaseCreate and the methods getAgent, releaseAgent, getPoolStats,
and shutdown to locate where to perform the cleanup.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review Summary

No significant issues identified in this PR. This is a well-structured XL release PR consolidating five features with comprehensive test coverage. The core changes -- agent pool reference counting with generation-token protection, ErrorRuleDetector reload queue coalescing, provider testing preset overhaul with automatic template fallback, and case-sensitive model ID handling -- are logically sound and well-tested.

PR Size: XL

  • Lines changed: 4218 (3220 additions + 998 deletions)
  • Files changed: 53

Split suggestion (XL): This PR bundles 5 distinct features. While each feature is coherent internally, future releases could benefit from merging features individually to simplify bisection. The current structure is acceptable for a release branch merge.

Review Coverage

  • Logic and correctness - Clean. Reload queue do-while loop correctly handles DB failure circuit-breaking. Agent pool dispatcherId generation tokens prevent cross-generation ref-count corruption. Template fallback retries on 400/404/405/415/422 are appropriate.
  • Security (OWASP Top 10) - Clean. No injection vectors, credentials handled properly (API keys passed via headers, not logged). rawResponse passthrough is intentional per PR description.
  • Error handling - Clean. All new catch blocks have appropriate logging or documented rationale. releaseSessionAgent idempotent design prevents double-release. DB failure during reload queue breaks the loop to avoid hot-retry storms.
  • Type safety - Acceptable. The releaseAgent callback attached via type cast on ProxySession is repeated in forwarder.ts and response-handler.ts; adding it to the ProxySession interface would be cleaner but this is a design preference, not a defect (already noted by Greptile).
  • Documentation accuracy - Clean. Comments match code behavior. Chinese comments are consistent with codebase conventions.
  • Test coverage - Excellent. 1000+ lines of new tests covering: reload queue race conditions (5 scenarios including microtask-level timing), agent pool reference counting (10 scenarios including concurrent waiters, cross-generation release, hard upper bound), provider testing presets and template fallback, schema validation with new limits, and UI component updates.
  • Code clarity - Good. Extracted helpers (buildMatchedRuleDetails, buildMatchedRuleLogContext, buildClientErrorChainEntry) reduce duplication in forwarder.ts. ApiTestButton simplification removes significant UI complexity.

Automated review by Claude AI

if (s.releaseAgent) {
try {
s.releaseAgent();
} catch {

@github-actions github-actions Bot Apr 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[CRITICAL] [ERROR-SILENT] Swallowed exception in releaseSessionAgent hides agent release failures

Evidence: src/app/v1/_lib/proxy/response-handler.ts:52

    } catch {
      // ignore - agent may already be evicted
    }

Why this is a problem: If session.releaseAgent() throws (unexpected agent-pool state, bugs in the callback, etc.), the exception is silently dropped. That can mask refcount leaks and makes production failures much harder to diagnose.

Suggested fix:

function releaseSessionAgent(session: ProxySession): void {
  const s = session as ProxySession & { releaseAgent?: () => void };
  if (!s.releaseAgent) return;

  try {
    s.releaseAgent();
  } catch (error) {
    logger.debug("ProxyResponseHandler: releaseAgent failed", {
      error: error instanceof Error ? error.message : String(error),
    });
  } finally {
    s.releaseAgent = undefined;
  }
}

if (now - cached.createdAt > MAX_AGENT_LIFETIME_MS) return true;

// Never expire agents with in-flight requests
if (cached.activeRequests > 0) return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] [LOGIC-BUG] LRU eviction can still destroy in-flight dispatchers despite activeRequests

Evidence: src/lib/proxy-agent/agent-pool.ts:437

// Never expire agents with in-flight requests
if (cached.activeRequests > 0) return false;

Why this is a problem: enforceMaxSize() evicts solely by lastUsedAt and calls closeAgent() even when activeRequests > 0. If the pool exceeds maxTotalAgents during a long-lived stream (or any slow request), an in-flight dispatcher can be destroyed, causing stream truncation / request failures.

Suggested fix (skip in-flight entries during LRU eviction):

private async enforceMaxSize(): Promise<void> {
  if (this.cache.size <= this.config.maxTotalAgents) return;

  const needed = this.cache.size - this.config.maxTotalAgents;

  const evictable = Array.from(this.cache.entries())
    .filter(([, cached]) => cached.activeRequests === 0)
    .sort(([, a], [, b]) => a.lastUsedAt - b.lastUsedAt);

  for (const [key] of evictable.slice(0, needed)) {
    await this.evictByKey(key);
  }

  // Optional: if still over limit because all agents are active, defer eviction until releases occur.
}

let response: Response;
const fetchStartTime = Date.now();
try {
// ⭐ 把 agent 获取 & 配置日志放进 try 块,确保获取后到 fetch 之前任何异常

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] [STANDARD-VIOLATION] Emoji characters added in code comments

Evidence:

  • src/app/v1/_lib/proxy/forwarder.ts:2503
    // ⭐ 把 agent 获取 & 配置日志放进 try 块,确保获取后到 fetch 之前任何异常
  • src/app/v1/_lib/proxy/forwarder.ts:2861
    // ⚠️ 关键:catch 入口已经释放了代理 agent 的 ref count(见本 catch 块顶部),

Why this is a problem: CLAUDE.md prohibits emoji characters in code, including comments: "No Emoji in Code - Never use emoji characters in any code, comments, or string literals".

Suggested fix (remove emojis, keep the intent):

// IMPORTANT: Put agent acquisition and logging inside the try so pre-fetch failures use unified cleanup.

// IMPORTANT: After proxy->direct fallback, clear proxy tracking to avoid double releaseAgent().

if (pending) {
// Wait for the pending creation and return its result
const result = await pending;
// ⚠️ 关键:等待 pending 创建的调用者也必须计入 activeRequests。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] [STANDARD-VIOLATION] Emoji character in comment violates repository rule

Evidence: src/lib/proxy-agent/agent-pool.ts:247

// ⚠️ 关键:等待 pending 创建的调用者也必须计入 activeRequests。

Why this is a problem: CLAUDE.md forbids emoji characters in code comments.

Suggested fix:

// IMPORTANT: Pending-creation waiters must also increment activeRequests.

latencyPassed: isSuccess && latencyMs < 5000,
latencyMs,
contentPassed: isSuccess,
contentTarget: "pong",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] [LOGIC-BUG] Gemini content validation is hard-coded to "pong" and always marked as passed

Evidence: src/app/[locale]/settings/providers/_components/forms/api-test-button.tsx:259

contentPassed: isSuccess,
contentTarget: "pong",

Why this is a problem: This makes the 3-tier validation UI misleading: it reports a content check against "pong" even though the Gemini test request does not enforce that target, and the UI never verifies the returned content.

Suggested fix (make the content tier reflect actual data for Gemini):

const hasContent = Boolean(details?.content?.trim());

validationDetails: {
  httpPassed: isSuccess,
  latencyPassed: isSuccess && latencyMs < 5000,
  latencyMs,
  contentPassed: isSuccess && hasContent,
  contentTarget: undefined,
},

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review Summary

This is an XL release PR that mixes core proxy runtime changes (agent pooling + forwarding/response handling), provider-testing plumbing, and settings UI/schema updates. The main risks are (1) silent error swallowing in the new agent-release path and (2) a remaining eviction path that can destroy in-flight dispatchers under LRU pressure.

PR Size: XL

  • Lines changed: 4182
  • Files changed: 52
  • Split suggestion (required for XL):
    • PR 1: Agent pool refcounting + ProxyForwarder/ProxyResponseHandler integration
    • PR 2: Provider testing service (src/lib/provider-testing/*) + actions/UI wiring
    • PR 3: Provider rule limits/schemas + settings editors + i18n message updates

Issues Found

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

Critical Issues (Must Fix)

  • src/app/v1/_lib/proxy/response-handler.ts:52 [CRITICAL] [ERROR-SILENT] Empty catch {} in releaseSessionAgent swallows exceptions (Confidence: 95)
  • src/lib/proxy-agent/agent-pool.ts:437 [HIGH] [LOGIC-BUG] enforceMaxSize() can still evict agents with activeRequests > 0 (Confidence: 95)

High Priority Issues (Should Fix)

  • src/app/[locale]/settings/providers/_components/forms/api-test-button.tsx:260 [MEDIUM] [LOGIC-BUG] Gemini content validation is hard-coded to "pong" and always marked passed (Confidence: 88)
  • src/app/v1/_lib/proxy/forwarder.ts:2503 [MEDIUM] [STANDARD-VIOLATION] Emoji characters added in code comments (Confidence: 80)
  • src/lib/proxy-agent/agent-pool.ts:247 [MEDIUM] [STANDARD-VIOLATION] Emoji character added in code comment (Confidence: 80)

Review Coverage

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

Automated review by Codex AI

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  • Identified PR #1014 (release v0.6.8) as XL (4182 lines changed across 52 files) and applied the label size/XL.
  • Posted inline review comments on the diff (silent error swallowing in agent release, in-flight agent eviction risk under LRU, Gemini content-validation reporting, and new emoji-in-comment rule violations).
  • Submitted the required “Code Review Summary” as a PR review comment via gh pr review.

ding113 and others added 2 commits April 13, 2026 08:22
- Use nullish coalescing (??) instead of logical OR (||) for parsed.content
  in test-service.ts to preserve empty string as valid content
- Wrap parseResponse() in try-catch to prevent parse exceptions from
  masking HTTP status codes as network_error
- Add releaseSessionAgent() to early-return branches in response-handler.ts
  to prevent agent pool reference count leaks
- Fix error-rule-detector reload() to always queue a fresh pass when
  called during an active reload, preventing stale snapshot reads
- Remove pre-clearing of isInitialized in event handler to avoid
  retry storms when database is unavailable
- Use ICU {max, number} format in i18n messages for locale-aware
  number formatting of rule limits across all 5 languages
- Remove emoji from test comments and fix pool cleanup in finally block

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/lib/error-rule-detector.ts (1)

102-106: ⚠️ Potential issue | 🟠 Major

不要在事件回调里预先清掉 dbLoadedSuccessfully

Line 105 仍然会让 ensureInitialized() 失去快速返回条件,因为它要求 dbLoadedSuccessfully && isInitialized 同时为 true。一旦事件驱动的 reload() 失败,旧缓存虽然还可用,但后续请求会再次触发打库,和这里注释里“避免每次请求重试”的目标相反。

建议修改
         const handleUpdated = () => {
           // 标记数据可能过期,让 reload 成功后刷新标记。
           // 注意:不清 isInitialized,避免 reload 失败时 ensureInitialized() 在每次请求上重试打库。
-          this.dbLoadedSuccessfully = false;
           this.reload({ queueIfRunning: true }).catch((error) => {
             logger.error("[ErrorRuleDetector] Failed to reload cache on event:", error);
           });
         };

如果还需要表达“缓存已过期”,建议单独引入一个 stale 标记,而不是复用 dbLoadedSuccessfully

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/error-rule-detector.ts` around lines 102 - 106, The event callback
handleUpdated must not clear dbLoadedSuccessfully because that causes
ensureInitialized() to lose its fast-return condition; instead, introduce a
separate stale flag (e.g., isStale) and set isStale = true in handleUpdated,
leaving dbLoadedSuccessfully untouched; update reload({ queueIfRunning: true })
to clear isStale only on successful completion and ensure ensureInitialized()
checks dbLoadedSuccessfully && isInitialized to short-circuit while also
considering isStale when deciding whether to reload.
🧹 Nitpick comments (1)
src/lib/provider-testing/test-service.ts (1)

280-288: 超时处理可能超出预期的 timeoutMs。

Line 282 使用 Math.max(1000, deadline - Date.now()) 确保每次尝试至少有 1 秒。但若前几次尝试耗时较长导致 deadline 已过,后续尝试仍会获得 1000ms,使总执行时间可能超出原始 timeoutMs

如果这是预期行为(给每个模板公平机会),可忽略此建议。否则可考虑在循环开始时检查是否已超时:

可选优化
   for (const plan of plans) {
+    if (Date.now() >= deadline) {
+      break;
+    }
     const remainingTimeoutMs = Math.max(1000, deadline - Date.now());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/provider-testing/test-service.ts` around lines 280 - 288, The loop
uses Math.max(1000, deadline - Date.now()) which can give each plan a full extra
1s even after the global deadline, potentially exceeding the intended timeout;
update the loop in test-service.ts that iterates over plans (where
fallbackResult, runSingleAttempt, shouldRetryWithNextTemplate and deadline are
used) to check remaining time at the start of each iteration (compute remaining
= deadline - Date.now()) and if remaining <= 0 return or break and return
fallbackResult, otherwise pass the actual remaining (not a forced 1000ms) into
runSingleAttempt; if you intentionally want a minimum per-attempt allowance,
document that behavior and use a bounded remaining = Math.min(remaining,
someMax) but do not unconditionally force a 1000ms when remaining is already
expired.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/lib/error-rule-detector.ts`:
- Around line 102-106: The event callback handleUpdated must not clear
dbLoadedSuccessfully because that causes ensureInitialized() to lose its
fast-return condition; instead, introduce a separate stale flag (e.g., isStale)
and set isStale = true in handleUpdated, leaving dbLoadedSuccessfully untouched;
update reload({ queueIfRunning: true }) to clear isStale only on successful
completion and ensure ensureInitialized() checks dbLoadedSuccessfully &&
isInitialized to short-circuit while also considering isStale when deciding
whether to reload.

---

Nitpick comments:
In `@src/lib/provider-testing/test-service.ts`:
- Around line 280-288: The loop uses Math.max(1000, deadline - Date.now()) which
can give each plan a full extra 1s even after the global deadline, potentially
exceeding the intended timeout; update the loop in test-service.ts that iterates
over plans (where fallbackResult, runSingleAttempt, shouldRetryWithNextTemplate
and deadline are used) to check remaining time at the start of each iteration
(compute remaining = deadline - Date.now()) and if remaining <= 0 return or
break and return fallbackResult, otherwise pass the actual remaining (not a
forced 1000ms) into runSingleAttempt; if you intentionally want a minimum
per-attempt allowance, document that behavior and use a bounded remaining =
Math.min(remaining, someMax) but do not unconditionally force a 1000ms when
remaining is already expired.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f1c2fa06-931f-48e7-83fa-e268a4815242

📥 Commits

Reviewing files that changed from the base of the PR and between 16aaa7a and 9a71530.

📒 Files selected for processing (14)
  • messages/en/settings/providers/form/allowedModelRules.json
  • messages/en/settings/providers/form/modelRedirect.json
  • messages/ja/settings/providers/form/allowedModelRules.json
  • messages/ja/settings/providers/form/modelRedirect.json
  • messages/ru/settings/providers/form/allowedModelRules.json
  • messages/ru/settings/providers/form/modelRedirect.json
  • messages/zh-CN/settings/providers/form/allowedModelRules.json
  • messages/zh-CN/settings/providers/form/modelRedirect.json
  • messages/zh-TW/settings/providers/form/allowedModelRules.json
  • messages/zh-TW/settings/providers/form/modelRedirect.json
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/lib/error-rule-detector.ts
  • src/lib/provider-testing/test-service.ts
  • tests/unit/lib/proxy-agent/agent-pool.test.ts
✅ Files skipped from review due to trivial changes (9)
  • messages/en/settings/providers/form/allowedModelRules.json
  • messages/en/settings/providers/form/modelRedirect.json
  • messages/zh-CN/settings/providers/form/modelRedirect.json
  • messages/zh-TW/settings/providers/form/modelRedirect.json
  • messages/ru/settings/providers/form/modelRedirect.json
  • messages/zh-CN/settings/providers/form/allowedModelRules.json
  • messages/zh-TW/settings/providers/form/allowedModelRules.json
  • messages/ru/settings/providers/form/allowedModelRules.json
  • tests/unit/lib/proxy-agent/agent-pool.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • messages/ja/settings/providers/form/allowedModelRules.json
  • messages/ja/settings/providers/form/modelRedirect.json

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a71530214

ℹ️ About Codex in GitHub

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

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

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

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

Comment on lines +436 to 439
// Never expire agents with in-flight requests
if (cached.activeRequests > 0) return false;

return now - cached.lastUsedAt > this.config.agentTtlMs;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Exclude active agents from max-size eviction

This new activeRequests guard claims in-flight agents should not expire, but only the TTL path uses it; enforceMaxSize() still evicts purely by LRU and can destroy a dispatcher that is actively serving a stream when the pool exceeds maxTotalAgents. In high-cardinality traffic (many endpoint/proxy combinations), this can still trigger mid-stream transport failures (UND_ERR_DESTROYED/stream processing errors) even though ref-counting was added.

Useful? React with 👍 / 👎.

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

Labels

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

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant