fix gateway loopback validation chains
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)),
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user