208 lines
6.4 KiB
TypeScript
208 lines
6.4 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { generateVideoViaOpenAICompatTemplate } from '@/lib/model-gateway/openai-compat/template-video'
|
|
import { pollAsyncTask } from '@/lib/async-poll'
|
|
import { startScenarioServer } from '../../helpers/fakes/scenario-server'
|
|
|
|
const getProviderConfigMock = vi.hoisted(() => vi.fn())
|
|
const getUserModelsMock = vi.hoisted(() => vi.fn())
|
|
|
|
vi.mock('@/lib/api-config', () => ({
|
|
getProviderConfig: getProviderConfigMock,
|
|
getUserModels: getUserModelsMock,
|
|
}))
|
|
|
|
function encode(value: string): string {
|
|
return Buffer.from(value, 'utf8').toString('base64url')
|
|
}
|
|
|
|
describe('provider contract - openai compatible media template', () => {
|
|
let server: Awaited<ReturnType<typeof startScenarioServer>> | null = null
|
|
|
|
beforeEach(async () => {
|
|
server = await startScenarioServer()
|
|
vi.clearAllMocks()
|
|
getProviderConfigMock.mockResolvedValue({
|
|
id: 'openai-compatible:provider-local',
|
|
apiKey: 'sk-local',
|
|
baseUrl: `${server.baseUrl}/compat/v1`,
|
|
})
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await server?.close()
|
|
server = null
|
|
})
|
|
|
|
it('renders create request against provider baseUrl and returns OCOMPAT externalId', async () => {
|
|
server!.defineScenario({
|
|
method: 'POST',
|
|
path: '/compat/v1/video/create',
|
|
mode: 'success',
|
|
submitResponse: {
|
|
status: 200,
|
|
body: { status: 'queued', task_id: 'task_local_1' },
|
|
},
|
|
})
|
|
|
|
const result = await generateVideoViaOpenAICompatTemplate({
|
|
userId: 'user-local',
|
|
providerId: 'openai-compatible:provider-local',
|
|
modelId: 'veo-local',
|
|
modelKey: 'openai-compatible:provider-local::veo-local',
|
|
imageUrl: 'data:image/png;base64,AAAA',
|
|
prompt: 'animate this frame',
|
|
options: {
|
|
duration: 5,
|
|
aspectRatio: '9:16',
|
|
},
|
|
profile: 'openai-compatible',
|
|
template: {
|
|
version: 1,
|
|
mediaType: 'video',
|
|
mode: 'async',
|
|
create: {
|
|
method: 'POST',
|
|
path: '/video/create',
|
|
bodyTemplate: {
|
|
model: '{{model}}',
|
|
prompt: '{{prompt}}',
|
|
image: '{{image}}',
|
|
duration: '{{duration}}',
|
|
},
|
|
},
|
|
status: { method: 'GET', path: '/video/status/{{task_id}}' },
|
|
response: {
|
|
taskIdPath: '$.task_id',
|
|
statusPath: '$.status',
|
|
},
|
|
polling: {
|
|
intervalMs: 1000,
|
|
timeoutMs: 30_000,
|
|
doneStates: ['done'],
|
|
failStates: ['failed'],
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(result).toMatchObject({
|
|
success: true,
|
|
async: true,
|
|
requestId: 'task_local_1',
|
|
externalId: `OCOMPAT:VIDEO:b64_${encode('openai-compatible:provider-local')}:${encode('veo-local')}:task_local_1`,
|
|
})
|
|
|
|
const requests = server!.getRequests('POST', '/compat/v1/video/create')
|
|
expect(requests).toHaveLength(1)
|
|
expect(requests[0]?.headers.authorization).toBe('Bearer sk-local')
|
|
expect(JSON.parse(requests[0]?.bodyText || '{}')).toEqual({
|
|
model: 'veo-local',
|
|
prompt: 'animate this frame',
|
|
image: 'data:image/png;base64,AAAA',
|
|
duration: 5,
|
|
})
|
|
})
|
|
|
|
it('polls localhost provider status and falls back to content endpoint when output url is missing', async () => {
|
|
getUserModelsMock.mockResolvedValue([
|
|
{
|
|
modelKey: 'openai-compatible:provider-local::veo-local',
|
|
modelId: 'veo-local',
|
|
name: 'Local Veo',
|
|
type: 'video',
|
|
provider: 'openai-compatible:provider-local',
|
|
price: 0,
|
|
compatMediaTemplate: {
|
|
version: 1,
|
|
mediaType: 'video',
|
|
mode: 'async',
|
|
create: { method: 'POST', path: '/video/create' },
|
|
status: { method: 'GET', path: '/video/status/{{task_id}}' },
|
|
content: { method: 'GET', path: '/video/content/{{task_id}}' },
|
|
response: {
|
|
statusPath: '$.status',
|
|
},
|
|
polling: {
|
|
intervalMs: 1000,
|
|
timeoutMs: 30_000,
|
|
doneStates: ['done'],
|
|
failStates: ['failed'],
|
|
},
|
|
},
|
|
},
|
|
])
|
|
server!.defineScenario({
|
|
method: 'GET',
|
|
path: '/compat/v1/video/status/task_local_2',
|
|
mode: 'queued_then_success',
|
|
pollSequence: [
|
|
{ status: 200, body: { status: 'running' } },
|
|
{ status: 200, body: { status: 'done' } },
|
|
],
|
|
})
|
|
|
|
const first = await pollAsyncTask(
|
|
`OCOMPAT:VIDEO:${encode('openai-compatible:provider-local')}:${encode('openai-compatible:provider-local::veo-local')}:task_local_2`,
|
|
'user-local',
|
|
)
|
|
const second = await pollAsyncTask(
|
|
`OCOMPAT:VIDEO:${encode('openai-compatible:provider-local')}:${encode('openai-compatible:provider-local::veo-local')}:task_local_2`,
|
|
'user-local',
|
|
)
|
|
|
|
expect(first).toEqual({ status: 'pending' })
|
|
expect(second).toEqual({
|
|
status: 'completed',
|
|
resultUrl: `${server!.baseUrl}/compat/v1/video/content/task_local_2`,
|
|
videoUrl: `${server!.baseUrl}/compat/v1/video/content/task_local_2`,
|
|
downloadHeaders: {
|
|
Authorization: 'Bearer sk-local',
|
|
},
|
|
})
|
|
})
|
|
|
|
it('fails explicitly when async create response omits task id', async () => {
|
|
server!.defineScenario({
|
|
method: 'POST',
|
|
path: '/compat/v1/video/create',
|
|
mode: 'malformed_response',
|
|
submitResponse: {
|
|
status: 200,
|
|
body: { status: 'queued' },
|
|
},
|
|
})
|
|
|
|
await expect(
|
|
generateVideoViaOpenAICompatTemplate({
|
|
userId: 'user-local',
|
|
providerId: 'openai-compatible:provider-local',
|
|
modelId: 'veo-local',
|
|
modelKey: 'openai-compatible:provider-local::veo-local',
|
|
imageUrl: 'data:image/png;base64,AAAA',
|
|
prompt: 'bad create payload',
|
|
profile: 'openai-compatible',
|
|
template: {
|
|
version: 1,
|
|
mediaType: 'video',
|
|
mode: 'async',
|
|
create: {
|
|
method: 'POST',
|
|
path: '/video/create',
|
|
bodyTemplate: { prompt: '{{prompt}}' },
|
|
},
|
|
status: { method: 'GET', path: '/video/status/{{task_id}}' },
|
|
response: {
|
|
taskIdPath: '$.task_id',
|
|
statusPath: '$.status',
|
|
},
|
|
polling: {
|
|
intervalMs: 1000,
|
|
timeoutMs: 30_000,
|
|
doneStates: ['done'],
|
|
failStates: ['failed'],
|
|
},
|
|
},
|
|
}),
|
|
).rejects.toThrow('OPENAI_COMPAT_VIDEO_TEMPLATE_TASK_ID_NOT_FOUND')
|
|
})
|
|
})
|