mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(model-test): wire image previews into provider tests
This commit is contained in:
@@ -374,6 +374,7 @@ import {
|
||||
isModelTestableEndpoint,
|
||||
parseModelTestRequestHeadersDraft,
|
||||
parseModelTestRequestBodyDraft,
|
||||
selectPreferredModelTestEndpoint,
|
||||
syncModelTestRequestBodyDraft,
|
||||
} from './model-test-request'
|
||||
|
||||
@@ -428,6 +429,7 @@ const deletingGroup = ref<AliasGroup | null>(null)
|
||||
const testingMapping = ref<string | null>(null)
|
||||
const pendingMappingKey = ref<string | null>(null)
|
||||
const testingModelName = ref<string | null>(null)
|
||||
const testingSourceModel = ref<Model | null>(null)
|
||||
const preselectedModelId = ref<string | null>(null)
|
||||
const selectedTestEndpoint = ref<ProviderEndpoint | null>(null)
|
||||
const testRequestHeadersDraft = ref('')
|
||||
@@ -715,6 +717,7 @@ function handleTestDialogClose() {
|
||||
modelTest.resetState()
|
||||
pendingMappingKey.value = null
|
||||
testingModelName.value = null
|
||||
testingSourceModel.value = null
|
||||
testingMapping.value = null
|
||||
selectedTestEndpoint.value = null
|
||||
mappingTestEndpoints.value = null
|
||||
@@ -737,8 +740,25 @@ function handleSelectTestEndpoint(endpointId: string) {
|
||||
syncMappingTestRequestBody()
|
||||
}
|
||||
|
||||
function findMappingTestModel(modelName: string): Model | null {
|
||||
const normalized = modelName.trim()
|
||||
if (!normalized) return null
|
||||
|
||||
return models.value.find(model => (
|
||||
model.provider_model_name === normalized
|
||||
|| model.global_model_name === normalized
|
||||
|| model.global_model_display_name === normalized
|
||||
|| (model.provider_model_mappings ?? []).some(alias => alias.name === normalized)
|
||||
)) ?? null
|
||||
}
|
||||
|
||||
// 测试映射(直连测试,带故障转移和实时进度)
|
||||
function runMappingTest(testingKey: string, modelName: string, endpointsOverride?: ProviderEndpoint[]) {
|
||||
function runMappingTest(
|
||||
testingKey: string,
|
||||
modelName: string,
|
||||
endpointsOverride?: ProviderEndpoint[],
|
||||
sourceModel?: Model | null,
|
||||
) {
|
||||
const endpoints = endpointsOverride ?? activeEndpoints.value
|
||||
if (endpoints.length === 0) {
|
||||
showError('暂无可用于测试的活跃端点')
|
||||
@@ -749,8 +769,12 @@ function runMappingTest(testingKey: string, modelName: string, endpointsOverride
|
||||
modelTest.dialogOpen.value = true
|
||||
testingMapping.value = null
|
||||
testingModelName.value = modelName
|
||||
testingSourceModel.value = sourceModel ?? findMappingTestModel(modelName)
|
||||
mappingTestEndpoints.value = endpointsOverride ?? null
|
||||
selectedTestEndpoint.value = endpoints[0] ?? null
|
||||
selectedTestEndpoint.value = selectPreferredModelTestEndpoint(
|
||||
testingSourceModel.value,
|
||||
endpoints,
|
||||
)
|
||||
testRequestHeadersResetValue.value = buildDefaultModelTestRequestHeaders()
|
||||
testRequestHeadersDraft.value = testRequestHeadersResetValue.value
|
||||
resetMappingTestRequestBody()
|
||||
@@ -762,6 +786,7 @@ function resetMappingTestRequestBody() {
|
||||
testRequestBodyResetValue.value = buildDefaultModelTestRequestBody(
|
||||
testingModelName.value,
|
||||
selectedTestEndpoint.value?.api_format,
|
||||
testingSourceModel.value,
|
||||
)
|
||||
testRequestBodyDraft.value = testRequestBodyResetValue.value
|
||||
}
|
||||
@@ -772,6 +797,7 @@ function syncMappingTestRequestBody() {
|
||||
const nextResetValue = buildDefaultModelTestRequestBody(
|
||||
testingModelName.value,
|
||||
selectedTestEndpoint.value?.api_format,
|
||||
testingSourceModel.value,
|
||||
)
|
||||
const next = syncModelTestRequestBodyDraft(
|
||||
testRequestBodyDraft.value,
|
||||
@@ -839,7 +865,7 @@ function scopedMappingEndpoints(item: CombinedMapping): ProviderEndpoint[] {
|
||||
|
||||
// 测试精确映射
|
||||
function testMapping(item: CombinedMapping, mapping: MappingItem) {
|
||||
runMappingTest(`${item.key}-${mapping.name}`, mapping.name, scopedMappingEndpoints(item))
|
||||
runMappingTest(`${item.key}-${mapping.name}`, mapping.name, scopedMappingEndpoints(item), item.group?.model)
|
||||
}
|
||||
|
||||
// 测试正则映射
|
||||
|
||||
@@ -444,7 +444,27 @@
|
||||
{{ attempt.key_name || maskKey(attempt.key_id) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="attemptDetail(attempt) !== '-'"
|
||||
v-if="attemptImagePreviews(attempt).length > 0"
|
||||
class="mt-2 flex flex-wrap gap-2"
|
||||
>
|
||||
<button
|
||||
v-for="(preview, imageIndex) in attemptImagePreviews(attempt).slice(0, 3)"
|
||||
:key="`${preview.src}-${imageIndex}`"
|
||||
type="button"
|
||||
class="h-16 w-16 overflow-hidden rounded-md border border-border/60 bg-muted/30 transition-colors hover:border-primary/60"
|
||||
:title="preview.label"
|
||||
@click.stop="openImagePreview(preview)"
|
||||
>
|
||||
<img
|
||||
:src="preview.src"
|
||||
:alt="preview.label"
|
||||
class="h-full w-full object-contain"
|
||||
loading="lazy"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="attemptDetail(attempt) !== '-'"
|
||||
class="mt-1 break-all text-muted-foreground"
|
||||
>
|
||||
{{ attemptDetail(attempt) }}
|
||||
@@ -519,6 +539,33 @@
|
||||
</td>
|
||||
<td class="px-3 py-2 text-muted-foreground">
|
||||
<div
|
||||
v-if="attemptImagePreviews(attempt).length > 0"
|
||||
class="flex flex-wrap gap-2"
|
||||
>
|
||||
<button
|
||||
v-for="(preview, imageIndex) in attemptImagePreviews(attempt).slice(0, 4)"
|
||||
:key="`${preview.src}-${imageIndex}`"
|
||||
type="button"
|
||||
class="h-14 w-14 overflow-hidden rounded-md border border-border/60 bg-muted/30 transition-colors hover:border-primary/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/70"
|
||||
:title="preview.label"
|
||||
@click.stop="openImagePreview(preview)"
|
||||
>
|
||||
<img
|
||||
:src="preview.src"
|
||||
:alt="preview.label"
|
||||
class="h-full w-full object-contain"
|
||||
loading="lazy"
|
||||
>
|
||||
</button>
|
||||
<span
|
||||
v-if="attemptImagePreviews(attempt).length > 4"
|
||||
class="flex h-14 items-center rounded-md border border-border/60 px-2 text-xs text-muted-foreground"
|
||||
>
|
||||
+{{ attemptImagePreviews(attempt).length - 4 }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="line-clamp-2 break-all"
|
||||
:title="attemptDetail(attempt)"
|
||||
>
|
||||
@@ -641,6 +688,36 @@
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="response-body">
|
||||
<div
|
||||
v-if="selectedInspectionImagePreviews.length > 0"
|
||||
class="mb-3 rounded-md border border-border/60 bg-muted/20 p-3"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between gap-3 text-xs text-muted-foreground">
|
||||
<span>图片预览</span>
|
||||
<span>{{ selectedInspectionImagePreviews.length }} 张</span>
|
||||
</div>
|
||||
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<button
|
||||
v-for="(preview, index) in selectedInspectionImagePreviews"
|
||||
:key="`${preview.src}-${index}`"
|
||||
type="button"
|
||||
class="group block overflow-hidden rounded-md border border-border/60 bg-background text-left transition-colors hover:border-primary/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/70"
|
||||
@click="openImagePreview(preview)"
|
||||
>
|
||||
<div class="aspect-square w-full overflow-hidden bg-muted/30">
|
||||
<img
|
||||
:src="preview.src"
|
||||
:alt="preview.label"
|
||||
class="h-full w-full object-contain"
|
||||
loading="lazy"
|
||||
>
|
||||
</div>
|
||||
<div class="border-t border-border/60 px-2 py-1 text-[11px] text-muted-foreground">
|
||||
{{ preview.label }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<JsonContent
|
||||
:data="selectedInspectionAttempt.response_body"
|
||||
view-mode="formatted"
|
||||
@@ -673,6 +750,39 @@
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
:open="Boolean(activeImagePreview)"
|
||||
size="6xl"
|
||||
:z-index="120"
|
||||
@update:open="(val: boolean) => { if (!val) activeImagePreview = null }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="border-b border-border px-6 py-4">
|
||||
<div class="text-lg font-semibold text-foreground leading-tight">
|
||||
{{ activeImagePreview?.label || '图片预览' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex max-h-[76vh] items-center justify-center overflow-auto rounded-md bg-muted/20 p-3">
|
||||
<img
|
||||
v-if="activeImagePreview"
|
||||
:src="activeImagePreview.src"
|
||||
:alt="activeImagePreview.label"
|
||||
class="max-h-[72vh] max-w-full rounded-md object-contain"
|
||||
>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button
|
||||
variant="outline"
|
||||
@click="activeImagePreview = null"
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -698,7 +808,12 @@ import type { CandidateRecord, RequestTrace } from '@/api/requestTrace'
|
||||
import JsonContent from '@/features/usage/components/RequestDetailDrawer/JsonContent.vue'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { useDarkMode } from '@/composables/useDarkMode'
|
||||
import { extractModelTestResponsePreview, formatModelTestDiagnostic } from './model-test-request'
|
||||
import {
|
||||
extractModelTestImagePreviews,
|
||||
extractModelTestResponsePreview,
|
||||
formatModelTestDiagnostic,
|
||||
} from './model-test-request'
|
||||
import type { ModelTestImagePreview } from './model-test-request'
|
||||
|
||||
type TestEndpointOption = {
|
||||
id: string
|
||||
@@ -816,6 +931,7 @@ const inspectionTab = ref<'request-headers' | 'request-body' | 'response-headers
|
||||
const selectedInspectionKey = ref<string | null>(null)
|
||||
const inspectionExpandDepth = ref(0)
|
||||
const inspectionCopiedStates = ref<Record<string, boolean>>({})
|
||||
const activeImagePreview = ref<ModelTestImagePreview | null>(null)
|
||||
|
||||
watch(() => props.result, () => {
|
||||
showAllAttempts.value = false
|
||||
@@ -1129,6 +1245,12 @@ const selectedInspectionAttempt = computed(() => {
|
||||
return inspectableAttempts.value[0] ?? resultAttempts.value[0] ?? null
|
||||
})
|
||||
|
||||
const selectedInspectionImagePreviews = computed(() => (
|
||||
selectedInspectionAttempt.value
|
||||
? extractModelTestImagePreviews(selectedInspectionAttempt.value.response_body)
|
||||
: []
|
||||
))
|
||||
|
||||
const resultWinningTitle = computed(() => {
|
||||
const summary = resultSummary.value
|
||||
const keyName = summary.winning_key_name || summary.winning_key_id
|
||||
@@ -1238,6 +1360,7 @@ function formatAuthType(authType: string): string {
|
||||
if (lowered === 'codex') return 'Codex OAuth'
|
||||
if (lowered === 'antigravity') return 'Antigravity OAuth'
|
||||
if (lowered === 'kiro') return 'Kiro OAuth'
|
||||
if (lowered === 'grok') return 'Grok OAuth'
|
||||
return authType
|
||||
}
|
||||
|
||||
@@ -1295,6 +1418,14 @@ function attemptDetail(attempt: TestAttemptDetail): string {
|
||||
return '-'
|
||||
}
|
||||
|
||||
function attemptImagePreviews(attempt: TestAttemptDetail): ModelTestImagePreview[] {
|
||||
return extractModelTestImagePreviews(attempt.response_body)
|
||||
}
|
||||
|
||||
function openImagePreview(preview: ModelTestImagePreview) {
|
||||
activeImagePreview.value = preview
|
||||
}
|
||||
|
||||
function inspectionKey(attempt: TestAttemptDetail): string {
|
||||
return `${attempt.candidate_index}:${attempt.retry_index ?? 0}:${attempt.key_id}`
|
||||
}
|
||||
|
||||
@@ -272,6 +272,7 @@ import {
|
||||
normalizeModelTestMappedModelSelection,
|
||||
parseModelTestRequestHeadersDraft,
|
||||
parseModelTestRequestBodyDraft,
|
||||
selectPreferredModelTestEndpoint,
|
||||
syncModelTestRequestBodyDraft,
|
||||
} from './model-test-request'
|
||||
|
||||
@@ -586,7 +587,7 @@ async function testModelConnection(model: Model) {
|
||||
}
|
||||
|
||||
pendingTestModel.value = model
|
||||
selectedTestEndpoint.value = activeEndpoints.value[0] ?? null
|
||||
selectedTestEndpoint.value = selectPreferredModelTestEndpoint(model, activeEndpoints.value)
|
||||
const requestedModelName = getModelTestRequestedModelName(model)
|
||||
selectedTestMappedModelName.value = null
|
||||
testRequestHeadersResetValue.value = buildDefaultModelTestRequestHeaders()
|
||||
@@ -594,6 +595,7 @@ async function testModelConnection(model: Model) {
|
||||
testRequestBodyResetValue.value = buildDefaultModelTestRequestBody(
|
||||
requestedModelName,
|
||||
selectedTestEndpoint.value?.api_format,
|
||||
model,
|
||||
)
|
||||
testRequestBodyDraft.value = testRequestBodyResetValue.value
|
||||
modelTest.testResult.value = null
|
||||
@@ -619,7 +621,11 @@ function syncTestRequestBodyModel() {
|
||||
if (!modelName) return
|
||||
|
||||
const resetDraft = testRequestBodyResetValue.value
|
||||
|| buildDefaultModelTestRequestBody(modelName, selectedTestEndpoint.value?.api_format)
|
||||
|| buildDefaultModelTestRequestBody(
|
||||
modelName,
|
||||
selectedTestEndpoint.value?.api_format,
|
||||
pendingTestModel.value,
|
||||
)
|
||||
const next = syncModelTestRequestBodyDraft(
|
||||
testRequestBodyDraft.value,
|
||||
testRequestBodyResetValue.value,
|
||||
@@ -637,6 +643,7 @@ function resetTestRequestBodyForSelectedEndpoint() {
|
||||
const nextResetValue = buildDefaultModelTestRequestBody(
|
||||
modelName,
|
||||
selectedTestEndpoint.value?.api_format,
|
||||
pendingTestModel.value,
|
||||
)
|
||||
const next = syncModelTestRequestBodyDraft(
|
||||
testRequestBodyDraft.value,
|
||||
|
||||
@@ -3,12 +3,15 @@ import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
buildDefaultModelTestRequestBody,
|
||||
buildExactModelMappingTestRequest,
|
||||
extractModelTestImagePreviews,
|
||||
extractModelTestResponsePreview,
|
||||
formatModelTestDiagnostic,
|
||||
getOpenAiImageModelTestMaxGenerationCount,
|
||||
isModelTestableEndpoint,
|
||||
isModelTestableApiFormat,
|
||||
listModelTestMappedModelOptions,
|
||||
normalizeModelTestMappedModelSelection,
|
||||
selectPreferredModelTestEndpoint,
|
||||
setModelTestRequestBodyModel,
|
||||
syncModelTestRequestBodyDraft,
|
||||
} from '../model-test-request'
|
||||
@@ -55,6 +58,41 @@ describe('buildDefaultModelTestRequestBody', () => {
|
||||
expect(body.input).toBeUndefined()
|
||||
})
|
||||
|
||||
it('uses prompt payloads for openai image api formats', () => {
|
||||
const body = JSON.parse(buildDefaultModelTestRequestBody('gpt-image-2', 'openai:image'))
|
||||
|
||||
expect(body).toEqual({
|
||||
model: 'gpt-image-2',
|
||||
prompt: 'Hello! This is a test message.',
|
||||
n: 1,
|
||||
size: '1024x1024',
|
||||
stream: true,
|
||||
})
|
||||
expect(body.messages).toBeUndefined()
|
||||
})
|
||||
|
||||
it('uses image generation tools for image models on OpenAI Responses endpoints', () => {
|
||||
const body = JSON.parse(buildDefaultModelTestRequestBody(
|
||||
'gpt-image-2',
|
||||
'openai:responses',
|
||||
{
|
||||
effective_supports_image_generation: true,
|
||||
},
|
||||
))
|
||||
|
||||
expect(body.model).toBe('gpt-image-2')
|
||||
expect(body.input).toBe('Hello! This is a test message.')
|
||||
expect(body.tools).toEqual([
|
||||
{
|
||||
type: 'image_generation',
|
||||
size: '1024x1024',
|
||||
output_format: 'png',
|
||||
},
|
||||
])
|
||||
expect(body.tool_choice).toEqual({ type: 'image_generation' })
|
||||
expect(body.messages).toBeUndefined()
|
||||
})
|
||||
|
||||
it('lists endpoint-scoped provider model mappings in test selection order', () => {
|
||||
const options = listModelTestMappedModelOptions({
|
||||
provider_model_name: 'claude-opus-4-6',
|
||||
@@ -204,6 +242,7 @@ describe('isModelTestableApiFormat', () => {
|
||||
'openai:responses',
|
||||
'claude:messages',
|
||||
'gemini:generate_content',
|
||||
'openai:image',
|
||||
'openai:embedding',
|
||||
'jina:rerank',
|
||||
])('allows synchronous model-test endpoint formats: %s', (apiFormat) => {
|
||||
@@ -211,6 +250,75 @@ describe('isModelTestableApiFormat', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('selectPreferredModelTestEndpoint', () => {
|
||||
it('prefers openai image endpoints for image generation models', () => {
|
||||
const chatEndpoint = { id: 'chat', api_format: 'openai:chat', is_active: true }
|
||||
const imageEndpoint = { id: 'image', api_format: 'openai:image', is_active: true }
|
||||
|
||||
expect(selectPreferredModelTestEndpoint({
|
||||
effective_supports_image_generation: true,
|
||||
}, [chatEndpoint, imageEndpoint])).toBe(imageEndpoint)
|
||||
})
|
||||
|
||||
it('prefers openai image endpoints from model-test capability metadata', () => {
|
||||
const chatEndpoint = { id: 'chat', api_format: 'openai:chat', is_active: true }
|
||||
const imageEndpoint = { id: 'image', api_format: 'openai:image', is_active: true }
|
||||
|
||||
expect(selectPreferredModelTestEndpoint({
|
||||
effective_supports_image_generation: false,
|
||||
model_test_capabilities: {
|
||||
'openai:image': {
|
||||
supports_generation: true,
|
||||
max_generation_count: 4,
|
||||
},
|
||||
},
|
||||
}, [chatEndpoint, imageEndpoint])).toBe(imageEndpoint)
|
||||
})
|
||||
|
||||
it('does not treat edit-only image capability as generation support', () => {
|
||||
const chatEndpoint = { id: 'chat', api_format: 'openai:chat', is_active: true }
|
||||
const imageEndpoint = { id: 'image', api_format: 'openai:image', is_active: true }
|
||||
|
||||
expect(selectPreferredModelTestEndpoint({
|
||||
effective_supports_image_generation: true,
|
||||
model_test_capabilities: {
|
||||
'openai:image': {
|
||||
supports_generation: false,
|
||||
supports_edit: true,
|
||||
max_generation_count: 4,
|
||||
},
|
||||
},
|
||||
}, [chatEndpoint, imageEndpoint])).toBe(chatEndpoint)
|
||||
})
|
||||
|
||||
it('keeps the existing endpoint order for non-image models', () => {
|
||||
const chatEndpoint = { id: 'chat', api_format: 'openai:chat', is_active: true }
|
||||
const imageEndpoint = { id: 'image', api_format: 'openai:image', is_active: true }
|
||||
|
||||
expect(selectPreferredModelTestEndpoint({
|
||||
effective_supports_image_generation: false,
|
||||
}, [chatEndpoint, imageEndpoint])).toBe(chatEndpoint)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getOpenAiImageModelTestMaxGenerationCount', () => {
|
||||
it('reads image generation count from backend capability metadata', () => {
|
||||
expect(getOpenAiImageModelTestMaxGenerationCount({
|
||||
model_test_capabilities: {
|
||||
'openai:image': {
|
||||
max_generation_count: 4,
|
||||
},
|
||||
},
|
||||
})).toBe(4)
|
||||
})
|
||||
|
||||
it('returns null when backend capability metadata is absent', () => {
|
||||
expect(getOpenAiImageModelTestMaxGenerationCount({
|
||||
effective_supports_image_generation: true,
|
||||
})).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('isModelTestableEndpoint', () => {
|
||||
it('requires at least one active key compatible with the endpoint format', () => {
|
||||
const keys = [
|
||||
@@ -341,6 +449,95 @@ describe('extractModelTestResponsePreview', () => {
|
||||
})).toBe('Rerank 结果:2 条')
|
||||
})
|
||||
|
||||
it('extracts image URLs from OpenAI image responses', () => {
|
||||
expect(extractModelTestResponsePreview({
|
||||
data: [
|
||||
{
|
||||
url: 'https://example.com/generated.png',
|
||||
revised_prompt: 'A generated image',
|
||||
},
|
||||
],
|
||||
})).toBe('图片:https://example.com/generated.png')
|
||||
})
|
||||
|
||||
it('summarizes base64 image responses without dumping the image payload', () => {
|
||||
expect(extractModelTestResponsePreview({
|
||||
data: [
|
||||
{
|
||||
b64_json: 'aGVsbG8=',
|
||||
},
|
||||
],
|
||||
})).toBe('图片:base64')
|
||||
})
|
||||
|
||||
it('extracts renderable base64 image previews from OpenAI image responses', () => {
|
||||
expect(extractModelTestImagePreviews({
|
||||
data: [
|
||||
{
|
||||
b64_json: 'aGVsbG8=',
|
||||
mime_type: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
})).toEqual([
|
||||
{
|
||||
src: 'data:image/jpeg;base64,aGVsbG8=',
|
||||
label: '图片 1',
|
||||
source: 'base64',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('extracts image previews from nested response image urls', () => {
|
||||
expect(extractModelTestImagePreviews({
|
||||
output: [
|
||||
{
|
||||
content: [
|
||||
{
|
||||
type: 'output_image',
|
||||
image_url: {
|
||||
url: 'https://example.com/generated.png',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})).toEqual([
|
||||
{
|
||||
src: 'https://example.com/generated.png',
|
||||
label: '图片 1',
|
||||
source: 'url',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('extracts image previews from OpenAI Responses image_generation_call results', () => {
|
||||
expect(extractModelTestResponsePreview({
|
||||
output: [
|
||||
{
|
||||
type: 'image_generation_call',
|
||||
output_format: 'png',
|
||||
result: 'aGVsbG8=',
|
||||
},
|
||||
],
|
||||
})).toBe('图片:base64')
|
||||
|
||||
expect(extractModelTestImagePreviews({
|
||||
output: [
|
||||
{
|
||||
type: 'image_generation_call',
|
||||
output_format: 'png',
|
||||
result: 'aGVsbG8=',
|
||||
},
|
||||
],
|
||||
})).toEqual([
|
||||
{
|
||||
src: 'data:image/png;base64,aGVsbG8=',
|
||||
label: '图片 1',
|
||||
source: 'base64',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('falls back to response model when no text payload exists', () => {
|
||||
expect(extractModelTestResponsePreview({
|
||||
model: 'glm-4.5-air',
|
||||
|
||||
Reference in New Issue
Block a user