fix gateway loopback validation chains

This commit is contained in:
2026-05-11 08:48:02 +08:00
parent ff666b1ece
commit ca7e76e815
42 changed files with 1641 additions and 129 deletions
+3 -3
View File
@@ -173,8 +173,8 @@ function identityPanelProps(props: {
function OverviewPanel(props: { data: ConsoleData; stats: StatItem[] }) {
const enabledPlatforms = props.data.platforms.filter((item) => item.status === 'enabled');
const chatModels = props.data.models.filter((item) => item.modelType === 'chat' && item.enabled);
const imageModels = props.data.models.filter((item) => item.modelType === 'image' && item.enabled);
const chatModels = props.data.models.filter((item) => item.modelType.includes('text_generate') && item.enabled);
const imageModels = props.data.models.filter((item) => item.modelType.some((type) => type.includes('image')) && item.enabled);
return (
<div className="pageStack">
@@ -229,7 +229,7 @@ function OverviewPanel(props: { data: ConsoleData; stats: StatItem[] }) {
rows={[
['对话', chatModels.length, 'Phase 1', '/v1/chat/completions'],
['图像', imageModels.length, 'Phase 1', '/v1/images/*'],
['视频', props.data.models.filter((item) => item.modelType === 'video').length, 'Next', '/v1/videos/*'],
['视频', props.data.models.filter((item) => item.modelType.some((type) => type.includes('video'))).length, 'Next', '/v1/videos/*'],
]}
/>
</CardContent>
+25 -9
View File
@@ -9,7 +9,7 @@ import type {
import type { ConsoleData } from '../app-state';
import { PageHeader } from '../components/PageHeader';
import { Badge, Card, CardContent, Input } from '../components/ui';
import { primaryBaseModelType, stableModelAlias } from './admin/platform-form';
import { stableModelAlias } from './admin/platform-form';
type ModelListItem = {
id: string;
@@ -17,7 +17,7 @@ type ModelListItem = {
platformName?: string;
modelName: string;
modelAlias?: string;
modelType: string;
modelType: string[];
displayName: string;
capabilities?: Record<string, unknown>;
pricingMode: string;
@@ -69,7 +69,7 @@ const publicModels: PlatformModel[] = [
platformName: 'OpenAI Simulation',
modelName: 'gpt-4o-mini',
modelAlias: 'gpt-4o-mini',
modelType: 'chat',
modelType: ['text_generate'],
displayName: 'gpt-4o-mini',
capabilities: { multimodal: true },
pricingMode: 'inherit',
@@ -84,7 +84,7 @@ const publicModels: PlatformModel[] = [
platformName: 'OpenAI Simulation',
modelName: 'gpt-image-1',
modelAlias: 'gpt-image-1',
modelType: 'image',
modelType: ['image_generate', 'image_edit'],
displayName: 'gpt-image-1',
capabilities: { imageEdit: true },
pricingMode: 'inherit',
@@ -99,7 +99,7 @@ const publicModels: PlatformModel[] = [
platformName: 'Gemini Simulation',
modelName: 'gemini-2.0-flash',
modelAlias: 'gemini-2.0-flash',
modelType: 'chat',
modelType: ['text_generate'],
displayName: 'gemini-2.0-flash',
capabilities: { multimodal: true, vision: true },
pricingMode: 'inherit_discount',
@@ -148,7 +148,7 @@ export function ModelsPage(props: { data: ConsoleData }) {
return sourceModels.filter((model) => {
const providerInfo = providerMap.get(model.providerKey);
const matchedProvider = provider === 'all' || model.providerKey === provider;
const matchedCapability = capability === 'all' || model.modelType === capability;
const matchedCapability = modelMatchesCapability(model.modelType, capability);
const matchedQuery = [
model.modelName,
model.modelAlias,
@@ -331,7 +331,7 @@ function modelFromBaseModel(model: BaseModelCatalogItem): ModelListItem {
providerKey: model.providerKey,
modelName: model.providerModelName,
modelAlias: stableModelAlias(model),
modelType: primaryBaseModelType(model),
modelType: model.modelType,
displayName: stableModelAlias(model),
capabilities: model.capabilities,
pricingMode: 'inherit',
@@ -363,7 +363,7 @@ function providerInitials(label: string) {
}
function tagsForModel(model: ModelListItem) {
const tags = [capabilityName(model.modelType)];
const tags = model.modelType.map(capabilityName);
const capabilities = model.capabilities ?? {};
if (capabilities.multimodal || capabilities.vision) tags.push('多模态');
if (capabilities.reasoning) tags.push('推理');
@@ -373,7 +373,23 @@ function tagsForModel(model: ModelListItem) {
}
function capabilityName(type: string) {
return capabilityFilters.find((item) => item.value === type)?.label ?? type;
const labels: Record<string, string> = {
text_generate: '对话',
image_generate: '绘图',
image_edit: '图像编辑',
video_generate: '视频',
image_to_video: '图生视频',
audio_generate: '音频',
};
return labels[type] ?? capabilityFilters.find((item) => item.value === type)?.label ?? type;
}
function modelMatchesCapability(modelTypes: string[], capability: string) {
if (capability === 'all') return true;
if (capability === 'chat') return modelTypes.includes('text_generate') || modelTypes.includes('chat');
if (capability === 'image') return modelTypes.some((type) => type.includes('image'));
if (capability === 'video') return modelTypes.some((type) => type.includes('video'));
return modelTypes.includes(capability);
}
function priceLabel(model: ModelListItem) {
+2 -2
View File
@@ -988,8 +988,8 @@ function filterModelsForMode(models: PlatformModel[], mode: PlaygroundMode, hasR
}
function filterWithFallback(models: PlatformModel[], modelTypes: string[]) {
const exact = models.filter((model) => modelTypes.includes(model.modelType));
return exact.length ? exact : models.filter((model) => modelTypes.some((type) => model.modelType.includes(type) || type.includes(model.modelType)));
const exact = models.filter((model) => model.modelType.some((type) => modelTypes.includes(type)));
return exact.length ? exact : models.filter((model) => modelTypes.some((type) => model.modelType.some((modelType) => modelType.includes(type) || type.includes(modelType))));
}
function buildModelOptions(models: PlatformModel[]): ModelOption[] {
+39 -19
View File
@@ -1,6 +1,6 @@
import { useMemo, useState, type FormEvent, type ReactNode } from 'react';
import { Copy, CreditCard, KeyRound, ListChecks, Plus, ShieldCheck, Trash2, UserRound } from 'lucide-react';
import type { GatewayAccessRuleBatchRequest, GatewayApiKey, IntegrationPlatform, PlatformModel } from '@easyai-ai-gateway/contracts';
import type { GatewayAccessRuleBatchRequest, GatewayApiKey, GatewayTask, IntegrationPlatform, PlatformModel } from '@easyai-ai-gateway/contracts';
import type { ConsoleData } from '../app-state';
import { EntityTable } from '../components/EntityTable';
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, ConfirmDialog, DateTimePicker, FormDialog, Input, Label, Table, TableCell, TableHead, TableRow, Tabs } from '../components/ui';
@@ -297,30 +297,22 @@ function ApiKeyPanel(props: {
}
function TaskPanel(props: { data: ConsoleData }) {
const task = props.data.taskResult;
const usage = task?.usage ?? {};
const tokenText = usage.totalTokens ? `${usage.totalTokens}` : '-';
const chargeText = task?.finalChargeAmount ? `${task.finalChargeAmount}` : '-';
const tasks = useMemo(() => {
const latest = props.data.taskResult;
if (!latest) return props.data.tasks;
return [latest, ...props.data.tasks.filter((item) => item.id !== latest.id)];
}, [props.data.taskResult, props.data.tasks]);
return (
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
{task ? (
<div className="taskPreview">
<Badge variant={task.status === 'succeeded' ? 'success' : 'secondary'}>{task.status}</Badge>
<strong>{task.kind}</strong>
<span>{task.model}</span>
<div className="infoGrid compact">
<InfoItem label="API Key" value={task.apiKeyName || task.apiKeyId || '-'} />
<InfoItem label="RequestID" value={task.requestId || '-'} />
<InfoItem label="实际模型" value={task.resolvedModel || task.model} />
<InfoItem label="Token" value={tokenText} />
<InfoItem label="扣费" value={chargeText} />
<InfoItem label="响应耗时" value={task.responseDurationMs ? `${task.responseDurationMs}ms` : '-'} />
</div>
<pre>{JSON.stringify({ result: task.result, usage: task.usage, billings: task.billings, billingSummary: task.billingSummary, metrics: task.metrics }, null, 2)}</pre>
{tasks.length ? (
<div className="taskList">
{tasks.map((task) => (
<TaskRecord key={task.id} task={task} />
))}
</div>
) : (
<div className="emptyState">
@@ -332,6 +324,34 @@ function TaskPanel(props: { data: ConsoleData }) {
);
}
function TaskRecord(props: { task: GatewayTask }) {
const usage = props.task.usage ?? {};
const tokenText = usage.totalTokens ? `${usage.totalTokens}` : '-';
const chargeText = props.task.finalChargeAmount !== undefined ? `${props.task.finalChargeAmount}` : '-';
const badgeVariant = props.task.status === 'succeeded' ? 'success' : props.task.status === 'failed' ? 'destructive' : 'secondary';
return (
<div className="taskPreview">
<div className="taskRecordHeader">
<Badge variant={badgeVariant}>{props.task.status}</Badge>
<strong>{props.task.kind}</strong>
<span>{props.task.model}</span>
<span>{formatDateTime(props.task.createdAt)}</span>
</div>
<div className="infoGrid compact">
<InfoItem label="API Key" value={props.task.apiKeyName || props.task.apiKeyId || '-'} />
<InfoItem label="RequestID" value={props.task.requestId || '-'} />
<InfoItem label="模型类型" value={props.task.modelType || '-'} />
<InfoItem label="实际模型" value={props.task.resolvedModel || props.task.model} />
<InfoItem label="Token" value={tokenText} />
<InfoItem label="扣费" value={chargeText} />
<InfoItem label="响应耗时" value={props.task.responseDurationMs ? `${props.task.responseDurationMs}ms` : '-'} />
<InfoItem label="错误" value={props.task.errorCode || props.task.errorMessage || '-'} />
</div>
<pre>{JSON.stringify({ result: props.task.result, usage: props.task.usage, billings: props.task.billings, billingSummary: props.task.billingSummary, metrics: props.task.metrics }, null, 2)}</pre>
</div>
);
}
function InfoItem(props: { label: string; value: string }) {
return (
<div className="infoItem">
@@ -304,7 +304,7 @@ function buildPlatformTree(platforms: IntegrationPlatform[], platformModels: Pla
.map((model) => ({
id: model.id,
name: modelLabel(model),
subtitle: `${model.modelType} / ${model.modelName}`,
subtitle: `${model.modelType.join(', ')} / ${model.modelName}`,
})),
})).sort((a, b) => a.name.localeCompare(b.name));
}
@@ -59,7 +59,7 @@ export function PlatformManagementPanel(props: {
model.displayName,
model.modelName,
model.modelAlias,
model.modelType,
...model.modelType,
model.provider,
platform?.name,
platform?.internalName,
@@ -473,7 +473,7 @@ function PlatformModelTable(props: {
<ModelCatalogCard
key={model.id}
badges={[
<Badge variant="outline">{model.modelType}</Badge>,
<Badge variant="outline">{model.modelType.join(', ')}</Badge>,
<Badge variant={model.enabled ? 'success' : 'secondary'}>{model.enabled ? 'enabled' : 'disabled'}</Badge>,
]}
chips={platformModelChips(model)}
@@ -889,7 +889,7 @@ function findBaseModelForPlatformModel(platform: IntegrationPlatform | undefined
return baseModels.find((item) => item.id === model.baseModelId) ??
baseModels.find((item) => item.canonicalModelKey === model.modelAlias) ??
baseModels.find((item) => stableModelAlias(item) === model.modelAlias) ??
baseModels.find((item) => item.providerKey === platform?.provider && item.providerModelName === model.modelName && baseModelTypes(item).includes(model.modelType));
baseModels.find((item) => item.providerKey === platform?.provider && item.providerModelName === model.modelName && model.modelType.some((type) => baseModelTypes(item).includes(type)));
}
function readPlatformModelIconPath(model: PlatformModel, baseModel?: BaseModelCatalogItem) {
+1 -1
View File
@@ -149,7 +149,7 @@ export function platformModelPayloads(models: BaseModelCatalogItem[], form: Plat
canonicalModelKey: model.canonicalModelKey,
modelName: model.providerModelName,
modelAlias: stableModelAlias(model),
modelType: primaryBaseModelType(model),
modelType: baseModelTypes(model),
displayName: stableModelAlias(model) || model.providerModelName,
pricingMode: 'inherit_discount',
discountFactor: optionalPositiveNumber(form.modelDiscountFactors[model.id]) ?? optionalPositiveNumber(form.modelDiscountFactor),
+1 -1
View File
@@ -638,7 +638,7 @@ function capabilityTypeKeys(
: [contextKey, 'text_to_video', 'image_to_video', 'omni_video', 'video_generate', 'video'];
return uniqueStrings([
...stringListFromCapability(source.originalTypes),
model.modelType,
...model.modelType,
...modeTypeHints.filter((item): item is string => Boolean(item)),
]);
}