190 lines
6.0 KiB
TypeScript
190 lines
6.0 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
buildRenderedTemplateRequest,
|
|
buildTemplateVariables,
|
|
extractTemplateError,
|
|
readJsonPath,
|
|
renderTemplateString,
|
|
renderTemplateValue,
|
|
resolveTemplateEndpointUrl,
|
|
} from '@/lib/openai-compat-template-runtime'
|
|
|
|
describe('model-gateway openai-compat template renderer', () => {
|
|
it('renders placeholders in strings and nested body values', () => {
|
|
const variables = buildTemplateVariables({
|
|
model: 'veo3.1',
|
|
prompt: 'a cat running',
|
|
image: 'https://a.test/cat.png',
|
|
taskId: 'task_1',
|
|
})
|
|
|
|
expect(renderTemplateString('/videos/{{task_id}}', variables)).toBe('/videos/task_1')
|
|
expect(renderTemplateValue({
|
|
model: '{{model}}',
|
|
prompt: '{{prompt}}',
|
|
images: '{{images}}',
|
|
nested: [{ value: '{{task_id}}' }],
|
|
}, variables)).toEqual({
|
|
model: 'veo3.1',
|
|
prompt: 'a cat running',
|
|
images: [],
|
|
nested: [{ value: 'task_1' }],
|
|
})
|
|
})
|
|
|
|
it('resolves relative path against base url and injects auth header', async () => {
|
|
const request = await buildRenderedTemplateRequest({
|
|
baseUrl: 'https://compat.example.com/v1/',
|
|
endpoint: {
|
|
method: 'POST',
|
|
path: '/v2/videos/generations',
|
|
contentType: 'application/json',
|
|
bodyTemplate: {
|
|
model: '{{model}}',
|
|
prompt: '{{prompt}}',
|
|
},
|
|
},
|
|
variables: buildTemplateVariables({
|
|
model: 'veo3.1',
|
|
prompt: 'hello',
|
|
}),
|
|
defaultAuthHeader: 'Bearer sk-test',
|
|
})
|
|
|
|
expect(resolveTemplateEndpointUrl('https://compat.example.com/v1/', '/v2/videos/generations'))
|
|
.toBe('https://compat.example.com/v1/v2/videos/generations')
|
|
expect(request.endpointUrl).toBe('https://compat.example.com/v1/v2/videos/generations')
|
|
expect(request.headers.Authorization).toBe('Bearer sk-test')
|
|
expect(request.headers['Content-Type']).toBe('application/json')
|
|
expect(request.body).toBe(JSON.stringify({
|
|
model: 'veo3.1',
|
|
prompt: 'hello',
|
|
}))
|
|
})
|
|
|
|
it('deduplicates /v1 prefix when base url already ends with /v1', async () => {
|
|
const request = await buildRenderedTemplateRequest({
|
|
baseUrl: 'https://yunwu.ai/v1',
|
|
endpoint: {
|
|
method: 'GET',
|
|
path: '/v1/video/query?id={{task_id}}',
|
|
},
|
|
variables: buildTemplateVariables({
|
|
model: 'veo_3_1-fast-4K',
|
|
prompt: '',
|
|
taskId: 'task_abc',
|
|
}),
|
|
defaultAuthHeader: 'Bearer sk-test',
|
|
})
|
|
|
|
expect(resolveTemplateEndpointUrl('https://yunwu.ai/v1', '/v1/video/create'))
|
|
.toBe('https://yunwu.ai/v1/video/create')
|
|
expect(request.endpointUrl).toBe('https://yunwu.ai/v1/video/query?id=task_abc')
|
|
expect(request.headers.Authorization).toBe('Bearer sk-test')
|
|
})
|
|
|
|
it('builds multipart form data and omits explicit content-type header', async () => {
|
|
const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO0p6s8AAAAASUVORK5CYII='
|
|
const request = await buildRenderedTemplateRequest({
|
|
baseUrl: 'https://compat.example.com/v1',
|
|
endpoint: {
|
|
method: 'POST',
|
|
path: '/videos',
|
|
contentType: 'multipart/form-data',
|
|
multipartFileFields: ['input_reference'],
|
|
bodyTemplate: {
|
|
model: '{{model}}',
|
|
prompt: '{{prompt}}',
|
|
input_reference: '{{image}}',
|
|
},
|
|
},
|
|
variables: buildTemplateVariables({
|
|
model: 'veo3.1',
|
|
prompt: 'hello',
|
|
image: dataUrl,
|
|
}),
|
|
defaultAuthHeader: 'Bearer sk-test',
|
|
})
|
|
|
|
expect(request.endpointUrl).toBe('https://compat.example.com/v1/videos')
|
|
expect(request.headers.Authorization).toBe('Bearer sk-test')
|
|
expect(request.headers['Content-Type']).toBeUndefined()
|
|
expect(request.body).toBeInstanceOf(FormData)
|
|
|
|
const formData = request.body as FormData
|
|
expect(formData.get('model')).toBe('veo3.1')
|
|
expect(formData.get('prompt')).toBe('hello')
|
|
const fileValue = formData.get('input_reference')
|
|
expect(fileValue).toBeInstanceOf(File)
|
|
expect((fileValue as File).name).toBe('reference-0.png')
|
|
})
|
|
|
|
it('builds application/x-www-form-urlencoded bodies', async () => {
|
|
const request = await buildRenderedTemplateRequest({
|
|
baseUrl: 'https://compat.example.com/v1',
|
|
endpoint: {
|
|
method: 'POST',
|
|
path: '/videos/query',
|
|
contentType: 'application/x-www-form-urlencoded',
|
|
bodyTemplate: {
|
|
model: '{{model}}',
|
|
task_id: '{{task_id}}',
|
|
},
|
|
},
|
|
variables: buildTemplateVariables({
|
|
model: 'veo3.1',
|
|
prompt: 'hello',
|
|
taskId: 'task_1',
|
|
}),
|
|
})
|
|
|
|
expect(request.headers['Content-Type']).toBe('application/x-www-form-urlencoded')
|
|
expect(request.body).toBeInstanceOf(URLSearchParams)
|
|
expect((request.body as URLSearchParams).toString()).toBe('model=veo3.1&task_id=task_1')
|
|
})
|
|
|
|
it('reads json path for array/object outputs', () => {
|
|
const payload = {
|
|
data: [{ url: 'https://cdn.test/1.png' }],
|
|
task: {
|
|
status: 'succeeded',
|
|
},
|
|
}
|
|
expect(readJsonPath(payload, '$.data[0].url')).toBe('https://cdn.test/1.png')
|
|
expect(readJsonPath(payload, '$.task.status')).toBe('succeeded')
|
|
})
|
|
|
|
it('extracts upstream error message from common payload shape', () => {
|
|
const message = extractTemplateError({
|
|
version: 1,
|
|
mediaType: 'video',
|
|
mode: 'async',
|
|
create: {
|
|
method: 'POST',
|
|
path: '/video/create',
|
|
},
|
|
status: {
|
|
method: 'GET',
|
|
path: '/video/query?id={{task_id}}',
|
|
},
|
|
response: {
|
|
taskIdPath: '$.id',
|
|
statusPath: '$.status',
|
|
},
|
|
polling: {
|
|
intervalMs: 5000,
|
|
timeoutMs: 600000,
|
|
doneStates: ['completed'],
|
|
failStates: ['failed'],
|
|
},
|
|
}, {
|
|
error: {
|
|
message_zh: '当前分组上游负载已饱和,请稍后再试',
|
|
},
|
|
}, 500)
|
|
|
|
expect(message).toContain('status 500')
|
|
expect(message).toContain('当前分组上游负载已饱和,请稍后再试')
|
|
})
|
|
})
|