Skip to content

fix: align non-chat endpoint fallback with raw policy#1091

Merged
ding113 merged 9 commits into
devfrom
fix/non-chat-endpoint-fallback-20260423T113809Z
Apr 24, 2026
Merged

fix: align non-chat endpoint fallback with raw policy#1091
ding113 merged 9 commits into
devfrom
fix/non-chat-endpoint-fallback-20260423T113809Z

Conversation

@ding113

@ding113 ding113 commented Apr 23, 2026

Copy link
Copy Markdown
Owner

Summary

Add allowNonConversationEndpointProviderFallback system setting with settings UI and 5-language i18n. When enabled, /v1/messages/count_tokens and /v1/responses/compact reuse the session/provider decision chain to perform compatible cross-provider fallback after the current provider fails -- while preserving raw passthrough, strict endpoint-pool, and non-billing semantics. The two target endpoints remain hidden by default in usage queries.

Problem

Non-conversation endpoints (/v1/messages/count_tokens, /v1/responses/compact) used a RAW_PASSTHROUGH_PIPELINE that skipped session and message-context guards entirely, preventing any retry or provider fallback. If the initial provider was unreachable or errored, the request failed immediately with no recovery path.

Related Issues:

Solution

  1. New EndpointPolicy capability flag (allowRawCrossProviderFallback) that only the raw passthrough policy enables -- the default chat policy remains false
  2. Per-request runtime gating via rawCrossProviderFallbackEnabled on ProxySession, driven by the new allowNonConversationEndpointProviderFallback system setting
  3. New RAW_SAFE_SESSION_PIPELINE guard pipeline (auth -> client -> model -> version -> probe -> session -> provider -> messageContext) that runs session/provider resolution for these endpoints when fallback is enabled, falling back to the original RAW_PASSTHROUGH_PIPELINE when disabled
  4. Non-billing enforcement expanded from a single-endpoint constant to NON_BILLING_ENDPOINTS array covering both target paths. The ledger trigger (fn_upsert_usage_ledger) deletes ledger rows for these endpoints; the response handler skips cost calculation and Redis cost tracking
  5. Hardened provider-selector checks: format-compatibility validation and stale-binding cleanup (clearSessionProvider) when the session-bound provider mismatches the request format
  6. Usage log hiding: both endpoints are excluded from dashboard usage queries by default via DEFAULT_HIDDEN_USAGE_LOG_ENDPOINTS

Changes

Core Changes

  • endpoint-policy.ts - Add allowRawCrossProviderFallback flag to EndpointPolicy interface; enabled only for raw_passthrough kind
  • guard-pipeline.ts - New RAW_SAFE_SESSION_PIPELINE config; fromEndpointPolicy / fromSession route to it when raw cross-provider fallback is active
  • session.ts - rawCrossProviderFallbackEnabled runtime flag with isRawCrossProviderFallbackEnabled() gating; shouldReuseProvider() now considers this flag
  • session-guard.ts - Propagate system setting to session; skip codex completion and session mutation when raw session context is active
  • forwarder.ts - Implement retry / provider-switch logic for raw fallback path; scrub readonly fields; align chain entry builders
  • proxy-handler.ts - Read allowNonConversationEndpointProviderFallback from system settings and inject into session
  • response-handler.ts - Skip billing (cost calc, Redis cost tracking, service tier special settings) for non-billing endpoints
  • provider-selector.ts - Add format-compatibility check for session-bound providers; clear stale bindings; pass keyId to getSessionProvider

Database / Schema

  • schema.ts - New allowNonConversationEndpointProviderFallback boolean column on system_settings (default true)
  • 0098_equal_selene.sql - Migration: add column, purge existing ledger rows for target endpoints, and rebuild fn_upsert_usage_ledger while preserving actual_response_model propagation and non-billing endpoint exclusion logic

Usage / Billing

  • performance-formatter.ts - NON_BILLING_ENDPOINT -> NON_BILLING_ENDPOINTS array + isNonBillingEndpoint() function with trailing-slash normalization
  • ledger-conditions.ts - Replace single-endpoint filter with NON_BILLING_LEDGER_ENDPOINT_CONDITION derived from NON_BILLING_ENDPOINTS
  • usage-log-filters.ts - DEFAULT_HIDDEN_USAGE_LOG_ENDPOINTS; normalized endpoint filtering
  • usage-logs.ts / my-usage.ts - Apply hidden-endpoint filters in queries; scrub readonly chain data

Settings UI & i18n

  • system-settings-form.tsx / page.tsx - New toggle for the setting
  • messages/{en,ja,ru,zh-CN,zh-TW}/settings/config.json - i18n strings for all 5 languages

Supporting Changes

  • session-manager.ts - getSessionProvider accepts optional keyId; new clearSessionProvider usage
  • system-settings-cache.ts / repository/system-config.ts - Surface new setting
  • validation/schemas.ts / types/system-config.ts - Type definitions for the new setting
  • ledger-backfill/service.ts / trigger.sql - Updated backfill logic for new ledger function

Tests (8 new test files, 12 updated)

  • non-chat-endpoint-policy.test.ts - Policy flag assertions
  • non-chat-endpoint-session-context.test.ts - Session/guard integration for raw fallback
  • non-chat-endpoint-fallback.test.ts - Forwarder retry/provider-switch behavior
  • non-chat-endpoint-non-billing.test.ts - Non-billing enforcement
  • usage-logs-hidden-non-chat-endpoints.test.ts - Hidden endpoint filtering
  • system-config-non-chat-retry-setting.test.ts - Setting persistence
  • system-settings-form-non-chat-fallback.test.tsx - UI toggle rendering
  • non-chat-endpoint-fallback-observability.test.ts - Integration (observability)
  • Updated: endpoint-policy.test.ts, endpoint-policy-parity.test.ts, guard-pipeline-warmup.test.ts, proxy-forwarder-endpoint-audit.test.ts, proxy-forwarder-retry-limit.test.ts, proxy-forwarder-hedge-first-byte.test.ts, response-handler-lease-decrement.test.ts, session-guard-warmup-intercept.test.ts, provider-selector-model-mismatch-binding.test.ts, key.test.ts, usage-logs-min-retry-count-filter.test.ts, integration.config.ts

Migration Safety

  • drizzle/0098_equal_selene.sql contains an irreversible DELETE FROM "usage_ledger" for historical /v1/messages/count_tokens and /v1/responses/compact rows.
  • Scope: only existing ledger rows for those two non-billing endpoints are removed during rollout; future rows for the same endpoints are also excluded by the rebuilt trigger.
  • Before applying in production, take a snapshot/backup of usage_ledger if those historical rows are needed for audit or rollback analysis.
  • Suggested preflight: run a count such as SELECT count(*) FROM usage_ledger WHERE endpoint IS NOT NULL AND LOWER(REGEXP_REPLACE(endpoint, '/+$', '')) IN ('/v1/messages/count_tokens', '/v1/responses/compact'); to estimate impact.

Breaking Changes

Change Impact Migration
NON_BILLING_ENDPOINT constant still exported but deprecated in favor of NON_BILLING_ENDPOINTS array and isNonBillingEndpoint() Code referencing the old constant directly Replace with isNonBillingEndpoint(endpoint)
LEDGER_BILLING_CONDITION replaced with NON_BILLING_LEDGER_ENDPOINT_CONDITION Custom ledger queries using the old export Use the new condition export
COUNT_TOKENS_PIPELINE now points to RAW_SAFE_SESSION_PIPELINE Tests or extensions referencing the old pipeline shape Update to expect session/provider steps
Migration 0098 deletes existing ledger rows for target endpoints Historical data for count_tokens/compact is removed Snapshot or back up usage_ledger before production rollout if those rows are needed for audit

Verification

bunx vitest run tests/unit/actions/system-config-non-chat-retry-setting.test.ts --reporter=verbose
bunx vitest run tests/unit/proxy/non-chat-endpoint-policy.test.ts tests/unit/proxy/non-chat-endpoint-session-context.test.ts tests/unit/proxy/non-chat-endpoint-fallback.test.ts tests/unit/proxy/non-chat-endpoint-non-billing.test.ts tests/unit/actions/usage-logs-hidden-non-chat-endpoints.test.ts --reporter=verbose
bunx vitest run --config tests/configs/integration.config.ts tests/integration/non-chat-endpoint-fallback-observability.test.ts --reporter=verbose
bunx vitest run tests/unit/proxy/endpoint-policy.test.ts tests/unit/proxy/endpoint-policy-parity.test.ts tests/unit/proxy/proxy-handler-session-id-error.test.ts tests/unit/proxy/proxy-forwarder-retry-limit.test.ts tests/unit/proxy/proxy-forwarder-thinking-signature-rectifier.test.ts --reporter=verbose
bunx vitest run tests/unit/proxy/hedge-error-pipeline.test.ts tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts --reporter=verbose
bunx vitest run tests/unit/k8s-cch-update-flow.test.ts --reporter=verbose
bun run lint:fix && bun run typecheck && bun run lint && bun run build

Screenshot

!settings config screenshot

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests pass locally (targeted test suites; full run encountered Vitest worker OOM near the end)
  • i18n strings added for all 5 languages
  • Migration generated via db:generate

Description enhanced by Claude AI

Greptile Summary

This PR introduces allowNonConversationEndpointProviderFallback — a system setting that enables /v1/messages/count_tokens and /v1/responses/compact to reuse the session/provider resolution chain for cross-provider fallback instead of failing immediately. It ships a new RAW_SAFE_SESSION_PIPELINE, non-billing billing guards in the response handler, keyId-scoped session provider validation in the session manager, normalized endpoint filtering across all usage-log queries, a Drizzle migration that permanently deletes historical ledger rows for those two endpoints, and i18n + settings UI for the new toggle.

Confidence Score: 4/5

Safe to merge after addressing the redundant endpointPolicy shadow and reviewing non-atomic key binding; no P0 issues found.

All remaining findings are P2. The duplicate endpointPolicy in the forwarder loop body produces no behavioral difference since both declarations call the same pure method on the same session, but it is confusing. The non-atomic provider+key binding in session-manager is mitigated by fail-closed behavior in getSessionProvider, limiting blast radius to a stale Redis entry for one TTL window. The migration's irreversible DELETE is clearly documented with a preflight query. No P0/P1 issues remain beyond prior-thread items.

src/lib/session-manager.ts (first-bind branches lack pipeline atomicity); src/app/v1/_lib/proxy/forwarder.ts (duplicate endpointPolicy const).

Important Files Changed

Filename Overview
src/app/v1/_lib/proxy/forwarder.ts Adds raw cross-provider fallback retry/switch logic with shouldSkipRawRetryAndProviderSwitch and per-provider single-attempt cap; has a redundant inner endpointPolicy declaration that shadows the outer one.
src/lib/session-manager.ts Adds keyId cross-validation to getSessionProvider (fail-closed) and propagates keyId through all binding paths; provider + key writes are non-atomic in the SET NX first-bind branches.
src/app/v1/_lib/proxy/session.ts Adds rawCrossProviderFallbackEnabled runtime flag with getter/setter; shouldReuseProvider now returns true for raw fallback sessions; clean implementation.
src/app/v1/_lib/proxy/response-handler.ts Introduces billableUsageMetrics/billableUsageForCost guards to skip cost calculation, Redis tracking, Codex 1M context, and service-tier special settings for non-billing endpoints; looks correct.
src/app/v1/_lib/proxy/guard-pipeline.ts New RAW_SAFE_SESSION_PIPELINE routes raw endpoints through session/provider/messageContext steps when fallback is enabled; fromRequestType(COUNT_TOKENS) always uses this pipeline regardless of the runtime flag (only called from tests).
drizzle/0098_equal_selene.sql Adds allow_non_conversation_endpoint_provider_fallback column; deletes historical non-billing ledger rows; rebuilds fn_upsert_usage_ledger with non-billing endpoint exclusion and actual_response_model propagation preserved.
src/repository/usage-logs.ts Applies normalized endpoint match and hidden-endpoint conditions across findUsageLogsBatch, buildKeyLedgerConditions, and findUsageLogsStats; adds a non-ledger message-request path for non-billing endpoint stats.
src/app/v1/_lib/proxy/provider-selector.ts Adds format-compatibility check and clearSessionProvider on stale binding; passes keyId to getSessionProvider for ownership validation.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Incoming Request
/v1/messages/count_tokens
/v1/responses/compact] --> B{resolveEndpointPolicy}
    B --> C[RAW_PASSTHROUGH_ENDPOINT_POLICY
allowRawCrossProviderFallback=true]
    C --> D{proxy-handler:
allowNonConversationEndpointProviderFallback
system setting}
    D -- enabled --> E[session.setRawCrossProviderFallbackEnabled=true]
    D -- disabled --> F[session.setRawCrossProviderFallbackEnabled=false]
    E --> G[fromSession → RAW_SAFE_SESSION_PIPELINE
auth→client→model→version→probe
→session→provider→messageContext]
    F --> H[fromSession → RAW_PASSTHROUGH_PIPELINE
auth→client→model→version→probe→provider]
    G --> I[ProxyForwarder
maxAttemptsPerProvider=1
rawCrossProviderFallbackEnabled=true]
    H --> J[ProxyForwarder
skipRawRetryAndProviderSwitch=true]
    I -- provider fails --> K[Switch to next provider
no circuit breaker accounting]
    I -- success --> L[Response
skip billing/cost/Redis tracking]
    J -- any error --> M[Fail immediately
503/original error]
    K --> I
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 974-976

Comment:
**Redundant `endpointPolicy` re-declaration shadows outer binding**

`endpointPolicy` is already declared before the while loop (line ~942) using `ProxyForwarder.getEndpointPolicy(session)`. The inner `const endpointPolicy` inside the loop body re-declares the same value in every iteration, shadowing the outer binding without producing a different result. The outer declaration is only used for `shouldSkipRawRetryAndProviderSwitch`, while `shouldAccountCircuitBreaker` and `shouldEnforceStrictEndpointPool` rely on the inner one — both end up with identical data. Consider removing the inner declaration and reading `.allowCircuitBreakerAccounting` and `.endpointPoolStrictness` from the outer `endpointPolicy` directly, or promoting `shouldAccountCircuitBreaker` / `shouldEnforceStrictEndpointPool` next to `shouldSkipRawRetryAndProviderSwitch` before the loop.

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

---

This is a comment left during a code review.
Path: src/lib/session-manager.ts
Line: 619-627

Comment:
**Non-atomic provider + key binding can leave orphaned provider entries**

`bindSessionToProvider` first does an atomic `SET NX` for `session:{id}:provider`, then — only if that succeeds — does a separate `setex` for `session:{id}:key`. If the second write fails (Redis disconnect, crash, OOM), the provider is bound with no key owner recorded. `getSessionProvider` is fail-closed (`boundKeyId !== keyId``return null`), so no wrong provider is ever reused, but the orphaned provider key lives in Redis until TTL, and every subsequent request for this session fails-closed and picks a fresh provider, defeating the sticky-session guarantee until TTL expires.

The same pattern is repeated in the `isFailoverSuccess`, `no previous binding`, priority-migration, and circuit-open branches below. Batching both writes in a single `pipeline.exec()` (like the failover branch already does) would make the pair atomic from the perspective of network-partition failures.

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

Reviews (6): Last reviewed commit: "fix: close remaining review threads" | Re-trigger Greptile

@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

引入面向非会话(raw)端点的跨提供商回退开关,并贯穿数据库 schema、代理会话/forwarder、计费/账本过滤、前端设置 UI 与大量测试,调整相关运行时策略与观测行为。

Changes

