Skip to content

feat: support provider model redirect rules#993

Merged
ding113 merged 5 commits intodevfrom
feat/provider-model-routing-rules
Apr 4, 2026
Merged

feat: support provider model redirect rules#993
ding113 merged 5 commits intodevfrom
feat/provider-model-routing-rules

Conversation

@ding113
Copy link
Copy Markdown
Owner

@ding113 ding113 commented Apr 4, 2026

Summary

  • Upgrade provider model redirects from exact map objects (Record<string, string>) to ordered rule lists with exact/prefix/suffix/contains/regex matching
  • Normalize legacy stored redirect maps into exact rules while keeping batch patch, actions, repository, and runtime routing consistent
  • Improve provider settings UI so whitelist entries are fully editable outside the dropdown and selected models are pinned at the top of the picker

Problem

The previous model redirect system only supported exact string matching (modelRedirects: {"claude-3-opus": "glm-4.6"}). This was insufficient for scenarios where a single rule should cover a family of model names (e.g., all claude-opus-4-* variants) or pattern-based redirects. Users had to manually create one redirect entry per model name, which was error-prone and difficult to maintain.

Additionally, the model whitelist UI required opening the dropdown to edit or remove selected models, making management cumbersome.

Related Issues and PRs:

Solution

Replace the Record<string, string> data model with an ordered ProviderModelRedirectRule[] array where each rule specifies a matchType (exact, prefix, suffix, contains, regex), a source pattern, and a target model name. Rules are evaluated in order and the first match wins.

Key design decisions:

  • Backward-compatible reads: The DB schema accepts both old Record<string, string> and new ProviderModelRedirectRule[] formats; normalizeProviderModelRedirectRules() converts legacy maps to exact-match rules on read
  • ReDoS protection: Regex rules are validated against unsafe patterns using safe-regex at both the Zod schema level and the UI level
  • Ordered evaluation: Rules are evaluated top-to-bottom; the UI provides up/down reordering controls
  • Input validation: Source and target are capped at 255 characters; regex patterns are validated for syntax and safety

Changes

Core Changes

  • src/types/provider.ts (+15/-7): New ProviderModelRedirectMatchType union type and ProviderModelRedirectRule interface; all provider types updated from Record<string, string> to ProviderModelRedirectRule[]
  • src/lib/provider-model-redirects.ts (+138, new): Core matching engine with normalizeProviderModelRedirectRules(), findMatchingProviderModelRedirectRule(), matchesProviderModelRedirectRule(), and getProviderModelRedirectTarget()
  • src/lib/provider-model-redirect-schema.ts (+56, new): Zod validation schemas with ReDoS protection via safe-regex
  • src/app/v1/_lib/proxy/model-redirector.ts (+22/-5): Replaced map lookup with rule-based matching; logs matchType and matchedSource for audit; passes matchedRule in provider chain metadata
  • src/drizzle/schema.ts (+4/-2): DB schema accepts both old and new formats: ProviderModelRedirectRule[] | Record<string, string> | null
  • src/types/message.ts (+5): Added matchedRule field to ProviderChainItem.modelRedirect for audit trail

API & Validation Changes

  • src/actions/providers.ts (+19/-4): batchUpdateProviders validates rules with PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA and normalizes them
  • src/lib/validation/schemas.ts (+3/-2): CreateProviderSchema and UpdateProviderSchema use new rule schema
  • src/lib/provider-patch-contract.ts (+2/-11): Replaced isStringRecord() with PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA validation
  • src/repository/_shared/transformers.ts (+2/-1): toProvider() normalizes legacy maps on read
  • src/repository/provider.ts (+4/-2): createProvider() normalizes input redirects
  • src/app/api/actions/[...route]/route.ts (+5/-1): OpenAPI response schema updated to array format

UI Changes

  • model-redirect-editor.tsx (+359/-135): Complete rewrite with match type selector (exact/prefix/suffix/contains/regex), rule reordering (up/down), inline editing of all rule fields, regex validation with ReDoS protection, and ordered evaluation hint
  • model-multi-select.tsx (+330/-142): Selected models now displayed as editable list outside the dropdown; selected models pinned at top of dropdown picker; inline edit and remove buttons per model

Supporting Changes

  • Provider forms (provider-form.legacy.tsx, provider-form/index.tsx, provider-form-context.tsx, provider-form-types.ts, routing-section.tsx): Updated to use ProviderModelRedirectRule[] instead of Record<string, string>
  • Batch edit (analyze-batch-settings.ts, build-patch-draft.ts): Updated field analysis and patch draft building
  • i18n (5 languages): New translation keys for match type labels, validation messages, regex errors, and UI hints

Test Changes

  • tests/unit/lib/provider-model-redirects.test.ts (+78, new): Unit tests for matching engine (prefix/suffix/contains/regex), null-match, and legacy normalization
  • tests/unit/settings/providers/model-redirect-editor.test.tsx (+171, new): UI tests for rule editing after deletion and source length validation
  • tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx (+111/-1): Tests for out-of-dropdown editing and selected models pinned section
  • tests/unit/actions/providers-patch-contract.test.ts (+26/-4): Tests for new rule validation (accepts rules, rejects unsafe regex, rejects overlong source)
  • tests/unit/actions/providers-batch-field-mapping.test.ts (+19/-3): Updated batch mapping tests for new format
  • Updated 7 additional test files to use new ProviderModelRedirectRule[] format

Breaking Changes

Change Impact Migration
API input format changed from Record<string, string> to ProviderModelRedirectRule[] API consumers using addProvider/editProvider with model_redirects must use new format Change {"claude-3-opus": "glm-4.6"} to [{"matchType":"exact","source":"claude-3-opus","target":"glm-4.6"}]
API response format changed Consumers reading model_redirects from provider responses receive array instead of object Update parsing to handle ProviderModelRedirectRule[]

Note: Existing database records in Record<string, string> format are automatically normalized to exact-match rules on read. No database migration is required.

Testing

Automated Tests

  • Unit tests for redirect matching engine (new file)
  • Unit tests for redirect editor UI (new file)
  • Unit tests for multi-select UI enhancements (extended)
  • Unit tests for patch contract validation (extended)
  • Unit tests for batch field mapping (updated)
  • Integration tests for batch edit prefill (updated)
  • All existing tests updated for new data format

Verification

  • bun run lint -- --max-diagnostics 60
  • bun run typecheck
  • bunx vitest run tests/unit/lib/provider-model-redirects.test.ts tests/unit/settings/providers/model-redirect-editor.test.tsx tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx tests/unit/actions/providers-patch-contract.test.ts tests/unit/actions/providers-batch-field-mapping.test.ts
  • bun run test
  • bun run build

Checklist

  • Code follows project conventions
  • i18n strings added for all 5 languages
  • ReDoS protection for regex patterns
  • Backward-compatible with existing DB records
  • Tests pass locally

Description enhanced by Claude AI

Greptile Summary

This PR upgrades model redirect rules from a flat Record<string, string> to an ordered ProviderModelRedirectRule[] with five match types (exact, prefix, suffix, contains, regex), backward-compatible normalization of legacy DB records, ReDoS protection via safe-regex, and a fully rewritten UI editor with rule reordering and inline editing. The previous P1 concerns around safe-regex being called before regex-syntax validation (in both the Zod schema and the UI validateRule) have been addressed in this revision — the new RegExp check now runs first with an early return, so safeRegex is only reached on syntactically valid patterns.

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 style/design suggestions that do not block correctness.

The previously flagged P1 concerns (safe-regex called before syntax validation in both schema and UI) are resolved in this revision — new RegExp runs first with an early return, so safeRegex is only reached on syntactically valid patterns. The two new findings (case-insensitive dedup for regex rules, one hardcoded Chinese error string) are P2 and do not affect runtime correctness.

src/lib/provider-model-redirect-schema.ts (dedup key case-sensitivity for regex), src/actions/providers.ts (i18n for one error string)

Important Files Changed

Filename Overview
src/lib/provider-model-redirects.ts New matching engine with exact/prefix/suffix/contains/regex rule evaluation; correctly normalizes legacy Record maps to exact-match rules on read
src/lib/provider-model-redirect-schema.ts Zod schema with ReDoS protection; syntax check correctly precedes safeRegex; dedup key lowercases source for all match types including regex, which over-restricts valid distinct regex patterns
src/app/v1/_lib/proxy/model-redirector.ts Proxy-path rule application updated to use rule-based matching; logs matchType and matchedSource; correctly attaches matchedRule to provider chain audit trail
src/types/provider.ts New ProviderModelRedirectMatchType union and ProviderModelRedirectRule interface; all provider-facing types migrated from Record<string,string> to ProviderModelRedirectRule[]
src/app/[locale]/settings/providers/_components/model-redirect-editor.tsx Full UI rewrite with match type selector, rule reordering, inline edit; onInput handlers alongside onChange are redundant dead code (previously flagged)
src/actions/providers.ts batchUpdateProviders validates and normalizes redirect rules with Zod schema; one new error string is hardcoded in Chinese rather than using i18n
src/repository/_shared/transformers.ts toProvider now calls normalizeProviderModelRedirectRules on DB read, enabling transparent backward-compatible legacy format conversion
src/drizzle/schema.ts DB column type widened to accept both ProviderModelRedirectRule[] and legacy Record<string,string> for backward compatibility with no migration required
src/lib/provider-patch-contract.ts model_redirects field validation replaced with PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA; isValidSetValue updated accordingly
tests/unit/lib/provider-model-redirects.test.ts New unit tests cover ordered prefix/suffix/contains/regex matching, null-match, and legacy Record normalization

