Skip to content

fix: isolate provider endpoint pools and sticky sessions#919

Merged
ding113 merged 3 commits into
devfrom
fix/provider-endpoint-isolation-and-sticky-session
Mar 15, 2026
Merged

fix: isolate provider endpoint pools and sticky sessions#919
ding113 merged 3 commits into
devfrom
fix/provider-endpoint-isolation-and-sticky-session

Conversation

@ding113

@ding113 ding113 commented Mar 14, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes critical provider endpoint isolation bugs where vendor clustering incorrectly merged self-hosted providers, create/clone forms auto-attached to shared endpoint pools, endpoint deletion provided no actionable errors, and sticky sessions persisted after routing configuration changes.

Problem

Issue #918 - Provider endpoint logic had multiple interrelated defects:

  1. Port-ignoring vendor clustering: URLs like 192.168.1.1:111 and 192.168.1.1:222 were treated as the same vendor because only hostname was used for clustering

  2. Clone/create inheritance: Cloning a provider automatically inherited the original's endpoint pool, making it impossible to create truly independent providers

  3. Silent URL replacement: The create/clone form would silently replace user-provided URLs with endpoints from shared pools

  4. Undiagnosable delete failures: Endpoint deletion failures gave no information about which providers were blocking the operation

  5. Stale sticky sessions: After endpoint/provider changes, existing sessions continued routing to old configurations until manual termination or TTL expiration

Related Issues:

Solution

1. Port-Aware Vendor Clustering

Modified normalizeWebsiteDomainKeyFromUrl to preserve explicit ports:

  • https://example.com → key = example.com (no port)
  • https://example.com:3000 → key = example.com:3000 (port preserved)

2. Create/Clone Form Isolation

Refactored provider form:

  • Removed vendor resolution from websiteUrl in create/clone modes
  • Removed auto-URL-fill from shared endpoint pools
  • Create/clone modes now require explicit endpoint configuration

3. Actionable Delete Errors

Enhanced endpoint deletion:

  • New query returns referencing provider IDs and names
  • New error code ENDPOINT_REFERENCED_BY_ENABLED_PROVIDERS with i18n
  • Error message includes count and provider names

4. Sticky Session Termination

Added session invalidation:

  • New SessionManager.terminateProviderSessionsBatch method
  • Integrated into provider edit/delete and endpoint changes
  • Tracks routing-affecting field changes

Changes

Core Changes

File Change
src/repository/provider-endpoints.ts Port-aware vendor key; new repository queries
src/lib/session-manager.ts New terminateProviderSessionsBatch method
src/actions/providers.ts Sticky session invalidation on provider changes
src/actions/provider-endpoints.ts Detailed delete errors; session termination
src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx Create/clone form isolation
src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx Enhanced delete error handling

Supporting Changes

  • i18n error messages for 5 languages
  • Comprehensive test coverage (vendor keys, sessions, form behavior)

Testing

Automated Tests

  • Unit tests for port-aware vendor key computation
  • Unit tests for terminateProviderSessionsBatch
  • Unit tests for detailed endpoint delete errors
  • Unit tests for provider sticky session invalidation
  • Unit tests updated for provider form create/clone isolation

Verification Commands

bunx biome check <changed-files>
bun run typecheck
bun run build
bun run test

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests pass locally
  • i18n strings added for all 5 languages
  • No breaking API changes

Original Greptile Analysis

Greptile Summary

This PR fixes five interrelated provider endpoint isolation bugs: port-ignoring vendor clustering, create/clone forms silently inheriting shared endpoint pools, uninformative delete-conflict errors, and stale sticky sessions persisting after routing configuration changes. The fixes are well-scoped and each change is backed by new unit tests.

Key changes:

  • normalizeWebsiteDomainKeyFromUrl now preserves explicit ports in the vendor clustering key, so 192.168.1.1:111 and 192.168.1.1:222 are no longer merged into the same vendor
  • Provider create/clone forms no longer resolve a vendor from websiteUrl or auto-fill the URL from a shared endpoint pool — users must provide explicit endpoint configuration
  • removeProviderEndpoint now returns an i18n-translated error (ENDPOINT_REFERENCED_BY_ENABLED_PROVIDERS) with the count and names of blocking providers instead of a generic conflict
  • SessionManager.terminateProviderSessionsBatch added; called after endpoint edits/deletes and provider edits/removes to evict stale sticky sessions from Redis
  • allowed_clients / blocked_clients were missing from SINGLE_EDIT_PREIMAGE_FIELD_TO_PROVIDER_KEY, meaning changes to those fields via the single-edit path never triggered session invalidation — this is now fixed

Issues found:

  • In removeProviderEndpoint, the new findEnabledProviderIdsByVendorAndType DB call (line 629) sits outside any try-catch after the soft-delete has already committed. A transient DB error here would cause the outer catch to return DELETE_FAILED even though the deletion succeeded — unlike the other post-delete calls (resetEndpointCircuitState, tryDeleteProviderVendorIfEmpty, publishProviderCacheInvalidation) which are all individually guarded
  • The catch block in terminateStickySessionsForProviders is unreachable because terminateProviderSessionsBatch already swallows all its own exceptions internally
  • The error fallback string for the ENDPOINT_REFERENCED_BY_ENABLED_PROVIDERS error path is still a hardcoded Chinese string, inconsistent with the i18n approach used elsewhere

Confidence Score: 3/5

  • Safe to merge after addressing the unguarded DB call in removeProviderEndpoint that can return DELETE_FAILED on a successful deletion
  • The overall approach is correct and well-tested. The session-termination logic and form isolation changes are solid. The main concern is a logic flaw in removeProviderEndpoint where a post-delete DB query is unguarded — unlike every other post-delete step in the same function — meaning a transient DB error after a successful soft-delete could produce a misleading error response. This is not a data-loss risk but is a real correctness issue that could confuse users and operators.
  • src/actions/provider-endpoints.ts — specifically the unguarded findEnabledProviderIdsByVendorAndType call after the soft-delete

Important Files Changed

Filename Overview
src/repository/provider-endpoints.ts Port-aware vendor key computation added to normalizeWebsiteDomainKeyFromUrl; two new queries findEnabledProviderReferencesForVendorTypeUrl and findEnabledProviderIdsByVendorAndType added. Logic is sound and well-tested.
src/lib/session-manager.ts New terminateProviderSessionsBatch and terminateStickySessionsForProviders methods added. Logic is correct but terminateStickySessionsForProviders contains an unreachable catch block since terminateProviderSessionsBatch already swallows all exceptions.
src/actions/provider-endpoints.ts Session termination added to editProviderEndpoint and removeProviderEndpoint. In removeProviderEndpoint, the new findEnabledProviderIdsByVendorAndType DB call is unguarded after the soft-delete, which can surface a misleading DELETE_FAILED response if the query throws while the deletion already succeeded. Also retains a hardcoded Chinese error fallback string.
src/actions/providers.ts Sticky session termination integrated into editProvider and removeProvider; routing-affecting field set defined. Also adds allowed_clients/blocked_clients to the preimage field map, fixing a missing mapping that would have prevented session invalidation for those edits.
src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx Vendor resolution and auto-URL-fill removed from create/clone form paths. URL input is now always shown and always requires explicit user input in create/clone mode. The simplification is clean and correct.
src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx Delete error handling enhanced to use i18n-translated error messages from errorCode/errorParams rather than a generic toast. The Object.assign pattern for attaching actionResult to the thrown Error is clean and type-safe.

Sequence Diagram