Cohort / File(s) Summary
Database schema & migrations
drizzle/meta/_journal.json, drizzle/schema.ts, drizzle/0098_equal_selene.sql
新增 allow_non_conversation_endpoint_provider_fallback 布尔列(默认 true);更新/新增迁移与触发器 fn_upsert_usage_ledger(),在触发器中规范化端点并跳过/删除指定非会话端点的 ledger 条目。
国际化 & 设置类型/校验
messages/en/settings/config.json, messages/ja/settings/config.json, messages/ru/settings/config.json, messages/zh-CN/settings/config.json, messages/zh-TW/settings/config.json, src/types/system-config.ts, src/lib/validation/schemas.ts
为多语言 settings 添加 allowNonConversationEndpointProviderFallback 文案,更新类型与验证 schema 支持该字段。
系统设置持久层与缓存
src/repository/system-config.ts, src/repository/_shared/transformers.ts, src/lib/config/system-settings-cache.ts
将新设置纳入 SELECT/UPDATE 投影与 transformer 默认值;缓存默认对象与错误路径的保守默认行为做相应调整。
设置 UI 与保存动作
src/app/[locale]/settings/config/_components/system-settings-form.tsx, src/app/[locale]/settings/config/page.tsx, src/actions/system-config.ts
系统设置页面新增开关并将值纳入保存 payload,更新表单初始值与保存调用签名。
Proxy 会话与策略解析
src/app/v1/_lib/proxy-handler.ts, src/app/v1/_lib/proxy/session.ts, src/app/v1/_lib/proxy/session-guard.ts, src/app/v1/_lib/proxy/endpoint-policy.ts, src/app/v1/_lib/proxy/guard-pipeline.ts
引入会话级 rawCrossProviderFallbackEnabled(setter/getter),EndpointPolicy 增加 allowRawCrossProviderFallback,GuardPipeline 新增 RAW_SAFE_SESSION_PIPELINE 并基于策略/运行时选择管道;pathname 归一化与 strictness 判定迁移到 policy 层。
Forwarder / 提供者选择 / 错误链
src/app/v1/_lib/proxy/forwarder.ts, src/app/v1/_lib/proxy/provider-selector.ts, src/app/v1/_lib/proxy/response-handler.ts
Forwarder 支持基于会话/策略的原始跨提供商回退开关(控制重试/切换/电路计数并在错误链中记录相关元数据);provider-selector 强化复用验证并在兼容性不符时清除绑定;response-handler 在非计费端点跳过计费/成本计算与 Codex 特殊处理。
非计费端点识别与账单过滤
src/lib/utils/performance-formatter.ts, src/repository/_shared/ledger-conditions.ts, src/repository/_shared/usage-log-filters.ts, src/repository/usage-logs.ts, src/lib/ledger-backfill/service.ts, src/lib/ledger-backfill/trigger.sql
由单一常量改为 NON_BILLING_ENDPOINTS 列表并新增 isNonBillingEndpoint(),在 ledger/backfill/usage-log 过滤与触发器中采用规范化 NOT IN/匹配逻辑以排除非计费端点并提供默认隐藏条件构建。
前端日志表 & 成本渲染
src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx, .../virtualized-logs-table.tsx
将非计费行判定改为调用 isNonBillingEndpoint(log.endpoint),现有渲染逻辑(如 cost cell 显示 "-")不变。
Types / provider-chain 元数据 & my-usage
src/types/message.ts, src/actions/my-usage.ts
ProviderChainItem 上新增可选字段 rawCrossProviderFallbackEnabled?: boolean;my-usage 的只读链路脱敏逻辑根据该标志调整对 clientError 与 provider 上游字段的清理。
Session 管理 & Redis 绑定
src/lib/session-manager.ts, src/app/v1/_lib/proxy/provider-selector.ts
SessionManager 接受并传播可选 keyId 以在刷新 TTL、绑定与获取绑定时校验 keyId;不匹配时返回/清除绑定以防跨 key 重用。
SQL/仓库逻辑与查询行为
src/repository/usage-logs.ts, src/repository/_shared/usage-log-filters.ts, src/repository/_shared/ledger-conditions.ts
usage-logs 查找/聚合加入端点规范化的隐藏/匹配条件;对非账本场景(非计费端点)提供快速路径返回零成本统计并调整 endpoint 比较语义。
测试新增/更新
tests/...(多处新增/修改,如 tests/unit/proxy/non-chat-endpoint-fallback.test.ts, tests/integration/non-chat-endpoint-fallback-observability.test.ts, tests/unit/actions/system-config-non-chat-retry-setting.test.ts 等)
新增与修改大量单元与集成测试,覆盖策略解析、guard pipeline、forwarder 回退行为、非计费账本过滤、设置持久化/缓存降级、前端表单与 i18n,以及相应 mocks/fixture 调整。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.36% 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 标题准确总结了主要变更:为非对话端点启用跨提供商回退,与原始策略对齐。符合简明清晰的原则。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed PR 描述清晰详尽,与代码变更直接相关,涵盖问题背景、解决方案、所有核心变更、数据库变更、使用/计费、设置界面和国际化、支持变更、测试、迁移安全性、破坏性变更和验证步骤。

✏️ 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 fix/non-chat-endpoint-fallback-20260423T113809Z

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

❤️ Share

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3146acfa5a

ℹ️ 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 thread src/app/v1/_lib/proxy/response-handler.ts Outdated
@github-actions github-actions Bot added the size/XL Extra Large PR (> 1000 lines) label Apr 23, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review Summary

This PR adds allowNonConversationEndpointProviderFallback system setting with 5-language i18n support, enabling cross-provider fallback for /v1/messages/count_tokens and /v1/responses/compact endpoints. The implementation correctly maintains raw-safe session context and non-billing behavior for these endpoints.

PR Size: XL

  • Lines changed: 7,367 (7,220 additions, 147 deletions)
  • Files changed: 55

Split Suggestion: This PR could be split into:

  1. Core proxy/endpoint policy changes (endpoint-policy.ts, guard-pipeline.ts, forwarder.ts)
  2. Session management changes (session.ts, session-guard.ts)
  3. System settings & i18n changes (types, schema, validation, i18n files)
  4. SQL migrations and repository changes
  5. UI components and dashboard changes

Issues Found

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

High Priority Issues (Should Fix)

[TYPE-ASSERTION] Type Safety Workaround in proxy-handler.ts

Location: src/app/v1/_lib/proxy-handler.ts:24-30

The code uses a type assertion to access setRawCrossProviderFallbackEnabled:

(
  session as ProxySession & {
    setRawCrossProviderFallbackEnabled?: ((enabled: boolean) => void) | undefined;
  }
).setRawCrossProviderFallbackEnabled?.(
  systemSettings.allowNonConversationEndpointProviderFallback ?? true
);

Issue: This type assertion bypasses TypeScript's type checking. The method should be part of the proper ProxySession type definition.

Suggested Fix: Add setRawCrossProviderFallbackEnabled to the ProxySession class type definition and remove the type assertion:

// In session.ts, the method is already defined
// Just ensure it's included in the type exports

// In proxy-handler.ts, use directly:
session.setRawCrossProviderFallbackEnabled(
  systemSettings.allowNonConversationEndpointProviderFallback ?? true
);

[TYPE-ASSERTION] Type Safety Workaround in forwarder.ts

Location: src/app/v1/_lib/proxy/forwarder.ts:471-495

The isSessionRawCrossProviderFallbackEnabled function uses unknown type assertion:

const rawSession = session as unknown as {
  requestUrl?: URL;
  isRawCrossProviderFallbackEnabled?: (() => boolean) | undefined;
  getEndpointPolicy?: (() => ReturnType<typeof resolveEndpointPolicy>) | undefined;
  endpointPolicy?: ReturnType<typeof resolveEndpointPolicy>;
};

Issue: This is another type assertion bypass. The function should properly use the ProxySession type.

Suggested Fix: Use proper method calls on the typed ProxySession:

function isSessionRawCrossProviderFallbackEnabled(session: ProxySession): boolean {
  if (typeof (session as ProxySession & { isRawCrossProviderFallbackEnabled?: () => boolean }).isRawCrossProviderFallbackEnabled === "function") {
    return (session as ProxySession & { isRawCrossProviderFallbackEnabled: () => boolean }).isRawCrossProviderFallbackEnabled();
  }
  return session.getEndpointPolicy().allowRawCrossProviderFallback;
}

[SQL-TRANSACTION] Missing Transaction in Migration

Location: drizzle/0096_equal_selene.sql

The migration performs multiple operations:

  1. ALTER TABLE to add column
  2. DELETE FROM usage_ledger
  3. CREATE OR REPLACE FUNCTION

Issue: If the migration fails partway through (e.g., between the ALTER and DELETE), the database could be in an inconsistent state.

Suggested Fix: Wrap the migration in a transaction:

BEGIN;

ALTER TABLE "system_settings" ADD COLUMN "allow_non_conversation_endpoint_provider_fallback" boolean DEFAULT true NOT NULL;

DELETE FROM "usage_ledger"
WHERE "endpoint" IS NOT NULL
  AND LOWER(REGEXP_REPLACE("endpoint", '/+$', ''))
    IN ('/v1/messages/count_tokens', '/v1/responses/compact');

-- ... rest of migration

COMMIT;

[EDGE-CASE] Empty Endpoint String Handling

Location: src/repository/_shared/usage-log-filters.ts:20-28

The normalizeUsageLogEndpoint function returns an empty string for empty input, which could cause issues if used in SQL comparisons.

Suggested Fix: Consider returning null for empty input or adding explicit handling:

function normalizeUsageLogEndpoint(endpoint: string): string | null {
  const trimmed = endpoint.trim().toLowerCase();
  if (\!trimmed) {
    return null;
  }
  // ...
}

Review Coverage

  • Logic and correctness - No critical bugs found
  • Security (OWASP Top 10) - No security issues found
  • Error handling - Proper error handling in new code
  • Type safety - Two type assertion workarounds noted
  • Documentation accuracy - Comments match code behavior
  • Test coverage - Adequate test coverage for new feature
  • Code clarity - Generally clear implementation

Notes

  • The NON_BILLING_ENDPOINTS constant correctly includes both target endpoints
  • The SQL function properly handles non-billing endpoints by deleting ledger entries
  • i18n translations are complete for all 5 supported languages
  • The guard pipeline correctly uses RAW_SAFE_SESSION_PIPELINE for the target endpoints

Automated review by Claude AI

@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 implements a cross-provider fallback mechanism for non-conversation endpoints, specifically /v1/messages/count_tokens and /v1/responses/compact. It introduces a new system setting, allowNonConversationEndpointProviderFallback, to manage this feature. Key changes include updates to the proxy forwarder to support fallbacks for raw endpoints, adjustments to the guard pipeline to incorporate session context while bypassing chat-specific guards, and comprehensive modifications to billing and logging to ensure these endpoints remain non-billable and hidden from default views. Review feedback suggests refining the implementation by removing unnecessary asynchronous markers, simplifying helper function calls in forwarder.ts, and ensuring that side effects like Codex context tiering are correctly guarded against non-billing requests.

Comment thread src/app/v1/_lib/proxy/response-handler.ts Outdated
Comment thread src/app/v1/_lib/proxy/forwarder.ts Outdated
Comment thread src/app/v1/_lib/proxy/forwarder.ts Outdated
Comment thread src/app/v1/_lib/proxy/forwarder.ts Outdated
Comment thread src/app/v1/_lib/proxy/forwarder.ts Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review Summary

This PR adds allowNonConversationEndpointProviderFallback system setting with 5-language i18n support, enabling cross-provider fallback for /v1/messages/count_tokens and /v1/responses/compact endpoints. The implementation correctly maintains raw-safe session context and non-billing behavior for these endpoints.

PR Size: XL

  • Lines changed: 7,367 (7,220 additions, 147 deletions)
  • Files changed: 55

Split Suggestion: This PR could be split into:

  1. Core proxy/endpoint policy changes (endpoint-policy.ts, guard-pipeline.ts, forwarder.ts)
  2. Session management changes (session.ts, session-guard.ts)
  3. System settings & i18n changes (types, schema, validation, i18n files)
  4. SQL migrations and repository changes
  5. UI components and dashboard changes

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 1 0
Security 0 0 0 0
Error Handling 0 0 1 0
Types 0 0 2 0
SQL 0 0 1 0

Medium Priority Issues (Should Fix)

[TYPE-ASSERTION] Type Safety Workaround in proxy-handler.ts
Location: src/app/v1/_lib/proxy-handler.ts:24-30

The code uses a type assertion to access setRawCrossProviderFallbackEnabled. This bypasses TypeScript's type checking. The method should be part of the proper ProxySession type definition.

Suggested fix: Add setRawCrossProviderFallbackEnabled to the ProxySession class type definition and remove the type assertion.


[TYPE-ASSERTION] Type Safety Workaround in forwarder.ts
Location: src/app/v1/_lib/proxy/forwarder.ts:471-495

The isSessionRawCrossProviderFallbackEnabled function uses unknown type assertion. This is another type assertion bypass that should be properly typed.

Suggested fix: Use proper method calls on the typed ProxySession by ensuring isRawCrossProviderFallbackEnabled is part of the exported type definition.


[SQL-TRANSACTION] Missing Transaction in Migration
Location: drizzle/0096_equal_selene.sql

The migration performs multiple independent operations (ALTER, DELETE, CREATE OR REPLACE FUNCTION) without a transaction wrapper. If it fails partway through, the database could be in an inconsistent state.

Suggested fix: Wrap the migration in BEGIN/COMMIT to ensure atomicity.


[EDGE-CASE] Empty Endpoint String Handling
Location: src/repository/_shared/usage-log-filters.ts:19-28

The normalizeUsageLogEndpoint function returns an empty string for empty input, which could cause issues in SQL comparisons.

Suggested fix: Consider returning null for empty input instead of empty string.

Review Coverage

  • Logic and correctness - No critical bugs found
  • Security (OWASP Top 10) - No security issues found
  • Error handling - Proper error handling in new code
  • Type safety - Two type assertion workarounds noted
  • Documentation accuracy - Comments match code behavior
  • Test coverage - Adequate test coverage for new feature
  • Code clarity - Generally clear implementation

Notes

  • The NON_BILLING_ENDPOINTS constant correctly includes both target endpoints
  • The SQL function properly handles non-billing endpoints by deleting ledger entries
  • i18n translations are complete for all 5 supported languages
  • The guard pipeline correctly uses RAW_SAFE_SESSION_PIPELINE for the target endpoints

Automated review by Claude AI

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

Caution

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

⚠️ Outside diff range comments (3)
tests/unit/proxy/endpoint-policy-parity.test.ts (1)

239-245: ⚠️ Potential issue | 🟡 Minor

测试名已不符合新的默认策略。

默认端点现在并不是“所有 allow flags 都为 true”,allowRawCrossProviderFallback 应保持 false。建议把测试名改成新语义,避免误导。

