fix image model alias compatibility
Some checks failed
Build & Push Docker Image / build-and-push (push) Has been cancelled

This commit is contained in:
2026-04-20 10:39:59 +08:00
parent 022d581c60
commit d2e793c6cf
4 changed files with 240 additions and 15 deletions

View File

@@ -59,6 +59,12 @@ interface CustomProvider {
type LlmProtocolType = 'responses' | 'chat-completions'
const IMAGE_MODEL_ID_ALIASES: Record<string, string> = {
'gemini-3-pro-image': 'gemini-3-pro-image-preview',
'gemini-3.1-flash-image': 'gemini-3.1-flash-image-preview',
'gemini-2.5-flash-image': 'gemini-2.5-flash-image-preview',
}
function normalizeProviderBaseUrl(providerId: string, rawBaseUrl?: string): string | undefined {
const providerKey = getProviderKey(providerId)
if (providerKey === 'minimax') {
@@ -110,6 +116,23 @@ function isLlmProtocol(value: unknown): value is LlmProtocolType {
return value === 'responses' || value === 'chat-completions'
}
function resolveCompatibleImageModelIds(modelId: string): string[] {
const normalized = modelId.trim()
if (!normalized) return []
const candidates = new Set<string>([normalized])
const forwardAlias = IMAGE_MODEL_ID_ALIASES[normalized]
if (forwardAlias) {
candidates.add(forwardAlias)
}
for (const [legacyModelId, canonicalModelId] of Object.entries(IMAGE_MODEL_ID_ALIASES)) {
if (canonicalModelId === normalized) {
candidates.add(legacyModelId)
}
}
return Array.from(candidates)
}
function assertModelKey(value: string, field: string): { provider: string; modelId: string; modelKey: string } {
const parsed = parseModelKeyStrict(value)
if (!parsed) {
@@ -303,9 +326,16 @@ async function readUserConfig(userId: string): Promise<{ models: CustomModel[];
}
}
function findModelByKey(models: CustomModel[], modelKey: string): CustomModel | null {
function findModelByKey(models: CustomModel[], modelKey: string, mediaType?: ModelMediaType): CustomModel | null {
const parsed = assertModelKey(modelKey, 'model')
return models.find((model) => model.modelId === parsed.modelId && model.provider === parsed.provider) || null
const exactMatch = models.find((model) => model.modelId === parsed.modelId && model.provider === parsed.provider) || null
if (exactMatch) return exactMatch
if (mediaType !== 'image') return null
const compatibleIds = resolveCompatibleImageModelIds(parsed.modelId)
if (compatibleIds.length <= 1) return null
return models.find((model) => model.provider === parsed.provider && compatibleIds.includes(model.modelId)) || null
}
/**
@@ -328,7 +358,7 @@ export async function resolveModelSelection(
const parsed = assertModelKey(model, `${mediaType} model`)
const models = await getModelsByType(userId, mediaType)
const exact = findModelByKey(models, parsed.modelKey)
const exact = findModelByKey(models, parsed.modelKey, mediaType)
if (!exact) {
throw new Error(`MODEL_NOT_FOUND: ${parsed.modelKey} is not enabled for ${mediaType}`)
}

View File

@@ -29,10 +29,90 @@ const OPENAI_COMPAT_IMAGE_MODEL_ID_ALIASES: Record<string, string> = {
'gemini-2.5-flash-image': 'gemini-2.5-flash-image-preview',
}
function normalizeOpenAICompatImageModelId(modelId: string): string {
function resolveOpenAICompatImageModelIdCandidates(modelId: string): string[] {
const normalized = modelId.trim()
if (!normalized) return normalized
return OPENAI_COMPAT_IMAGE_MODEL_ID_ALIASES[normalized] || normalized
if (!normalized) return []
const candidates = new Set<string>([normalized])
const forwardAlias = OPENAI_COMPAT_IMAGE_MODEL_ID_ALIASES[normalized]
if (forwardAlias) {
candidates.add(forwardAlias)
}
for (const [legacyModelId, canonicalModelId] of Object.entries(OPENAI_COMPAT_IMAGE_MODEL_ID_ALIASES)) {
if (canonicalModelId === normalized) {
candidates.add(legacyModelId)
}
}
return Array.from(candidates)
}
function shouldRetryOpenAICompatImageModel(error: unknown): boolean {
const message = (error instanceof Error ? error.message : String(error || '')).trim().toLowerCase()
if (!message) return false
return (
message === 'openai_error'
|| message.includes('invalid model')
|| message.includes('unknown model')
|| message.includes('unrecognized model')
|| message.includes('unsupported model')
|| message.includes('model not supported')
|| message.includes('model_not_found')
|| message.includes('model not found')
|| message.includes('param=model')
|| message.includes('parameter=model')
)
}
async function generateOpenAICompatImageWithFallback(input: {
userId: string
providerId: string
modelId: string
modelKey: string
prompt: string
referenceImages?: string[]
options: Record<string, unknown>
compatTemplate?: unknown
}): Promise<GenerateResult> {
const modelIdCandidates = resolveOpenAICompatImageModelIdCandidates(input.modelId)
let lastError: unknown = null
for (let index = 0; index < modelIdCandidates.length; index += 1) {
const candidateModelId = modelIdCandidates[index]
try {
if (input.compatTemplate) {
return await generateImageViaOpenAICompatTemplate({
userId: input.userId,
providerId: input.providerId,
modelId: candidateModelId,
modelKey: input.modelKey,
prompt: input.prompt,
referenceImages: input.referenceImages,
options: input.options,
profile: 'openai-compatible',
template: input.compatTemplate as Parameters<typeof generateImageViaOpenAICompatTemplate>[0]['template'],
})
}
return await generateImageViaOpenAICompat({
userId: input.userId,
providerId: input.providerId,
modelId: candidateModelId,
prompt: input.prompt,
referenceImages: input.referenceImages,
options: input.options,
profile: 'openai-compatible',
})
} catch (error) {
lastError = error
const hasMoreCandidates = index < modelIdCandidates.length - 1
if (!hasMoreCandidates || !shouldRetryOpenAICompatImageModel(error)) {
throw error
}
_ulogInfo(`[generateImage] retrying openai-compatible image model fallback: ${input.modelId} -> ${modelIdCandidates[index + 1]}`)
}
}
throw lastError instanceof Error ? lastError : new Error('OPENAI_COMPAT_IMAGE_GENERATION_FAILED')
}
/**
@@ -117,15 +197,14 @@ export async function generateImage(
const { referenceImages, ...generatorOptions } = options || {}
if (gatewayRoute === 'openai-compat') {
const compatTemplate = selection.compatMediaTemplate
const normalizedCompatModelId = normalizeOpenAICompatImageModelId(selection.modelId)
if (providerKey === 'openai-compatible' && !compatTemplate) {
throw new Error(`MODEL_COMPAT_MEDIA_TEMPLATE_REQUIRED: ${selection.modelKey}`)
}
if (compatTemplate) {
return await generateImageViaOpenAICompatTemplate({
return await generateOpenAICompatImageWithFallback({
userId,
providerId: selection.provider,
modelId: normalizedCompatModelId,
modelId: selection.modelId,
modelKey: selection.modelKey,
prompt,
referenceImages,
@@ -135,8 +214,7 @@ export async function generateImage(
modelId: selection.modelId,
modelKey: selection.modelKey,
},
profile: 'openai-compatible',
template: compatTemplate,
compatTemplate,
})
}
@@ -151,10 +229,11 @@ export async function generateImage(
delete openaiCompatOptions.aspectRatio
}
return await generateImageViaOpenAICompat({
return await generateOpenAICompatImageWithFallback({
userId,
providerId: selection.provider,
modelId: normalizedCompatModelId,
modelId: selection.modelId,
modelKey: selection.modelKey,
prompt,
referenceImages,
options: {
@@ -163,7 +242,6 @@ export async function generateImage(
modelId: selection.modelId,
modelKey: selection.modelKey,
},
profile: 'openai-compatible',
})
}