fix: polish parameter preprocessing details
This commit is contained in:
@@ -359,15 +359,16 @@ func (contentFilterProcessor) Process(params map[string]any, modelType string, c
|
|||||||
next := make([]map[string]any, 0, len(content))
|
next := make([]map[string]any, 0, len(content))
|
||||||
for index, item := range content {
|
for index, item := range content {
|
||||||
if isImageContent(item) {
|
if isImageContent(item) {
|
||||||
|
reason, path, value := imageContentRemovalEvidence(item, modelType, context)
|
||||||
context.recordChange(
|
context.recordChange(
|
||||||
"ContentFilterProcessor",
|
"ContentFilterProcessor",
|
||||||
"remove",
|
"remove",
|
||||||
fmt.Sprintf("content[%d]", index),
|
fmt.Sprintf("content[%d]", index),
|
||||||
item,
|
item,
|
||||||
nil,
|
nil,
|
||||||
"当前候选模型没有图像参考输入模式,需移除 image_url。",
|
reason,
|
||||||
capabilityPath(modelType, ""),
|
path,
|
||||||
capabilityForType(context.modelCapability, modelType),
|
value,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -408,6 +409,38 @@ func (contentFilterProcessor) Process(params map[string]any, modelType string, c
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func imageContentRemovalEvidence(item map[string]any, modelType string, context *paramProcessContext) (string, string, any) {
|
||||||
|
role := stringFromAny(item["role"])
|
||||||
|
switch role {
|
||||||
|
case "first_frame":
|
||||||
|
return "模型能力未开启首帧输入,已移除 first_frame。", capabilityPath(modelType, "input_first_frame"), map[string]any{
|
||||||
|
"input_first_frame": capabilityValue(context.modelCapability, modelType, "input_first_frame"),
|
||||||
|
"input_first_last_frame": capabilityValue(context.modelCapability, modelType, "input_first_last_frame"),
|
||||||
|
}
|
||||||
|
case "last_frame":
|
||||||
|
return "模型能力未开启尾帧或首尾帧输入,已移除 last_frame。", capabilityPath(modelType, "input_first_last_frame"), map[string]any{
|
||||||
|
"input_last_frame": capabilityValue(context.modelCapability, modelType, "input_last_frame"),
|
||||||
|
"input_first_last_frame": capabilityValue(context.modelCapability, modelType, "input_first_last_frame"),
|
||||||
|
"max_images_for_last_frame": capabilityValue(context.modelCapability, modelType, "max_images_for_last_frame"),
|
||||||
|
"max_images_for_first_frame": capabilityValue(context.modelCapability, modelType, "max_images_for_first_frame"),
|
||||||
|
"max_images_for_middle_frame": capabilityValue(context.modelCapability, modelType, "max_images_for_middle_frame"),
|
||||||
|
}
|
||||||
|
case "reference_image":
|
||||||
|
return "模型能力未开启参考图输入,已移除 reference_image。", capabilityPath(modelType, "input_reference_generate_single"), map[string]any{
|
||||||
|
"input_reference_generate_single": capabilityValue(context.modelCapability, modelType, "input_reference_generate_single"),
|
||||||
|
"input_reference_generate_multiple": capabilityValue(context.modelCapability, modelType, "input_reference_generate_multiple"),
|
||||||
|
"max_images": capabilityValue(context.modelCapability, modelType, "max_images"),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return "当前模型能力未开启图像输入,已移除 image_url。", capabilityPath(modelType, "input_first_frame"), map[string]any{
|
||||||
|
"input_first_frame": capabilityValue(context.modelCapability, modelType, "input_first_frame"),
|
||||||
|
"input_first_last_frame": capabilityValue(context.modelCapability, modelType, "input_first_last_frame"),
|
||||||
|
"input_reference_generate_single": capabilityValue(context.modelCapability, modelType, "input_reference_generate_single"),
|
||||||
|
"input_reference_generate_multiple": capabilityValue(context.modelCapability, modelType, "input_reference_generate_multiple"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type inputAudioProcessor struct{}
|
type inputAudioProcessor struct{}
|
||||||
|
|
||||||
func (inputAudioProcessor) Name() string { return "InputAudioProcessor" }
|
func (inputAudioProcessor) Name() string { return "InputAudioProcessor" }
|
||||||
|
|||||||
@@ -180,6 +180,44 @@ func TestParamProcessorVideoCapabilitiesNormalizeAndFilter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParamProcessorVideoGenerateLogsFirstFrameRemoval(t *testing.T) {
|
||||||
|
body := map[string]any{
|
||||||
|
"model": "Seedance T2V",
|
||||||
|
"prompt": "animate it",
|
||||||
|
"content": []any{
|
||||||
|
map[string]any{"type": "text", "text": "animate it"},
|
||||||
|
map[string]any{"type": "image_url", "role": "first_frame", "image_url": "https://example.com/first.png"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
candidate := store.RuntimeModelCandidate{
|
||||||
|
ModelType: "video_generate",
|
||||||
|
Capabilities: map[string]any{
|
||||||
|
"video_generate": map[string]any{
|
||||||
|
"duration_range": []any{3, 12},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := preprocessRequestWithLog("videos.generations", body, candidate)
|
||||||
|
for _, item := range contentItems(result.Body["content"]) {
|
||||||
|
if isImageContent(item) {
|
||||||
|
t.Fatalf("first frame image should be removed for video_generate: %+v", result.Body["content"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, change := range result.Log.Changes {
|
||||||
|
if change.Path == "content[1]" {
|
||||||
|
if change.Reason != "模型能力未开启首帧输入,已移除 first_frame。" {
|
||||||
|
t.Fatalf("unexpected first frame removal reason: %+v", change)
|
||||||
|
}
|
||||||
|
if change.CapabilityPath != "capabilities.video_generate.input_first_frame" {
|
||||||
|
t.Fatalf("unexpected first frame capability path: %+v", change)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("expected first frame removal log, got %+v", result.Log.Changes)
|
||||||
|
}
|
||||||
|
|
||||||
func TestParamProcessorImageResolutionAndOutputCount(t *testing.T) {
|
func TestParamProcessorImageResolutionAndOutputCount(t *testing.T) {
|
||||||
body := map[string]any{
|
body := map[string]any{
|
||||||
"model": "即梦V4.0",
|
"model": "即梦V4.0",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState, type FormEvent, type ReactNode } from 'react';
|
import { useEffect, useMemo, useRef, useState, type FormEvent, type ReactNode } from 'react';
|
||||||
import { Popover as AntPopover } from 'antd';
|
import { Popover as AntPopover } from 'antd';
|
||||||
import { ChevronLeft, ChevronRight, Copy, CreditCard, Eye, KeyRound, ListChecks, Plus, ReceiptText, RotateCcw, Search, ShieldCheck, SlidersHorizontal, Trash2, UserRound } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, Copy, CreditCard, Eye, KeyRound, ListChecks, Plus, ReceiptText, RotateCcw, Search, ShieldCheck, SlidersHorizontal, Trash2, UserRound } from 'lucide-react';
|
||||||
import type { GatewayAccessRuleBatchRequest, GatewayApiKey, GatewayTask, GatewayTaskParamPreprocessingLog, GatewayWalletAccount, GatewayWalletTransaction, IntegrationPlatform, PlatformModel } from '@easyai-ai-gateway/contracts';
|
import type { GatewayAccessRuleBatchRequest, GatewayApiKey, GatewayTask, GatewayTaskParamPreprocessingLog, GatewayWalletAccount, GatewayWalletTransaction, IntegrationPlatform, PlatformModel } from '@easyai-ai-gateway/contracts';
|
||||||
@@ -838,27 +838,46 @@ function TaskParamConversionCell(props: { task: GatewayTask; token: string }) {
|
|||||||
const [logs, setLogs] = useState<GatewayTaskParamPreprocessingLog[] | null>(null);
|
const [logs, setLogs] = useState<GatewayTaskParamPreprocessingLog[] | null>(null);
|
||||||
const [loadState, setLoadState] = useState<'idle' | 'loading' | 'ready' | 'error'>('idle');
|
const [loadState, setLoadState] = useState<'idle' | 'loading' | 'ready' | 'error'>('idle');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const logsRef = useRef<GatewayTaskParamPreprocessingLog[] | null>(null);
|
||||||
|
const loadingRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !summary.changed || logs || loadState === 'loading' || !props.token) return;
|
logsRef.current = null;
|
||||||
|
loadingRef.current = false;
|
||||||
|
setLogs(null);
|
||||||
|
setLoadState('idle');
|
||||||
|
setError('');
|
||||||
|
}, [props.task.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !summary.changed || logsRef.current || loadingRef.current || !props.token) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
loadingRef.current = true;
|
||||||
setLoadState('loading');
|
setLoadState('loading');
|
||||||
setError('');
|
setError('');
|
||||||
listTaskParamPreprocessing(props.token, props.task.id)
|
listTaskParamPreprocessing(props.token, props.task.id)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setLogs(response.items ?? []);
|
const items = response.items ?? [];
|
||||||
|
logsRef.current = items;
|
||||||
|
setLogs(items);
|
||||||
setLoadState('ready');
|
setLoadState('ready');
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setError(err instanceof Error ? err.message : '参数转换明细加载失败');
|
setError(err instanceof Error ? err.message : '参数转换明细加载失败');
|
||||||
setLoadState('error');
|
setLoadState('error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) {
|
||||||
|
loadingRef.current = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
loadingRef.current = false;
|
||||||
};
|
};
|
||||||
}, [loadState, logs, open, props.task.id, props.token, summary.changed]);
|
}, [open, props.task.id, props.token, summary.changed]);
|
||||||
|
|
||||||
if (!summary.changed) {
|
if (!summary.changed) {
|
||||||
return <span className="taskParamConversionEmpty">无转换</span>;
|
return <span className="taskParamConversionEmpty">无转换</span>;
|
||||||
@@ -900,8 +919,11 @@ function TaskParamConversionPopover(props: {
|
|||||||
{logs.map((log) => (
|
{logs.map((log) => (
|
||||||
<span key={log.id} className="taskParamConversionLog">
|
<span key={log.id} className="taskParamConversionLog">
|
||||||
<span className="taskParamConversionLogHeader">
|
<span className="taskParamConversionLogHeader">
|
||||||
<strong>{taskParamLogTitle(log)}</strong>
|
<span className="taskParamConversionLogTitleBlock">
|
||||||
<Badge variant={log.changed ? 'secondary' : 'outline'}>{log.changeCount} 项</Badge>
|
<strong>{taskParamLogTitle(log)}</strong>
|
||||||
|
<small title={log.clientId || undefined}>{taskParamLogSubtitle(log)}</small>
|
||||||
|
</span>
|
||||||
|
<Badge className="taskParamConversionCountBadge" variant={log.changed ? 'secondary' : 'outline'}>{log.changeCount} 项</Badge>
|
||||||
</span>
|
</span>
|
||||||
{(log.changes ?? []).slice(0, 8).map((change, index) => (
|
{(log.changes ?? []).slice(0, 8).map((change, index) => (
|
||||||
<span key={`${log.id}-change-${index}`} className="taskParamConversionChange">
|
<span key={`${log.id}-change-${index}`} className="taskParamConversionChange">
|
||||||
@@ -1116,9 +1138,16 @@ function taskParamConversionSummary(task: GatewayTask): TaskParamConversionSumma
|
|||||||
paths: [],
|
paths: [],
|
||||||
capabilityPaths: [],
|
capabilityPaths: [],
|
||||||
};
|
};
|
||||||
mergeTaskParamSummary(summary, metadataObject(task.metrics, 'parameterPreprocessingSummary'));
|
let mergedAttemptSummary = false;
|
||||||
for (const attempt of task.attempts ?? []) {
|
for (const attempt of task.attempts ?? []) {
|
||||||
mergeTaskParamSummary(summary, metadataObject(attempt.metrics, 'parameterPreprocessingSummary'));
|
const attemptSummary = metadataObject(attempt.metrics, 'parameterPreprocessingSummary');
|
||||||
|
if (Object.keys(attemptSummary).length) {
|
||||||
|
mergedAttemptSummary = true;
|
||||||
|
mergeTaskParamSummary(summary, attemptSummary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mergedAttemptSummary) {
|
||||||
|
mergeTaskParamSummary(summary, metadataObject(task.metrics, 'parameterPreprocessingSummary'));
|
||||||
}
|
}
|
||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
@@ -1147,11 +1176,30 @@ function taskParamLogTitle(log: GatewayTaskParamPreprocessingLog) {
|
|||||||
const parts = [
|
const parts = [
|
||||||
log.attemptNo ? `#${log.attemptNo}` : '',
|
log.attemptNo ? `#${log.attemptNo}` : '',
|
||||||
log.modelType || '',
|
log.modelType || '',
|
||||||
log.clientId || '',
|
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
return parts.join(' · ') || '预处理记录';
|
return parts.join(' · ') || '预处理记录';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function taskParamLogSubtitle(log: GatewayTaskParamPreprocessingLog) {
|
||||||
|
const model = log.model ?? {};
|
||||||
|
const modelLabel = firstNonEmptyText(
|
||||||
|
objectString(model, 'modelAlias'),
|
||||||
|
objectString(model, 'providerModelName'),
|
||||||
|
objectString(model, 'modelName'),
|
||||||
|
);
|
||||||
|
const provider = objectString(model, 'provider');
|
||||||
|
const parts = [
|
||||||
|
provider,
|
||||||
|
modelLabel,
|
||||||
|
].filter(Boolean);
|
||||||
|
if (parts.length) return parts.join(' · ');
|
||||||
|
return log.clientId || '候选模型';
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstNonEmptyText(...values: string[]) {
|
||||||
|
return values.find((value) => value.trim()) ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
function taskParamActionLabel(action: string) {
|
function taskParamActionLabel(action: string) {
|
||||||
if (action === 'remove') return '移除';
|
if (action === 'remove') return '移除';
|
||||||
if (action === 'adjust') return '调整';
|
if (action === 'adjust') return '调整';
|
||||||
|
|||||||
@@ -521,6 +521,36 @@ strong {
|
|||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.taskParamConversionLogHeader {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskParamConversionLogTitleBlock {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskParamConversionLogTitleBlock strong {
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskParamConversionLogTitleBlock small {
|
||||||
|
max-width: 32rem;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-soft);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
line-height: 1.35;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskParamConversionCountBadge {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.taskParamConversionChange {
|
.taskParamConversionChange {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.28rem;
|
gap: 0.28rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user