Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions messages/en/settings/providers/form/modelRedirect.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
8 changes: 7 additions & 1 deletion messages/en/settings/providers/form/modelSelect.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
21 changes: 20 additions & 1 deletion messages/ja/settings/providers/form/modelRedirect.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 のリスクがあります"
}
8 changes: 7 additions & 1 deletion messages/ja/settings/providers/form/modelSelect.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "利用可能なモデル"
}
21 changes: 20 additions & 1 deletion messages/ru/settings/providers/form/modelRedirect.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
8 changes: 7 additions & 1 deletion messages/ru/settings/providers/form/modelSelect.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "Доступные модели"
}
19 changes: 19 additions & 0 deletions messages/zh-CN/settings/providers/form/modelRedirect.json
Original file line number Diff line number Diff line change
@@ -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": "暂无重定向规则。添加规则后,系统将自动重写请求中的模型名称。"
}
8 changes: 7 additions & 1 deletion messages/zh-CN/settings/providers/form/modelSelect.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,11 @@
"sourceUpstreamDesc": "模型列表来自上游服务商 API",
"sourceFallback": "本地",
"sourceFallbackDesc": "使用本地价格表中的模型列表(上游获取失败或不支持)",
"refresh": "刷新模型列表"
"refresh": "刷新模型列表",
"selectedEditEmpty": "模型名称不能为空",
"selectedEditExists": "模型 \"{model}\" 已在白名单中",
"selectedGroupLabel": "已选模型",
"selectedListHint": "无需展开下拉框即可直接编辑或删除",
"selectedListLabel": "已选白名单 ({count})",
"availableGroupLabel": "可选模型"
}
21 changes: 20 additions & 1 deletion messages/zh-TW/settings/providers/form/modelRedirect.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 風險"
}
8 changes: 7 additions & 1 deletion messages/zh-TW/settings/providers/form/modelSelect.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "可選模型"
}
23 changes: 19 additions & 4 deletions src/actions/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -85,6 +87,7 @@ import type {
ProviderBatchPatch,
ProviderBatchPatchField,
ProviderDisplay,
ProviderModelRedirectRule,
ProviderPatchOperation,
ProviderStatisticsMap,
ProviderType,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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[];
Expand Down Expand Up @@ -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: "模型重定向规则格式无效" };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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

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

}
repositoryUpdates.modelRedirects = normalizeProviderModelRedirectRules(
parsedRedirectRules.data
);
}
}
if (updates.allowed_models !== undefined) {
repositoryUpdates.allowedModels =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
GeminiGoogleSearchPreference,
McpPassthroughType,
ProviderDisplay,
ProviderModelRedirectRule,
} from "@/types/provider";
import { deepEquals } from "./deep-equals";

Expand All @@ -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[]>;
Expand Down Expand Up @@ -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 ?? []),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
Loading
Loading