fix image model alias compatibility
Some checks failed
Build & Push Docker Image / build-and-push (push) Has been cancelled
Some checks failed
Build & Push Docker Image / build-and-push (push) Has been cancelled
This commit is contained in:
88
tests/unit/api-config/model-selection-alias.test.ts
Normal file
88
tests/unit/api-config/model-selection-alias.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const prismaMock = vi.hoisted(() => ({
|
||||
userPreference: {
|
||||
findUnique: vi.fn<(...args: unknown[]) => Promise<{ customModels: string; customProviders: string | null } | null>>(async () => null),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/prisma', () => ({
|
||||
prisma: prismaMock,
|
||||
}))
|
||||
|
||||
import { resolveModelSelection } from '@/lib/api-config'
|
||||
|
||||
const compatImageTemplate = {
|
||||
version: 1 as const,
|
||||
mediaType: 'image' as const,
|
||||
mode: 'sync' as const,
|
||||
create: {
|
||||
method: 'POST' as const,
|
||||
path: '/images/generations',
|
||||
contentType: 'application/json' as const,
|
||||
bodyTemplate: {
|
||||
model: '{{model}}',
|
||||
prompt: '{{prompt}}',
|
||||
},
|
||||
},
|
||||
response: {
|
||||
outputUrlPath: '$.data[0].url',
|
||||
errorPath: '$.error.message',
|
||||
},
|
||||
}
|
||||
|
||||
describe('api-config image model alias resolution', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('matches a preview image model when the caller still uses the legacy model id', async () => {
|
||||
prismaMock.userPreference.findUnique.mockResolvedValueOnce({
|
||||
customProviders: null,
|
||||
customModels: JSON.stringify([
|
||||
{
|
||||
modelId: 'gemini-3.1-flash-image-preview',
|
||||
modelKey: 'openai-compatible:oa-1::gemini-3.1-flash-image-preview',
|
||||
name: 'Nano Banana 2',
|
||||
type: 'image',
|
||||
provider: 'openai-compatible:oa-1',
|
||||
compatMediaTemplate: compatImageTemplate,
|
||||
},
|
||||
]),
|
||||
})
|
||||
|
||||
const selection = await resolveModelSelection(
|
||||
'user-1',
|
||||
'openai-compatible:oa-1::gemini-3.1-flash-image',
|
||||
'image',
|
||||
)
|
||||
|
||||
expect(selection.modelId).toBe('gemini-3.1-flash-image-preview')
|
||||
expect(selection.modelKey).toBe('openai-compatible:oa-1::gemini-3.1-flash-image-preview')
|
||||
})
|
||||
|
||||
it('matches a legacy image model when the caller already uses the preview model id', async () => {
|
||||
prismaMock.userPreference.findUnique.mockResolvedValueOnce({
|
||||
customProviders: null,
|
||||
customModels: JSON.stringify([
|
||||
{
|
||||
modelId: 'gemini-3.1-flash-image',
|
||||
modelKey: 'openai-compatible:oa-1::gemini-3.1-flash-image',
|
||||
name: 'Nano Banana 2',
|
||||
type: 'image',
|
||||
provider: 'openai-compatible:oa-1',
|
||||
compatMediaTemplate: compatImageTemplate,
|
||||
},
|
||||
]),
|
||||
})
|
||||
|
||||
const selection = await resolveModelSelection(
|
||||
'user-1',
|
||||
'openai-compatible:oa-1::gemini-3.1-flash-image-preview',
|
||||
'image',
|
||||
)
|
||||
|
||||
expect(selection.modelId).toBe('gemini-3.1-flash-image')
|
||||
expect(selection.modelKey).toBe('openai-compatible:oa-1::gemini-3.1-flash-image')
|
||||
})
|
||||
})
|
||||
@@ -118,7 +118,7 @@ describe('generator-api gateway routing', () => {
|
||||
expect(result).toEqual({ success: true, imageUrl: 'compat-template-image' })
|
||||
})
|
||||
|
||||
it('normalizes legacy openai-compatible gemini image model ids before template gateway call', async () => {
|
||||
it('prefers the resolved openai-compatible image model id before trying aliases', async () => {
|
||||
resolveModelSelectionMock.mockResolvedValueOnce({
|
||||
provider: 'openai-compatible:oa-1',
|
||||
modelId: 'gemini-3.1-flash-image',
|
||||
@@ -137,6 +137,35 @@ describe('generator-api gateway routing', () => {
|
||||
await generateImage('user-1', 'openai-compatible:oa-1::gemini-3.1-flash-image', 'draw hero')
|
||||
|
||||
expect(generateImageViaOpenAICompatTemplateMock).toHaveBeenCalledWith(expect.objectContaining({
|
||||
modelId: 'gemini-3.1-flash-image',
|
||||
}))
|
||||
})
|
||||
|
||||
it('retries openai-compatible image generation with a compatible alias after a model-id rejection', async () => {
|
||||
resolveModelSelectionMock.mockResolvedValueOnce({
|
||||
provider: 'openai-compatible:oa-1',
|
||||
modelId: 'gemini-3.1-flash-image',
|
||||
modelKey: 'openai-compatible:oa-1::gemini-3.1-flash-image',
|
||||
mediaType: 'image',
|
||||
compatMediaTemplate: {
|
||||
version: 1,
|
||||
mediaType: 'image',
|
||||
mode: 'sync',
|
||||
create: { method: 'POST', path: '/v1/images/generations' },
|
||||
response: { outputUrlPath: '$.data[0].url' },
|
||||
},
|
||||
})
|
||||
resolveModelGatewayRouteMock.mockReturnValueOnce('openai-compat')
|
||||
generateImageViaOpenAICompatTemplateMock
|
||||
.mockRejectedValueOnce(new Error('Template request failed with status 400: invalid model'))
|
||||
.mockResolvedValueOnce({ success: true, imageUrl: 'compat-template-image' })
|
||||
|
||||
await generateImage('user-1', 'openai-compatible:oa-1::gemini-3.1-flash-image', 'draw hero')
|
||||
|
||||
expect(generateImageViaOpenAICompatTemplateMock).toHaveBeenNthCalledWith(1, expect.objectContaining({
|
||||
modelId: 'gemini-3.1-flash-image',
|
||||
}))
|
||||
expect(generateImageViaOpenAICompatTemplateMock).toHaveBeenNthCalledWith(2, expect.objectContaining({
|
||||
modelId: 'gemini-3.1-flash-image-preview',
|
||||
}))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user