建议更新测试名
-  test("default policy has all allow flags set to true", () => {
+  test("default policy allows normal chat flow but keeps raw cross-provider fallback disabled", () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/proxy/endpoint-policy-parity.test.ts` around lines 239 - 245, The
test name is misleading because the default policy no longer has all allow flags
true (specifically allowRawCrossProviderFallback remains false); update the test
title string in the test that iterates over DEFAULT_ENDPOINTS and calls
resolveEndpointPolicy(pathname) to reflect the new semantics (e.g., "default
policy has expected allow flags" or "default policy has allow flags set (raw
cross-provider fallback false)"), leaving the assertions on policy.allowRetry,
policy.allowProviderSwitch, policy.allowRawCrossProviderFallback, and
policy.allowCircuitBreakerAccounting unchanged.
tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts (1)

1723-1787: ⚠️ Potential issue | 🟡 Minor

同步更新测试意图并精确锁定 winner。

测试名仍描述旧行为,而且 winnerEntry 先匹配 request_success 再断言 hedge_winner。建议直接按 provider2 + hedge_winner 查找,避免链路里出现其他成功项时产生脆弱失败。

建议收紧测试断言
-  test("endpoint resolution failure should not inflate launchedProviderCount, winner gets request_success not hedge_winner", async () => {
+  test("endpoint resolution fallback counts the initial attempt, so the later provider is hedge_winner", async () => {
@@
-      const winnerEntry = chain.find(
-        (entry) => entry.reason === "request_success" || entry.reason === "hedge_winner"
-      );
+      const winnerEntry = chain.find(
+        (entry) => entry.id === provider2.id && entry.reason === "hedge_winner"
+      );
       expect(winnerEntry).toBeDefined();
-      expect(winnerEntry!.reason).toBe("hedge_winner");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts` around lines 1723
- 1787, Update the test to assert the specific provider2 hedge-winner entry
instead of loosely matching any "request_success" or "hedge_winner": change the
chain lookup that assigns winnerEntry to find an entry where entry.provider?.id
=== 2 && entry.reason === "hedge_winner" (using session.getProviderChain()), and
update the test name string to reflect that provider2 should be the
hedge_winner; keep the existing assertions that the response contains
"provider":"p2" and session.provider?.id === 2.
src/app/v1/_lib/proxy/guard-pipeline.ts (1)

189-196: ⚠️ Potential issue | 🟡 Minor

确认 fromRequestType 仅在测试代码中使用,生产环境无影响,但方法设计存在不一致之处。

此改动在生产代码中的实际影响已被限制:proxy-handler.ts 的生产路径通过 fromSession() 进行管道构建(第 81 行),该方法会根据 allowRawCrossProviderFallback 标志在 RAW_SAFE_SESSION_PIPELINERAW_PASSTHROUGH_PIPELINE 间选择。

fromRequestType() 仅在测试代码中被调用(见 guard-pipeline-warmup.test.ts),不存在遗留的生产调用点。因此原有对 count_tokens 创建 session 绑定的担忧实际上不会在生产环境中发生。

然而,方法设计不一致性仍存在:

  • fromRequestType(RequestType.COUNT_TOKENS) 硬编码返回 RAW_SAFE_SESSION_PIPELINE(始终包含 "session" 和 "messageContext" 步骤)
  • fromEndpointPolicy() 则根据标志条件地选择管道

此不一致可能在未来维护中造成混淆。若测试意图是总是验证完整的会话流程,建议在方法或注释中明确说明该设计选择。

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

In `@src/app/v1/_lib/proxy/guard-pipeline.ts` around lines 189 - 196,
fromRequestType currently hardcodes RequestType.COUNT_TOKENS to
RAW_SAFE_SESSION_PIPELINE which is inconsistent with
fromSession/fromEndpointPolicy that respect allowRawCrossProviderFallback;
update fromRequestType(RequestType.COUNT_TOKENS) to mirror the same conditional
selection between RAW_SAFE_SESSION_PIPELINE and RAW_PASSTHROUGH_PIPELINE (using
the same flag/logic as fromSession/fromEndpointPolicy) or, if the hardcoded
behavior is intentional for tests, add a clear comment above fromRequestType
explaining it is test-only and why RAW_SAFE_SESSION_PIPELINE is required
(reference symbols: fromRequestType, RequestType.COUNT_TOKENS,
RAW_SAFE_SESSION_PIPELINE, RAW_PASSTHROUGH_PIPELINE, fromSession,
fromEndpointPolicy, allowRawCrossProviderFallback,
guard-pipeline-warmup.test.ts).
🧹 Nitpick comments (15)
src/lib/ledger-backfill/trigger.sql (1)

172-176: 非对话端点的早退逻辑正确,但建议补充注释说明语义。

该分支在非对话端点写入时会删除已存在的 usage_ledger 行并直接返回,覆盖了「历史 UPDATE 将旧对话端点改写为非对话端点」这种罕见但可能触发的场景。归一化规则(lowercase + 去尾斜杠)与 service.tsledger-conditions.ts 中保持一致。

建议在该分支上方添加简短注释,说明为何要 DELETE 而不是仅跳过 UPSERT(即:保证之前可能已写入的 ledger 行被清理),便于后续维护者理解。

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

In `@src/lib/ledger-backfill/trigger.sql` around lines 172 - 176, 在触发器中针对非对话端点(使用
LOWER(REGEXP_REPLACE(COALESCE(NEW.endpoint, ''), '/+$', '')) 与
'/v1/messages/count_tokens'、'/v1/responses/compact' 比对)直接 DELETE FROM
usage_ledger WHERE request_id = NEW.id 并 RETURN NEW 的分支需要补充注释,说明之所以 DELETE
而不是仅跳过 UPSERT 是为了清理可能在历史写入/旧对话端点写入时已存在的 ledger 行,避免遗留无效记录;请在该分支上方添加一行简短注释,明确引用
NEW、usage_ledger 和归一化规则以便后续维护者理解语义和原因。
src/app/v1/_lib/proxy-handler.ts (1)

24-40: 改为直接调用 session 已公开的方法。

setRawCrossProviderFallbackEnabled 已在 ProxySession 类(src/app/v1/_lib/proxy/session.ts:353)中公开定义为实例方法。第 24-30 和 36-40 行不应使用类型交叉和可选调用,直接调用即可,这样可以避免隐藏集成缺失问题(如果方法被删除或重命名,类型检查会立即捕获错误)。

建议改为直接调用
-      (
-        session as ProxySession & {
-          setRawCrossProviderFallbackEnabled?: ((enabled: boolean) => void) | undefined;
-        }
-      ).setRawCrossProviderFallbackEnabled?.(
+      session.setRawCrossProviderFallbackEnabled(
         systemSettings.allowNonConversationEndpointProviderFallback ?? true
       );

同样改进第 36-40 行。

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

In `@src/app/v1/_lib/proxy-handler.ts` around lines 24 - 40, The code is using a
type intersection and optional chaining to call
setRawCrossProviderFallbackEnabled on session; since ProxySession already
publicly defines setRawCrossProviderFallbackEnabled (see ProxySession), remove
the cast and optional call and invoke
session.setRawCrossProviderFallbackEnabled(...) directly in both places (the try
block and the catch block), keeping the same boolean arguments; also keep the
existing logger.warn and session.setHighConcurrencyModeEnabled(false) behavior
unchanged.
src/app/v1/_lib/proxy/session.ts (1)

357-365: ?? resolveEndpointPolicy(...) 兜底分支不可达,可简化。

endpointPolicyreadonly 且在构造函数里已经通过 resolveSessionEndpointPolicy(init.requestUrl)(第 206 行)赋值,运行时永远不会是 null/undefined。第 358-360 行的 ?? 兜底在类型上也没有意义,保留会让读者误以为该字段可能缺失。

Proposed simplification
   isRawCrossProviderFallbackEnabled(): boolean {
-    const endpointPolicy =
-      this.endpointPolicy ??
-      resolveEndpointPolicy((this.requestUrl as URL | undefined)?.pathname ?? "/");
     return (
-      endpointPolicy.allowRawCrossProviderFallback &&
+      this.endpointPolicy.allowRawCrossProviderFallback &&
       (this.rawCrossProviderFallbackEnabled ?? false)
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/session.ts` around lines 357 - 365, The
isRawCrossProviderFallbackEnabled method contains an unnecessary fallback
expression "this.endpointPolicy ?? resolveEndpointPolicy(...)" because
endpointPolicy is readonly and already set in the constructor via
resolveSessionEndpointPolicy, so remove the fallback and read endpointPolicy
directly; update isRawCrossProviderFallbackEnabled to reference
this.endpointPolicy only and keep the boolean expression using
this.endpointPolicy.allowRawCrossProviderFallback and
this.rawCrossProviderFallbackEnabled to simplify the code and avoid implying
endpointPolicy can be undefined.
src/repository/_shared/ledger-conditions.ts (1)

5-20: SQL 归一化与 JS 归一化存在轻微差异。

isNonBillingEndpointperformance-formatter.ts)先 trim()toLowerCase() + 去尾斜杠;这里的 SQL 版本仅做 LOWER + REGEXP_REPLACE('/+$', ''),不处理前后空白。虽然 usageLedger.endpoint 通常来自 URL pathname 不会含空白,但建议保持两处一致性以免未来某条写入路径没有 trim 时出现跨层判定错位。

Proposed fix
 const NON_BILLING_LEDGER_ENDPOINT_CONDITION = sql`(
   ${usageLedger.endpoint} IS NULL
-  OR LOWER(REGEXP_REPLACE(${usageLedger.endpoint}, '/+$', '')) NOT IN (
+  OR LOWER(REGEXP_REPLACE(BTRIM(${usageLedger.endpoint}), '/+$', '')) NOT IN (
     ${sql.join(
       NON_BILLING_ENDPOINTS.map((endpoint) => sql`${endpoint}`),
       sql`, `
     )}
   )
 )`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/repository/_shared/ledger-conditions.ts` around lines 5 - 20, The SQL
endpoint normalization in NON_BILLING_LEDGER_ENDPOINT_CONDITION differs from the
JS isNonBillingEndpoint: it doesn't trim whitespace first, so align them by
applying a trim step to ${usageLedger.endpoint} before lowercasing and removing
trailing slashes; update the expression used in
NON_BILLING_LEDGER_ENDPOINT_CONDITION (which is also referenced by
LEDGER_BILLING_CONDITION) to perform whitespace trimming then
REGEXP_REPLACE('/+$', '') and LOWER, and keep the comparison against the joined
NON_BILLING_ENDPOINTS unchanged.
tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts (1)

171-220: createSession 仍通过 Object.create + Object.assign 绕过构造函数,且手动挂 getEndpointPolicy——易与 ProxySession 内部字段漂移。

当前写法把 endpointPolicy 作为普通属性而非 private readonly 字段挂载(原类中是 private readonly),并手动重写 getEndpointPolicy,但 isRawCrossProviderFallbackEnabled 等新方法会直接访问 this.endpointPolicy(字段访问而非 getter),目前两者恰好一致所以通过。若未来 ProxySession 把 endpoint policy 的访问改成懒加载或私有化再加 getter,这套反射式测试会悄无声息地偏离真实行为。建议后续为 ProxySession 增加一个测试工厂(例如 ProxySession.forTest(...))统一构造,避免继续扩大反射式构造的面。非本 PR 必须修复。

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

In `@tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts` around lines 171 -
220, The test helper createSession currently fabricates a ProxySession using
Object.create + Object.assign and manually sets endpointPolicy and
getEndpointPolicy, which can drift from the real ProxySession behavior (see
getEndpointPolicy, endpointPolicy, isRawCrossProviderFallbackEnabled); replace
this by adding a test factory on the real class (e.g. static
ProxySession.forTest(options) that constructs a real ProxySession instance with
sensible defaults and accepts overrides) and update createSession to call
ProxySession.forTest(...) instead of using Object.create, so private/readonly
fields and any lazy/getter logic in ProxySession remain accurate in tests.
src/app/v1/_lib/proxy/response-handler.ts (1)

3604-3617: 计费归零路径内部冗余,但通过“双重防线”保证了正确性

注意到这里把 normalizedUsage 而不是 billableNormalizedUsage 再次喂给 updateRequestCostFromUsage(3611)和 trackCostToRedis(3625),它们内部分别在 3412、3722 行对 non-billing endpoint 做了短路,所以结果仍然正确。双重防线在这里是好事,但读起来容易让人以为有遗漏。

如果想让阅读更顺畅,可把 billableNormalizedUsage 的变量名提示语义,或在调用两个下游前集中注释一次“非计费端点的成本/限额写入依赖下游内部短路”。非强制。

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

In `@src/app/v1/_lib/proxy/response-handler.ts` around lines 3604 - 3617, The code
uses normalizedUsage (not billableNormalizedUsage) when calling
updateRequestCostFromUsage and trackCostToRedis which is redundant and confusing
because those callee functions short-circuit non-billing endpoints; change the
calls to pass billableNormalizedUsage instead of normalizedUsage (references:
billableNormalizedUsage, normalizedUsage, updateRequestCostFromUsage,
trackCostToRedis, isNonBillingUsageEndpoint, maybeSetCodexContext1m) so the
intent is clear, or if you prefer to keep normalizedUsage for a deliberate
“double‑check” safety, add a single clarifying comment above the two calls
stating “non-billing endpoints are handled by internal short-circuit in
updateRequestCostFromUsage/trackCostToRedis” to avoid reader confusion.
src/app/v1/_lib/proxy/forwarder.ts (4)

958-995: endpointPolicysend 中被声明了两次(L960 外层循环前、L992 外层循环体内),含义相同。

ProxyForwarder.getEndpointPolicy(session) 在同一请求生命周期里返回同一个策略(基于 session.requestUrl.pathname),外层已计算过,内层可以直接复用。移除内层声明避免读者误以为两者可能不同,也能略微省去一次原型方法解析。

建议
-      const endpointPolicy = ProxyForwarder.getEndpointPolicy(session);
       const shouldAccountCircuitBreaker = endpointPolicy.allowCircuitBreakerAccounting;
       const shouldEnforceStrictEndpointPool =
         !isMcpRequest && isStrictEndpointPoolPolicy(endpointPolicy) && providerVendorId > 0;
🤖 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 958 - 995, The duplicate
declaration of endpointPolicy inside send masks the outer value and causes an
unnecessary second call to ProxyForwarder.getEndpointPolicy(session); remove the
inner declaration at the start of the outer loop body and reuse the outer
endpointPolicy variable instead (keep the outer const endpointPolicy =
ProxyForwarder.getEndpointPolicy(session); and delete the inner const
endpointPolicy = ProxyForwarder.getEndpointPolicy(session); so subsequent uses
like shouldAccountCircuitBreaker and isStrictEndpointPoolPolicy refer to the
same endpointPolicy).

1929-1942: 此处的 !endpointPolicy.allowRetry && !rawCrossProviderFallbackEnabled 与外层 shouldSkipRawRetryAndProviderSwitch 语义完全一致(L961-962)。

其它几个分支(L1667、L1783、L1850)都已经直接使用 shouldSkipRawRetryAndProviderSwitch;这里保留原始写法不一致,且日志消息重复表达同一语义。建议统一:

建议
-            if (!endpointPolicy.allowRetry && !rawCrossProviderFallbackEnabled) {
+            if (shouldSkipRawRetryAndProviderSwitch) {
               logger.debug(
                 "ProxyForwarder: raw passthrough endpoint error, skipping circuit breaker and provider switch",
🤖 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 1929 - 1942, The condition
here duplicates the same semantics as the existing boolean
shouldSkipRawRetryAndProviderSwitch; update the if block in ProxyForwarder to
use shouldSkipRawRetryAndProviderSwitch instead of manually checking
!endpointPolicy.allowRetry && !rawCrossProviderFallbackEnabled, and simplify the
logger.debug message to reference shouldSkipRawRetryAndProviderSwitch (remove
the redundant "skipping circuit breaker and provider switch" phrasing) while
preserving the same structured fields (providerId, providerName, statusCode,
error, policyKind), then throw lastError as before.

534-649: isSessionRawCrossProviderFallbackEnabled(session) ? { …无 upstream … } : { …含 upstream … } 的三元结构在本文件内重复了 5 次。

分别出现在:

  • buildClientErrorChainEntry (L544-560)
  • buildRetryFailedChainEntry (L601-617)
  • resource_not_found 分支 (L1763-1778)
  • vendor_type_all_timeout 分支 (L1900-1915)
  • retry_failed(PROVIDER_ERROR)分支 (L1969-1984)

每份副本都必须同步维护"raw 模式下剥离 upstreamBody / upstreamParsed"的策略,未来只要任何一处漏改,就会在 raw 回退路径上泄漏上游响应体到决策链。建议抽取一个小 helper,把策略集中到一处:

建议重构
function buildChainProviderDetails(
  session: ProxySession,
  provider: Provider,
  statusCode: number | undefined,
  statusText: string,
  upstream: ProxyError["upstreamError"] | undefined
) {
  const base = { id: provider.id, name: provider.name, statusCode, statusText };
  if (isSessionRawCrossProviderFallbackEnabled(session)) {
    return base;
  }
  return {
    ...base,
    upstreamBody: upstream?.body,
    upstreamParsed: upstream?.parsed,
  };
}

调用点可改为 provider: buildChainProviderDetails(session, currentProvider, statusCode, proxyError.message, proxyError.upstreamError) 等,显著降低维护成本并减少漏改风险。

🤖 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 534 - 649, The repeated
ternary that strips upstreamBody/upstreamParsed for raw cross-provider fallback
should be extracted into a single helper (e.g.,
buildChainProviderDetails(session, provider, statusCode, statusText, upstream))
and used wherever provider-details are built (replace the inline ternary in
buildClientErrorChainEntry and buildRetryFailedChainEntry and the other branches
noted: resource_not_found, vendor_type_all_timeout, retry_failed
(PROVIDER_ERROR)). Implement the helper to return { id, name, statusCode,
statusText } when isSessionRawCrossProviderFallbackEnabled(session) is true,
otherwise include upstreamBody and upstreamParsed from upstream; then call that
helper at each location and remove the duplicated ternary logic.

3377-3383: isRawCrossProviderFallbackEnabled 没有真正的异步工作,可以简化且去掉 async

当前实现等价于直接返回 isSessionRawCrossProviderFallbackEnabled(session),但被写成 async 并通过 !x ? false : true 绕了一圈,调用侧的 await(L959)也会额外产生一次微任务切换。

建议简化
-  private static async isRawCrossProviderFallbackEnabled(session: ProxySession): Promise<boolean> {
-    if (!isSessionRawCrossProviderFallbackEnabled(session)) {
-      return false;
-    }
-
-    return true;
-  }
+  private static isRawCrossProviderFallbackEnabled(session: ProxySession): boolean {
+    return isSessionRawCrossProviderFallbackEnabled(session);
+  }

对应调用点(L958-959)去掉 await 即可。后续如果确实需要查询 getCachedSystemSettings() 再改回 async 也很直接。

🤖 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 3377 - 3383, The method
isRawCrossProviderFallbackEnabled currently adds unnecessary async/await
overhead; change its implementation to non-async and simply return
isSessionRawCrossProviderFallbackEnabled(session) (i.e., remove async and the
redundant conditional), and update callers that do "await
isRawCrossProviderFallbackEnabled(...)" to call it synchronously (remove the
await). Refer to the symbols isRawCrossProviderFallbackEnabled and
isSessionRawCrossProviderFallbackEnabled and remove the extra microtask by
adjusting both the function and its call sites.
src/repository/_shared/usage-log-filters.ts (1)

17-17: 建议对 DEFAULT_HIDDEN_USAGE_LOG_ENDPOINTS 做一次规范化,确保与 buildNormalizedEndpointSql 对齐。

buildDefaultHiddenUsageLogEndpointCondition 在 SQL 侧对列做了 LOWER(REGEXP_REPLACE(..., '/+$', '')) 规范化,但右侧比较值直接来自 NON_BILLING_ENDPOINTS。如果未来 NON_BILLING_ENDPOINTS 里出现任何大小写或尾部斜杠差异,NOT IN 会漏掉记录(即应被隐藏的端点被意外暴露)。在构造时统一规范化一次可以消除该隐性耦合:

建议修改
-export const DEFAULT_HIDDEN_USAGE_LOG_ENDPOINTS = [...NON_BILLING_ENDPOINTS];
+export const DEFAULT_HIDDEN_USAGE_LOG_ENDPOINTS = NON_BILLING_ENDPOINTS.map((endpoint) =>
+  endpoint.trim().toLowerCase().replace(/\/+$/, "") || endpoint
+);

备注:如果 NON_BILLING_ENDPOINTS 已有明确约束(全小写、无尾斜杠),可以加一个单元测试守住该约束,或者直接 Object.freeze 并注释说明。

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

In `@src/repository/_shared/usage-log-filters.ts` at line 17,
DEFAULT_HIDDEN_USAGE_LOG_ENDPOINTS must be normalized to match the SQL-side
normalization performed by
buildNormalizedEndpointSql/buildDefaultHiddenUsageLogEndpointCondition; update
DEFAULT_HIDDEN_USAGE_LOG_ENDPOINTS (constructed from NON_BILLING_ENDPOINTS) to
map each entry to a normalized form (apply LOWER and strip trailing slashes,
e.g., value.toLowerCase().replace(/\/+$/, '')) so the NOT IN comparison is
correct, and consider freezing the array or adding a unit test to enforce that
NON_BILLING_ENDPOINTS entries remain normalized if you choose not to normalize
at construction time; update references to DEFAULT_HIDDEN_USAGE_LOG_ENDPOINTS
accordingly.
src/app/v1/_lib/proxy/guard-pipeline.ts (2)

161-172: 缓存 session.getEndpointPolicy() 的返回值,避免重复调用。

isRawCrossProviderFallbackEnabled 方法不存在时,session.getEndpointPolicy() 会被调用两次(L167、L170),且需要将同一个策略对象同时传给 fromEndpointPolicy。虽然通常开销可忽略,但会话上的 getter 实现可能存在副作用或内部计算,先取一次更稳妥。

建议重构
   static fromSession(
     session: Pick<ProxySession, "getEndpointPolicy"> & {
       isRawCrossProviderFallbackEnabled?: (() => boolean) | undefined;
     }
   ): GuardPipeline {
-    return GuardPipelineBuilder.fromEndpointPolicy(
-      session.getEndpointPolicy(),
-      typeof session.isRawCrossProviderFallbackEnabled === "function"
-        ? session.isRawCrossProviderFallbackEnabled()
-        : session.getEndpointPolicy().allowRawCrossProviderFallback
-    );
+    const policy = session.getEndpointPolicy();
+    const rawEnabled =
+      typeof session.isRawCrossProviderFallbackEnabled === "function"
+        ? session.isRawCrossProviderFallbackEnabled()
+        : policy.allowRawCrossProviderFallback;
+    return GuardPipelineBuilder.fromEndpointPolicy(policy, rawEnabled);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/guard-pipeline.ts` around lines 161 - 172, In
GuardPipeline.fromSession, avoid calling session.getEndpointPolicy() twice;
cache its result in a local variable (e.g., endpointPolicy) and use that same
object when calling GuardPipelineBuilder.fromEndpointPolicy and when reading
allowRawCrossProviderFallback as the fallback for
isRawCrossProviderFallbackEnabled; this ensures the same policy instance is
passed and prevents repeated getter side effects when evaluating
session.getEndpointPolicy() and session.isRawCrossProviderFallbackEnabled().

223-227: COUNT_TOKENS_PIPELINE 作为 RAW_SAFE_SESSION_PIPELINE 的别名导出,建议在注释中标明用途与弃用倾向。

保留兼容命名是合理的,但没有任何注释说明"新代码应直接使用 RAW_SAFE_SESSION_PIPELINE"。加一行 JSDoc 可避免未来有人基于命名(COUNT_TOKENS_PIPELINE)误以为它仅服务于 count_tokens 端点而去扩展它:

建议
+/**
+ * `@deprecated` 保留用于兼容;新代码请直接使用 `RAW_SAFE_SESSION_PIPELINE`。
+ * 该别名的语义是"带 session/messageContext 的 raw passthrough 流水线",
+ * 不再与具体端点(count_tokens)耦合。
+ */
 export const COUNT_TOKENS_PIPELINE: GuardConfig = RAW_SAFE_SESSION_PIPELINE;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/guard-pipeline.ts` around lines 223 - 227,
COUNT_TOKENS_PIPELINE is exported as an alias of RAW_SAFE_SESSION_PIPELINE
without any doc explaining intent; add a JSDoc comment above
COUNT_TOKENS_PIPELINE stating that it is a compatibility alias for
RAW_SAFE_SESSION_PIPELINE, mark it as deprecated for new code and instruct
callers to use RAW_SAFE_SESSION_PIPELINE directly (mention both identifiers
RAW_SAFE_SESSION_PIPELINE and COUNT_TOKENS_PIPELINE so reviewers can find the
change).
tests/unit/proxy/non-chat-endpoint-fallback.test.ts (2)

161-226: 测试会话通过 Object.create(ProxySession.prototype) 手工拼装,依赖 ProxySession 当前的内部字段布局。

ProxySession 后续新增必需的私有状态(例如在构造器里初始化的集合/映射)时,这些测试会静默漂移——方法在原型上可以调用,但内部状态缺失。可以考虑后续抽取一个受测类专用的 createFakeProxySession(overrides) 工厂,把字段初始化集中维护,降低这类维护成本。当前实现可接受。

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

In `@tests/unit/proxy/non-chat-endpoint-fallback.test.ts` around lines 161 - 226,
Tests build a ProxySession by Object.create(ProxySession.prototype) which relies
on the current internal field layout and will break silently if ProxySession
later adds required private state; replace or supplement createRawSession with a
dedicated factory (e.g., createFakeProxySession(overrides)) that centrally
initializes all internal fields and any collections/maps/sets that ProxySession
constructor normally creates, merge caller overrides, and ensure methods like
setProvider, addProviderToChain, getProviderChain, isHeaderModified, etc.
operate against those initialized defaults so future private-state additions
won't silently break tests.

289-406: disabled setting 四个用例的 session 组装完全同构,可以抽取共享 setup 降噪。

RESPONSES_COMPACT + codex provider + response originalFormat + gpt-5 message 的组合在 4 个 test 中逐字重复。抽成 buildDisabledRawCompactSession() 辅助可以让每个用例只保留"它在测什么"的差异(错误分类、抛出的错误类型),提升可读性。属于可选优化。

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

In `@tests/unit/proxy/non-chat-endpoint-fallback.test.ts` around lines 289 - 406,
The four tests duplicate identical session setup
(V1_ENDPOINT_PATHS.RESPONSES_COMPACT, createRawSession,
createProvider(...,{providerType:"codex"}), session.originalFormat="response",
session.setRawCrossProviderFallbackEnabled(false), session.request.message {
model: "gpt-5", ... }, session.setProvider(providerA}); refactor by extracting a
helper (e.g., buildDisabledRawCompactSession()) that performs this common
assembly and returns the session (and provider if needed), then replace the
repeated blocks in the tests with calls to that helper and keep each test only
showing its differing parts (mocked categorizeErrorAsync/doForward behavior and
expected assertions).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@drizzle/0096_equal_selene.sql`:
- Line 47: v_is_success currently only checks NEW.error_message which can mark
4xx/5xx responses as successful if error_message is empty; update the
v_is_success assignment to also validate NEW.status_code (e.g., require
NEW.status_code BETWEEN 200 AND 299 or NEW.status_code < 400) in addition to
(NEW.error_message IS NULL OR NEW.error_message = ''), so the trigger/assignment
that sets v_is_success rejects non-2xx status codes; modify the expression that
assigns v_is_success (referencing v_is_success, NEW.error_message, and
NEW.status_code) accordingly.

In `@src/app/v1/_lib/proxy/response-handler.ts`:
- Line 3606: maybeSetCodexContext1m is being called for non-billing endpoints
(/v1/messages/count_tokens and /v1/responses/compact) which writes
context_1m_applied to the DB; while current logic (updateRequestCostFromUsage
early return, calculateRequestCost comments, and tests) prevents billing impact,
make intent explicit: either remove/skip calling maybeSetCodexContext1m for
those non-billing paths or add a guard in maybeSetCodexContext1m (or the caller
in response-handler.ts) to no-op when handling non-billing endpoints, and ensure
any persisted flag stays purely informational (reference functions
maybeSetCodexContext1m, updateRequestCostFromUsage, calculateRequestCost and the
context_1m_applied field/ledger-backfill flow).

In `@src/lib/config/system-settings-cache.ts`:
- Line 61: Change the fail-open default so cold-cache/db-read failures don't
re-enable cross-vendor raw fallback: set
allowNonConversationEndpointProviderFallback to false in the DEFAULT_SETTINGS
object (and any duplicate DEFAULT_SETTINGS or literal defaults at the other
occurrence around lines 140-141) so that getCachedSystemSettings() returning
DEFAULT_SETTINGS on DB errors will be fail-closed; update the same flag wherever
the default settings are defined to keep behavior consistent.

In `@src/lib/session-manager.ts`:
- Around line 646-655: The SessionManager code currently only rejects a provider
binding when a bound key exists but mismatches; update the check in the method
containing the shown block (the SessionManager code that reads
`redis.get('session:${sessionId}:key')`) to treat a missing boundKeyId as a
validation failure as well — i.e., if `boundKeyId` is falsy or differs from
`keyId.toString()` then log the warning and return null so we never reuse a
provider when the session:key is absent; alternatively ensure any codepath that
writes `session:${sessionId}:provider` also atomically writes
`session:${sessionId}:key` with the same TTL, but the immediate fix is to change
the conditional around `boundKeyId` to deny when it's null/undefined.

In `@src/repository/_shared/usage-log-filters.ts`:
- Around line 156-160: filters.endpoint currently uses a truthy check which
treats pure-whitespace strings as true and causes
buildUsageLogEndpointMatchCondition (which returns null for empty trimmed input)
to push null into conditions; update the check to mirror
buildDefaultHiddenUsageLogEndpointCondition by testing trimmed content (e.g. use
filters.endpoint?.trim() or equivalent) before calling
buildUsageLogEndpointMatchCondition with messageRequest.endpoint so only
non-empty endpoints produce a SQL condition and null is never pushed into
conditions.

In `@src/repository/system-config.ts`:
- Around line 211-212: The current fallback/projection logic unconditionally
falls back to a smaller legacy/minimal projection that still references
systemSettings.allowNonConversationEndpointProviderFallback, which causes other
modern fields to be lost when only that new column is missing; add an
intermediate projection and corresponding read/returning/update downgrade that
only strips the allowNonConversationEndpointProviderFallback field (keep all
other modern systemSettings fields) and try that projection before falling back
to the broader legacy/minimal projection. Update the code paths that build
projections/returning clauses and the update logic that mentions
allowNonConversationEndpointProviderFallback so they first attempt the
"only-remove-this-new-column" projection, and apply the same change to the other
occurrences where projections are defined (the other blocks referencing
allowNonConversationEndpointProviderFallback).

In `@src/repository/usage-logs.ts`:
- Around line 1583-1589: The totalTokens calculation is double-counting cache
creation by adding totalCacheCreation5mTokens and totalCacheCreation1hTokens in
addition to totalCacheCreationTokens; update the logic in the totalTokens
assignment (the variable named totalTokens that reads summaryResult.* fields) to
match other branches by only summing
input+output+totalCacheCreationTokens+totalCacheReadTokens (i.e., remove or
replace addition of totalCacheCreation5mTokens and totalCacheCreation1hTokens),
or alternatively compute cacheCreation = totalCacheCreationTokens ||
(totalCacheCreation5mTokens + totalCacheCreation1hTokens) and use that single
cacheCreation value in the totalTokens sum so all endpoints use the same
cache-creation accounting.

In `@tests/unit/repository/usage-logs-min-retry-count-filter.test.ts`:
- Around line 45-49: The test obtains `condition` from
`buildUsageLogConditions({ minRetryCount: 1 })` and then calls
`sqlToString(condition)`, but under strict null checks `condition` has type `SQL
| undefined`; after the `expect(condition).toBeDefined()` you must narrow the
type—either use a non-null assertion when passing to `sqlToString` (e.g.
`sqlToString(condition!)`) or add an explicit runtime guard that throws or fails
the test if `condition` is undefined (e.g. `if (!condition) throw new
Error("expected condition")`) before calling `sqlToString`; apply this change
where `condition` and `sqlToString` are used in the test.

---

Outside diff comments:
In `@src/app/v1/_lib/proxy/guard-pipeline.ts`:
- Around line 189-196: fromRequestType currently hardcodes
RequestType.COUNT_TOKENS to RAW_SAFE_SESSION_PIPELINE which is inconsistent with
fromSession/fromEndpointPolicy that respect allowRawCrossProviderFallback;
update fromRequestType(RequestType.COUNT_TOKENS) to mirror the same conditional
selection between RAW_SAFE_SESSION_PIPELINE and RAW_PASSTHROUGH_PIPELINE (using
the same flag/logic as fromSession/fromEndpointPolicy) or, if the hardcoded
behavior is intentional for tests, add a clear comment above fromRequestType
explaining it is test-only and why RAW_SAFE_SESSION_PIPELINE is required
(reference symbols: fromRequestType, RequestType.COUNT_TOKENS,
RAW_SAFE_SESSION_PIPELINE, RAW_PASSTHROUGH_PIPELINE, fromSession,
fromEndpointPolicy, allowRawCrossProviderFallback,
guard-pipeline-warmup.test.ts).

In `@tests/unit/proxy/endpoint-policy-parity.test.ts`:
- Around line 239-245: The test name is misleading because the default policy no
longer has all allow flags true (specifically allowRawCrossProviderFallback
remains false); update the test title string in the test that iterates over
DEFAULT_ENDPOINTS and calls resolveEndpointPolicy(pathname) to reflect the new
semantics (e.g., "default policy has expected allow flags" or "default policy
has allow flags set (raw cross-provider fallback false)"), leaving the
assertions on policy.allowRetry, policy.allowProviderSwitch,
policy.allowRawCrossProviderFallback, and policy.allowCircuitBreakerAccounting
unchanged.

In `@tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts`:
- Around line 1723-1787: Update the test to assert the specific provider2
hedge-winner entry instead of loosely matching any "request_success" or
"hedge_winner": change the chain lookup that assigns winnerEntry to find an
entry where entry.provider?.id === 2 && entry.reason === "hedge_winner" (using
session.getProviderChain()), and update the test name string to reflect that
provider2 should be the hedge_winner; keep the existing assertions that the
response contains "provider":"p2" and session.provider?.id === 2.

---

Nitpick comments:
In `@src/app/v1/_lib/proxy-handler.ts`:
- Around line 24-40: The code is using a type intersection and optional chaining
to call setRawCrossProviderFallbackEnabled on session; since ProxySession
already publicly defines setRawCrossProviderFallbackEnabled (see ProxySession),
remove the cast and optional call and invoke
session.setRawCrossProviderFallbackEnabled(...) directly in both places (the try
block and the catch block), keeping the same boolean arguments; also keep the
existing logger.warn and session.setHighConcurrencyModeEnabled(false) behavior
unchanged.

In `@src/app/v1/_lib/proxy/forwarder.ts`:
- Around line 958-995: The duplicate declaration of endpointPolicy inside send
masks the outer value and causes an unnecessary second call to
ProxyForwarder.getEndpointPolicy(session); remove the inner declaration at the
start of the outer loop body and reuse the outer endpointPolicy variable instead
(keep the outer const endpointPolicy =
ProxyForwarder.getEndpointPolicy(session); and delete the inner const
endpointPolicy = ProxyForwarder.getEndpointPolicy(session); so subsequent uses
like shouldAccountCircuitBreaker and isStrictEndpointPoolPolicy refer to the
same endpointPolicy).
- Around line 1929-1942: The condition here duplicates the same semantics as the
existing boolean shouldSkipRawRetryAndProviderSwitch; update the if block in
ProxyForwarder to use shouldSkipRawRetryAndProviderSwitch instead of manually
checking !endpointPolicy.allowRetry && !rawCrossProviderFallbackEnabled, and
simplify the logger.debug message to reference
shouldSkipRawRetryAndProviderSwitch (remove the redundant "skipping circuit
breaker and provider switch" phrasing) while preserving the same structured
fields (providerId, providerName, statusCode, error, policyKind), then throw
lastError as before.
- Around line 534-649: The repeated ternary that strips
upstreamBody/upstreamParsed for raw cross-provider fallback should be extracted
into a single helper (e.g., buildChainProviderDetails(session, provider,
statusCode, statusText, upstream)) and used wherever provider-details are built
(replace the inline ternary in buildClientErrorChainEntry and
buildRetryFailedChainEntry and the other branches noted: resource_not_found,
vendor_type_all_timeout, retry_failed (PROVIDER_ERROR)). Implement the helper to
return { id, name, statusCode, statusText } when
isSessionRawCrossProviderFallbackEnabled(session) is true, otherwise include
upstreamBody and upstreamParsed from upstream; then call that helper at each
location and remove the duplicated ternary logic.
- Around line 3377-3383: The method isRawCrossProviderFallbackEnabled currently
adds unnecessary async/await overhead; change its implementation to non-async
and simply return isSessionRawCrossProviderFallbackEnabled(session) (i.e.,
remove async and the redundant conditional), and update callers that do "await
isRawCrossProviderFallbackEnabled(...)" to call it synchronously (remove the
await). Refer to the symbols isRawCrossProviderFallbackEnabled and
isSessionRawCrossProviderFallbackEnabled and remove the extra microtask by
adjusting both the function and its call sites.

In `@src/app/v1/_lib/proxy/guard-pipeline.ts`:
- Around line 161-172: In GuardPipeline.fromSession, avoid calling
session.getEndpointPolicy() twice; cache its result in a local variable (e.g.,
endpointPolicy) and use that same object when calling
GuardPipelineBuilder.fromEndpointPolicy and when reading
allowRawCrossProviderFallback as the fallback for
isRawCrossProviderFallbackEnabled; this ensures the same policy instance is
passed and prevents repeated getter side effects when evaluating
session.getEndpointPolicy() and session.isRawCrossProviderFallbackEnabled().
- Around line 223-227: COUNT_TOKENS_PIPELINE is exported as an alias of
RAW_SAFE_SESSION_PIPELINE without any doc explaining intent; add a JSDoc comment
above COUNT_TOKENS_PIPELINE stating that it is a compatibility alias for
RAW_SAFE_SESSION_PIPELINE, mark it as deprecated for new code and instruct
callers to use RAW_SAFE_SESSION_PIPELINE directly (mention both identifiers
RAW_SAFE_SESSION_PIPELINE and COUNT_TOKENS_PIPELINE so reviewers can find the
change).

In `@src/app/v1/_lib/proxy/response-handler.ts`:
- Around line 3604-3617: The code uses normalizedUsage (not
billableNormalizedUsage) when calling updateRequestCostFromUsage and
trackCostToRedis which is redundant and confusing because those callee functions
short-circuit non-billing endpoints; change the calls to pass
billableNormalizedUsage instead of normalizedUsage (references:
billableNormalizedUsage, normalizedUsage, updateRequestCostFromUsage,
trackCostToRedis, isNonBillingUsageEndpoint, maybeSetCodexContext1m) so the
intent is clear, or if you prefer to keep normalizedUsage for a deliberate
“double‑check” safety, add a single clarifying comment above the two calls
stating “non-billing endpoints are handled by internal short-circuit in
updateRequestCostFromUsage/trackCostToRedis” to avoid reader confusion.

In `@src/app/v1/_lib/proxy/session.ts`:
- Around line 357-365: The isRawCrossProviderFallbackEnabled method contains an
unnecessary fallback expression "this.endpointPolicy ??
resolveEndpointPolicy(...)" because endpointPolicy is readonly and already set
in the constructor via resolveSessionEndpointPolicy, so remove the fallback and
read endpointPolicy directly; update isRawCrossProviderFallbackEnabled to
reference this.endpointPolicy only and keep the boolean expression using
this.endpointPolicy.allowRawCrossProviderFallback and
this.rawCrossProviderFallbackEnabled to simplify the code and avoid implying
endpointPolicy can be undefined.

In `@src/lib/ledger-backfill/trigger.sql`:
- Around line 172-176: 在触发器中针对非对话端点(使用
LOWER(REGEXP_REPLACE(COALESCE(NEW.endpoint, ''), '/+$', '')) 与
'/v1/messages/count_tokens'、'/v1/responses/compact' 比对)直接 DELETE FROM
usage_ledger WHERE request_id = NEW.id 并 RETURN NEW 的分支需要补充注释,说明之所以 DELETE
而不是仅跳过 UPSERT 是为了清理可能在历史写入/旧对话端点写入时已存在的 ledger 行,避免遗留无效记录;请在该分支上方添加一行简短注释,明确引用
NEW、usage_ledger 和归一化规则以便后续维护者理解语义和原因。

In `@src/repository/_shared/ledger-conditions.ts`:
- Around line 5-20: The SQL endpoint normalization in
NON_BILLING_LEDGER_ENDPOINT_CONDITION differs from the JS isNonBillingEndpoint:
it doesn't trim whitespace first, so align them by applying a trim step to
${usageLedger.endpoint} before lowercasing and removing trailing slashes; update
the expression used in NON_BILLING_LEDGER_ENDPOINT_CONDITION (which is also
referenced by LEDGER_BILLING_CONDITION) to perform whitespace trimming then
REGEXP_REPLACE('/+$', '') and LOWER, and keep the comparison against the joined
NON_BILLING_ENDPOINTS unchanged.

In `@src/repository/_shared/usage-log-filters.ts`:
- Line 17: DEFAULT_HIDDEN_USAGE_LOG_ENDPOINTS must be normalized to match the
SQL-side normalization performed by
buildNormalizedEndpointSql/buildDefaultHiddenUsageLogEndpointCondition; update
DEFAULT_HIDDEN_USAGE_LOG_ENDPOINTS (constructed from NON_BILLING_ENDPOINTS) to
map each entry to a normalized form (apply LOWER and strip trailing slashes,
e.g., value.toLowerCase().replace(/\/+$/, '')) so the NOT IN comparison is
correct, and consider freezing the array or adding a unit test to enforce that
NON_BILLING_ENDPOINTS entries remain normalized if you choose not to normalize
at construction time; update references to DEFAULT_HIDDEN_USAGE_LOG_ENDPOINTS
accordingly.

In `@tests/unit/proxy/non-chat-endpoint-fallback.test.ts`:
- Around line 161-226: Tests build a ProxySession by
Object.create(ProxySession.prototype) which relies on the current internal field
layout and will break silently if ProxySession later adds required private
state; replace or supplement createRawSession with a dedicated factory (e.g.,
createFakeProxySession(overrides)) that centrally initializes all internal
fields and any collections/maps/sets that ProxySession constructor normally
creates, merge caller overrides, and ensure methods like setProvider,
addProviderToChain, getProviderChain, isHeaderModified, etc. operate against
those initialized defaults so future private-state additions won't silently
break tests.
- Around line 289-406: The four tests duplicate identical session setup
(V1_ENDPOINT_PATHS.RESPONSES_COMPACT, createRawSession,
createProvider(...,{providerType:"codex"}), session.originalFormat="response",
session.setRawCrossProviderFallbackEnabled(false), session.request.message {
model: "gpt-5", ... }, session.setProvider(providerA}); refactor by extracting a
helper (e.g., buildDisabledRawCompactSession()) that performs this common
assembly and returns the session (and provider if needed), then replace the
repeated blocks in the tests with calls to that helper and keep each test only
showing its differing parts (mocked categorizeErrorAsync/doForward behavior and
expected assertions).

In `@tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts`:
- Around line 171-220: The test helper createSession currently fabricates a
ProxySession using Object.create + Object.assign and manually sets
endpointPolicy and getEndpointPolicy, which can drift from the real ProxySession
behavior (see getEndpointPolicy, endpointPolicy,
isRawCrossProviderFallbackEnabled); replace this by adding a test factory on the
real class (e.g. static ProxySession.forTest(options) that constructs a real
ProxySession instance with sensible defaults and accepts overrides) and update
createSession to call ProxySession.forTest(...) instead of using Object.create,
so private/readonly fields and any lazy/getter logic in ProxySession remain
accurate in tests.
🪄 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: 8ae90dda-e6f1-4fa9-8437-cc2b72a4adb9

📥 Commits

Reviewing files that changed from the base of the PR and between 312e0a7 and 3146acf.

📒 Files selected for processing (55)
  • drizzle/0096_equal_selene.sql
  • drizzle/meta/0096_snapshot.json
  • drizzle/meta/_journal.json
  • messages/en/settings/config.json
  • messages/ja/settings/config.json
  • messages/ru/settings/config.json
  • messages/zh-CN/settings/config.json
  • messages/zh-TW/settings/config.json
  • src/actions/my-usage.ts
  • src/actions/system-config.ts
  • src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
  • src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
  • src/app/[locale]/settings/config/_components/system-settings-form.tsx
  • src/app/[locale]/settings/config/page.tsx
  • src/app/v1/_lib/proxy-handler.ts
  • src/app/v1/_lib/proxy/endpoint-policy.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/guard-pipeline.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/app/v1/_lib/proxy/session-guard.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/drizzle/schema.ts
  • src/lib/config/system-settings-cache.ts
  • src/lib/ledger-backfill/service.ts
  • src/lib/ledger-backfill/trigger.sql
  • src/lib/session-manager.ts
  • src/lib/utils/performance-formatter.ts
  • src/lib/validation/schemas.ts
  • src/repository/_shared/ledger-conditions.ts
  • src/repository/_shared/transformers.ts
  • src/repository/_shared/usage-log-filters.ts
  • src/repository/system-config.ts
  • src/repository/usage-logs.ts
  • src/types/system-config.ts
  • tests/configs/integration.config.ts
  • tests/integration/non-chat-endpoint-fallback-observability.test.ts
  • tests/unit/actions/system-config-non-chat-retry-setting.test.ts
  • tests/unit/actions/usage-logs-hidden-non-chat-endpoints.test.ts
  • tests/unit/proxy/endpoint-policy-parity.test.ts
  • tests/unit/proxy/endpoint-policy.test.ts
  • tests/unit/proxy/guard-pipeline-warmup.test.ts
  • tests/unit/proxy/non-chat-endpoint-fallback.test.ts
  • tests/unit/proxy/non-chat-endpoint-non-billing.test.ts
  • tests/unit/proxy/non-chat-endpoint-policy.test.ts
  • tests/unit/proxy/non-chat-endpoint-session-context.test.ts
  • tests/unit/proxy/provider-selector-model-mismatch-binding.test.ts
  • tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts
  • tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts
  • tests/unit/proxy/proxy-forwarder-retry-limit.test.ts
  • tests/unit/proxy/response-handler-lease-decrement.test.ts
  • tests/unit/proxy/session-guard-warmup-intercept.test.ts
  • tests/unit/repository/key.test.ts
  • tests/unit/repository/usage-logs-min-retry-count-filter.test.ts
  • tests/unit/settings/system-settings-form-non-chat-fallback.test.tsx

Comment thread drizzle/0096_equal_selene.sql Outdated
Comment thread src/app/v1/_lib/proxy/response-handler.ts Outdated
Comment thread src/lib/config/system-settings-cache.ts Outdated
Comment thread src/lib/session-manager.ts
Comment thread src/repository/_shared/usage-log-filters.ts Outdated
Comment thread src/repository/system-config.ts
Comment thread src/repository/usage-logs.ts Outdated
Comment thread tests/unit/repository/usage-logs-min-retry-count-filter.test.ts
Comment thread src/app/v1/_lib/proxy-handler.ts Outdated
Comment thread src/repository/_shared/usage-log-filters.ts Outdated
Comment thread src/actions/my-usage.ts
Comment thread src/app/v1/_lib/proxy-handler.ts
@ding113
ding113 force-pushed the fix/non-chat-endpoint-fallback-20260423T113809Z branch from 3b5621e to 9f8b5f4 Compare April 23, 2026 19:46
@github-actions github-actions Bot mentioned this pull request Apr 23, 2026
3 tasks

@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: 9f8b5f433a

ℹ️ 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 thread drizzle/0098_equal_selene.sql
Comment thread src/lib/session-manager.ts
Comment thread src/repository/usage-logs.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
src/lib/ledger-backfill/service.ts (1)

42-107: ⚠️ Potential issue | 🟠 Major

回填里的 is_success 口径还没和新触发器对齐。

Line 74 这里仍然只看 error_message,但 0098 里的新 fn_upsert_usage_ledger() 已经改成同时要求 status_code IS NULL OR < 400。这样一旦执行 backfill,usage_ledger.is_success 会对同类记录出现“实时写入=false、回填=true”的分裂结果,统计会不稳定。这里也要同步成和触发器一致的判定。

建议修复
-            (mr.error_message IS NULL OR mr.error_message = '') AS is_success,
+            (
+              (mr.error_message IS NULL OR mr.error_message = '')
+              AND (mr.status_code IS NULL OR mr.status_code < 400)
+            ) AS is_success,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/ledger-backfill/service.ts` around lines 42 - 107, The backfill
SELECT sets is_success using only mr.error_message but must match the new
trigger logic in fn_upsert_usage_ledger; update the is_success expression in the
batch SELECT (the column alias is_success in the query against message_request
mr used to populate usage_ledger) to require both no error_message and a
successy status_code (i.e., status_code IS NULL OR status_code < 400) so the
backfill and fn_upsert_usage_ledger produce identical is_success values and
avoid split results between real-time writes and backfill.
♻️ Duplicate comments (1)
src/app/v1/_lib/proxy/response-handler.ts (1)

2277-2291: ⚠️ Potential issue | 🟠 Major

流式路径里仍然会把非计费端点写成 context1mApplied=true

Line 2290 还在 billableUsageForCost 计算之前调用 maybeSetCodexContext1m。这样一来,/v1/messages/count_tokens/v1/responses/compact 一旦走到 SSE 结算路径,仍然可能把当前 session 标成 1M context,后续真实计费请求就会继承这个脏状态。

建议修改
         if (usageForCost) {
           usageForCost = normalizeUsageWithSwap(
             usageForCost,
             session,
             provider.swapCacheTtlBilling
           );
         }
-
-        maybeSetCodexContext1m(session, provider, usageForCost?.input_tokens);
+        const billableUsageForCost =
+          usageForCost && !isNonBillingUsageEndpoint(session) ? usageForCost : null;
+
+        if (billableUsageForCost) {
+          maybeSetCodexContext1m(session, provider, billableUsageForCost.input_tokens);
+        }
 
         // Codex: Extract prompt_cache_key from SSE events and update session binding
@@
-        const billableUsageForCost =
-          usageForCost && !isNonBillingUsageEndpoint(session) ? usageForCost : null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/response-handler.ts` around lines 2277 - 2291, The
stream/SSE path sets context1mApplied too early—maybeSetCodexContext1m is called
before computing billable usage, causing non-billing endpoints
(isNonBillingUsageEndpoint) to be marked as 1M and pollute later billing; fix by
moving the maybeSetCodexContext1m(...) call to after billable usage is computed
(after usageForCost / billableUsageForCost normalization) and only invoke it
when the request is confirmed billable (i.e., ensure
isNonBillingUsageEndpoint(session) is false and priorityServiceTierApplied logic
remains correct); adjust calls around normalizeUsageWithSwap and
ensureCodexServiceTierResultSpecialSetting to ensure context1mApplied is only
set for true billable paths.
🧹 Nitpick comments (8)
src/app/v1/_lib/proxy/session-guard.ts (2)

78-82: 冗余回读:allowRawSessionContext 可直接使用局部变量

第 82 行 session.isRawCrossProviderFallbackEnabled() 只是回读刚在 81 行 setter 写入的同一个值,没有任何旁路/异步观察者会在 setter 和 getter 之间改写它。直接复用 rawFallbackEnabled 可以减少一次属性访问、让阅读者少做一次心算,也能消除“setter 与 getter 语义是否真正对称”这类潜在疑问。

♻️ 建议化简
       const rawFallbackEnabled =
         (systemSettings.allowNonConversationEndpointProviderFallback ?? true) &&
         session.getEndpointPolicy().allowRawCrossProviderFallback;
       session.setRawCrossProviderFallbackEnabled(rawFallbackEnabled);
-      const allowRawSessionContext = session.isRawCrossProviderFallbackEnabled();
+      const allowRawSessionContext = rawFallbackEnabled;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/session-guard.ts` around lines 78 - 82, The code
redundantly reads back the value just set; replace the call to
session.isRawCrossProviderFallbackEnabled() and the local allowRawSessionContext
with reuse of the computed rawFallbackEnabled: after computing
rawFallbackEnabled and calling
session.setRawCrossProviderFallbackEnabled(rawFallbackEnabled), assign
allowRawSessionContext = rawFallbackEnabled (or remove allowRawSessionContext
and use rawFallbackEnabled directly) so you avoid the extra getter call to
session.isRawCrossProviderFallbackEnabled().

78-82: 跨 provider fallback 与 session 上下文注入的耦合值得在注释中点明

现在 allowRawSessionContext === rawFallbackEnabled:只要允许 raw 跨供应商 fallback,就同时禁用 Codex session id 补全与 Claude metadata.user_id 注入。这个耦合在功能上合理(raw 透传不应被中间件改写请求体),但两个语义其实是独立的:

  • "可跨供应商回退" = 决策链能在失败时切到别的 provider;
  • "请求体保持原样(raw)" = 不能对用户负载做任何注入/补全。

一旦未来想要"允许 fallback 但仍做 user_id 注入"或反之,当前的命名会让读者误以为两者必然一致。建议:

  1. 在此块上方加一段注释,解释"两者当前语义绑定"的原因(即 count_tokens/compact 这两个端点的 raw 透传契约);
  2. 或者把 allowRawSessionContext 改名为更贴近行为的 skipSessionContextMutation,明确它控制的是请求体改写而不是 fallback 决策。

Also applies to: 103-103, 147-153

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

In `@src/app/v1/_lib/proxy/session-guard.ts` around lines 78 - 82, The current
code binds the "allow raw cross-provider fallback" decision (rawFallbackEnabled)
to session context mutation by deriving allowRawSessionContext from
session.isRawCrossProviderFallbackEnabled(), which conflates fallback decision
with whether request bodies may be mutated; either add a clear comment above
this block explaining why these two behaviors are intentionally coupled
(reference the count_tokens/compact endpoints' raw-pass-through contract) or
rename allowRawSessionContext to a behavior-focused name such as
skipSessionContextMutation and update all uses
(session.setRawCrossProviderFallbackEnabled,
session.isRawCrossProviderFallbackEnabled, and any logic that reads
allowRawSessionContext) so it explicitly controls request-body mutation while
leaving the fallback decision variable (rawFallbackEnabled) separate; apply the
same change/comment at the other similar locations that mirror this pattern (the
other occurrences around the same blocks).
tests/unit/proxy/response-handler-lease-decrement.test.ts (1)

421-447: LGTM,建议补一条 /v1/messages/count_tokens 对称用例

此用例验证了 /v1/responses/compact/ 变体(含尾斜杠,能覆盖归一化路径)下不触发计费相关的 Redis 追踪 / lease decrement,同时仍落库 token 统计,覆盖点合理。

为防止未来回归只保护其中一个端点,建议用 it.each/v1/messages/count_tokens 也纳入同一套断言(尤其大小写、尾斜杠这类归一化差异),确保两个 NON_BILLING_ENDPOINTS 对称生效。

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

In `@tests/unit/proxy/response-handler-lease-decrement.test.ts` around lines 421 -
447, Add a symmetric test case covering the /v1/messages/count_tokens endpoint
variants by converting the existing test into a parameterized it.each that
iterates over the non-billing paths (including variants with/without trailing
slash and different casing) and runs the same assertions: call
ProxyResponseHandler.dispatch with the session.pathname set to each path, ensure
RateLimitService.trackCost, trackUserDailyCost, and decrementLeaseBudget are not
called, and verify updateMessageRequestDetails is called with the expected token
fields; reference ProxyResponseHandler.dispatch,
RateLimitService.trackCost/trackUserDailyCost/decrementLeaseBudget, and
updateMessageRequestDetails to locate where to add the parameterized test.
src/app/v1/_lib/proxy-handler.ts (1)

28-33: 日志级别建议与可控硅:加载失败是否应为 error

系统设置加载失败会直接导致高并发、raw fallback 这两个开关被降级到 false,用户看到的是“功能悄然变差”。建议改为 logger.error 以便告警系统能捕获;同时把 settingsError 放到结构化字段里,避免被 logger 序列化策略丢失。此外消息里"fallback highConcurrency=false and rawCrossProviderFallback=false"属内部日志,保留英文无问题。

♻️ 建议调整
-      logger.warn(
-        "[ProxyHandler] Failed to load proxy system settings, fallback highConcurrency=false and rawCrossProviderFallback=false",
-        {
-          error: settingsError,
-        }
-      );
+      logger.error(
+        "[ProxyHandler] Failed to load proxy system settings; degrading highConcurrency=false, rawCrossProviderFallback=false",
+        { err: settingsError }
+      );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy-handler.ts` around lines 28 - 33, Change the log level
from warn to error in the ProxyHandler settings load failure so alerts can
trigger; in the logger call that currently references settingsError (inside
src/app/v1/_lib/proxy-handler.ts), move the error into a dedicated structured
field (e.g., { error: settingsError } or { settingsError }) as the second
argument so the logger preserves the exception details rather than embedding it
in the message, and keep the existing English message text about fallback
highConcurrency/rawCrossProviderFallback unchanged.
tests/unit/actions/usage-logs-hidden-non-chat-endpoints.test.ts (1)

51-64: 通过源码字符串扫描函数边界相对脆弱

这里通过 indexOf("export async function getUsedEndpoints") 与下一个函数名来切片源码,一旦 src/repository/usage-logs.ts 中这些函数被改名、拆分或调整顺序,测试会静默变空或跨段误匹配(例如空 getUsedEndpointsSegment 会让后续 not.toContain(...) 恒真)。建议至少加上对切片非空的断言,或用正则捕获函数体以避免假绿。

♻️ 可选加固
+    expect(getUsedEndpointsSegment.length).toBeGreaterThan(0);
+    expect(getDistinctEndpointsSegment.length).toBeGreaterThan(0);
     expect(getUsedEndpointsSegment).not.toContain("buildDefaultHiddenUsageLogEndpointCondition");
     expect(getDistinctEndpointsSegment).not.toContain(
       "buildDefaultHiddenUsageLogEndpointCondition"
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/actions/usage-logs-hidden-non-chat-endpoints.test.ts` around lines
51 - 64, The test uses brittle string slicing via indexOf("export async function
getUsedEndpoints") and indexOf("export async function
getDistinctEndpointsForKey") which can produce empty or wrong segments if
functions are renamed/reordered; update the test to either (A) assert that
getUsedEndpointsSegment and getDistinctEndpointsSegment are non-empty before
asserting not.toContain("buildDefaultHiddenUsageLogEndpointCondition") or (B)
replace the slicing with a regex that reliably captures the full function bodies
(e.g. match /export async function getUsedEndpoints[\s\S]*?{[\s\S]*?^}/m or
similar) so you directly search inside the captured bodies for
buildDefaultHiddenUsageLogEndpointCondition; reference getUsedEndpointsSegment,
getDistinctEndpointsSegment, getUsedEndpoints, getDistinctEndpointsForKey, and
buildDefaultHiddenUsageLogEndpointCondition when making the change.
src/app/v1/_lib/proxy/forwarder.ts (2)

1746-1761: provider 错误详情的“有无 upstreamBody/upstreamParsed”分支在多处重复

Line 526-540 / 584-598(两个 builder 内部)以及 Line 1747-1761、1886-1900、1956-1970(三处内联)都使用同一结构 rawCrossProviderFallbackEnabled ? minimal : withUpstream 构造 errorDetails.provider。五处重复容易在未来调整字段时漏改,进而导致 src/actions/my-usage.ts 的 scrubbing 语义与 forwarder 出口脱节。

建议抽取一个小工具函数统一生成 provider 字段(例如 buildProviderErrorDetails(provider, statusCode, statusText, rawUpstream, rawCrossProviderFallbackEnabled)),在 builder 和内联分支里共用。

Also applies to: 1885-1900, 1955-1970

🤖 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 1746 - 1761, The provider
error detail construction is duplicated across multiple builders and inline
branches; add a shared helper (e.g. buildProviderErrorDetails(provider,
statusCode, statusText, upstreamError, rawCrossProviderFallbackEnabled)) that
returns either the minimal shape or the shape with upstreamBody/upstreamParsed
based on rawCrossProviderFallbackEnabled, then replace the repeated ternary
constructions that assign errorDetails.provider in the builders and inline
branches with calls to this helper (use currentProvider, 404, proxyError.message
and proxyError.upstreamError as arguments where those values are used now).

939-977: endpointPolicy 在外层和 while 循环内重复解析,且 shouldSkipRawRetryAndProviderSwitch 未被后续代码复用

Line 942 已在函数作用域解析了 endpointPolicy 并据此计算 shouldSkipRawRetryAndProviderSwitch,但 Line 974 又在 while 循环内用 const endpointPolicy = ... 重新解析同一条策略(遮蔽外层变量),并在 Line 1914 再次以内联方式重写了 !endpointPolicy.allowRetry && !rawCrossProviderFallbackEnabled(与 Line 943-944 定义完全一致)。外层 shouldSkipRawRetryAndProviderSwitch 在 Line 1649/1766/1834 被复用,但 Line 1914 未使用,容易在后续策略演进时让两处条件出现漂移。

建议删除内层 endpointPolicy 的重复声明,并让 Line 1914 直接复用 shouldSkipRawRetryAndProviderSwitch,保持单一真相源。

♻️ 建议的重构
-      const endpointPolicy = ProxyForwarder.getEndpointPolicy(session);
-      const shouldAccountCircuitBreaker = endpointPolicy.allowCircuitBreakerAccounting;
+      const shouldAccountCircuitBreaker = endpointPolicy.allowCircuitBreakerAccounting;
       const shouldEnforceStrictEndpointPool =
         !isMcpRequest && isStrictEndpointPoolPolicy(endpointPolicy) && providerVendorId > 0;

并在 Line 1914 处:

-            if (!endpointPolicy.allowRetry && !rawCrossProviderFallbackEnabled) {
+            if (shouldSkipRawRetryAndProviderSwitch) {
🤖 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 939 - 977, The code
redeclares endpointPolicy inside the while loop and duplicates the retry-skip
logic, causing shadowing and potential drift; remove the inner declaration of
endpointPolicy (the call to ProxyForwarder.getEndpointPolicy(session) inside the
loop) so the loop reuses the outer endpointPolicy, and replace the inlined
condition there with the previously computed shouldSkipRawRetryAndProviderSwitch
variable to ensure a single source of truth for retry/provider-switch decisions.
tests/unit/proxy/non-chat-endpoint-fallback.test.ts (1)

161-226: createRawSession 通过原型克隆构造会话,需留意未来 ProxySession 若改用私有字段会破坏测试

目前通过 Object.create(ProxySession.prototype) + Object.assign 来伪造会话实例,仅依赖 setRawCrossProviderFallbackEnabled / isRawCrossProviderFallbackEnabled / getEndpointPolicy 读取/写入普通属性。如果后续 ProxySession 重构为 #private 字段或在构造器内做必要初始化,这个 fixture 会在运行原型方法时抛出“Cannot read private field”。可以在 createRawSession 顶部加一条 JSDoc 注释提示这一耦合点,或未来考虑提供 ProxySession.forTest(...) 静态工厂统一测试构造。非阻塞项,当前用法可行。

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

In `@tests/unit/proxy/non-chat-endpoint-fallback.test.ts` around lines 161 - 226,
The test helper createRawSession constructs a fake ProxySession via
Object.create(ProxySession.prototype) and Object.assign, which will break if
ProxySession later adopts private (#) fields or requires ctor initialization; to
fix, add a JSDoc note at the top of createRawSession calling out this coupling
to ProxySession's internal implementation and the reliance on prototype methods
setRawCrossProviderFallbackEnabled/isRawCrossProviderFallbackEnabled/getEndpointPolicy,
and consider replacing this pattern in future with a dedicated test factory like
ProxySession.forTest(...) that performs proper construction and exposes only the
needed hooks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@drizzle/0098_equal_selene.sql`:
- Around line 50-99: The INSERT ... ON CONFLICT DO UPDATE for the usage_ledger
trigger is missing the actual_response_model column; update the VALUES list to
include NEW.actual_response_model (or v_... if computed) and add
actual_response_model = EXCLUDED.actual_response_model in the DO UPDATE SET
clause so both insertion and conflict-update paths write the
actual_response_model field for usage_ledger (look for the INSERT INTO
usage_ledger (...) VALUES (...) and the DO UPDATE SET ... blocks referencing NEW
and EXCLUDED).

In `@src/drizzle/schema.ts`:
- Around line 794-801: The migration (drizzle/0098_equal_selene.sql) includes
irreversible destructive deletes targeting usage_ledger (the DELETE FROM
"usage_ledger" ... IN ('/v1/messages/count_tokens','/v1/responses/compact')) and
the same DELETE is present in the trigger function, so update the PR migration
notes to explicitly mark this as a data-destructive change: state that
historical usage_ledger rows for the two endpoints will be deleted, reference
the affected field/flag allowNonConversationEndpointProviderFallback and the
usage_ledger table, require a pre-deployment backup, list expected impact window
and scope (which endpoints and approximate volume), and add a runbook step to
restore or snapshot before applying in production.

In `@src/lib/session-manager.ts`:
- Around line 638-657: getSessionProvider enforces a fail-closed check against
the Redis key "session:{sessionId}:key" but the code paths that create/renew
client-provided sessions and write provider bindings do not persist that key,
causing boundKeyId to be null and breaking provider reuse; update the session
creation/renewal and all provider-binding/TTL-refresh code paths that write
"session:{sessionId}:provider" (the same places that currently call redis.set or
refresh TTL) to also set "session:{sessionId}:key" with the same TTL and value
(use keyId as string) and prefer atomic/transactional writes (MULTI/EXEC or
pipeline) so the provider and key are written/updated together. Ensure
getSessionProvider still reads the same key format and compare stringified
keyId.

In `@tests/integration/non-chat-endpoint-fallback-observability.test.ts`:
- Around line 123-140: The test uses
originalAllowNonConversationEndpointProviderFallback as a sentinel but currently
initializes it to null; change the sentinel to undefined (i.e., let
originalAllowNonConversationEndpointProviderFallback: boolean | undefined =
undefined) and update the afterAll check to restore settings only when
originalAllowNonConversationEndpointProviderFallback !== undefined; ensure the
assignments that capture the original value (in beforeAll via
getSystemSettings()) and the restore call (using updateSystemSettings()) keep
the same symbol names and that cleanupTestRows() calls remain unchanged.

---

Outside diff comments:
In `@src/lib/ledger-backfill/service.ts`:
- Around line 42-107: The backfill SELECT sets is_success using only
mr.error_message but must match the new trigger logic in fn_upsert_usage_ledger;
update the is_success expression in the batch SELECT (the column alias
is_success in the query against message_request mr used to populate
usage_ledger) to require both no error_message and a successy status_code (i.e.,
status_code IS NULL OR status_code < 400) so the backfill and
fn_upsert_usage_ledger produce identical is_success values and avoid split
results between real-time writes and backfill.

---

Duplicate comments:
In `@src/app/v1/_lib/proxy/response-handler.ts`:
- Around line 2277-2291: The stream/SSE path sets context1mApplied too
early—maybeSetCodexContext1m is called before computing billable usage, causing
non-billing endpoints (isNonBillingUsageEndpoint) to be marked as 1M and pollute
later billing; fix by moving the maybeSetCodexContext1m(...) call to after
billable usage is computed (after usageForCost / billableUsageForCost
normalization) and only invoke it when the request is confirmed billable (i.e.,
ensure isNonBillingUsageEndpoint(session) is false and
priorityServiceTierApplied logic remains correct); adjust calls around
normalizeUsageWithSwap and ensureCodexServiceTierResultSpecialSetting to ensure
context1mApplied is only set for true billable paths.

---

Nitpick comments:
In `@src/app/v1/_lib/proxy-handler.ts`:
- Around line 28-33: Change the log level from warn to error in the ProxyHandler
settings load failure so alerts can trigger; in the logger call that currently
references settingsError (inside src/app/v1/_lib/proxy-handler.ts), move the
error into a dedicated structured field (e.g., { error: settingsError } or {
settingsError }) as the second argument so the logger preserves the exception
details rather than embedding it in the message, and keep the existing English
message text about fallback highConcurrency/rawCrossProviderFallback unchanged.

In `@src/app/v1/_lib/proxy/forwarder.ts`:
- Around line 1746-1761: The provider error detail construction is duplicated
across multiple builders and inline branches; add a shared helper (e.g.
buildProviderErrorDetails(provider, statusCode, statusText, upstreamError,
rawCrossProviderFallbackEnabled)) that returns either the minimal shape or the
shape with upstreamBody/upstreamParsed based on rawCrossProviderFallbackEnabled,
then replace the repeated ternary constructions that assign
errorDetails.provider in the builders and inline branches with calls to this
helper (use currentProvider, 404, proxyError.message and
proxyError.upstreamError as arguments where those values are used now).
- Around line 939-977: The code redeclares endpointPolicy inside the while loop
and duplicates the retry-skip logic, causing shadowing and potential drift;
remove the inner declaration of endpointPolicy (the call to
ProxyForwarder.getEndpointPolicy(session) inside the loop) so the loop reuses
the outer endpointPolicy, and replace the inlined condition there with the
previously computed shouldSkipRawRetryAndProviderSwitch variable to ensure a
single source of truth for retry/provider-switch decisions.

In `@src/app/v1/_lib/proxy/session-guard.ts`:
- Around line 78-82: The code redundantly reads back the value just set; replace
the call to session.isRawCrossProviderFallbackEnabled() and the local
allowRawSessionContext with reuse of the computed rawFallbackEnabled: after
computing rawFallbackEnabled and calling
session.setRawCrossProviderFallbackEnabled(rawFallbackEnabled), assign
allowRawSessionContext = rawFallbackEnabled (or remove allowRawSessionContext
and use rawFallbackEnabled directly) so you avoid the extra getter call to
session.isRawCrossProviderFallbackEnabled().
- Around line 78-82: The current code binds the "allow raw cross-provider
fallback" decision (rawFallbackEnabled) to session context mutation by deriving
allowRawSessionContext from session.isRawCrossProviderFallbackEnabled(), which
conflates fallback decision with whether request bodies may be mutated; either
add a clear comment above this block explaining why these two behaviors are
intentionally coupled (reference the count_tokens/compact endpoints'
raw-pass-through contract) or rename allowRawSessionContext to a
behavior-focused name such as skipSessionContextMutation and update all uses
(session.setRawCrossProviderFallbackEnabled,
session.isRawCrossProviderFallbackEnabled, and any logic that reads
allowRawSessionContext) so it explicitly controls request-body mutation while
leaving the fallback decision variable (rawFallbackEnabled) separate; apply the
same change/comment at the other similar locations that mirror this pattern (the
other occurrences around the same blocks).

In `@tests/unit/actions/usage-logs-hidden-non-chat-endpoints.test.ts`:
- Around line 51-64: The test uses brittle string slicing via indexOf("export
async function getUsedEndpoints") and indexOf("export async function
getDistinctEndpointsForKey") which can produce empty or wrong segments if
functions are renamed/reordered; update the test to either (A) assert that
getUsedEndpointsSegment and getDistinctEndpointsSegment are non-empty before
asserting not.toContain("buildDefaultHiddenUsageLogEndpointCondition") or (B)
replace the slicing with a regex that reliably captures the full function bodies
(e.g. match /export async function getUsedEndpoints[\s\S]*?{[\s\S]*?^}/m or
similar) so you directly search inside the captured bodies for
buildDefaultHiddenUsageLogEndpointCondition; reference getUsedEndpointsSegment,
getDistinctEndpointsSegment, getUsedEndpoints, getDistinctEndpointsForKey, and
buildDefaultHiddenUsageLogEndpointCondition when making the change.

In `@tests/unit/proxy/non-chat-endpoint-fallback.test.ts`:
- Around line 161-226: The test helper createRawSession constructs a fake
ProxySession via Object.create(ProxySession.prototype) and Object.assign, which
will break if ProxySession later adopts private (#) fields or requires ctor
initialization; to fix, add a JSDoc note at the top of createRawSession calling
out this coupling to ProxySession's internal implementation and the reliance on
prototype methods
setRawCrossProviderFallbackEnabled/isRawCrossProviderFallbackEnabled/getEndpointPolicy,
and consider replacing this pattern in future with a dedicated test factory like
ProxySession.forTest(...) that performs proper construction and exposes only the
needed hooks.

In `@tests/unit/proxy/response-handler-lease-decrement.test.ts`:
- Around line 421-447: Add a symmetric test case covering the
/v1/messages/count_tokens endpoint variants by converting the existing test into
a parameterized it.each that iterates over the non-billing paths (including
variants with/without trailing slash and different casing) and runs the same
assertions: call ProxyResponseHandler.dispatch with the session.pathname set to
each path, ensure RateLimitService.trackCost, trackUserDailyCost, and
decrementLeaseBudget are not called, and verify updateMessageRequestDetails is
called with the expected token fields; reference ProxyResponseHandler.dispatch,
RateLimitService.trackCost/trackUserDailyCost/decrementLeaseBudget, and
updateMessageRequestDetails to locate where to add the parameterized test.
🪄 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: 18a37025-2436-4d0b-a393-9733130c993f

📥 Commits

Reviewing files that changed from the base of the PR and between 3b5621e and 9f8b5f4.

📒 Files selected for processing (59)
  • drizzle/0098_equal_selene.sql
  • drizzle/meta/0098_snapshot.json
  • drizzle/meta/_journal.json
  • messages/en/settings/config.json
  • messages/ja/settings/config.json
  • messages/ru/settings/config.json
  • messages/zh-CN/settings/config.json
  • messages/zh-TW/settings/config.json
  • src/actions/my-usage.ts
  • src/actions/system-config.ts
  • src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
  • src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
  • src/app/[locale]/settings/config/_components/system-settings-form.tsx
  • src/app/[locale]/settings/config/page.tsx
  • src/app/v1/_lib/proxy-handler.ts
  • src/app/v1/_lib/proxy/endpoint-policy.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/guard-pipeline.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/app/v1/_lib/proxy/session-guard.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/drizzle/schema.ts
  • src/lib/config/system-settings-cache.ts
  • src/lib/ledger-backfill/service.ts
  • src/lib/ledger-backfill/trigger.sql
  • src/lib/session-manager.ts
  • src/lib/utils/performance-formatter.ts
  • src/lib/validation/schemas.ts
  • src/repository/_shared/ledger-conditions.ts
  • src/repository/_shared/transformers.ts
  • src/repository/_shared/usage-log-filters.ts
  • src/repository/system-config.ts
  • src/repository/usage-logs.ts
  • src/types/message.ts
  • src/types/system-config.ts
  • tests/configs/integration.config.ts
  • tests/integration/non-chat-endpoint-fallback-observability.test.ts
  • tests/unit/actions/my-usage-readonly-provider-chain.test.ts
  • tests/unit/actions/system-config-non-chat-retry-setting.test.ts
  • tests/unit/actions/usage-logs-hidden-non-chat-endpoints.test.ts
  • tests/unit/proxy/endpoint-policy-parity.test.ts
  • tests/unit/proxy/endpoint-policy.test.ts
  • tests/unit/proxy/guard-pipeline-warmup.test.ts
  • tests/unit/proxy/hedge-error-pipeline.test.ts
  • tests/unit/proxy/non-chat-endpoint-fallback.test.ts
  • tests/unit/proxy/non-chat-endpoint-non-billing.test.ts
  • tests/unit/proxy/non-chat-endpoint-policy.test.ts
  • tests/unit/proxy/non-chat-endpoint-session-context.test.ts
  • tests/unit/proxy/provider-selector-model-mismatch-binding.test.ts
  • tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts
  • tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts
  • tests/unit/proxy/proxy-forwarder-retry-limit.test.ts
  • tests/unit/proxy/proxy-handler-session-id-error.test.ts
  • tests/unit/proxy/response-handler-lease-decrement.test.ts
  • tests/unit/proxy/session-guard-warmup-intercept.test.ts
  • tests/unit/repository/key.test.ts
  • tests/unit/repository/usage-logs-min-retry-count-filter.test.ts
  • tests/unit/settings/system-settings-form-non-chat-fallback.test.tsx
✅ Files skipped from review due to trivial changes (10)
  • src/app/[locale]/settings/config/page.tsx
  • src/types/message.ts
  • tests/unit/proxy/endpoint-policy.test.ts
  • messages/zh-CN/settings/config.json
  • tests/unit/proxy/session-guard-warmup-intercept.test.ts
  • src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
  • tests/configs/integration.config.ts
  • tests/unit/proxy/non-chat-endpoint-policy.test.ts
  • messages/zh-TW/settings/config.json
  • src/repository/usage-logs.ts
🚧 Files skipped from review as they are similar to previous changes (22)
  • src/repository/_shared/transformers.ts
  • src/lib/validation/schemas.ts
  • messages/ja/settings/config.json
  • tests/unit/repository/key.test.ts
  • messages/en/settings/config.json
  • tests/unit/proxy/endpoint-policy-parity.test.ts
  • tests/unit/repository/usage-logs-min-retry-count-filter.test.ts
  • src/lib/ledger-backfill/trigger.sql
  • src/app/[locale]/settings/config/_components/system-settings-form.tsx
  • src/lib/config/system-settings-cache.ts
  • tests/unit/proxy/provider-selector-model-mismatch-binding.test.ts
  • src/lib/utils/performance-formatter.ts
  • src/actions/my-usage.ts
  • tests/unit/actions/system-config-non-chat-retry-setting.test.ts
  • tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts
  • drizzle/meta/_journal.json
  • tests/unit/proxy/guard-pipeline-warmup.test.ts
  • tests/unit/proxy/non-chat-endpoint-non-billing.test.ts
  • src/repository/system-config.ts
  • tests/unit/proxy/proxy-forwarder-retry-limit.test.ts
  • messages/ru/settings/config.json
  • tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts

Comment thread drizzle/0098_equal_selene.sql
Comment thread src/drizzle/schema.ts
Comment thread src/lib/session-manager.ts
Comment thread tests/integration/non-chat-endpoint-fallback-observability.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/integration/my-usage-imported-ledger.test.ts`:
- Around line 61-79: The test can still flake around server-local midnight
because getStableRecentUtcTimestamp() only zeroes seconds/millis and
insertLedgerOnlyRow writes a UTC createdAt that may fall on a different
server-local date than the "today" used by getServerDateString() and
getMyStatsSummary(); change the test to avoid the midnight edge by either (A)
when building the test timestamp (now) subtract 5–10 minutes from
getStableRecentUtcTimestamp() so createdAt is safely away from midnight, or (B)
derive the server-local date via getServerDateString(...) and then set createdAt
to a deterministic mid-day UTC instant for that date (e.g., 12:00 server time
converted to UTC) before calling insertLedgerOnlyRow, ensuring
getMyStatsSummary({ startDate: today, endDate: today }) consistently includes
the inserted row.
🪄 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: b9938edc-7e4f-4fbf-9191-f96fed7b4ae3

📥 Commits

Reviewing files that changed from the base of the PR and between 9f8b5f4 and b38180d.

📒 Files selected for processing (2)
  • drizzle/meta/_journal.json
  • tests/integration/my-usage-imported-ledger.test.ts
✅ Files skipped from review due to trivial changes (1)
  • drizzle/meta/_journal.json

Comment thread tests/integration/my-usage-imported-ledger.test.ts
@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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

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

ℹ️ 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 thread src/app/v1/_lib/proxy/forwarder.ts Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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

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

ℹ️ 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 thread src/app/v1/_lib/proxy/forwarder.ts

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

♻️ Duplicate comments (1)
src/app/v1/_lib/proxy/response-handler.ts (1)

2284-2293: ⚠️ Potential issue | 🟡 Minor

这个流式分支里仍会把非计费请求标成 context1mApplied

这里在做 billableUsageForCost 过滤之前就调用了 maybeSetCodexContext1m。如果 /v1/messages/count_tokens/v1/responses/compact 以后走到这条收尾路径,session 会被污染成 1M context,后续同 session 的真实计费请求就可能沿用错误状态。

建议修复
-        maybeSetCodexContext1m(session, provider, usageForCost?.input_tokens);
-
         // Codex: Extract prompt_cache_key from SSE events and update session binding
         if (provider.providerType === "codex" && session.sessionId && provider.id) {
           try {
@@
         const billableUsageForCost =
           usageForCost && !isNonBillingUsageEndpoint(session) ? usageForCost : null;
+
+        if (billableUsageForCost) {
+          maybeSetCodexContext1m(session, provider, billableUsageForCost.input_tokens);
+        }

这和前面已经补过的非计费保护是同一类问题,只是这里还漏了一个流式分支。

Also applies to: 2321-2322

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

In `@src/app/v1/_lib/proxy/response-handler.ts` around lines 2284 - 2293, The
streaming branch currently calls maybeSetCodexContext1m(session, provider,
usageForCost?.input_tokens) before the billable filtering, causing non-billable
requests (e.g., /v1/messages/count_tokens or /v1/responses/compact) to mark
session.context1mApplied; move or guard that call so it only runs for actual
billable usage: ensure you apply normalizeUsageWithSwap and then check the
billable usage filter (the same logic used for billableUsageForCost) and only
after that invoke maybeSetCodexContext1m with the verified billable usage
(references: usageForCost, normalizeUsageWithSwap, maybeSetCodexContext1m,
provider.swapCacheTtlBilling); replicate the same fix for the other streaming
path noted around the second occurrence (lines ~2321-2322).
🧹 Nitpick comments (5)
src/repository/usage-logs.ts (1)

1559-1618: 非计费端点快速路径实现符合预期;5m/1h token 不再重复累加。

totalCost 硬编码为 0totalTokens 仅累加 input+output+cacheCreation+cacheRead(第 1601-1605 行),与其他分支口径保持一致,并附了说明注释。ledger-only 模式下继续走 ledger 分支(由 migration 清理),符合设计意图。

可选:此块约 60 行,与后续 ledger 分支在 userId/keyId/providerId 条件构建上存在模式重复,后续若再增加端点类型,可考虑抽出辅助函数(如 buildMessageRequestStatsQuery)统一收拢;本 PR 维持现状也可以。

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

In `@src/repository/usage-logs.ts` around lines 1559 - 1618, Non-billing quick
path in the if (!ledgerOnly && isNonBillingEndpoint(filters.endpoint)) block
works as intended (totalCost hardcoded to "0" and totalTokens computed as
input+output+cacheCreation+cacheRead) but there is duplicated condition-building
logic with the ledger branch; either leave as-is (approved) or refactor by
extracting the shared condition/query construction into a helper (e.g.,
buildMessageRequestStatsQuery) that accepts filters/userId/keyId/providerId and
returns the baseQuery (using messageRequest, keysTable, buildUsageLogConditions)
so both branches reuse the same query-building code.
src/app/v1/_lib/proxy/forwarder.ts (3)

941-979: endpointPolicy 在外层与 while 内层被重复声明,形成无必要的遮蔽

第 942 行已在外层声明 endpointPolicy 并据此计算 shouldSkipRawRetryAndProviderSwitch;第 974 行又在 while 循环内用 const endpointPolicy 再次调用 ProxyForwarder.getEndpointPolicy(session) 并在后续分支(shouldAccountCircuitBreakershouldEnforceStrictEndpointPool、1659/1776/1844/1925、以及 bypassForwarderPreprocessing 等)复用。

由于 endpoint policy 在单次 send() 内不会随 provider 切换而变化(它来源于 session.getEndpointPolicy()requestUrl.pathname),每次循环重新解析不仅是重复调用,也让 endpointPolicy 在方法内出现两个同名变量,阅读时需要来回确认指向的是哪一个。

建议直接复用外层变量,删除循环内的重新声明:

♻️ 建议 diff
     const env = getEnvConfig();
     const envDefaultMaxAttempts = clampRetryAttempts(env.MAX_RETRY_ATTEMPTS_DEFAULT);
     const rawCrossProviderFallbackEnabled = session.isRawCrossProviderFallbackEnabled();
     const endpointPolicy = ProxyForwarder.getEndpointPolicy(session);
     const shouldSkipRawRetryAndProviderSwitch =
       !endpointPolicy.allowRetry && !rawCrossProviderFallbackEnabled;
@@
-      const endpointPolicy = ProxyForwarder.getEndpointPolicy(session);
       const shouldAccountCircuitBreaker = endpointPolicy.allowCircuitBreakerAccounting;
🤖 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 941 - 979, The code declares
endpointPolicy twice (once before the while loop and again inside it), causing
shadowing and redundant calls to ProxyForwarder.getEndpointPolicy(session);
remove the inner declaration (the const endpointPolicy inside the while loop)
and update uses of shouldAccountCircuitBreaker and
shouldEnforceStrictEndpointPool (and any downstream uses like
bypassForwarderPreprocessing) to reference the outer endpointPolicy so the loop
reuses the single endpointPolicy value and avoids duplicate computations and
shadowing.

1917-1930: 此处条件与 shouldSkipRawRetryAndProviderSwitch 完全等价,建议直接复用

!endpointPolicy.allowRetry && !rawCrossProviderFallbackEnabled 与第 943–944 行定义的 shouldSkipRawRetryAndProviderSwitch 是同一个表达式;SYSTEM_ERROR(1652)、RESOURCE_NOT_FOUND(1769)、空响应(1837)三处已统一使用变量,这里是唯一残留的字面重复,容易在后续修改语义时漏改其中一处,导致 raw 端点的透传语义在 PROVIDER_ERROR 分支与其他分支不一致。

♻️ 建议 diff
-            // Raw passthrough endpoints: no circuit breaker, no provider switch, no retry
-            if (!endpointPolicy.allowRetry && !rawCrossProviderFallbackEnabled) {
+            // Raw passthrough endpoints: no circuit breaker, no provider switch, no retry
+            if (shouldSkipRawRetryAndProviderSwitch) {
               logger.debug(
🤖 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 1917 - 1930, The condition
near the throw in ProxyForwarder (the block that logs "ProxyForwarder: raw
passthrough endpoint error, skipping circuit breaker and provider switch" and
then throws lastError) duplicates the expression !endpointPolicy.allowRetry &&
!rawCrossProviderFallbackEnabled; replace that literal with the existing
shouldSkipRawRetryAndProviderSwitch variable (defined earlier) so the
PROVIDER_ERROR branch uses the same predicate as the
SYSTEM_ERROR/RESOURCE_NOT_FOUND/empty-response branches and avoid divergent
behavior if the logic changes later.

1745-1767: provider 错误详情的条件性脱敏逻辑在多处重复

当前 rawCrossProviderFallbackEnabled ? {...精简字段} : {...含 upstreamBody/upstreamParsed} 的三元写法出现在 5 个位置:buildClientErrorChainEntry(526–540)、buildRetryFailedChainEntry(584–598)、以及 send() 内的 resource_not_found(1750–1764)、vendor_type_all_timeout(1889–1903)、retry_failed(1959–1973)。结构完全一致,仅 statusCode/statusText 字段取值不同。

一旦后续需要调整脱敏的字段集合(例如再去掉一个字段,或加入 rawBody 相关控制),需要同时修改 5 处,容易漏改,也正是本 PR 注明的“clientError scrubbing regression”这类回归最容易出现的土壤。

建议抽一个辅助函数把 {provider, statusCode, statusText, upstreamBody?, upstreamParsed?} 的拼装收敛在一处:

function buildProviderErrorDetail(
  provider: Provider,
  statusCode: number,
  statusText: string,
  upstreamError: ProxyError["upstreamError"],
  rawCrossProviderFallbackEnabled: boolean,
) {
  const base = { id: provider.id, name: provider.name, statusCode, statusText };
  return rawCrossProviderFallbackEnabled
    ? base
    : {
        ...base,
        upstreamBody: upstreamError?.body,
        upstreamParsed: upstreamError?.parsed,
      };
}

然后在 5 个调用点替换相应字面对象。这也有助于后续若要把 clientError / safeClientMessageCandidate 等字段纳入同一脱敏策略时集中处理。

Also applies to: 1885-1906, 1952-1976

🤖 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 1745 - 1767, Extract the
repeated ternary that builds the provider error object into a single helper
function (e.g., buildProviderErrorDetail) that accepts (provider, statusCode,
statusText, upstreamError, rawCrossProviderFallbackEnabled) and returns the base
{id, name, statusCode, statusText} when rawCrossProviderFallbackEnabled is true
or the base plus upstreamBody/upstreamParsed when false; then replace the inline
ternaries in buildClientErrorChainEntry, buildRetryFailedChainEntry and the
three send() cases (resource_not_found, vendor_type_all_timeout, retry_failed)
to call this helper so the scrubbing logic is centralized and consistent across
those five call sites.
src/app/v1/_lib/proxy/endpoint-policy.ts (1)

65-73: 两个新谓词缺少显式返回类型注解,且 shouldEnforceStrictEndpointPoolPolicy 当前逻辑恒为真

  1. 缺少返回类型isStrictEndpointPoolPolicyshouldEnforceStrictEndpointPoolPolicy 均未声明显式返回类型,与同文件其他导出的 isRawPassthroughEndpointPathisRawPassthroughEndpointPolicy 不一致(都显式标注了 : boolean)。建议统一补上 : boolean

  2. 逻辑问题EndpointPoolStrictness 当前只有 "inherit" | "strict" 两个取值,而 shouldEnforceStrictEndpointPoolPolicy 对两者均返回 true,因此对任何合法的 EndpointPolicy 都恒为真。虽然该函数在 forwarder.ts 的实际使用中是与其他条件(!isMcpRequest && ... && providerVendorId > 0)组合的,但单看函数定义时容易产生误解。

建议二选一:

  • 若这是为未来新增取值(例如 "disabled" / "off")预留的扩展点,请在函数上方加注释说明,避免后续开发者误认为冗余;
  • 若当前没有此计划,可考虑删除该层调用或与 isStrictEndpointPoolPolicy 合并为语义清晰的单一谓词。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/endpoint-policy.ts` around lines 65 - 73, Add explicit
boolean return annotations to isStrictEndpointPoolPolicy and
shouldEnforceStrictEndpointPoolPolicy (declare both as ": boolean"), and fix the
confusing logic: either change shouldEnforceStrictEndpointPoolPolicy to only
return true for strict (i.e., return policy.endpointPoolStrictness === "strict")
or, if "inherit" is intentionally treated as enforcing for future values, add a
clear comment above shouldEnforceStrictEndpointPoolPolicy explaining why inherit
is considered enforcing and that this is an extension point for future
EndpointPoolStrictness values; update the functions isStrictEndpointPoolPolicy
and shouldEnforceStrictEndpointPoolPolicy accordingly.
🤖 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/lib/session-manager.ts`:
- Around line 578-591: In refreshSessionTTL, don't overwrite the session:key
binding during TTL refresh — remove the
pipeline.setex(`session:${sessionId}:key`, ...) path that writes keyId and
instead only refresh TTL (use pipeline.expire for `session:${sessionId}:key`) so
you don't change ownership; if backfilling missing `session:${sessionId}:key` is
required, do it only in the verified binding or atomic create/update paths (not
in SessionManager.refreshSessionTTL) to avoid bypassing getSessionProvider's
fail-closed checks.

In `@src/repository/usage-logs.ts`:
- Around line 323-333: The endpoint matching logic is inconsistent: some
branches use strict eq(usageLedger.endpoint, filters.endpoint) while
findUsageLogsStats uses buildUsageLogEndpointMatchCondition (which lowercases
and normalizes trailing slashes); this causes mismatched results between list
and stats. Replace the eq(...) comparisons in the ledger fallback within
findUsageLogsBatch and in buildKeyLedgerConditions with
buildUsageLogEndpointMatchCondition(usageLedger.endpoint, filters.endpoint) and
keep using buildDefaultHiddenUsageLogEndpointCondition as-is (it returns null
when filters.endpoint is present), so all three places (findUsageLogsStats,
findUsageLogsBatch ledger branch, and buildKeyLedgerConditions) use
buildUsageLogEndpointMatchCondition for consistent normalization/matching.

---

Duplicate comments:
In `@src/app/v1/_lib/proxy/response-handler.ts`:
- Around line 2284-2293: The streaming branch currently calls
maybeSetCodexContext1m(session, provider, usageForCost?.input_tokens) before the
billable filtering, causing non-billable requests (e.g.,
/v1/messages/count_tokens or /v1/responses/compact) to mark
session.context1mApplied; move or guard that call so it only runs for actual
billable usage: ensure you apply normalizeUsageWithSwap and then check the
billable usage filter (the same logic used for billableUsageForCost) and only
after that invoke maybeSetCodexContext1m with the verified billable usage
(references: usageForCost, normalizeUsageWithSwap, maybeSetCodexContext1m,
provider.swapCacheTtlBilling); replicate the same fix for the other streaming
path noted around the second occurrence (lines ~2321-2322).

---

Nitpick comments:
In `@src/app/v1/_lib/proxy/endpoint-policy.ts`:
- Around line 65-73: Add explicit boolean return annotations to
isStrictEndpointPoolPolicy and shouldEnforceStrictEndpointPoolPolicy (declare
both as ": boolean"), and fix the confusing logic: either change
shouldEnforceStrictEndpointPoolPolicy to only return true for strict (i.e.,
return policy.endpointPoolStrictness === "strict") or, if "inherit" is
intentionally treated as enforcing for future values, add a clear comment above
shouldEnforceStrictEndpointPoolPolicy explaining why inherit is considered
enforcing and that this is an extension point for future EndpointPoolStrictness
values; update the functions isStrictEndpointPoolPolicy and
shouldEnforceStrictEndpointPoolPolicy accordingly.

In `@src/app/v1/_lib/proxy/forwarder.ts`:
- Around line 941-979: The code declares endpointPolicy twice (once before the
while loop and again inside it), causing shadowing and redundant calls to
ProxyForwarder.getEndpointPolicy(session); remove the inner declaration (the
const endpointPolicy inside the while loop) and update uses of
shouldAccountCircuitBreaker and shouldEnforceStrictEndpointPool (and any
downstream uses like bypassForwarderPreprocessing) to reference the outer
endpointPolicy so the loop reuses the single endpointPolicy value and avoids
duplicate computations and shadowing.
- Around line 1917-1930: The condition near the throw in ProxyForwarder (the
block that logs "ProxyForwarder: raw passthrough endpoint error, skipping
circuit breaker and provider switch" and then throws lastError) duplicates the
expression !endpointPolicy.allowRetry && !rawCrossProviderFallbackEnabled;
replace that literal with the existing shouldSkipRawRetryAndProviderSwitch
variable (defined earlier) so the PROVIDER_ERROR branch uses the same predicate
as the SYSTEM_ERROR/RESOURCE_NOT_FOUND/empty-response branches and avoid
divergent behavior if the logic changes later.
- Around line 1745-1767: Extract the repeated ternary that builds the provider
error object into a single helper function (e.g., buildProviderErrorDetail) that
accepts (provider, statusCode, statusText, upstreamError,
rawCrossProviderFallbackEnabled) and returns the base {id, name, statusCode,
statusText} when rawCrossProviderFallbackEnabled is true or the base plus
upstreamBody/upstreamParsed when false; then replace the inline ternaries in
buildClientErrorChainEntry, buildRetryFailedChainEntry and the three send()
cases (resource_not_found, vendor_type_all_timeout, retry_failed) to call this
helper so the scrubbing logic is centralized and consistent across those five
call sites.

In `@src/repository/usage-logs.ts`:
- Around line 1559-1618: Non-billing quick path in the if (!ledgerOnly &&
isNonBillingEndpoint(filters.endpoint)) block works as intended (totalCost
hardcoded to "0" and totalTokens computed as
input+output+cacheCreation+cacheRead) but there is duplicated condition-building
logic with the ledger branch; either leave as-is (approved) or refactor by
extracting the shared condition/query construction into a helper (e.g.,
buildMessageRequestStatsQuery) that accepts filters/userId/keyId/providerId and
returns the baseQuery (using messageRequest, keysTable, buildUsageLogConditions)
so both branches reuse the same query-building code.
🪄 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: f4aada0b-ece9-482d-8759-3f02dd45cb27

📥 Commits

Reviewing files that changed from the base of the PR and between b38180d and dfed0df.

📒 Files selected for processing (11)
  • drizzle/0098_equal_selene.sql
  • src/app/v1/_lib/proxy/endpoint-policy.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/lib/session-manager.ts
  • src/repository/usage-logs.ts
  • tests/integration/my-usage-imported-ledger.test.ts
  • tests/integration/non-chat-endpoint-fallback-observability.test.ts
  • tests/unit/proxy/endpoint-policy.test.ts
  • tests/unit/proxy/non-chat-endpoint-fallback.test.ts
  • tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts
✅ Files skipped from review due to trivial changes (2)
  • tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts
  • tests/integration/non-chat-endpoint-fallback-observability.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/unit/proxy/endpoint-policy.test.ts
  • drizzle/0098_equal_selene.sql
  • tests/unit/proxy/non-chat-endpoint-fallback.test.ts

Comment thread src/lib/session-manager.ts Outdated
Comment thread src/repository/usage-logs.ts
Comment thread src/repository/usage-logs.ts Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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

@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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

@ding113
ding113 merged commit 84744a7 into dev Apr 24, 2026
11 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Claude Code Hub Roadmap Apr 24, 2026
@ding113
ding113 deleted the fix/non-chat-endpoint-fallback-20260423T113809Z branch May 13, 2026 06:22
ding113 added a commit that referenced this pull request May 17, 2026
…-only scans (#1194)

* fix(db): cover endpoint in usage_ledger cost indexes to restore index-only scans

#1091 added a non-billing-endpoint filter to LEDGER_BILLING_CONDITION, so every
SUM(cost_usd) query now also references usage_ledger.endpoint. That column was
not part of the three cost covering indexes (idx_usage_ledger_user_cost_cover,
idx_usage_ledger_provider_cost_cover, idx_usage_ledger_key_cost), so the
rate-limit and Quotas-page hot-path queries lost their Index Only Scan and
degraded to a Bitmap Heap Scan with one heap fetch per matching row.

Reproduced on Postgres 18 (1,000,000 rows, 200 users). The per-user
SUM(cost_usd) query (sumUserTotalCost):
  - pre-#1091 condition:  Index Only Scan, 46 shared buffers, Heap Fetches: 0, ~1ms
  - post-#1091 condition: Bitmap Heap Scan, 5027 shared buffers, 5000 heap blocks, ~15ms

Add endpoint as a trailing column to the three covering indexes so the endpoint
filter can be evaluated from the index. After applying the migration the
post-#1091 query is back to Index Only Scan / Heap Fetches: 0 / ~40 buffers.

Drizzle has no INCLUDE support, so endpoint is added as a trailing key column,
matching the existing convention on idx_usage_ledger_key_created_at_desc_cover.

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

* fix(db): make the 0103 cost-index migration idempotent and operator-safe

Addresses PR review feedback on the index rebuild.

A plain CREATE INDEX on usage_ledger holds a SHARE lock that blocks writes for
the whole rebuild, and Drizzle's migrator is transactional so CREATE INDEX
CONCURRENTLY cannot be inlined. Wrap each rebuild in a guarded DO block that
skips when the index definition already contains `endpoint`. Operators on a
large / busy database can now rebuild the three indexes ahead of time with
CREATE INDEX CONCURRENTLY and the migration becomes a no-op -- the escape
hatch documented on migrations 0082 / 0088, extended to the drop+recreate case.

Verified on Postgres 18: 3-column to 4-column on the first apply, clean no-op
on a second apply.

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

---------

Co-authored-by: ding113 <ding113@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant