mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b3c4189f1 | ||
|
|
db5fb0d125 | ||
|
|
9515d88e3c | ||
|
|
2bf721974b | ||
|
|
0c53dcfa80 | ||
|
|
034c086e31 |
@@ -55,7 +55,10 @@ export const buildOpenAIChatCompletionsEndpoint = (baseUrl: string): string => {
|
||||
if (trimmed.endsWith('/chat/completions')) {
|
||||
return trimmed;
|
||||
}
|
||||
return trimmed.endsWith('/v1') ? `${trimmed}/chat/completions` : `${trimmed}/v1/chat/completions`;
|
||||
if (trimmed.endsWith('/v1')) {
|
||||
return `${trimmed.slice(0, -3)}/chat/completions`;
|
||||
}
|
||||
return `${trimmed}/chat/completions`;
|
||||
};
|
||||
|
||||
// 根据 source (apiKey) 获取统计数据 - 与旧版逻辑一致
|
||||
|
||||
@@ -6,6 +6,8 @@ import styles from '@/pages/UsagePage.module.scss';
|
||||
export interface ModelStat {
|
||||
model: string;
|
||||
requests: number;
|
||||
successCount: number;
|
||||
failureCount: number;
|
||||
tokens: number;
|
||||
cost: number;
|
||||
}
|
||||
@@ -38,7 +40,15 @@ export function ModelStatsCard({ modelStats, loading, hasPrices }: ModelStatsCar
|
||||
{modelStats.map((stat) => (
|
||||
<tr key={stat.model}>
|
||||
<td className={styles.modelCell}>{stat.model}</td>
|
||||
<td>{stat.requests.toLocaleString()}</td>
|
||||
<td>
|
||||
<span className={styles.requestCountCell}>
|
||||
<span>{stat.requests.toLocaleString()}</span>
|
||||
<span className={styles.requestBreakdown}>
|
||||
(<span className={styles.statSuccess}>{stat.successCount.toLocaleString()}</span>{' '}
|
||||
<span className={styles.statFailure}>{stat.failureCount.toLocaleString()}</span>)
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>{formatTokensInMillions(stat.tokens)}</td>
|
||||
{hasPrices && <td>{stat.cost > 0 ? formatUsd(stat.cost) : '--'}</td>}
|
||||
</tr>
|
||||
|
||||
@@ -249,10 +249,10 @@
|
||||
"vertex_edit_modal_url_label": "Base URL (Required):",
|
||||
"vertex_edit_modal_proxy_label": "Proxy URL (Optional):",
|
||||
"vertex_delete_confirm": "Are you sure you want to delete this Vertex configuration?",
|
||||
"vertex_models_label": "Model mappings (alias required):",
|
||||
"vertex_models_label": "Model aliases (alias required):",
|
||||
"vertex_models_add_btn": "Add Mapping",
|
||||
"vertex_models_hint": "Each mapping needs both the original model and its alias.",
|
||||
"vertex_models_count": "Mapping count",
|
||||
"vertex_models_hint": "Each alias needs both the original model and the alias.",
|
||||
"vertex_models_count": "Alias count",
|
||||
"ampcode_title": "Amp CLI Integration (ampcode)",
|
||||
"ampcode_modal_title": "Configure Ampcode",
|
||||
"ampcode_upstream_url_label": "Upstream URL",
|
||||
@@ -316,7 +316,7 @@
|
||||
"openai_keys_count": "Keys Count",
|
||||
"openai_models_count": "Models Count",
|
||||
"openai_test_title": "Connection Test",
|
||||
"openai_test_hint": "Send a /v1/chat/completions request with the current settings to verify availability.",
|
||||
"openai_test_hint": "Send a /chat/completions request with the current settings to verify availability.",
|
||||
"openai_test_model_placeholder": "Model to test",
|
||||
"openai_test_action": "Run Test",
|
||||
"openai_test_running": "Sending test request...",
|
||||
@@ -488,8 +488,10 @@
|
||||
"provider_placeholder": "e.g. gemini-cli",
|
||||
"provider_hint": "Defaults to the current filter; pick an existing provider or type a new name.",
|
||||
"models_label": "Models to exclude",
|
||||
"models_placeholder": "gpt-4.1-mini\n*-preview",
|
||||
"models_hint": "Separate by commas or new lines; saving an empty list removes that provider. * wildcards are supported.",
|
||||
"models_loading": "Loading models...",
|
||||
"models_unsupported": "Current CPA version does not support fetching model lists.",
|
||||
"models_loaded": "{{count}} models loaded. Check the models to exclude.",
|
||||
"no_models_available": "No models available for this provider.",
|
||||
"save": "Save/Update",
|
||||
"saving": "Saving...",
|
||||
"save_success": "Excluded models updated",
|
||||
@@ -512,39 +514,35 @@
|
||||
"upgrade_required_title": "Please upgrade CLI Proxy API",
|
||||
"upgrade_required_desc": "The current server does not support the OAuth excluded models API. Please upgrade to the latest CLI Proxy API (CPA) version."
|
||||
},
|
||||
"oauth_model_mappings": {
|
||||
"title": "OAuth Model Mappings",
|
||||
"add": "Add Mapping",
|
||||
"add_title": "Add provider model mappings",
|
||||
"oauth_model_alias": {
|
||||
"title": "OAuth Model Aliases",
|
||||
"add": "Add Alias",
|
||||
"add_title": "Add provider model aliases",
|
||||
"provider_label": "Provider",
|
||||
"provider_placeholder": "e.g. gemini-cli / vertex",
|
||||
"provider_hint": "Defaults to the current filter; pick an existing provider or type a new name.",
|
||||
"model_source_label": "Auth file model source",
|
||||
"model_source_placeholder": "Select an auth file (for model suggestions)",
|
||||
"model_source_hint": "Pick an auth file to enable model suggestions for “Source model name”. You can still type custom values.",
|
||||
"model_source_loading": "Loading models...",
|
||||
"model_source_unsupported": "The current CPA version does not support fetching model lists (manual input still works).",
|
||||
"model_source_loaded": "{{count}} models loaded. Use the dropdown in “Source model name”, or type custom values.",
|
||||
"mappings_label": "Model mappings",
|
||||
"mapping_name_placeholder": "Source model name",
|
||||
"mapping_alias_placeholder": "Alias (required)",
|
||||
"mapping_fork_label": "Keep original",
|
||||
"mappings_hint": "Saving an empty list removes that provider. Enable “Keep original” to keep the original name while adding the alias.",
|
||||
"add_mapping": "Add mapping",
|
||||
"model_source_loaded": "{{count}} models loaded. Use the dropdown in 'Source model name', or type custom values. Saving an empty list removes that provider. Enable 'Keep original' to keep the original name while adding the alias.",
|
||||
"alias_label": "Model aliases",
|
||||
"alias_name_placeholder": "Source model name",
|
||||
"alias_placeholder": "Alias (required)",
|
||||
"alias_fork_label": "Keep original",
|
||||
"add_alias": "Add alias",
|
||||
"save": "Save/Update",
|
||||
"save_success": "Model mappings updated",
|
||||
"save_failed": "Failed to update model mappings",
|
||||
"save_success": "Model aliases updated",
|
||||
"save_failed": "Failed to update model aliases",
|
||||
"delete": "Delete Provider",
|
||||
"delete_confirm": "Delete model mappings for {{provider}}?",
|
||||
"delete_success": "Model mappings removed",
|
||||
"delete_failed": "Failed to delete model mappings",
|
||||
"no_models": "No model mappings",
|
||||
"model_count": "{{count}} mappings",
|
||||
"list_empty_all": "No model mappings yet—use “Add Mapping” to create one.",
|
||||
"delete_confirm": "Delete model aliases for {{provider}}?",
|
||||
"delete_success": "Model aliases removed",
|
||||
"delete_failed": "Failed to delete model aliases",
|
||||
"no_models": "No model aliases",
|
||||
"model_count": "{{count}} aliases",
|
||||
"list_empty_all": "No model aliases yet—use “Add Alias” to create one.",
|
||||
"provider_required": "Please enter a provider first",
|
||||
"upgrade_required": "This feature requires a newer CLI Proxy API (CPA) version. Please upgrade.",
|
||||
"upgrade_required_title": "Please upgrade CLI Proxy API",
|
||||
"upgrade_required_desc": "The current server does not support the OAuth model mappings API. Please upgrade to the latest CLI Proxy API (CPA) version."
|
||||
"upgrade_required_desc": "The current server does not support the OAuth model aliases API. Please upgrade to the latest CLI Proxy API (CPA) version."
|
||||
},
|
||||
"auth_login": {
|
||||
"codex_oauth_title": "Codex OAuth",
|
||||
|
||||
@@ -249,10 +249,10 @@
|
||||
"vertex_edit_modal_url_label": "Base URL (必填):",
|
||||
"vertex_edit_modal_proxy_label": "代理 URL (可选):",
|
||||
"vertex_delete_confirm": "确定要删除这个Vertex配置吗?",
|
||||
"vertex_models_label": "模型映射 (别名必填):",
|
||||
"vertex_models_label": "模型别名 (别名必填):",
|
||||
"vertex_models_add_btn": "添加映射",
|
||||
"vertex_models_hint": "每条映射需要填写原模型与别名。",
|
||||
"vertex_models_count": "映射数量",
|
||||
"vertex_models_hint": "每条别名需要填写原模型与别名。",
|
||||
"vertex_models_count": "别名数量",
|
||||
"ampcode_title": "Amp CLI 集成 (ampcode)",
|
||||
"ampcode_modal_title": "配置 Ampcode",
|
||||
"ampcode_upstream_url_label": "Upstream URL",
|
||||
@@ -316,7 +316,7 @@
|
||||
"openai_keys_count": "密钥数量",
|
||||
"openai_models_count": "模型数量",
|
||||
"openai_test_title": "连通性测试",
|
||||
"openai_test_hint": "使用当前配置向 /v1/chat/completions 请求,验证是否可用。",
|
||||
"openai_test_hint": "使用当前配置向 /chat/completions 请求,验证是否可用。",
|
||||
"openai_test_model_placeholder": "选择或输入要测试的模型",
|
||||
"openai_test_action": "发送测试",
|
||||
"openai_test_running": "正在发送测试请求...",
|
||||
@@ -488,8 +488,10 @@
|
||||
"provider_placeholder": "例如 gemini-cli / openai",
|
||||
"provider_hint": "默认选中当前筛选的提供商,也可直接输入或选择其他名称。",
|
||||
"models_label": "排除的模型",
|
||||
"models_placeholder": "gpt-4.1-mini\n*-preview",
|
||||
"models_hint": "逗号或换行分隔;留空保存将删除该提供商记录;支持 * 通配符。",
|
||||
"models_loading": "正在加载模型列表...",
|
||||
"models_unsupported": "当前 CPA 版本不支持获取模型列表。",
|
||||
"models_loaded": "已加载 {{count}} 个模型,勾选要排除的模型。",
|
||||
"no_models_available": "该提供商暂无可用模型列表。",
|
||||
"save": "保存/更新",
|
||||
"saving": "正在保存...",
|
||||
"save_success": "排除列表已更新",
|
||||
@@ -512,39 +514,35 @@
|
||||
"upgrade_required_title": "需要升级 CPA 版本",
|
||||
"upgrade_required_desc": "当前服务器版本不支持获取模型排除列表功能,请升级到最新版本的 CPA(CLI Proxy API)后重试。"
|
||||
},
|
||||
"oauth_model_mappings": {
|
||||
"title": "OAuth 模型映射",
|
||||
"add": "新增映射",
|
||||
"add_title": "新增提供商模型映射",
|
||||
"oauth_model_alias": {
|
||||
"title": "OAuth 模型别名",
|
||||
"add": "新增别名",
|
||||
"add_title": "新增提供商模型别名",
|
||||
"provider_label": "提供商",
|
||||
"provider_placeholder": "例如 gemini-cli / vertex",
|
||||
"provider_hint": "默认选中当前筛选的提供商,也可直接输入或选择其他名称。",
|
||||
"model_source_label": "模型来源认证文件",
|
||||
"model_source_placeholder": "选择认证文件(用于原模型下拉建议)",
|
||||
"model_source_hint": "选择一个认证文件后,“原模型名称”支持下拉选择;也可手动输入自定义模型。",
|
||||
"model_source_loading": "正在加载模型列表...",
|
||||
"model_source_unsupported": "当前 CPA 版本不支持获取模型列表(仍可手动输入)。",
|
||||
"model_source_loaded": "已加载 {{count}} 个模型,可在“原模型名称”中下拉选择;也可手动输入。",
|
||||
"mappings_label": "模型映射",
|
||||
"mapping_name_placeholder": "原模型名称",
|
||||
"mapping_alias_placeholder": "别名 (必填)",
|
||||
"mapping_fork_label": "保留原名",
|
||||
"mappings_hint": "留空保存将删除该提供商记录;开启“保留原名”会在保留原模型名的同时新增别名。",
|
||||
"add_mapping": "添加映射",
|
||||
"model_source_loaded": "已加载 {{count}} 个模型,可在“原模型名称”中下拉选择;也可手动输入。留空保存将删除该提供商记录;开启“保留原名”会在保留原模型名的同时新增别名。",
|
||||
"alias_label": "模型别名",
|
||||
"alias_name_placeholder": "原模型名称",
|
||||
"alias_placeholder": "别名 (必填)",
|
||||
"alias_fork_label": "保留原名",
|
||||
"add_alias": "添加别名",
|
||||
"save": "保存/更新",
|
||||
"save_success": "模型映射已更新",
|
||||
"save_failed": "更新模型映射失败",
|
||||
"save_success": "模型别名已更新",
|
||||
"save_failed": "更新模型别名失败",
|
||||
"delete": "删除提供商",
|
||||
"delete_confirm": "确定要删除 {{provider}} 的模型映射吗?",
|
||||
"delete_success": "已删除该提供商的模型映射",
|
||||
"delete_failed": "删除模型映射失败",
|
||||
"no_models": "未配置模型映射",
|
||||
"model_count": "映射 {{count}} 条模型",
|
||||
"list_empty_all": "暂无任何提供商的模型映射,点击“新增映射”创建。",
|
||||
"delete_confirm": "确定要删除 {{provider}} 的模型别名吗?",
|
||||
"delete_success": "已删除该提供商的模型别名",
|
||||
"delete_failed": "删除模型别名失败",
|
||||
"no_models": "未配置模型别名",
|
||||
"model_count": "{{count}} 条别名",
|
||||
"list_empty_all": "暂无任何提供商的模型别名,点击“新增别名”创建。",
|
||||
"provider_required": "请先填写提供商名称",
|
||||
"upgrade_required": "当前 CPA 版本不支持模型映射功能,请升级 CPA 版本",
|
||||
"upgrade_required": "当前 CPA 版本不支持模型别名功能,请升级 CPA 版本",
|
||||
"upgrade_required_title": "需要升级 CPA 版本",
|
||||
"upgrade_required_desc": "当前服务器版本不支持 OAuth 模型映射功能,请升级到最新版本的 CPA(CLI Proxy API)后重试。"
|
||||
"upgrade_required_desc": "当前服务器版本不支持 OAuth 模型别名功能,请升级到最新版本的 CPA(CLI Proxy API)后重试。"
|
||||
},
|
||||
"auth_login": {
|
||||
"codex_oauth_title": "Codex OAuth",
|
||||
|
||||
@@ -995,3 +995,53 @@
|
||||
border: 1px solid var(--danger-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 排除模型勾选列表
|
||||
.excludedCheckList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-xs;
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-sm;
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.excludedCheckItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
border-radius: $radius-sm;
|
||||
cursor: pointer;
|
||||
transition: background-color $transition-fast;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-hover);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.excludedCheckLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
font-size: 13px;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
color: var(--text-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.excludedCheckDisplayName {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import { useAuthStore, useNotificationStore, useThemeStore } from '@/stores';
|
||||
import { authFilesApi, usageApi } from '@/services/api';
|
||||
import { apiClient } from '@/services/api/client';
|
||||
import type { AuthFileItem, OAuthModelMappingEntry } from '@/types';
|
||||
import type { AuthFileItem, OAuthModelAliasEntry } from '@/types';
|
||||
import {
|
||||
calculateStatusBarData,
|
||||
collectUsageDetails,
|
||||
@@ -104,12 +104,12 @@ const clampCardPageSize = (value: number) =>
|
||||
|
||||
interface ExcludedFormState {
|
||||
provider: string;
|
||||
modelsText: string;
|
||||
selectedModels: Set<string>;
|
||||
}
|
||||
|
||||
type OAuthModelMappingFormEntry = OAuthModelMappingEntry & { id: string };
|
||||
type OAuthModelMappingFormEntry = OAuthModelAliasEntry & { id: string };
|
||||
|
||||
interface ModelMappingsFormState {
|
||||
interface ModelAliasFormState {
|
||||
provider: string;
|
||||
mappings: OAuthModelMappingFormEntry[];
|
||||
}
|
||||
@@ -232,19 +232,21 @@ export function AuthFilesPage() {
|
||||
const [excludedModalOpen, setExcludedModalOpen] = useState(false);
|
||||
const [excludedForm, setExcludedForm] = useState<ExcludedFormState>({
|
||||
provider: '',
|
||||
modelsText: '',
|
||||
selectedModels: new Set(),
|
||||
});
|
||||
const [excludedModelsList, setExcludedModelsList] = useState<AuthFileModelItem[]>([]);
|
||||
const [excludedModelsLoading, setExcludedModelsLoading] = useState(false);
|
||||
const [excludedModelsError, setExcludedModelsError] = useState<'unsupported' | null>(null);
|
||||
const [savingExcluded, setSavingExcluded] = useState(false);
|
||||
|
||||
// OAuth 模型映射相关
|
||||
const [modelMappings, setModelMappings] = useState<Record<string, OAuthModelMappingEntry[]>>({});
|
||||
const [modelMappingsError, setModelMappingsError] = useState<'unsupported' | null>(null);
|
||||
const [modelAlias, setModelAlias] = useState<Record<string, OAuthModelAliasEntry[]>>({});
|
||||
const [modelAliasError, setModelAliasError] = useState<'unsupported' | null>(null);
|
||||
const [mappingModalOpen, setMappingModalOpen] = useState(false);
|
||||
const [mappingForm, setMappingForm] = useState<ModelMappingsFormState>({
|
||||
const [mappingForm, setMappingForm] = useState<ModelAliasFormState>({
|
||||
provider: '',
|
||||
mappings: [buildEmptyMappingEntry()],
|
||||
});
|
||||
const [mappingModelsFileName, setMappingModelsFileName] = useState('');
|
||||
const [mappingModelsList, setMappingModelsList] = useState<AuthFileModelItem[]>([]);
|
||||
const [mappingModelsLoading, setMappingModelsLoading] = useState(false);
|
||||
const [mappingModelsError, setMappingModelsError] = useState<'unsupported' | null>(null);
|
||||
@@ -265,55 +267,21 @@ export function AuthFilesPage() {
|
||||
setPageSizeInput(String(pageSize));
|
||||
}, [pageSize]);
|
||||
|
||||
const modelSourceFileOptions = useMemo(() => {
|
||||
const normalizedProvider = normalizeProviderKey(mappingForm.provider);
|
||||
const matching: string[] = [];
|
||||
const others: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
files.forEach((file) => {
|
||||
const isRuntimeOnly = isRuntimeOnlyAuthFile(file);
|
||||
const isAistudio = (file.type || '').toLowerCase() === 'aistudio';
|
||||
const canShowModels = !isRuntimeOnly || isAistudio;
|
||||
if (!canShowModels) return;
|
||||
|
||||
const fileName = String(file.name || '').trim();
|
||||
if (!fileName) return;
|
||||
if (seen.has(fileName)) return;
|
||||
seen.add(fileName);
|
||||
|
||||
if (!normalizedProvider) {
|
||||
matching.push(fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
const typeKey = normalizeProviderKey(String(file.type || ''));
|
||||
const providerKey = normalizeProviderKey(String(file.provider || ''));
|
||||
const isMatch = typeKey === normalizedProvider || providerKey === normalizedProvider;
|
||||
if (isMatch) {
|
||||
matching.push(fileName);
|
||||
} else {
|
||||
others.push(fileName);
|
||||
}
|
||||
});
|
||||
|
||||
matching.sort((a, b) => a.localeCompare(b));
|
||||
others.sort((a, b) => a.localeCompare(b));
|
||||
return [...matching, ...others];
|
||||
}, [files, mappingForm.provider]);
|
||||
// 模型定义缓存(按 channel 缓存)
|
||||
const modelDefinitionsCacheRef = useRef<Map<string, AuthFileModelItem[]>>(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
if (!mappingModalOpen) return;
|
||||
|
||||
const fileName = mappingModelsFileName.trim();
|
||||
if (!fileName) {
|
||||
const channel = normalizeProviderKey(mappingForm.provider);
|
||||
if (!channel) {
|
||||
setMappingModelsList([]);
|
||||
setMappingModelsError(null);
|
||||
setMappingModelsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const cached = modelsCacheRef.current.get(fileName);
|
||||
const cached = modelDefinitionsCacheRef.current.get(channel);
|
||||
if (cached) {
|
||||
setMappingModelsList(cached);
|
||||
setMappingModelsError(null);
|
||||
@@ -326,10 +294,10 @@ export function AuthFilesPage() {
|
||||
setMappingModelsError(null);
|
||||
|
||||
authFilesApi
|
||||
.getModelsForAuthFile(fileName)
|
||||
.getModelDefinitions(channel)
|
||||
.then((models) => {
|
||||
if (cancelled) return;
|
||||
modelsCacheRef.current.set(fileName, models);
|
||||
modelDefinitionsCacheRef.current.set(channel, models);
|
||||
setMappingModelsList(models);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
@@ -354,7 +322,62 @@ export function AuthFilesPage() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [mappingModalOpen, mappingModelsFileName, showNotification, t]);
|
||||
}, [mappingModalOpen, mappingForm.provider, showNotification, t]);
|
||||
|
||||
// 排除列表弹窗:根据 provider 加载模型定义
|
||||
useEffect(() => {
|
||||
if (!excludedModalOpen) return;
|
||||
|
||||
const channel = normalizeProviderKey(excludedForm.provider);
|
||||
if (!channel) {
|
||||
setExcludedModelsList([]);
|
||||
setExcludedModelsError(null);
|
||||
setExcludedModelsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const cached = modelDefinitionsCacheRef.current.get(channel);
|
||||
if (cached) {
|
||||
setExcludedModelsList(cached);
|
||||
setExcludedModelsError(null);
|
||||
setExcludedModelsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setExcludedModelsLoading(true);
|
||||
setExcludedModelsError(null);
|
||||
|
||||
authFilesApi
|
||||
.getModelDefinitions(channel)
|
||||
.then((models) => {
|
||||
if (cancelled) return;
|
||||
modelDefinitionsCacheRef.current.set(channel, models);
|
||||
setExcludedModelsList(models);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (cancelled) return;
|
||||
const errorMessage = err instanceof Error ? err.message : '';
|
||||
if (
|
||||
errorMessage.includes('404') ||
|
||||
errorMessage.includes('not found') ||
|
||||
errorMessage.includes('Not Found')
|
||||
) {
|
||||
setExcludedModelsList([]);
|
||||
setExcludedModelsError('unsupported');
|
||||
return;
|
||||
}
|
||||
showNotification(`${t('notification.load_failed')}: ${errorMessage}`, 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
if (cancelled) return;
|
||||
setExcludedModelsLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [excludedModalOpen, excludedForm.provider, showNotification, t]);
|
||||
|
||||
const prefixProxyUpdatedText = useMemo(() => {
|
||||
if (!prefixProxyEditor?.json) return prefixProxyEditor?.rawText ?? '';
|
||||
@@ -489,12 +512,12 @@ export function AuthFilesPage() {
|
||||
}, [showNotification, t]);
|
||||
|
||||
// 加载 OAuth 模型映射
|
||||
const loadModelMappings = useCallback(async () => {
|
||||
const loadModelAlias = useCallback(async () => {
|
||||
try {
|
||||
const res = await authFilesApi.getOauthModelMappings();
|
||||
const res = await authFilesApi.getOauthModelAlias();
|
||||
mappingsUnsupportedRef.current = false;
|
||||
setModelMappings(res || {});
|
||||
setModelMappingsError(null);
|
||||
setModelAlias(res || {});
|
||||
setModelAliasError(null);
|
||||
} catch (err: unknown) {
|
||||
const status =
|
||||
typeof err === 'object' && err !== null && 'status' in err
|
||||
@@ -502,11 +525,11 @@ export function AuthFilesPage() {
|
||||
: undefined;
|
||||
|
||||
if (status === 404) {
|
||||
setModelMappings({});
|
||||
setModelMappingsError('unsupported');
|
||||
setModelAlias({});
|
||||
setModelAliasError('unsupported');
|
||||
if (!mappingsUnsupportedRef.current) {
|
||||
mappingsUnsupportedRef.current = true;
|
||||
showNotification(t('oauth_model_mappings.upgrade_required'), 'warning');
|
||||
showNotification(t('oauth_model_alias.upgrade_required'), 'warning');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -515,8 +538,8 @@ export function AuthFilesPage() {
|
||||
}, [showNotification, t]);
|
||||
|
||||
const handleHeaderRefresh = useCallback(async () => {
|
||||
await Promise.all([loadFiles(), loadKeyStats(), loadExcluded(), loadModelMappings()]);
|
||||
}, [loadFiles, loadKeyStats, loadExcluded, loadModelMappings]);
|
||||
await Promise.all([loadFiles(), loadKeyStats(), loadExcluded(), loadModelAlias()]);
|
||||
}, [loadFiles, loadKeyStats, loadExcluded, loadModelAlias]);
|
||||
|
||||
useHeaderRefresh(handleHeaderRefresh);
|
||||
|
||||
@@ -524,8 +547,8 @@ export function AuthFilesPage() {
|
||||
loadFiles();
|
||||
loadKeyStats();
|
||||
loadExcluded();
|
||||
loadModelMappings();
|
||||
}, [loadFiles, loadKeyStats, loadExcluded, loadModelMappings]);
|
||||
loadModelAlias();
|
||||
}, [loadFiles, loadKeyStats, loadExcluded, loadModelAlias]);
|
||||
|
||||
// 定时刷新状态数据(每240秒)
|
||||
useInterval(loadKeyStats, 240_000);
|
||||
@@ -554,14 +577,14 @@ export function AuthFilesPage() {
|
||||
|
||||
const mappingProviderLookup = useMemo(() => {
|
||||
const lookup = new Map<string, string>();
|
||||
Object.keys(modelMappings).forEach((provider) => {
|
||||
Object.keys(modelAlias).forEach((provider) => {
|
||||
const key = provider.trim().toLowerCase();
|
||||
if (key && !lookup.has(key)) {
|
||||
lookup.set(key, provider);
|
||||
}
|
||||
});
|
||||
return lookup;
|
||||
}, [modelMappings]);
|
||||
}, [modelAlias]);
|
||||
|
||||
const providerOptions = useMemo(() => {
|
||||
const extraProviders = new Set<string>();
|
||||
@@ -569,7 +592,7 @@ export function AuthFilesPage() {
|
||||
Object.keys(excluded).forEach((provider) => {
|
||||
extraProviders.add(provider);
|
||||
});
|
||||
Object.keys(modelMappings).forEach((provider) => {
|
||||
Object.keys(modelAlias).forEach((provider) => {
|
||||
extraProviders.add(provider);
|
||||
});
|
||||
files.forEach((file) => {
|
||||
@@ -591,7 +614,7 @@ export function AuthFilesPage() {
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
return [...OAUTH_PROVIDER_PRESETS, ...extraList];
|
||||
}, [excluded, files, modelMappings]);
|
||||
}, [excluded, files, modelAlias]);
|
||||
|
||||
// 过滤和搜索
|
||||
const filtered = useMemo(() => {
|
||||
@@ -1043,11 +1066,13 @@ export function AuthFilesPage() {
|
||||
const fallbackProvider =
|
||||
normalizedProvider || (filter !== 'all' ? normalizeProviderKey(String(filter)) : '');
|
||||
const lookupKey = fallbackProvider ? excludedProviderLookup.get(fallbackProvider) : undefined;
|
||||
const models = lookupKey ? excluded[lookupKey] : [];
|
||||
const existingModels = lookupKey ? excluded[lookupKey] : [];
|
||||
setExcludedForm({
|
||||
provider: lookupKey || fallbackProvider,
|
||||
modelsText: Array.isArray(models) ? models.join('\n') : '',
|
||||
selectedModels: new Set(existingModels),
|
||||
});
|
||||
setExcludedModelsList([]);
|
||||
setExcludedModelsError(null);
|
||||
setExcludedModalOpen(true);
|
||||
};
|
||||
|
||||
@@ -1057,10 +1082,7 @@ export function AuthFilesPage() {
|
||||
showNotification(t('oauth_excluded.provider_required'), 'error');
|
||||
return;
|
||||
}
|
||||
const models = excludedForm.modelsText
|
||||
.split(/[\n,]+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
const models = [...excludedForm.selectedModels];
|
||||
setSavingExcluded(true);
|
||||
try {
|
||||
if (models.length) {
|
||||
@@ -1123,7 +1145,7 @@ export function AuthFilesPage() {
|
||||
|
||||
// OAuth 模型映射相关方法
|
||||
const normalizeMappingEntries = (
|
||||
entries?: OAuthModelMappingEntry[]
|
||||
entries?: OAuthModelAliasEntry[]
|
||||
): OAuthModelMappingFormEntry[] => {
|
||||
if (!Array.isArray(entries) || entries.length === 0) {
|
||||
return [buildEmptyMappingEntry()];
|
||||
@@ -1142,29 +1164,13 @@ export function AuthFilesPage() {
|
||||
const lookupKey = fallbackProvider
|
||||
? mappingProviderLookup.get(fallbackProvider.toLowerCase())
|
||||
: undefined;
|
||||
const mappings = lookupKey ? modelMappings[lookupKey] : [];
|
||||
const mappings = lookupKey ? modelAlias[lookupKey] : [];
|
||||
const providerValue = lookupKey || fallbackProvider;
|
||||
|
||||
const normalizedProviderKey = normalizeProviderKey(providerValue);
|
||||
const defaultModelsFileName = files
|
||||
.filter((file) => {
|
||||
const isRuntimeOnly = isRuntimeOnlyAuthFile(file);
|
||||
const isAistudio = (file.type || '').toLowerCase() === 'aistudio';
|
||||
const canShowModels = !isRuntimeOnly || isAistudio;
|
||||
if (!canShowModels) return false;
|
||||
if (!normalizedProviderKey) return false;
|
||||
const typeKey = normalizeProviderKey(String(file.type || ''));
|
||||
const providerKey = normalizeProviderKey(String(file.provider || ''));
|
||||
return typeKey === normalizedProviderKey || providerKey === normalizedProviderKey;
|
||||
})
|
||||
.map((file) => file.name)
|
||||
.sort((a, b) => a.localeCompare(b))[0];
|
||||
|
||||
setMappingForm({
|
||||
provider: providerValue,
|
||||
mappings: normalizeMappingEntries(mappings),
|
||||
});
|
||||
setMappingModelsFileName(defaultModelsFileName || '');
|
||||
setMappingModelsList([]);
|
||||
setMappingModelsError(null);
|
||||
setMappingModalOpen(true);
|
||||
@@ -1172,7 +1178,7 @@ export function AuthFilesPage() {
|
||||
|
||||
const updateMappingEntry = (
|
||||
index: number,
|
||||
field: keyof OAuthModelMappingEntry,
|
||||
field: keyof OAuthModelAliasEntry,
|
||||
value: string | boolean
|
||||
) => {
|
||||
setMappingForm((prev) => ({
|
||||
@@ -1200,10 +1206,10 @@ export function AuthFilesPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const saveModelMappings = async () => {
|
||||
const saveModelAlias = async () => {
|
||||
const provider = mappingForm.provider.trim();
|
||||
if (!provider) {
|
||||
showNotification(t('oauth_model_mappings.provider_required'), 'error');
|
||||
showNotification(t('oauth_model_alias.provider_required'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1218,40 +1224,40 @@ export function AuthFilesPage() {
|
||||
seen.add(key);
|
||||
return entry.fork ? { name, alias, fork: true } : { name, alias };
|
||||
})
|
||||
.filter(Boolean) as OAuthModelMappingEntry[];
|
||||
.filter(Boolean) as OAuthModelAliasEntry[];
|
||||
|
||||
setSavingMappings(true);
|
||||
try {
|
||||
if (mappings.length) {
|
||||
await authFilesApi.saveOauthModelMappings(provider, mappings);
|
||||
await authFilesApi.saveOauthModelAlias(provider, mappings);
|
||||
} else {
|
||||
await authFilesApi.deleteOauthModelMappings(provider);
|
||||
await authFilesApi.deleteOauthModelAlias(provider);
|
||||
}
|
||||
await loadModelMappings();
|
||||
showNotification(t('oauth_model_mappings.save_success'), 'success');
|
||||
await loadModelAlias();
|
||||
showNotification(t('oauth_model_alias.save_success'), 'success');
|
||||
setMappingModalOpen(false);
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : '';
|
||||
showNotification(`${t('oauth_model_mappings.save_failed')}: ${errorMessage}`, 'error');
|
||||
showNotification(`${t('oauth_model_alias.save_failed')}: ${errorMessage}`, 'error');
|
||||
} finally {
|
||||
setSavingMappings(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteModelMappings = async (provider: string) => {
|
||||
const deleteModelAlias = async (provider: string) => {
|
||||
showConfirmation({
|
||||
title: t('oauth_model_mappings.delete_title', { defaultValue: 'Delete Mappings' }),
|
||||
message: t('oauth_model_mappings.delete_confirm', { provider }),
|
||||
title: t('oauth_model_alias.delete_title', { defaultValue: 'Delete Mappings' }),
|
||||
message: t('oauth_model_alias.delete_confirm', { provider }),
|
||||
variant: 'danger',
|
||||
confirmText: t('common.confirm'),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await authFilesApi.deleteOauthModelMappings(provider);
|
||||
await loadModelMappings();
|
||||
showNotification(t('oauth_model_mappings.delete_success'), 'success');
|
||||
await authFilesApi.deleteOauthModelAlias(provider);
|
||||
await loadModelAlias();
|
||||
showNotification(t('oauth_model_alias.delete_success'), 'success');
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : '';
|
||||
showNotification(`${t('oauth_model_mappings.delete_failed')}: ${errorMessage}`, 'error');
|
||||
showNotification(`${t('oauth_model_alias.delete_failed')}: ${errorMessage}`, 'error');
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1657,42 +1663,42 @@ export function AuthFilesPage() {
|
||||
|
||||
{/* OAuth 模型映射卡片 */}
|
||||
<Card
|
||||
title={t('oauth_model_mappings.title')}
|
||||
title={t('oauth_model_alias.title')}
|
||||
extra={
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => openMappingsModal()}
|
||||
disabled={disableControls || modelMappingsError === 'unsupported'}
|
||||
disabled={disableControls || modelAliasError === 'unsupported'}
|
||||
>
|
||||
{t('oauth_model_mappings.add')}
|
||||
{t('oauth_model_alias.add')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{modelMappingsError === 'unsupported' ? (
|
||||
{modelAliasError === 'unsupported' ? (
|
||||
<EmptyState
|
||||
title={t('oauth_model_mappings.upgrade_required_title')}
|
||||
description={t('oauth_model_mappings.upgrade_required_desc')}
|
||||
title={t('oauth_model_alias.upgrade_required_title')}
|
||||
description={t('oauth_model_alias.upgrade_required_desc')}
|
||||
/>
|
||||
) : Object.keys(modelMappings).length === 0 ? (
|
||||
<EmptyState title={t('oauth_model_mappings.list_empty_all')} />
|
||||
) : Object.keys(modelAlias).length === 0 ? (
|
||||
<EmptyState title={t('oauth_model_alias.list_empty_all')} />
|
||||
) : (
|
||||
<div className={styles.excludedList}>
|
||||
{Object.entries(modelMappings).map(([provider, mappings]) => (
|
||||
{Object.entries(modelAlias).map(([provider, mappings]) => (
|
||||
<div key={provider} className={styles.excludedItem}>
|
||||
<div className={styles.excludedInfo}>
|
||||
<div className={styles.excludedProvider}>{provider}</div>
|
||||
<div className={styles.excludedModels}>
|
||||
{mappings?.length
|
||||
? t('oauth_model_mappings.model_count', { count: mappings.length })
|
||||
: t('oauth_model_mappings.no_models')}
|
||||
? t('oauth_model_alias.model_count', { count: mappings.length })
|
||||
: t('oauth_model_alias.no_models')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.excludedActions}>
|
||||
<Button variant="secondary" size="sm" onClick={() => openMappingsModal(provider)}>
|
||||
{t('common.edit')}
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" onClick={() => deleteModelMappings(provider)}>
|
||||
{t('oauth_model_mappings.delete')}
|
||||
<Button variant="danger" size="sm" onClick={() => deleteModelAlias(provider)}>
|
||||
{t('oauth_model_alias.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1937,16 +1943,55 @@ export function AuthFilesPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* 模型勾选列表 */}
|
||||
<div className={styles.formGroup}>
|
||||
<label>{t('oauth_excluded.models_label')}</label>
|
||||
<textarea
|
||||
className={styles.textarea}
|
||||
rows={4}
|
||||
placeholder={t('oauth_excluded.models_placeholder')}
|
||||
value={excludedForm.modelsText}
|
||||
onChange={(e) => setExcludedForm((prev) => ({ ...prev, modelsText: e.target.value }))}
|
||||
/>
|
||||
<div className={styles.hint}>{t('oauth_excluded.models_hint')}</div>
|
||||
{excludedModelsLoading ? (
|
||||
<div className={styles.hint}>{t('common.loading')}</div>
|
||||
) : excludedModelsList.length > 0 ? (
|
||||
<>
|
||||
<div className={styles.excludedCheckList}>
|
||||
{excludedModelsList.map((model) => {
|
||||
const isChecked = excludedForm.selectedModels.has(model.id);
|
||||
return (
|
||||
<label key={model.id} className={styles.excludedCheckItem}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
disabled={savingExcluded}
|
||||
onChange={(e) => {
|
||||
setExcludedForm((prev) => {
|
||||
const next = new Set(prev.selectedModels);
|
||||
if (e.target.checked) {
|
||||
next.add(model.id);
|
||||
} else {
|
||||
next.delete(model.id);
|
||||
}
|
||||
return { ...prev, selectedModels: next };
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span className={styles.excludedCheckLabel}>
|
||||
{model.id}
|
||||
{model.display_name && model.display_name !== model.id && (
|
||||
<span className={styles.excludedCheckDisplayName}>{model.display_name}</span>
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{excludedForm.provider.trim() && (
|
||||
<div className={styles.hint}>
|
||||
{excludedModelsError === 'unsupported'
|
||||
? t('oauth_excluded.models_unsupported')
|
||||
: t('oauth_excluded.models_loaded', { count: excludedModelsList.length })}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : excludedForm.provider.trim() && !excludedModelsLoading ? (
|
||||
<div className={styles.hint}>{t('oauth_excluded.no_models_available')}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1954,7 +1999,7 @@ export function AuthFilesPage() {
|
||||
<Modal
|
||||
open={mappingModalOpen}
|
||||
onClose={() => setMappingModalOpen(false)}
|
||||
title={t('oauth_model_mappings.add_title')}
|
||||
title={t('oauth_model_alias.add_title')}
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
@@ -1964,8 +2009,8 @@ export function AuthFilesPage() {
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={saveModelMappings} loading={savingMappings}>
|
||||
{t('oauth_model_mappings.save')}
|
||||
<Button onClick={saveModelAlias} loading={savingMappings}>
|
||||
{t('oauth_model_alias.save')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
@@ -1973,9 +2018,9 @@ export function AuthFilesPage() {
|
||||
<div className={styles.providerField}>
|
||||
<AutocompleteInput
|
||||
id="oauth-model-alias-provider"
|
||||
label={t('oauth_model_mappings.provider_label')}
|
||||
hint={t('oauth_model_mappings.provider_hint')}
|
||||
placeholder={t('oauth_model_mappings.provider_placeholder')}
|
||||
label={t('oauth_model_alias.provider_label')}
|
||||
hint={t('oauth_model_alias.provider_hint')}
|
||||
placeholder={t('oauth_model_alias.provider_placeholder')}
|
||||
value={mappingForm.provider}
|
||||
onChange={(val) => setMappingForm((prev) => ({ ...prev, provider: val }))}
|
||||
options={providerOptions}
|
||||
@@ -2000,37 +2045,27 @@ export function AuthFilesPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.providerField}>
|
||||
<AutocompleteInput
|
||||
id="oauth-model-mapping-model-source"
|
||||
label={t('oauth_model_mappings.model_source_label')}
|
||||
hint={
|
||||
mappingModelsLoading
|
||||
? t('oauth_model_mappings.model_source_loading')
|
||||
: mappingModelsError === 'unsupported'
|
||||
? t('oauth_model_mappings.model_source_unsupported')
|
||||
: !mappingModelsFileName.trim()
|
||||
? t('oauth_model_mappings.model_source_hint')
|
||||
: t('oauth_model_mappings.model_source_loaded', {
|
||||
count: mappingModelsList.length,
|
||||
})
|
||||
}
|
||||
placeholder={t('oauth_model_mappings.model_source_placeholder')}
|
||||
value={mappingModelsFileName}
|
||||
onChange={(val) => setMappingModelsFileName(val)}
|
||||
disabled={savingMappings}
|
||||
options={modelSourceFileOptions}
|
||||
/>
|
||||
</div>
|
||||
{/* 模型定义加载状态提示 */}
|
||||
{mappingForm.provider.trim() && (
|
||||
<div className={styles.hint}>
|
||||
{mappingModelsLoading
|
||||
? t('oauth_model_alias.model_source_loading')
|
||||
: mappingModelsError === 'unsupported'
|
||||
? t('oauth_model_alias.model_source_unsupported')
|
||||
: t('oauth_model_alias.model_source_loaded', {
|
||||
count: mappingModelsList.length,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.formGroup}>
|
||||
<label>{t('oauth_model_mappings.mappings_label')}</label>
|
||||
<label>{t('oauth_model_alias.alias_label')}</label>
|
||||
<div className="header-input-list">
|
||||
{(mappingForm.mappings.length ? mappingForm.mappings : [buildEmptyMappingEntry()]).map(
|
||||
(entry, index) => (
|
||||
<div key={entry.id} className={styles.mappingRow}>
|
||||
<AutocompleteInput
|
||||
wrapperStyle={{ flex: 1, marginBottom: 0 }}
|
||||
placeholder={t('oauth_model_mappings.mapping_name_placeholder')}
|
||||
placeholder={t('oauth_model_alias.alias_name_placeholder')}
|
||||
value={entry.name}
|
||||
onChange={(val) => updateMappingEntry(index, 'name', val)}
|
||||
disabled={savingMappings}
|
||||
@@ -2042,7 +2077,7 @@ export function AuthFilesPage() {
|
||||
<span className={styles.mappingSeparator}>→</span>
|
||||
<input
|
||||
className="input"
|
||||
placeholder={t('oauth_model_mappings.mapping_alias_placeholder')}
|
||||
placeholder={t('oauth_model_alias.alias_placeholder')}
|
||||
value={entry.alias}
|
||||
onChange={(e) => updateMappingEntry(index, 'alias', e.target.value)}
|
||||
disabled={savingMappings}
|
||||
@@ -2050,7 +2085,7 @@ export function AuthFilesPage() {
|
||||
/>
|
||||
<div className={styles.mappingFork}>
|
||||
<ToggleSwitch
|
||||
label={t('oauth_model_mappings.mapping_fork_label')}
|
||||
label={t('oauth_model_alias.alias_fork_label')}
|
||||
labelPosition="left"
|
||||
checked={Boolean(entry.fork)}
|
||||
onChange={(value) => updateMappingEntry(index, 'fork', value)}
|
||||
@@ -2077,10 +2112,9 @@ export function AuthFilesPage() {
|
||||
disabled={savingMappings}
|
||||
className="align-start"
|
||||
>
|
||||
{t('oauth_model_mappings.add_mapping')}
|
||||
{t('oauth_model_alias.add_alias')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.hint}>{t('oauth_model_mappings.mappings_hint')}</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
@@ -456,6 +456,18 @@
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.requestCountCell {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.requestBreakdown {
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// Pricing Section (80%比例)
|
||||
.pricingSection {
|
||||
display: flex;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { apiClient } from './client';
|
||||
import type { AuthFilesResponse } from '@/types/authFile';
|
||||
import type { OAuthModelMappingEntry } from '@/types';
|
||||
import type { OAuthModelAliasEntry } from '@/types';
|
||||
|
||||
type StatusError = { status?: number };
|
||||
type AuthFileStatusResponse = { status: string; disabled: boolean };
|
||||
@@ -53,18 +53,17 @@ const normalizeOauthExcludedModels = (payload: unknown): Record<string, string[]
|
||||
return result;
|
||||
};
|
||||
|
||||
const normalizeOauthModelMappings = (payload: unknown): Record<string, OAuthModelMappingEntry[]> => {
|
||||
const normalizeOauthModelAlias = (payload: unknown): Record<string, OAuthModelAliasEntry[]> => {
|
||||
if (!payload || typeof payload !== 'object') return {};
|
||||
|
||||
const record = payload as Record<string, unknown>;
|
||||
const source =
|
||||
record['oauth-model-mappings'] ??
|
||||
record['oauth-model-alias'] ??
|
||||
record.items ??
|
||||
payload;
|
||||
if (!source || typeof source !== 'object') return {};
|
||||
|
||||
const result: Record<string, OAuthModelMappingEntry[]> = {};
|
||||
const result: Record<string, OAuthModelAliasEntry[]> = {};
|
||||
|
||||
Object.entries(source as Record<string, unknown>).forEach(([channel, mappings]) => {
|
||||
const key = String(channel ?? '')
|
||||
@@ -86,12 +85,12 @@ const normalizeOauthModelMappings = (payload: unknown): Record<string, OAuthMode
|
||||
})
|
||||
.filter(Boolean)
|
||||
.filter((entry) => {
|
||||
const mapping = entry as OAuthModelMappingEntry;
|
||||
const dedupeKey = `${mapping.name.toLowerCase()}::${mapping.alias.toLowerCase()}::${mapping.fork ? '1' : '0'}`;
|
||||
const aliasEntry = entry as OAuthModelAliasEntry;
|
||||
const dedupeKey = `${aliasEntry.name.toLowerCase()}::${aliasEntry.alias.toLowerCase()}::${aliasEntry.fork ? '1' : '0'}`;
|
||||
if (seen.has(dedupeKey)) return false;
|
||||
seen.add(dedupeKey);
|
||||
return true;
|
||||
}) as OAuthModelMappingEntry[];
|
||||
}) as OAuthModelAliasEntry[];
|
||||
|
||||
if (normalized.length) {
|
||||
result[key] = normalized;
|
||||
@@ -101,8 +100,7 @@ const normalizeOauthModelMappings = (payload: unknown): Record<string, OAuthMode
|
||||
return result;
|
||||
};
|
||||
|
||||
const OAUTH_MODEL_MAPPINGS_ENDPOINT = '/oauth-model-mappings';
|
||||
const OAUTH_MODEL_MAPPINGS_LEGACY_ENDPOINT = '/oauth-model-alias';
|
||||
const OAUTH_MODEL_ALIAS_ENDPOINT = '/oauth-model-alias';
|
||||
|
||||
export const authFilesApi = {
|
||||
list: () => apiClient.get<AuthFilesResponse>('/auth-files'),
|
||||
@@ -143,63 +141,31 @@ export const authFilesApi = {
|
||||
replaceOauthExcludedModels: (map: Record<string, string[]>) =>
|
||||
apiClient.put('/oauth-excluded-models', normalizeOauthExcludedModels(map)),
|
||||
|
||||
// OAuth 模型映射
|
||||
async getOauthModelMappings(): Promise<Record<string, OAuthModelMappingEntry[]>> {
|
||||
try {
|
||||
const data = await apiClient.get(OAUTH_MODEL_MAPPINGS_ENDPOINT);
|
||||
return normalizeOauthModelMappings(data);
|
||||
} catch (err: unknown) {
|
||||
if (getStatusCode(err) !== 404) throw err;
|
||||
const data = await apiClient.get(OAUTH_MODEL_MAPPINGS_LEGACY_ENDPOINT);
|
||||
return normalizeOauthModelMappings(data);
|
||||
}
|
||||
// OAuth 模型别名
|
||||
async getOauthModelAlias(): Promise<Record<string, OAuthModelAliasEntry[]>> {
|
||||
const data = await apiClient.get(OAUTH_MODEL_ALIAS_ENDPOINT);
|
||||
return normalizeOauthModelAlias(data);
|
||||
},
|
||||
|
||||
saveOauthModelMappings: async (channel: string, mappings: OAuthModelMappingEntry[]) => {
|
||||
saveOauthModelAlias: async (channel: string, aliases: OAuthModelAliasEntry[]) => {
|
||||
const normalizedChannel = String(channel ?? '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const normalizedMappings = normalizeOauthModelMappings({ [normalizedChannel]: mappings })[normalizedChannel] ?? [];
|
||||
|
||||
try {
|
||||
await apiClient.patch(OAUTH_MODEL_MAPPINGS_ENDPOINT, { channel: normalizedChannel, mappings: normalizedMappings });
|
||||
return;
|
||||
} catch (err: unknown) {
|
||||
if (getStatusCode(err) !== 404) throw err;
|
||||
await apiClient.patch(OAUTH_MODEL_MAPPINGS_LEGACY_ENDPOINT, { channel: normalizedChannel, aliases: normalizedMappings });
|
||||
}
|
||||
const normalizedAliases = normalizeOauthModelAlias({ [normalizedChannel]: aliases })[normalizedChannel] ?? [];
|
||||
await apiClient.patch(OAUTH_MODEL_ALIAS_ENDPOINT, { channel: normalizedChannel, aliases: normalizedAliases });
|
||||
},
|
||||
|
||||
deleteOauthModelMappings: async (channel: string) => {
|
||||
deleteOauthModelAlias: async (channel: string) => {
|
||||
const normalizedChannel = String(channel ?? '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
const deleteViaPatch = async () => {
|
||||
try {
|
||||
await apiClient.patch(OAUTH_MODEL_MAPPINGS_ENDPOINT, { channel: normalizedChannel, mappings: [] });
|
||||
return true;
|
||||
} catch (err: unknown) {
|
||||
if (getStatusCode(err) !== 404) throw err;
|
||||
await apiClient.patch(OAUTH_MODEL_MAPPINGS_LEGACY_ENDPOINT, { channel: normalizedChannel, aliases: [] });
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await deleteViaPatch();
|
||||
return;
|
||||
await apiClient.patch(OAUTH_MODEL_ALIAS_ENDPOINT, { channel: normalizedChannel, aliases: [] });
|
||||
} catch (err: unknown) {
|
||||
const status = getStatusCode(err);
|
||||
if (status !== 405) throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.delete(`${OAUTH_MODEL_MAPPINGS_ENDPOINT}?channel=${encodeURIComponent(normalizedChannel)}`);
|
||||
return;
|
||||
} catch (err: unknown) {
|
||||
if (getStatusCode(err) !== 404) throw err;
|
||||
await apiClient.delete(`${OAUTH_MODEL_MAPPINGS_LEGACY_ENDPOINT}?channel=${encodeURIComponent(normalizedChannel)}`);
|
||||
await apiClient.delete(`${OAUTH_MODEL_ALIAS_ENDPOINT}?channel=${encodeURIComponent(normalizedChannel)}`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -207,5 +173,13 @@ export const authFilesApi = {
|
||||
async getModelsForAuthFile(name: string): Promise<{ id: string; display_name?: string; type?: string; owned_by?: string }[]> {
|
||||
const data = await apiClient.get(`/auth-files/models?name=${encodeURIComponent(name)}`);
|
||||
return (data && Array.isArray(data['models'])) ? data['models'] : [];
|
||||
},
|
||||
|
||||
// 获取指定 channel 的模型定义
|
||||
async getModelDefinitions(channel: string): Promise<{ id: string; display_name?: string; type?: string; owned_by?: string }[]> {
|
||||
const normalizedChannel = String(channel ?? '').trim().toLowerCase();
|
||||
if (!normalizedChannel) return [];
|
||||
const data = await apiClient.get(`/model-definitions/${encodeURIComponent(normalizedChannel)}`);
|
||||
return (data && Array.isArray(data['models'])) ? data['models'] : [];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,11 +34,11 @@ export interface OAuthExcludedModels {
|
||||
models: string[];
|
||||
}
|
||||
|
||||
// OAuth 模型映射
|
||||
export interface OAuthModelMappingEntry {
|
||||
// OAuth 模型别名
|
||||
export interface OAuthModelAliasEntry {
|
||||
name: string;
|
||||
alias: string;
|
||||
fork?: boolean;
|
||||
}
|
||||
|
||||
export type OAuthModelMappings = Record<string, OAuthModelMappingEntry[]>;
|
||||
export type OAuthModelAlias = Record<string, OAuthModelAliasEntry[]>;
|
||||
|
||||
@@ -579,6 +579,8 @@ export function getApiStats(usageData: any, modelPrices: Record<string, ModelPri
|
||||
export function getModelStats(usageData: any, modelPrices: Record<string, ModelPrice>): Array<{
|
||||
model: string;
|
||||
requests: number;
|
||||
successCount: number;
|
||||
failureCount: number;
|
||||
tokens: number;
|
||||
cost: number;
|
||||
}> {
|
||||
@@ -586,20 +588,39 @@ export function getModelStats(usageData: any, modelPrices: Record<string, ModelP
|
||||
return [];
|
||||
}
|
||||
|
||||
const modelMap = new Map<string, { requests: number; tokens: number; cost: number }>();
|
||||
const modelMap = new Map<string, { requests: number; successCount: number; failureCount: number; tokens: number; cost: number }>();
|
||||
|
||||
Object.values(usageData.apis as Record<string, any>).forEach(apiData => {
|
||||
const models = apiData?.models || {};
|
||||
Object.entries(models as Record<string, any>).forEach(([modelName, modelData]) => {
|
||||
const existing = modelMap.get(modelName) || { requests: 0, tokens: 0, cost: 0 };
|
||||
const existing = modelMap.get(modelName) || { requests: 0, successCount: 0, failureCount: 0, tokens: 0, cost: 0 };
|
||||
existing.requests += modelData.total_requests || 0;
|
||||
existing.tokens += modelData.total_tokens || 0;
|
||||
|
||||
const details = Array.isArray(modelData.details) ? modelData.details : [];
|
||||
|
||||
const price = modelPrices[modelName];
|
||||
if (price) {
|
||||
const details = Array.isArray(modelData.details) ? modelData.details : [];
|
||||
|
||||
const hasExplicitCounts =
|
||||
typeof modelData.success_count === 'number' || typeof modelData.failure_count === 'number';
|
||||
if (hasExplicitCounts) {
|
||||
existing.successCount += Number(modelData.success_count) || 0;
|
||||
existing.failureCount += Number(modelData.failure_count) || 0;
|
||||
}
|
||||
|
||||
if (details.length > 0 && (!hasExplicitCounts || price)) {
|
||||
details.forEach((detail: any) => {
|
||||
existing.cost += calculateCost({ ...detail, __modelName: modelName }, modelPrices);
|
||||
if (!hasExplicitCounts) {
|
||||
if (detail?.failed === true) {
|
||||
existing.failureCount += 1;
|
||||
} else {
|
||||
existing.successCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (price) {
|
||||
existing.cost += calculateCost({ ...detail, __modelName: modelName }, modelPrices);
|
||||
}
|
||||
});
|
||||
}
|
||||
modelMap.set(modelName, existing);
|
||||
|
||||
Reference in New Issue
Block a user