sequenceDiagram
    participant UI as UI (provider-endpoints-table)
    participant AE as actions/provider-endpoints
    participant AR as repository/provider-endpoints
    participant SM as SessionManager
    participant Redis as Redis

    Note over UI,Redis: removeProviderEndpoint flow
    UI->>AE: removeProviderEndpoint({ endpointId })
    AE->>AR: findEnabledProviderReferencesForVendorTypeUrl(vendorId, type, url)
    AR-->>AE: references[]
    alt references.length > 0
        AE-->>UI: { ok: false, errorCode: ENDPOINT_REFERENCED_BY_ENABLED_PROVIDERS, errorParams }
        UI->>UI: toast.error(getErrorMessage(tErrors, errorCode, errorParams))
    else no references
        AE->>AR: softDeleteProviderEndpoint(endpointId)
        AE->>AR: findEnabledProviderIdsByVendorAndType(vendorId, type)
        AR-->>AE: providerIds[]
        AE->>SM: terminateStickySessionsForProviders(providerIds)
        SM->>SM: terminateProviderSessionsBatch(providerIds)
        SM->>Redis: pipeline.zrange(provider:N:active_sessions)
        Redis-->>SM: sessionIds[]
        SM->>SM: terminateSessionsBatch(sessionIds)
        AE-->>UI: { ok: true }
    end

    Note over UI,Redis: editProviderEndpoint flow (url/sortOrder/isEnabled changed)
    UI->>AE: editProviderEndpoint({ endpointId, isEnabled: false })
    AE->>AR: updateProviderEndpoint(endpointId, data)
    AR-->>AE: updated endpoint
    AE->>AR: findEnabledProviderIdsByVendorAndType(vendorId, type)
    AR-->>AE: providerIds[]
    AE->>SM: terminateStickySessionsForProviders(providerIds)
    SM->>Redis: pipeline + terminateSessionsBatch
    AE-->>UI: { ok: true, data: { endpoint } }
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/actions/provider-endpoints.ts
Line: 629-636

Comment:
**Unguarded DB call after soft-delete can produce misleading error**

`findEnabledProviderIdsByVendorAndType` is called outside any try-catch at line 629, after the endpoint has already been soft-deleted at line 610. Every other non-critical post-delete operation in this function (`resetEndpointCircuitState`, `tryDeleteProviderVendorIfEmpty`, `publishProviderCacheInvalidation`) is wrapped in its own try-catch precisely to prevent this scenario. If the DB query at line 629 throws (e.g. transient DB connectivity issue), the outer catch at line 660 returns `{ ok: false, errorCode: DELETE_FAILED }` — even though the deletion already succeeded. The UI and the caller will believe the operation failed and likely show an error, while the endpoint is actually gone.

```suggestion
    try {
      const affectedProviderIds = await findEnabledProviderIdsByVendorAndType(
        endpoint.vendorId,
        endpoint.providerType
      );
      await SessionManager.terminateStickySessionsForProviders(
        affectedProviderIds,
        "removeProviderEndpoint"
      );
    } catch (error) {
      logger.warn("removeProviderEndpoint:terminate_sessions_failed", {
        endpointId: parsed.data.endpointId,
        vendorId: endpoint.vendorId,
        error: error instanceof Error ? error.message : String(error),
      });
    }
```

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

---

This is a comment left during a code review.
Path: src/actions/provider-endpoints.ts
Line: 599-607

Comment:
**Hardcoded Chinese fallback in `error` field**

The `error` field still contains a hardcoded Chinese string. Since `errorCode` is always set for this code path, the UI's `onError` handler will never reach this fallback — but it is inconsistent with the i18n approach used everywhere else in this file and is misleading for non-Chinese readers who might encounter it in logs or during debugging.

Consider replacing it with a neutral English string or the error code itself:

```suggestion
      return {
        ok: false,
        error: `Endpoint is still referenced by ${references.length} enabled provider(s)`,
        errorCode: ENDPOINT_REFERENCED_BY_ENABLED_PROVIDERS_CODE,
        errorParams: {
          count: references.length,
          providers: formatProviderReferenceSummary(references),
        },
      };
```

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

---

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

Comment:
**Dead `catch` block in `terminateStickySessionsForProviders`**

`terminateProviderSessionsBatch` internally catches all errors (via its own try-catch at the bottom of the method) and always returns `0` on failure — it never propagates exceptions. As a result, the `catch` block in `terminateStickySessionsForProviders` can never be triggered, making it unreachable dead code. This could mislead future maintainers into thinking the wrapper provides meaningful error isolation that it does not.

If the goal is to provide a fire-and-forget, non-throwing wrapper, consider documenting that `terminateProviderSessionsBatch` is already non-throwing, and simplify:

```ts
static async terminateStickySessionsForProviders(
  providerIds: number[],
  context: string
): Promise<void> {
  const uniqueProviderIds = Array.from(
    new Set(providerIds.filter((id) => Number.isInteger(id) && id > 0))
  );
  if (uniqueProviderIds.length === 0) return;
  // terminateProviderSessionsBatch is non-throwing; it catches all errors internally
  await SessionManager.terminateProviderSessionsBatch(uniqueProviderIds);
}
```

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

Last reviewed commit: 22c68ac

Greptile also left 2 inline comments on this PR.

@coderabbitai

coderabbitai Bot commented Mar 14, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

添加了多语言错误消息、增强的端点引用检查、新的批量会话终止 API、在 provider 编辑/删除流程中触发粘性会话失效,以及若干前端表单与测试调整以支持这些行为变更。

Changes

Cohort / File(s) Summary
多语言错误消息
messages/en/errors.json, messages/ja/errors.json, messages/ru/errors.json, messages/zh-CN/errors.json, messages/zh-TW/errors.json
新增键 ENDPOINT_REFERENCED_BY_ENABLED_PROVIDERS 及其各语言翻译,用于在删除 endpoint 被启用 provider 引用时返回结构化提示。
Provider endpoint 操作逻辑
src/actions/provider-endpoints.ts, src/lib/provider-endpoint-error-codes.ts
新增对引用检查的调用与错误码 ENDPOINT_REFERENCED_BY_ENABLED_PROVIDERS_CODE;在编辑/删除端点时使用新的引用查询并在必要时触发会话终止;新增 provider 引用汇总格式化函数。
Provider 仓储与 vendor key
src/repository/provider-endpoints.ts
重命名并改进网站域名规范化以保留显式端口;新增查询助手 findEnabledProviderReferencesForVendorTypeUrlfindEnabledProviderIdsByVendorAndType
会话管理扩展
src/lib/session-manager.ts
新增静态方法 terminateProviderSessionsBatch(providerIds: number[]): Promise<number>terminateStickySessionsForProviders(providerIds: number[], context: string): Promise<void>,支持按 provider 批量终止会话并增加 Redis/日志鲁棒性。
Provider 编辑/删除流程
src/actions/providers.ts
引入 SessionManager、定义需触发粘性会话失效的字段集与检测逻辑;在 editProviderremoveProvider 中调用会话终止。
前端表单/表格调整
src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx, src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx
移除基于 websiteUrl 的自动 vendor/endpoint 池继承与相关查询;增强端点删除错误处理以展示本地化的结构化错误消息。
测试——会话与端点行为
tests/unit/actions/provider-endpoints.test.ts, tests/unit/lib/session-manager-terminate-provider-sessions.test.ts, tests/unit/actions/providers.test.ts
新增/调整单元测试以 mock 新增的仓储查询和 SessionManager 批量终止方法,覆盖删除阻塞路径和编辑触发的会话终止。
测试——表单与前端
tests/unit/settings/providers/provider-endpoints-table.test.tsx, tests/unit/settings/providers/provider-form-endpoint-pool.test.tsx, src/app/[locale]/dashboard/logs/_components/provider-chain-popover.test.tsx
新增删除失败展示的测试、clone/create 模式下不自动继承 endpoint pool 的测试,并调整 hedge 请求计数断言。
测试——仓储 vendor key 与查询模拟
tests/unit/repository/provider-endpoints-vendor-key.test.ts, tests/unit/repository/statistics-quota-costs-all-time.test.ts
补充显式端口保留为 vendor key 的测试;扩展 drizzle-orm mock(sqlrelationseq 等)以支持新的测试断言。

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 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 标题准确总结了主要变更:分离 provider endpoint pools 和 sticky sessions,直接对应 Issue #918 中的端点隔离与会话失效问题。
Description check ✅ Passed PR 描述详尽关联 Issue #918,清晰阐述五个问题(端口忽略、表单继承、无诊断错误、会话持久化、重复预处理映射),说明解决方案与实现细节,并附测试命令。
Linked Issues check ✅ Passed 代码变更覆盖 Issue #918 的全部预期行为:端口感知的 vendor key [repo-endpoints]、create/clone 表单隔离 [provider-form]、详细删除错误 [provider-endpoints]、session 自动失效 [session-manager/providers],所有验收标准已实现。
Out of Scope Changes check ✅ Passed 所有变更严格围绕 Issue #918:vendor key 端口处理、endpoint 删除诊断、session 失效、表单隔离、i18n 错误消息、相关测试覆盖。统计文件与日志测试修改为非阻塞性调整,未超出预期范围。

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/provider-endpoint-isolation-and-sticky-session
📝 Coding Plan
  • Generate coding plan for human review comments

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
Copy Markdown
Contributor

