From 4f123ea2722e514aa160a90c601091bae639197c Mon Sep 17 00:00:00 2001 From: WuYongJie <549727955@qq.com> Date: Fri, 16 Oct 2026 14:25:35 +0800 Subject: [PATCH] fix(ai): bind image parameters and expose actual request settings --- .../admin/AiModelManagementPanel.test.tsx | 28 ++++++++ components/admin/AiModelManagementPanel.tsx | 4 ++ docs/ai-model-contracts-2026-10-16.md | 53 +++++++-------- docs/ai-model-management.md | 6 ++ lib/__tests__/ai-model-capabilities.test.ts | 7 ++ lib/__tests__/ai-model-protocol-http.test.ts | 8 +++ lib/__tests__/ai-model-protocol.test.ts | 68 +++++++++++++++++++ lib/ai-model-capabilities.ts | 7 +- lib/ai-model-http.ts | 3 +- lib/ai-model-protocol.ts | 51 ++++++++++---- lib/ai-model-types.ts | 2 +- 11 files changed, 190 insertions(+), 47 deletions(-) diff --git a/components/admin/AiModelManagementPanel.test.tsx b/components/admin/AiModelManagementPanel.test.tsx index b07731f..abbfb15 100644 --- a/components/admin/AiModelManagementPanel.test.tsx +++ b/components/admin/AiModelManagementPanel.test.tsx @@ -19,6 +19,34 @@ const configured = (): AiModelAdminState => ({ afterEach(() => jest.restoreAllMocks()) beforeEach(() => sessionStorage.clear()) +test('展开详情显示服务端实际请求参数和参考图传法,不会调用或修改模型', async () => { + const next = configured(), model = next.providers[0].models[1] + model.capabilities.protocols = { image: { id: 'async-image-task', path: '/extensions/v1/tasks', inputField: 'file_urls', referenceTransport: 'json-inline', maxPromptLength: 32000, requestParameters: { size: 'auto', resolution: '1K', aspect_ratio: '3:4' } } } + global.fetch = jest.fn(async () => response(next)) as typeof fetch + render() + const row = await screen.findByLabelText('nano-banana-2 · 现有上游', { selector: 'div' }) + fireEvent.click(within(row).getByRole('button', { name: 'nano-banana-2 · 现有上游 图片生成测试详情' })) + const parameters = within(row).getByLabelText('nano-banana-2 当前请求参数') + expect(within(parameters).getByText('尺寸')).toBeVisible() + expect(within(parameters).getByText('auto')).toBeVisible() + expect(within(parameters).getByText('1K')).toBeVisible() + expect(within(parameters).getByText('3:4')).toBeVisible() + expect(within(row).getByText('提示词:最多 32000 字')).toBeVisible() + expect(within(row).getByText('参考图参数:file_urls(内联图片)')).toBeVisible() + expect((global.fetch as jest.Mock).mock.calls.every(([, init]) => !init?.method)).toBe(true) +}) + +test('没有指定的异步图片参数明确显示为上游默认,不伪造尺寸和画质', async () => { + const next = configured(), model = next.providers[0].models[1] + model.capabilities.protocols = { image: { id: 'async-image-task', path: '/extensions/v1/tasks', requestParameters: {} } } + global.fetch = jest.fn(async () => response(next)) as typeof fetch + render() + const row = await screen.findByLabelText('nano-banana-2 · 现有上游', { selector: 'div' }) + fireEvent.click(within(row).getByRole('button', { name: 'nano-banana-2 · 现有上游 图片生成测试详情' })) + expect(within(row).getByText('未指定尺寸、比例和画质,使用上游默认值。')).toBeVisible() + expect(screen.queryByText('1024x1024')).not.toBeInTheDocument() +}) + test.each(['untested', 'passed'] as const)('参考图数量以实测证据为准,%s 不把平台测试上限当成已支持', async status => { const next = configured(), model = next.providers[0].models[1] model.capabilities.protocols = { image: { id: 'openai-image-generate', path: '/v1/images/generations', maxImages: 8, referenceLimitSource: 'verification' } } diff --git a/components/admin/AiModelManagementPanel.tsx b/components/admin/AiModelManagementPanel.tsx index 29b826b..a790752 100644 --- a/components/admin/AiModelManagementPanel.tsx +++ b/components/admin/AiModelManagementPanel.tsx @@ -21,6 +21,7 @@ const primaryClass = `${buttonClass} !border-[#0050cb] !bg-[#0050cb] !text-white const checkboxClass = 'h-4 w-4 shrink-0 accent-[#0050cb] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff] focus-visible:ring-offset-2 disabled:cursor-not-allowed' const capabilityLabels = { text: '对话', vision: '视觉理解', image: '生图', tts: '语音合成', asr: '语音识别', music: '音乐', video: '视频', embedding: '向量', 'image-tool': '图像工具', 'three-d': '3D', other: '其他', unknown: '待识别' } as const const verificationLabels = { untested: '未测试', running: '测试中', passed: '已通过', failed: '测试失败', unknown: '待核查', stale: '需重新测试' } as const +const parameterLabels: Record = { n: '生成数量', stream: '流式输出', response_format: '返回格式', size: '尺寸', aspect_ratio: '画幅比例', resolution: '分辨率', quality: '画质', background: '背景', output_format: '文件格式', version: '模型版本', thinkingLevel: '思考级别' } function verificationLabel(result: AiModelVerificationSummary) { return result.reconciled && result.status === 'failed' ? '已核查,可重测' : verificationLabels[result.status] } @@ -367,6 +368,9 @@ export default function AiModelManagementPanel({ onStateChange }: { onStateChang {showBlockedReason &&

{blockedReason}

} {canInspect &&