diff --git a/messages/en/settings/providers/form/modelRedirect.json b/messages/en/settings/providers/form/modelRedirect.json
index 48628bb53..157fcd5be 100644
--- a/messages/en/settings/providers/form/modelRedirect.json
+++ b/messages/en/settings/providers/form/modelRedirect.json
@@ -3,12 +3,31 @@
   "addNewRule": "Add New Rule",
   "alreadyExists": "Model \"{model}\" already has a redirect rule",
   "currentRules": "Current Rules ({count})",
+  "matchTypeLabel": "Match Type",
+  "matchTypeExact": "Exact",
+  "matchTypePrefix": "Prefix",
+  "matchTypeSuffix": "Suffix",
+  "matchTypeContains": "Contains",
+  "matchTypeRegex": "Regex",
+  "orderHint": "Top rules match first",
+  "maxRules": "You can add up to 100 redirect rules",
+  "saveRule": "Save rule",
+  "cancelEdit": "Cancel edit",
+  "moveRuleUp": "Move rule up",
+  "moveRuleDown": "Move rule down",
+  "editRule": "Edit rule",
+  "deleteRule": "Delete rule",
+  "ruleMoved": "This rule changed while you were editing. Please try again.",
   "description": "Redirect Claude Code client requested models (e.g. claude-sonnet-4.5) to upstream provider supported models (e.g. glm-4.6, gemini-pro). For cost optimization or third-party AI integration.",
   "emptyState": "No redirect rules yet. After adding rules, the system will automatically rewrite model names in requests.",
   "sourceEmpty": "Source model name cannot be empty",
+  "sourceTooLong": "Source model name is too long",
   "sourceModel": "User Requested Model",
   "sourcePlaceholder": "e.g. claude-sonnet-4-5-20250929",
+  "regexInvalid": "Regex pattern is invalid",
+  "regexUnsafe": "Regex pattern has potential ReDoS risk",
   "targetEmpty": "Target model name cannot be empty",
+  "targetTooLong": "Target model name is too long",
   "targetModel": "Actual Forwarded Model",
   "targetPlaceholder": "e.g. glm-4.6"
 }
diff --git a/messages/en/settings/providers/form/modelSelect.json b/messages/en/settings/providers/form/modelSelect.json
index 03fbb53e6..50dadc138 100644
--- a/messages/en/settings/providers/form/modelSelect.json
+++ b/messages/en/settings/providers/form/modelSelect.json
@@ -10,11 +10,17 @@
   "notFound": "Model not found",
   "openai": "OpenAI",
   "refresh": "Refresh model list",
+  "selectedEditEmpty": "Model name cannot be empty",
+  "selectedEditExists": "Model \"{model}\" is already selected",
+  "selectedGroupLabel": "Selected Models",
   "searchPlaceholder": "Search model name...",
   "selectAll": "Select All ({count})",
   "selectedCount": "Selected {count} models",
+  "selectedListHint": "Edit or remove without opening the dropdown",
+  "selectedListLabel": "Selected whitelist ({count})",
   "sourceFallback": "Local",
   "sourceFallbackDesc": "Using local price list (upstream unavailable or unsupported)",
   "sourceUpstream": "Upstream",
-  "sourceUpstreamDesc": "Model list from upstream provider API"
+  "sourceUpstreamDesc": "Model list from upstream provider API",
+  "availableGroupLabel": "Available Models"
 }
diff --git a/messages/ja/settings/providers/form/modelRedirect.json b/messages/ja/settings/providers/form/modelRedirect.json
index cd55a47ec..f631af6ea 100644
--- a/messages/ja/settings/providers/form/modelRedirect.json
+++ b/messages/ja/settings/providers/form/modelRedirect.json
@@ -3,12 +3,31 @@
   "addNewRule": "新規ルールを追加",
   "alreadyExists": "モデル \"{model}\" のリダイレクトルールは既に存在します",
   "currentRules": "現在のルール ({count})",
+  "matchTypeLabel": "一致方法",
+  "matchTypeExact": "完全一致",
+  "matchTypePrefix": "前方一致",
+  "matchTypeSuffix": "後方一致",
+  "matchTypeContains": "部分一致",
+  "matchTypeRegex": "正規表現",
+  "orderHint": "上のルールから順に適用されます",
+  "maxRules": "追加できるリダイレクトルールは 100 件までです",
+  "saveRule": "ルールを保存",
+  "cancelEdit": "編集をキャンセル",
+  "moveRuleUp": "ルールを上へ移動",
+  "moveRuleDown": "ルールを下へ移動",
+  "editRule": "ルールを編集",
+  "deleteRule": "ルールを削除",
+  "ruleMoved": "編集中にルールが変更されました。もう一度お試しください。",
   "description": "Claude Code クライアントがリクエストするモデル（例：claude-sonnet-4.5）を、上流プロバイダーが実際にサポートするモデル（例：glm-4.6、gemini-pro）にリダイレクトします。コスト最適化やサードパーティAIサービスへの接続に使用します。",
   "emptyState": "リダイレクトルールがありません。ルールを追加すると、システムは自動的にリクエスト内のモデル名を書き換えます。",
   "sourceEmpty": "ソースモデル名を入力してください",
+  "sourceTooLong": "ソースモデル名が長すぎます",
   "sourceModel": "ユーザーがリクエストするモデル",
   "sourcePlaceholder": "例：claude-sonnet-4-5-20250929",
   "targetEmpty": "ターゲットモデル名を入力してください",
+  "targetTooLong": "ターゲットモデル名が長すぎます",
   "targetModel": "実際に転送されるモデル",
-  "targetPlaceholder": "例：glm-4.6"
+  "targetPlaceholder": "例：glm-4.6",
+  "regexInvalid": "正規表現が無効です",
+  "regexUnsafe": "正規表現に ReDoS のリスクがあります"
 }
diff --git a/messages/ja/settings/providers/form/modelSelect.json b/messages/ja/settings/providers/form/modelSelect.json
index 501307f22..1d034d487 100644
--- a/messages/ja/settings/providers/form/modelSelect.json
+++ b/messages/ja/settings/providers/form/modelSelect.json
@@ -10,11 +10,17 @@
   "notFound": "モデルが見つかりません",
   "openai": "OpenAI",
   "refresh": "モデルリストを更新",
+  "selectedEditEmpty": "モデル名は空にできません",
+  "selectedEditExists": "モデル \"{model}\" はすでに選択されています",
+  "selectedGroupLabel": "選択済みモデル",
   "searchPlaceholder": "モデル名を検索...",
   "selectAll": "すべて選択 ({count})",
   "selectedCount": "{count} 個のモデルを選択済み",
+  "selectedListHint": "ドロップダウンを開かずに編集・削除できます",
+  "selectedListLabel": "選択済みホワイトリスト ({count})",
   "sourceFallback": "ローカル",
   "sourceFallbackDesc": "ローカル価格表のモデルリストを使用（上流が利用不可または未対応）",
   "sourceUpstream": "上流",
-  "sourceUpstreamDesc": "モデルリストは上流プロバイダーAPIから取得"
+  "sourceUpstreamDesc": "モデルリストは上流プロバイダーAPIから取得",
+  "availableGroupLabel": "利用可能なモデル"
 }
diff --git a/messages/ru/settings/providers/form/modelRedirect.json b/messages/ru/settings/providers/form/modelRedirect.json
index 6ec29dce4..79a7c9f67 100644
--- a/messages/ru/settings/providers/form/modelRedirect.json
+++ b/messages/ru/settings/providers/form/modelRedirect.json
@@ -3,12 +3,31 @@
   "addNewRule": "Добавить новое правило",
   "alreadyExists": "Правило перенаправления для модели \"{model}\" уже существует",
   "currentRules": "Текущие правила ({count})",
+  "matchTypeLabel": "Тип совпадения",
+  "matchTypeExact": "Точное совпадение",
+  "matchTypePrefix": "Префикс",
+  "matchTypeSuffix": "Суффикс",
+  "matchTypeContains": "Содержит",
+  "matchTypeRegex": "Регулярное выражение",
+  "orderHint": "Правила сверху применяются первыми",
+  "maxRules": "Можно добавить не более 100 правил перенаправления",
+  "saveRule": "Сохранить правило",
+  "cancelEdit": "Отменить редактирование",
+  "moveRuleUp": "Переместить правило вверх",
+  "moveRuleDown": "Переместить правило вниз",
+  "editRule": "Редактировать правило",
+  "deleteRule": "Удалить правило",
+  "ruleMoved": "Правило изменилось во время редактирования. Повторите попытку.",
   "description": "Перенаправлять запросы моделей от клиента Claude Code (например, claude-sonnet-4.5) к моделям, поддерживаемым вышестоящим провайдером (например, glm-4.6, gemini-pro). Используется для оптимизации затрат или подключения сторонних AI-сервисов.",
   "emptyState": "Пока нет правил перенаправления. После добавления правил система автоматически переписывает названия моделей в запросах.",
   "sourceEmpty": "Название исходной модели не может быть пустым",
+  "sourceTooLong": "Название исходной модели слишком длинное",
   "sourceModel": "Запрашиваемая пользователем модель",
   "sourcePlaceholder": "Например: claude-sonnet-4-5-20250929",
   "targetEmpty": "Название целевой модели не может быть пустым",
+  "targetTooLong": "Название целевой модели слишком длинное",
   "targetModel": "Фактически перенаправляемая модель",
-  "targetPlaceholder": "Например: glm-4.6"
+  "targetPlaceholder": "Например: glm-4.6",
+  "regexInvalid": "Некорректное регулярное выражение",
+  "regexUnsafe": "Регулярное выражение имеет риск ReDoS"
 }
diff --git a/messages/ru/settings/providers/form/modelSelect.json b/messages/ru/settings/providers/form/modelSelect.json
index 2fb1713ef..d5393d750 100644
--- a/messages/ru/settings/providers/form/modelSelect.json
+++ b/messages/ru/settings/providers/form/modelSelect.json
@@ -10,11 +10,17 @@
   "notFound": "Модели не найдены",
   "openai": "OpenAI",
   "refresh": "Обновить список моделей",
+  "selectedEditEmpty": "Имя модели не может быть пустым",
+  "selectedEditExists": "Модель \"{model}\" уже выбрана",
+  "selectedGroupLabel": "Выбранные модели",
   "searchPlaceholder": "Поиск по названию модели...",
   "selectAll": "Выбрать все ({count})",
   "selectedCount": "Выбрано моделей: {count}",
+  "selectedListHint": "Можно редактировать и удалять без открытия списка",
+  "selectedListLabel": "Белый список ({count})",
   "sourceFallback": "Локально",
   "sourceFallbackDesc": "Используется локальный прайс-лист (upstream недоступен или не поддерживается)",
   "sourceUpstream": "Upstream",
-  "sourceUpstreamDesc": "Список моделей из API провайдера"
+  "sourceUpstreamDesc": "Список моделей из API провайдера",
+  "availableGroupLabel": "Доступные модели"
 }
diff --git a/messages/zh-CN/settings/providers/form/modelRedirect.json b/messages/zh-CN/settings/providers/form/modelRedirect.json
index fc00c1e20..b6e0da055 100644
--- a/messages/zh-CN/settings/providers/form/modelRedirect.json
+++ b/messages/zh-CN/settings/providers/form/modelRedirect.json
@@ -1,14 +1,33 @@
 {
   "currentRules": "当前规则 ({count})",
   "addNewRule": "添加新规则",
+  "matchTypeLabel": "匹配方式",
+  "matchTypeExact": "精确匹配",
+  "matchTypePrefix": "前缀匹配",
+  "matchTypeSuffix": "后缀匹配",
+  "matchTypeContains": "关键词匹配",
+  "matchTypeRegex": "正则匹配",
+  "orderHint": "靠前规则优先生效",
+  "maxRules": "最多只能添加 100 条重定向规则",
+  "saveRule": "保存规则",
+  "cancelEdit": "取消编辑",
+  "moveRuleUp": "上移规则",
+  "moveRuleDown": "下移规则",
+  "editRule": "编辑规则",
+  "deleteRule": "删除规则",
+  "ruleMoved": "规则在编辑期间发生了变化，请重试",
   "sourceModel": "用户请求的模型",
   "targetModel": "实际转发的模型",
   "sourcePlaceholder": "例如: claude-sonnet-4-5-20250929",
   "targetPlaceholder": "例如: glm-4.6",
   "add": "添加",
   "sourceEmpty": "源模型名称不能为空",
+  "sourceTooLong": "源模型名称过长",
   "targetEmpty": "目标模型名称不能为空",
+  "targetTooLong": "目标模型名称过长",
   "alreadyExists": "模型 \"{model}\" 已存在重定向规则",
+  "regexInvalid": "正则表达式无效",
+  "regexUnsafe": "正则表达式存在 ReDoS 风险",
   "description": "将 Claude Code 客户端请求的模型（如 claude-sonnet-4.5）重定向到上游供应商实际支持的模型（如 glm-4.6、gemini-pro）。用于成本优化或接入第三方 AI 服务。",
   "emptyState": "暂无重定向规则。添加规则后，系统将自动重写请求中的模型名称。"
 }
diff --git a/messages/zh-CN/settings/providers/form/modelSelect.json b/messages/zh-CN/settings/providers/form/modelSelect.json
index 663c2e8d2..46e1f5a82 100644
--- a/messages/zh-CN/settings/providers/form/modelSelect.json
+++ b/messages/zh-CN/settings/providers/form/modelSelect.json
@@ -16,5 +16,11 @@
   "sourceUpstreamDesc": "模型列表来自上游服务商 API",
   "sourceFallback": "本地",
   "sourceFallbackDesc": "使用本地价格表中的模型列表（上游获取失败或不支持）",
-  "refresh": "刷新模型列表"
+  "refresh": "刷新模型列表",
+  "selectedEditEmpty": "模型名称不能为空",
+  "selectedEditExists": "模型 \"{model}\" 已在白名单中",
+  "selectedGroupLabel": "已选模型",
+  "selectedListHint": "无需展开下拉框即可直接编辑或删除",
+  "selectedListLabel": "已选白名单 ({count})",
+  "availableGroupLabel": "可选模型"
 }
diff --git a/messages/zh-TW/settings/providers/form/modelRedirect.json b/messages/zh-TW/settings/providers/form/modelRedirect.json
index 779c7bd00..9a4f7b8b4 100644
--- a/messages/zh-TW/settings/providers/form/modelRedirect.json
+++ b/messages/zh-TW/settings/providers/form/modelRedirect.json
@@ -3,12 +3,31 @@
   "addNewRule": "新增規則",
   "alreadyExists": "模型「{model}」已存在重新導向規則",
   "currentRules": "目前規則 ({count})",
+  "matchTypeLabel": "比對方式",
+  "matchTypeExact": "精確比對",
+  "matchTypePrefix": "前綴比對",
+  "matchTypeSuffix": "後綴比對",
+  "matchTypeContains": "關鍵字比對",
+  "matchTypeRegex": "正則比對",
+  "orderHint": "越前面的規則越先命中",
+  "maxRules": "最多只能新增 100 條重新導向規則",
+  "saveRule": "儲存規則",
+  "cancelEdit": "取消編輯",
+  "moveRuleUp": "上移規則",
+  "moveRuleDown": "下移規則",
+  "editRule": "編輯規則",
+  "deleteRule": "刪除規則",
+  "ruleMoved": "規則在編輯期間已變更，請再試一次",
   "description": "將 Claude Code 客戶端請求的模型（如 claude-sonnet-4.5）重新導向到上游供應商實際支援的模型（如 glm-4.6、gemini-pro）。用於成本最佳化或接入第三方 AI 服務。",
   "emptyState": "目前無重新導向規則。新增規則後，系統將自動重寫請求中的模型名稱。",
   "sourceEmpty": "來源模型名稱不能為空",
+  "sourceTooLong": "來源模型名稱過長",
   "sourceModel": "使用者請求的模型",
   "sourcePlaceholder": "例如：claude-sonnet-4-5-20250929",
   "targetEmpty": "目標模型名稱不能為空",
+  "targetTooLong": "目標模型名稱過長",
   "targetModel": "實際轉發的模型",
-  "targetPlaceholder": "例如：glm-4.6"
+  "targetPlaceholder": "例如：glm-4.6",
+  "regexInvalid": "正則表達式無效",
+  "regexUnsafe": "正則表達式存在 ReDoS 風險"
 }
diff --git a/messages/zh-TW/settings/providers/form/modelSelect.json b/messages/zh-TW/settings/providers/form/modelSelect.json
index 9a4186b49..1f0e99089 100644
--- a/messages/zh-TW/settings/providers/form/modelSelect.json
+++ b/messages/zh-TW/settings/providers/form/modelSelect.json
@@ -10,11 +10,17 @@
   "notFound": "找不到模型",
   "openai": "OpenAI",
   "refresh": "重新整理模型列表",
+  "selectedEditEmpty": "模型名稱不能為空",
+  "selectedEditExists": "模型「{model}」已在白名單中",
+  "selectedGroupLabel": "已選模型",
   "searchPlaceholder": "搜尋模型名稱...",
   "selectAll": "全選 ({count})",
   "selectedCount": "已選擇 {count} 個模型",
+  "selectedListHint": "無需展開下拉框即可直接編輯或刪除",
+  "selectedListLabel": "已選白名單 ({count})",
   "sourceFallback": "本機",
   "sourceFallbackDesc": "使用本地價格表中的模型列表（上游獲取失敗或不支援）",
   "sourceUpstream": "上游來源",
-  "sourceUpstreamDesc": "模型列表來自上游服務商 API"
+  "sourceUpstreamDesc": "模型列表來自上游服務商 API",
+  "availableGroupLabel": "可選模型"
 }
diff --git a/src/actions/providers.ts b/src/actions/providers.ts
index d9dd1ccec..5b32f38b3 100644
--- a/src/actions/providers.ts
+++ b/src/actions/providers.ts
@@ -20,6 +20,8 @@ import {
 import { PROVIDER_GROUP, PROVIDER_TIMEOUT_DEFAULTS } from "@/lib/constants/provider.constants";
 import { logger } from "@/lib/logger";
 import { PROVIDER_BATCH_PATCH_ERROR_CODES } from "@/lib/provider-batch-patch-error-codes";
+import { PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA } from "@/lib/provider-model-redirect-schema";
+import { normalizeProviderModelRedirectRules } from "@/lib/provider-model-redirects";
 import {
   buildProviderBatchApplyUpdates,
   hasProviderBatchPatchChanges,
@@ -85,6 +87,7 @@ import type {
   ProviderBatchPatch,
   ProviderBatchPatchField,
   ProviderDisplay,
+  ProviderModelRedirectRule,
   ProviderPatchOperation,
   ProviderStatisticsMap,
   ProviderType,
@@ -500,7 +503,7 @@ export async function addProvider(data: {
   provider_type?: ProviderType;
   preserve_client_ip?: boolean;
   disable_session_reuse?: boolean;
-  model_redirects?: Record<string, string> | null;
+  model_redirects?: ProviderModelRedirectRule[] | null;
   active_time_start?: string | null;
   active_time_end?: string | null;
   allowed_models?: string[] | null;
@@ -679,7 +682,7 @@ export async function editProvider(
     provider_type?: ProviderType;
     preserve_client_ip?: boolean;
     disable_session_reuse?: boolean;
-    model_redirects?: Record<string, string> | null;
+    model_redirects?: ProviderModelRedirectRule[] | null;
     active_time_start?: string | null;
     active_time_end?: string | null;
     allowed_models?: string[] | null;
@@ -2197,7 +2200,7 @@ export interface BatchUpdateProvidersParams {
     weight?: number;
     cost_multiplier?: number;
     group_tag?: string | null;
-    model_redirects?: Record<string, string> | null;
+    model_redirects?: ProviderModelRedirectRule[] | null;
     allowed_models?: string[] | null;
     allowed_clients?: string[];
     blocked_clients?: string[];
@@ -2244,7 +2247,19 @@ export async function batchUpdateProviders(
       repositoryUpdates.groupTag = normalizeProviderGroupTag(updates.group_tag);
     }
     if (updates.model_redirects !== undefined) {
-      repositoryUpdates.modelRedirects = updates.model_redirects;
+      if (updates.model_redirects === null) {
+        repositoryUpdates.modelRedirects = null;
+      } else {
+        const parsedRedirectRules = PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA.safeParse(
+          updates.model_redirects
+        );
+        if (!parsedRedirectRules.success) {
+          return { ok: false, error: "模型重定向规则格式无效" };
+        }
+        repositoryUpdates.modelRedirects = normalizeProviderModelRedirectRules(
+          parsedRedirectRules.data
+        );
+      }
     }
     if (updates.allowed_models !== undefined) {
       repositoryUpdates.allowedModels =
diff --git a/src/app/[locale]/settings/providers/_components/batch-edit/analyze-batch-settings.ts b/src/app/[locale]/settings/providers/_components/batch-edit/analyze-batch-settings.ts
index 2fddf03ea..6f2c797a7 100644
--- a/src/app/[locale]/settings/providers/_components/batch-edit/analyze-batch-settings.ts
+++ b/src/app/[locale]/settings/providers/_components/batch-edit/analyze-batch-settings.ts
@@ -12,6 +12,7 @@ import type {
   GeminiGoogleSearchPreference,
   McpPassthroughType,
   ProviderDisplay,
+  ProviderModelRedirectRule,
 } from "@/types/provider";
 import { deepEquals } from "./deep-equals";
 
@@ -30,7 +31,7 @@ export interface BatchSettingsAnalysis {
     groupTag: FieldAnalysisResult<string[]>;
     preserveClientIp: FieldAnalysisResult<boolean>;
     disableSessionReuse: FieldAnalysisResult<boolean>;
-    modelRedirects: FieldAnalysisResult<Record<string, string>>;
+    modelRedirects: FieldAnalysisResult<ProviderModelRedirectRule[]>;
     allowedModels: FieldAnalysisResult<string[]>;
     allowedClients: FieldAnalysisResult<string[]>;
     blockedClients: FieldAnalysisResult<string[]>;
@@ -121,7 +122,7 @@ export function analyzeBatchProviderSettings(providers: ProviderDisplay[]): Batc
       groupTag: analyzeField(providers, (p) => parseProviderGroups(p.groupTag)),
       preserveClientIp: analyzeField(providers, (p) => p.preserveClientIp),
       disableSessionReuse: analyzeField(providers, (p) => p.disableSessionReuse),
-      modelRedirects: analyzeField(providers, (p) => p.modelRedirects ?? {}),
+      modelRedirects: analyzeField(providers, (p) => p.modelRedirects ?? []),
       allowedModels: analyzeField(providers, (p) => p.allowedModels ?? []),
       allowedClients: analyzeField(providers, (p) => p.allowedClients ?? []),
       blockedClients: analyzeField(providers, (p) => p.blockedClients ?? []),
diff --git a/src/app/[locale]/settings/providers/_components/batch-edit/build-patch-draft.ts b/src/app/[locale]/settings/providers/_components/batch-edit/build-patch-draft.ts
index b0ec4490a..51895e7b9 100644
--- a/src/app/[locale]/settings/providers/_components/batch-edit/build-patch-draft.ts
+++ b/src/app/[locale]/settings/providers/_components/batch-edit/build-patch-draft.ts
@@ -61,8 +61,7 @@ export function buildPatchDraftFromFormState(
     }
   }
   if (dirtyFields.has("routing.modelRedirects")) {
-    const entries = Object.keys(state.routing.modelRedirects);
-    if (entries.length === 0) {
+    if (state.routing.modelRedirects.length === 0) {
       draft.model_redirects = { clear: true };
     } else {
       draft.model_redirects = { set: state.routing.modelRedirects };
diff --git a/src/app/[locale]/settings/providers/_components/forms/provider-form.legacy.tsx b/src/app/[locale]/settings/providers/_components/forms/provider-form.legacy.tsx
index 916a23c9f..bb8b40371 100644
--- a/src/app/[locale]/settings/providers/_components/forms/provider-form.legacy.tsx
+++ b/src/app/[locale]/settings/providers/_components/forms/provider-form.legacy.tsx
@@ -33,6 +33,7 @@ import { Switch } from "@/components/ui/switch";
 import { TagInput } from "@/components/ui/tag-input";
 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
 import { PROVIDER_DEFAULTS, PROVIDER_TIMEOUT_DEFAULTS } from "@/lib/constants/provider.constants";
+import { normalizeProviderModelRedirectRules } from "@/lib/provider-model-redirects";
 import { getProviderTypeConfig } from "@/lib/provider-type-utils";
 import { parseProviderGroups } from "@/lib/utils/provider-group";
 import {
@@ -48,6 +49,7 @@ import type {
   CodexTextVerbosityPreference,
   McpPassthroughType,
   ProviderDisplay,
+  ProviderModelRedirectRule,
   ProviderType,
 } from "@/types/provider";
 import { ModelMultiSelect } from "../model-multi-select";
@@ -168,8 +170,8 @@ export function ProviderForm({
   const [preserveClientIp, setPreserveClientIp] = useState<boolean>(
     sourceProvider?.preserveClientIp ?? false
   );
-  const [modelRedirects, setModelRedirects] = useState<Record<string, string>>(
-    sourceProvider?.modelRedirects ?? {}
+  const [modelRedirects, setModelRedirects] = useState<ProviderModelRedirectRule[]>(
+    normalizeProviderModelRedirectRules(sourceProvider?.modelRedirects) ?? []
   );
   const [priority, setPriority] = useState<number>(sourceProvider?.priority ?? 0);
   const [weight, setWeight] = useState<number>(sourceProvider?.weight ?? 1);
@@ -454,8 +456,8 @@ export function ProviderForm({
 
   // 实际提交逻辑
   const performSubmit = () => {
-    // 处理模型重定向（空对象转为 null）
-    const parsedModelRedirects = Object.keys(modelRedirects).length > 0 ? modelRedirects : null;
+    // 处理模型重定向（空数组转为 null）
+    const parsedModelRedirects = modelRedirects.length > 0 ? modelRedirects : null;
 
     startTransition(async () => {
       try {
@@ -465,7 +467,7 @@ export function ProviderForm({
             url?: string;
             key?: string;
             provider_type?: ProviderType;
-            model_redirects?: Record<string, string> | null;
+            model_redirects?: ProviderModelRedirectRule[] | null;
             allowed_models?: string[] | null;
             priority?: number;
             weight?: number;
@@ -629,7 +631,7 @@ export function ProviderForm({
           setKey("");
           setProviderType("claude");
           setPreserveClientIp(false);
-          setModelRedirects({});
+          setModelRedirects([]);
           setAllowedModels([]);
           setPriority(0);
           setWeight(1);
@@ -805,10 +807,10 @@ export function ProviderForm({
                           count: allowedModels.length,
                         })
                       );
-                    if (Object.keys(modelRedirects).length > 0)
+                    if (modelRedirects.length > 0)
                       parts.push(
                         t("sections.routing.summary.redirects", {
-                          count: Object.keys(modelRedirects).length,
+                          count: modelRedirects.length,
                         })
                       );
                     return parts.length > 0 ? parts.join(", ") : t("sections.routing.summary.none");
diff --git a/src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx b/src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
index 8ed6d99b5..4d06b6cd7 100644
--- a/src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
+++ b/src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
@@ -299,7 +299,8 @@ function ProviderFormContent({
           provider_type: state.routing.providerType,
           preserve_client_ip: state.routing.preserveClientIp,
           disable_session_reuse: state.routing.disableSessionReuse,
-          model_redirects: state.routing.modelRedirects,
+          model_redirects:
+            state.routing.modelRedirects.length > 0 ? state.routing.modelRedirects : null,
           allowed_models:
             state.routing.allowedModels.length > 0 ? state.routing.allowedModels : null,
           allowed_clients: state.routing.allowedClients,
@@ -488,10 +489,7 @@ function ProviderFormContent({
     }
 
     // Routing - configured if models/redirects set
-    if (
-      state.routing.allowedModels.length > 0 ||
-      Object.keys(state.routing.modelRedirects).length > 0
-    ) {
+    if (state.routing.allowedModels.length > 0 || state.routing.modelRedirects.length > 0) {
       status.routing = "configured";
     }
 
diff --git a/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx b/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
index bc9f61580..4c3f396a5 100644
--- a/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
+++ b/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
@@ -10,6 +10,7 @@ import {
   useReducer,
   useRef,
 } from "react";
+import { normalizeProviderModelRedirectRules } from "@/lib/provider-model-redirects";
 import { parseProviderGroups } from "@/lib/utils/provider-group";
 import type { ProviderDisplay, ProviderType } from "@/types/provider";
 import { analyzeBatchProviderSettings } from "../../batch-edit/analyze-batch-settings";
@@ -110,7 +111,7 @@ export function createInitialState(
         modelRedirects:
           analysis.routing.modelRedirects.status === "uniform"
             ? analysis.routing.modelRedirects.value
-            : {},
+            : [],
         allowedModels:
           analysis.routing.allowedModels.status === "uniform"
             ? analysis.routing.allowedModels.value
@@ -288,7 +289,7 @@ export function createInitialState(
         groupTag: [],
         preserveClientIp: false,
         disableSessionReuse: false,
-        modelRedirects: {},
+        modelRedirects: [],
         allowedModels: [],
         allowedClients: [],
         blockedClients: [],
@@ -363,7 +364,7 @@ export function createInitialState(
       groupTag: parseProviderGroups(sourceProvider?.groupTag),
       preserveClientIp: sourceProvider?.preserveClientIp ?? false,
       disableSessionReuse: sourceProvider?.disableSessionReuse ?? false,
-      modelRedirects: sourceProvider?.modelRedirects ?? {},
+      modelRedirects: normalizeProviderModelRedirectRules(sourceProvider?.modelRedirects) ?? [],
       allowedModels: sourceProvider?.allowedModels ?? [],
       allowedClients: sourceProvider?.allowedClients ?? [],
       blockedClients: sourceProvider?.blockedClients ?? [],
diff --git a/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts b/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts
index 243f89ee3..f6467b0a1 100644
--- a/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts
+++ b/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts
@@ -13,6 +13,7 @@ import type {
   GeminiGoogleSearchPreference,
   McpPassthroughType,
   ProviderDisplay,
+  ProviderModelRedirectRule,
   ProviderType,
 } from "@/types/provider";
 import type { BatchSettingsAnalysis } from "../../batch-edit/analyze-batch-settings";
@@ -49,7 +50,7 @@ export interface RoutingState {
   groupTag: string[];
   preserveClientIp: boolean;
   disableSessionReuse: boolean;
-  modelRedirects: Record<string, string>;
+  modelRedirects: ProviderModelRedirectRule[];
   allowedModels: string[];
   allowedClients: string[];
   blockedClients: string[];
@@ -142,7 +143,7 @@ export type ProviderFormAction =
   | { type: "SET_GROUP_TAG"; payload: string[] }
   | { type: "SET_PRESERVE_CLIENT_IP"; payload: boolean }
   | { type: "SET_DISABLE_SESSION_REUSE"; payload: boolean }
-  | { type: "SET_MODEL_REDIRECTS"; payload: Record<string, string> }
+  | { type: "SET_MODEL_REDIRECTS"; payload: ProviderModelRedirectRule[] }
   | { type: "SET_ALLOWED_MODELS"; payload: string[] }
   | { type: "SET_ALLOWED_CLIENTS"; payload: string[] }
   | { type: "SET_BLOCKED_CLIENTS"; payload: string[] }
diff --git a/src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx b/src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
index c781c4e74..e03e01676 100644
--- a/src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
+++ b/src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
@@ -189,9 +189,7 @@ export function RoutingSection({ subSectionRefs }: RoutingSectionProps) {
             <div className="space-y-2">
               <ModelRedirectEditor
                 value={state.routing.modelRedirects}
-                onChange={(value: Record<string, string>) =>
-                  dispatch({ type: "SET_MODEL_REDIRECTS", payload: value })
-                }
+                onChange={(value) => dispatch({ type: "SET_MODEL_REDIRECTS", payload: value })}
                 disabled={state.ui.isPending}
               />
               {isBatch && batchAnalysis?.routing.modelRedirects.status === "mixed" && (
diff --git a/src/app/[locale]/settings/providers/_components/model-multi-select.tsx b/src/app/[locale]/settings/providers/_components/model-multi-select.tsx
index 41f3e9976..a56b36d13 100644
--- a/src/app/[locale]/settings/providers/_components/model-multi-select.tsx
+++ b/src/app/[locale]/settings/providers/_components/model-multi-select.tsx
@@ -1,5 +1,15 @@
 "use client";
-import { Check, ChevronsUpDown, Cloud, Database, Loader2, Plus, RefreshCw } from "lucide-react";
+import {
+  Check,
+  ChevronsUpDown,
+  Cloud,
+  Database,
+  Loader2,
+  Pencil,
+  Plus,
+  RefreshCw,
+  X,
+} from "lucide-react";
 import { useTranslations } from "next-intl";
 import { useCallback, useEffect, useMemo, useState } from "react";
 import { getAvailableModelsByProviderType } from "@/actions/model-prices";
@@ -89,6 +99,9 @@ export function ModelMultiSelect({
   const [loading, setLoading] = useState(true);
   const [modelSource, setModelSource] = useState<ModelSource>("loading");
   const [customModel, setCustomModel] = useState("");
+  const [editingIndex, setEditingIndex] = useState<number | null>(null);
+  const [editValue, setEditValue] = useState("");
+  const [managementError, setManagementError] = useState<string | null>(null);
 
   const displayedModels = useMemo(() => {
     const seen = new Set<string>();
@@ -110,6 +123,11 @@ export function ModelMultiSelect({
     return merged;
   }, [availableModels, selectedModels]);
 
+  const availableDisplayedModels = useMemo(
+    () => displayedModels.filter((model) => !selectedModels.includes(model)),
+    [displayedModels, selectedModels]
+  );
+
   // 供应商类型到显示名称的映射
   const getProviderTypeLabel = (type: string): string => {
     const typeMap: Record<string, string> = {
@@ -171,6 +189,7 @@ export function ModelMultiSelect({
   }, [loadModels]);
 
   const toggleModel = (model: string) => {
+    setManagementError(null);
     if (selectedModels.includes(model)) {
       onChange(selectedModels.filter((m) => m !== model));
     } else {
@@ -179,11 +198,17 @@ export function ModelMultiSelect({
   };
 
   const selectAll = () => onChange(availableModels);
-  const clearAll = () => onChange([]);
+  const clearAll = () => {
+    setManagementError(null);
+    setEditingIndex(null);
+    setEditValue("");
+    onChange([]);
+  };
 
   const handleAddCustomModel = () => {
     const trimmed = customModel.trim();
     if (!trimmed) return;
+    setManagementError(null);
 
     if (selectedModels.includes(trimmed)) {
       setCustomModel("");
@@ -198,161 +223,321 @@ export function ModelMultiSelect({
   const sourceLabel = isUpstream ? t("sourceUpstream") : t("sourceFallback");
   const sourceDescription = isUpstream ? t("sourceUpstreamDesc") : t("sourceFallbackDesc");
 
+  const handleRemoveSelectedModel = (index: number) => {
+    setManagementError(null);
+    if (editingIndex === index) {
+      setEditingIndex(null);
+      setEditValue("");
+    }
+    onChange(selectedModels.filter((_, currentIndex) => currentIndex !== index));
+  };
+
+  const handleStartEditSelectedModel = (index: number, model: string) => {
+    setEditingIndex(index);
+    setEditValue(model);
+    setManagementError(null);
+  };
+
+  const handleCancelEditSelectedModel = () => {
+    setEditingIndex(null);
+    setEditValue("");
+    setManagementError(null);
+  };
+
+  const handleSaveEditSelectedModel = (index: number) => {
+    const trimmed = editValue.trim();
+    if (!trimmed) {
+      setManagementError(t("selectedEditEmpty"));
+      return;
+    }
+
+    if (selectedModels.some((model, currentIndex) => currentIndex !== index && model === trimmed)) {
+      setManagementError(t("selectedEditExists", { model: trimmed }));
+      return;
+    }
+
+    setManagementError(null);
+    onChange(
+      selectedModels.map((model, currentIndex) => (currentIndex === index ? trimmed : model))
+    );
+    setEditingIndex(null);
+    setEditValue("");
+  };
+
   return (
-    <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>
-          ) : (
-            <div className="flex gap-2 items-center">
-              <span className="truncate">
-                {t("selectedCount", { count: selectedModels.length })}
+    <div className="space-y-3">
+      <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>
+
+          <div className="border-t p-3 space-y-2">
+            <Label className="text-xs font-medium">{t("manualAdd")}</Label>
+            <div className="flex gap-2">
+              <Input
+                placeholder={t("manualPlaceholder")}
+                value={customModel}
+                onChange={(e) => setCustomModel(e.target.value)}
+                onKeyDown={(e) => {
+                  if (e.key === "Enter") {
+                    e.preventDefault();
+                    handleAddCustomModel();
+                  }
+                }}
+                disabled={disabled}
+                className="font-mono text-sm flex-1"
+              />
+              <Button
+                size="sm"
+                onClick={handleAddCustomModel}
+                disabled={disabled || !customModel.trim()}
+                type="button"
+              >
+                <Plus className="h-4 w-4" />
+              </Button>
             </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}
+            <p className="text-xs text-muted-foreground">{t("manualDesc")}</p>
+          </div>
+        </PopoverContent>
+      </Popover>
+
+      {selectedModels.length > 0 && (
+        <div className="rounded-md border border-border/60 bg-muted/20 p-3 space-y-2">
+          <div className="flex items-center justify-between gap-2">
+            <Label className="text-xs font-medium">
+              {t("selectedListLabel", { count: selectedModels.length })}
+            </Label>
+            <span className="text-xs text-muted-foreground">{t("selectedListHint")}</span>
+          </div>
+
+          <div className="space-y-1">
+            {selectedModels.map((model, index) => {
+              const isEditing = editingIndex === index;
+
+              return (
+                <div
+                  key={`${index}:${model}`}
+                  data-model-row={`${index}:${model}`}
+                  className="flex flex-wrap items-center gap-2 rounded-md border border-border/50 bg-background px-3 py-2"
+                >
+                  {isEditing ? (
+                    <>
+                      <Input
+                        value={editValue}
+                        data-model-edit-input={model}
+                        onChange={(e) => setEditValue(e.target.value)}
+                        onInput={(e) => setEditValue((e.target as HTMLInputElement).value)}
+                        onKeyDown={(e) => {
+                          if (e.key === "Enter") {
+                            e.preventDefault();
+                            handleSaveEditSelectedModel(index);
+                          } else if (e.key === "Escape") {
+                            e.preventDefault();
+                            handleCancelEditSelectedModel();
+                          }
+                        }}
+                        className="font-mono text-sm h-8 flex-1"
+                        autoFocus
                       />
-                      <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
+                        type="button"
+                        variant="ghost"
                         size="sm"
-                        variant="outline"
-                        onClick={selectAll}
-                        className="h-7 text-xs"
+                        data-model-edit-save={model}
+                        onClick={() => handleSaveEditSelectedModel(index)}
+                        disabled={disabled}
+                        className="h-8 w-8 p-0"
+                      >
+                        <Check className="h-3.5 w-3.5 text-green-600" />
+                      </Button>
+                      <Button
                         type="button"
+                        variant="ghost"
+                        size="sm"
+                        onClick={handleCancelEditSelectedModel}
+                        disabled={disabled}
+                        className="h-8 w-8 p-0"
                       >
-                        {t("selectAll", { count: availableModels.length })}
+                        <X className="h-3.5 w-3.5 text-muted-foreground" />
                       </Button>
+                    </>
+                  ) : (
+                    <>
+                      <span className="font-mono text-sm flex-1 break-all">{model}</span>
                       <Button
+                        type="button"
+                        variant="ghost"
                         size="sm"
-                        variant="outline"
-                        onClick={clearAll}
-                        disabled={selectedModels.length === 0}
-                        className="h-7 text-xs"
+                        data-model-edit={model}
+                        onClick={() => handleStartEditSelectedModel(index, model)}
+                        disabled={disabled}
+                        className="h-8 w-8 p-0"
+                      >
+                        <Pencil className="h-3.5 w-3.5 text-muted-foreground" />
+                      </Button>
+                      <Button
                         type="button"
+                        variant="ghost"
+                        size="sm"
+                        data-model-remove={model}
+                        onClick={() => handleRemoveSelectedModel(index)}
+                        disabled={disabled}
+                        className="h-8 w-8 p-0"
                       >
-                        {t("clear")}
+                        <X className="h-3.5 w-3.5 text-muted-foreground" />
                       </Button>
-                    </div>
-                  </div>
-                </CommandGroup>
-
-                {/* 模型列表 */}
-                <CommandGroup>
-                  {displayedModels.map((model) => (
-                    <CommandItem
-                      key={model}
-                      value={model}
-                      onSelect={() => toggleModel(model)}
-                      className="cursor-pointer"
-                    >
-                      <Checkbox
-                        checked={selectedModels.includes(model)}
-                        className="mr-2"
-                        onCheckedChange={() => toggleModel(model)}
-                      />
-                      <span className="font-mono text-sm flex-1">{model}</span>
-                      {selectedModels.includes(model) && <Check className="h-4 w-4 text-primary" />}
-                    </CommandItem>
-                  ))}
-                </CommandGroup>
-              </>
-            )}
-          </CommandList>
-        </Command>
-
-        {/* 手动输入区域 */}
-        <div className="border-t p-3 space-y-2">
-          <Label className="text-xs font-medium">{t("manualAdd")}</Label>
-          <div className="flex gap-2">
-            <Input
-              placeholder={t("manualPlaceholder")}
-              value={customModel}
-              onChange={(e) => setCustomModel(e.target.value)}
-              onKeyDown={(e) => {
-                if (e.key === "Enter") {
-                  e.preventDefault();
-                  handleAddCustomModel();
-                }
-              }}
-              disabled={disabled}
-              className="font-mono text-sm flex-1"
-            />
-            <Button
-              size="sm"
-              onClick={handleAddCustomModel}
-              disabled={disabled || !customModel.trim()}
-              type="button"
-            >
-              <Plus className="h-4 w-4" />
-            </Button>
+                    </>
+                  )}
+                </div>
+              );
+            })}
           </div>
-          <p className="text-xs text-muted-foreground">{t("manualDesc")}</p>
+
+          {managementError && <div className="text-xs text-destructive">{managementError}</div>}
         </div>
-      </PopoverContent>
-    </Popover>
+      )}
+    </div>
   );
 }
diff --git a/src/app/[locale]/settings/providers/_components/model-redirect-editor.tsx b/src/app/[locale]/settings/providers/_components/model-redirect-editor.tsx
index 382a22df5..784f0fba7 100644
--- a/src/app/[locale]/settings/providers/_components/model-redirect-editor.tsx
+++ b/src/app/[locale]/settings/providers/_components/model-redirect-editor.tsx
@@ -1,108 +1,214 @@
 "use client";
-import { AlertCircle, ArrowRight, Check, Pencil, Plus, X } from "lucide-react";
+import {
+  AlertCircle,
+  ArrowRight,
+  Check,
+  ChevronDown,
+  ChevronUp,
+  Pencil,
+  Plus,
+  X,
+} from "lucide-react";
 import { useTranslations } from "next-intl";
 import { useState } from "react";
+import safeRegex from "safe-regex";
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import type { ProviderModelRedirectMatchType, ProviderModelRedirectRule } from "@/types/provider";
 
 interface ModelRedirectEditorProps {
-  value: Record<string, string>;
-  onChange: (value: Record<string, string>) => void;
+  value: ProviderModelRedirectRule[];
+  onChange: (value: ProviderModelRedirectRule[]) => void;
   disabled?: boolean;
 }
 
+const DEFAULT_RULE: ProviderModelRedirectRule = {
+  matchType: "exact",
+  source: "",
+  target: "",
+};
+
+function normalizeRule(rule: ProviderModelRedirectRule): ProviderModelRedirectRule {
+  return {
+    matchType: rule.matchType,
+    source: rule.source.trim(),
+    target: rule.target.trim(),
+  };
+}
+
+function getRuleIdentity(rule: Pick<ProviderModelRedirectRule, "matchType" | "source">): string {
+  return `${rule.matchType}:${rule.source.trim().toLowerCase()}`;
+}
+
 export function ModelRedirectEditor({
   value,
   onChange,
   disabled = false,
 }: ModelRedirectEditorProps) {
   const t = useTranslations("settings.providers.form.modelRedirect");
-  const [newSource, setNewSource] = useState("");
-  const [newTarget, setNewTarget] = useState("");
+  const [newRule, setNewRule] = useState<ProviderModelRedirectRule>(DEFAULT_RULE);
   const [error, setError] = useState<string | null>(null);
-  const [editingSource, setEditingSource] = useState<string | null>(null);
-  const [editTarget, setEditTarget] = useState("");
+  const [editingRuleKey, setEditingRuleKey] = useState<string | null>(null);
+  const [editRule, setEditRule] = useState<ProviderModelRedirectRule>(DEFAULT_RULE);
 
-  // 将 Record 转换为数组用于渲染
-  const redirects = Object.entries(value);
+  const redirects = value;
 
-  const handleAdd = () => {
-    setError(null);
+  const matchTypeOptions: Array<{
+    value: ProviderModelRedirectMatchType;
+    label: string;
+  }> = [
+    { value: "exact", label: t("matchTypeExact") },
+    { value: "prefix", label: t("matchTypePrefix") },
+    { value: "suffix", label: t("matchTypeSuffix") },
+    { value: "contains", label: t("matchTypeContains") },
+    { value: "regex", label: t("matchTypeRegex") },
+  ];
 
-    // 验证输入
-    if (!newSource.trim()) {
-      setError(t("sourceEmpty"));
-      return;
+  const hasDuplicateRule = (rule: ProviderModelRedirectRule, ignoreRuleKey?: string): boolean => {
+    const normalized = normalizeRule(rule);
+    const nextRuleKey = getRuleIdentity(normalized);
+
+    return redirects.some((item) => {
+      const currentKey = getRuleIdentity(item);
+      if (ignoreRuleKey && currentKey === ignoreRuleKey) {
+        return false;
+      }
+      return currentKey === nextRuleKey;
+    });
+  };
+
+  const validateRule = (rule: ProviderModelRedirectRule, ignoreRuleKey?: string): string | null => {
+    const normalized = normalizeRule(rule);
+
+    if (!normalized.source) {
+      return t("sourceEmpty");
+    }
+    if (!normalized.target) {
+      return t("targetEmpty");
+    }
+    if (normalized.source.length > 255) {
+      return t("sourceTooLong");
+    }
+    if (normalized.target.length > 255) {
+      return t("targetTooLong");
+    }
+    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");
+      }
+    }
+    if (hasDuplicateRule(normalized, ignoreRuleKey)) {
+      return t("alreadyExists", {
+        model: `${normalized.matchType}:${normalized.source}`,
+      });
     }
-    if (!newTarget.trim()) {
-      setError(t("targetEmpty"));
+    return null;
+  };
+
+  const handleAdd = () => {
+    if (redirects.length >= 100) {
+      setError(t("maxRules"));
       return;
     }
 
-    // 检查是否已存在
-    if (value[newSource.trim()]) {
-      setError(t("alreadyExists", { model: newSource.trim() }));
+    const nextRule = normalizeRule(newRule);
+    const validationError = validateRule(nextRule);
+    if (validationError) {
+      setError(validationError);
       return;
     }
 
-    // 添加新的映射
-    onChange({
-      ...value,
-      [newSource.trim()]: newTarget.trim(),
-    });
+    setError(null);
+    onChange([...redirects, nextRule]);
+    setNewRule(DEFAULT_RULE);
+  };
 
-    // 清空输入
-    setNewSource("");
-    setNewTarget("");
+  const handleRemove = (ruleKey: string) => {
+    onChange(redirects.filter((rule) => getRuleIdentity(rule) !== ruleKey));
+    if (editingRuleKey === ruleKey) {
+      setEditingRuleKey(null);
+      setEditRule(DEFAULT_RULE);
+      setError(null);
+    }
   };
 
-  const handleRemove = (sourceModel: string) => {
-    const newValue = { ...value };
-    delete newValue[sourceModel];
-    onChange(newValue);
+  const handleMove = (ruleKey: string, direction: -1 | 1) => {
+    const index = redirects.findIndex((rule) => getRuleIdentity(rule) === ruleKey);
+    if (index < 0) return;
+
+    const nextIndex = index + direction;
+    if (nextIndex < 0 || nextIndex >= redirects.length) {
+      return;
+    }
+
+    const nextRules = [...redirects];
+    const [item] = nextRules.splice(index, 1);
+    nextRules.splice(nextIndex, 0, item);
+    onChange(nextRules);
   };
 
-  const handleStartEdit = (source: string, target: string) => {
-    setEditingSource(source);
-    setEditTarget(target);
+  const handleStartEdit = (rule: ProviderModelRedirectRule) => {
+    setEditingRuleKey(getRuleIdentity(rule));
+    setEditRule(normalizeRule(rule));
     setError(null);
   };
 
   const handleCancelEdit = () => {
-    setEditingSource(null);
-    setEditTarget("");
+    setEditingRuleKey(null);
+    setEditRule(DEFAULT_RULE);
     setError(null);
   };
 
-  const handleSaveEdit = (originalSource: string) => {
-    setError(null);
-
-    if (!editTarget.trim()) {
-      setError(t("targetEmpty"));
+  const handleSaveEdit = (originalRuleKey: string) => {
+    const nextRule = normalizeRule(editRule);
+    const validationError = validateRule(nextRule, originalRuleKey);
+    if (validationError) {
+      setError(validationError);
       return;
     }
 
-    const newValue = { ...value };
-    newValue[originalSource] = editTarget.trim();
-    onChange(newValue);
+    const currentIndex = redirects.findIndex((rule) => getRuleIdentity(rule) === originalRuleKey);
+    if (currentIndex < 0) {
+      setError(t("ruleMoved"));
+      return;
+    }
 
-    setEditingSource(null);
-    setEditTarget("");
+    setError(null);
+    onChange(redirects.map((rule, index) => (index === currentIndex ? nextRule : rule)));
+    setEditingRuleKey(null);
+    setEditRule(DEFAULT_RULE);
   };
 
-  const handleKeyDown = (e: React.KeyboardEvent) => {
+  const handleCreateKeyDown = (e: React.KeyboardEvent) => {
     if (e.key === "Enter") {
       e.preventDefault();
       handleAdd();
     }
   };
 
-  const handleEditKeyDown = (e: React.KeyboardEvent, originalSource: string) => {
+  const handleEditKeyDown = (e: React.KeyboardEvent, originalRuleKey: string) => {
     if (e.key === "Enter") {
       e.preventDefault();
-      handleSaveEdit(originalSource);
+      handleSaveEdit(originalRuleKey);
     } else if (e.key === "Escape") {
       e.preventDefault();
       handleCancelEdit();
@@ -111,88 +217,189 @@ export function ModelRedirectEditor({
 
   return (
     <div className="space-y-3">
-      {/* 现有的重定向规则列表 */}
       {redirects.length > 0 && (
         <div className="space-y-2">
-          <div className="text-xs font-medium text-muted-foreground">
-            {t("currentRules", { count: redirects.length })}
+          <div className="flex items-center justify-between gap-2">
+            <div className="text-xs font-medium text-muted-foreground">
+              {t("currentRules", { count: redirects.length })}
+            </div>
+            <div className="text-xs text-muted-foreground">{t("orderHint")}</div>
           </div>
+
           <div className="space-y-1">
-            {redirects.map(([source, target]) => {
-              const isEditing = editingSource === source;
+            {redirects.map((rule, index) => {
+              const ruleKey = getRuleIdentity(rule);
+              const isEditing = editingRuleKey === ruleKey;
 
               return (
-                <div
-                  key={source}
-                  className="group flex items-center gap-2 py-2 px-3 rounded-md hover:bg-muted/50 transition-colors"
-                >
-                  <Badge variant="outline" className="font-mono text-xs shrink-0">
-                    {source}
-                  </Badge>
-                  <ArrowRight className="h-3 w-3 text-muted-foreground shrink-0" />
-
+                <div key={ruleKey} className="group rounded-md border border-border/60 px-3 py-2">
                   {isEditing ? (
-                    <Input
-                      value={editTarget}
-                      onChange={(e) => setEditTarget(e.target.value)}
-                      onKeyDown={(e) => handleEditKeyDown(e, source)}
-                      disabled={disabled}
-                      className="font-mono text-sm h-7 flex-1"
-                      autoFocus
-                    />
-                  ) : (
-                    <Badge variant="secondary" className="font-mono text-xs shrink-0">
-                      {target}
-                    </Badge>
-                  )}
+                    <div className="grid gap-2 md:grid-cols-[140px_1fr_24px_1fr_auto] md:items-end">
+                      <div className="space-y-1">
+                        <Label className="text-xs">{t("matchTypeLabel")}</Label>
+                        <Select
+                          value={editRule.matchType}
+                          onValueChange={(matchType) =>
+                            setEditRule((current) => ({
+                              ...current,
+                              matchType: matchType as ProviderModelRedirectMatchType,
+                            }))
+                          }
+                          disabled={disabled}
+                        >
+                          <SelectTrigger className="h-8">
+                            <SelectValue />
+                          </SelectTrigger>
+                          <SelectContent>
+                            {matchTypeOptions.map((option) => (
+                              <SelectItem key={option.value} value={option.value}>
+                                {option.label}
+                              </SelectItem>
+                            ))}
+                          </SelectContent>
+                        </Select>
+                      </div>
 
-                  <div className="flex-1" />
+                      <div className="space-y-1">
+                        <Label className="text-xs">{t("sourceModel")}</Label>
+                        <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
+                        />
+                      </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,
+                            }))
+                          }
+                          onKeyDown={(e) => handleEditKeyDown(e, ruleKey)}
+                          disabled={disabled}
+                          className="font-mono text-sm h-8 flex-1"
+                        />
+                      </div>
+
+                      <div className="flex items-center gap-1 md:pb-0 md:justify-end">
+                        <Button
+                          type="button"
+                          variant="ghost"
+                          size="sm"
+                          data-redirect-save={ruleKey}
+                          onClick={() => handleSaveEdit(ruleKey)}
+                          disabled={disabled}
+                          className="h-8 w-8 p-0"
+                          aria-label={t("saveRule")}
+                          title={t("saveRule")}
+                        >
+                          <Check className="h-3.5 w-3.5 text-green-600" />
+                        </Button>
+                        <Button
+                          type="button"
+                          variant="ghost"
+                          size="sm"
+                          onClick={handleCancelEdit}
+                          disabled={disabled}
+                          className="h-8 w-8 p-0"
+                          aria-label={t("cancelEdit")}
+                          title={t("cancelEdit")}
+                        >
+                          <X className="h-3.5 w-3.5 text-muted-foreground" />
+                        </Button>
+                      </div>
                     </div>
                   ) : (
-                    <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
-                      <Button
-                        type="button"
-                        variant="ghost"
-                        size="sm"
-                        onClick={() => handleStartEdit(source, target)}
-                        disabled={disabled}
-                        className="h-6 w-6 p-0"
-                      >
-                        <Pencil className="h-3 w-3 text-muted-foreground" />
-                      </Button>
-                      <Button
-                        type="button"
-                        variant="ghost"
-                        size="sm"
-                        onClick={() => handleRemove(source)}
-                        disabled={disabled}
-                        className="h-6 w-6 p-0"
-                      >
-                        <X className="h-3 w-3 text-muted-foreground" />
-                      </Button>
+                    <div className="flex flex-wrap items-center gap-2">
+                      <Badge variant="secondary" className="text-xs">
+                        {matchTypeOptions.find((option) => option.value === rule.matchType)?.label}
+                      </Badge>
+                      <Badge variant="outline" className="font-mono text-xs">
+                        {rule.source}
+                      </Badge>
+                      <ArrowRight className="h-3 w-3 text-muted-foreground shrink-0" />
+                      <Badge variant="secondary" className="font-mono text-xs">
+                        {rule.target}
+                      </Badge>
+
+                      <div className="ml-auto flex items-center gap-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity">
+                        <Button
+                          type="button"
+                          variant="ghost"
+                          size="sm"
+                          data-redirect-move-up={ruleKey}
+                          onClick={() => handleMove(ruleKey, -1)}
+                          disabled={disabled || index === 0}
+                          className="h-7 w-7 p-0"
+                          aria-label={t("moveRuleUp")}
+                          title={t("moveRuleUp")}
+                        >
+                          <ChevronUp className="h-3.5 w-3.5 text-muted-foreground" />
+                        </Button>
+                        <Button
+                          type="button"
+                          variant="ghost"
+                          size="sm"
+                          data-redirect-move-down={ruleKey}
+                          onClick={() => handleMove(ruleKey, 1)}
+                          disabled={disabled || index === redirects.length - 1}
+                          className="h-7 w-7 p-0"
+                          aria-label={t("moveRuleDown")}
+                          title={t("moveRuleDown")}
+                        >
+                          <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
+                        </Button>
+                        <Button
+                          type="button"
+                          variant="ghost"
+                          size="sm"
+                          data-redirect-edit={ruleKey}
+                          onClick={() => handleStartEdit(rule)}
+                          disabled={disabled}
+                          className="h-7 w-7 p-0"
+                          aria-label={t("editRule")}
+                          title={t("editRule")}
+                        >
+                          <Pencil className="h-3.5 w-3.5 text-muted-foreground" />
+                        </Button>
+                        <Button
+                          type="button"
+                          variant="ghost"
+                          size="sm"
+                          data-redirect-remove={ruleKey}
+                          onClick={() => handleRemove(ruleKey)}
+                          disabled={disabled}
+                          className="h-7 w-7 p-0"
+                          aria-label={t("deleteRule")}
+                          title={t("deleteRule")}
+                        >
+                          <X className="h-3.5 w-3.5 text-muted-foreground" />
+                        </Button>
+                      </div>
                     </div>
                   )}
                 </div>
@@ -202,26 +409,61 @@ export function ModelRedirectEditor({
         </div>
       )}
 
-      {/* 添加新规则表单 */}
       <div className="space-y-2">
         <div className="text-xs font-medium text-muted-foreground">{t("addNewRule")}</div>
-        <div className="grid grid-cols-[1fr_auto_1fr_auto] gap-2 items-end">
+
+        <div className="grid gap-2 md:grid-cols-[140px_1fr_24px_1fr_auto] md:items-end">
+          <div className="space-y-1">
+            <Label htmlFor="new-match-type" className="text-xs">
+              {t("matchTypeLabel")}
+            </Label>
+            <Select
+              value={newRule.matchType}
+              onValueChange={(matchType) =>
+                setNewRule((current) => ({
+                  ...current,
+                  matchType: matchType as ProviderModelRedirectMatchType,
+                }))
+              }
+              disabled={disabled}
+            >
+              <SelectTrigger id="new-match-type" className="h-9">
+                <SelectValue />
+              </SelectTrigger>
+              <SelectContent>
+                {matchTypeOptions.map((option) => (
+                  <SelectItem key={option.value} value={option.value}>
+                    {option.label}
+                  </SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+          </div>
+
           <div className="space-y-1">
             <Label htmlFor="new-source" className="text-xs">
               {t("sourceModel")}
             </Label>
             <Input
               id="new-source"
-              value={newSource}
-              onChange={(e) => setNewSource(e.target.value)}
-              onKeyDown={handleKeyDown}
+              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"
             />
           </div>
 
-          <div className="text-muted-foreground pb-2">→</div>
+          <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 htmlFor="new-target" className="text-xs">
@@ -229,9 +471,15 @@ export function ModelRedirectEditor({
             </Label>
             <Input
               id="new-target"
-              value={newTarget}
-              onChange={(e) => setNewTarget(e.target.value)}
-              onKeyDown={handleKeyDown}
+              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"
@@ -240,8 +488,9 @@ export function ModelRedirectEditor({
 
           <Button
             type="button"
+            data-redirect-add
             onClick={handleAdd}
-            disabled={disabled || !newSource.trim() || !newTarget.trim()}
+            disabled={disabled || !newRule.source.trim() || !newRule.target.trim()}
             size="default"
             className="mb-0"
           >
@@ -250,19 +499,16 @@ export function ModelRedirectEditor({
           </Button>
         </div>
 
-        {/* 错误提示 */}
         {error && (
-          <div className="flex items-center gap-2 text-xs text-destructive">
+          <div className="flex items-center gap-2 text-xs text-destructive" data-redirect-error>
             <AlertCircle className="h-3 w-3" />
             <span>{error}</span>
           </div>
         )}
 
-        {/* 帮助文本 */}
         <p className="text-xs text-muted-foreground">{t("description")}</p>
       </div>
 
-      {/* 空状态提示 */}
       {redirects.length === 0 && (
         <div className="text-center py-6 text-sm text-muted-foreground border border-dashed rounded-md">
           {t("emptyState")}
diff --git a/src/app/api/actions/[...route]/route.ts b/src/app/api/actions/[...route]/route.ts
index ac20853fd..6d53e2368 100644
--- a/src/app/api/actions/[...route]/route.ts
+++ b/src/app/api/actions/[...route]/route.ts
@@ -35,6 +35,7 @@ import * as userActions from "@/actions/users";
 import * as webhookTargetActions from "@/actions/webhook-targets";
 import { createActionRoute } from "@/lib/api/action-adapter-openapi";
 import { NOTIFICATION_JOB_TYPES } from "@/lib/constants/notification.constants";
+import { PROVIDER_MODEL_REDIRECT_RULE_SCHEMA } from "@/lib/provider-model-redirect-schema";
 // 导入 validation schemas
 import {
   CreateProviderSchema,
@@ -538,7 +539,10 @@ const { route: getProvidersRoute, handler: getProvidersHandler } = createActionR
         weight: z.number().describe("权重"),
         priority: z.number().describe("优先级"),
         costMultiplier: z.number().describe("成本系数"),
-        modelRedirects: z.record(z.string(), z.string()).nullable().describe("模型重定向映射"),
+        modelRedirects: z
+          .array(PROVIDER_MODEL_REDIRECT_RULE_SCHEMA)
+          .nullable()
+          .describe("模型重定向规则列表"),
         proxyUrl: z.string().nullable().describe("代理地址"),
         maxConcurrency: z.number().nullable().describe("最大并发数"),
         rpmLimit: z.number().nullable().describe("RPM 限制"),
diff --git a/src/app/v1/_lib/proxy/model-redirector.ts b/src/app/v1/_lib/proxy/model-redirector.ts
index 27f4b14c3..b2343ed4a 100644
--- a/src/app/v1/_lib/proxy/model-redirector.ts
+++ b/src/app/v1/_lib/proxy/model-redirector.ts
@@ -1,4 +1,9 @@
 import { logger } from "@/lib/logger";
+import {
+  findMatchingProviderModelRedirectRule,
+  getProviderModelRedirectTarget,
+  hasProviderModelRedirectRules,
+} from "@/lib/provider-model-redirects";
 import type { Provider } from "@/types/provider";
 import type { ProxySession } from "./session";
 
@@ -22,7 +27,7 @@ export class ModelRedirector {
     const trueOriginalModel = session.getOriginalModel() || session.request.model;
 
     // 检查是否配置了模型重定向
-    if (!provider.modelRedirects || Object.keys(provider.modelRedirects).length === 0) {
+    if (!hasProviderModelRedirectRules(provider.modelRedirects)) {
       session.clearCurrentModelRedirect();
       // 如果新供应商没有重定向配置，且之前发生过重定向，需要重置
       if (session.isModelRedirected() && trueOriginalModel) {
@@ -42,8 +47,11 @@ export class ModelRedirector {
     }
 
     // 检查是否有该模型的重定向配置
-    const redirectedModel = provider.modelRedirects[originalModel];
-    if (!redirectedModel) {
+    const matchedRule = findMatchingProviderModelRedirectRule(
+      originalModel,
+      provider.modelRedirects
+    );
+    if (!matchedRule) {
       session.clearCurrentModelRedirect();
       // 如果新供应商对此模型没有重定向规则，且之前发生过重定向，需要重置
       if (session.isModelRedirected()) {
@@ -58,10 +66,14 @@ export class ModelRedirector {
       return false;
     }
 
+    const redirectedModel = matchedRule.target;
+
     // 执行重定向
     logger.info("[ModelRedirector] Model redirected", {
       originalModel,
       redirectedModel,
+      matchType: matchedRule.matchType,
+      matchedSource: matchedRule.source,
       providerId: provider.id,
       providerName: provider.name,
       providerType: provider.providerType,
@@ -114,7 +126,12 @@ export class ModelRedirector {
     const redirectInfo = {
       originalModel,
       redirectedModel,
-      billingModel: originalModel, // 始终使用原始模型计费
+      billingModel: originalModel,
+      matchedRule: {
+        matchType: matchedRule.matchType,
+        source: matchedRule.source,
+        target: matchedRule.target,
+      },
     } as const;
 
     // 记录当前 attempt 的 redirect 快照。
@@ -143,7 +160,7 @@ export class ModelRedirector {
       return originalModel;
     }
 
-    return provider.modelRedirects[originalModel] || originalModel;
+    return getProviderModelRedirectTarget(originalModel, provider.modelRedirects);
   }
 
   /**
@@ -154,7 +171,7 @@ export class ModelRedirector {
    * @returns 是否配置了重定向
    */
   static hasRedirect(model: string, provider: Provider): boolean {
-    return !!(provider.modelRedirects && model && provider.modelRedirects[model]);
+    return !!findMatchingProviderModelRedirectRule(model, provider.modelRedirects);
   }
 
   /**
diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts
index a0eecb8cb..91d17caba 100644
--- a/src/drizzle/schema.ts
+++ b/src/drizzle/schema.ts
@@ -16,7 +16,7 @@ import {
 import { relations, sql } from 'drizzle-orm';
 import type { SpecialSetting } from '@/types/special-settings';
 import type { ResponseFixerConfig } from '@/types/system-config';
-import type { ProviderType } from "@/types/provider";
+import type { ProviderModelRedirectRule, ProviderType } from "@/types/provider";
 import type { FilterOperation } from "@/lib/request-filter-types";
 
 // Enums
@@ -195,7 +195,9 @@ export const providers = pgTable('providers', {
   disableSessionReuse: boolean('disable_session_reuse').notNull().default(false),
 
   // 模型重定向：将请求的模型名称重定向到另一个模型
-  modelRedirects: jsonb('model_redirects').$type<Record<string, string>>(),
+  modelRedirects: jsonb('model_redirects').$type<
+    ProviderModelRedirectRule[] | Record<string, string> | null
+  >(),
 
   // 模型列表：双重语义
   // - Anthropic 提供商：白名单（管理员限制可调度的模型，可选）
diff --git a/src/lib/provider-model-redirect-schema.ts b/src/lib/provider-model-redirect-schema.ts
new file mode 100644
index 000000000..b609cd599
--- /dev/null
+++ b/src/lib/provider-model-redirect-schema.ts
@@ -0,0 +1,80 @@
+import safeRegex from "safe-regex";
+import { z } from "zod";
+
+export const PROVIDER_MODEL_REDIRECT_MATCH_TYPE_SCHEMA = z.enum([
+  "exact",
+  "prefix",
+  "suffix",
+  "contains",
+  "regex",
+]);
+
+export const PROVIDER_MODEL_REDIRECT_RULE_SCHEMA = z
+  .object({
+    matchType: PROVIDER_MODEL_REDIRECT_MATCH_TYPE_SCHEMA,
+    source: z
+      .string()
+      .trim()
+      .min(1, "Redirect source cannot be empty")
+      .max(255, "Redirect source is too long"),
+    target: z
+      .string()
+      .trim()
+      .min(1, "Redirect target cannot be empty")
+      .max(255, "Redirect target is too long"),
+  })
+  .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"],
+      });
+    }
+  });
+
+export const PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA = z
+  .array(PROVIDER_MODEL_REDIRECT_RULE_SCHEMA)
+  .max(100, "Redirect rules cannot exceed 100 entries")
+  .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",
+    }
+  );
+
+export const PROVIDER_MODEL_REDIRECT_RULES_SCHEMA =
+  PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA.nullable().optional();
diff --git a/src/lib/provider-model-redirects.ts b/src/lib/provider-model-redirects.ts
new file mode 100644
index 000000000..cb7c1bbd3
--- /dev/null
+++ b/src/lib/provider-model-redirects.ts
@@ -0,0 +1,138 @@
+import type { ProviderModelRedirectMatchType, ProviderModelRedirectRule } from "@/types/provider";
+
+const PROVIDER_MODEL_REDIRECT_MATCH_TYPES = new Set<ProviderModelRedirectMatchType>([
+  "exact",
+  "prefix",
+  "suffix",
+  "contains",
+  "regex",
+]);
+
+function isRecord(value: unknown): value is Record<string, unknown> {
+  return typeof value === "object" && value !== null && !Array.isArray(value);
+}
+
+function trimString(value: unknown): string | null {
+  return typeof value === "string" ? value.trim() : null;
+}
+
+export function isProviderModelRedirectRule(value: unknown): value is ProviderModelRedirectRule {
+  if (!isRecord(value)) {
+    return false;
+  }
+
+  const matchType = value.matchType;
+  const source = trimString(value.source);
+  const target = trimString(value.target);
+
+  return (
+    typeof matchType === "string" &&
+    PROVIDER_MODEL_REDIRECT_MATCH_TYPES.has(matchType as ProviderModelRedirectMatchType) &&
+    !!source &&
+    !!target
+  );
+}
+
+export function isProviderModelRedirectRuleList(
+  value: unknown
+): value is ProviderModelRedirectRule[] {
+  return Array.isArray(value) && value.every((rule) => isProviderModelRedirectRule(rule));
+}
+
+export function normalizeProviderModelRedirectRule(
+  value: ProviderModelRedirectRule
+): ProviderModelRedirectRule {
+  return {
+    matchType: value.matchType,
+    source: value.source.trim(),
+    target: value.target.trim(),
+  };
+}
+
+export function normalizeProviderModelRedirectRules(
+  value: unknown
+): ProviderModelRedirectRule[] | null {
+  if (value == null) {
+    return null;
+  }
+
+  if (isProviderModelRedirectRuleList(value)) {
+    return value.map((rule) => normalizeProviderModelRedirectRule(rule));
+  }
+
+  if (!isRecord(value)) {
+    return null;
+  }
+
+  const normalized = Object.entries(value)
+    .map(([source, target]): ProviderModelRedirectRule | null => {
+      const normalizedSource = source.trim();
+      const normalizedTarget = trimString(target);
+      if (!normalizedSource || !normalizedTarget) {
+        return null;
+      }
+
+      return {
+        matchType: "exact" as const,
+        source: normalizedSource,
+        target: normalizedTarget,
+      };
+    })
+    .filter((rule): rule is ProviderModelRedirectRule => rule !== null);
+
+  return normalized;
+}
+
+export function hasProviderModelRedirectRules(
+  rules: ProviderModelRedirectRule[] | null | undefined
+): boolean {
+  return Array.isArray(rules) && rules.length > 0;
+}
+
+export function matchesProviderModelRedirectRule(
+  model: string,
+  rule: ProviderModelRedirectRule
+): boolean {
+  switch (rule.matchType) {
+    case "exact":
+      return model === rule.source;
+    case "prefix":
+      return model.startsWith(rule.source);
+    case "suffix":
+      return model.endsWith(rule.source);
+    case "contains":
+      return model.includes(rule.source);
+    case "regex":
+      try {
+        return new RegExp(rule.source).test(model);
+      } catch {
+        return false;
+      }
+    default:
+      return false;
+  }
+}
+
+export function findMatchingProviderModelRedirectRule(
+  model: string,
+  rules: ProviderModelRedirectRule[] | null | undefined
+): ProviderModelRedirectRule | null {
+  if (!model || !hasProviderModelRedirectRules(rules)) {
+    return null;
+  }
+
+  for (const rule of rules ?? []) {
+    if (matchesProviderModelRedirectRule(model, rule)) {
+      return rule;
+    }
+  }
+
+  return null;
+}
+
+export function getProviderModelRedirectTarget(
+  model: string,
+  rules: ProviderModelRedirectRule[] | null | undefined
+): string {
+  return findMatchingProviderModelRedirectRule(model, rules)?.target ?? model;
+}
diff --git a/src/lib/provider-patch-contract.ts b/src/lib/provider-patch-contract.ts
index 0bf04181b..d244a2ebe 100644
--- a/src/lib/provider-patch-contract.ts
+++ b/src/lib/provider-patch-contract.ts
@@ -7,6 +7,7 @@ import type {
   ProviderPatchDraftInput,
   ProviderPatchOperation,
 } from "@/types/provider";
+import { PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA } from "./provider-model-redirect-schema";
 
 export const PROVIDER_PATCH_ERROR_CODES = {
   INVALID_PATCH_SHAPE: "INVALID_PATCH_SHAPE",
@@ -131,16 +132,6 @@ const CLEARABLE_FIELDS: Record<ProviderBatchPatchField, boolean> = {
   mcp_passthrough_url: true,
 };
 
-function isStringRecord(value: unknown): value is Record<string, string> {
-  if (!isRecord(value) || Array.isArray(value)) {
-    return false;
-  }
-
-  return Object.entries(value).every(
-    ([key, entry]) => typeof key === "string" && typeof entry === "string"
-  );
-}
-
 function isNumberRecord(value: unknown): value is Record<string, number> {
   if (!isRecord(value) || Array.isArray(value)) {
     return false;
@@ -287,7 +278,7 @@ function isValidSetValue(field: ProviderBatchPatchField, value: unknown): boolea
     case "mcp_passthrough_type":
       return value === "none" || value === "minimax" || value === "glm" || value === "custom";
     case "model_redirects":
-      return isStringRecord(value);
+      return PROVIDER_MODEL_REDIRECT_RULE_LIST_SCHEMA.safeParse(value).success;
     case "allowed_models":
       return Array.isArray(value) && value.every((model) => typeof model === "string");
     case "allowed_clients":
diff --git a/src/lib/validation/schemas.ts b/src/lib/validation/schemas.ts
index 5a256dd8f..bbe608791 100644
--- a/src/lib/validation/schemas.ts
+++ b/src/lib/validation/schemas.ts
@@ -5,6 +5,7 @@ import {
   PROVIDER_TIMEOUT_LIMITS,
 } from "@/lib/constants/provider.constants";
 import { USER_LIMITS } from "@/lib/constants/user.constants";
+import { PROVIDER_MODEL_REDIRECT_RULES_SCHEMA } from "@/lib/provider-model-redirect-schema";
 import { CURRENCY_CONFIG } from "@/lib/utils/currency";
 import { isValidIANATimezone } from "@/lib/utils/timezone";
 
@@ -449,7 +450,7 @@ export const CreateProviderSchema = z
       .default("claude"),
     preserve_client_ip: z.boolean().optional().default(false),
     disable_session_reuse: z.boolean().optional().default(false),
-    model_redirects: z.record(z.string(), z.string()).nullable().optional(),
+    model_redirects: PROVIDER_MODEL_REDIRECT_RULES_SCHEMA,
     // Scheduled active time window (HH:mm format)
     active_time_start: z
       .string()
@@ -687,7 +688,7 @@ export const UpdateProviderSchema = z
       .optional(),
     preserve_client_ip: z.boolean().optional(),
     disable_session_reuse: z.boolean().optional(),
-    model_redirects: z.record(z.string(), z.string()).nullable().optional(),
+    model_redirects: PROVIDER_MODEL_REDIRECT_RULES_SCHEMA,
     active_time_start: z
       .string()
       .regex(/^([01][0-9]|2[0-3]):[0-5][0-9]$/, "active_time_start must be HH:mm format")
diff --git a/src/repository/_shared/transformers.ts b/src/repository/_shared/transformers.ts
index edca6fa78..95749629d 100644
--- a/src/repository/_shared/transformers.ts
+++ b/src/repository/_shared/transformers.ts
@@ -1,4 +1,5 @@
 import { PROVIDER_TIMEOUT_DEFAULTS } from "@/lib/constants/provider.constants";
+import { normalizeProviderModelRedirectRules } from "@/lib/provider-model-redirects";
 import { formatCostForStorage } from "@/lib/utils/currency";
 import type { Key } from "@/types/key";
 import type { MessageRequest } from "@/types/message";
@@ -95,7 +96,7 @@ export function toProvider(dbProvider: any): Provider {
     providerType: dbProvider?.providerType ?? "claude",
     preserveClientIp: dbProvider?.preserveClientIp ?? false,
     disableSessionReuse: dbProvider?.disableSessionReuse ?? false,
-    modelRedirects: dbProvider?.modelRedirects ?? null,
+    modelRedirects: normalizeProviderModelRedirectRules(dbProvider?.modelRedirects),
     activeTimeStart: dbProvider?.activeTimeStart ?? null,
     activeTimeEnd: dbProvider?.activeTimeEnd ?? null,
     mcpPassthroughType: dbProvider?.mcpPassthroughType ?? "none",
diff --git a/src/repository/provider.ts b/src/repository/provider.ts
index d18c9b71d..344de1a3a 100644
--- a/src/repository/provider.ts
+++ b/src/repository/provider.ts
@@ -7,12 +7,14 @@ import { getCachedProviders } from "@/lib/cache/provider-cache";
 import { PROVIDER_TIMEOUT_DEFAULTS } from "@/lib/constants/provider.constants";
 import { resetEndpointCircuit } from "@/lib/endpoint-circuit-breaker";
 import { logger } from "@/lib/logger";
+import { normalizeProviderModelRedirectRules } from "@/lib/provider-model-redirects";
 import { parseProviderGroups } from "@/lib/utils/provider-group";
 import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import type {
   AnthropicAdaptiveThinkingConfig,
   CreateProviderData,
   Provider,
+  ProviderModelRedirectRule,
   UpdateProviderData,
 } from "@/types/provider";
 import { toProvider } from "./_shared/transformers";
@@ -186,7 +188,7 @@ export async function createProvider(providerData: CreateProviderData): Promise<
     providerType: providerData.provider_type,
     preserveClientIp: providerData.preserve_client_ip ?? false,
     disableSessionReuse: providerData.disable_session_reuse ?? false,
-    modelRedirects: providerData.model_redirects,
+    modelRedirects: normalizeProviderModelRedirectRules(providerData.model_redirects),
     allowedModels: providerData.allowed_models,
     allowedClients: providerData.allowed_clients ?? [],
     blockedClients: providerData.blocked_clients ?? [],
@@ -1029,7 +1031,7 @@ export interface BatchProviderUpdates {
   weight?: number;
   costMultiplier?: string;
   groupTag?: string | null;
-  modelRedirects?: Record<string, string> | null;
+  modelRedirects?: ProviderModelRedirectRule[] | null;
   allowedModels?: string[] | null;
   allowedClients?: string[] | null;
   blockedClients?: string[] | null;
diff --git a/src/types/message.ts b/src/types/message.ts
index 382f8cbd6..fe3f3bfe5 100644
--- a/src/types/message.ts
+++ b/src/types/message.ts
@@ -92,6 +92,11 @@ export interface ProviderChainItem {
     originalModel: string; // 用户请求的模型（计费依据）
     redirectedModel: string; // 实际转发的模型
     billingModel: string; // 计费模型（通常等于 originalModel）
+    matchedRule?: {
+      matchType: "exact" | "prefix" | "suffix" | "contains" | "regex";
+      source: string;
+      target: string;
+    };
   };
 
   // 错误信息（记录失败时的上游报错）
diff --git a/src/types/provider.ts b/src/types/provider.ts
index c86974f20..46c092efd 100644
--- a/src/types/provider.ts
+++ b/src/types/provider.ts
@@ -46,6 +46,14 @@ export interface AnthropicAdaptiveThinkingConfig {
   models: string[];
 }
 
+export type ProviderModelRedirectMatchType = "exact" | "prefix" | "suffix" | "contains" | "regex";
+
+export interface ProviderModelRedirectRule {
+  matchType: ProviderModelRedirectMatchType;
+  source: string;
+  target: string;
+}
+
 export type ProviderPatchOperation<T> =
   | { mode: "no_change" }
   | { mode: "set"; value: T }
@@ -117,7 +125,7 @@ export interface ProviderBatchPatchDraft {
   weight?: ProviderPatchDraftInput<number>;
   cost_multiplier?: ProviderPatchDraftInput<number>;
   group_tag?: ProviderPatchDraftInput<string>;
-  model_redirects?: ProviderPatchDraftInput<Record<string, string>>;
+  model_redirects?: ProviderPatchDraftInput<ProviderModelRedirectRule[]>;
   allowed_models?: ProviderPatchDraftInput<string[]>;
   allowed_clients?: ProviderPatchDraftInput<string[]>;
   blocked_clients?: ProviderPatchDraftInput<string[]>;
@@ -171,7 +179,7 @@ export interface ProviderBatchPatch {
   weight: ProviderPatchOperation<number>;
   cost_multiplier: ProviderPatchOperation<number>;
   group_tag: ProviderPatchOperation<string>;
-  model_redirects: ProviderPatchOperation<Record<string, string>>;
+  model_redirects: ProviderPatchOperation<ProviderModelRedirectRule[]>;
   allowed_models: ProviderPatchOperation<string[]>;
   allowed_clients: ProviderPatchOperation<string[]>;
   blocked_clients: ProviderPatchOperation<string[]>;
@@ -225,7 +233,7 @@ export interface ProviderBatchApplyUpdates {
   weight?: number;
   cost_multiplier?: number;
   group_tag?: string | null;
-  model_redirects?: Record<string, string> | null;
+  model_redirects?: ProviderModelRedirectRule[] | null;
   allowed_models?: string[] | null;
   allowed_clients?: string[];
   blocked_clients?: string[];
@@ -305,7 +313,7 @@ export interface Provider {
   preserveClientIp: boolean;
   // 是否跳过当前供应商的 sticky session 复用
   disableSessionReuse: boolean;
-  modelRedirects: Record<string, string> | null;
+  modelRedirects: ProviderModelRedirectRule[] | null;
 
   // Scheduled active time window (HH:mm format, null = always active)
   activeTimeStart: string | null;
@@ -423,7 +431,7 @@ export interface ProviderDisplay {
   preserveClientIp: boolean;
   // 是否跳过当前供应商的 sticky session 复用
   disableSessionReuse: boolean;
-  modelRedirects: Record<string, string> | null;
+  modelRedirects: ProviderModelRedirectRule[] | null;
   // Scheduled active time window
   activeTimeStart: string | null;
   activeTimeEnd: string | null;
@@ -520,7 +528,7 @@ export interface CreateProviderData {
   provider_type?: ProviderType;
   preserve_client_ip?: boolean;
   disable_session_reuse?: boolean;
-  model_redirects?: Record<string, string> | null;
+  model_redirects?: ProviderModelRedirectRule[] | null;
   active_time_start?: string | null;
   active_time_end?: string | null;
   allowed_models?: string[] | null;
@@ -600,7 +608,7 @@ export interface UpdateProviderData {
   provider_type?: ProviderType;
   preserve_client_ip?: boolean;
   disable_session_reuse?: boolean;
-  model_redirects?: Record<string, string> | null;
+  model_redirects?: ProviderModelRedirectRule[] | null;
   active_time_start?: string | null;
   active_time_end?: string | null;
   allowed_models?: string[] | null;
diff --git a/tests/api/providers-actions.test.ts b/tests/api/providers-actions.test.ts
index e34aaf344..78c4dcc81 100644
--- a/tests/api/providers-actions.test.ts
+++ b/tests/api/providers-actions.test.ts
@@ -223,10 +223,18 @@ describe.skip("供应商管理 - 添加供应商 (addProvider)", () => {
       url: "https://api.anthropic.com",
       key: "sk-test-key",
       provider_type: "claude",
-      model_redirects: {
-        "claude-3-opus": "claude-3-sonnet",
-        "gpt-4": "claude-3-opus",
-      },
+      model_redirects: [
+        {
+          matchType: "prefix",
+          source: "claude-3-opus",
+          target: "claude-3-sonnet",
+        },
+        {
+          matchType: "contains",
+          source: "gpt-4",
+          target: "claude-3-opus",
+        },
+      ],
     });
 
     expect(response.ok).toBe(true);
diff --git a/tests/integration/batch-edit-prefill.test.ts b/tests/integration/batch-edit-prefill.test.ts
index 596690797..5b6475110 100644
--- a/tests/integration/batch-edit-prefill.test.ts
+++ b/tests/integration/batch-edit-prefill.test.ts
@@ -11,7 +11,7 @@ describe("批量编辑预填充集成测试", () => {
         priority: 10,
         weight: 5,
         costMultiplier: 1.5,
-        modelRedirects: { "model-a": "model-b" },
+        modelRedirects: [{ matchType: "exact", source: "model-a", target: "model-b" }],
         allowedModels: ["model-1", "model-2"],
         limit5hUsd: 100,
         circuitBreakerFailureThreshold: 5,
@@ -23,7 +23,7 @@ describe("批量编辑预填充集成测试", () => {
         priority: 10,
         weight: 5,
         costMultiplier: 1.5,
-        modelRedirects: { "model-a": "model-b" },
+        modelRedirects: [{ matchType: "exact", source: "model-a", target: "model-b" }],
         allowedModels: ["model-1", "model-2"],
         limit5hUsd: 100,
         circuitBreakerFailureThreshold: 5,
@@ -37,7 +37,9 @@ describe("批量编辑预填充集成测试", () => {
     expect(state.routing.priority).toBe(10);
     expect(state.routing.weight).toBe(5);
     expect(state.routing.costMultiplier).toBe(1.5);
-    expect(state.routing.modelRedirects).toEqual({ "model-a": "model-b" });
+    expect(state.routing.modelRedirects).toEqual([
+      { matchType: "exact", source: "model-a", target: "model-b" },
+    ]);
     expect(state.routing.allowedModels).toEqual(["model-1", "model-2"]);
     expect(state.rateLimit.limit5hUsd).toBe(100);
     expect(state.circuitBreaker.failureThreshold).toBe(5);
@@ -74,7 +76,7 @@ describe("批量编辑预填充集成测试", () => {
     expect(state.routing.priority).toBe(0);
     expect(state.routing.weight).toBe(1);
     expect(state.routing.costMultiplier).toBe(1.0);
-    expect(state.routing.modelRedirects).toEqual({});
+    expect(state.routing.modelRedirects).toEqual([]);
     expect(state.routing.allowedModels).toEqual([]);
     expect(state.rateLimit.limit5hUsd).toBeNull();
     expect(state.circuitBreaker.failureThreshold).toBeUndefined();
diff --git a/tests/unit/actions/providers-batch-field-mapping.test.ts b/tests/unit/actions/providers-batch-field-mapping.test.ts
index 95cac0389..cfd8ba3b8 100644
--- a/tests/unit/actions/providers-batch-field-mapping.test.ts
+++ b/tests/unit/actions/providers-batch-field-mapping.test.ts
@@ -64,7 +64,10 @@ describe("batchUpdateProviders - advanced field mapping", () => {
   });
 
   it("should map model_redirects to repository modelRedirects", async () => {
-    const redirects = { "claude-3-opus": "claude-3.5-sonnet", "gpt-4": "gpt-4o" };
+    const redirects = [
+      { matchType: "exact", source: "claude-3-opus", target: "claude-3.5-sonnet" },
+      { matchType: "exact", source: "gpt-4", target: "gpt-4o" },
+    ];
 
     const { batchUpdateProviders } = await import("@/actions/providers");
     const result = await batchUpdateProviders({
@@ -91,6 +94,19 @@ describe("batchUpdateProviders - advanced field mapping", () => {
     });
   });
 
+  it("should reject unsafe regex model_redirects in direct batchUpdateProviders", async () => {
+    const { batchUpdateProviders } = await import("@/actions/providers");
+    const result = await batchUpdateProviders({
+      providerIds: [5],
+      updates: {
+        model_redirects: [{ matchType: "regex", source: "(a+)+", target: "glm-4.6" }],
+      },
+    });
+
+    expect(result.ok).toBe(false);
+    expect(updateProvidersBatchMock).not.toHaveBeenCalled();
+  });
+
   it("should map allowed_models with values correctly", async () => {
     const { batchUpdateProviders } = await import("@/actions/providers");
     const result = await batchUpdateProviders({
@@ -217,7 +233,7 @@ describe("batchUpdateProviders - advanced field mapping", () => {
         weight: 3,
         cost_multiplier: 0.8,
         group_tag: "mixed-batch",
-        model_redirects: { "old-model": "new-model" },
+        model_redirects: [{ matchType: "exact", source: "old-model", target: "new-model" }],
         allowed_models: ["claude-3-opus"],
         anthropic_thinking_budget_preference: "5000",
         anthropic_adaptive_thinking: adaptiveConfig,
@@ -234,7 +250,7 @@ describe("batchUpdateProviders - advanced field mapping", () => {
       weight: 3,
       costMultiplier: "0.8",
       groupTag: "mixed-batch",
-      modelRedirects: { "old-model": "new-model" },
+      modelRedirects: [{ matchType: "exact", source: "old-model", target: "new-model" }],
       allowedModels: ["claude-3-opus"],
       anthropicThinkingBudgetPreference: "5000",
       anthropicAdaptiveThinking: adaptiveConfig,
diff --git a/tests/unit/actions/providers-patch-contract.test.ts b/tests/unit/actions/providers-patch-contract.test.ts
index 6ea46db3b..f80f14977 100644
--- a/tests/unit/actions/providers-patch-contract.test.ts
+++ b/tests/unit/actions/providers-patch-contract.test.ts
@@ -111,17 +111,45 @@ describe("provider patch contract", () => {
     expect(result.error.field).toBe("weight");
   });
 
-  it("rejects model_redirects arrays", () => {
+  it("accepts model_redirects with redirect rule array", () => {
     const result = normalizeProviderBatchPatchDraft({
       model_redirects: {
-        set: ["not-a-record"],
-      } as never,
+        set: [{ matchType: "prefix", source: "claude-opus", target: "glm-4.6" }],
+      },
+    });
+
+    expect(result.ok).toBe(true);
+    if (!result.ok) return;
+    expect(result.data.model_redirects.mode).toBe("set");
+    if (result.data.model_redirects.mode !== "set") return;
+    expect(result.data.model_redirects.value).toEqual([
+      { matchType: "prefix", source: "claude-opus", target: "glm-4.6" },
+    ]);
+  });
+
+  it("rejects model_redirects with unsafe regex rule", () => {
+    const result = normalizeProviderBatchPatchDraft({
+      model_redirects: {
+        set: [{ matchType: "regex", source: "(a+)+", target: "glm-4.6" }],
+      },
+    });
+
+    expect(result.ok).toBe(false);
+    if (result.ok) return;
+
+    expect(result.error.field).toBe("model_redirects");
+  });
+
+  it("rejects model_redirects with overlong source", () => {
+    const result = normalizeProviderBatchPatchDraft({
+      model_redirects: {
+        set: [{ matchType: "exact", source: "a".repeat(256), target: "glm-4.6" }],
+      },
     });
 
     expect(result.ok).toBe(false);
     if (result.ok) return;
 
-    expect(result.error.code).toBe(PROVIDER_PATCH_ERROR_CODES.INVALID_PATCH_SHAPE);
     expect(result.error.field).toBe("model_redirects");
   });
 
diff --git a/tests/unit/batch-edit/analyze-batch-settings.test.ts b/tests/unit/batch-edit/analyze-batch-settings.test.ts
index 362c296cc..d453df7cf 100644
--- a/tests/unit/batch-edit/analyze-batch-settings.test.ts
+++ b/tests/unit/batch-edit/analyze-batch-settings.test.ts
@@ -46,15 +46,19 @@ describe("analyzeBatchProviderSettings", () => {
 
     it("应该识别所有供应商有相同的对象值", () => {
       const providers: ProviderDisplay[] = [
-        { modelRedirects: { "model-a": "model-b" } } as ProviderDisplay,
-        { modelRedirects: { "model-a": "model-b" } } as ProviderDisplay,
+        {
+          modelRedirects: [{ matchType: "exact", source: "model-a", target: "model-b" }],
+        } as ProviderDisplay,
+        {
+          modelRedirects: [{ matchType: "exact", source: "model-a", target: "model-b" }],
+        } as ProviderDisplay,
       ];
 
       const result = analyzeBatchProviderSettings(providers);
 
       expect(result.routing.modelRedirects).toEqual({
         status: "uniform",
-        value: { "model-a": "model-b" },
+        value: [{ matchType: "exact", source: "model-a", target: "model-b" }],
       });
     });
 
@@ -106,8 +110,12 @@ describe("analyzeBatchProviderSettings", () => {
 
     it("应该识别供应商有不同的对象值", () => {
       const providers: ProviderDisplay[] = [
-        { modelRedirects: { "model-a": "model-b" } } as ProviderDisplay,
-        { modelRedirects: { "model-c": "model-d" } } as ProviderDisplay,
+        {
+          modelRedirects: [{ matchType: "exact", source: "model-a", target: "model-b" }],
+        } as ProviderDisplay,
+        {
+          modelRedirects: [{ matchType: "exact", source: "model-c", target: "model-d" }],
+        } as ProviderDisplay,
       ];
 
       const result = analyzeBatchProviderSettings(providers);
@@ -115,8 +123,8 @@ describe("analyzeBatchProviderSettings", () => {
       expect(result.routing.modelRedirects.status).toBe("mixed");
       if (result.routing.modelRedirects.status === "mixed") {
         expect(result.routing.modelRedirects.values).toEqual([
-          { "model-a": "model-b" },
-          { "model-c": "model-d" },
+          [{ matchType: "exact", source: "model-a", target: "model-b" }],
+          [{ matchType: "exact", source: "model-c", target: "model-d" }],
         ]);
       }
     });
diff --git a/tests/unit/dashboard/provider-form-clone-deep-copy.test.ts b/tests/unit/dashboard/provider-form-clone-deep-copy.test.ts
index d5a0932ba..53faabc00 100644
--- a/tests/unit/dashboard/provider-form-clone-deep-copy.test.ts
+++ b/tests/unit/dashboard/provider-form-clone-deep-copy.test.ts
@@ -17,7 +17,7 @@ function makeProvider(overrides?: Partial<ProviderDisplay>): ProviderDisplay {
     providerType: "claude",
     providerVendorId: null,
     preserveClientIp: false,
-    modelRedirects: { "claude-3": "claude-3.5" },
+    modelRedirects: [{ matchType: "exact", source: "claude-3", target: "claude-3.5" }],
     allowedModels: ["claude-3", "claude-3.5"],
     mcpPassthroughType: "none",
     mcpPassthroughUrl: null,
@@ -66,7 +66,7 @@ function makeProvider(overrides?: Partial<ProviderDisplay>): ProviderDisplay {
 
 describe("createInitialState deep-copy safety", () => {
   describe("clone mode", () => {
-    it("modelRedirects is a distinct object with equal values", () => {
+    it("modelRedirects is a distinct array with equal values", () => {
       const source = makeProvider();
       const state = createInitialState("create", undefined, source);
       expect(state.routing.modelRedirects).toEqual(source.modelRedirects);
@@ -103,10 +103,10 @@ describe("createInitialState deep-copy safety", () => {
       expect(state.routing.anthropicAdaptiveThinking).toBeNull();
     });
 
-    it("null modelRedirects falls back to empty object", () => {
+    it("null modelRedirects falls back to empty array", () => {
       const source = makeProvider({ modelRedirects: null });
       const state = createInitialState("create", undefined, source);
-      expect(state.routing.modelRedirects).toEqual({});
+      expect(state.routing.modelRedirects).toEqual([]);
     });
 
     it("null allowedModels falls back to empty array", () => {
@@ -149,7 +149,7 @@ describe("createInitialState deep-copy safety", () => {
   describe("create mode without clone source", () => {
     it("nested objects use fresh defaults", () => {
       const state = createInitialState("create");
-      expect(state.routing.modelRedirects).toEqual({});
+      expect(state.routing.modelRedirects).toEqual([]);
       expect(state.routing.allowedModels).toEqual([]);
       expect(state.routing.groupPriorities).toEqual({});
       expect(state.routing.anthropicAdaptiveThinking).toBeNull();
diff --git a/tests/unit/lib/provider-model-redirects.test.ts b/tests/unit/lib/provider-model-redirects.test.ts
new file mode 100644
index 000000000..635fbf2a7
--- /dev/null
+++ b/tests/unit/lib/provider-model-redirects.test.ts
@@ -0,0 +1,81 @@
+import { describe, expect, it } from "vitest";
+import type { ProviderModelRedirectRule } from "@/types/provider";
+import {
+  findMatchingProviderModelRedirectRule,
+  normalizeProviderModelRedirectRules,
+} from "@/lib/provider-model-redirects";
+
+describe("provider model redirect rules", () => {
+  it("supports prefix suffix contains and regex matching in rule order", () => {
+    const rules: ProviderModelRedirectRule[] = [
+      {
+        matchType: "contains",
+        source: "opus",
+        target: "contains-opus",
+      },
+      {
+        matchType: "prefix",
+        source: "claude-opus",
+        target: "prefix-opus",
+      },
+      {
+        matchType: "suffix",
+        source: "20251001",
+        target: "suffix-version",
+      },
+      {
+        matchType: "regex",
+        source: "^claude-opus-4-.*$",
+        target: "regex-opus",
+      },
+    ];
+
+    expect(findMatchingProviderModelRedirectRule("claude-opus-4-5-20251001", rules)?.target).toBe(
+      "contains-opus"
+    );
+    expect(findMatchingProviderModelRedirectRule("claude-opus-4-5", rules)?.target).toBe(
+      "contains-opus"
+    );
+    expect(findMatchingProviderModelRedirectRule("foo-20251001", rules)?.target).toBe(
+      "suffix-version"
+    );
+    expect(findMatchingProviderModelRedirectRule("claude-opus-4-6", rules)?.target).toBe(
+      "contains-opus"
+    );
+  });
+
+  it("returns null when no rule matches", () => {
+    const rules: ProviderModelRedirectRule[] = [
+      {
+        matchType: "prefix",
+        source: "claude-opus",
+        target: "glm-4.6",
+      },
+    ];
+
+    expect(findMatchingProviderModelRedirectRule("claude-sonnet-4-5", rules)).toBeNull();
+  });
+
+  it("normalizes legacy exact redirect maps into exact-match rules", () => {
+    const normalized = normalizeProviderModelRedirectRules({
+      "claude-opus-4-5": "glm-4.6",
+      "gpt-4": "gpt-4o",
+    });
+
+    expect(normalized).toHaveLength(2);
+    expect(normalized).toEqual(
+      expect.arrayContaining([
+        {
+          matchType: "exact",
+          source: "claude-opus-4-5",
+          target: "glm-4.6",
+        },
+        {
+          matchType: "exact",
+          source: "gpt-4",
+          target: "gpt-4o",
+        },
+      ])
+    );
+  });
+});
diff --git a/tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts b/tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts
index 991efd120..5ec623343 100644
--- a/tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts
+++ b/tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts
@@ -312,16 +312,12 @@ describe("ProxyForwarder - first-byte hedge scheduling", () => {
     const fireworks = createProvider({
       id: 383,
       name: "fireworks",
-      modelRedirects: {
-        [requestedModel]: fireworksRedirect,
-      },
+      modelRedirects: [{ matchType: "exact", source: requestedModel, target: fireworksRedirect }],
     });
     const minimax = createProvider({
       id: 206,
       name: "Minimax Max",
-      modelRedirects: {
-        [requestedModel]: minimaxRedirect,
-      },
+      modelRedirects: [{ matchType: "exact", source: requestedModel, target: minimaxRedirect }],
     });
 
     const session = createSession();
@@ -385,9 +381,7 @@ describe("ProxyForwarder - first-byte hedge scheduling", () => {
     const fireworks = createProvider({
       id: 383,
       name: "fireworks",
-      modelRedirects: {
-        [requestedModel]: fireworksRedirect,
-      },
+      modelRedirects: [{ matchType: "exact", source: requestedModel, target: fireworksRedirect }],
     });
     const fallback = createProvider({
       id: 206,
@@ -445,9 +439,7 @@ describe("ProxyForwarder - first-byte hedge scheduling", () => {
     const fireworks = createProvider({
       id: 383,
       name: "fireworks",
-      modelRedirects: {
-        [requestedModel]: fireworksRedirect,
-      },
+      modelRedirects: [{ matchType: "exact", source: requestedModel, target: fireworksRedirect }],
     });
     const plainProvider = createProvider({
       id: 520,
@@ -495,17 +487,13 @@ describe("ProxyForwarder - first-byte hedge scheduling", () => {
         id: 383,
         name: "fireworks",
         firstByteTimeoutStreamingMs: 100,
-        modelRedirects: {
-          [requestedModel]: fireworksRedirect,
-        },
+        modelRedirects: [{ matchType: "exact", source: requestedModel, target: fireworksRedirect }],
       });
       const minimax = createProvider({
         id: 206,
         name: "Minimax Max",
         firstByteTimeoutStreamingMs: 100,
-        modelRedirects: {
-          [requestedModel]: minimaxRedirect,
-        },
+        modelRedirects: [{ matchType: "exact", source: requestedModel, target: minimaxRedirect }],
       });
       const session = createSession();
       session.request.model = requestedModel;
@@ -595,17 +583,13 @@ describe("ProxyForwarder - first-byte hedge scheduling", () => {
         id: 383,
         name: "fireworks",
         firstByteTimeoutStreamingMs: 100,
-        modelRedirects: {
-          [requestedModel]: fireworksRedirect,
-        },
+        modelRedirects: [{ matchType: "exact", source: requestedModel, target: fireworksRedirect }],
       });
       const minimax = createProvider({
         id: 206,
         name: "Minimax Max",
         firstByteTimeoutStreamingMs: 100,
-        modelRedirects: {
-          [requestedModel]: minimaxRedirect,
-        },
+        modelRedirects: [{ matchType: "exact", source: requestedModel, target: minimaxRedirect }],
       });
       const session = createSession();
       session.request.model = requestedModel;
@@ -1015,17 +999,21 @@ describe("ProxyForwarder - first-byte hedge scheduling", () => {
         id: 1,
         name: "p1",
         firstByteTimeoutStreamingMs: 100,
-        modelRedirects: {
-          [requestedModel]: "accounts/fireworks/routers/kimi-k2p5-turbo",
-        },
+        modelRedirects: [
+          {
+            matchType: "exact",
+            source: requestedModel,
+            target: "accounts/fireworks/routers/kimi-k2p5-turbo",
+          },
+        ],
       });
       const provider2 = createProvider({
         id: 2,
         name: "p2",
         firstByteTimeoutStreamingMs: 100,
-        modelRedirects: {
-          [requestedModel]: "MiniMax-M2.7-highspeed",
-        },
+        modelRedirects: [
+          { matchType: "exact", source: requestedModel, target: "MiniMax-M2.7-highspeed" },
+        ],
       });
       const clientAbortController = new AbortController();
       const session = createSession(clientAbortController.signal);
diff --git a/tests/unit/settings/providers/build-patch-draft.test.ts b/tests/unit/settings/providers/build-patch-draft.test.ts
index f568662fe..41c0ad42d 100644
--- a/tests/unit/settings/providers/build-patch-draft.test.ts
+++ b/tests/unit/settings/providers/build-patch-draft.test.ts
@@ -14,7 +14,7 @@ function createBatchState(): ProviderFormState {
       groupTag: [],
       preserveClientIp: false,
       disableSessionReuse: false,
-      modelRedirects: {},
+      modelRedirects: [],
       allowedModels: [],
       allowedClients: [],
       blockedClients: [],
@@ -164,7 +164,7 @@ describe("buildPatchDraftFromFormState", () => {
     expect(draft.group_tag).toEqual({ set: "tagA,tagB" });
   });
 
-  it("clears modelRedirects when dirty and empty object", () => {
+  it("clears modelRedirects when dirty and empty list", () => {
     const state = createBatchState();
     const dirty = new Set(["routing.modelRedirects"]);
 
@@ -175,12 +175,14 @@ describe("buildPatchDraftFromFormState", () => {
 
   it("sets modelRedirects when dirty and has entries", () => {
     const state = createBatchState();
-    state.routing.modelRedirects = { "model-a": "model-b" };
+    state.routing.modelRedirects = [{ matchType: "exact", source: "model-a", target: "model-b" }];
     const dirty = new Set(["routing.modelRedirects"]);
 
     const draft = buildPatchDraftFromFormState(state, dirty);
 
-    expect(draft.model_redirects).toEqual({ set: { "model-a": "model-b" } });
+    expect(draft.model_redirects).toEqual({
+      set: [{ matchType: "exact", source: "model-a", target: "model-b" }],
+    });
   });
 
   it("clears allowedModels when dirty and empty array", () => {
diff --git a/tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx b/tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx
index d6aacfedc..9f14a1c48 100644
--- a/tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx
+++ b/tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx
@@ -3,7 +3,7 @@
  */
 
 import type { ReactNode } from "react";
-import { act } from "react";
+import { act, useState } from "react";
 import { createRoot } from "react-dom/client";
 import { NextIntlClientProvider } from "next-intl";
 import { beforeEach, describe, expect, test, vi } from "vitest";
@@ -109,4 +109,114 @@ describe("ModelMultiSelect: 自定义白名单模型应可在列表中取消选
 
     unmount();
   });
+
+  test("无需展开下拉框也应显示完整已选白名单，并支持直接删除与编辑", async () => {
+    const messages = loadMessages();
+
+    function StatefulHarness() {
+      const [selectedModels, setSelectedModels] = useState([
+        "custom-model-x",
+        "claude-opus-4-5-20251001",
+      ]);
+
+      return (
+        <NextIntlClientProvider locale="en" messages={messages} timeZone="UTC">
+          <ModelMultiSelect
+            providerType="claude"
+            selectedModels={selectedModels}
+            onChange={setSelectedModels}
+          />
+        </NextIntlClientProvider>
+      );
+    }
+
+    const { unmount } = render(<StatefulHarness />);
+
+    await flushTicks(5);
+
+    expect(document.body.textContent || "").toContain("custom-model-x");
+    expect(document.body.textContent || "").toContain("claude-opus-4-5-20251001");
+
+    const removeButton = document.querySelector(
+      '[data-model-remove="custom-model-x"]'
+    ) as HTMLButtonElement | null;
+    expect(removeButton).toBeTruthy();
+
+    await act(async () => {
+      removeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+    });
+
+    await flushTicks(2);
+    expect(document.body.textContent || "").not.toContain("custom-model-x");
+    expect(document.body.textContent || "").toContain("claude-opus-4-5-20251001");
+
+    const editButton = document.querySelector(
+      '[data-model-edit="claude-opus-4-5-20251001"]'
+    ) as HTMLButtonElement | null;
+    expect(editButton).toBeTruthy();
+
+    await act(async () => {
+      editButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+    });
+
+    const editInput = document.querySelector(
+      '[data-model-edit-input="claude-opus-4-5-20251001"]'
+    ) as HTMLInputElement | null;
+    expect(editInput).toBeTruthy();
+
+    await act(async () => {
+      if (editInput) {
+        editInput.value = "claude-opus-4-6-latest";
+        editInput.dispatchEvent(new Event("input", { bubbles: true }));
+        editInput.dispatchEvent(new Event("change", { bubbles: true }));
+      }
+    });
+
+    const saveButton = document.querySelector(
+      '[data-model-edit-save="claude-opus-4-5-20251001"]'
+    ) as HTMLButtonElement | null;
+    expect(saveButton).toBeTruthy();
+
+    await act(async () => {
+      saveButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+    });
+
+    await flushTicks(2);
+    expect(document.body.textContent || "").toContain("claude-opus-4-6-latest");
+    expect(document.body.textContent || "").not.toContain("claude-opus-4-5-20251001");
+
+    unmount();
+  });
+
+  test("下拉框应把已选模型单独置顶显示", async () => {
+    const messages = loadMessages();
+
+    const { unmount } = render(
+      <NextIntlClientProvider locale="en" messages={messages} timeZone="UTC">
+        <ModelMultiSelect
+          providerType="claude"
+          selectedModels={["custom-model-x"]}
+          onChange={vi.fn()}
+        />
+      </NextIntlClientProvider>
+    );
+
+    await flushTicks(5);
+
+    const trigger = document.querySelector("button[role='combobox']") as HTMLButtonElement | null;
+    expect(trigger).toBeTruthy();
+
+    await act(async () => {
+      trigger?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+      trigger?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+    });
+
+    await flushTicks(5);
+
+    expect(document.body.textContent || "").toContain("Selected Models");
+    expect(document.querySelector('[data-model-group="selected"]')).toBeTruthy();
+    expect(document.querySelector('[data-model-group="available"]')).toBeTruthy();
+
+    unmount();
+  });
 });
diff --git a/tests/unit/settings/providers/model-redirect-editor.test.tsx b/tests/unit/settings/providers/model-redirect-editor.test.tsx
new file mode 100644
index 000000000..c0a25d5fd
--- /dev/null
+++ b/tests/unit/settings/providers/model-redirect-editor.test.tsx
@@ -0,0 +1,171 @@
+/**
+ * @vitest-environment happy-dom
+ */
+
+import type { ReactNode } from "react";
+import { act, useState } from "react";
+import { createRoot } from "react-dom/client";
+import { NextIntlClientProvider } from "next-intl";
+import { beforeEach, describe, expect, test, vi } from "vitest";
+import { ModelRedirectEditor } from "@/app/[locale]/settings/providers/_components/model-redirect-editor";
+import type { ProviderModelRedirectRule } from "@/types/provider";
+import commonMessages from "../../../../messages/en/common.json";
+import errorsMessages from "../../../../messages/en/errors.json";
+import formsMessages from "../../../../messages/en/forms.json";
+import settingsMessages from "../../../../messages/en/settings";
+import uiMessages from "../../../../messages/en/ui.json";
+
+function loadMessages() {
+  return {
+    common: commonMessages,
+    errors: errorsMessages,
+    ui: uiMessages,
+    forms: formsMessages,
+    settings: settingsMessages,
+  };
+}
+
+function render(node: ReactNode) {
+  const container = document.createElement("div");
+  document.body.appendChild(container);
+  const root = createRoot(container);
+
+  act(() => {
+    root.render(node);
+  });
+
+  return {
+    unmount: () => {
+      act(() => root.unmount());
+      container.remove();
+    },
+  };
+}
+
+async function flushTicks(times = 3) {
+  for (let i = 0; i < times; i++) {
+    await act(async () => {
+      await new Promise((r) => setTimeout(r, 0));
+    });
+  }
+}
+
+describe("ModelRedirectEditor", () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  test("编辑中的规则在前一条被删除后仍应保存到原规则，而不是错位到其他行", async () => {
+    const messages = loadMessages();
+
+    const initialRules: ProviderModelRedirectRule[] = [
+      { matchType: "exact", source: "model-a", target: "target-a" },
+      { matchType: "prefix", source: "model-b", target: "target-b" },
+    ];
+
+    function StatefulHarness() {
+      const [rules, setRules] = useState(initialRules);
+
+      return (
+        <NextIntlClientProvider locale="en" messages={messages} timeZone="UTC">
+          <ModelRedirectEditor value={rules} onChange={setRules} />
+        </NextIntlClientProvider>
+      );
+    }
+
+    const { unmount } = render(<StatefulHarness />);
+
+    await flushTicks(3);
+
+    const editButton = document.querySelector(
+      '[data-redirect-edit="prefix:model-b"]'
+    ) as HTMLButtonElement | null;
+    expect(editButton).toBeTruthy();
+
+    await act(async () => {
+      editButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+    });
+
+    const editSourceInput = document.querySelector(
+      '[data-redirect-edit-source="prefix:model-b"]'
+    ) as HTMLInputElement | null;
+    expect(editSourceInput).toBeTruthy();
+
+    await act(async () => {
+      if (editSourceInput) {
+        editSourceInput.value = "model-b-updated";
+        editSourceInput.dispatchEvent(new Event("input", { bubbles: true }));
+        editSourceInput.dispatchEvent(new Event("change", { bubbles: true }));
+      }
+    });
+
+    const removeFirstButton = document.querySelector(
+      '[data-redirect-remove="exact:model-a"]'
+    ) as HTMLButtonElement | null;
+    expect(removeFirstButton).toBeTruthy();
+
+    await act(async () => {
+      removeFirstButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+    });
+
+    const saveButton = document.querySelector(
+      '[data-redirect-save="prefix:model-b"]'
+    ) as HTMLButtonElement | null;
+    expect(saveButton).toBeTruthy();
+
+    await act(async () => {
+      saveButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+    });
+
+    await flushTicks(2);
+
+    expect(document.body.textContent || "").toContain("model-b-updated");
+    expect(document.body.textContent || "").not.toContain("target-a");
+
+    unmount();
+  });
+
+  test("新增规则时应在本地拦截超长 source，避免直到提交 provider 才失败", async () => {
+    const messages = loadMessages();
+    const onChange = vi.fn();
+
+    const { unmount } = render(
+      <NextIntlClientProvider locale="en" messages={messages} timeZone="UTC">
+        <ModelRedirectEditor value={[]} onChange={onChange} />
+      </NextIntlClientProvider>
+    );
+
+    const sourceInput = document.querySelector("#new-source") as HTMLInputElement | null;
+    const targetInput = document.querySelector("#new-target") as HTMLInputElement | null;
+    expect(sourceInput).toBeTruthy();
+    expect(targetInput).toBeTruthy();
+
+    await act(async () => {
+      if (sourceInput) {
+        sourceInput.value = "a".repeat(256);
+        sourceInput.dispatchEvent(new Event("input", { bubbles: true }));
+        sourceInput.dispatchEvent(new Event("change", { bubbles: true }));
+      }
+
+      if (targetInput) {
+        targetInput.value = "glm-4.6";
+        targetInput.dispatchEvent(new Event("input", { bubbles: true }));
+        targetInput.dispatchEvent(new Event("change", { bubbles: true }));
+      }
+    });
+
+    const addButton = document.querySelector("[data-redirect-add]") as HTMLButtonElement | null;
+    expect(addButton).toBeTruthy();
+
+    await act(async () => {
+      addButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+    });
+
+    expect(onChange).not.toHaveBeenCalled();
+    expect(document.querySelector("[data-redirect-error]")?.textContent || "").toContain(
+      "Source model name is too long"
+    );
+
+    unmount();
+  });
+});
diff --git a/tests/unit/settings/providers/provider-form-batch-context.test.ts b/tests/unit/settings/providers/provider-form-batch-context.test.ts
index de0bd281d..cc0599fb8 100644
--- a/tests/unit/settings/providers/provider-form-batch-context.test.ts
+++ b/tests/unit/settings/providers/provider-form-batch-context.test.ts
@@ -23,7 +23,7 @@ describe("createInitialState - batch mode", () => {
     expect(state.routing.costMultiplier).toBe(1.0);
     expect(state.routing.groupTag).toEqual([]);
     expect(state.routing.preserveClientIp).toBe(false);
-    expect(state.routing.modelRedirects).toEqual({});
+    expect(state.routing.modelRedirects).toEqual([]);
     expect(state.routing.allowedModels).toEqual([]);
     expect(state.routing.cacheTtlPreference).toBe("inherit");
     expect(state.routing.swapCacheTtlBilling).toBe(false);
