feat: support provider model redirect rules#993
Conversation
📝 WalkthroughWalkthrough将 provider 的 modelRedirects 数据模型从键值映射迁移为有序规则数组(ProviderModelRedirectRule[]),引入匹配类型、规则验证/规范化与安全检查、规则应用/查找工具、编辑/重排 UI、对应本地化字符串及大量测试更新(不超过50字)。 Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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: "模型重定向规则格式无效" }; |
There was a problem hiding this comment.
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.
| 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); |
| onChange={(e) => setEditValue(e.target.value)} | ||
| onInput={(e) => setEditValue((e.target as HTMLInputElement).value)} |
There was a problem hiding this comment.
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.
| onChange={(e) => setEditValue(e.target.value)} | |
| onInput={(e) => setEditValue((e.target as HTMLInputElement).value)} | |
| onChange={(e) => setEditValue(e.target.value)} |
There was a problem hiding this comment.
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
-
Backwards Compatibility: The
normalizeProviderModelRedirectRulesfunction elegantly handles legacy map-to-array migration without breaking changes. -
Security Considerations: Proper ReDoS protection using
safe-regexfor regex patterns, with 255-character limits on source/target strings. -
Test Coverage: Includes tests for rule ordering, match types, legacy normalization, and UI interaction edge cases.
-
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(returningfalseoncatch) 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
orderHinti18n key.
Conclusion
No blocking issues identified. The PR is approved for merge to dev.
Reviewed by Claude AI
| case "regex": | ||
| try { | ||
| return new RegExp(rule.source).test(model); | ||
| } catch { | ||
| return false; | ||
| } |
There was a problem hiding this 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.
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.| 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, | ||
| })) |
There was a problem hiding this 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.
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.There was a problem hiding this comment.
💡 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".
| case "model_redirects": | ||
| return isStringRecord(value); | ||
| return PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA.safeParse(value).success; |
There was a problem hiding this comment.
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 👍 / 👎.
| function getRuleIdentity(rule: Pick<ProviderModelRedirectRule, "matchType" | "source">): string { | ||
| return `${rule.matchType}:${rule.source.trim()}`; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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处理器。
onChange和onInput都会触发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
📒 Files selected for processing (42)
messages/en/settings/providers/form/modelRedirect.jsonmessages/en/settings/providers/form/modelSelect.jsonmessages/ja/settings/providers/form/modelRedirect.jsonmessages/ja/settings/providers/form/modelSelect.jsonmessages/ru/settings/providers/form/modelRedirect.jsonmessages/ru/settings/providers/form/modelSelect.jsonmessages/zh-CN/settings/providers/form/modelRedirect.jsonmessages/zh-CN/settings/providers/form/modelSelect.jsonmessages/zh-TW/settings/providers/form/modelRedirect.jsonmessages/zh-TW/settings/providers/form/modelSelect.jsonsrc/actions/providers.tssrc/app/[locale]/settings/providers/_components/batch-edit/analyze-batch-settings.tssrc/app/[locale]/settings/providers/_components/batch-edit/build-patch-draft.tssrc/app/[locale]/settings/providers/_components/forms/provider-form.legacy.tsxsrc/app/[locale]/settings/providers/_components/forms/provider-form/index.tsxsrc/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsxsrc/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.tssrc/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsxsrc/app/[locale]/settings/providers/_components/model-multi-select.tsxsrc/app/[locale]/settings/providers/_components/model-redirect-editor.tsxsrc/app/api/actions/[...route]/route.tssrc/app/v1/_lib/proxy/model-redirector.tssrc/drizzle/schema.tssrc/lib/provider-model-redirect-schema.tssrc/lib/provider-model-redirects.tssrc/lib/provider-patch-contract.tssrc/lib/validation/schemas.tssrc/repository/_shared/transformers.tssrc/repository/provider.tssrc/types/message.tssrc/types/provider.tstests/api/providers-actions.test.tstests/integration/batch-edit-prefill.test.tstests/unit/actions/providers-batch-field-mapping.test.tstests/unit/actions/providers-patch-contract.test.tstests/unit/batch-edit/analyze-batch-settings.test.tstests/unit/dashboard/provider-form-clone-deep-copy.test.tstests/unit/lib/provider-model-redirects.test.tstests/unit/settings/providers/build-patch-draft.test.tstests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsxtests/unit/settings/providers/model-redirect-editor.test.tsxtests/unit/settings/providers/provider-form-batch-context.test.ts
src/app/[locale]/settings/providers/_components/model-redirect-editor.tsx
Show resolved
Hide resolved
src/app/[locale]/settings/providers/_components/model-redirect-editor.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/app/[locale]/settings/providers/_components/model-redirect-editor.tsx (2)
443-482: 移除冗余的onInput处理器。新规则输入框同样存在
onChange和onInput重复绑定的问题,建议保持一致性移除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处理器。编辑模式下的
source和target输入框同时绑定了onChange和onInput,两者执行相同的状态更新操作。在 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
📒 Files selected for processing (9)
messages/en/settings/providers/form/modelRedirect.jsonmessages/ja/settings/providers/form/modelRedirect.jsonmessages/ru/settings/providers/form/modelRedirect.jsonmessages/zh-CN/settings/providers/form/modelRedirect.jsonmessages/zh-TW/settings/providers/form/modelRedirect.jsonsrc/app/[locale]/settings/providers/_components/model-redirect-editor.tsxtests/unit/actions/providers-patch-contract.test.tstests/unit/lib/provider-model-redirects.test.tstests/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
| 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"], | ||
| }); | ||
| } |
There was a problem hiding this 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.
| 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.| if (normalized.matchType === "regex") { | ||
| if (!safeRegex(normalized.source)) { | ||
| return t("regexUnsafe"); | ||
| } | ||
|
|
||
| try { | ||
| new RegExp(normalized.source); | ||
| } catch { | ||
| return t("regexInvalid"); | ||
| } |
There was a problem hiding this 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.
| 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.There was a problem hiding this comment.
💡 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".
| {selectedModels.map((model) => { | ||
| const isEditing = editingModel === model; | ||
|
|
||
| return ( | ||
| <div |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
💡 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)) { |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
💡 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)) { |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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 👍 / 👎.
🧪 测试结果
总体结果: ✅ 所有测试通过 |
There was a problem hiding this comment.
💡 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".
| if (editingIndex === index) { | ||
| setEditingIndex(null); | ||
| setEditValue(""); | ||
| } | ||
| onChange(selectedModels.filter((_, currentIndex) => currentIndex !== index)); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
src/app/[locale]/settings/providers/_components/model-multi-select.tsxsrc/app/[locale]/settings/providers/_components/model-redirect-editor.tsxsrc/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
| const [editingIndex, setEditingIndex] = useState<number | null>(null); | ||
| const [editValue, setEditValue] = useState(""); | ||
| const [managementError, setManagementError] = useState<string | null>(null); |
There was a problem hiding this comment.
数组替换和索引编辑绑在一起,会把修改写到错误项。
这里的编辑态用 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.
| <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> | ||
|
|
There was a problem hiding this comment.
禁用态下弹层里的修改入口仍然可用。
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.
| <Button | ||
| size="icon" | ||
| variant="ghost" | ||
| className="h-6 w-6" | ||
| onClick={(e) => { | ||
| e.stopPropagation(); | ||
| loadModels(); | ||
| }} | ||
| type="button" | ||
| > | ||
| <RefreshCw className="h-3 w-3" /> | ||
| </Button> |
There was a problem hiding this comment.
纯图标按钮需要补可访问名称。
这些按钮现在只有 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.
| .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", | ||
| } | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 检查运行时匹配逻辑中是否有大小写转换
rg -n -A5 'matchesProviderModelRedirectRule' src/lib/provider-model-redirects.tsRepository: ding113/claude-code-hub
Length of output: 373
🏁 Script executed:
cat -n src/lib/provider-model-redirects.ts | head -150 | tail -70Repository: ding113/claude-code-hub
Length of output: 2290
🏁 Script executed:
sed -n '62,77p' src/lib/provider-model-redirect-schema.tsRepository: ding113/claude-code-hub
Length of output: 443
Schema 去重逻辑与运行时匹配逻辑的大小写处理不一致。
第 66 行使用 .toLowerCase() 进行去重,意味着 "GPT-4" 和 "gpt-4" 会被视为重复规则而被拒绝。但根据 src/lib/provider-model-redirects.ts 的运行时匹配逻辑,所有匹配类型都采用大小写敏感的比较(exact 类型使用 model === rule.source、prefix 用 startsWith、suffix 用 endsWith、contains 用 includes,regex 未使用 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.
| .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 的运行时匹配逻辑一致。
Summary
Record<string, string>) to ordered rule lists with exact/prefix/suffix/contains/regex matchingProblem
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., allclaude-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 orderedProviderModelRedirectRule[]array where each rule specifies amatchType(exact, prefix, suffix, contains, regex), asourcepattern, and atargetmodel name. Rules are evaluated in order and the first match wins.Key design decisions:
Record<string, string>and newProviderModelRedirectRule[]formats;normalizeProviderModelRedirectRules()converts legacy maps to exact-match rules on readsafe-regexat both the Zod schema level and the UI levelChanges
Core Changes
src/types/provider.ts(+15/-7): NewProviderModelRedirectMatchTypeunion type andProviderModelRedirectRuleinterface; all provider types updated fromRecord<string, string>toProviderModelRedirectRule[]src/lib/provider-model-redirects.ts(+138, new): Core matching engine withnormalizeProviderModelRedirectRules(),findMatchingProviderModelRedirectRule(),matchesProviderModelRedirectRule(), andgetProviderModelRedirectTarget()src/lib/provider-model-redirect-schema.ts(+56, new): Zod validation schemas with ReDoS protection viasafe-regexsrc/app/v1/_lib/proxy/model-redirector.ts(+22/-5): Replaced map lookup with rule-based matching; logsmatchTypeandmatchedSourcefor audit; passesmatchedRulein provider chain metadatasrc/drizzle/schema.ts(+4/-2): DB schema accepts both old and new formats:ProviderModelRedirectRule[] | Record<string, string> | nullsrc/types/message.ts(+5): AddedmatchedRulefield toProviderChainItem.modelRedirectfor audit trailAPI & Validation Changes
src/actions/providers.ts(+19/-4):batchUpdateProvidersvalidates rules withPROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMAand normalizes themsrc/lib/validation/schemas.ts(+3/-2):CreateProviderSchemaandUpdateProviderSchemause new rule schemasrc/lib/provider-patch-contract.ts(+2/-11): ReplacedisStringRecord()withPROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMAvalidationsrc/repository/_shared/transformers.ts(+2/-1):toProvider()normalizes legacy maps on readsrc/repository/provider.ts(+4/-2):createProvider()normalizes input redirectssrc/app/api/actions/[...route]/route.ts(+5/-1): OpenAPI response schema updated to array formatUI 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 hintmodel-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 modelSupporting Changes
provider-form.legacy.tsx,provider-form/index.tsx,provider-form-context.tsx,provider-form-types.ts,routing-section.tsx): Updated to useProviderModelRedirectRule[]instead ofRecord<string, string>analyze-batch-settings.ts,build-patch-draft.ts): Updated field analysis and patch draft buildingTest Changes
tests/unit/lib/provider-model-redirects.test.ts(+78, new): Unit tests for matching engine (prefix/suffix/contains/regex), null-match, and legacy normalizationtests/unit/settings/providers/model-redirect-editor.test.tsx(+171, new): UI tests for rule editing after deletion and source length validationtests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx(+111/-1): Tests for out-of-dropdown editing and selected models pinned sectiontests/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 formatProviderModelRedirectRule[]formatBreaking Changes
Record<string, string>toProviderModelRedirectRule[]addProvider/editProviderwithmodel_redirectsmust use new format{"claude-3-opus": "glm-4.6"}to[{"matchType":"exact","source":"claude-3-opus","target":"glm-4.6"}]model_redirectsfrom provider responses receive array instead of objectProviderModelRedirectRule[]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
Verification
bun run lint -- --max-diagnostics 60bun run typecheckbunx 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.tsbun run testbun run buildChecklist
Description enhanced by Claude AI
Greptile Summary
This PR upgrades model redirect rules from a flat
Record<string, string>to an orderedProviderModelRedirectRule[]with five match types (exact, prefix, suffix, contains, regex), backward-compatible normalization of legacy DB records, ReDoS protection viasafe-regex, and a fully rewritten UI editor with rule reordering and inline editing. The previous P1 concerns aroundsafe-regexbeing called before regex-syntax validation (in both the Zod schema and the UIvalidateRule) have been addressed in this revision — thenew RegExpcheck now runs first with an earlyreturn, sosafeRegexis 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
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)Prompt To Fix All With AI
Reviews (4): Last reviewed commit: "fix: harden provider redirect validation" | Re-trigger Greptile