feat: improve model catalog aggregation

This commit is contained in:
2026-05-11 17:44:57 +08:00
parent ec87816c95
commit 0431cb8157
41 changed files with 4745 additions and 550 deletions
+60 -9
View File
@@ -12,6 +12,7 @@ import type {
GatewayTenant,
GatewayUser,
IntegrationPlatform,
ModelCatalogResponse,
PlatformModel,
PricingRule,
PricingRuleSet,
@@ -42,6 +43,7 @@ import {
listApiKeys,
listBaseModels,
listCatalogProviders,
listModelCatalog,
listModels,
listPlayableApiKeys,
listPlayableModels,
@@ -87,6 +89,8 @@ import {
pathForPage,
pathForPlaygroundMode,
pathForWorkspaceSection,
pathForWorkspaceTaskQuery,
workspaceTaskQueryKey,
type AppRouteState,
} from './routing';
import type {
@@ -103,6 +107,7 @@ import type {
PlatformWithModelsInput,
RegisterForm,
TaskForm,
WorkspaceTaskQuery,
WorkspaceSection,
} from './types';
@@ -111,6 +116,7 @@ type DataKey =
| 'publicCatalog'
| 'playgroundApiKeys'
| 'playgroundModels'
| 'modelCatalog'
| 'platforms'
| 'models'
| 'providers'
@@ -131,6 +137,7 @@ export function App() {
const [activePage, setActivePage] = useState<PageKey>(initialRoute.activePage);
const [adminSection, setAdminSection] = useState<AdminSection>(initialRoute.adminSection);
const [workspaceSection, setWorkspaceSection] = useState<WorkspaceSection>(initialRoute.workspaceSection);
const [workspaceTaskQuery, setWorkspaceTaskQuery] = useState<WorkspaceTaskQuery>(initialRoute.workspaceTaskQuery);
const [apiDocSection, setApiDocSection] = useState<ApiDocSection>(initialRoute.apiDocSection);
const [playgroundMode, setPlaygroundMode] = useState<PlaygroundMode>(initialRoute.playgroundMode);
const [token, setToken] = useState(readStoredAccessToken);
@@ -141,6 +148,11 @@ export function App() {
const [health, setHealth] = useState<HealthResponse | null>(null);
const [platforms, setPlatforms] = useState<IntegrationPlatform[]>([]);
const [models, setModels] = useState<PlatformModel[]>([]);
const [modelCatalog, setModelCatalog] = useState<ModelCatalogResponse>({
items: [],
filters: { capabilities: [], providers: [] },
summary: { modelCount: 0, sourceCount: 0 },
});
const [playgroundModels, setPlaygroundModels] = useState<PlatformModel[]>([]);
const [providers, setProviders] = useState<CatalogProvider[]>([]);
const [baseModels, setBaseModels] = useState<BaseModelCatalogItem[]>([]);
@@ -160,12 +172,15 @@ export function App() {
const [taskForm, setTaskForm] = useState<TaskForm>({ kind: 'chat.completions', model: 'gpt-4o-mini', prompt: '用一句话确认 AI Gateway simulation 链路正常。' });
const [taskResult, setTaskResult] = useState<GatewayTask | null>(null);
const [tasks, setTasks] = useState<GatewayTask[]>([]);
const [taskTotal, setTaskTotal] = useState(0);
const [coreState, setCoreState] = useState<LoadState>('idle');
const [coreMessage, setCoreMessage] = useState('');
const [state, setState] = useState<LoadState>('idle');
const [error, setError] = useState('');
const loadedDataKeysRef = useRef(new Set<DataKey>());
const loadingDataKeysRef = useRef(new Set<DataKey>());
const loadedTaskQueryKeyRef = useRef('');
const currentTaskQueryKeyRef = useRef('');
const { removeBaseModel, removeProvider, resetAllBaseModelsToDefault, resetBaseModelToDefault, saveBaseModel, saveProvider } = useCatalogOperations({
setBaseModels,
setCoreMessage,
@@ -185,13 +200,15 @@ export function App() {
setRuntimePolicySets,
token,
});
const taskListRequestKey = workspaceTaskQueryKey(workspaceTaskQuery);
currentTaskQueryKeyRef.current = taskListRequestKey;
useEffect(() => {
void ensureData(['health']);
}, []);
useEffect(() => {
void ensureRouteData(token);
}, [activePage, adminSection, workspaceSection, token]);
}, [activePage, adminSection, taskListRequestKey, workspaceSection, token]);
useEffect(() => {
function handlePopState() {
applyRoute(parseAppRoute());
@@ -223,6 +240,7 @@ export function App() {
accessRules,
apiKeys,
baseModels,
modelCatalog,
models,
platforms,
pricingRules,
@@ -235,7 +253,7 @@ export function App() {
tenants,
userGroups,
users,
}), [accessRules, apiKeys, baseModels, models, platforms, pricingRuleSets, pricingRules, providers, rateLimitWindows, runtimePolicySets, taskResult, tasks, tenants, userGroups, users]);
}), [accessRules, apiKeys, baseModels, modelCatalog, models, platforms, pricingRuleSets, pricingRules, providers, rateLimitWindows, runtimePolicySets, taskResult, tasks, tenants, userGroups, users]);
async function refresh(nextToken = token) {
await ensureRouteData(nextToken, true);
@@ -246,6 +264,10 @@ export function App() {
}
async function ensureRouteData(nextToken = token, force = false) {
if (activePage === 'workspace' && workspaceSection === 'tasks' && loadedTaskQueryKeyRef.current !== taskListRequestKey) {
loadedDataKeysRef.current.delete('tasks');
loadingDataKeysRef.current.delete('tasks');
}
await ensureData(dataKeysForRoute(activePage, adminSection, workspaceSection, Boolean(nextToken)), nextToken, force);
}
@@ -294,6 +316,9 @@ export function App() {
case 'models':
setModels((await listModels(nextToken)).items);
return;
case 'modelCatalog':
setModelCatalog(await listModelCatalog(nextToken));
return;
case 'playgroundModels':
setPlaygroundModels((await listPlayableModels(nextToken)).items);
return;
@@ -332,7 +357,14 @@ export function App() {
setUserGroups((await listUserGroups(nextToken)).items);
return;
case 'tasks':
setTasks((await listTasks(nextToken)).items);
{
const requestKey = taskListRequestKey;
const response = await listTasks(nextToken, workspaceTaskQuery);
if (requestKey !== currentTaskQueryKeyRef.current) return;
setTasks(response.items);
setTaskTotal(response.total ?? response.items.length);
loadedTaskQueryKeyRef.current = requestKey;
}
return;
case 'accessRules':
setAccessRules((await (activePage === 'workspace' && workspaceSection === 'apiKeys'
@@ -420,6 +452,7 @@ export function App() {
const modelsResponse = await replacePlatformModels(token, platform.id, modelBindings);
setPlatforms((current) => [platformForState, ...current.filter((item) => item.id !== platform.id)]);
setModels((current) => [...current.filter((model) => model.platformId !== platform.id), ...modelsResponse.items]);
invalidateDataKeys('modelCatalog');
setCoreState('ready');
setCoreMessage(input.platformId
? `平台已更新,当前绑定 ${input.models.length} 个模型。`
@@ -438,6 +471,7 @@ export function App() {
await deletePlatform(token, platformId);
setPlatforms((current) => current.filter((item) => item.id !== platformId));
setModels((current) => current.filter((item) => item.platformId !== platformId));
invalidateDataKeys('modelCatalog');
setCoreState('ready');
setCoreMessage('平台已删除。');
} catch (err) {
@@ -514,6 +548,7 @@ export function App() {
try {
const item = groupId ? await updateUserGroup(token, groupId, input) : await createUserGroup(token, input);
setUserGroups((current) => [item, ...current.filter((group) => group.id !== item.id)]);
invalidateDataKeys('modelCatalog');
setCoreState('ready');
setCoreMessage(groupId ? '用户组已更新。' : '用户组已创建。');
} catch (err) {
@@ -531,6 +566,7 @@ export function App() {
setUserGroups((current) => current.filter((group) => group.id !== groupId));
setTenants((current) => current.map((tenant) => tenant.defaultUserGroupId === groupId ? { ...tenant, defaultUserGroupId: undefined } : tenant));
setUsers((current) => current.map((user) => user.defaultUserGroupId === groupId ? { ...user, defaultUserGroupId: undefined } : user));
invalidateDataKeys('modelCatalog');
invalidateDataKeys('playgroundModels');
setCoreState('ready');
setCoreMessage('用户组已删除。');
@@ -569,7 +605,7 @@ export function App() {
try {
const item = ruleId ? await updateAccessRule(token, ruleId, input) : await createAccessRule(token, input);
setAccessRules((current) => [item, ...current.filter((rule) => rule.id !== item.id)]);
invalidateDataKeys('playgroundModels');
invalidateDataKeys('playgroundModels', 'modelCatalog');
setCoreState('ready');
setCoreMessage(ruleId ? '访问权限规则已更新。' : '访问权限规则已创建。');
} catch (err) {
@@ -585,7 +621,7 @@ export function App() {
try {
await deleteAccessRule(token, ruleId);
setAccessRules((current) => current.filter((rule) => rule.id !== ruleId));
invalidateDataKeys('playgroundModels');
invalidateDataKeys('playgroundModels', 'modelCatalog');
setCoreState('ready');
setCoreMessage('访问权限规则已删除。');
} catch (err) {
@@ -601,7 +637,7 @@ export function App() {
try {
const response = await batchAccessRules(token, input);
setAccessRules(response.items);
invalidateDataKeys('playgroundModels');
invalidateDataKeys('playgroundModels', 'modelCatalog');
setCoreState('ready');
setCoreMessage('访问权限已更新。');
} catch (err) {
@@ -653,6 +689,7 @@ export function App() {
setState('idle');
setPlatforms([]);
setModels([]);
setModelCatalog({ items: [], filters: { capabilities: [], providers: [] }, summary: { modelCount: 0, sourceCount: 0 } });
setPlaygroundModels([]);
setProviders([]);
setBaseModels([]);
@@ -670,6 +707,7 @@ export function App() {
setSelectedPlaygroundApiKeyId('');
setTaskResult(null);
setTasks([]);
setTaskTotal(0);
setCoreMessage('');
navigatePath('/');
}
@@ -680,7 +718,7 @@ export function App() {
}
function currentRouteState(): AppRouteState {
return { activePage, adminSection, apiDocSection, playgroundMode, workspaceSection };
return { activePage, adminSection, apiDocSection, playgroundMode, workspaceSection, workspaceTaskQuery };
}
function applyRoute(route: AppRouteState) {
@@ -689,10 +727,11 @@ export function App() {
setApiDocSection(route.apiDocSection);
setPlaygroundMode(route.playgroundMode);
setWorkspaceSection(route.workspaceSection);
setWorkspaceTaskQuery(route.workspaceTaskQuery);
}
function navigatePath(path: string) {
if (window.location.pathname !== path) {
if (`${window.location.pathname}${window.location.search}` !== path) {
window.history.pushState(null, '', path);
}
applyRoute(parseAppRoute(path));
@@ -710,6 +749,10 @@ export function App() {
navigatePath(pathForWorkspaceSection(section));
}
function navigateWorkspaceTaskQuery(query: WorkspaceTaskQuery) {
navigatePath(pathForWorkspaceTaskQuery(query));
}
function navigateApiDocSection(section: ApiDocSection) {
navigatePath(pathForApiDocSection(section));
}
@@ -768,11 +811,14 @@ export function App() {
message={coreMessage}
section={workspaceSection}
state={coreState}
taskQuery={workspaceTaskQuery}
taskTotal={taskTotal}
onBatchAccessRules={batchSaveAPIKeyAccessRules}
onDeleteApiKey={removeAPIKey}
onApiKeyFormChange={setApiKeyForm}
onSectionChange={navigateWorkspaceSection}
onSubmitApiKey={submitAPIKey}
onTaskQueryChange={navigateWorkspaceTaskQuery}
onUseApiKeyForPlayground={useApiKeyForPlayground}
/>
) : (
@@ -877,6 +923,7 @@ function mergeExistingPlatformModelInput(input: PlatformModelBindingInput, curre
if (!existing) return input;
return {
...input,
providerModelName: input.providerModelName ?? existing.providerModelName,
discountFactor: (input.discountFactor ?? existing.discountFactor) || undefined,
pricingRuleSetId: input.pricingRuleSetId ?? existing.pricingRuleSetId,
rateLimitPolicy: input.rateLimitPolicy ?? existing.rateLimitPolicy,
@@ -933,7 +980,11 @@ function dataKeysForRoute(
isAuthenticated: boolean,
): DataKey[] {
if (activePage === 'playground') return isAuthenticated ? ['playgroundModels', 'playgroundApiKeys'] : [];
if (activePage === 'models') return ['publicCatalog'];
if (activePage === 'models') {
return isAuthenticated
? ['modelCatalog']
: ['publicCatalog'];
}
if (activePage === 'home' || activePage === 'docs') return [];
if (!isAuthenticated) return [];