Sequence Diagram

sequenceDiagram
    participant Client
    participant ProxyHandler
    participant ModelRedirector
    participant MatchEngine as provider-model-redirects
    participant Upstream

    Client->>ProxyHandler: POST /v1/messages {model: "claude-opus-4"}
    ProxyHandler->>ModelRedirector: apply(session, provider)
    ModelRedirector->>MatchEngine: findMatchingProviderModelRedirectRule(model, rules)
    loop For each rule (in order)
        MatchEngine->>MatchEngine: matchesProviderModelRedirectRule(model, rule)
        Note over MatchEngine: exact / prefix / suffix / contains / regex
    end
    MatchEngine-->>ModelRedirector: matched rule {matchType, source, target}
    ModelRedirector->>ModelRedirector: setOriginalModel("claude-opus-4")
    ModelRedirector->>ModelRedirector: session.request.model = "glm-4.6"
    ModelRedirector->>ModelRedirector: attachCurrentModelRedirectToLastChainItem()
    ModelRedirector-->>ProxyHandler: redirected=true
    ProxyHandler->>Upstream: POST {model: "glm-4.6"}
    Upstream-->>ProxyHandler: Response
    ProxyHandler-->>Client: Response (billed as claude-opus-4)
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/lib/provider-model-redirect-schema.ts
Line: 64-76

Comment:
**Case-insensitive dedup rejects legitimately distinct regex rules**

The duplicate-detection key lowercases `rule.source`, but `matchesProviderModelRedirectRule` compares case-sensitively. For `matchType: "regex"`, two patterns that differ only by case — e.g. `^Claude-Opus-4$` vs `^claude-opus-4$` — are different patterns that match different strings, but this check treats them as identical and rejects the pair. For `exact`/`prefix`/`suffix`/`contains` the over-restriction is cosmetic; for `regex` it blocks valid rule combinations.

Consider skipping the `.toLowerCase()` for the `regex` match type:

```ts
const key =
  rule.matchType === "regex"
    ? `${rule.matchType}:${rule.source.trim()}`
    : `${rule.matchType}:${rule.source.trim().toLowerCase()}`;
```

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/providers.ts
Line: 2257

Comment:
**Hardcoded Chinese error string violates i18n requirement**

The error message `"模型重定向规则格式无效"` is a raw Chinese literal. CLAUDE.md requires all user-facing strings to go through i18n (5 languages). This string surfaces in the UI toast when batch-patching providers with an invalid redirect rule array.

```suggestion
          return { ok: false, error: "Invalid model redirect rule format" };
```
(Or, if server-action error strings have a dedicated i18n key path in this codebase, use that mechanism instead.)

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

Reviews (4): Last reviewed commit: "fix: harden provider redirect validation" | Re-trigger Greptile

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

将 provider 的 modelRedirects 数据模型从键值映射迁移为有序规则数组(ProviderModelRedirectRule[]),引入匹配类型、规则验证/规范化与安全检查、规则应用/查找工具、编辑/重排 UI、对应本地化字符串及大量测试更新(不超过50字)。

Changes

