feat: improve model catalog aggregation
This commit is contained in:
+60
-9
@@ -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 [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user