Summary of Changes

Hello, 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 refines how provider endpoints and sessions are managed, focusing on improving isolation, user feedback, and system responsiveness. The changes ensure that provider configurations, especially for self-hosted instances, behave predictably and that any modifications to routing-sensitive fields correctly invalidate existing user sessions, prompting re-evaluation of routing decisions. This leads to a more robust and transparent provider management experience.

Highlights

  • Provider Endpoint Isolation & Sticky Sessions: Implemented logic to isolate provider endpoint pools, ensuring that websiteUrl ports are preserved for self-hosted/local providers. This prevents unintended auto-attachment to shared endpoint pools or silent URL replacements in provider creation/cloning forms.
  • Actionable Endpoint Delete Errors: Enhanced the endpoint deletion process to provide more specific and actionable error messages when an endpoint is still referenced by enabled providers, including a summary of the referencing providers.
  • Sticky Session Termination: Introduced mechanisms to terminate sticky provider sessions when provider or endpoint routing surfaces (like URL, enabled status, or group tags) change, ensuring that new requests correctly route through updated configurations.
  • Comprehensive Regression Coverage: Added extensive regression tests for provider endpoint pooling, sticky sessions, vendor key clustering, and related repository test fixtures to ensure the stability and correctness of these critical features.
Changelog
  • messages/en/errors.json
    • Added a new error message for endpoints referenced by enabled providers.
  • messages/ja/errors.json
    • Added a new error message for endpoints referenced by enabled providers.
  • messages/ru/errors.json
    • Added a new error message for endpoints referenced by enabled providers.
  • messages/zh-CN/errors.json
    • Added a new error message for endpoints referenced by enabled providers.
  • messages/zh-TW/errors.json
    • Added a new error message for endpoints referenced by enabled providers.
  • src/actions/provider-endpoints.ts
    • Imported SessionManager and new error codes.
    • Added helper functions formatProviderReferenceSummary and terminateStickySessionsForProviders.
    • Modified editProviderEndpoint to terminate sticky sessions upon relevant changes.
    • Updated removeProviderEndpoint to check for enabled provider references and terminate sticky sessions.
  • src/actions/providers.ts
    • Imported SessionManager.
    • Added helper functions STICKY_SESSION_INVALIDATING_PROVIDER_FIELDS, shouldInvalidateStickySessionsOnProviderEdit, and terminateStickySessionsForProviders.
    • Integrated sticky session termination into editProvider and removeProvider actions.
  • src/app/[locale]/dashboard/logs/_components/provider-chain-popover.test.tsx
    • Updated test assertion for request row count.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
    • Removed getProviderVendors import and related logic.
    • Removed normalizeWebsiteDomainFromUrl utility function.
    • Simplified endpoint pool vendor ID resolution logic.
    • Removed an useEffect hook that auto-filled the URL based on endpoint pool preferences.
  • src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx
    • Imported tErrors for localized error messages.
    • Updated the onError handler for endpoint deletion to display detailed, translated error messages.
  • src/lib/provider-endpoint-error-codes.ts
    • Added ENDPOINT_REFERENCED_BY_ENABLED_PROVIDERS_CODE.
  • src/lib/session-manager.ts
    • Added a static method terminateProviderSessionsBatch to terminate sessions associated with a list of provider IDs.
  • src/repository/provider-endpoints.ts
    • Renamed normalizeWebsiteDomainFromUrl to normalizeWebsiteDomainKeyFromUrl and updated its logic to preserve explicit ports in websiteUrl for vendor key clustering.
    • Updated computeVendorKey to use the new normalization logic.
    • Added findEnabledProviderReferencesForVendorTypeUrl to retrieve details of providers referencing a specific endpoint.
    • Added findEnabledProviderIdsByVendorAndType to get IDs of enabled providers for a given vendor and type.
  • tests/unit/actions/provider-endpoints.test.ts
    • Added mocks for SessionManager and new repository functions.
    • Updated beforeEach to reset new mocks.
    • Added tests for detailed conflict error on endpoint removal and sticky session termination on endpoint edit.
  • tests/unit/actions/providers.test.ts
    • Added mock for SessionManager.
    • Updated beforeEach to reset new mock.
    • Added tests for sticky session termination on provider edit and removal, including changes to group tags and allowed models.
  • tests/unit/lib/session-manager-terminate-provider-sessions.test.ts
    • Added a new unit test file for SessionManager.terminateProviderSessionsBatch.
  • tests/unit/repository/provider-endpoints-vendor-key.test.ts
    • Updated tests for computeVendorKey to verify correct handling of explicit ports in websiteUrl.
  • tests/unit/repository/statistics-quota-costs-all-time.test.ts
    • Updated Drizzle ORM mocks to include sqlMock and other functions for better test isolation.
  • tests/unit/settings/providers/provider-endpoints-table.test.tsx
    • Added confirmMock and updated beforeEach setup.
    • Added a test case to verify that delete failure shows translated reference details.
  • tests/unit/settings/providers/provider-form-endpoint-pool.test.tsx
    • Added makeCloneProvider helper function.
    • Updated ProviderDisplay import.
    • Added tests to confirm that manual URL input is retained in create mode and that clone mode submits the explicit URL, not an inherited endpoint pool URL.
Activity
  • The author, ding113, initiated this pull request to address several issues related to provider endpoint management and sticky sessions.
  • The changes include significant refactoring of provider and endpoint actions, repository functions, and UI components.
  • New error messages and their translations were added to provide clearer feedback to users.
  • Comprehensive unit tests were added or updated across various modules to cover the new logic and prevent regressions.
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/L Large PR (< 1000 lines) bug Something isn't working area:provider area:session area:i18n labels Mar 14, 2026

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces several valuable fixes and improvements related to provider endpoints and sticky sessions. The changes to vendor clustering, UI forms for provider creation, and more detailed error messages for endpoint deletion are well-implemented. The addition of logic to terminate sticky sessions when routing configurations change is a crucial enhancement for system stability. The new regression tests provide good coverage for these changes. My only suggestion is to address a piece of duplicated code to improve maintainability.