Cohort / File(s) Summary
本地化消息文件
messages/en/settings/providers/form/modelRedirect.json, messages/en/settings/providers/form/modelSelect.json, messages/ja/settings/providers/form/modelRedirect.json, messages/ja/settings/providers/form/modelSelect.json, messages/ru/settings/providers/form/modelRedirect.json, messages/ru/settings/providers/form/modelSelect.json, messages/zh-CN/settings/providers/form/modelRedirect.json, messages/zh-CN/settings/providers/form/modelSelect.json, messages/zh-TW/settings/providers/form/modelRedirect.json, messages/zh-TW/settings/providers/form/modelSelect.json
新增/扩展多语言重定向规则与模型选择相关字符串(匹配类型、排序提示、编辑/移动/删除操作文本、长度与正则校验错误等)。
类型定义
src/types/provider.ts, src/types/message.ts, src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts
新增 ProviderModelRedirectMatchTypeProviderModelRedirectRule;将多处 modelRedirectsRecord<string,string> 切换为 ProviderModelRedirectRule[],并在 ProviderChainItem 中加入 matchedRule 元数据。
Zod 验证模式
src/lib/provider-model-redirect-schema.ts, src/lib/validation/schemas.ts, src/lib/provider-patch-contract.ts
新增单条规则与规则列表 Zod schema(matchType 枚举、长度限制、regex 语法与 ReDoS 安全检查、列表上限 100),并将 provider 创建/更新/补丁验证改为使用该模式。
规则实现与工具函数
src/lib/provider-model-redirects.ts
新增模块:规则类型守卫、normalize(兼容旧对象映射)、has/匹配/查找首个规则、获取重定向目标,包含对无效或不安全正则的安全处理。
重定向应用逻辑
src/app/v1/_lib/proxy/model-redirector.ts
重定向查找与应用改为基于规则数组(findMatchingProviderModelRedirectRule / getProviderModelRedirectTarget),记录 matchedRule 并在日志/会话中保存匹配元数据。
表单组件 — 重定向编辑器
src/app/[locale]/settings/providers/_components/model-redirect-editor.tsx
编辑器数据模型改为规则数组;新增 matchType 选择、增删改、上下移动、综合校验(空、长度、重复、regex 语法与安全)、编辑冲突检测等交互与校验逻辑;组件 props 类型随之改变。
表单组件 — 模型多选管理
src/app/[locale]/settings/providers/_components/model-multi-select.tsx
新增已选模型内联编辑/删除、错误管理、分组渲染与交互处理(保存/取消/重复校验等),调整可选/已选列表渲染与状态重置行为。
表单数据流与上下文
src/app/[locale]/settings/providers/_components/forms/provider-form.legacy.tsx, src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx, src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx, src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
初始化、提交、重置与“已配置”判定改为基于数组([])与 length 判定,默认值从 {} 统一改为 [] 并使用规范化函数。
API 与后端操作
src/actions/providers.ts, src/app/api/actions/[...route]/route.ts
将 add/edit/batchUpdate 与 OpenAPI 响应的 model_redirects 验证/类型改为规则数组;batchUpdateProviders 增加规则列表解析/验证与规范化处理,返回格式化错误信息。
数据库与仓储
src/drizzle/schema.ts, src/repository/provider.ts, src/repository/_shared/transformers.ts
调整 providers 表的 TypeScript 表示以允许规则数组(兼容旧映射);在创建/转换时应用规范化以保证输出为规则数组或 null。
批量编辑支持
src/app/[locale]/settings/providers/_components/batch-edit/analyze-batch-settings.ts, src/app/[locale]/settings/providers/_components/batch-edit/build-patch-draft.ts
将批量分析与 patch 草稿构建中的 modelRedirects 处理改为数组,使用 length 判定空值并保留列表合并/清除逻辑。
补充更改与测试
多处测试与实现文件(例如 src/lib/provider-patch-contract.ts、tests/** 下的多个文件、组件 props 类型调整等)
同步修改并新增单元/集成测试以覆盖新规则模型、规范化路径、正则语法与 ReDoS 校验、编辑行为与 UI 交互。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 标题清晰准确地概括了此次 PR 的主要变化:将提供商模型重定向从简单的映射格式升级为支持多种匹配类型的规则列表。
Description check ✅ Passed PR 描述详尽完整,清晰说明了问题、解决方案、核心变更、API 变更、UI 变更、测试覆盖和兼容性迁移路径,内容与 changeset 密切相关。

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/provider-model-routing-rules

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.

Copy link
Copy Markdown
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 upgrades the model redirection system from a simple key-value mapping to a structured rule-based system. It introduces support for multiple match types—exact, prefix, suffix, contains, and regex—allowing for more flexible model routing. Key changes include the addition of Zod schemas for validation, a centralized matching utility, and significant UI updates to the provider form and model selection components to support rule management. Review feedback suggests optimizing regex performance by caching compiled expressions, localizing hardcoded error strings in the backend, and removing redundant event handlers in the frontend components.

updates.model_redirects
);
if (!parsedRedirectRules.success) {
return { ok: false, 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

The error message is hardcoded in Chinese. In a localized application, it's better to use a translation key or at least English as the default to ensure consistency across different locales, especially since the rest of the codebase appears to use English for logic and types.

Suggested change
return { ok: false, error: "模型重定向规则格式无效" };
return { ok: false, error: "Invalid model redirect rules format" };

return model.includes(rule.source);
case "regex":
try {
return new RegExp(rule.source).test(model);
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

Compiling a new RegExp on every request can be a performance bottleneck, especially in a high-throughput proxy path with multiple rules. Consider caching the compiled RegExp objects (e.g., using a Map keyed by the source string) to improve efficiency.

Comment on lines +473 to +474
onChange={(e) => setEditValue(e.target.value)}
onInput={(e) => setEditValue((e.target as HTMLInputElement).value)}
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

The onInput handler is redundant here because React's onChange for input elements already fires on every keystroke. Removing it simplifies the code without changing behavior.

Suggested change
onChange={(e) => setEditValue(e.target.value)}
onInput={(e) => setEditValue((e.target as HTMLInputElement).value)}
onChange={(e) => setEditValue(e.target.value)}

@github-actions github-actions bot added the size/L Large PR (< 1000 lines) label Apr 4, 2026
Copy link
Copy Markdown
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 implements a well-structured upgrade to provider model redirect functionality, transitioning from exact map objects to ordered rule lists with support for exact/prefix/suffix/contains/regex matching. The implementation demonstrates solid engineering practices with proper backwards compatibility handling, comprehensive test coverage, and adherence to project standards.

PR Size: L

  • Lines changed: 1915 (1537 additions, 378 deletions)
  • Files changed: 42

Split Suggestion for Future: This PR touches UI components, core library logic, schema definitions, actions, and repository layers. While the changes are cohesive, future similar features could be split into: (1) core library + schema changes, (2) UI components, (3) integration/wiring.

Review Coverage Summary

Category Status
Logic and correctness Clean - rule matching logic is sound with proper ordering
Security (OWASP Top 10) Clean - ReDoS protection via safe-regex, input validation via Zod
Error handling Clean - appropriate error handling with fallback behavior
Type safety Clean - proper TypeScript types, no unsafe any usage in new code
Documentation accuracy Clean - comments match implementation
Test coverage Adequate - unit tests for core redirect logic and UI components
Code clarity Good - clear naming, logical structure
i18n compliance Clean - all 5 language files updated appropriately

Notable Implementation Highlights

  1. Backwards Compatibility: The normalizeProviderModelRedirectRules function elegantly handles legacy map-to-array migration without breaking changes.

  2. Security Considerations: Proper ReDoS protection using safe-regex for regex patterns, with 255-character limits on source/target strings.

  3. Test Coverage: Includes tests for rule ordering, match types, legacy normalization, and UI interaction edge cases.

  4. Standards Compliance: Follows CLAUDE.md guidelines - no emoji, proper i18n usage via next-intl, appropriate test structure.

Minor Observations (Non-blocking)

  • The silent handling of invalid regex patterns in matchesProviderModelRedirectRule (returning false on catch) is intentional defensive programming - invalid patterns simply don't match rather than crashing the request pipeline.
  • Rule order precedence is well-tested and documented via the orderHint i18n key.

Conclusion

No blocking issues identified. The PR is approved for merge to dev.


Reviewed by Claude AI

Comment on lines +105 to +110
case "regex":
try {
return new RegExp(rule.source).test(model);
} catch {
return false;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Regex recompiled on every proxy request

For the regex match type, new RegExp(rule.source) is constructed on every call to matchesProviderModelRedirectRule. This function sits in the hot proxy path and is invoked for every forwarded request; providers with regex redirect rules pay regex-compilation overhead on each call. A small module-level Map<string, RegExp> cache keyed on rule.source avoids repeated compilation with no correctness risk, since rule sources are already validated as safe before storage.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/provider-model-redirects.ts
Line: 105-110

Comment:
**Regex recompiled on every proxy request**

For the `regex` match type, `new RegExp(rule.source)` is constructed on every call to `matchesProviderModelRedirectRule`. This function sits in the hot proxy path and is invoked for every forwarded request; providers with regex redirect rules pay regex-compilation overhead on each call. A small module-level `Map<string, RegExp>` cache keyed on `rule.source` avoids repeated compilation with no correctness risk, since rule sources are already validated as safe before storage.

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

Comment on lines +259 to +291
onChange={(e) =>
setEditRule((current) => ({ ...current, source: e.target.value }))
}
onInput={(e) =>
setEditRule((current) => ({
...current,
source: (e.target as HTMLInputElement).value,
}))
}
onKeyDown={(e) => handleEditKeyDown(e, ruleKey)}
disabled={disabled}
className="font-mono text-sm h-8 flex-1"
autoFocus
/>
</div>

{isEditing ? (
<div className="flex items-center gap-1">
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleSaveEdit(source)}
disabled={disabled || !editTarget.trim()}
className="h-6 w-6 p-0"
>
<Check className="h-3 w-3 text-green-600" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={handleCancelEdit}
disabled={disabled}
className="h-6 w-6 p-0"
>
<X className="h-3 w-3 text-muted-foreground" />
</Button>
<div className="hidden md:flex items-center justify-center pb-2">
<ArrowRight className="h-3 w-3 text-muted-foreground" />
</div>

<div className="space-y-1">
<Label className="text-xs">{t("targetModel")}</Label>
<Input
value={editRule.target}
data-redirect-edit-target={ruleKey}
onChange={(e) =>
setEditRule((current) => ({ ...current, target: e.target.value }))
}
onInput={(e) =>
setEditRule((current) => ({
...current,
target: (e.target as HTMLInputElement).value,
}))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Redundant onInput alongside onChange

In React 17+, onChange on <input> is internally mapped to the native input event and fires on every keystroke — the exact same event that onInput handles. Both handlers here update state with the identical value, making the four onInput callbacks (edit-mode source/target and create-mode source/target at lines 428-434 and 452-458) dead code. Removing them simplifies the component without changing behaviour.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/model-redirect-editor.tsx
Line: 259-291

Comment:
**Redundant `onInput` alongside `onChange`**

In React 17+, `onChange` on `<input>` is internally mapped to the native `input` event and fires on every keystroke — the exact same event that `onInput` handles. Both handlers here update state with the identical value, making the four `onInput` callbacks (edit-mode source/target and create-mode source/target at lines 428-434 and 452-458) dead code. Removing them simplifies the component without changing behaviour.

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

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7f6045e083

ℹ️ About Codex in GitHub

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

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

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

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

Comment on lines 280 to +281
case "model_redirects":
return isStringRecord(value);
return PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA.safeParse(value).success;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Carry parsed redirect rules into batch patch values

model_redirects is validated with PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA, but only .success is used here; the parsed payload (which applies .trim() and other normalization) is discarded, and later normalizePatchField stores the original input.set. In the preview/apply batch patch flow, this accepts values like " source " yet persists them with whitespace, so redirect matching can silently fail for exact/prefix/suffix rules even though validation passed. Persist the parsed/normalized data instead of the raw input.

Useful? React with 👍 / 👎.

Comment on lines +48 to +49
function getRuleIdentity(rule: Pick<ProviderModelRedirectRule, "matchType" | "source">): string {
return `${rule.matchType}:${rule.source.trim()}`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use collision-free identity for redirect rule rows

The editor identifies rules only by matchType + source, so two rules sharing that pair collide in UI state and list keys; when such data exists (the backend schema currently allows duplicate entries), operations like remove/move/edit can target multiple rows unintentionally or become ambiguous. This is a regression from the old map-based shape where key uniqueness was guaranteed, and it can corrupt rule order/content when loading provider configs created via API/import.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@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

Caution

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

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

49-67: ⚠️ Potential issue | 🟠 Major

运行时直接执行持久化 regex 仍然有 ReDoS 风险。

这里会把数据库中的规则交给 findMatchingProviderModelRedirectRule(...) / getProviderModelRedirectTarget(...),而底层匹配最终是 new RegExp(rule.source).test(model)。写入端的 unsafe-regex 校验挡不住历史脏数据、手工导入或绕过 schema 的写入;一条灾难性回溯表达式就可能卡住代理热路径。建议在 provider 读出/归一化阶段再次做安全校验并丢弃不安全 regex,或者预编译后只让运行时消费已验证规则。

Also applies to: 160-171

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

In `@src/app/v1/_lib/proxy/model-redirector.ts` around lines 49 - 67, The runtime
construction and testing of user-provided regexes in
findMatchingProviderModelRedirectRule/getProviderModelRedirectTarget causes
ReDoS risk; change the provider rule normalization path so that
provider.modelRedirects are validated and precompiled once when reading or
normalizing providers (e.g., on provider load/normalize), reject or drop rules
whose source fails a safe-regex check (use a safe-regex library or a
timeout-bound/AST-based validator) and store only compiled RegExp objects or a
safe flag; update findMatchingProviderModelRedirectRule to consume only those
prevalidated/compiled rules (so
ModelRedirector.resetToOriginal/session.isModelRedirected logic only ever sees
safe regexes).
🧹 Nitpick comments (8)
src/app/[locale]/settings/providers/_components/model-multi-select.tsx (1)

473-474: 建议移除冗余的 onInput 处理器。

onChangeonInput 都会触发 setEditValue,这会导致同一次输入触发两次状态更新。保留 onChange 即可满足需求。

建议的修改
                      <Input
                        value={editValue}
                        data-model-edit-input={model}
                        onChange={(e) => setEditValue(e.target.value)}
-                       onInput={(e) => setEditValue((e.target as HTMLInputElement).value)}
                        onKeyDown={(e) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/providers/_components/model-multi-select.tsx
around lines 473 - 474, In the model-multi-select component, remove the
redundant onInput handler that also calls setEditValue so the same keystroke
doesn't update state twice; keep the existing onChange={(e) =>
setEditValue(e.target.value)} handler and delete the onInput={(e) =>
setEditValue((e.target as HTMLInputElement).value)} line (ensure setEditValue
remains the single updater for input changes).
messages/zh-TW/settings/providers/form/modelRedirect.json (1)

6-13: 建议统一 zh-TW 术语风格。

当前新增文案中的“匹配”在繁体语境下可读,但更常见写法是“比對”。建议统一为“比對方式 / 精確比對 / 前綴比對 …”,提升本地化一致性。

可选文案调整示例
-  "matchTypeLabel": "匹配方式",
-  "matchTypeExact": "精確匹配",
-  "matchTypePrefix": "前綴匹配",
-  "matchTypeSuffix": "後綴匹配",
-  "matchTypeContains": "關鍵字匹配",
-  "matchTypeRegex": "正則匹配",
+  "matchTypeLabel": "比對方式",
+  "matchTypeExact": "精確比對",
+  "matchTypePrefix": "前綴比對",
+  "matchTypeSuffix": "後綴比對",
+  "matchTypeContains": "關鍵字比對",
+  "matchTypeRegex": "正則比對",

Also applies to: 17-25

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

In `@messages/zh-TW/settings/providers/form/modelRedirect.json` around lines 6 -
13, Update the Traditional Chinese (zh-TW) strings to use the more common term
"比對" instead of "匹配": change matchTypeLabel to "比對方式", matchTypeExact to "精確比對",
matchTypePrefix to "前綴比對", matchTypeSuffix to "後綴比對", matchTypeContains to
"關鍵字比對", matchTypeRegex to "正則比對", and also update any other occurrences
referenced (noted as also applies to keys around lines 17-25) to maintain
consistency across the file.
src/lib/provider-patch-contract.ts (1)

10-10: 建议将相对导入改为 @/ 别名导入

这里新增了相对路径导入,和仓库统一导入约定不一致,建议改为 @/lib/provider-model-redirect-schema 以保持一致性。

建议修改
-import { PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA } from "./provider-model-redirect-schema";
+import { PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA } from "@/lib/provider-model-redirect-schema";

As per coding guidelines, **/*.{ts,tsx,js,jsx}: "Use path alias @/ mapped to ./src/ for imports".

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

In `@src/lib/provider-patch-contract.ts` at line 10, The import in
provider-patch-contract.ts uses a relative path; change it to the project path
alias by replacing the relative import of
PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA from "./provider-model-redirect-schema"
with "@/lib/provider-model-redirect-schema" so it conforms to the repository's
alias convention and keeps imports consistent across the codebase.
src/repository/provider.ts (2)

623-624: updateProvider 缺少 modelRedirects 规范化处理。

createProvider(第191行)在持久化前调用了 normalizeProviderModelRedirectRules,但 updateProvider 直接赋值而未规范化。这可能导致数据格式不一致。

建议保持一致性,在更新时也进行规范化:

♻️ 建议的修改
   if (providerData.model_redirects !== undefined)
-    dbData.modelRedirects = providerData.model_redirects;
+    dbData.modelRedirects = normalizeProviderModelRedirectRules(providerData.model_redirects);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/repository/provider.ts` around lines 623 - 624, The updateProvider flow
assigns providerData.model_redirects directly to dbData.modelRedirects leading
to inconsistent format; modify updateProvider to run
providerData.model_redirects through normalizeProviderModelRedirectRules (same
helper used in createProvider) before assigning to dbData.modelRedirects so
updates are normalized the same way as creations (refer to updateProvider,
createProvider, normalizeProviderModelRedirectRules, dbData.modelRedirects,
providerData.model_redirects).

1108-1109: updateProvidersBatch 同样缺少规范化处理。

updateProvider 相同的问题。虽然 BatchProviderUpdates 接口已将类型更新为 ProviderModelRedirectRule[] | null,但为了防御性编程和与 createProvider 保持一致,建议添加规范化:

♻️ 建议的修改
   if (updates.modelRedirects !== undefined) {
-    setClauses.modelRedirects = updates.modelRedirects;
+    setClauses.modelRedirects = normalizeProviderModelRedirectRules(updates.modelRedirects);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/repository/provider.ts` around lines 1108 - 1109, The batch update path
updateProvidersBatch is missing the same normalization as
updateProvider/createProvider for modelRedirects: when updates.modelRedirects is
present, normalize its value to the same canonical form (e.g., convert undefined
to null or ensure an array type) before assigning to setClauses.modelRedirects;
update the handling in updateProvidersBatch to use the same normalization helper
or logic used by createProvider/updateProvider (and respect the
BatchProviderUpdates type ProviderModelRedirectRule[] | null) so stored values
are consistent and defensive against undefined or malformed inputs.
tests/unit/lib/provider-model-redirects.test.ts (1)

59-77: 旧版格式规范化测试依赖对象属性顺序。

此测试假设 Object.entries() 按插入顺序返回条目。虽然现代 JavaScript 引擎对字符串键确实保持插入顺序,但如果测试意图是验证规范化行为而非特定顺序,可考虑使用 expect.arrayContaining 或对结果排序后比较。

当前实现在实际运行中应该没有问题,仅作为防御性建议。

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

In `@tests/unit/lib/provider-model-redirects.test.ts` around lines 59 - 77, The
test relies on insertion order from Object.entries when calling
normalizeProviderModelRedirectRules; instead make the assertion order-agnostic
by either asserting the result contains the expected entries with
expect.arrayContaining or by sorting the returned array (e.g., by source) before
comparing; modify the test in provider-model-redirects.test.ts to use one of
these approaches so the assertion no longer depends on object property ordering.
tests/unit/settings/providers/model-redirect-editor.test.tsx (1)

157-167: 避免把测试绑死在英文文案上。

这里同时用 includes("Add") 和完整英文报错文本做断言,翻译文案一改就会误报,哪怕行为没变。前一个用例已经在用 data-redirect-* 选择器;建议给“新增”按钮和错误提示也补稳定 selector,或者至少从 messages 对象取期望值。

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

In `@tests/unit/settings/providers/model-redirect-editor.test.tsx` around lines
157 - 167, The test is brittle because it relies on English text; update the
selector and assertion to use stable identifiers instead: locate the "Add"
button by its data attribute (e.g. the same data-redirect-* pattern used
elsewhere) instead of querying textContent and assert the validation message by
either querying a dedicated data-test/data-redirect attribute on the error
element or by comparing against the expected string from the messages object
used by the component; update references to addButton, onChange, and the error
assertion in model-redirect-editor.test.tsx accordingly so the test no longer
depends on hard-coded English copy.
tests/unit/actions/providers-patch-contract.test.ts (1)

114-122: 把规则数组的归一化结果也断言住。

这里现在只验证 normalizeProviderBatchPatchDraft(...) 成功,不验证 model_redirects 的实际归一化结果。若后续实现把规则重排、折叠成旧形态,或者丢掉 matchType,这个用例仍会通过。既然这次变更的核心契约是“有序规则数组”,建议把 result.data.model_redirects 的 mode 和规则顺序一起断言出来。

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

In `@tests/unit/actions/providers-patch-contract.test.ts` around lines 114 - 122,
当前测试只断言 normalizeProviderBatchPatchDraft(...) 返回成功,但未验证
result.data.model_redirects 的归一化内容;请在该用例中同时断言 result.data.model_redirects.mode
与规则数组的顺序/内容(例如期望模式为 "ordered" 或相应字符串,以及期望的 rules 数组包含一个对象 { matchType: "prefix",
source: "claude-opus", target: "glm-4.6"
}),以确保模式和规则顺序未被改变或丢失;定位点:normalizeProviderBatchPatchDraft 和
result.data.model_redirects(包括 mode 与 rules 字段)。
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`[locale]/settings/providers/_components/model-redirect-editor.tsx:
- Around line 123-134: The add path currently allows creating more than 100
rules client-side; in handleAdd, before calling validateRule/onChange, check if
redirects.length >= 100 and if so call setError with a clear "maximum 100 rules"
message and return to block the addition; after a successful add keep the
existing setError(null)/onChange([...redirects,
nextRule])/setNewRule(DEFAULT_RULE) flow. Apply the same pre-flight limit check
to the other handler that appends multiple rules (the bulk-add/paste handler
that also calls onChange to add rules) so it prevents exceeding 100 and surfaces
the same error message.
- Around line 299-320: Add localized accessible names to all icon-only buttons
in this component (e.g., the save/cancel buttons that call handleSaveEdit and
handleCancelEdit and the edit/delete/move-up/move-down buttons referenced later
around lines 337-380): supply an aria-label (and title if desired) using the
localization function in this file (e.g., t or useTranslations) with
context-aware text like "Save rule", "Cancel edit", "Move rule up", "Move rule
down", "Edit rule", "Delete rule" and include the ruleKey or index when
appropriate to disambiguate; ensure the aria-label strings are localized and add
the same text to title for hover tooltips if needed so screen readers and
sighted users can understand each icon-only Button.

In `@src/lib/provider-model-redirect-schema.ts`:
- Around line 51-53: 当前 PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA 只限制了数组长度,但未在
schema 层保证归一化后的 matchType + source 唯一,导致前端以该组合作为规则主键时出现 React key 冲突;在
PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA 上添加一个 .refine 校验,遍历数组为每条规则构造规范化键(例如对
matchType 和 source 做 trim 和 toLowerCase 后拼接成 `${matchType}:${source}`),用 Set
判断去重后长度是否与原数组相同,若不同则返回自定义错误信息(如 "Duplicate redirect rule for
matchType+source")以阻止重复规则写入即可。

---

Outside diff comments:
In `@src/app/v1/_lib/proxy/model-redirector.ts`:
- Around line 49-67: The runtime construction and testing of user-provided
regexes in findMatchingProviderModelRedirectRule/getProviderModelRedirectTarget
causes ReDoS risk; change the provider rule normalization path so that
provider.modelRedirects are validated and precompiled once when reading or
normalizing providers (e.g., on provider load/normalize), reject or drop rules
whose source fails a safe-regex check (use a safe-regex library or a
timeout-bound/AST-based validator) and store only compiled RegExp objects or a
safe flag; update findMatchingProviderModelRedirectRule to consume only those
prevalidated/compiled rules (so
ModelRedirector.resetToOriginal/session.isModelRedirected logic only ever sees
safe regexes).

---

Nitpick comments:
In `@messages/zh-TW/settings/providers/form/modelRedirect.json`:
- Around line 6-13: Update the Traditional Chinese (zh-TW) strings to use the
more common term "比對" instead of "匹配": change matchTypeLabel to "比對方式",
matchTypeExact to "精確比對", matchTypePrefix to "前綴比對", matchTypeSuffix to "後綴比對",
matchTypeContains to "關鍵字比對", matchTypeRegex to "正則比對", and also update any
other occurrences referenced (noted as also applies to keys around lines 17-25)
to maintain consistency across the file.

In `@src/app/`[locale]/settings/providers/_components/model-multi-select.tsx:
- Around line 473-474: In the model-multi-select component, remove the redundant
onInput handler that also calls setEditValue so the same keystroke doesn't
update state twice; keep the existing onChange={(e) =>
setEditValue(e.target.value)} handler and delete the onInput={(e) =>
setEditValue((e.target as HTMLInputElement).value)} line (ensure setEditValue
remains the single updater for input changes).

In `@src/lib/provider-patch-contract.ts`:
- Line 10: The import in provider-patch-contract.ts uses a relative path; change
it to the project path alias by replacing the relative import of
PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA from "./provider-model-redirect-schema"
with "@/lib/provider-model-redirect-schema" so it conforms to the repository's
alias convention and keeps imports consistent across the codebase.

In `@src/repository/provider.ts`:
- Around line 623-624: The updateProvider flow assigns
providerData.model_redirects directly to dbData.modelRedirects leading to
inconsistent format; modify updateProvider to run providerData.model_redirects
through normalizeProviderModelRedirectRules (same helper used in createProvider)
before assigning to dbData.modelRedirects so updates are normalized the same way
as creations (refer to updateProvider, createProvider,
normalizeProviderModelRedirectRules, dbData.modelRedirects,
providerData.model_redirects).
- Around line 1108-1109: The batch update path updateProvidersBatch is missing
the same normalization as updateProvider/createProvider for modelRedirects: when
updates.modelRedirects is present, normalize its value to the same canonical
form (e.g., convert undefined to null or ensure an array type) before assigning
to setClauses.modelRedirects; update the handling in updateProvidersBatch to use
the same normalization helper or logic used by createProvider/updateProvider
(and respect the BatchProviderUpdates type ProviderModelRedirectRule[] | null)
so stored values are consistent and defensive against undefined or malformed
inputs.

In `@tests/unit/actions/providers-patch-contract.test.ts`:
- Around line 114-122: 当前测试只断言 normalizeProviderBatchPatchDraft(...) 返回成功,但未验证
result.data.model_redirects 的归一化内容;请在该用例中同时断言 result.data.model_redirects.mode
与规则数组的顺序/内容(例如期望模式为 "ordered" 或相应字符串,以及期望的 rules 数组包含一个对象 { matchType: "prefix",
source: "claude-opus", target: "glm-4.6"
}),以确保模式和规则顺序未被改变或丢失;定位点:normalizeProviderBatchPatchDraft 和
result.data.model_redirects(包括 mode 与 rules 字段)。

In `@tests/unit/lib/provider-model-redirects.test.ts`:
- Around line 59-77: The test relies on insertion order from Object.entries when
calling normalizeProviderModelRedirectRules; instead make the assertion
order-agnostic by either asserting the result contains the expected entries with
expect.arrayContaining or by sorting the returned array (e.g., by source) before
comparing; modify the test in provider-model-redirects.test.ts to use one of
these approaches so the assertion no longer depends on object property ordering.

In `@tests/unit/settings/providers/model-redirect-editor.test.tsx`:
- Around line 157-167: The test is brittle because it relies on English text;
update the selector and assertion to use stable identifiers instead: locate the
"Add" button by its data attribute (e.g. the same data-redirect-* pattern used
elsewhere) instead of querying textContent and assert the validation message by
either querying a dedicated data-test/data-redirect attribute on the error
element or by comparing against the expected string from the messages object
used by the component; update references to addButton, onChange, and the error
assertion in model-redirect-editor.test.tsx accordingly so the test no longer
depends on hard-coded English copy.
🪄 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: 92c1030d-f3a9-4f6c-a0e3-35cbab269232

📥 Commits

Reviewing files that changed from the base of the PR and between 7ea7ba4 and 7f6045e.

📒 Files selected for processing (42)
  • messages/en/settings/providers/form/modelRedirect.json
  • messages/en/settings/providers/form/modelSelect.json
  • messages/ja/settings/providers/form/modelRedirect.json
  • messages/ja/settings/providers/form/modelSelect.json
  • messages/ru/settings/providers/form/modelRedirect.json
  • messages/ru/settings/providers/form/modelSelect.json
  • messages/zh-CN/settings/providers/form/modelRedirect.json
  • messages/zh-CN/settings/providers/form/modelSelect.json
  • messages/zh-TW/settings/providers/form/modelRedirect.json
  • messages/zh-TW/settings/providers/form/modelSelect.json
  • src/actions/providers.ts
  • src/app/[locale]/settings/providers/_components/batch-edit/analyze-batch-settings.ts
  • src/app/[locale]/settings/providers/_components/batch-edit/build-patch-draft.ts
  • src/app/[locale]/settings/providers/_components/forms/provider-form.legacy.tsx
  • src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
  • src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
  • src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
  • src/app/[locale]/settings/providers/_components/model-multi-select.tsx
  • src/app/[locale]/settings/providers/_components/model-redirect-editor.tsx
  • src/app/api/actions/[...route]/route.ts
  • src/app/v1/_lib/proxy/model-redirector.ts
  • src/drizzle/schema.ts
  • src/lib/provider-model-redirect-schema.ts
  • src/lib/provider-model-redirects.ts
  • src/lib/provider-patch-contract.ts
  • src/lib/validation/schemas.ts
  • src/repository/_shared/transformers.ts
  • src/repository/provider.ts
  • src/types/message.ts
  • src/types/provider.ts
  • tests/api/providers-actions.test.ts
  • tests/integration/batch-edit-prefill.test.ts
  • tests/unit/actions/providers-batch-field-mapping.test.ts
  • tests/unit/actions/providers-patch-contract.test.ts
  • tests/unit/batch-edit/analyze-batch-settings.test.ts
  • tests/unit/dashboard/provider-form-clone-deep-copy.test.ts
  • tests/unit/lib/provider-model-redirects.test.ts
  • tests/unit/settings/providers/build-patch-draft.test.ts
  • tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx
  • tests/unit/settings/providers/model-redirect-editor.test.tsx
  • tests/unit/settings/providers/provider-form-batch-context.test.ts

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (2)
src/app/[locale]/settings/providers/_components/model-redirect-editor.tsx (2)

443-482: 移除冗余的 onInput 处理器。

新规则输入框同样存在 onChangeonInput 重复绑定的问题,建议保持一致性移除 onInput

建议的修改
            <Input
              id="new-source"
              value={newRule.source}
              onChange={(e) => setNewRule((current) => ({ ...current, source: e.target.value }))}
-             onInput={(e) =>
-               setNewRule((current) => ({
-                 ...current,
-                 source: (e.target as HTMLInputElement).value,
-               }))
-             }
              onKeyDown={handleCreateKeyDown}
              placeholder={t("sourcePlaceholder")}
              disabled={disabled}
              className="font-mono text-sm"
            />
            <Input
              id="new-target"
              value={newRule.target}
              onChange={(e) => setNewRule((current) => ({ ...current, target: e.target.value }))}
-             onInput={(e) =>
-               setNewRule((current) => ({
-                 ...current,
-                 target: (e.target as HTMLInputElement).value,
-               }))
-             }
              onKeyDown={handleCreateKeyDown}
              placeholder={t("targetPlaceholder")}
              disabled={disabled}
              className="font-mono text-sm"
            />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/providers/_components/model-redirect-editor.tsx
around lines 443 - 482, Remove the redundant onInput handlers on the new-rule
inputs: the Input with id "new-source" and the Input with id "new-target"
currently have duplicate onChange and onInput updates to setNewRule; delete the
onInput props so only onChange (and existing
onKeyDown/placeholder/disabled/className) update the newRule state, keeping
handleCreateKeyDown intact and preserving the value bindings to newRule.source
and newRule.target.

261-301: 移除冗余的 onInput 处理器。

编辑模式下的 sourcetarget 输入框同时绑定了 onChangeonInput,两者执行相同的状态更新操作。在 React 中,onChange 已经会在每次输入时触发(不同于原生 DOM 行为),onInput 是多余的,可能导致每次按键触发两次状态更新。

建议的修改
                        <Input
                          value={editRule.source}
                          data-redirect-edit-source={ruleKey}
                          onChange={(e) =>
                            setEditRule((current) => ({ ...current, source: e.target.value }))
                          }
-                         onInput={(e) =>
-                           setEditRule((current) => ({
-                             ...current,
-                             source: (e.target as HTMLInputElement).value,
-                           }))
-                         }
                          onKeyDown={(e) => handleEditKeyDown(e, ruleKey)}
                          disabled={disabled}
                          className="font-mono text-sm h-8 flex-1"
                          autoFocus
                        />
                        <Input
                          value={editRule.target}
                          data-redirect-edit-target={ruleKey}
                          onChange={(e) =>
                            setEditRule((current) => ({ ...current, target: e.target.value }))
                          }
-                         onInput={(e) =>
-                           setEditRule((current) => ({
-                             ...current,
-                             target: (e.target as HTMLInputElement).value,
-                           }))
-                         }
                          onKeyDown={(e) => handleEditKeyDown(e, ruleKey)}
                          disabled={disabled}
                          className="font-mono text-sm h-8 flex-1"
                        />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/providers/_components/model-redirect-editor.tsx
around lines 261 - 301, Remove the redundant onInput handlers from the source
and target Input components so state updates only occur via onChange; update the
two Input elements that reference editRule.source and editRule.target (they
currently call setEditRule in both onChange and onInput) to keep only the
onChange handler, leaving onKeyDown (handleEditKeyDown), disabled, className and
data attributes intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app/`[locale]/settings/providers/_components/model-redirect-editor.tsx:
- Around line 443-482: Remove the redundant onInput handlers on the new-rule
inputs: the Input with id "new-source" and the Input with id "new-target"
currently have duplicate onChange and onInput updates to setNewRule; delete the
onInput props so only onChange (and existing
onKeyDown/placeholder/disabled/className) update the newRule state, keeping
handleCreateKeyDown intact and preserving the value bindings to newRule.source
and newRule.target.
- Around line 261-301: Remove the redundant onInput handlers from the source and
target Input components so state updates only occur via onChange; update the two
Input elements that reference editRule.source and editRule.target (they
currently call setEditRule in both onChange and onInput) to keep only the
onChange handler, leaving onKeyDown (handleEditKeyDown), disabled, className and
data attributes intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 59608cb9-b2f7-48bf-a955-269652a483cc

📥 Commits

Reviewing files that changed from the base of the PR and between 7f6045e and 69d7f52.

📒 Files selected for processing (9)
  • messages/en/settings/providers/form/modelRedirect.json
  • messages/ja/settings/providers/form/modelRedirect.json
  • messages/ru/settings/providers/form/modelRedirect.json
  • messages/zh-CN/settings/providers/form/modelRedirect.json
  • messages/zh-TW/settings/providers/form/modelRedirect.json
  • src/app/[locale]/settings/providers/_components/model-redirect-editor.tsx
  • tests/unit/actions/providers-patch-contract.test.ts
  • tests/unit/lib/provider-model-redirects.test.ts
  • tests/unit/settings/providers/model-redirect-editor.test.tsx
✅ Files skipped from review due to trivial changes (2)
  • messages/ru/settings/providers/form/modelRedirect.json
  • messages/zh-CN/settings/providers/form/modelRedirect.json
🚧 Files skipped from review as they are similar to previous changes (6)
  • messages/ja/settings/providers/form/modelRedirect.json
  • tests/unit/lib/provider-model-redirects.test.ts
  • tests/unit/actions/providers-patch-contract.test.ts
  • tests/unit/settings/providers/model-redirect-editor.test.tsx
  • messages/en/settings/providers/form/modelRedirect.json
  • messages/zh-TW/settings/providers/form/modelRedirect.json

Comment on lines +31 to +48
if (!safeRegex(rule.source)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Redirect regex has potential ReDoS risk",
path: ["source"],
});
return;
}

try {
new RegExp(rule.source);
} catch {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Redirect regex is invalid",
path: ["source"],
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 safe-regex called before syntax validation, can throw on invalid patterns

The existing codebase already knows safe-regex can throw for syntactically invalid regex (e.g. [, (): src/lib/error-rule-detector.ts wraps every safeRegex() call in a try/catch (line 250). This PR skips that guard. When a user submits a regex like [, safeRegex(rule.source) will throw (via regjsparser internally) before the try { new RegExp(...) } catch block is ever reached, propagating an unhandled exception out of superRefine instead of returning a clean validation error.

Suggested change
if (!safeRegex(rule.source)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Redirect regex has potential ReDoS risk",
path: ["source"],
});
return;
}
try {
new RegExp(rule.source);
} catch {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Redirect regex is invalid",
path: ["source"],
});
}
.superRefine((rule, ctx) => {
if (rule.matchType !== "regex") {
return;
}
try {
new RegExp(rule.source);
} catch {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Redirect regex is invalid",
path: ["source"],
});
return;
}
try {
if (!safeRegex(rule.source)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Redirect regex has potential ReDoS risk",
path: ["source"],
});
}
} catch {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Redirect regex has potential ReDoS risk",
path: ["source"],
});
}
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/provider-model-redirect-schema.ts
Line: 31-48

Comment:
**`safe-regex` called before syntax validation, can throw on invalid patterns**

The existing codebase already knows `safe-regex` can throw for syntactically invalid regex (e.g. `[`, `(`): `src/lib/error-rule-detector.ts` wraps every `safeRegex()` call in a `try/catch` (line 250). This PR skips that guard. When a user submits a regex like `[`, `safeRegex(rule.source)` will throw (via `regjsparser` internally) before the `try { new RegExp(...) }` catch block is ever reached, propagating an unhandled exception out of `superRefine` instead of returning a clean validation error.

```suggestion
  .superRefine((rule, ctx) => {
    if (rule.matchType !== "regex") {
      return;
    }

    try {
      new RegExp(rule.source);
    } catch {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Redirect regex is invalid",
        path: ["source"],
      });
      return;
    }

    try {
      if (!safeRegex(rule.source)) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          message: "Redirect regex has potential ReDoS risk",
          path: ["source"],
        });
      }
    } catch {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Redirect regex has potential ReDoS risk",
        path: ["source"],
      });
    }
  });
```

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

Comment on lines +104 to +113
if (normalized.matchType === "regex") {
if (!safeRegex(normalized.source)) {
return t("regexUnsafe");
}

try {
new RegExp(normalized.source);
} catch {
return t("regexInvalid");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Same safe-regex-before-syntax-check ordering bug in the UI

The UI validateRule function has the identical problem: safeRegex(normalized.source) runs before new RegExp(...), so an invalid pattern like [ throws before the catch block for syntax errors can fire, causing an uncaught exception in the React render path.

Suggested change
if (normalized.matchType === "regex") {
if (!safeRegex(normalized.source)) {
return t("regexUnsafe");
}
try {
new RegExp(normalized.source);
} catch {
return t("regexInvalid");
}
if (normalized.matchType === "regex") {
try {
new RegExp(normalized.source);
} catch {
return t("regexInvalid");
}
try {
if (!safeRegex(normalized.source)) {
return t("regexUnsafe");
}
} catch {
return t("regexUnsafe");
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/model-redirect-editor.tsx
Line: 104-113

Comment:
**Same safe-regex-before-syntax-check ordering bug in the UI**

The UI `validateRule` function has the identical problem: `safeRegex(normalized.source)` runs before `new RegExp(...)`, so an invalid pattern like `[` throws before the `catch` block for syntax errors can fire, causing an uncaught exception in the React render path.

```suggestion
    if (normalized.matchType === "regex") {
      try {
        new RegExp(normalized.source);
      } catch {
        return t("regexInvalid");
      }

      try {
        if (!safeRegex(normalized.source)) {
          return t("regexUnsafe");
        }
      } catch {
        return t("regexUnsafe");
      }
    }
```

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

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 69d7f52ee6

ℹ️ About Codex in GitHub

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

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

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

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

Comment on lines +459 to +463
{selectedModels.map((model) => {
const isEditing = editingModel === model;

return (
<div
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use stable row ids for selected model entries

When allowed_models contains duplicates (possible via API/import because schema accepts plain z.array(z.string())), this list uses model as the React key and identity, so duplicate rows collide and React may reuse the wrong row state; downstream handlers also match by value, which edits/removes every duplicate at once instead of one entry. This makes the new outside-dropdown editor unable to safely manage real-world duplicated data and can silently rewrite a provider’s whitelist.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 69dffeead2

ℹ️ About Codex in GitHub

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

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

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

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

return;
}

if (!safeRegex(rule.source)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Validate regex syntax before calling safeRegex

For matchType: "regex", this calls safeRegex(rule.source) before new RegExp(rule.source). Inputs like "[" can throw inside safe-regex itself, so the try/catch around new RegExp is never reached and validation exits via exception instead of returning a normal field error. In create/edit/batch flows this can surface as an unexpected server error path rather than a deterministic validation response.

Useful? React with 👍 / 👎.

return t("targetTooLong");
}
if (normalized.matchType === "regex") {
if (!safeRegex(normalized.source)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard regex safety check from invalid-pattern throws

The editor’s client-side validator has the same ordering issue: safeRegex(normalized.source) runs before syntax validation. When an admin types an invalid regex (e.g. "["), safeRegex can throw before the new RegExp(...) catch runs, causing an uncaught error in the UI interaction path instead of showing the localized regexInvalid message.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 69dffeead2

ℹ️ About Codex in GitHub

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

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

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

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

return;
}

if (!safeRegex(rule.source)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Validate regex syntax before calling safeRegex

For matchType: "regex", this calls safeRegex(rule.source) before new RegExp(rule.source). Inputs like "[" can throw inside safe-regex itself, so the try/catch around new RegExp is never reached and validation exits via exception instead of returning a normal field error. In create/edit/batch flows this can surface as an unexpected server error path rather than a deterministic validation response.

Useful? React with 👍 / 👎.

return t("targetTooLong");
}
if (normalized.matchType === "regex") {
if (!safeRegex(normalized.source)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard regex safety check from invalid-pattern throws

The editor’s client-side validator has the same ordering issue: safeRegex(normalized.source) runs before syntax validation. When an admin types an invalid regex (e.g. "["), safeRegex can throw before the new RegExp(...) catch runs, causing an uncaught error in the UI interaction path instead of showing the localized regexInvalid message.

Useful? React with 👍 / 👎.

@ding113 ding113 merged commit dd9751c into dev Apr 4, 2026
8 of 9 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Apr 4, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 4, 2026

🧪 测试结果

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

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

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 14d6d6e880

ℹ️ About Codex in GitHub

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

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

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

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

Comment on lines +228 to +232
if (editingIndex === index) {
setEditingIndex(null);
setEditValue("");
}
onChange(selectedModels.filter((_, currentIndex) => currentIndex !== index));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Rebase edit index when removing another selected model

When an item is being edited by editingIndex, removing a different row before it shifts selectedModels indices but leaves editingIndex unchanged. In that state, the editor can jump to the wrong model and handleSaveEditSelectedModel will apply the pending text to a different entry than the one the user started editing, which can silently rewrite the whitelist after mixed edit/remove interactions in the selected-model list UI.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

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

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

Inline comments:
In `@src/app/`[locale]/settings/providers/_components/model-multi-select.tsx:
- Around line 326-337: The icon-only Button in ModelMultiSelect (the Refresh
button using <Button ... onClick={...} loadModels />) lacks an accessible name;
add an aria-label prop that uses the i18n function t() (do not hardcode) and
corresponding keys in the language pack; apply the same change to the other
icon-only Buttons referenced (lines handling add/edit/save/cancel/delete) so
each Button component has aria-label={t('...')} with matching translation
entries for zh-CN, zh-TW, en, ja, ru.
- Around line 102-104: The bug is that editingIndex/editValue track an item by
positional index so concurrent mutations (delete, selectAll) shift indices and
write edits to the wrong item; change logic to either 1) exit edit mode whenever
selectedModels changes (call setEditingIndex(null) and clear editValue inside
the selectedModels update handlers) OR 2) track the in-progress edit by a stable
identifier (e.g., model id/key stored instead of index) so saves apply to the
correct model; also rewrite the selectAll path to merge selectedModels with the
displayed/available models (combine selectedModels + displayedAvailableModels
and dedupe) instead of blindly writing availableModels back (this preserves
manually added items and the displayedModels dedupe behavior). Ensure these
changes touch editingIndex/editValue state management, the save handler,
deletion handlers, and the selectAll implementation.
- Around line 269-415: The Popover allows writes when disabled because only the
trigger is disabled; update the Popover open handling and mutation handlers to
block changes: ensure the Popover's onOpenChange (where setOpen is used)
prevents opening when disabled and closes the Popover when disabled becomes
true, and add defensive guards at the start of each mutating handler
(loadModels, selectAll, clearAll, toggleModel, handleRemoveSelectedModel, and
any other handlers that change allowedModels) that return early if disabled;
this prevents interactions (refresh, select all, clear, item toggle/remove)
while disabled and guarantees the popover is closed whenever disabled is true.

In `@src/lib/provider-model-redirect-schema.ts`:
- Around line 62-77: 当前 refine 去重逻辑在构建唯一键时对 rule.source 使用 .toLowerCase(), 导致
schema 将 "GPT-4" 与 "gpt-4" 视为重复但运行时匹配是大小写敏感的;请在该 .refine 的循环中移除
.toLowerCase()(保留 .trim() 如需),即将 key 从
`${rule.matchType}:${rule.source.trim().toLowerCase()}` 改为
`${rule.matchType}:${rule.source.trim()}`,以使 provider-model-redirect-schema
的去重行为与 src/lib/provider-model-redirects.ts 的运行时匹配逻辑一致。
🪄 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: 26815c7b-5f08-4096-81d8-54d0b25df278

📥 Commits

Reviewing files that changed from the base of the PR and between 69dffee and 14d6d6e.

📒 Files selected for processing (3)
  • src/app/[locale]/settings/providers/_components/model-multi-select.tsx
  • src/app/[locale]/settings/providers/_components/model-redirect-editor.tsx
  • src/lib/provider-model-redirect-schema.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/[locale]/settings/providers/_components/model-redirect-editor.tsx

Comment on lines +102 to +104
const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [editValue, setEditValue] = useState("");
const [managementError, setManagementError] = useState<string | null>(null);
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

数组替换和索引编辑绑在一起,会把修改写到错误项。

这里的编辑态用 editingIndex 跟踪位置,但选中列表还能被“全选”或删除操作继续改写。比如编辑第 2 项时删除第 1 项,保存就会把 editValue 写到新的第 2 项。与此同时,selectAll 直接把原始 availableModels 整体写回去,还会丢掉手动添加但不在远端列表里的项,并绕过前面 displayedModels 的去重。

建议把“全选”改成基于 selectedModels + availableDisplayedModels 合并去重,并在任何 selectedModels 变更时统一退出编辑态,或改用稳定 key 跟踪正在编辑的项。

Proposed fix
- const selectAll = () => onChange(availableModels);
+ const selectAll = () => {
+   setManagementError(null);
+   setEditingIndex(null);
+   setEditValue("");
+   onChange(Array.from(new Set([...selectedModels, ...availableDisplayedModels])));
+ };
+
+ useEffect(() => {
+   if (editingIndex === null) return;
+   setEditingIndex(null);
+   setEditValue("");
+   setManagementError(null);
+ }, [selectedModels, editingIndex]);

Also applies to: 126-129, 200-206, 226-265

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

In `@src/app/`[locale]/settings/providers/_components/model-multi-select.tsx
around lines 102 - 104, The bug is that editingIndex/editValue track an item by
positional index so concurrent mutations (delete, selectAll) shift indices and
write edits to the wrong item; change logic to either 1) exit edit mode whenever
selectedModels changes (call setEditingIndex(null) and clear editValue inside
the selectedModels update handlers) OR 2) track the in-progress edit by a stable
identifier (e.g., model id/key stored instead of index) so saves apply to the
correct model; also rewrite the selectAll path to merge selectedModels with the
displayed/available models (combine selectedModels + displayedAvailableModels
and dedupe) instead of blindly writing availableModels back (this preserves
manually added items and the displayedModels dedupe behavior). Ensure these
changes touch editingIndex/editValue state management, the save handler,
deletion handlers, and the selectAll implementation.

Comment on lines +269 to +415
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
disabled={disabled}
className="w-full justify-between"
>
{selectedModels.length === 0 ? (
<span className="text-muted-foreground">
{t("allowAllModels", {
type: getProviderTypeLabel(providerType),
})}
</span>
<Badge variant="secondary" className="ml-auto">
{selectedModels.length}
</Badge>
) : (
<div className="flex gap-2 items-center">
<span className="truncate">
{t("selectedCount", { count: selectedModels.length })}
</span>
<Badge variant="secondary" className="ml-auto">
{selectedModels.length}
</Badge>
</div>
)}
{loading ? (
<Loader2 className="ml-2 h-4 w-4 shrink-0 animate-spin opacity-50" />
) : (
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
)}
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[400px] max-w-[calc(100vw-2rem)] p-0 flex flex-col"
align="start"
onWheel={(e) => e.stopPropagation()}
onTouchMove={(e) => e.stopPropagation()}
>
<Command shouldFilter={true}>
<CommandInput placeholder={t("searchPlaceholder")} />
<CommandList className="max-h-[250px] overflow-y-auto">
<CommandEmpty>{loading ? t("loading") : t("notFound")}</CommandEmpty>

{!loading && (
<>
<CommandGroup>
<div className="flex items-center justify-between gap-2 p-2">
<div className="flex items-center gap-2">
<ModelSourceIndicator
loading={loading}
isUpstream={isUpstream}
label={sourceLabel}
description={sourceDescription}
/>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={(e) => {
e.stopPropagation();
loadModels();
}}
type="button"
>
<RefreshCw className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<p className="text-xs">{t("refresh")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={selectAll}
className="h-7 text-xs"
type="button"
>
{t("selectAll", { count: availableModels.length })}
</Button>
<Button
size="sm"
variant="outline"
onClick={clearAll}
disabled={selectedModels.length === 0}
className="h-7 text-xs"
type="button"
>
{t("clear")}
</Button>
</div>
</div>
</CommandGroup>

{selectedModels.length > 0 && (
<div data-model-group="selected">
<CommandGroup heading={t("selectedGroupLabel")}>
{selectedModels.map((model, index) => (
<CommandItem
key={`selected:${index}:${model}`}
value={model}
onSelect={() => handleRemoveSelectedModel(index)}
className="cursor-pointer"
>
<Checkbox
checked={true}
className="mr-2"
onCheckedChange={() => handleRemoveSelectedModel(index)}
/>
<span className="font-mono text-sm flex-1">{model}</span>
<Check className="h-4 w-4 text-primary" />
</CommandItem>
))}
</CommandGroup>
</div>
)}

<div data-model-group="available">
<CommandGroup heading={t("availableGroupLabel")}>
{availableDisplayedModels.map((model) => (
<CommandItem
key={model}
value={model}
onSelect={() => toggleModel(model)}
className="cursor-pointer"
>
<Checkbox
checked={false}
className="mr-2"
onCheckedChange={() => toggleModel(model)}
/>
<span className="font-mono text-sm flex-1">{model}</span>
</CommandItem>
))}
</CommandGroup>
</div>
</>
)}
</CommandList>
</Command>

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

禁用态下弹层里的修改入口仍然可用。

disabled 这里只禁掉了 trigger,但如果 Popover 已经打开,刷新、全选、清空以及列表项点击仍然会继续触发写操作。父组件又把它接到了 isPending,所以提交流程里仍可能改动 allowedModels

建议在 disabled 变成 true 时主动关闭 Popover,并在所有变更 handler 开头加一次 if (disabled) return; 防御。

Proposed fix
+ useEffect(() => {
+   if (!disabled) return;
+   setOpen(false);
+   setEditingIndex(null);
+   setEditValue("");
+   setManagementError(null);
+ }, [disabled]);
+
  const toggleModel = (model: string) => {
+   if (disabled) return;
    setManagementError(null);
    if (selectedModels.includes(model)) {
      onChange(selectedModels.filter((m) => m !== model));
    } else {
      onChange([...selectedModels, model]);
    }
  };
 
- const selectAll = () => onChange(availableModels);
+ const selectAll = () => {
+   if (disabled) return;
+   onChange(availableModels);
+ };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/providers/_components/model-multi-select.tsx
around lines 269 - 415, The Popover allows writes when disabled because only the
trigger is disabled; update the Popover open handling and mutation handlers to
block changes: ensure the Popover's onOpenChange (where setOpen is used)
prevents opening when disabled and closes the Popover when disabled becomes
true, and add defensive guards at the start of each mutating handler
(loadModels, selectAll, clearAll, toggleModel, handleRemoveSelectedModel, and
any other handlers that change allowedModels) that return early if disabled;
this prevents interactions (refresh, select all, clear, item toggle/remove)
while disabled and guarantees the popover is closed whenever disabled is true.

Comment on lines +326 to +337
<Button
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={(e) => {
e.stopPropagation();
loadModels();
}}
type="button"
>
<RefreshCw className="h-3 w-3" />
</Button>
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

纯图标按钮需要补可访问名称。

这些按钮现在只有 SVG。Tooltip 文案不会稳定成为 accessible name,屏幕阅读器大多只会读到无名按钮,无法区分刷新、添加、编辑、保存、取消和删除。请补 aria-label,并把新增文案继续走 t() 和语言包。

Proposed fix
<Button
+ aria-label={t("refresh")}
  size="icon"
  variant="ghost"
  className="h-6 w-6"
  onClick={(e) => {
    e.stopPropagation();
    loadModels();
  }}
  type="button"
>
<Button
  size="sm"
  onClick={handleAddCustomModel}
  disabled={disabled || !customModel.trim()}
  type="button"
+ aria-label={t("manualAdd")}
>
<Button
  type="button"
  variant="ghost"
  size="sm"
  data-model-edit-save={model}
  onClick={() => handleSaveEditSelectedModel(index)}
  disabled={disabled}
  className="h-8 w-8 p-0"
+ aria-label={t("selectedEditSave")}
>
<Button
  type="button"
  variant="ghost"
  size="sm"
  onClick={handleCancelEditSelectedModel}
  disabled={disabled}
  className="h-8 w-8 p-0"
+ aria-label={t("selectedEditCancel")}
>
<Button
  type="button"
  variant="ghost"
  size="sm"
  data-model-edit={model}
  onClick={() => handleStartEditSelectedModel(index, model)}
  disabled={disabled}
  className="h-8 w-8 p-0"
+ aria-label={t("selectedEditStart", { model })}
>
<Button
  type="button"
  variant="ghost"
  size="sm"
  data-model-remove={model}
  onClick={() => handleRemoveSelectedModel(index)}
  disabled={disabled}
  className="h-8 w-8 p-0"
+ aria-label={t("selectedRemove", { model })}
>

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: 432-439, 484-529

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

In `@src/app/`[locale]/settings/providers/_components/model-multi-select.tsx
around lines 326 - 337, The icon-only Button in ModelMultiSelect (the Refresh
button using <Button ... onClick={...} loadModels />) lacks an accessible name;
add an aria-label prop that uses the i18n function t() (do not hardcode) and
corresponding keys in the language pack; apply the same change to the other
icon-only Buttons referenced (lines handling add/edit/save/cancel/delete) so
each Button component has aria-label={t('...')} with matching translation
entries for zh-CN, zh-TW, en, ja, ru.

Comment on lines +62 to +77
.refine(
(rules) => {
const keys = new Set<string>();
for (const rule of rules) {
const key = `${rule.matchType}:${rule.source.trim().toLowerCase()}`;
if (keys.has(key)) {
return false;
}
keys.add(key);
}
return true;
},
{
message: "Duplicate redirect rule for matchType+source",
}
);
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 检查运行时匹配逻辑中是否有大小写转换
rg -n -A5 'matchesProviderModelRedirectRule' src/lib/provider-model-redirects.ts

Repository: ding113/claude-code-hub

Length of output: 373


🏁 Script executed:

cat -n src/lib/provider-model-redirects.ts | head -150 | tail -70

Repository: ding113/claude-code-hub

Length of output: 2290


🏁 Script executed:

sed -n '62,77p' src/lib/provider-model-redirect-schema.ts

Repository: ding113/claude-code-hub

Length of output: 443


Schema 去重逻辑与运行时匹配逻辑的大小写处理不一致。

第 66 行使用 .toLowerCase() 进行去重,意味着 "GPT-4""gpt-4" 会被视为重复规则而被拒绝。但根据 src/lib/provider-model-redirects.ts 的运行时匹配逻辑,所有匹配类型都采用大小写敏感的比较(exact 类型使用 model === rule.sourceprefixstartsWithsuffixendsWithcontainsincludesregex 未使用 i 标志)。

这会导致语义不一致:用户无法同时添加 "GPT-4""gpt-4" 的规则(被 schema 拒绝),但实际上它们在运行时匹配的是不同的模型名称。

建议移除 .toLowerCase() 使去重逻辑与运行时保持一致:

建议修改
   .refine(
     (rules) => {
       const keys = new Set<string>();
       for (const rule of rules) {
-        const key = `${rule.matchType}:${rule.source.trim().toLowerCase()}`;
+        const key = `${rule.matchType}:${rule.source.trim()}`;
         if (keys.has(key)) {
           return false;
         }
         keys.add(key);
       }
       return true;
     },
📝 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
.refine(
(rules) => {
const keys = new Set<string>();
for (const rule of rules) {
const key = `${rule.matchType}:${rule.source.trim().toLowerCase()}`;
if (keys.has(key)) {
return false;
}
keys.add(key);
}
return true;
},
{
message: "Duplicate redirect rule for matchType+source",
}
);
.refine(
(rules) => {
const keys = new Set<string>();
for (const rule of rules) {
const key = `${rule.matchType}:${rule.source.trim()}`;
if (keys.has(key)) {
return false;
}
keys.add(key);
}
return true;
},
{
message: "Duplicate redirect rule for matchType+source",
}
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/provider-model-redirect-schema.ts` around lines 62 - 77, 当前 refine
去重逻辑在构建唯一键时对 rule.source 使用 .toLowerCase(), 导致 schema 将 "GPT-4" 与 "gpt-4"
视为重复但运行时匹配是大小写敏感的;请在该 .refine 的循环中移除 .toLowerCase()(保留 .trim() 如需),即将 key 从
`${rule.matchType}:${rule.source.trim().toLowerCase()}` 改为
`${rule.matchType}:${rule.source.trim()}`,以使 provider-model-redirect-schema
的去重行为与 src/lib/provider-model-redirects.ts 的运行时匹配逻辑一致。

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

Labels

area:core area:provider area:UI enhancement New feature or request size/L Large PR (< 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant