Skip to content

feat(circuit): unify provider-endpoint circuit visibility and notifications#755

Merged
ding113 merged 7 commits intodevfrom
feat/circuit-breaker-visibility-754
Feb 10, 2026
Merged

feat(circuit): unify provider-endpoint circuit visibility and notifications#755
ding113 merged 7 commits intodevfrom
feat/circuit-breaker-visibility-754

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Feb 10, 2026

Summary

Unify provider/endpoint circuit breaker visibility and notification semantics across the settings UI, proxy decision chain, and webhook alerts.

Problem

The system has three layers of circuit breakers (provider / endpoint / vendor-type), but the UI and decision chain only had full visibility into provider (key) level circuits. Endpoint-level circuit breakers had blind spots:

  1. Endpoint table: Only showed isEnabled from DB, not runtime circuit state -- users saw "enabled" for actually-unavailable endpoints
  2. Provider filter/count: Circuit broken filter and counter only covered key-level circuits, missing providers whose endpoints were all circuit-broken
  3. Decision chain/timeline: endpoint-selector silently filtered circuit-broken endpoints without recording to ProviderChainItem, making it impossible to diagnose why requests hung

Related Issues:

Solution

Unified Status Semantics (Foundation)

  • Add IncidentSource enum (provider | endpoint) and resolveEndpointDisplayStatus() with priority: circuit-open > circuit-half-open > enabled/disabled
  • Add batchGetEndpointCircuitInfo() server action for bulk endpoint circuit state queries

Settings UI (Feature Surfaces)

  • Display circuit-open/half-open badges in the endpoints table with a reset button in the row action menu
  • Extend provider filter and counter to include endpoint-level circuit breakers (deduplicated with key-level)
  • Show layered badges in provider list items distinguishing "Key Circuit" vs "Endpoint Circuit"

Proxy Explainability (Decision Chain)

  • Add endpoint_pool_exhausted reason to ProviderChainItem with strictBlockCause and endpointFilterStats metadata
  • Record endpoint filtering statistics (total/enabled/circuitOpen/available) when no candidates are available
  • Render the new reason in the provider chain timeline with full stats breakdown

Notifications

  • Extend CircuitBreakerAlertData with incidentSource, endpointId, endpointUrl
  • Dedup keys now distinguish provider vs endpoint events (circuit-breaker-alert:{id}:endpoint:{endpointId})
  • Endpoint circuit OPEN triggers notifications via the same toggle as provider circuits
  • Add {{incident_source}}, {{endpoint_id}}, {{endpoint_url}} template placeholders for custom webhook templates

Changes

Core Changes

  • src/lib/endpoint-circuit-breaker.ts - Trigger endpoint circuit alerts on OPEN, add triggerEndpointCircuitBreakerAlert()
  • src/lib/provider-endpoints/endpoint-selector.ts - Add getEndpointFilterStats() for audit trail
  • src/app/v1/_lib/proxy/forwarder.ts - Record endpoint_pool_exhausted in provider chain with filter stats
  • src/app/v1/_lib/proxy/session.ts - Accept strictBlockCause and endpointFilterStats in chain metadata
  • src/types/message.ts - Extend ProviderChainItem with endpoint_pool_exhausted reason and stats fields
  • src/lib/webhook/types.ts - Extend CircuitBreakerAlertData with incident source fields
  • src/lib/notification/notifier.ts - Source-aware dedup keys and logging
  • src/lib/webhook/templates/circuit-breaker.ts - Endpoint-specific alert title/description/fields
  • src/lib/webhook/templates/placeholders.ts - New template variables for endpoint circuit events

UI Changes

  • src/app/[locale]/settings/providers/_components/endpoint-status.ts - Add IncidentSource, EndpointDisplayStatus, resolveEndpointDisplayStatus()
  • src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx - Batch-fetch circuit states, show badges, add reset action
  • src/app/[locale]/settings/providers/_components/provider-manager.tsx - Unified hasAnyCircuitOpen() for filter and counter
  • src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx - Layered "Key Circuit" / "Endpoint Circuit" badges
  • src/app/[locale]/settings/providers/_components/provider-list.tsx - Pass endpointCircuitInfo prop
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx - Accept endpointCircuitInfo prop
  • src/actions/provider-endpoints.ts - Add batchGetEndpointCircuitInfo() action
  • src/lib/utils/provider-chain-formatter.ts - Render endpoint_pool_exhausted in summary, description, and timeline

i18n

  • 12+ new keys across all 5 languages (zh-CN, zh-TW, en, ja, ru) for endpoint circuit states, filter stats, strict block causes, and reset actions

Testing

Automated Tests

  • Unit tests added for batchGetEndpointCircuitInfo action (tests/unit/actions/provider-endpoints.test.ts)
  • Unit tests added for triggerEndpointCircuitBreakerAlert (tests/unit/lib/endpoint-circuit-breaker.test.ts)
  • Unit tests added for getEndpointFilterStats (tests/unit/lib/provider-endpoints/endpoint-selector.test.ts)
  • Unit tests added for proxy forwarder endpoint audit trail (tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts)
  • Unit tests added for resolveEndpointDisplayStatus (tests/unit/settings/providers/endpoint-status.test.ts)
  • Unit tests added for provider manager circuit filter/counter (tests/unit/settings/providers/provider-manager.test.tsx)
  • Unit tests added for provider vendor view circuit UI (tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx)
  • Unit tests added for notifier circuit breaker dedup (tests/unit/webhook/notifier-circuit-breaker.test.ts)
  • Unit tests added for template placeholders (tests/unit/webhook/templates/placeholders.test.ts)
  • Unit tests added for circuit breaker webhook templates (tests/unit/webhook/templates/templates.test.ts)
  • Unit tests added for provider chain formatter (src/lib/utils/provider-chain-formatter.test.ts)

Manual Testing

  1. Open Settings > Providers, verify endpoint table shows circuit-open/half-open badges when endpoints are circuit-broken
  2. Use the "Reset Circuit" action from the endpoint row menu and verify the badge clears
  3. Toggle the "Circuit Broken" filter and verify it catches providers with endpoint-level circuits (not just key-level)
  4. Trigger an endpoint circuit break and verify webhook notification includes endpoint-specific fields
  5. Check the decision chain timeline for a request that hit endpoint pool exhaustion -- verify stats breakdown is shown

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests pass locally (2129 tests passing)
  • i18n strings added for all 5 languages
  • No breaking changes to existing APIs

Description enhanced by Claude AI

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

Greptile Overview

Greptile Summary

This PR unifies circuit-breaker visibility and semantics across the settings UI, proxy decision chain, and webhook notifications by adding an endpoint incident source, bulk endpoint circuit-state lookups, and a new endpoint_pool_exhausted provider-chain reason with filter statistics.

Key integrations:

  • Settings Providers UI now fetches endpoint circuit state in batch, displays circuit-open/half-open badges, offers a per-endpoint reset action, and updates provider-level circuit filters/counters to include endpoint-level circuits.
  • Proxy forwarder records strict endpoint pool exhaustion into the provider chain (including strictBlockCause and optional endpoint filter stats) to improve explainability.
  • Notifications/templates are extended to distinguish provider vs endpoint circuit incidents, with new placeholders and endpoint metadata fields.

Blocking issues to address before merge:

  • The “batch” endpoint circuit lookup still does per-endpoint health reads (Promise.all), which can become a large burst (up to 500 calls) and undermine UI performance.
  • The reset-circuit mutation invalidates a base query key that may not match the composed circuit-info query key, potentially leaving stale circuit badges after a reset depending on React Query’s partial matching behavior.
  • The new server action returns Chinese-only error strings, which can surface inconsistent UX in non-zh locales; it should follow the repo’s standard error/i18n strategy.
  • Endpoint filter stats collection performs per-endpoint circuit checks, which can add load/latency during strict-block incident paths; a batched circuit-state read (or reuse of computed state) is preferable.

Confidence Score: 3/5

  • This PR is close to mergeable but has a few correctness/performance integration issues to fix first.
  • Main logic changes are coherent and covered by tests, but the new endpoint circuit “batch” path is still N+1 and the UI reset invalidation may not refresh the active query cache, both of which can cause real user-visible problems (latency/stale badges).
  • src/actions/provider-endpoints.ts, src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx, src/lib/provider-endpoints/endpoint-selector.ts

Important Files Changed

Filename Overview
src/actions/provider-endpoints.ts Adds batchGetEndpointCircuitInfo server action for endpoint circuit state; still does per-endpoint health lookups and returns hardcoded Chinese errors on permission/exception paths.
src/lib/endpoint-circuit-breaker.ts Triggers async notifications when an endpoint circuit opens and enriches alert with endpoint metadata; adds safer logging on enrichment failure.
src/lib/provider-endpoints/endpoint-selector.ts Adds getEndpointFilterStats to compute enabled/circuit-open/available counts; uses per-endpoint circuit checks which may be expensive under load.
src/app/v1/_lib/proxy/forwarder.ts Records endpoint_pool_exhausted in provider chain with strictBlockCause and optional filter stats; guards attachment of endpointFilterStats when collection fails.
src/lib/utils/provider-chain-formatter.ts Formats new endpoint_pool_exhausted reason in summary/description/timeline; renders stats only when numeric totals are present.
src/lib/notification/notifier.ts Updates circuit-breaker dedup keys to include incidentSource/endpointId and logs incidentSource; avoids endpointId=undefined collision by falling back to source-only suffix.
src/lib/webhook/templates/circuit-breaker.ts Adds endpoint-vs-provider messaging and includes endpointId/endpointUrl fields when incidentSource is endpoint; uses undefined checks for endpointId.
src/lib/webhook/templates/placeholders.ts Adds incident_source/endpoint_id/endpoint_url placeholders and populates them in buildTemplateVariables.
src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx Batch fetches endpoint circuit states via React Query and displays circuit badges/reset action; invalidation uses a base key that may not match composed query keys depending on React Query config.
src/app/[locale]/settings/providers/_components/endpoint-status.ts Introduces IncidentSource/EndpointDisplayStatus and resolveEndpointDisplayStatus with correct source for non-circuit states.
src/app/[locale]/settings/providers/_components/provider-manager.tsx Extends circuit-broken filter/count to include endpoint-level circuit info; uses Array.isArray guard to avoid runtime crashes on malformed endpointCircuitInfo.
src/types/message.ts Extends ProviderChainItem with endpoint_pool_exhausted reason and endpointFilterStats/strictBlockCause fields used by forwarder + formatter.
src/app/v1/_lib/proxy/session.ts Extends provider chain metadata typing to include endpoint_pool_exhausted-related fields so forwarder can record strict block causes and stats.
src/lib/webhook/types.ts Extends CircuitBreakerAlertData with incidentSource and optional endpoint fields to support endpoint circuit notifications.
src/lib/utils/provider-chain-formatter.test.ts Adds unit tests for endpoint_pool_exhausted formatting with and without stats/selector errors.
messages/en/provider-chain.json Adds i18n strings for endpoint_pool_exhausted reason and endpoint filter stats/timeline details.
messages/en/settings/providers/filter.json Adds i18n keys for circuit-broken filter semantics covering endpoint-level circuits.
messages/en/settings/providers/list.json Adds i18n keys for layered key/endpoint circuit badges in provider list items.
messages/en/settings/providers/strings.json Adds i18n keys for endpoint status labels and reset-circuit actions.
messages/ja/provider-chain.json Adds Japanese i18n strings for endpoint_pool_exhausted and endpoint stats.
messages/ja/settings/providers/filter.json Adds Japanese i18n keys for provider filters including endpoint-level circuit conditions.
messages/ja/settings/providers/list.json Adds Japanese i18n keys for provider list circuit badges and counts.
messages/ja/settings/providers/strings.json Adds Japanese i18n keys for endpoint circuit status and reset actions.
messages/ru/provider-chain.json Adds Russian i18n strings for endpoint_pool_exhausted and endpoint stats.
messages/ru/settings/providers/filter.json Adds Russian i18n keys for provider filters including endpoint-level circuit breakers.
messages/ru/settings/providers/list.json Adds Russian i18n keys for provider list layered circuit badges.
messages/ru/settings/providers/strings.json Adds Russian i18n keys for endpoint status and reset-circuit actions.
messages/zh-CN/provider-chain.json Adds zh-CN i18n strings for endpoint_pool_exhausted and endpoint stats.
messages/zh-CN/settings/providers/filter.json Adds zh-CN i18n keys for circuit-broken filter that includes endpoint circuits.
messages/zh-CN/settings/providers/list.json Adds zh-CN i18n keys for provider list endpoint/key circuit badges.
messages/zh-CN/settings/providers/strings.json Adds zh-CN i18n keys for endpoint circuit-open/half-open and reset-circuit actions.
messages/zh-TW/provider-chain.json Adds zh-TW i18n strings for endpoint_pool_exhausted and endpoint stats.
messages/zh-TW/settings/providers/filter.json Adds zh-TW i18n keys for circuit-broken filter including endpoint circuits.
messages/zh-TW/settings/providers/list.json Adds zh-TW i18n keys for provider list circuit badges.
messages/zh-TW/settings/providers/strings.json Adds zh-TW i18n keys for endpoint circuit status and reset actions.
src/app/[locale]/settings/providers/_components/provider-list.tsx Wires endpointCircuitInfo through provider list rendering so child components can display endpoint-level circuit badges.
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx Displays layered key-circuit vs endpoint-circuit badges for providers when endpointCircuitInfo indicates open endpoints.
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx Accepts endpointCircuitInfo prop and passes through to endpoints table / provider list UI for endpoint-level circuit visibility.
tests/unit/actions/provider-endpoints.test.ts Adds unit tests covering batchGetEndpointCircuitInfo behavior and edge cases.
tests/unit/lib/endpoint-circuit-breaker.test.ts Updates endpoint circuit breaker tests to assert alert triggering/logging behavior more explicitly.
tests/unit/lib/provider-endpoints/endpoint-selector.test.ts Adds tests for getEndpointFilterStats counting enabled/circuit-open endpoints.
tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts Adds tests ensuring endpoint_pool_exhausted chain items include strictBlockCause and omit stats when collection fails.
tests/unit/settings/providers/endpoint-status.test.ts Adds tests for resolveEndpointDisplayStatus priority ordering and correct source assignment.
tests/unit/settings/providers/provider-manager.test.tsx Updates provider-manager tests to cover circuit-broken count/filter behavior with endpointCircuitInfo.
tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx Adds tests verifying vendor view renders endpoint circuit badges and reset behavior integration.
tests/unit/webhook/notifier-circuit-breaker.test.ts Adds tests for notifier dedup behavior across provider vs endpoint incidents.
tests/unit/webhook/templates/placeholders.test.ts Adds tests for new circuit-breaker template placeholders for incident source and endpoint fields.
tests/unit/webhook/templates/templates.test.ts Updates webhook template tests to cover endpoint circuit breaker message rendering.

Sequence Diagram

sequenceDiagram
  participant UI as Settings UI
  participant Actions as provider-endpoints actions
  participant Redis as Endpoint circuit store
  participant Proxy as ProxyForwarder
  participant Session as Proxy Session (ProviderChain)
  participant Webhook as Notifier/Webhook

  UI->>Actions: batchGetEndpointCircuitInfo({endpointIds[]})
  Actions->>Redis: getEndpointHealthInfo(endpointId) (per id)
  Redis-->>Actions: {circuitState,failureCount,circuitOpenUntil}
  Actions-->>UI: map endpointId -> circuitState
  UI->>Actions: resetEndpointCircuit({endpointId})
  Actions->>Redis: deleteEndpointCircuitState(endpointId)
  Actions-->>UI: ok

  Proxy->>Proxy: getPreferredProviderEndpoints(...)
  alt strict policy + no candidates
    Proxy->>Proxy: getEndpointFilterStats(vendorId, providerType)
    Proxy->>Session: addProviderToChain(reason=endpoint_pool_exhausted, strictBlockCause, endpointFilterStats?)
    Session-->>Proxy: chain updated
  end

  Proxy->>Redis: recordEndpointFailure(endpointId)
  alt circuit opens
    Proxy->>Webhook: triggerEndpointCircuitBreakerAlert(endpointId,...)
    Webhook->>Webhook: sendCircuitBreakerAlert(incidentSource=endpoint, endpointId, endpointUrl?)
  end
Loading

ding113 and others added 5 commits February 10, 2026 14:54
Add IncidentSource type and resolveEndpointDisplayStatus() with priority:
circuit-open > circuit-half-open > enabled > disabled.
25 tests covering all status combinations.
Add batchGetEndpointCircuitInfo() action for bulk endpoint circuit
state reads, avoiding N+1 queries in table views.
- ProviderEndpointsTable: display circuit state badge + reset button
- ProviderManager: endpoint circuits included in filter/counters
- ProviderRichListItem: layered badges (key vs endpoint)
- Notification: unified provider+endpoint alerts under one toggle
- i18n: 5 languages (en, zh-CN, zh-TW, ja, ru)

Tests: 28 passed
- Add strictBlockCause and endpointFilterStats to chain metadata
- New getEndpointFilterStats function for endpoint pool statistics
- Record no_endpoint_candidates with full stats when strict policy blocks

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

coderabbitai bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

新增端点级熔断可观测性:扩展国际化文案、在 UI 中展示与重置端点熔断状态、批量查询端点熔断信息、在代理决策链记录端点池耗尽与统计,并新增告警/模板与多处类型与测试覆盖。

Changes

Cohort / File(s) Summary
国际化:provider-chain 与设置文案
messages/en/provider-chain.json, messages/ja/provider-chain.json, messages/ru/provider-chain.json, messages/zh-CN/provider-chain.json, messages/zh-TW/provider-chain.json, messages/*/settings/providers/filter.json, messages/*/settings/providers/list.json, messages/*/settings/providers/strings.json
新增多语言键:endpoint_pool_exhausted、endpoint_circuit_open、endpoint_disabled、endpointPoolExhausted、endpointStats 与 endpoint 重置/标签文案等。
Endpoint circuit 后端操作 & API
src/actions/provider-endpoints.ts, src/lib/provider-endpoints/endpoint-selector.ts, src/lib/endpoint-circuit-breaker.ts
新增 batchGetEndpointCircuitInfo、getEndpointFilterStats、EndpointFilterStats 类型与 triggerEndpointCircuitBreakerAlert;记录/触发端点熔断告警并提供统计查询。
类型与消息结构
src/types/message.ts, src/lib/webhook/types.ts, src/lib/webhook/templates/placeholders.ts
扩展 ProviderChainItem.reason(加入 endpoint_pool_exhausted),新增 strictBlockCause、endpointFilterStats、webhook 中 incidentSource/endpointId/endpointUrl 等字段与模板占位符。
代理与会话审计
src/app/v1/_lib/proxy/forwarder.ts, src/app/v1/_lib/proxy/session.ts
在严格模式端点池耗尽路径记录 strictBlockCause 与 endpointFilterStats 并将其加入 provider-chain 审计条目。
Provider UI:状态、表格与管理组件
src/app/[locale]/settings/providers/_components/endpoint-status.ts, .../provider-endpoints-table.tsx, .../provider-list.tsx, .../provider-manager.tsx, .../provider-rich-list-item.tsx, .../provider-vendor-view.tsx
新增 IncidentSource 与 resolveEndpointDisplayStatus;表格批量获取并展示端点 circuit 状态、提供重置操作;向上层组件传递 endpointCircuitInfo 并在列表/项中展示端点熔断徽章与计数/过滤。
决策链格式化与展示
src/lib/utils/provider-chain-formatter.ts, src/lib/utils/provider-chain-formatter.test.ts
将 endpoint_pool_exhausted 视为失败并在描述/时间线中渲染 endpointFilterStats、strictBlockCause(no_endpoint_candidates / selector_error)与可选错误信息;增加相应单元测试。
Webhook 与通知去重
src/lib/notification/notifier.ts, src/lib/webhook/templates/circuit-breaker.ts, src/lib/webhook/templates/placeholders.ts
按 incidentSource(provider/endpoint)区分通知去重键,模板与告警正文支持端点上下文(ID/URL/来源)。
测试
tests/unit/**(涉及 provider-endpoints、endpoint-circuit-breaker、endpoint-selector、proxy-forwarder-endpoint-audit、settings/providers/、webhook/ 等大量新增/扩展测试)
添加/扩展大量单元测试,覆盖批量查询、告警触发、统计计算、审计记录、UI 显示与重置交互以及 webhook 模板变量与去重行为。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat(circuit): unify provider-endpoint circuit visibility and notifications' clearly summarizes the main objective of unifying circuit-breaker visibility across UI, proxy decision chain, and webhook notifications.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset, explaining the problem, solution, changes across multiple systems (UI, proxy, webhooks), testing approach, and linking to issue #754.
Linked Issues check ✅ Passed The code changes comprehensively address all objectives from issue #754: endpoint runtime circuit status visibility in UI, filter/counter integration, decision chain recording with statistics, and webhook endpoint-awareness. All three visibility gaps (endpoint table, provider filter/count, decision chain) are resolved.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to the stated objectives of unifying endpoint circuit-breaker visibility and notifications. No out-of-scope changes detected; i18n additions, UI components, proxy logic, and notification templates are all cohesively integrated around the core feature.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/circuit-breaker-visibility-754

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.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

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

This pull request significantly enhances the circuit breaker functionality by unifying the visibility, management, and notification systems for both provider-level and individual endpoint-level circuit breakers. It introduces new UI elements for monitoring and resetting endpoint circuits, improves filtering capabilities, and provides more granular detail in the audit trail when endpoint pools are exhausted. The changes aim to provide administrators with a clearer understanding and better control over the health and availability of their configured providers and endpoints.

Highlights

  • Unified Circuit Breaker Visibility and Semantics: Implemented a unified approach for displaying and managing circuit breaker states for both providers and individual endpoints. This includes new IncidentSource and EndpointDisplayStatus types, and a resolveEndpointDisplayStatus function to prioritize display status (circuit-open > half-open > enabled/disabled).
  • Endpoint Circuit Breaker Management in UI: Added the ability to view endpoint-level circuit breaker status directly in the provider endpoints table, including 'Circuit Open' and 'Circuit Half-Open' badges. A 'Reset Circuit' action has been introduced, allowing administrators to manually reset a tripped endpoint circuit breaker.
  • Enhanced Provider Filtering and Counting: Updated the provider list to include endpoint-level circuit breaker status in filtering and counting mechanisms. Providers with either a key-level or any endpoint-level circuit open will now be counted and filterable under a unified 'Circuit Broken' filter, with layered badges distinguishing between key and endpoint incidents.
  • Detailed Endpoint Pool Exhaustion Tracking: Introduced strictBlockCause and endpointFilterStats to the provider chain metadata. This allows for detailed auditing and timeline visualization when a provider is skipped due to an exhausted endpoint pool (e.g., all endpoints are disabled or circuit-open), providing insights into why no suitable endpoints were found.
  • Unified Circuit Breaker Notifications: Extended the CircuitBreakerAlertData interface with incidentSource, endpointId, and endpointUrl to unify notification semantics. Endpoint circuit OPEN events now trigger notifications via the same toggle, with improved deduplication logic that differentiates between provider and endpoint incidents.
  • Internationalization Updates: Added 12 new i18n keys across five languages (English, Japanese, Russian, Simplified Chinese, Traditional Chinese) to support the new UI elements and timeline messages related to endpoint circuit breakers and pool exhaustion.
Changelog
  • messages/en/provider-chain.json
    • Added new translation keys for 'Endpoint Pool Exhausted', 'Endpoint Circuit Open', 'Endpoint Disabled', 'Endpoint Filter Stats', and strict block reasons.
  • messages/en/settings/providers/filter.json
    • Added new translation keys 'Key Circuit Broken' and 'Endpoint Circuit Broken' for filtering options.
  • messages/en/settings/providers/list.json
    • Added new translation keys 'Key Circuit Broken' and 'Endpoint Circuit Broken' for list display.
  • messages/en/settings/providers/strings.json
    • Added new translation keys for 'Reset Circuit', 'Endpoint circuit breaker reset', and 'Failed to reset endpoint circuit breaker'.
  • messages/ja/provider-chain.json
    • Added new Japanese translation keys for endpoint pool exhaustion, circuit states, and filter statistics.
  • messages/ja/settings/providers/filter.json
    • Added new Japanese translation keys for key and endpoint circuit broken filters.
  • messages/ja/settings/providers/list.json
    • Added new Japanese translation keys for key and endpoint circuit broken list items.
  • messages/ja/settings/providers/strings.json
    • Added new Japanese translation keys for circuit reset actions and messages.
  • messages/ru/provider-chain.json
    • Added new Russian translation keys for endpoint pool exhaustion, circuit states, and filter statistics.
  • messages/ru/settings/providers/filter.json
    • Added new Russian translation keys for key and endpoint circuit broken filters.
  • messages/ru/settings/providers/list.json
    • Added new Russian translation keys for key and endpoint circuit broken list items.
  • messages/ru/settings/providers/strings.json
    • Added new Russian translation keys for circuit reset actions and messages.
  • messages/zh-CN/provider-chain.json
    • Added new Simplified Chinese translation keys for endpoint pool exhaustion, circuit states, and filter statistics.
  • messages/zh-CN/settings/providers/filter.json
    • Added new Simplified Chinese translation keys for key and endpoint circuit broken filters.
  • messages/zh-CN/settings/providers/list.json
    • Added new Simplified Chinese translation keys for key and endpoint circuit broken list items.
  • messages/zh-CN/settings/providers/strings.json
    • Added new Simplified Chinese translation keys for circuit reset actions and messages.
  • messages/zh-TW/provider-chain.json
    • Added new Traditional Chinese translation keys for endpoint pool exhaustion, circuit states, and filter statistics.
  • messages/zh-TW/settings/providers/filter.json
    • Added new Traditional Chinese translation keys for key and endpoint circuit broken filters.
  • messages/zh-TW/settings/providers/list.json
    • Added new Traditional Chinese translation keys for key and endpoint circuit broken list items.
  • messages/zh-TW/settings/providers/strings.json
    • Added new Traditional Chinese translation keys for circuit reset actions and messages.
  • src/actions/provider-endpoints.ts
    • Added BatchGetEndpointCircuitInfoSchema for validating batch endpoint circuit info requests.
    • Implemented batchGetEndpointCircuitInfo to retrieve circuit states for multiple endpoints.
    • Modified resetEndpointCircuit to handle endpoint-specific circuit resets.
  • src/app/[locale]/settings/providers/_components/endpoint-status.ts
    • Defined IncidentSource enum to distinguish between provider and endpoint incidents.
    • Introduced EndpointDisplayStatus interface for unified status semantics.
    • Added resolveEndpointDisplayStatus function to determine endpoint display status based on circuit state and enabled status.
  • src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx
    • Imported batchGetEndpointCircuitInfo and resetEndpointCircuit actions.
    • Imported EndpointCircuitState type.
    • Fetched endpoint circuit breaker states in a batch using useQuery.
    • Passed circuitState to EndpointRow component.
    • Displayed circuit status badges ('Circuit Open', 'Circuit Half-Open') in the endpoint table.
    • Added a 'Reset Circuit' dropdown menu item for tripped endpoint circuits.
  • src/app/[locale]/settings/providers/_components/provider-list.tsx
    • Added endpointCircuitInfo prop to ProviderListProps.
    • Passed endpointCircuitInfo to ProviderRichListItem.
  • src/app/[locale]/settings/providers/_components/provider-manager.tsx
    • Defined EndpointCircuitInfoMap type for endpoint circuit breaker states.
    • Added endpointCircuitInfo prop to ProviderManagerProps.
    • Implemented hasAnyCircuitOpen callback to check for both key-level and endpoint-level circuit breaks.
    • Updated circuitBrokenCount and filtering logic to include endpoint circuit breaker states.
    • Passed endpointCircuitInfo to ProviderList component.
  • src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx
    • Added endpointCircuitInfo prop to ProviderRichListItemProps.
    • Rendered separate badges for 'Key Circuit Broken' and 'Endpoint Circuit Broken' based on healthStatus and endpointCircuitInfo.
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
    • Added endpointCircuitInfo prop to ProviderVendorViewProps.
  • src/app/v1/_lib/proxy/forwarder.ts
    • Imported getEndpointFilterStats from endpoint-selector.
    • Recorded strictBlockCause and endpointFilterStats in the provider chain when an endpoint pool is exhausted.
  • src/app/v1/_lib/proxy/session.ts
    • Added endpoint_pool_exhausted as a possible reason for ProviderChainItem.
    • Added strictBlockCause and endpointFilterStats to addProviderToChain metadata.
  • src/lib/endpoint-circuit-breaker.ts
    • Added triggerEndpointCircuitBreakerAlert to send alerts when an endpoint circuit opens.
    • Defined EndpointCircuitAlertData interface for structured endpoint alert data.
  • src/lib/notification/notifier.ts
    • Modified sendCircuitBreakerAlert to use incidentSource for generating unique deduplication keys, allowing separate alerts for provider and endpoint incidents.
    • Included incidentSource in logger info and error messages.
  • src/lib/provider-endpoints/endpoint-selector.ts
    • Defined EndpointFilterStats interface.
    • Implemented getEndpointFilterStats to collect statistics on total, enabled, circuit-open, and available endpoints for a given vendor and type.
  • src/lib/utils/provider-chain-formatter.test.ts
    • Added comprehensive test suite for endpoint_pool_exhausted reason in formatProviderSummary, formatProviderDescription, and formatProviderTimeline.
    • Included tests for graceful degradation when endpointFilterStats is missing or for unknown reasons.
  • src/lib/utils/provider-chain-formatter.ts
    • Updated getProviderStatus and isActualRequest to recognize endpoint_pool_exhausted as a failure reason.
    • Modified formatProviderDescription to include a description for endpoint_pool_exhausted.
    • Added a new section in formatProviderTimeline to render details for endpoint_pool_exhausted, including filter statistics and strict block cause.
  • src/lib/webhook/templates/circuit-breaker.ts
    • Updated buildCircuitBreakerMessage to dynamically set the title and description based on incidentSource (provider or endpoint).
    • Added endpoint-specific fields (endpointId, endpointUrl) to the webhook message when the incident source is an endpoint.
  • src/lib/webhook/templates/placeholders.ts
    • Added new template placeholders for circuit breaker alerts: {{incident_source}}, {{endpoint_id}}, and {{endpoint_url}}.
  • src/lib/webhook/types.ts
    • Extended CircuitBreakerAlertData interface with incidentSource, endpointId, and endpointUrl.
  • src/types/message.ts
    • Added endpoint_pool_exhausted to the reason type of ProviderChainItem.
    • Added strictBlockCause and endpointFilterStats fields to ProviderChainItem for detailed endpoint pool exhaustion information.
  • tests/unit/actions/provider-endpoints.test.ts
    • Added tests for batchGetEndpointCircuitInfo covering successful retrieval, empty input, admin session requirement, and input validation.
  • tests/unit/lib/endpoint-circuit-breaker.test.ts
    • Added tests for triggerEndpointCircuitBreakerAlert to ensure it calls sendCircuitBreakerAlert and includes endpoint URL when available.
  • tests/unit/lib/provider-endpoints/endpoint-selector.test.ts
    • Added tests for getEndpointFilterStats to verify correct counting of total, enabled, circuit-open, and available endpoints, including edge cases.
  • tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts
    • Updated mocks to include getEndpointFilterStats.
    • Added tests to verify endpoint_pool_exhausted reason, strictBlockCause, and endpointFilterStats are correctly recorded in the provider chain for both no_endpoint_candidates and selector_error scenarios.
    • Included tests for graceful handling of getEndpointFilterStats failures.
  • tests/unit/settings/providers/endpoint-status.test.ts
    • Added tests for IncidentSource type.
    • Added comprehensive tests for resolveEndpointDisplayStatus covering priority ordering (circuit-open > half-open > enabled > disabled), various circuit states, and edge cases.
  • tests/unit/settings/providers/provider-manager.test.tsx
    • Added new test file for ProviderManager component.
    • Tested circuitBrokenCount logic to ensure it correctly counts providers with key-level or endpoint-level circuit breaks, including deduplication.
    • Verified that the circuit broken filter correctly filters providers based on endpoint circuit status.
    • Confirmed that endpointCircuitInfo is passed to ProviderList for rendering layered circuit labels.
  • tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
    • Updated makeProviderDisplay with additional provider properties.
    • Added mocks for batchGetEndpointCircuitInfo and resetEndpointCircuit.
    • Added tests for endpoint circuit breaker badge display, verifying correct rendering for open, half-open, and closed states.
    • Added tests for the 'Reset Circuit' action, confirming its availability when a circuit is open and its success/failure handling.
  • tests/unit/webhook/notifier-circuit-breaker.test.ts
    • Added new test file for sendCircuitBreakerAlert.
    • Tested deduplication logic for circuit breaker alerts, ensuring it differentiates between provider and endpoint incidents using incidentSource and endpointId.
  • tests/unit/webhook/templates/placeholders.test.ts
    • Added tests to verify that buildTemplateVariables correctly includes endpoint-specific variables (incident_source, endpoint_id, endpoint_url) when the incident source is an endpoint, and leaves them empty otherwise.
  • tests/unit/webhook/templates/templates.test.ts
    • Added tests for buildCircuitBreakerMessage to ensure it produces provider-specific or endpoint-specific messages based on incidentSource, including relevant details.
Activity
  • The pull request was created by ding113.
  • The pull request description indicates it was generated with Claude Code and co-authored by Claude Opus 4.6.
  • The description outlines a detailed plan for unifying circuit breaker visibility and notifications, covering foundation, feature surfaces, and explainability aspects.
  • Verification steps include passing lint/typecheck/build/test, 2129 passing tests, and maintained coverage.
Using Gemini Code Assist

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

Invoking Gemini

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

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

Customization

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

Limitations & Feedback

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

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

Footnotes

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

@github-actions github-actions bot added size/XL Extra Large PR (> 1000 lines) enhancement New feature or request area:UI area:provider area:i18n labels Feb 10, 2026
@github-actions
Copy link
Contributor

🧪 测试结果

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

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

Copy link
Contributor

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

Choose a reason for hiding this comment

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

Code Review

This pull request is a significant and well-executed feature enhancement that unifies circuit breaker visibility and notifications for both providers and endpoints. The changes are comprehensive, covering backend logic, UI components, internationalization, and extensive testing. The introduction of IncidentSource, batch endpoint queries, and detailed provider chain logging for endpoint exhaustion are excellent improvements for maintainability and observability. My review focuses on minor improvements related to internationalization, notification enrichment, and UI responsiveness.

Comment on lines +567 to +571
return {
ok: false,
error: "无权限执行此操作",
errorCode: ERROR_CODES.PERMISSION_DENIED,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Hardcoded Chinese error messages should be avoided for better internationalization and maintainability. Please use English messages or error codes that can be translated on the client side.

Suggested change
return {
ok: false,
error: "无权限执行此操作",
errorCode: ERROR_CODES.PERMISSION_DENIED,
};
return {
ok: false,
error: "Permission denied",
errorCode: ERROR_CODES.PERMISSION_DENIED,
};

return { ok: true, data: results };
} catch (error) {
logger.error("batchGetEndpointCircuitInfo:error", error);
const message = error instanceof Error ? error.message : "批量获取端点熔断状态失败";
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This hardcoded Chinese error message should be in English for consistency and maintainability, allowing for proper internationalization on the client side.

Suggested change
const message = error instanceof Error ? error.message : "批量获取端点熔断状态失败";
const message = error instanceof Error ? error.message : "Failed to batch get endpoint circuit info";

return map;
},
enabled: endpointIds.length > 0,
staleTime: 15_000,
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

To provide a more dynamic user experience, consider adding a refetchInterval to this query. This will automatically poll for updates to the circuit breaker states, ensuring the UI reflects the current status without requiring a manual refresh.

    staleTime: 15_000,
    refetchInterval: 15_000,

Comment on lines 238 to 259
// Try to enrich with endpoint URL from database
let endpointUrl: string | undefined;
try {
const { findProviderEndpointById } = await import("@/repository");
const endpoint = await findProviderEndpointById(endpointId);
if (endpoint) {
endpointUrl = endpoint.url;
}
} catch {
// DB lookup failure should not block alert
}

await sendCircuitBreakerAlert({
providerId: endpointId, // Use endpointId as providerId for dedup purposes
providerName: "",
failureCount,
retryAt,
lastError,
incidentSource: "endpoint",
endpointId,
endpointUrl,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The providerName is currently hardcoded to an empty string when triggering an endpoint circuit breaker alert. This results in a less informative notification message (e.g., '供应商 的端点...'). To improve this, you can fetch the vendor's display name using the vendorId from the endpoint details. This will provide more context in the alert.

    // Try to enrich with endpoint URL and vendor name from database
    let endpointUrl: string | undefined;
    let providerName = "";
    try {
      const { findProviderEndpointById, findProviderVendorById } = await import("@/repository");
      const endpoint = await findProviderEndpointById(endpointId);
      if (endpoint) {
        endpointUrl = endpoint.url;
        const vendor = await findProviderVendorById(endpoint.vendorId);
        if (vendor) {
          providerName = vendor.displayName || vendor.websiteDomain || `Vendor #${vendor.id}`;
        }
      }
    } catch (dbError) {
      // DB lookup failure should not block alert
      logger.warn("[triggerEndpointCircuitBreakerAlert] DB lookup failed", {
        endpointId,
        error: dbError instanceof Error ? dbError.message : String(dbError),
      });
    }

    await sendCircuitBreakerAlert({
      providerId: endpointId, // Use endpointId as providerId for dedup purposes
      providerName,
      failureCount,
      retryAt,
      lastError,
      incidentSource: "endpoint",
      endpointId,
      endpointUrl,
    });

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🤖 Fix all issues with AI agents
In `@messages/en/settings/providers/filter.json`:
- Around line 3-4: The English translations for the keys "keyCircuitBroken" and
"endpointCircuitBroken" are missing the "Broken" suffix; update their string
values to match other locales (e.g., change "Key Circuit" to "Key Circuit
Broken" and "Endpoint Circuit" to "Endpoint Circuit Broken" or use the preferred
phrasing like "Key Circuit Open" / "Endpoint Circuit Open") so the English
entries align semantically with other languages.

In `@messages/ru/provider-chain.json`:
- Line 41: The new translation uses the transliterated term "эндпоинт" for keys
like "endpointPoolExhausted" (and the other new keys around lines 67-68 and
193-200), but the file already uses the formal Russian term "Конечная точка" in
the existing key "details.endpoint"; update the new keys to use the same formal
term ("Конечная точка") so terminology is consistent across the file, and run a
quick grep/scan for any other occurrences of "эндпоинт" to replace them with
"Конечная точка".

In `@messages/ru/settings/providers/filter.json`:
- Around line 3-4: The Russian translation currently leaves "endpoint"
untranslated for the "endpointCircuitBroken" key; update the value for
"endpointCircuitBroken" to a proper Russian term (either the transliteration
"эндпоинт" or the translation "конечная точка") to match how other languages
translate "endpoint" and ensure consistency with the "keyCircuitBroken" entry
and other locales.

In `@src/actions/provider-endpoints.ts`:
- Around line 126-128: The BatchGetEndpointCircuitInfoSchema currently allows an
unbounded endpointIds array (z.array(EndpointIdSchema).min(0)), which can lead
to huge concurrent calls to getEndpointHealthInfo via Promise.all; update
BatchGetEndpointCircuitInfoSchema to add a sensible upper bound (e.g., .max(200)
or .max(500)) on endpointIds to cap batch size and prevent mass concurrency, and
ensure any callers or docs reflect the new limit so callers split requests when
needed.

In `@src/app/`[locale]/settings/providers/_components/provider-vendor-view.tsx:
- Around line 37-38: The prop endpointCircuitInfo declared on
ProviderVendorViewProps is unused inside the ProviderVendorView component;
either remove it from the interface or thread it through to the components that
need it (e.g., pass endpointCircuitInfo from ProviderVendorView into VendorCard
and/or into VendorKeysCompactList and ProviderEndpointsSection), and update
those child component prop types to accept EndpointCircuitInfoMap where required
so the value is consumed rather than left unused.

In `@src/app/v1/_lib/proxy/forwarder.ts`:
- Around line 498-528: The code unsafely casts exhaustionContext to
ProviderChainItem["decisionContext"] before calling session.addProviderToChain;
remove the type assertion and either (A) omit decisionContext entirely and pass
strictBlockCause and selectorError via their existing fields (keep
endpointFilterStats as-is), or (B) construct a complete decisionContext object
with the required properties (e.g., totalProviders, enabledProviders,
targetType, etc.) before passing it to session.addProviderToChain so no required
fields are undefined; update the session.addProviderToChain call to use the safe
option and drop the "as ProviderChainItem['decisionContext']" cast on
exhaustionContext.

In `@src/lib/endpoint-circuit-breaker.ts`:
- Around line 250-259: The call to sendCircuitBreakerAlert is populating
providerId with endpointId and leaving providerName empty, which is misleading
to webhook consumers; update the payload in the sendCircuitBreakerAlert
invocation so providerId is an explicit placeholder (e.g., -1 or 0) instead of
endpointId, set providerName to a clear readable placeholder (e.g., "Endpoint"
or `endpoint:${endpointId}`), and ensure the payload includes endpointId and
endpointUrl prominently so templates can display those instead of provider
fields (change in the invocation around sendCircuitBreakerAlert, adjust
providerId and providerName there).

In `@src/lib/notification/notifier.ts`:
- Around line 27-28: The deduplication suffix construction uses
`data.endpointId` when `source === "endpoint"`, which can produce
"endpoint:undefined" because `endpointId` is optional in
`CircuitBreakerAlertData`; update the logic in notifier.ts where `source` and
`dedupSuffix` are computed to defensively handle a missing endpointId: if
`source === "endpoint"` and `data.endpointId` is falsy/undefined, either
return/emit a warning or fall back to a stable alternative suffix (e.g.,
"endpoint:unknown" or include only "endpoint") so alerts for different endpoints
are not collapsed; change the expression that sets `dedupSuffix` (and any
related downstream key generation) to check `data.endpointId` explicitly before
concatenation.

In `@src/lib/webhook/templates/circuit-breaker.ts`:
- Around line 29-32: The description template can emit "ID: undefined" because
endpointId is optional; update the logic that builds description (the const
description using isEndpoint and data.endpointId) to only include the "(ID:
...)" segment when data.endpointId is defined (e.g., use a conditional/ternary
to append ` (ID: ${data.endpointId})` only if data.endpointId !== undefined,
otherwise omit or use a clear placeholder), ensuring you keep the existing
isEndpoint check and the providerName/providerId text intact.

In `@tests/unit/lib/endpoint-circuit-breaker.test.ts`:
- Around line 158-199: The second test for triggerEndpointCircuitBreakerAlert is
missing a vi.resetModules() call before setting up vi.doMock, which can cause
the dynamic import of "@/lib/endpoint-circuit-breaker" to return a cached module
and ignore the new findProviderEndpointById mock; add vi.resetModules()
immediately before vi.doMock(...) in that test so the subsequent await
import("@/lib/endpoint-circuit-breaker") loads a fresh module that uses the
mocked findProviderEndpointById and sendCircuitBreakerAlert.

In `@tests/unit/settings/providers/endpoint-status.test.ts`:
- Around line 201-210: Rename the test case to accurately reflect the actual
expected behavior: update the test title for the spec that calls
createEndpoint(...) and resolveEndpointDisplayStatus(endpoint, "closed") so it
states that when isEnabled is null the endpoint is treated as enabled (e.g.,
"should return enabled when circuit is closed and isEnabled is null"); no
changes to resolveEndpointDisplayStatus or createEndpoint are required—only the
test description string should be corrected to match the asserted expectation of
status: "enabled", source: "endpoint", priority: 2.

In `@tests/unit/settings/providers/provider-manager.test.tsx`:
- Around line 328-340: The test silently skips assertions when toggle is
missing; change it to first assert the toggle exists (e.g.,
expect(toggle).not.toBeNull() / toBeTruthy()) immediately after querying
"#circuit-broken-filter" before calling act, then proceed to dispatch the click
on the toggle and assert the provider list
(querySelectorAll("[data-testid^='provider-']") and providerNames expectations).
Ensure you reference the same variable names (toggle, container) and keep the
act wrapper around the click.

In `@tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx`:
- Around line 520-650: Tests for resetEndpointCircuit currently call
providerEndpointsActionMocks.resetEndpointCircuit directly instead of exercising
the UI; update each of the three tests that reference
providerEndpointsActionMocks.resetEndpointCircuit so they locate the reset
button rendered by ProviderVendorView (e.g., find the endpoint row for
endpointId 1 and querySelector by the reset button's aria-label or CSS
selector), simulate a user click on that button, await flushTicks to let the
component call the action, then assert
providerEndpointsActionMocks.resetEndpointCircuit was called with
expect.objectContaining({ endpointId: 1 }) and verify the resulting UI feedback
(success or error) is displayed; use the existing ProviderVendorView render and
flushTicks utilities and the providerEndpointsActionMocks.resetEndpointCircuit
mock to drive expected responses.
🧹 Nitpick comments (12)
src/types/message.ts (1)

58-65: endpointFilterStats 改用已定义的 EndpointFilterStats 类型。

EndpointFilterStats 接口已在 src/lib/provider-endpoints/endpoint-selector.ts 中定义并导出(lines 56-61),结构与此处的内联类型完全相同。建议导入该类型以避免重复定义。

建议修改
import type { EndpointFilterStats } from "@/lib/provider-endpoints/endpoint-selector";

// 在 message.ts 中,将
endpointFilterStats?: {
  total: number;
  enabled: number;
  circuitOpen: number;
  available: number;
};

// 改为
endpointFilterStats?: EndpointFilterStats;
tests/unit/webhook/notifier-circuit-breaker.test.ts (1)

50-180: 测试覆盖全面,去重 key 的各场景验证清晰。

建议补充一个 incidentSource: "endpoint"endpointIdundefined 的用例,以验证边界行为(与 notifier.ts 中可能产生 "endpoint:undefined" 的问题对应)。

src/lib/endpoint-circuit-breaker.ts (1)

213-223: EndpointCircuitAlertData 接口与实际发送的数据不一致

此接口定义的字段(endpointIdfailureCountretryAtlastErrorendpointUrl)无法准确反映 triggerEndpointCircuitBreakerAlert 实际发送的数据结构。函数实际发送的是 CircuitBreakerAlertData 类型,包含额外的 providerIdproviderNameincidentSource 字段。此外,EndpointCircuitAlertData 被导出但在整个代码库中未被使用。

建议移除此未使用的导出接口,或如果有外部消费需求,应更新其定义以匹配实际发送的数据结构。

src/lib/webhook/templates/circuit-breaker.ts (1)

19-27: 新增的硬编码中文字符串与已有代码风格一致,但不符合 i18n 规范。

"端点ID"、"端点地址" 等新增字符串与文件中已有的 "失败次数"、"预计恢复" 等保持了一致的风格。不过按照项目编码规范,用户可见的文本应使用 i18n。如果当前 webhook 模板有计划后续迁移 i18n,可暂时保持现状;否则建议在适当时机统一处理。

src/lib/webhook/templates/placeholders.ts (1)

23-61: 所有模板占位符的 labeldescription 均为硬编码中文字符串。

这是现有的模式,非本 PR 引入。但按照编码规范要求所有用户可见字符串使用 i18n(支持 5 种语言),如果这些占位符元数据会在 UI 中展示给用户,建议后续迭代中将其国际化。

src/app/[locale]/settings/providers/_components/endpoint-status.ts (1)

22-36: 新增类型定义语义清晰。

IncidentSourceEndpointDisplayStatus 类型定义合理。不过 EndpointDisplayStatus.status 声明为 string,而实际返回值只有 "circuit-open" | "circuit-half-open" | "enabled" | "disabled" 四种。建议使用联合类型以获得更好的类型安全性。

建议收窄 status 类型
+export type EndpointDisplayStatusToken =
+  | "circuit-open"
+  | "circuit-half-open"
+  | "enabled"
+  | "disabled";
+
 export interface EndpointDisplayStatus {
-  status: string;
+  status: EndpointDisplayStatusToken;
   source: IncidentSource;
   priority: number;
 }
tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx (1)

685-688: 通过 Tailwind CSS 类名断言 UI 状态较为脆弱。

[class*="text-rose-500"][class*="text-amber-500"] 依赖具体的样式类名,一旦颜色调整或类名变更测试就会失败。考虑使用 data-testid 属性或检查文本内容作为更稳定的断言方式。

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

511-527: 移动端和桌面端的端点熔断徽章渲染逻辑重复。

Lines 518-527 和 Lines 717-726 的 JSX 结构几乎完全相同(Badge 组件、className、图标和文本)。虽然这是响应式布局中移动/桌面分离 JSX 的常见模式,可以考虑提取为一个小组件以减少重复。

tests/unit/settings/providers/provider-manager.test.tsx (2)

389-440: 测试名称 "passes endpointCircuitInfo to ProviderList" 具有误导性

当前 mock 的 ProviderList(第 28-38 行)没有使用或检查 endpointCircuitInfo 属性。此测试实际验证的是 circuitBrokenCount 显示为 (3),而非 prop 是否传递到了 ProviderList。建议修改测试名称使其更准确,或在 mock 中捕获 endpointCircuitInfo prop 并进行断言。


162-186: 断言 container.textContent 包含 "(1)" 较为脆弱

"(1)" 可能在其他 UI 元素中意外匹配。不过考虑到组件已被大量 mock,当前上下文中出现误匹配的风险较低,可以接受。

tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts (1)

622-678: 建议将两个场景拆分为独立测试用例

当前测试在一个 test 中包含两个完整的 session 场景(selector_errorno_endpoint_candidates)。如果第一个场景失败,第二个场景将被跳过,降低了诊断效率。不过功能上并无问题。

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

133-149: 查询键结构可优化:避免端点顺序变化导致的缓存碎片化

当前 queryKey: ["endpoint-circuit-info", ...endpointIds] 将所有端点 ID 展开至数组中。当端点顺序变化时(即使 ID 集合相同),会创建不同的缓存条目。建议使用排序后的 ID 数组作为单一元素,确保相同端点集合始终使用同一缓存:

建议优化
-  const endpointIds = useMemo(() => endpoints.map((ep) => ep.id), [endpoints]);
+  const endpointIds = useMemo(
+    () => endpoints.map((ep) => ep.id).sort((a, b) => a - b),
+    [endpoints],
+  );
   const { data: circuitInfoMap = {} } = useQuery({
-    queryKey: ["endpoint-circuit-info", ...endpointIds],
+    queryKey: ["endpoint-circuit-info", endpointIds],

注:第 290 行的 invalidateQueries({ queryKey: ["endpoint-circuit-info"] }) 使用前缀匹配,无论采用哪种方式都能正确失效该查询。

"http2Fallback": "Откат HTTP/2",
"clientError": "Ошибка клиента"
"clientError": "Ошибка клиента",
"endpointPoolExhausted": "Пул эндпоинтов исчерпан"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

术语不一致:「эндпоинт」vs「Конечная точка」

新增的所有翻译键均使用音译词 «эндпоинт»(如 Line 41、67-68、193-200),但同一文件中 Line 73 已有的 details.endpoint 键使用的是正式俄语翻译 «Конечная точка»。建议在整个文件中统一术语,避免用户在同一界面看到两种不同的表述。

Also applies to: 67-68, 193-200

🤖 Prompt for AI Agents
In `@messages/ru/provider-chain.json` at line 41, The new translation uses the
transliterated term "эндпоинт" for keys like "endpointPoolExhausted" (and the
other new keys around lines 67-68 and 193-200), but the file already uses the
formal Russian term "Конечная точка" in the existing key "details.endpoint";
update the new keys to use the same formal term ("Конечная точка") so
terminology is consistent across the file, and run a quick grep/scan for any
other occurrences of "эндпоинт" to replace them with "Конечная точка".

Comment on lines +520 to +650
test("reset endpoint circuit action is available when circuit is open", async () => {
// Mock circuit state as open
providerEndpointsActionMocks.batchGetEndpointCircuitInfo.mockResolvedValue({
ok: true,
data: [
{
endpointId: 1,
circuitState: "open",
failureCount: 5,
circuitOpenUntil: Date.now() + 60000,
},
],
});
providerEndpointsActionMocks.resetEndpointCircuit.mockResolvedValue({ ok: true });

const { unmount } = renderWithProviders(
<ProviderVendorView
providers={[makeProviderDisplay()]}
currentUser={ADMIN_USER}
enableMultiProviderTypes={true}
healthStatus={{}}
statistics={{}}
statisticsLoading={false}
currencyCode="USD"
/>
);

await flushTicks(8);

// Verify the circuit state query was called with correct endpoint
expect(providerEndpointsActionMocks.batchGetEndpointCircuitInfo).toHaveBeenCalled();

// Verify circuit badge is shown
expect(document.body.textContent || "").toContain("Circuit Open");

// Call the reset action directly and verify it succeeds
const resetResult = await providerEndpointsActionMocks.resetEndpointCircuit({ endpointId: 1 });
expect(resetResult.ok).toBe(true);

unmount();
});

test("resetEndpointCircuit action is called correctly when circuit is open", async () => {
providerEndpointsActionMocks.batchGetEndpointCircuitInfo.mockResolvedValue({
ok: true,
data: [
{
endpointId: 1,
circuitState: "open",
failureCount: 5,
circuitOpenUntil: Date.now() + 60000,
},
],
});
providerEndpointsActionMocks.resetEndpointCircuit.mockResolvedValue({ ok: true });

const { unmount } = renderWithProviders(
<ProviderVendorView
providers={[makeProviderDisplay()]}
currentUser={ADMIN_USER}
enableMultiProviderTypes={true}
healthStatus={{}}
statistics={{}}
statisticsLoading={false}
currencyCode="USD"
/>
);

await flushTicks(8);

// Verify the circuit state query was called with correct endpoint
expect(providerEndpointsActionMocks.batchGetEndpointCircuitInfo).toHaveBeenCalled();

// Verify circuit badge is shown
expect(document.body.textContent || "").toContain("Circuit Open");

// The reset endpoint circuit action is available and will be called when user clicks
// Verify the mock is properly set up to handle the call
const resetResult = await providerEndpointsActionMocks.resetEndpointCircuit({ endpointId: 1 });
expect(resetResult.ok).toBe(true);
expect(providerEndpointsActionMocks.resetEndpointCircuit).toHaveBeenCalledWith(
expect.objectContaining({ endpointId: 1 })
);

unmount();
});

test("resetEndpointCircuit action handles failure correctly", async () => {
providerEndpointsActionMocks.batchGetEndpointCircuitInfo.mockResolvedValue({
ok: true,
data: [
{
endpointId: 1,
circuitState: "open",
failureCount: 5,
circuitOpenUntil: Date.now() + 60000,
},
],
});
providerEndpointsActionMocks.resetEndpointCircuit.mockResolvedValue({
ok: false,
error: "Failed to reset circuit",
});

const { unmount } = renderWithProviders(
<ProviderVendorView
providers={[makeProviderDisplay()]}
currentUser={ADMIN_USER}
enableMultiProviderTypes={true}
healthStatus={{}}
statistics={{}}
statisticsLoading={false}
currencyCode="USD"
/>
);

await flushTicks(8);

// Verify the circuit state query was called
expect(providerEndpointsActionMocks.batchGetEndpointCircuitInfo).toHaveBeenCalled();

// Verify circuit badge is shown
expect(document.body.textContent || "").toContain("Circuit Open");

// Call the reset action and expect failure
const resetResult = await providerEndpointsActionMocks.resetEndpointCircuit({ endpointId: 1 });
expect(resetResult.ok).toBe(false);
expect(resetResult.error).toBe("Failed to reset circuit");

unmount();
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

重置操作测试仅调用了 mock 函数,未测试实际的 UI 交互。

resetEndpointCircuit 相关的三个测试(成功/失败/关闭状态)都是直接调用 providerEndpointsActionMocks.resetEndpointCircuit(),而非通过点击 UI 中的重置按钮触发。这实际上只验证了 mock 函数本身,没有覆盖"用户点击重置按钮 → 调用 action → 显示结果"的完整交互路径。

建议找到重置按钮元素并模拟点击,验证 resetEndpointCircuit 是否被组件正确调用:

// 示例:通过 UI 触发重置
const resetButton = endpointRow?.querySelector('[aria-label="Reset Circuit"]');
resetButton?.click();
await flushTicks(4);
expect(providerEndpointsActionMocks.resetEndpointCircuit).toHaveBeenCalledWith(
  expect.objectContaining({ endpointId: 1 })
);
🤖 Prompt for AI Agents
In `@tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx`
around lines 520 - 650, Tests for resetEndpointCircuit currently call
providerEndpointsActionMocks.resetEndpointCircuit directly instead of exercising
the UI; update each of the three tests that reference
providerEndpointsActionMocks.resetEndpointCircuit so they locate the reset
button rendered by ProviderVendorView (e.g., find the endpoint row for
endpointId 1 and querySelector by the reset button's aria-label or CSS
selector), simulate a user click on that button, await flushTicks to let the
component call the action, then assert
providerEndpointsActionMocks.resetEndpointCircuit was called with
expect.objectContaining({ endpointId: 1 }) and verify the resulting UI feedback
(success or error) is displayed; use the existing ProviderVendorView render and
flushTicks utilities and the providerEndpointsActionMocks.resetEndpointCircuit
mock to drive expected responses.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

48 files reviewed, 6 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 16 to 18
fields.push({ label: "最后错误", value: data.lastError });
}

Copy link

Choose a reason for hiding this comment

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

Endpoint id dropped
When incidentSource === "endpoint", the endpoint fields are only added if data.endpointId is truthy. If endpointId can be 0 (or any falsy numeric ID), it will be omitted from both the fields and the description, making endpoint alerts ambiguous. Use an explicit undefined check (e.g. data.endpointId !== undefined) instead of a truthiness check.

Also applies to endpointId use in the description string in this function.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/webhook/templates/circuit-breaker.ts
Line: 16:18

Comment:
**Endpoint id dropped**
When `incidentSource === "endpoint"`, the endpoint fields are only added if `data.endpointId` is truthy. If `endpointId` can be `0` (or any falsy numeric ID), it will be omitted from both the fields and the description, making endpoint alerts ambiguous. Use an explicit undefined check (e.g. `data.endpointId !== undefined`) instead of a truthiness check.

Also applies to `endpointId` use in the description string in this function.

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

Comment on lines 26 to 28
const redisClient = getRedisClient();
const source = data.incidentSource ?? "provider";
const dedupSuffix = source === "endpoint" ? `endpoint:${data.endpointId}` : source;
Copy link

Choose a reason for hiding this comment

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

Dedup key collision
The new dedup suffix for endpoint incidents uses `endpoint:${data.endpointId}` even when endpointId is missing/undefined. That will collapse all endpoint alerts without an endpointId into the same Redis key (...:endpoint:undefined), suppressing unrelated alerts for 5 minutes. If endpoint incidents are possible without an ID, incorporate endpointUrl (or require endpointId when incidentSource === "endpoint") to avoid accidental suppression.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/notification/notifier.ts
Line: 26:28

Comment:
**Dedup key collision**
The new dedup suffix for endpoint incidents uses `` `endpoint:${data.endpointId}` `` even when `endpointId` is missing/undefined. That will collapse all endpoint alerts without an `endpointId` into the same Redis key (`...:endpoint:undefined`), suppressing unrelated alerts for 5 minutes. If endpoint incidents are possible without an ID, incorporate `endpointUrl` (or require `endpointId` when `incidentSource === "endpoint"`) to avoid accidental suppression.

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

Comment on lines +503 to +505
}

// Collect endpoint filter stats for no_endpoint_candidates (selector_error has no data)
Copy link

Choose a reason for hiding this comment

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

Undefined stats leaked
endpointFilterStats: filterStats is passed to session.addProviderToChain(...) even when filterStats was never assigned (e.g., getEndpointFilterStats throws). This will emit endpointFilterStats: undefined into the chain item payload, which can break downstream formatting/rendering code that assumes the object exists when the field is present. Consider only including endpointFilterStats when stats were successfully collected (or explicitly set it to null and handle that consistently).

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

Comment:
**Undefined stats leaked**
`endpointFilterStats: filterStats` is passed to `session.addProviderToChain(...)` even when `filterStats` was never assigned (e.g., `getEndpointFilterStats` throws). This will emit `endpointFilterStats: undefined` into the chain item payload, which can break downstream formatting/rendering code that assumes the object exists when the field is present. Consider only including `endpointFilterStats` when stats were successfully collected (or explicitly set it to `null` and handle that consistently).

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

Comment on lines 722 to 724
// 端点过滤统计
if (item.endpointFilterStats) {
const stats = item.endpointFilterStats;
Copy link

Choose a reason for hiding this comment

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

Stats fields may be undefined
endpointFilterStats is treated as fully populated when present, but the producer can attach endpointFilterStats: undefined (or a partial object) on failure to collect stats. That will render translations with count: undefined (e.g. "Total Endpoints: undefined"). Consider guarding that the required numeric fields exist before rendering, or ensure the chain item never includes the field unless stats were successfully collected.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/utils/provider-chain-formatter.ts
Line: 722:724

Comment:
**Stats fields may be undefined**
`endpointFilterStats` is treated as fully populated when present, but the producer can attach `endpointFilterStats: undefined` (or a partial object) on failure to collect stats. That will render translations with `count: undefined` (e.g. "Total Endpoints: undefined"). Consider guarding that the required numeric fields exist before rendering, or ensure the chain item never includes the field unless stats were successfully collected.

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

Comment on lines 111 to 113
const endpoints = endpointCircuitInfo[providerId];
if (endpoints?.some((ep) => ep.circuitState === "open")) {
return true;
Copy link

Choose a reason for hiding this comment

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

Type assumption on endpoints
endpoints?.some(...) assumes endpointCircuitInfo[providerId] is an array when defined. If the data source ever returns a non-array value (e.g. null or an object due to API/serialization issues), this will throw at runtime. A defensive Array.isArray(endpoints) check avoids crashing the settings page on bad data.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/provider-manager.tsx
Line: 111:113

Comment:
**Type assumption on endpoints**
`endpoints?.some(...)` assumes `endpointCircuitInfo[providerId]` is an array when defined. If the data source ever returns a non-array value (e.g. `null` or an object due to API/serialization issues), this will throw at runtime. A defensive `Array.isArray(endpoints)` check avoids crashing the settings page on bad data.

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

Comment on lines 250 to 252
await sendCircuitBreakerAlert({
providerId: endpointId, // Use endpointId as providerId for dedup purposes
providerName: "",
Copy link

Choose a reason for hiding this comment

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

Wrong providerId semantics
Endpoint circuit alerts call sendCircuitBreakerAlert with providerId: endpointId (comment says for dedup), but downstream templates and logs treat providerId as the provider's identifier. This makes endpoint alerts misleading (wrong providerId) and also skews dedup keys/counters. Pass the real providerId (and keep dedup uniqueness via incidentSource + endpointId) rather than overloading providerId with endpointId.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/endpoint-circuit-breaker.ts
Line: 250:252

Comment:
**Wrong providerId semantics**
Endpoint circuit alerts call `sendCircuitBreakerAlert` with `providerId: endpointId` (comment says for dedup), but downstream templates and logs treat `providerId` as the provider's identifier. This makes endpoint alerts misleading (wrong providerId) and also skews dedup keys/counters. Pass the real providerId (and keep dedup uniqueness via `incidentSource` + `endpointId`) rather than overloading `providerId` with `endpointId`.

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

if (endpoint) {
endpointUrl = endpoint.url;
}
} catch {
Copy link
Contributor

Choose a reason for hiding this comment

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

[Critical] [ERROR-SWALLOWED] Empty catch silently ignores alert enrichment failures

File: src/lib/endpoint-circuit-breaker.ts:246

Why this is a problem: The DB lookup failure is swallowed:

} catch {
  // DB lookup failure should not block alert
}

This makes production failures (DB outage/query bug/permission issue) invisible and removes evidence when endpoint alerts are missing endpointUrl.

Suggested fix:

} catch (error) {
  logger.warn("[EndpointCircuitBreaker] Failed to enrich alert with endpoint info", {
    endpointId,
    error: error instanceof Error ? error.message : String(error),
  });
}


await sendCircuitBreakerAlert({
providerId: endpointId, // Use endpointId as providerId for dedup purposes
providerName: "",
Copy link
Contributor

Choose a reason for hiding this comment

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

[High] [LOGIC-BUG] Endpoint circuit breaker alerts lose provider identity (providerName: "")

File: src/lib/endpoint-circuit-breaker.ts:252

Why this is a problem: The alert payload is sent as:

await sendCircuitBreakerAlert({
  providerId: endpointId,
  providerName: "",
  incidentSource: "endpoint",
  endpointId,
  endpointUrl,
});

But the circuit-breaker template renders endpoint incidents as:

`供应商 ${data.providerName} 的端点 (ID: ${data.endpointId}) 已触发熔断保护`

(src/lib/webhook/templates/circuit-breaker.ts:31) so the notification will show an empty provider name and a misleading providerId for endpoint incidents.

Suggested fix:

const { findProviderEndpointById, findProviderVendorById } = await import("@/repository");

const endpoint = await findProviderEndpointById(endpointId);
const vendor = endpoint ? await findProviderVendorById(endpoint.vendorId) : null;

const providerId = endpoint?.vendorId ?? endpointId;
const providerName =
  vendor?.displayName ?? vendor?.websiteDomain ?? endpoint?.url ?? String(providerId);

await sendCircuitBreakerAlert({
  providerId,
  providerName,
  failureCount,
  retryAt,
  lastError,
  incidentSource: "endpoint",
  endpointId,
  endpointUrl: endpoint?.url,
});

"doForward"
);

await ProxyForwarder.send(session).catch(() => {});
Copy link
Contributor

Choose a reason for hiding this comment

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

[Medium] [TEST-INCOMPLETE] Swallowing ProxyForwarder.send() rejection hides regressions

File: tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts:535

Why this is a problem: This test currently does:

await ProxyForwarder.send(session).catch(() => {});

That silences the failure mode completely (it will keep passing even if send() starts throwing an unexpected error, or starts resolving when it should reject). The earlier test in this file already asserts rejection explicitly; these new cases should do the same.

Suggested fix:

await expect(ProxyForwarder.send(session)).rejects.toThrow();

Apply similarly to the other swallowed rejections in this file (:590, :634, :662, :699).

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

This PR is XL and mixes proxy forwarding behavior changes, circuit breaker alerting/template changes, UI changes, and test updates into one diff, which increases regression and review risk. The highest-risk issues are in the new endpoint circuit breaker alert path: one new catch {} swallows DB failures without logging, and endpoint alerts are emitted with an empty provider identity, producing unclear notifications.

PR Size: XL

  • Lines changed: 2648
  • Files changed: 48
  • Split suggestion (recommended for follow-ups):
    • PR 1: Endpoint circuit breaker alerting + webhook types/templates/dedup key changes
    • PR 2: ProxyForwarder strict endpoint pool behavior + provider-chain audit fields + formatter updates
    • PR 3: Settings UI changes + i18n message updates
    • PR 4: Tests-only stabilization/coverage adjustments

Issues Found

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

Critical Issues (Must Fix)

  • (Confidence 95-100) [ERROR-SWALLOWED] Empty catch silently ignores DB lookup failures during endpoint alert enrichment (src/lib/endpoint-circuit-breaker.ts:246).
  • (Confidence 95-100) [LOGIC-BUG] Endpoint circuit breaker alerts send providerName: "" and use providerId: endpointId, producing unclear notifications (src/lib/endpoint-circuit-breaker.ts:252, see template usage in src/lib/webhook/templates/circuit-breaker.ts:31).

High Priority Issues (Should Fix)

  • (Confidence 80-94) [TEST-INCOMPLETE] New tests swallow ProxyForwarder.send() rejections via .catch(() => {}), allowing false positives (tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts:535, also :590, :634, :662, :699).

Review Coverage

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

Automated review by Codex AI

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

  • Identified PR #755 (“feat(circuit): unify provider-endpoint circuit visibility and notifications”) and applied the size/XL label.
  • Posted 3 inline review comments on new/modified diff lines:
    • src/lib/endpoint-circuit-breaker.ts:246[Critical] [ERROR-SWALLOWED] empty catch {} hides DB lookup failures during alert enrichment.
    • src/lib/endpoint-circuit-breaker.ts:252[High] [LOGIC-BUG] endpoint circuit alerts send providerName: "" / misleading providerId, producing unclear notifications.
    • tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts:535[Medium] [TEST-INCOMPLETE] .catch(() => {}) swallows ProxyForwarder.send() rejections (also at :590, :634, :662, :699).
  • Submitted the required PR review summary via gh pr review --comment.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

No significant issues identified in this PR. The implementation is well-structured with comprehensive test coverage across all new functionality. The 6-perspective review (logic, security, error handling, types, documentation, tests) found no issues meeting the confidence threshold for reporting.

PR Size: XL

  • Lines changed: 2648 (2599 additions, 49 deletions)
  • Files changed: 48

Split suggestion (XL PR): While the PR is large, the changes are cohesive around a single feature (endpoint circuit breaker visibility). The bulk of the additions are i18n strings (20 JSON files) and test files (11 test files with ~1200 lines). The core logic changes span ~400 lines across 9 source files, which is reasonable for a unified feature. No split is strictly necessary, but if desired, the notification/webhook changes could be separated from the UI changes.

Reviewed Areas

Potential concerns investigated and cleared:

  1. providerId: endpointId in triggerEndpointCircuitBreakerAlert (src/lib/endpoint-circuit-breaker.ts): The comment documents this is intentional for dedup purposes. The new dedup key format circuit-breaker-alert:{id}:{source}:{endpointId} in the notifier correctly differentiates provider vs endpoint alerts, preventing key collisions. The webhook template uses data.endpointId directly for endpoint alerts, so the display is correct.

  2. Dedup key with undefined endpointId (src/lib/notification/notifier.ts): The triggerEndpointCircuitBreakerAlert function always passes a concrete endpointId, so the dedup suffix endpoint:${data.endpointId} will always have a valid value in the endpoint alert path. Provider-level alerts use the provider suffix and don't touch endpointId.

  3. endpointFilterStats undefined in provider chain (src/app/v1/_lib/proxy/forwarder.ts): The formatter in provider-chain-formatter.ts guards with if (item.endpointFilterStats) before rendering stats. The test exhaustedNoStats explicitly validates graceful degradation when stats are missing. The forwarder correctly handles getEndpointFilterStats failures by logging a warning and leaving filterStats as undefined.

  4. batchGetEndpointCircuitInfo unbounded array: The function requires admin session and is called from the UI with endpoint IDs for a single provider. While there's no explicit .max() on the array schema, this matches the existing pattern in the codebase where admin-only batch operations don't impose artificial limits.

  5. Empty catch block (src/lib/endpoint-circuit-breaker.ts): The } catch { // DB lookup failure should not block alert } pattern is consistent with 20+ similar instances in the codebase for non-critical enrichment operations.

  6. if (data.endpointId) falsy check (src/lib/webhook/templates/circuit-breaker.ts): EndpointIdSchema requires z.number().int().positive(), so endpoint IDs are always >= 1. The falsy check is safe.

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Comprehensive (11 test files, covering all new functions)
  • Code clarity - Good

Automated review by Claude AI

- EN/RU i18n: fix inconsistent endpoint terminology and missing "Broken" label
- provider-endpoints: add .max(500) bound to batch query schema
- provider-vendor-view: remove unused endpointCircuitInfo prop
- forwarder: remove unsafe decisionContext cast, use typed errorMessage field
- endpoint-circuit-breaker: fix providerId semantics (use vendorId)
- notifier: add null check for endpointId in dedup key
- circuit-breaker template: use strict !== undefined check, add N/A fallback
- provider-manager: use Array.isArray guard for endpoint circuit check
- provider-chain-formatter: add typeof number guard for stats
- tests: fix assertions to match updated API contracts
@github-actions
Copy link
Contributor

🧪 测试结果

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

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

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

10 files reviewed, 6 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 123 to +126
manualOpen: z.boolean(),
});

const BatchGetEndpointCircuitInfoSchema = z.object({
Copy link

Choose a reason for hiding this comment

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

Invalid Zod constraint
BatchGetEndpointCircuitInfoSchema uses z.array(...).min(0), but min(0) is redundant and may not behave as intended (Zod array min is for length; 0 is already the default). If you want to allow empty arrays, you can drop .min(0) and keep .max(500), or set .min(1) if empties should be rejected.

Also, batchGetEndpointCircuitInfo() currently returns a Chinese permission error ("无权限执行此操作") in an otherwise English/locale-agnostic server action; this will surface inconsistent UX in non-zh locales. Consider using the project’s standard error message constant / i18n strategy for server actions.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/provider-endpoints.ts
Line: 123:126

Comment:
**Invalid Zod constraint**
`BatchGetEndpointCircuitInfoSchema` uses `z.array(...).min(0)`, but `min(0)` is redundant and may not behave as intended (Zod array min is for length; 0 is already the default). If you want to allow empty arrays, you can drop `.min(0)` and keep `.max(500)`, or set `.min(1)` if empties should be rejected.

Also, `batchGetEndpointCircuitInfo()` currently returns a Chinese permission error (`"无权限执行此操作"`) in an otherwise English/locale-agnostic server action; this will surface inconsistent UX in non-zh locales. Consider using the project’s standard error message constant / i18n strategy for server actions.

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

Comment on lines +573 to +589

const parsed = BatchGetEndpointCircuitInfoSchema.safeParse(input);
if (!parsed.success) {
return {
ok: false,
error: formatZodError(parsed.error),
errorCode: extractZodErrorCode(parsed.error),
};
}

if (parsed.data.endpointIds.length === 0) {
return { ok: true, data: [] };
}

const results = await Promise.all(
parsed.data.endpointIds.map(async (endpointId) => {
const { health } = await getEndpointHealthInfo(endpointId);
Copy link

Choose a reason for hiding this comment

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

N+1 health lookups
batchGetEndpointCircuitInfo() does Promise.all(endpointIds.map(getEndpointHealthInfo)), which becomes an N+1 pattern against whatever backing store getEndpointHealthInfo uses (often Redis/DB). With up to 500 endpoints, this can add significant latency to the settings page and increase load. This needs a real batch read path (e.g., multi-get in Redis / single query) or a lower hard cap with pagination, otherwise the “batch” action is still effectively per-endpoint.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/provider-endpoints.ts
Line: 573:589

Comment:
**N+1 health lookups**
`batchGetEndpointCircuitInfo()` does `Promise.all(endpointIds.map(getEndpointHealthInfo))`, which becomes an N+1 pattern against whatever backing store `getEndpointHealthInfo` uses (often Redis/DB). With up to 500 endpoints, this can add significant latency to the settings page and increase load. This needs a real batch read path (e.g., multi-get in Redis / single query) or a lower hard cap with pagination, otherwise the “batch” action is still effectively per-endpoint.

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

Comment on lines +60 to +83
available: number;
}

/**
* Collect endpoint filter statistics for a given vendor/type.
*
* Used for audit trail when all endpoints are exhausted (strict block).
* Returns null only when the raw endpoint query itself fails.
*/
export async function getEndpointFilterStats(input: {
vendorId: number;
providerType: ProviderType;
}): Promise<EndpointFilterStats> {
const endpoints = await findProviderEndpointsByVendorAndType(input.vendorId, input.providerType);
const total = endpoints.length;
const enabled = endpoints.filter((e) => e.isEnabled && !e.deletedAt).length;

const circuitResults = await Promise.all(
endpoints
.filter((e) => e.isEnabled && !e.deletedAt)
.map(async (e) => isEndpointCircuitOpen(e.id))
);
const circuitOpen = circuitResults.filter(Boolean).length;
const available = enabled - circuitOpen;
Copy link

Choose a reason for hiding this comment

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

Circuit stats can be expensive
getEndpointFilterStats() calls isEndpointCircuitOpen(e.id) for every enabled endpoint via Promise.all. In cases with many endpoints, this adds a burst of per-endpoint circuit checks right on the strict-block path, which can amplify latency/load during incidents (exactly when strict blocks happen). This should be implemented as a batched circuit-state read (or reuse the same data already computed in selection) to avoid hammering the circuit store.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/provider-endpoints/endpoint-selector.ts
Line: 60:83

Comment:
**Circuit stats can be expensive**
`getEndpointFilterStats()` calls `isEndpointCircuitOpen(e.id)` for every enabled endpoint via `Promise.all`. In cases with many endpoints, this adds a burst of per-endpoint circuit checks right on the strict-block path, which can amplify latency/load during incidents (exactly when strict blocks happen). This should be implemented as a batched circuit-state read (or reuse the same data already computed in selection) to avoid hammering the circuit store.

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

Comment on lines 130 to 144
});
}, [rawEndpoints]);

// Fetch circuit breaker states for all endpoints in batch
const endpointIds = useMemo(() => endpoints.map((ep) => ep.id), [endpoints]);
const { data: circuitInfoMap = {} } = useQuery({
queryKey: ["endpoint-circuit-info", ...endpointIds],
queryFn: async () => {
if (endpointIds.length === 0) return {};
const res = await batchGetEndpointCircuitInfo({ endpointIds });
if (!res.ok || !res.data) return {};
const map: Record<number, EndpointCircuitState> = {};
for (const item of res.data) {
map[item.endpointId] = item.circuitState as EndpointCircuitState;
}
Copy link

Choose a reason for hiding this comment

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

Unstable queryKey growth
The query key is ["endpoint-circuit-info", ...endpointIds]. If endpointIds order changes between renders (e.g., sorting/filtering endpoints), React Query will treat it as a different query and refetch unnecessarily (and potentially cache multiple equivalent entries). Prefer a stable key such as ["endpoint-circuit-info", vendorId] or ["endpoint-circuit-info", endpointIds.sort().join(",")] so the cache is order-insensitive.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx
Line: 130:144

Comment:
**Unstable queryKey growth**
The query key is `["endpoint-circuit-info", ...endpointIds]`. If `endpointIds` order changes between renders (e.g., sorting/filtering endpoints), React Query will treat it as a different query and refetch unnecessarily (and potentially cache multiple equivalent entries). Prefer a stable key such as `["endpoint-circuit-info", vendorId]` or `["endpoint-circuit-info", endpointIds.sort().join(",")]` so the cache is order-insensitive.

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

Comment on lines 280 to +296
},
});

const resetCircuitMutation = useMutation({
mutationFn: async () => {
const res = await resetEndpointCircuit({ endpointId: endpoint.id });
if (!res.ok) throw new Error(res.error);
return res;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["endpoint-circuit-info"] });
queryClient.invalidateQueries({ queryKey: ["provider-endpoints"] });
toast.success(tStatus("resetCircuitSuccess"));
},
onError: () => {
toast.error(tStatus("resetCircuitFailed"));
},
Copy link

Choose a reason for hiding this comment

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

InvalidateQueries key mismatch
resetCircuitMutation invalidates { queryKey: ["endpoint-circuit-info"] }, but the actual circuit info query uses a key shaped like ["endpoint-circuit-info", ...endpointIds]. Depending on React Query’s exact partial-match behavior here, this may not invalidate the active query, leaving stale circuit badges after reset.

To be safe, invalidate with predicate/exact: false (if supported) or match the full key shape, e.g. invalidate all keys starting with "endpoint-circuit-info".

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx
Line: 280:296

Comment:
**InvalidateQueries key mismatch**
`resetCircuitMutation` invalidates `{ queryKey: ["endpoint-circuit-info"] }`, but the actual circuit info query uses a key shaped like `["endpoint-circuit-info", ...endpointIds]`. Depending on React Query’s exact partial-match behavior here, this may not invalidate the active query, leaving stale circuit badges after reset.

To be safe, invalidate with `predicate`/`exact: false` (if supported) or match the full key shape, e.g. invalidate all keys starting with `"endpoint-circuit-info"`.

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

Comment on lines 121 to +145
borderColor: "border-slate-400/30",
};
}

/**
* Resolves the display status for an endpoint with unified semantics.
*
* Priority order:
* 1. circuit-open (priority 0) - Circuit breaker has opened
* 2. circuit-half-open (priority 1) - Circuit breaker is testing recovery
* 3. enabled (priority 2) - Circuit closed and endpoint is enabled
* 4. disabled (priority 3) - Circuit closed but endpoint is disabled
*
* @param endpoint - Endpoint data with optional isEnabled property
* @param circuitState - Current circuit breaker state
* @returns Display status with source indicator and priority
*/
export function resolveEndpointDisplayStatus(
endpoint: { lastProbeOk: boolean | null; isEnabled?: boolean | null },
circuitState?: EndpointCircuitState | null
): EndpointDisplayStatus {
// Priority 0: Circuit Open
if (circuitState === "open") {
return {
status: "circuit-open",
Copy link

Choose a reason for hiding this comment

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

Incorrect incident source
resolveEndpointDisplayStatus() always returns source: "endpoint", even for the enabled/disabled cases where there is no circuit incident. This makes the “incident source” metadata misleading (it will label normal enabled/disabled states as endpoint incidents).

If source is intended to mean “which subsystem determined the status”, this should likely be "provider" or omitted for non-incident states, or only set to "endpoint" when circuitState is open|half-open.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/endpoint-status.ts
Line: 121:145

Comment:
**Incorrect incident source**
`resolveEndpointDisplayStatus()` always returns `source: "endpoint"`, even for the `enabled`/`disabled` cases where there is no circuit incident. This makes the “incident source” metadata misleading (it will label normal enabled/disabled states as endpoint incidents).

If `source` is intended to mean “which subsystem determined the status”, this should likely be `"provider"` or omitted for non-incident states, or only set to `"endpoint"` when `circuitState` is `open|half-open`.

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tests/unit/settings/providers/provider-manager.test.tsx`:
- Around line 389-440: The test "passes endpointCircuitInfo to ProviderList for
rendering" currently only asserts the broken count text and does not verify
endpointCircuitInfo is forwarded; update the ProviderList mock (the mock defined
around the ProviderList mock block) to capture props (e.g., record the last call
into providerListProps or similar via a jest.fn or variable) and in this test
assert that ProviderList was invoked with an objectContaining({
endpointCircuitInfo: expect.any(Object) }) or a direct equality check against
the endpointCircuitInfo passed to ProviderManager; reference ProviderManager,
the ProviderList mock, and the endpointCircuitInfo fixture when making these
changes.
🧹 Nitpick comments (4)
src/lib/endpoint-circuit-breaker.ts (1)

250-252: catch 块吞掉了 DB 查询异常,建议加日志。

DB 查询失败时完全静默处理,不利于排查端点数据丢失或数据库连接问题。建议至少记录一条 warn 级别日志,与文件中其他 Redis 错误处理风格保持一致(如 Line 82-85)。

建议修复
-    } catch {
-      // DB lookup failure should not block alert
+    } catch (enrichError) {
+      logger.warn({
+        action: "endpoint_circuit_alert_enrich_failed",
+        endpointId,
+        error: enrichError instanceof Error ? enrichError.message : String(enrichError),
+      });
     }
src/app/[locale]/settings/providers/_components/provider-manager.tsx (1)

619-629: ProviderVendorView 未接收 endpointCircuitInfo

列表视图(ProviderList)传递了 endpointCircuitInfo,但厂商视图(ProviderVendorView)没有。如果后续需要在厂商视图中展示端点级熔断状态,需要补充此 prop。如果当前是有意为之,可以忽略。

tests/unit/settings/providers/provider-manager.test.tsx (2)

116-136: renderWithProviders 使用原生 createRoot 而非 @testing-library/react

当前实现可以工作,但使用 @testing-library/reactrender 方法可以自动处理清理、提供更丰富的查询 API(如 getByRolegetByText),并且更符合 React 测试生态的惯例。当前方案在 beforeEach 中手动清理 DOM,也是可以接受的。


162-186: 基于文本内容的断言方式较脆弱。

多个测试通过 expect(text).toContain("(1)") 来验证 circuit broken count,这种方式依赖渲染文本中恰好出现特定字符串。虽然在当前高度 mock 化的环境下不太可能产生误判,但如果后续 UI 增加了包含相同数字格式的内容(如 "(1 provider)" 或分页信息),断言可能会产生假阳性。

如果未来发现测试不稳定,可以考虑给 circuit broken count 元素添加 data-testid 并直接断言其内容。

Also applies to: 236-238, 282-284, 323-325, 435-437

Comment on lines 389 to 440
test("passes endpointCircuitInfo to ProviderList for rendering", () => {
const healthStatus = {
1: {
circuitState: "open" as const,
failureCount: 5,
lastFailureTime: Date.now(),
circuitOpenUntil: Date.now() + 60000,
recoveryMinutes: 1,
},
3: {
circuitState: "open" as const,
failureCount: 3,
lastFailureTime: Date.now(),
circuitOpenUntil: Date.now() + 30000,
recoveryMinutes: 0.5,
},
};

const endpointCircuitInfo = {
2: [
{
endpointId: 20,
circuitState: "open" as const,
failureCount: 2,
circuitOpenUntil: Date.now() + 60000,
},
],
3: [
{
endpointId: 30,
circuitState: "open" as const,
failureCount: 4,
circuitOpenUntil: Date.now() + 60000,
},
],
};

const { unmount, container } = renderWithProviders(
<ProviderManager
providers={providers}
healthStatus={healthStatus}
endpointCircuitInfo={endpointCircuitInfo}
enableMultiProviderTypes={true}
/>
);

// The circuit broken count should be 3 (all three providers have some form of circuit open)
const text = container.textContent || "";
expect(text).toContain("(3)");

unmount();
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

测试名称 "passes endpointCircuitInfo to ProviderList" 与实际断言不符。

此测试名声称验证 endpointCircuitInfo 是否传递给了 ProviderList,但实际上只断言了 circuit broken count 的文本内容 "(3)"。由于 ProviderList 的 mock(Line 28-38)并未捕获或断言 endpointCircuitInfo prop,无法真正验证 prop 传递。

建议:要么修改测试名称更准确地描述其行为(如 "counts all providers with any circuit open for layered labels"),要么让 mock 捕获 props 并断言 endpointCircuitInfo 确实被传递。

方案一:修改测试名称
-  test("passes endpointCircuitInfo to ProviderList for rendering", () => {
+  test("counts all providers with any form of open circuit", () => {
方案二:在 mock 中捕获 props 并断言
+const providerListProps = vi.fn();
+
 vi.mock("@/app/[locale]/settings/providers/_components/provider-list", () => ({
-  ProviderList: ({ providers }: { providers: ProviderDisplay[] }) => (
-    <ul data-testid="provider-list">
-      {providers.map((p) => (
-        <li key={p.id} data-testid={`provider-${p.id}`}>
-          {p.name}
-        </li>
-      ))}
-    </ul>
-  ),
+  ProviderList: (props: { providers: ProviderDisplay[]; endpointCircuitInfo?: Record<number, unknown[]> }) => {
+    providerListProps(props);
+    return (
+      <ul data-testid="provider-list">
+        {props.providers.map((p) => (
+          <li key={p.id} data-testid={`provider-${p.id}`}>
+            {p.name}
+          </li>
+        ))}
+      </ul>
+    );
+  },
 }));

然后在测试中添加:

expect(providerListProps).toHaveBeenCalledWith(
  expect.objectContaining({ endpointCircuitInfo: expect.any(Object) })
);
📝 Committable suggestion

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

Suggested change
test("passes endpointCircuitInfo to ProviderList for rendering", () => {
const healthStatus = {
1: {
circuitState: "open" as const,
failureCount: 5,
lastFailureTime: Date.now(),
circuitOpenUntil: Date.now() + 60000,
recoveryMinutes: 1,
},
3: {
circuitState: "open" as const,
failureCount: 3,
lastFailureTime: Date.now(),
circuitOpenUntil: Date.now() + 30000,
recoveryMinutes: 0.5,
},
};
const endpointCircuitInfo = {
2: [
{
endpointId: 20,
circuitState: "open" as const,
failureCount: 2,
circuitOpenUntil: Date.now() + 60000,
},
],
3: [
{
endpointId: 30,
circuitState: "open" as const,
failureCount: 4,
circuitOpenUntil: Date.now() + 60000,
},
],
};
const { unmount, container } = renderWithProviders(
<ProviderManager
providers={providers}
healthStatus={healthStatus}
endpointCircuitInfo={endpointCircuitInfo}
enableMultiProviderTypes={true}
/>
);
// The circuit broken count should be 3 (all three providers have some form of circuit open)
const text = container.textContent || "";
expect(text).toContain("(3)");
unmount();
});
test("counts all providers with any form of open circuit", () => {
const healthStatus = {
1: {
circuitState: "open" as const,
failureCount: 5,
lastFailureTime: Date.now(),
circuitOpenUntil: Date.now() + 60000,
recoveryMinutes: 1,
},
3: {
circuitState: "open" as const,
failureCount: 3,
lastFailureTime: Date.now(),
circuitOpenUntil: Date.now() + 30000,
recoveryMinutes: 0.5,
},
};
const endpointCircuitInfo = {
2: [
{
endpointId: 20,
circuitState: "open" as const,
failureCount: 2,
circuitOpenUntil: Date.now() + 60000,
},
],
3: [
{
endpointId: 30,
circuitState: "open" as const,
failureCount: 4,
circuitOpenUntil: Date.now() + 60000,
},
],
};
const { unmount, container } = renderWithProviders(
<ProviderManager
providers={providers}
healthStatus={healthStatus}
endpointCircuitInfo={endpointCircuitInfo}
enableMultiProviderTypes={true}
/>
);
// The circuit broken count should be 3 (all three providers have some form of circuit open)
const text = container.textContent || "";
expect(text).toContain("(3)");
unmount();
});
🤖 Prompt for AI Agents
In `@tests/unit/settings/providers/provider-manager.test.tsx` around lines 389 -
440, The test "passes endpointCircuitInfo to ProviderList for rendering"
currently only asserts the broken count text and does not verify
endpointCircuitInfo is forwarded; update the ProviderList mock (the mock defined
around the ProviderList mock block) to capture props (e.g., record the last call
into providerListProps or similar via a jest.fn or variable) and in this test
assert that ProviderList was invoked with an objectContaining({
endpointCircuitInfo: expect.any(Object) }) or a direct equality check against
the endpointCircuitInfo passed to ProviderManager; reference ProviderManager,
the ProviderList mock, and the endpointCircuitInfo fixture when making these
changes.

- Remove redundant .min(0) from zod array schema
- Fix endpoint status source to "provider" when no circuit incident
- Stabilize React Query key by sorting endpoint IDs
- Log endpoint info lookup failures instead of silently swallowing
- Replace .catch(() => {}) with expect().rejects.toThrow() in tests
- Improve test descriptions for clarity
@ding113 ding113 merged commit 7300247 into dev Feb 10, 2026
8 of 9 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Feb 10, 2026
@github-actions
Copy link
Contributor

🧪 测试结果

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

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@messages/ru/provider-chain.json`:
- Line 41: Replace the grammatically incorrect Russian phrase "конечная точкаов"
with the proper genitive plural "конечных точек" in this locale file;
specifically update the value for the endpointPoolExhausted key (and any other
keys showing "конечная точкаов" on lines noted) so the string becomes "Пул
конечных точек исчерпан" (or the equivalent correct form where used), and search
the file for any other occurrences of "конечная точкаов" to correct them all.

In `@src/actions/provider-endpoints.ts`:
- Around line 587-597: The batch endpoint health aggregation uses Promise.all
which fails the whole batch if any getEndpointHealthInfo(endpointId) rejects;
change to Promise.allSettled over parsed.data.endpointIds, then filter for
entries with status === "fulfilled", map their values to the existing shape ({
endpointId, circuitState, failureCount, circuitOpenUntil }) and discard or log
rejected entries so a single failure doesn't abort the entire results array;
ensure the variable names (parsed.data.endpointIds, getEndpointHealthInfo,
results) are preserved.

In `@tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts`:
- Around line 601-603: The test documents that the "selector_error" path should
NOT call getEndpointFilterStats but lacks an explicit mock call assertion;
update the selector_error case in proxy-forwarder-endpoint-audit.test.ts to
assert the mocked getEndpointFilterStats was not invoked (e.g.,
expect(getEndpointFilterStatsMock).not.toHaveBeenCalled() or
toHaveBeenCalledTimes(0)) after inspecting exhaustedItem for selector_error,
referencing the existing mock defined near the top of the test file and the
exhaustedItem variable to ensure the behavior is enforced rather than relying on
default mock return values.
🧹 Nitpick comments (10)
src/lib/notification/notifier.ts (1)

49-113: Redis 可用/不可用两条路径的通知发送逻辑完全重复,可考虑提取公共函数。

if (redisClient) 两个分支中,legacy mode 判断、bindings 查询、addNotificationJob / addNotificationJobForTarget 调用逻辑完全一致(lines 49-76 与 86-113),唯一差别是 Redis 路径多了 dedup 读写。提取为一个内部 helper 可以消除 ~30 行重复,也降低后续维护时两处逻辑不同步的风险。

重构示意
+async function dispatchCircuitBreakerAlert(
+  settings: NotificationSettings,
+  data: CircuitBreakerAlertData,
+): Promise<void> {
+  const { addNotificationJob, addNotificationJobForTarget } = await import(
+    "./notification-queue"
+  );
+
+  if (settings.useLegacyMode) {
+    if (!settings.circuitBreakerWebhook) {
+      logger.info({
+        action: "circuit_breaker_alert_disabled",
+        providerId: data.providerId,
+        reason: "legacy_webhook_missing",
+      });
+      return;
+    }
+    await addNotificationJob("circuit-breaker", settings.circuitBreakerWebhook, data);
+  } else {
+    const { getEnabledBindingsByType } = await import("@/repository/notification-bindings");
+    const bindings = await getEnabledBindingsByType("circuit_breaker");
+    if (bindings.length === 0) {
+      logger.info({
+        action: "circuit_breaker_alert_skipped",
+        providerId: data.providerId,
+        reason: "no_bindings",
+      });
+      return;
+    }
+    for (const binding of bindings) {
+      await addNotificationJobForTarget("circuit-breaker", binding.targetId, binding.id, data);
+    }
+  }
+}

然后在两个分支中调用 await dispatchCircuitBreakerAlert(settings, data) 即可。

tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts (2)

609-660: 可选优化:考虑拆分为独立测试用例。

当前将 selector_errorno_endpoint_candidates 两个场景合并在一个 test() 中。若第一个场景(session1)断言失败,第二个场景(session2)将不会执行,降低了可调试性。可考虑拆分为两个独立的 test case,或使用 test.each 参数化。

此外,与上面相同,session1 的 selector_error 路径(第 627 行)也缺少 expect(mocks.getEndpointFilterStats).not.toHaveBeenCalled() 断言。


662-691: 降级场景覆盖良好,建议补充日志验证。

测试正确验证了 getEndpointFilterStats 失败时流程不中断、endpointFilterStats 为 undefined 的行为。生产代码确实在失败时调用 logger.warn("[ProxyForwarder] Failed to collect endpoint filter stats", {...}),但当前测试未验证此日志记录。可参考同文件其他测试(如第434行)的模式,补充断言:expect(logger.warn).toHaveBeenCalledWith("[ProxyForwarder] Failed to collect endpoint filter stats", expect.objectContaining({...})),确保故障被正确记录而非静默吞掉。

src/app/[locale]/settings/providers/_components/endpoint-status.ts (2)

32-36: EndpointDisplayStatus.status 建议使用联合类型替代 string

resolveEndpointDisplayStatus 实际只返回四种状态值,使用 string 会丢失类型安全性,下游消费方需要手动判断字符串值。

建议修改
+export type EndpointDisplayStatusToken = "circuit-open" | "circuit-half-open" | "enabled" | "disabled";
+
 export interface EndpointDisplayStatus {
-  status: string;
+  status: EndpointDisplayStatusToken;
   source: IncidentSource;
   priority: number;
 }

138-166: lastProbeOk 参数在 resolveEndpointDisplayStatus 中未被使用。

该函数签名接受 lastProbeOk,但函数体仅使用了 isEnabledcircuitState。如果是为了与 getEndpointStatusModel 保持签名一致以便调用方统一传参,建议添加注释说明意图;否则可以从签名中移除以避免误导。

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

33-42: circuitState 的类型字面量与 EndpointCircuitState 重复定义。

EndpointCircuitInfoMap 中内联定义了 "closed" | "open" | "half-open",而 endpoint-status.ts 已导出 EndpointCircuitState 类型。建议复用以避免不一致风险。

建议修改
+import type { EndpointCircuitState } from "./endpoint-status";
+
 export type EndpointCircuitInfoMap = Record<
   number,
   Array<{
     endpointId: number;
-    circuitState: "closed" | "open" | "half-open";
+    circuitState: EndpointCircuitState;
     failureCount: number;
     circuitOpenUntil: number | null;
   }>
 >;
tests/unit/settings/providers/endpoint-status.test.ts (2)

116-117: createEndpoint 辅助函数的 isEnabled 参数类型应与被测函数签名一致。

resolveEndpointDisplayStatus 接受 isEnabled?: boolean | null,但此处 createEndpointisEnabled 参数类型为 boolean | undefined(隐式),导致第 202 行需要 null as unknown as undefined 强制转换。建议直接对齐类型。

建议修改
-  const createEndpoint = (lastProbeOk: boolean | null, isEnabled?: boolean) =>
-    ({ lastProbeOk, isEnabled }) as { lastProbeOk: boolean | null; isEnabled?: boolean };
+  const createEndpoint = (lastProbeOk: boolean | null, isEnabled?: boolean | null) =>
+    ({ lastProbeOk, isEnabled }) as { lastProbeOk: boolean | null; isEnabled?: boolean | null };

这样第 202 行可以简化为:

-      const endpoint = createEndpoint(true, null as unknown as undefined);
+      const endpoint = createEndpoint(true, null);

105-113: IncidentSource 类型测试价值有限。

这个测试仅验证 TypeScript 类型赋值是否合法,编译时已能保证。不过作为文档性质的测试保留也无妨。

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

116-136: renderWithProviders 建议添加 afterEach 清理或使用 try/finally。

如果测试在 unmount() 调用前抛异常,DOM 节点会残留。虽然 beforeEach 中有清理逻辑(第 147-149 行),但在同一测试内多次渲染时可能产生干扰。可考虑注册 afterEach 自动清理。

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

133-149: 查询键建议直接使用排序后的数组而非拼接字符串。

endpointIds.toSorted(...).join(",") 在端点数量较多时会产生很长的字符串键。React Query 原生支持数组作为查询键且内部使用深比较,直接传入排序后的数组即可。

建议修改
-  const { data: circuitInfoMap = {} } = useQuery({
-    queryKey: ["endpoint-circuit-info", endpointIds.toSorted((a, b) => a - b).join(",")],
+  const sortedEndpointIds = useMemo(() => [...endpointIds].sort((a, b) => a - b), [endpointIds]);
+  const { data: circuitInfoMap = {} } = useQuery({
+    queryKey: ["endpoint-circuit-info", sortedEndpointIds],

"http2Fallback": "Откат HTTP/2",
"clientError": "Ошибка клиента"
"clientError": "Ошибка клиента",
"endpointPoolExhausted": "Пул конечная точкаов исчерпан"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

俄语语法严重错误:"конечная точкаов" 不是有效的俄语词形。

多处新增翻译中出现了 "конечная точкаов" 这一拼写,这是将主格单数 "конечная точка" 与属格复数后缀 "-ов" 错误拼接的结果,在俄语中完全不合语法。正确的属格复数形式应为 "конечных точек"

涉及行:41、53、67、193、194、195、196、198、199。

📝 建议修改
-    "endpointPoolExhausted": "Пул конечная точкаов исчерпан"
+    "endpointPoolExhausted": "Пул конечных точек исчерпан"
-    "endpoint_pool_exhausted": "Пул конечная точкаов исчерпан"
+    "endpoint_pool_exhausted": "Пул конечных точек исчерпан"
-    "endpoint_circuit_open": "Автомат конечная точкаа открыт",
+    "endpoint_circuit_open": "Автомат конечной точки открыт",
-    "endpointPoolExhausted": "Пул конечная точкаов исчерпан (все конечная точкаы недоступны)",
-    "endpointStats": "Статистика фильтрации конечная точкаов",
-    "endpointStatsTotal": "Всего конечная точкаов: {count}",
-    "endpointStatsEnabled": "Включено конечная точкаов: {count}",
-    "endpointStatsCircuitOpen": "Эндпоинтов с открытым автоматом: {count}",
-    "endpointStatsAvailable": "Доступных конечная точкаов: {count}",
-    "strictBlockNoEndpoints": "Строгий режим: нет доступных кандидатов конечная точкаов, провайдер пропущен без отката",
-    "strictBlockSelectorError": "Строгий режим: ошибка селектора конечная точкаов, провайдер пропущен без отката"
+    "endpointPoolExhausted": "Пул конечных точек исчерпан (все конечные точки недоступны)",
+    "endpointStats": "Статистика фильтрации конечных точек",
+    "endpointStatsTotal": "Всего конечных точек: {count}",
+    "endpointStatsEnabled": "Включено конечных точек: {count}",
+    "endpointStatsCircuitOpen": "Конечных точек с открытым автоматом: {count}",
+    "endpointStatsAvailable": "Доступных конечных точек: {count}",
+    "strictBlockNoEndpoints": "Строгий режим: нет доступных кандидатов среди конечных точек, провайдер пропущен без отката",
+    "strictBlockSelectorError": "Строгий режим: ошибка селектора конечных точек, провайдер пропущен без отката"

Also applies to: 53-53, 67-67, 193-200

🤖 Prompt for AI Agents
In `@messages/ru/provider-chain.json` at line 41, Replace the grammatically
incorrect Russian phrase "конечная точкаов" with the proper genitive plural
"конечных точек" in this locale file; specifically update the value for the
endpointPoolExhausted key (and any other keys showing "конечная точкаов" on
lines noted) so the string becomes "Пул конечных точек исчерпан" (or the
equivalent correct form where used), and search the file for any other
occurrences of "конечная точкаов" to correct them all.

Comment on lines +587 to +597
const results = await Promise.all(
parsed.data.endpointIds.map(async (endpointId) => {
const { health } = await getEndpointHealthInfo(endpointId);
return {
endpointId,
circuitState: health.circuitState,
failureCount: health.failureCount,
circuitOpenUntil: health.circuitOpenUntil,
};
})
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Promise.all 会因单个端点查询失败导致整个批量请求失败。

当前实现中,如果 500 个端点中有 1 个查询 getEndpointHealthInfo 抛异常,整个请求都会返回错误。建议使用 Promise.allSettled 并过滤成功结果,使批量查询具备部分容错能力。

建议修改
-    const results = await Promise.all(
-      parsed.data.endpointIds.map(async (endpointId) => {
-        const { health } = await getEndpointHealthInfo(endpointId);
-        return {
-          endpointId,
-          circuitState: health.circuitState,
-          failureCount: health.failureCount,
-          circuitOpenUntil: health.circuitOpenUntil,
-        };
-      })
-    );
+    const settled = await Promise.allSettled(
+      parsed.data.endpointIds.map(async (endpointId) => {
+        const { health } = await getEndpointHealthInfo(endpointId);
+        return {
+          endpointId,
+          circuitState: health.circuitState,
+          failureCount: health.failureCount,
+          circuitOpenUntil: health.circuitOpenUntil,
+        };
+      })
+    );
+
+    const results = settled
+      .filter((r): r is PromiseFulfilledResult<typeof r extends PromiseFulfilledResult<infer T> ? T : never> => r.status === "fulfilled")
+      .map((r) => r.value);
🤖 Prompt for AI Agents
In `@src/actions/provider-endpoints.ts` around lines 587 - 597, The batch endpoint
health aggregation uses Promise.all which fails the whole batch if any
getEndpointHealthInfo(endpointId) rejects; change to Promise.allSettled over
parsed.data.endpointIds, then filter for entries with status === "fulfilled",
map their values to the existing shape ({ endpointId, circuitState,
failureCount, circuitOpenUntil }) and discard or log rejected entries so a
single failure doesn't abort the entire results array; ensure the variable names
(parsed.data.endpointIds, getEndpointHealthInfo, results) are preserved.

Comment on lines +601 to +603
// selector_error should NOT call getEndpointFilterStats (exception path, no data available)
// endpointFilterStats should be undefined for selector_error
expect(exhaustedItem!.endpointFilterStats).toBeUndefined();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

注释声明了意图但缺少对应断言。

第 601 行注释说明 selector_error 路径不应调用 getEndpointFilterStats,但没有对应的 mock 调用断言来验证。由于默认 mock 返回 null(第 6 行),若生产代码意外调用了该函数,endpointFilterStats 仍可能为 undefined(取决于 null 的处理方式),测试会静默通过而掩盖行为偏差。

建议添加显式断言
     // selector_error should NOT call getEndpointFilterStats (exception path, no data available)
+    expect(mocks.getEndpointFilterStats).not.toHaveBeenCalled();
     // endpointFilterStats should be undefined for selector_error
     expect(exhaustedItem!.endpointFilterStats).toBeUndefined();
📝 Committable suggestion

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

Suggested change
// selector_error should NOT call getEndpointFilterStats (exception path, no data available)
// endpointFilterStats should be undefined for selector_error
expect(exhaustedItem!.endpointFilterStats).toBeUndefined();
// selector_error should NOT call getEndpointFilterStats (exception path, no data available)
expect(mocks.getEndpointFilterStats).not.toHaveBeenCalled();
// endpointFilterStats should be undefined for selector_error
expect(exhaustedItem!.endpointFilterStats).toBeUndefined();
🤖 Prompt for AI Agents
In `@tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts` around lines 601 -
603, The test documents that the "selector_error" path should NOT call
getEndpointFilterStats but lacks an explicit mock call assertion; update the
selector_error case in proxy-forwarder-endpoint-audit.test.ts to assert the
mocked getEndpointFilterStats was not invoked (e.g.,
expect(getEndpointFilterStatsMock).not.toHaveBeenCalled() or
toHaveBeenCalledTimes(0)) after inspecting exhaustedItem for selector_error,
referencing the existing mock defined near the top of the test file and the
exhaustedItem variable to ensure the behavior is enforced rather than relying on
default mock return values.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

48 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +286 to +292
if (!res.ok) throw new Error(res.error);
return res;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["endpoint-circuit-info"] });
queryClient.invalidateQueries({ queryKey: ["provider-endpoints"] });
toast.success(tStatus("resetCircuitSuccess"));
Copy link

Choose a reason for hiding this comment

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

Query invalidation may miss
The circuit state query is keyed as ["endpoint-circuit-info", endpointIds.toSorted(...).join(",")], but the reset mutation only invalidates { queryKey: ["endpoint-circuit-info"] }. If your React Query version/config doesn’t treat this as a partial prefix match, the active query won’t be invalidated and badges can stay stale after reset. Consider invalidating with a predicate (keys starting with "endpoint-circuit-info") or invalidate the exact composed key you used for the query.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx
Line: 286:292

Comment:
**Query invalidation may miss**
The circuit state query is keyed as `["endpoint-circuit-info", endpointIds.toSorted(...).join(",")]`, but the reset mutation only invalidates `{ queryKey: ["endpoint-circuit-info"] }`. If your React Query version/config doesn’t treat this as a partial prefix match, the active query won’t be invalidated and badges can stay stale after reset. Consider invalidating with a predicate (keys starting with `"endpoint-circuit-info"`) or invalidate the exact composed key you used for the query.

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

Comment on lines 551 to +556
}
}

export async function batchGetEndpointCircuitInfo(input: unknown): Promise<
ActionResult<
Array<{
Copy link

Choose a reason for hiding this comment

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

Not actually batched read
batchGetEndpointCircuitInfo() still does Promise.all(endpointIds.map(getEndpointHealthInfo)), which is effectively an N+1 against the underlying store (often Redis/DB). With .max(500) this can create a large burst of reads and slow the settings page. This likely needs a true batch/multi-get path in getEndpointHealthInfo (or a dedicated batch helper) rather than per-endpoint calls.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/provider-endpoints.ts
Line: 551:556

Comment:
**Not actually batched read**
`batchGetEndpointCircuitInfo()` still does `Promise.all(endpointIds.map(getEndpointHealthInfo))`, which is effectively an N+1 against the underlying store (often Redis/DB). With `.max(500)` this can create a large burst of reads and slow the settings page. This likely needs a true batch/multi-get path in `getEndpointHealthInfo` (or a dedicated batch helper) rather than per-endpoint calls.

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

Comment on lines +559 to +566
failureCount: number;
circuitOpenUntil: number | null;
}>
>
> {
try {
const session = await getAdminSession();
if (!session) {
Copy link

Choose a reason for hiding this comment

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

Hardcoded Chinese errors
This action returns Chinese-only error strings (e.g. "无权限执行此操作", "批量获取端点熔断状态失败"). If these surface in non-zh locales, UX becomes inconsistent versus the rest of the admin actions. Use the project’s standard error message strategy (shared constants / i18n / error codes only) so callers can present locale-appropriate text.

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

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/provider-endpoints.ts
Line: 559:566

Comment:
**Hardcoded Chinese errors**
This action returns Chinese-only error strings (e.g. `"无权限执行此操作"`, `"批量获取端点熔断状态失败"`). If these surface in non-zh locales, UX becomes inconsistent versus the rest of the admin actions. Use the project’s standard error message strategy (shared constants / i18n / error codes only) so callers can present locale-appropriate text.

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

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

Comment on lines +66 to +74
* Used for audit trail when all endpoints are exhausted (strict block).
* Returns null only when the raw endpoint query itself fails.
*/
export async function getEndpointFilterStats(input: {
vendorId: number;
providerType: ProviderType;
}): Promise<EndpointFilterStats> {
const endpoints = await findProviderEndpointsByVendorAndType(input.vendorId, input.providerType);
const total = endpoints.length;
Copy link

Choose a reason for hiding this comment

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

Per-endpoint circuit checks
getEndpointFilterStats() calls isEndpointCircuitOpen(e.id) for every enabled endpoint via Promise.all. On vendors with many endpoints this will generate a burst of circuit-store reads right when strict blocking is happening (incident conditions), increasing latency/load. Prefer a batched circuit-state lookup (or reuse circuit state already computed during endpoint selection) to avoid amplifying incidents.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/provider-endpoints/endpoint-selector.ts
Line: 66:74

Comment:
**Per-endpoint circuit checks**
`getEndpointFilterStats()` calls `isEndpointCircuitOpen(e.id)` for every enabled endpoint via `Promise.all`. On vendors with many endpoints this will generate a burst of circuit-store reads right when strict blocking is happening (incident conditions), increasing latency/load. Prefer a batched circuit-state lookup (or reuse circuit state already computed during endpoint selection) to avoid amplifying incidents.

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

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

Labels

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

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant