feat(auth): load model lists via /model-definitions/{channel} instead of per-file

model sources.
This commit is contained in:
hkfires
2026-01-27 14:27:26 +08:00
parent 0c53dcfa80
commit 2bf721974b
5 changed files with 114 additions and 201 deletions

View File

@@ -512,7 +512,7 @@
"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 Aliases", "title": "OAuth Model Aliases",
"add": "Add Alias", "add": "Add Alias",
"add_title": "Add provider model aliases", "add_title": "Add provider model aliases",

View File

@@ -512,7 +512,7 @@
"upgrade_required_title": "需要升级 CPA 版本", "upgrade_required_title": "需要升级 CPA 版本",
"upgrade_required_desc": "当前服务器版本不支持获取模型排除列表功能,请升级到最新版本的 CPACLI Proxy API后重试。" "upgrade_required_desc": "当前服务器版本不支持获取模型排除列表功能,请升级到最新版本的 CPACLI Proxy API后重试。"
}, },
"oauth_model_mappings": { "oauth_model_alias": {
"title": "OAuth 模型别名", "title": "OAuth 模型别名",
"add": "新增别名", "add": "新增别名",
"add_title": "新增提供商模型别名", "add_title": "新增提供商模型别名",

View File

@@ -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,
@@ -107,9 +107,9 @@ interface ExcludedFormState {
modelsText: string; modelsText: string;
} }
type OAuthModelMappingFormEntry = OAuthModelMappingEntry & { id: string }; type OAuthModelMappingFormEntry = OAuthModelAliasEntry & { id: string };
interface ModelMappingsFormState { interface ModelAliasFormState {
provider: string; provider: string;
mappings: OAuthModelMappingFormEntry[]; mappings: OAuthModelMappingFormEntry[];
} }
@@ -237,14 +237,13 @@ export function AuthFilesPage() {
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 +264,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 +291,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 +319,7 @@ export function AuthFilesPage() {
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [mappingModalOpen, mappingModelsFileName, showNotification, t]); }, [mappingModalOpen, mappingForm.provider, showNotification, t]);
const prefixProxyUpdatedText = useMemo(() => { const prefixProxyUpdatedText = useMemo(() => {
if (!prefixProxyEditor?.json) return prefixProxyEditor?.rawText ?? ''; if (!prefixProxyEditor?.json) return prefixProxyEditor?.rawText ?? '';
@@ -489,12 +454,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 +467,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 +480,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 +489,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 +519,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 +534,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 +556,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(() => {
@@ -1123,7 +1088,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 +1107,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 +1121,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 +1149,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 +1167,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 +1606,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>
@@ -1954,7 +1903,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 +1913,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 +1922,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 +1949,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
? t('oauth_model_mappings.model_source_loading')
: mappingModelsError === 'unsupported' : mappingModelsError === 'unsupported'
? t('oauth_model_mappings.model_source_unsupported') ? t('oauth_model_alias.model_source_unsupported')
: !mappingModelsFileName.trim() : t('oauth_model_alias.model_source_loaded', {
? t('oauth_model_mappings.model_source_hint')
: t('oauth_model_mappings.model_source_loaded', {
count: mappingModelsList.length, count: mappingModelsList.length,
}) })}
}
placeholder={t('oauth_model_mappings.model_source_placeholder')}
value={mappingModelsFileName}
onChange={(val) => setMappingModelsFileName(val)}
disabled={savingMappings}
options={modelSourceFileOptions}
/>
</div> </div>
)}
<div className={styles.formGroup}> <div className={styles.formGroup}>
<label>{t('oauth_model_mappings.mappings_label')}</label> <label>{t('oauth_model_alias.mappings_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.mapping_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 +1981,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.mapping_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 +1989,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.mapping_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 +2016,10 @@ export function AuthFilesPage() {
disabled={savingMappings} disabled={savingMappings}
className="align-start" className="align-start"
> >
{t('oauth_model_mappings.add_mapping')} {t('oauth_model_alias.add_mapping')}
</Button> </Button>
</div> </div>
<div className={styles.hint}>{t('oauth_model_mappings.mappings_hint')}</div> <div className={styles.hint}>{t('oauth_model_alias.mappings_hint')}</div>
</div> </div>
</Modal> </Modal>
</div> </div>

View File

@@ -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 { try {
await apiClient.patch(OAUTH_MODEL_MAPPINGS_ENDPOINT, { channel: normalizedChannel, mappings: [] }); await apiClient.patch(OAUTH_MODEL_ALIAS_ENDPOINT, { channel: normalizedChannel, aliases: [] });
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;
} 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'] : [];
} }
}; };

View File

@@ -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[]>;