feat: initial release v0.3.0

This commit is contained in:
saturn
2026-03-08 03:15:27 +08:00
commit 881ed44996
1311 changed files with 225407 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
import type { GenerateResult } from '@/lib/generators/base'
import type { OpenAICompatImageRequest } from '../types'
import {
createOpenAICompatClient,
readStringOption,
resolveOpenAICompatClientConfig,
toUploadFile,
} from './common'
type OpenAIImageResponseFormat = 'url' | 'b64_json'
type OpenAIImageOutputFormat = 'png' | 'jpeg' | 'webp'
type OpenAIImageGenerateQuality = 'standard' | 'hd' | 'low' | 'medium' | 'high' | 'auto'
type OpenAIImageGenerateSize =
| 'auto'
| '1024x1024'
| '1536x1024'
| '1024x1536'
| '256x256'
| '512x512'
| '1792x1024'
| '1024x1792'
const OPENAI_IMAGE_OPTION_KEYS = new Set([
'provider',
'modelId',
'modelKey',
'size',
'resolution',
'quality',
'responseFormat',
'outputFormat',
])
function assertAllowedOptions(options: Record<string, unknown>) {
for (const [key, value] of Object.entries(options)) {
if (value === undefined) continue
if (!OPENAI_IMAGE_OPTION_KEYS.has(key)) {
throw new Error(`OPENAI_COMPAT_IMAGE_OPTION_UNSUPPORTED: ${key}`)
}
}
}
function normalizeResponseFormat(value: unknown): OpenAIImageResponseFormat {
const normalized = readStringOption(value, 'responseFormat')
if (!normalized) return 'b64_json'
if (normalized === 'url' || normalized === 'b64_json') return normalized
throw new Error(`OPENAI_COMPAT_IMAGE_OPTION_UNSUPPORTED: responseFormat=${normalized}`)
}
function normalizeOutputFormat(value: unknown): OpenAIImageOutputFormat | undefined {
const normalized = readStringOption(value, 'outputFormat')
if (!normalized) return undefined
if (normalized === 'png' || normalized === 'jpeg' || normalized === 'webp') return normalized
throw new Error(`OPENAI_COMPAT_IMAGE_OPTION_UNSUPPORTED: outputFormat=${normalized}`)
}
function normalizeGenerateQuality(value: unknown): OpenAIImageGenerateQuality | undefined {
const normalized = readStringOption(value, 'quality')
if (!normalized) return undefined
if (
normalized === 'standard'
|| normalized === 'hd'
|| normalized === 'low'
|| normalized === 'medium'
|| normalized === 'high'
|| normalized === 'auto'
) {
return normalized
}
throw new Error(`OPENAI_COMPAT_IMAGE_OPTION_UNSUPPORTED: quality=${normalized}`)
}
function normalizeOpenAIImageSize(value: string | undefined): OpenAIImageGenerateSize | undefined {
if (!value) return undefined
if (
value === 'auto'
|| value === '1024x1024'
|| value === '1536x1024'
|| value === '1024x1536'
|| value === '256x256'
|| value === '512x512'
|| value === '1792x1024'
|| value === '1024x1792'
) {
return value
}
throw new Error(`OPENAI_COMPAT_IMAGE_OPTION_UNSUPPORTED: size=${value}`)
}
function resolveRawSize(options: Record<string, unknown>): string | undefined {
const size = readStringOption(options.size, 'size')
const resolution = readStringOption(options.resolution, 'resolution')
if (size && resolution && size !== resolution) {
throw new Error('OPENAI_COMPAT_IMAGE_OPTION_CONFLICT: size and resolution must match')
}
return size || resolution
}
function resolveModelId(modelId: string | undefined, options: Record<string, unknown>): string {
const optionModelId = readStringOption(options.modelId, 'modelId')
const selected = (modelId || optionModelId || '').trim()
if (selected) return selected
return 'gpt-image-1'
}
function toMimeFromOutputFormat(outputFormat: string | undefined): string {
if (outputFormat === 'jpeg' || outputFormat === 'jpg') return 'image/jpeg'
if (outputFormat === 'webp') return 'image/webp'
return 'image/png'
}
function readFirstImagePayload(response: unknown): { b64Json: string | null; url: string | null } {
if (typeof response !== 'object' || response === null) {
return { b64Json: null, url: null }
}
const data = (response as { data?: unknown }).data
if (!Array.isArray(data) || data.length === 0) {
return { b64Json: null, url: null }
}
const first = data[0]
if (typeof first !== 'object' || first === null) {
return { b64Json: null, url: null }
}
const rawB64 = (first as { b64_json?: unknown }).b64_json
const rawUrl = (first as { url?: unknown }).url
return {
b64Json: typeof rawB64 === 'string' ? rawB64 : null,
url: typeof rawUrl === 'string' ? rawUrl : null,
}
}
export async function generateImageViaOpenAICompat(request: OpenAICompatImageRequest): Promise<GenerateResult> {
const {
userId,
providerId,
modelId,
prompt,
referenceImages = [],
options = {},
} = request
assertAllowedOptions(options)
const config = await resolveOpenAICompatClientConfig(userId, providerId)
const client = createOpenAICompatClient(config)
const normalizedModelId = resolveModelId(modelId, options)
const responseFormat = normalizeResponseFormat(options.responseFormat)
const outputFormat = normalizeOutputFormat(options.outputFormat)
const quality = normalizeGenerateQuality(options.quality)
const rawSize = resolveRawSize(options)
const size = normalizeOpenAIImageSize(rawSize)
if (referenceImages.length > 0) {
const response = await client.images.edit({
model: normalizedModelId,
prompt,
image: await Promise.all(referenceImages.map((image, index) => toUploadFile(image, index))),
response_format: responseFormat,
...(outputFormat ? { output_format: outputFormat } : {}),
...(quality ? { quality } : {}),
...(size ? { size } : {}),
} as unknown as Parameters<typeof client.images.edit>[0])
const imagePayload = readFirstImagePayload(response)
const imageBase64 = imagePayload.b64Json
if (typeof imageBase64 === 'string' && imageBase64.trim().length > 0) {
const mimeType = toMimeFromOutputFormat(outputFormat)
return {
success: true,
imageBase64,
imageUrl: `data:${mimeType};base64,${imageBase64}`,
}
}
const imageUrl = imagePayload.url
if (typeof imageUrl === 'string' && imageUrl.trim().length > 0) {
return { success: true, imageUrl }
}
throw new Error('OPENAI_COMPAT_IMAGE_EMPTY_RESPONSE: no image data returned')
}
const response = await client.images.generate({
model: normalizedModelId,
prompt,
response_format: responseFormat,
...(outputFormat ? { output_format: outputFormat } : {}),
...(quality ? { quality } : {}),
...(size ? { size } : {}),
} as unknown as Parameters<typeof client.images.generate>[0])
const imagePayload = readFirstImagePayload(response)
const imageBase64 = imagePayload.b64Json
if (typeof imageBase64 === 'string' && imageBase64.trim().length > 0) {
const mimeType = toMimeFromOutputFormat(outputFormat)
return {
success: true,
imageBase64,
imageUrl: `data:${mimeType};base64,${imageBase64}`,
}
}
const imageUrl = imagePayload.url
if (typeof imageUrl === 'string' && imageUrl.trim().length > 0) {
return { success: true, imageUrl }
}
throw new Error('OPENAI_COMPAT_IMAGE_EMPTY_RESPONSE: no image data returned')
}