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