Comment thread src/actions/providers.ts Outdated
Comment on lines +205 to +221
async function terminateStickySessionsForProviders(providerIds: number[], context: string) {
const uniqueProviderIds = Array.from(
new Set(providerIds.filter((providerId) => Number.isInteger(providerId) && providerId > 0))
);
if (uniqueProviderIds.length === 0) {
return;
}

try {
await SessionManager.terminateProviderSessionsBatch(uniqueProviderIds);
} catch (error) {
logger.warn(`${context}:terminate_provider_sessions_failed`, {
providerIds: uniqueProviderIds,
error: error instanceof Error ? error.message : String(error),
});
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

This helper function terminateStickySessionsForProviders is also defined in src/actions/provider-endpoints.ts. To follow the DRY (Don't Repeat Yourself) principle, it's best to move this function to a shared location, like a new utility file under src/lib/utils/.

Furthermore, the try-catch block here is likely unreachable because SessionManager.terminateProviderSessionsBatch already handles its own errors and doesn't re-throw them. This makes the context parameter unused and the catch block dead code.

A good refactoring approach would be to have a single, shared helper function. If contextual logging from the actions is important, terminateProviderSessionsBatch could be modified to accept a context string for its error logs, which would make this wrapper unnecessary.

Comment thread src/actions/providers.ts Outdated
Comment on lines +205 to +221
async function terminateStickySessionsForProviders(providerIds: number[], context: string) {
const uniqueProviderIds = Array.from(
new Set(providerIds.filter((providerId) => Number.isInteger(providerId) && providerId > 0))
);
if (uniqueProviderIds.length === 0) {
return;
}

try {
await SessionManager.terminateProviderSessionsBatch(uniqueProviderIds);
} catch (error) {
logger.warn(`${context}:terminate_provider_sessions_failed`, {
providerIds: uniqueProviderIds,
error: error instanceof Error ? error.message : String(error),
});
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Duplicate helper across action files

terminateStickySessionsForProviders is duplicated identically in src/actions/provider-endpoints.ts (line 246) and here (line 205). Consider extracting it into a shared module (e.g., src/lib/session-manager.ts or a new src/lib/sticky-session-utils.ts) to avoid drift between the two copies.

This is non-blocking but will help maintainability if the error handling or filtering logic needs to change later.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/providers.ts
Line: 205-221

Comment:
**Duplicate helper across action files**

`terminateStickySessionsForProviders` is duplicated identically in `src/actions/provider-endpoints.ts` (line 246) and here (line 205). Consider extracting it into a shared module (e.g., `src/lib/session-manager.ts` or a new `src/lib/sticky-session-utils.ts`) to avoid drift between the two copies.

This is non-blocking but will help maintainability if the error handling or filtering logic needs to change later.

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

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!

@@ -13,7 +13,10 @@ const updateProviderEndpointMock = vi.fn();
const findProviderEndpointProbeLogsBatchMock = vi.fn();
const findVendorTypeEndpointStatsBatchMock = vi.fn();
const hasEnabledProviderReferenceForVendorTypeUrlMock = vi.fn();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale mock for removed import

hasEnabledProviderReferenceForVendorTypeUrlMock is still declared, registered in vi.mock, and has default values set in beforeEach, but the actual action code no longer imports hasEnabledProviderReferenceForVendorTypeUrl from @/repository/provider-endpoints. The new code uses findEnabledProviderReferencesForVendorTypeUrl instead.

While the stale mock won't cause test failures (the module mock still exports it), it creates confusion about which function the action code actually calls. Consider removing the mock declaration, its entry in vi.mock, and the beforeEach default at line 98 to keep the test in sync with the production code.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/unit/actions/provider-endpoints.test.ts
Line: 15

Comment:
**Stale mock for removed import**

`hasEnabledProviderReferenceForVendorTypeUrlMock` is still declared, registered in `vi.mock`, and has default values set in `beforeEach`, but the actual action code no longer imports `hasEnabledProviderReferenceForVendorTypeUrl` from `@/repository/provider-endpoints`. The new code uses `findEnabledProviderReferencesForVendorTypeUrl` instead.

While the stale mock won't cause test failures (the module mock still exports it), it creates confusion about which function the action code actually calls. Consider removing the mock declaration, its entry in `vi.mock`, and the `beforeEach` default at line 98 to keep the test in sync with the production code.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
tests/unit/repository/statistics-quota-costs-all-time.test.ts (1)

42-68: 重复代码:两个测试用例的 drizzle-orm mock 设置几乎完全相同

这两个测试块中的 mock 设置代码(约 27 行)几乎完全重复。建议提取为共享的工厂函数,提高可维护性并确保修改时的一致性。

♻️ 建议的重构方案
+function createDrizzleMock(captureAndArgs: (args: unknown[]) => void) {
+  const sqlMock = Object.assign(
+    (strings: TemplateStringsArray, ...values: unknown[]) => ({
+      queryChunks: [...strings, ...values],
+    }),
+    {
+      raw: vi.fn(),
+      join: vi.fn(),
+      identifier: vi.fn(),
+    }
+  );
+  return {
+    relations: vi.fn(() => ({})),
+    eq: vi.fn((...args: unknown[]) => ({ kind: "eq", args })),
+    gte: vi.fn((...args: unknown[]) => ({ kind: "gte", args })),
+    inArray: vi.fn((...args: unknown[]) => ({ kind: "inArray", args })),
+    isNull: vi.fn((...args: unknown[]) => ({ kind: "isNull", args })),
+    lt: vi.fn((...args: unknown[]) => ({ kind: "lt", args })),
+    sql: sqlMock,
+    and: (...args: unknown[]) => {
+      captureAndArgs(args);
+      return { kind: "and", args };
+    },
+  };
+}

然后在每个测试中使用:

vi.doMock("drizzle-orm", async () => {
  const actual = await vi.importActual<typeof import("drizzle-orm")>("drizzle-orm");
  return {
    ...actual,
    ...createDrizzleMock((args) => { capturedAndArgs = args; }),
  };
});

Also applies to: 106-132

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

In `@tests/unit/repository/statistics-quota-costs-all-time.test.ts` around lines
42 - 68, Extract the repeated drizzle-orm mock into a shared factory function
(e.g., createDrizzleMock) that returns the mocked members (relations, eq, gte,
inArray, isNull, lt, sql: sqlMock, and) and accepts a callback to capture the
`and` args so tests can set `capturedAndArgs`; then replace each
`vi.doMock("drizzle-orm", async () => { ... })` block with a minimal wrapper
that imports the actual module and spreads `...createDrizzleMock((args) => {
capturedAndArgs = args })` into the returned object, reusing the
`sqlMock`/`capturedAndArgs` behavior and eliminating the duplicated 27-line mock
setup.
src/actions/provider-endpoints.ts (1)

541-551: 会话终止范围可能过宽

当编辑端点的 url 时,当前实现使用 findEnabledProviderIdsByVendorAndType 获取该 vendor/type 下的所有已启用 provider,而非仅获取实际引用该端点 URL 的 provider。相比之下,deleteProviderEndpoint 中的逻辑使用 findEnabledProviderReferencesForVendorTypeUrl 精确定位引用特定 URL 的 provider。

建议在 url 变更时采用更精确的查询,仅终止引用该 endpoint URL 的 provider 会话。对于 sortOrderisEnabled 的独立变更,如果整个 vendor/type 端点池的调度逻辑会受影响,则可保持当前的广泛范围。

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

In `@src/actions/provider-endpoints.ts` around lines 541 - 551, The current edit
logic uses findEnabledProviderIdsByVendorAndType for any URL change and thus
terminates sessions for all providers of the vendor/type; instead, when
parsed.data.url is changed, call
findEnabledProviderReferencesForVendorTypeUrl(endpoint.vendorId,
endpoint.providerType, oldUrlOrEndpointUrl) to get only providers that actually
reference that URL and pass those IDs to
terminateStickySessionsForProviders("editProviderEndpoint"); keep using
findEnabledProviderIdsByVendorAndType only for changes to sortOrder or isEnabled
where broad termination is intended. Ensure you determine the old URL vs new URL
as needed to query references precisely.
tests/unit/settings/providers/provider-form-endpoint-pool.test.tsx (1)

14-18: 建议将 src 类型导入改为 @/ 别名路径

这里从 ../../../../src/types/provider 导入,和仓库路径规范不一致。建议直接使用 @/types/provider,可读性和稳定性更好。

建议修改
 import enMessages from "../../../../messages/en";
 import type {
   ProviderDisplay,
   ProviderEndpoint,
   ProviderVendor,
-} from "../../../../src/types/provider";
+} from "@/types/provider";

As per coding guidelines **/*.{ts,tsx,js,jsx}: Use path alias @/ to reference files in ./src/ directory.

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

In `@tests/unit/settings/providers/provider-form-endpoint-pool.test.tsx` around
lines 14 - 18, The import of ProviderDisplay, ProviderEndpoint, and
ProviderVendor is using a relative path "../../../../src/types/provider" which
violates the repo alias convention; update the import in
provider-form-endpoint-pool.test.tsx to use the path alias (import from
"@/types/provider") so the test references the types via the project's '@/...'
alias instead of the relative src path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/actions/providers.ts`:
- Around line 201-203: The current shouldInvalidateStickySessionsOnProviderEdit
function only checks presence of keys in the payload and thus invalidates sticky
sessions even when values are unchanged; change it to detect actual value
changes by comparing incoming data fields (filtered to
STICKY_SESSION_INVALIDATING_PROVIDER_FIELDS) against the existing provider's
current values (e.g., load the provider/entity by id used in the edit flow) and
return true only if at least one of those fields' values differ (use deep
equality for objects/arrays); update the call-sites (the edit handler that uses
shouldInvalidateStickySessionsOnProviderEdit, referenced around lines ~816) to
pass the existing provider state so the function can perform the comparison.

In `@src/lib/session-manager.ts`:
- Around line 2056-2072: The pipeline result handling in sessionIds collection
ignores per-command errors from pipeline.exec(), causing failed subcommands
(e.g., a zrange error) to be silently skipped; change the logic around
pipeline.exec() and the subsequent loop over results to explicitly detect and
log/handle errors for each entry (inspect the tuple items returned by
pipeline.exec(), identify error objects vs array results), e.g., check each
result for an Error or non-null error indicator before treating it as the zrange
response, surface or log the error with context (provider/id) and skip only when
safe, and ensure functions using sessionIds (the session deletion flow) account
for reported failures so deletions/counts remain accurate.

---

Nitpick comments:
In `@src/actions/provider-endpoints.ts`:
- Around line 541-551: The current edit logic uses
findEnabledProviderIdsByVendorAndType for any URL change and thus terminates
sessions for all providers of the vendor/type; instead, when parsed.data.url is
changed, call findEnabledProviderReferencesForVendorTypeUrl(endpoint.vendorId,
endpoint.providerType, oldUrlOrEndpointUrl) to get only providers that actually
reference that URL and pass those IDs to
terminateStickySessionsForProviders("editProviderEndpoint"); keep using
findEnabledProviderIdsByVendorAndType only for changes to sortOrder or isEnabled
where broad termination is intended. Ensure you determine the old URL vs new URL
as needed to query references precisely.

In `@tests/unit/repository/statistics-quota-costs-all-time.test.ts`:
- Around line 42-68: Extract the repeated drizzle-orm mock into a shared factory
function (e.g., createDrizzleMock) that returns the mocked members (relations,
eq, gte, inArray, isNull, lt, sql: sqlMock, and) and accepts a callback to
capture the `and` args so tests can set `capturedAndArgs`; then replace each
`vi.doMock("drizzle-orm", async () => { ... })` block with a minimal wrapper
that imports the actual module and spreads `...createDrizzleMock((args) => {
capturedAndArgs = args })` into the returned object, reusing the
`sqlMock`/`capturedAndArgs` behavior and eliminating the duplicated 27-line mock
setup.

In `@tests/unit/settings/providers/provider-form-endpoint-pool.test.tsx`:
- Around line 14-18: The import of ProviderDisplay, ProviderEndpoint, and
ProviderVendor is using a relative path "../../../../src/types/provider" which
violates the repo alias convention; update the import in
provider-form-endpoint-pool.test.tsx to use the path alias (import from
"@/types/provider") so the test references the types via the project's '@/...'
alias instead of the relative src path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f1264f87-e015-41a4-a0b8-db734b87d550

📥 Commits

Reviewing files that changed from the base of the PR and between 8c15634 and 1addbde.

📒 Files selected for processing (20)
  • messages/en/errors.json
  • messages/ja/errors.json
  • messages/ru/errors.json
  • messages/zh-CN/errors.json
  • messages/zh-TW/errors.json
  • src/actions/provider-endpoints.ts
  • src/actions/providers.ts
  • src/app/[locale]/dashboard/logs/_components/provider-chain-popover.test.tsx
  • src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
  • src/app/[locale]/settings/providers/_components/provider-endpoints-table.tsx
  • src/lib/provider-endpoint-error-codes.ts
  • src/lib/session-manager.ts
  • src/repository/provider-endpoints.ts
  • tests/unit/actions/provider-endpoints.test.ts
  • tests/unit/actions/providers.test.ts
  • tests/unit/lib/session-manager-terminate-provider-sessions.test.ts
  • tests/unit/repository/provider-endpoints-vendor-key.test.ts
  • tests/unit/repository/statistics-quota-costs-all-time.test.ts
  • tests/unit/settings/providers/provider-endpoints-table.test.tsx
  • tests/unit/settings/providers/provider-form-endpoint-pool.test.tsx

Comment thread src/actions/providers.ts Outdated
Comment on lines +201 to +203
function shouldInvalidateStickySessionsOnProviderEdit(data: Record<string, unknown>): boolean {
return Object.keys(data).some((key) => STICKY_SESSION_INVALIDATING_PROVIDER_FIELDS.has(key));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

会话失效条件按“字段存在”判断会误杀未变更会话

Line 202 当前只要 payload 里出现目标字段就会触发失效。即使值未变化(例如编辑表单原样提交),Line 816 仍会终止 sticky sessions,和“仅在路由面发生变化时失效”的目标不一致。

建议修改(基于“实际变更字段”触发)
-const STICKY_SESSION_INVALIDATING_PROVIDER_FIELDS = new Set([
-  "url",
-  "website_url",
-  "provider_type",
-  "group_tag",
-  "is_enabled",
-  "allowed_models",
-  "allowed_clients",
-  "blocked_clients",
-  "model_redirects",
-  "active_time_start",
-  "active_time_end",
-]);
+const STICKY_SESSION_INVALIDATING_PROVIDER_FIELDS = new Set<keyof Provider>([
+  "url",
+  "websiteUrl",
+  "providerType",
+  "groupTag",
+  "isEnabled",
+  "allowedModels",
+  "allowedClients",
+  "blockedClients",
+  "modelRedirects",
+  "activeTimeStart",
+  "activeTimeEnd",
+]);
 
-function shouldInvalidateStickySessionsOnProviderEdit(data: Record<string, unknown>): boolean {
-  return Object.keys(data).some((key) => STICKY_SESSION_INVALIDATING_PROVIDER_FIELDS.has(key));
+function shouldInvalidateStickySessionsOnProviderEdit(changedFields: Set<keyof Provider>): boolean {
+  return [...changedFields].some((key) => STICKY_SESSION_INVALIDATING_PROVIDER_FIELDS.has(key));
 }
+    const changedProviderFields = new Set<keyof Provider>();
     const preimageFields: Record<string, unknown> = {};
     for (const [field, nextValue] of Object.entries(payload)) {
       if (field === "key") {
         continue;
       }
@@
       if (!hasProviderFieldChangedForUndo(currentValue, nextValue)) {
         continue;
       }
 
       preimageFields[providerKey] = currentValue;
+      changedProviderFields.add(providerKey);
     }
@@
-    if (shouldInvalidateStickySessionsOnProviderEdit(payload)) {
+    if (shouldInvalidateStickySessionsOnProviderEdit(changedProviderFields)) {
       await terminateStickySessionsForProviders([providerId], "editProvider");
     }

Also applies to: 816-818

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

In `@src/actions/providers.ts` around lines 201 - 203, The current
shouldInvalidateStickySessionsOnProviderEdit function only checks presence of
keys in the payload and thus invalidates sticky sessions even when values are
unchanged; change it to detect actual value changes by comparing incoming data
fields (filtered to STICKY_SESSION_INVALIDATING_PROVIDER_FIELDS) against the
existing provider's current values (e.g., load the provider/entity by id used in
the edit flow) and return true only if at least one of those fields' values
differ (use deep equality for objects/arrays); update the call-sites (the edit
handler that uses shouldInvalidateStickySessionsOnProviderEdit, referenced
around lines ~816) to pass the existing provider state so the function can
perform the comparison.

Comment thread src/lib/session-manager.ts
Comment thread src/lib/session-manager.ts Outdated
}

const sessionIds = new Set<string>();
for (const [, result] of results) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] [ERROR-SILENT] terminateProviderSessionsBatch ignores per-command Redis pipeline errors

Evidence (src/lib/session-manager.ts:2062):

for (const [, result] of results) {
  if (!Array.isArray(result)) {
    continue;
  }

pipeline.exec() returns [err, result] tuples. Dropping err means individual zrange failures (wrong key type, moved slot, transient Redis error) will be silently skipped and session termination can become a no-op without any log.

Suggested fix:

for (const [index, [err, result]] of results.entries()) {
  const providerId = uniqueProviderIds[index];

  if (err) {
    logger.warn("SessionManager: Failed to read provider active sessions", {
      providerId,
      error: err,
    });
    continue;
  }

  if (!Array.isArray(result)) continue;

  for (const sessionId of result) {
    if (typeof sessionId === "string" && sessionId.trim()) {
      sessionIds.add(sessionId);
    }
  }
}

Comment thread src/actions/providers.ts Outdated
"active_time_end",
]);

function shouldInvalidateStickySessionsOnProviderEdit(data: Record<string, unknown>): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] [LOGIC-BUG] Sticky-session invalidation checks only for key presence, not value changes

Evidence (src/actions/providers.ts:201):

function shouldInvalidateStickySessionsOnProviderEdit(data: Record<string, unknown>): boolean {
  return Object.keys(data).some((key) => STICKY_SESSION_INVALIDATING_PROVIDER_FIELDS.has(key));
}

This triggers termination whenever the caller includes any of these fields, even if the value is unchanged (e.g., callers that submit a full provider payload on edit).

Suggested fix (compare against currentProvider before calling updateProvider):

const STICKY_SESSION_INVALIDATING_FIELD_TO_PROVIDER_KEY: Record<string, keyof Provider> = {
  url: "url",
  website_url: "websiteUrl",
  provider_type: "providerType",
  group_tag: "groupTag",
  is_enabled: "isEnabled",
  allowed_models: "allowedModels",
  allowed_clients: "allowedClients",
  blocked_clients: "blockedClients",
  model_redirects: "modelRedirects",
  active_time_start: "activeTimeStart",
  active_time_end: "activeTimeEnd",
};

function shouldInvalidateStickySessionsOnProviderEdit(
  next: Record<string, unknown>,
  current: Provider
): boolean {
  return Object.entries(next).some(([field, value]) => {
    const providerKey = STICKY_SESSION_INVALIDATING_FIELD_TO_PROVIDER_KEY[field];
    if (!providerKey) return false;
    return hasProviderFieldChangedForUndo(current[providerKey], value);
  });
}

// ...inside editProvider
if (shouldInvalidateStickySessionsOnProviderEdit(payload, currentProvider)) {
  await terminateStickySessionsForProviders([providerId], "editProvider");
}

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review Summary

This PR is size L and spans provider routing/session invalidation, endpoint pooling/vendor key logic, UI error surfacing, i18n message updates, and several test fixtures. The main risks are (1) sticky sessions being terminated more broadly than intended and (2) Redis pipeline per-command failures being ignored during termination.

PR Size: L

  • Lines changed: 824
  • Files changed: 20
  • Split suggestions:
    • PR 1: SessionManager.terminateProviderSessionsBatch + action hooks + unit tests
    • PR 2: vendor key/endpoint pooling changes in src/repository/provider-endpoints.ts + repo tests
    • PR 3: UI delete error translation + messages/*/errors.json + UI tests
    • PR 4: unrelated test fixture/mocking adjustments (e.g., statistics-quota-costs-all-time.test.ts)

Issues Found

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

Critical Issues (Must Fix)

None.

High Priority Issues (Should Fix)

  • src/actions/providers.ts:201 [HIGH] [LOGIC-BUG] Sticky-session invalidation checks only key presence (Confidence: 85/100)
  • src/lib/session-manager.ts:2062 [HIGH] [ERROR-SILENT] Redis pipeline per-command errors ignored in provider session termination (Confidence: 85/100)

Review Coverage

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

Automated review by Codex AI

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  • PR detected: #919 (fix: isolate provider endpoint pools and sticky sessions)
  • Applied size label: size/L (824 lines, 20 files)
  • Posted 2 inline review comments:
    • src/actions/providers.ts:201 — sticky-session invalidation triggers on key presence, not actual value changes (can terminate sessions on “no-op” edits)
    • src/lib/session-manager.ts:2062terminateProviderSessionsBatch ignores per-command Redis pipeline errors (can silently skip termination)
  • Submitted the required review summary via gh pr review --comment

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review Summary

This PR implements provider endpoint pool isolation and sticky session management with comprehensive test coverage. Overall, the changes are well-structured and address the linked issue #918 effectively.

PR Size: L

  • Lines changed: ~600 (after excluding test fixture churn)
  • Files changed: 18
  • Note: This is a well-scoped PR that touches related areas (endpoint management, session handling, and i18n). No split suggested.

Issues Found

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

Low Priority Issues (Author's Discretion)

1. Duplicate helper function across action files

  • File: src/actions/providers.ts (~line 226) and src/actions/provider-endpoints.ts (~line 113)
  • Issue: The terminateStickySessionsForProviders helper is duplicated identically in both files
  • Suggestion: Extract to a shared module (e.g., src/lib/session-manager.ts or a new shared utility) to avoid maintenance drift

2. Stale mock in test file

  • File: tests/unit/actions/provider-endpoints.test.ts (lines 15, 67, 87, 347)
  • Issue: hasEnabledProviderReferenceForVendorTypeUrlMock is still declared and configured, but the action code now uses findEnabledProviderReferencesForVendorTypeUrl instead
  • Suggestion: Remove the stale mock declarations and cleanup references

Review Coverage

  • Logic and correctness - Clean implementation of sticky session termination
  • Security (OWASP Top 10) - No security issues identified
  • Error handling - Proper try/catch with logging, graceful fallbacks
  • Type safety - Proper TypeScript types maintained
  • Documentation accuracy - Comments explain the intent clearly
  • Test coverage - Comprehensive tests added for new behaviors
  • Code clarity - Good separation of concerns, clear naming

Highlights

  1. Vendor key port preservation: The fix to normalizeWebsiteDomainKeyFromUrl correctly preserves explicit ports (e.g., example.com:3000) for self-hosted provider clustering
  2. Provider form isolation: Clean removal of vendor resolution in create/clone modes prevents unintended endpoint pool attachment
  3. Actionable delete errors: The new ENDPOINT_REFERENCED_BY_ENABLED_PROVIDERS error code with provider name summary improves UX significantly
  4. Session termination: Best-effort session termination with proper error logging and fallbacks

Recommendation

Approve with minor suggestions. The code duplication and stale mock are non-blocking but worth addressing for long-term maintainability.


Automated review by Claude AI

ding113 and others added 2 commits March 15, 2026 04:31
- Extract duplicate terminateStickySessionsForProviders into
  SessionManager static method (DRY)
- Fix sticky session invalidation to check actual value changes
  via preimageFields instead of raw payload keys (logic bug)
- Add missing allowed_clients/blocked_clients to preimage mapping
- Handle Redis pipeline errors in terminateProviderSessionsBatch
  instead of silently dropping them
- Remove stale hasEnabledProviderReferenceForVendorTypeUrlMock
  from tests
@ding113
ding113 merged commit d85cc5d into dev Mar 15, 2026
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Claude Code Hub Roadmap Mar 15, 2026
Comment on lines +629 to +636
const affectedProviderIds = await findEnabledProviderIdsByVendorAndType(
endpoint.vendorId,
endpoint.providerType
);
await SessionManager.terminateStickySessionsForProviders(
affectedProviderIds,
"removeProviderEndpoint"
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unguarded DB call after soft-delete can produce misleading error

findEnabledProviderIdsByVendorAndType is called outside any try-catch at line 629, after the endpoint has already been soft-deleted at line 610. Every other non-critical post-delete operation in this function (resetEndpointCircuitState, tryDeleteProviderVendorIfEmpty, publishProviderCacheInvalidation) is wrapped in its own try-catch precisely to prevent this scenario. If the DB query at line 629 throws (e.g. transient DB connectivity issue), the outer catch at line 660 returns { ok: false, errorCode: DELETE_FAILED } — even though the deletion already succeeded. The UI and the caller will believe the operation failed and likely show an error, while the endpoint is actually gone.

Suggested change
const affectedProviderIds = await findEnabledProviderIdsByVendorAndType(
endpoint.vendorId,
endpoint.providerType
);
await SessionManager.terminateStickySessionsForProviders(
affectedProviderIds,
"removeProviderEndpoint"
);
try {
const affectedProviderIds = await findEnabledProviderIdsByVendorAndType(
endpoint.vendorId,
endpoint.providerType
);
await SessionManager.terminateStickySessionsForProviders(
affectedProviderIds,
"removeProviderEndpoint"
);
} catch (error) {
logger.warn("removeProviderEndpoint:terminate_sessions_failed", {
endpointId: parsed.data.endpointId,
vendorId: endpoint.vendorId,
error: error instanceof Error ? error.message : String(error),
});
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/provider-endpoints.ts
Line: 629-636

Comment:
**Unguarded DB call after soft-delete can produce misleading error**

`findEnabledProviderIdsByVendorAndType` is called outside any try-catch at line 629, after the endpoint has already been soft-deleted at line 610. Every other non-critical post-delete operation in this function (`resetEndpointCircuitState`, `tryDeleteProviderVendorIfEmpty`, `publishProviderCacheInvalidation`) is wrapped in its own try-catch precisely to prevent this scenario. If the DB query at line 629 throws (e.g. transient DB connectivity issue), the outer catch at line 660 returns `{ ok: false, errorCode: DELETE_FAILED }` — even though the deletion already succeeded. The UI and the caller will believe the operation failed and likely show an error, while the endpoint is actually gone.

```suggestion
    try {
      const affectedProviderIds = await findEnabledProviderIdsByVendorAndType(
        endpoint.vendorId,
        endpoint.providerType
      );
      await SessionManager.terminateStickySessionsForProviders(
        affectedProviderIds,
        "removeProviderEndpoint"
      );
    } catch (error) {
      logger.warn("removeProviderEndpoint:terminate_sessions_failed", {
        endpointId: parsed.data.endpointId,
        vendorId: endpoint.vendorId,
        error: error instanceof Error ? error.message : String(error),
      });
    }
```

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

Comment on lines +2076 to 2121
}
}
}

if (sessionIds.size === 0) {
return 0;
}

const terminatedCount = await SessionManager.terminateSessionsBatch([...sessionIds]);
logger.info("SessionManager: Terminated provider sessions batch", {
providerIds: uniqueProviderIds,
sessionCount: sessionIds.size,
terminatedCount,
});
return terminatedCount;
} catch (error) {
logger.error("SessionManager: Failed to terminate provider sessions batch", {
error,
providerIds: uniqueProviderIds,
});
return 0;
}
}

static async terminateStickySessionsForProviders(
providerIds: number[],
context: string
): Promise<void> {
const uniqueProviderIds = Array.from(
new Set(providerIds.filter((providerId) => Number.isInteger(providerId) && providerId > 0))
);
if (uniqueProviderIds.length === 0) {
return;
}

try {
await SessionManager.terminateProviderSessionsBatch(uniqueProviderIds);
} catch (error) {
logger.warn(`${context}:terminate_provider_sessions_failed`, {
providerIds: uniqueProviderIds,
error: error instanceof Error ? error.message : String(error),
});
}
}

/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Dead catch block in terminateStickySessionsForProviders

terminateProviderSessionsBatch internally catches all errors (via its own try-catch at the bottom of the method) and always returns 0 on failure — it never propagates exceptions. As a result, the catch block in terminateStickySessionsForProviders can never be triggered, making it unreachable dead code. This could mislead future maintainers into thinking the wrapper provides meaningful error isolation that it does not.

If the goal is to provide a fire-and-forget, non-throwing wrapper, consider documenting that terminateProviderSessionsBatch is already non-throwing, and simplify:

static async terminateStickySessionsForProviders(
  providerIds: number[],
  context: string
): Promise<void> {
  const uniqueProviderIds = Array.from(
    new Set(providerIds.filter((id) => Number.isInteger(id) && id > 0))
  );
  if (uniqueProviderIds.length === 0) return;
  // terminateProviderSessionsBatch is non-throwing; it catches all errors internally
  await SessionManager.terminateProviderSessionsBatch(uniqueProviderIds);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/session-manager.ts
Line: 2076-2121

Comment:
**Dead `catch` block in `terminateStickySessionsForProviders`**

`terminateProviderSessionsBatch` internally catches all errors (via its own try-catch at the bottom of the method) and always returns `0` on failure — it never propagates exceptions. As a result, the `catch` block in `terminateStickySessionsForProviders` can never be triggered, making it unreachable dead code. This could mislead future maintainers into thinking the wrapper provides meaningful error isolation that it does not.

If the goal is to provide a fire-and-forget, non-throwing wrapper, consider documenting that `terminateProviderSessionsBatch` is already non-throwing, and simplify:

```ts
static async terminateStickySessionsForProviders(
  providerIds: number[],
  context: string
): Promise<void> {
  const uniqueProviderIds = Array.from(
    new Set(providerIds.filter((id) => Number.isInteger(id) && id > 0))
  );
  if (uniqueProviderIds.length === 0) return;
  // terminateProviderSessionsBatch is non-throwing; it catches all errors internally
  await SessionManager.terminateProviderSessionsBatch(uniqueProviderIds);
}
```

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/actions/provider-endpoints.ts (1)

523-536: ⚠️ Potential issue | 🟠 Major

只在值真的变化时再终止 sticky sessions。

这里还是按字段“是否出现在 payload 里”判断。管理员如果原样提交 url / sortOrder / isEnabled,即使路由面没有变化,也会把同 vendor/type 下的会话全部清掉,和前面 provider 编辑路径里已经修过的问题是同一类。

可直接改成和 `previous` 的实际值比较
-    const shouldTerminateStickySessions =
-      parsed.data.url !== undefined ||
-      parsed.data.sortOrder !== undefined ||
-      parsed.data.isEnabled !== undefined;
+    const shouldTerminateStickySessions =
+      (parsed.data.url !== undefined && parsed.data.url !== previous.url) ||
+      (parsed.data.sortOrder !== undefined &&
+        parsed.data.sortOrder !== previous.sortOrder) ||
+      (parsed.data.isEnabled !== undefined && parsed.data.isEnabled !== previous.isEnabled);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/provider-endpoints.ts` around lines 523 - 536, The current check
uses presence in parsed.data to decide termination, which wipes sessions even if
values are unchanged; update the shouldTerminateStickySessions logic to compare
parsed.data fields to the existing endpoint values instead (e.g. check
parsed.data.url !== undefined && parsed.data.url !== endpoint.url,
parsed.data.sortOrder !== undefined && parsed.data.sortOrder !==
endpoint.sortOrder, parsed.data.isEnabled !== undefined && parsed.data.isEnabled
!== endpoint.isEnabled) and only call findEnabledProviderIdsByVendorAndType and
SessionManager.terminateStickySessionsForProviders when at least one of those
real-value comparisons indicates a change.
🧹 Nitpick comments (1)
tests/unit/actions/providers.test.ts (1)

579-595: allowed_clients / blocked_clients 也单独锁进回归测试。

这条用例目前只会在 group_tagallowed_models 回归时失败;而本次源码真正新增的 preimage 映射是 allowed_clients / blocked_clients。如果这两个 key 以后再次漏掉,这里不会报警。

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

In `@tests/unit/actions/providers.test.ts` around lines 579 - 595, The test only
asserts termination when group_tag or allowed_models change, so extend this test
(or add a new one) to also exercise edits to allowed_clients and
blocked_clients: call editProvider(1, { allowed_clients: [...], blocked_clients:
[...] }) (or include them in the existing payload), assert updateProviderMock
was called with objectContaining these keys (allowed_clients, blocked_clients)
and assert terminateProviderSessionsBatchMock was called with [1],
"editProvider"; reference the existing editProvider, updateProviderMock and
terminateProviderSessionsBatchMock symbols when adding the assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/actions/provider-endpoints.ts`:
- Around line 227-244: The function formatProviderReferenceSummary is assembling
user-facing summary text (e.g., "A, B, C +1") which bypasses i18n; instead,
change formatProviderReferenceSummary (and the similar logic around the other
occurrence) to stop returning formatted strings and return raw data for the
presentation layer — e.g., return an object with unique provider names array and
a total count (or just the names array and length) so the UI can compose
localized text using errorCode/errorParams; remove any hardcoded concatenation
or "+N" formatting from the action layer.

---

Duplicate comments:
In `@src/actions/provider-endpoints.ts`:
- Around line 523-536: The current check uses presence in parsed.data to decide
termination, which wipes sessions even if values are unchanged; update the
shouldTerminateStickySessions logic to compare parsed.data fields to the
existing endpoint values instead (e.g. check parsed.data.url !== undefined &&
parsed.data.url !== endpoint.url, parsed.data.sortOrder !== undefined &&
parsed.data.sortOrder !== endpoint.sortOrder, parsed.data.isEnabled !==
undefined && parsed.data.isEnabled !== endpoint.isEnabled) and only call
findEnabledProviderIdsByVendorAndType and
SessionManager.terminateStickySessionsForProviders when at least one of those
real-value comparisons indicates a change.

---

Nitpick comments:
In `@tests/unit/actions/providers.test.ts`:
- Around line 579-595: The test only asserts termination when group_tag or
allowed_models change, so extend this test (or add a new one) to also exercise
edits to allowed_clients and blocked_clients: call editProvider(1, {
allowed_clients: [...], blocked_clients: [...] }) (or include them in the
existing payload), assert updateProviderMock was called with objectContaining
these keys (allowed_clients, blocked_clients) and assert
terminateProviderSessionsBatchMock was called with [1], "editProvider";
reference the existing editProvider, updateProviderMock and
terminateProviderSessionsBatchMock symbols when adding the assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2b8467ae-d0de-4b39-9815-c06891065c8b

📥 Commits

Reviewing files that changed from the base of the PR and between 1addbde and 22c68ac.

📒 Files selected for processing (5)
  • src/actions/provider-endpoints.ts
  • src/actions/providers.ts
  • src/lib/session-manager.ts
  • tests/unit/actions/provider-endpoints.test.ts
  • tests/unit/actions/providers.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/session-manager.ts

Comment on lines +227 to +244
function formatProviderReferenceSummary(
references: Array<{ id: number; name: string }>,
maxDisplayCount: number = 3
): string {
const uniqueNames = Array.from(
new Set(references.map((reference) => reference.name.trim()).filter(Boolean))
);
if (uniqueNames.length === 0) {
return "";
}

if (uniqueNames.length <= maxDisplayCount) {
return uniqueNames.join(", ");
}

const displayed = uniqueNames.slice(0, maxDisplayCount).join(", ");
return `${displayed} +${uniqueNames.length - maxDisplayCount}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

不要在 action 层拼装新的用户可见文案。

这里新增的 error 中文句子,以及 providers"A, B, C +1" 摘要,都会绕过现有 i18n 流程。既然已经有 errorCodeerrorParams,更稳妥的是只返回原始数据(例如 provider names/count),由展示层按 locale 组装文案。

As per coding guidelines, "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text"

Also applies to: 593-606

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

In `@src/actions/provider-endpoints.ts` around lines 227 - 244, The function
formatProviderReferenceSummary is assembling user-facing summary text (e.g., "A,
B, C +1") which bypasses i18n; instead, change formatProviderReferenceSummary
(and the similar logic around the other occurrence) to stop returning formatted
strings and return raw data for the presentation layer — e.g., return an object
with unique provider names array and a total count (or just the names array and
length) so the UI can compose localized text using errorCode/errorParams; remove
any hardcoded concatenation or "+N" formatting from the action layer.

@github-actions github-actions Bot mentioned this pull request Mar 18, 2026
4 tasks
@ding113
ding113 deleted the fix/provider-endpoint-isolation-and-sticky-session branch May 13, 2026 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:i18n area:provider area:session bug Something isn't working size/L Large PR (< 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant