import sharp from "sharp"; import type { GenerationAdapter, GenerationAdapterRequest, GenerationAdapterResult, NormalizedGenerationOutput, } from "./ai-adapter-contract.js"; import { gptImageRequestSizeForRatio, normalizeImageOutputToRatio } from "./image-output-normalizer.mjs"; const geminiProductModelId = "gemini-3.1-flash-image-preview"; const geminiProviderModelId = "gemini-3.1-flash-image"; const gptImageModelId = "gpt-image-2"; const geminiEndpoint = "https://oneapi.intelligrow.cn/v1/chat/completions"; const gptImageEndpoint = "https://oneapi.intelligrow.cn/v1/images/generations"; const gptImageReferenceEndpoint = "https://oneapi.intelligrow.cn/v1/images/edits"; const maximumResponseBytes = 32 * 1024 * 1024; const requestTimeoutMilliseconds = 180_000; type FetchLike = typeof fetch; class OneApiRuntimeError extends Error { constructor( readonly category: "gateway_balance_insufficient" | "gateway_contract_invalid" | "model_disabled" | "reference_invalid" | "upstream_failed" | "upstream_timeout" | "unknown_non_retryable", readonly sourceCategory: string, ) { super(sourceCategory); } } function failure(error: unknown): GenerationAdapterResult { if (error instanceof OneApiRuntimeError) { return { category: error.category, sourceCategory: error.sourceCategory, status: "failed" }; } if (error instanceof Error && error.name === "AbortError") { return { category: "upstream_timeout", sourceCategory: "upstream_timeout", status: "failed" }; } return { category: "upstream_failed", sourceCategory: "upstream_failed", status: "failed" }; } function mapHttpFailure(status: number) { if (status === 408 || status === 504) return new OneApiRuntimeError("upstream_timeout", `upstream_http_${status}`); if (status === 429) return new OneApiRuntimeError("gateway_balance_insufficient", "upstream_http_429"); if (status >= 500) return new OneApiRuntimeError("upstream_failed", `upstream_http_${status}`); if (status === 400 || status === 404 || status === 422) return new OneApiRuntimeError("gateway_contract_invalid", `upstream_http_${status}`); return new OneApiRuntimeError("unknown_non_retryable", `upstream_http_${status}`); } async function readBoundedJson(response: Response) { const declaredLength = Number(response.headers.get("content-length") ?? 0); if (Number.isFinite(declaredLength) && declaredLength > maximumResponseBytes) { throw new OneApiRuntimeError("gateway_contract_invalid", "upstream_response_too_large"); } if (!response.body) throw new OneApiRuntimeError("gateway_contract_invalid", "upstream_response_empty"); const reader = response.body.getReader(); const chunks: Buffer[] = []; let total = 0; try { while (true) { const next = await reader.read(); if (next.done) break; const chunk = Buffer.from(next.value); total += chunk.length; if (total > maximumResponseBytes) { await reader.cancel(); throw new OneApiRuntimeError("gateway_contract_invalid", "upstream_response_too_large"); } chunks.push(chunk); } try { return JSON.parse(Buffer.concat(chunks).toString("utf8")) as unknown; } catch { throw new OneApiRuntimeError("gateway_contract_invalid", "upstream_response_invalid"); } } finally { for (const chunk of chunks) chunk.fill(0); } } function extractGeminiImage(response: unknown) { if (!response || typeof response !== "object" || !("choices" in response) || !Array.isArray(response.choices)) { throw new OneApiRuntimeError("gateway_contract_invalid", "response_shape_invalid"); } const choice = response.choices[0]; const content = choice && typeof choice === "object" && "message" in choice && choice.message && typeof choice.message === "object" && "content" in choice.message && typeof choice.message.content === "string" ? choice.message.content : ""; const matches = [...content.matchAll(/!\[[^\]]*\]\(\s*data:(image\/(?:jpeg|png|webp));base64,([A-Za-z0-9+/=\r\n]+)\s*\)/gi)]; if (matches.length !== 1) throw new OneApiRuntimeError("gateway_contract_invalid", "response_single_image_required"); return { bytes: Buffer.from(matches[0]![2]!, "base64"), declaredMimeType: matches[0]![1]!.toLowerCase() }; } function extractGptImage(response: unknown) { if (!response || typeof response !== "object" || !("data" in response) || !Array.isArray(response.data) || response.data.length !== 1 || !response.data[0] || typeof response.data[0] !== "object" || !("b64_json" in response.data[0]) || typeof response.data[0].b64_json !== "string") { throw new OneApiRuntimeError("gateway_contract_invalid", "response_single_image_required"); } return { bytes: Buffer.from(response.data[0].b64_json, "base64"), declaredMimeType: undefined }; } async function normalizeOutput(bytes: Buffer, declaredMimeType: string | undefined, ratio: GenerationAdapterRequest["ratio"]): Promise { try { const metadata = await sharp(bytes, { failOn: "error", limitInputPixels: 40_000_000 }).metadata(); const mimeType = metadata.format === "png" ? "image/png" : metadata.format === "jpeg" ? "image/jpeg" : metadata.format === "webp" ? "image/webp" : undefined; if (!mimeType || !metadata.width || !metadata.height || (declaredMimeType && declaredMimeType !== mimeType)) { throw new OneApiRuntimeError("gateway_contract_invalid", "response_media_invalid"); } const normalized = await normalizeImageOutputToRatio({ bytes, mimeType, pixelHeight: metadata.height, pixelWidth: metadata.width, ratio }); return { bytes: normalized.bytes, mimeType: normalized.mimeType, pixelHeight: normalized.pixelHeight, pixelWidth: normalized.pixelWidth }; } catch (error) { if (error instanceof OneApiRuntimeError) throw error; throw new OneApiRuntimeError("gateway_contract_invalid", "response_media_invalid"); } } function validateRequest(request: GenerationAdapterRequest) { if (!request.prompt.trim() || request.prompt.length > 1_000) throw new OneApiRuntimeError("gateway_contract_invalid", "prompt_invalid"); const references = request.referenceImages ?? []; if (references.length !== request.referenceAssetIds.length || references.length > 2) { throw new OneApiRuntimeError("reference_invalid", "reference_count_invalid"); } const totalBytes = references.reduce((total, reference) => total + reference.bytes.length, 0); if (totalBytes > 20 * 1024 * 1024 || references.some((reference) => reference.bytes.length === 0 || reference.bytes.length > 10 * 1024 * 1024)) { throw new OneApiRuntimeError("reference_invalid", "reference_size_invalid"); } return references; } function buildRequest(request: GenerationAdapterRequest) { const references = validateRequest(request); if (request.modelId === geminiProductModelId) { const content = references.length === 0 ? request.prompt : [ { text: request.prompt, type: "text" }, ...references.map((reference) => ({ image_url: { url: `data:${reference.mimeType};base64,${reference.bytes.toString("base64")}` }, type: "image_url", })), ]; return { body: JSON.stringify({ extra_body: { google: { image_config: { aspect_ratio: request.ratio, image_size: "1K" } } }, messages: [{ content, role: "user" }], model: geminiProviderModelId, stream: false, }), contentType: "application/json", endpoint: geminiEndpoint, parser: extractGeminiImage, }; } if (request.modelId !== gptImageModelId) throw new OneApiRuntimeError("model_disabled", "model_not_supported"); if (references.length > 0) { const form = new FormData(); form.append("model", gptImageModelId); form.append("prompt", request.prompt); form.append("response_format", "b64_json"); form.append("size", gptImageRequestSizeForRatio(request.ratio)); references.forEach((reference, index) => form.append("image[]", new Blob([reference.bytes], { type: reference.mimeType }), `reference-${index + 1}.png`)); return { body: form, contentType: undefined, endpoint: gptImageReferenceEndpoint, parser: extractGptImage }; } return { body: JSON.stringify({ model: gptImageModelId, prompt: request.prompt, response_format: "b64_json", size: gptImageRequestSizeForRatio(request.ratio) }), contentType: "application/json", endpoint: gptImageEndpoint, parser: extractGptImage, }; } export class OneApiGenerationAdapter implements GenerationAdapter { private readonly credential: Buffer; private readonly fetchImpl: FetchLike; private disposed = false; constructor(input: { credential: Buffer; fetch?: FetchLike }) { if (input.credential.length < 8) throw new Error("ai_gateway_credential_invalid"); this.credential = Buffer.from(input.credential); this.fetchImpl = input.fetch ?? fetch; } async start(request: GenerationAdapterRequest): Promise { if (this.disposed) return { category: "upstream_failed", sourceCategory: "adapter_disposed", status: "failed" }; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), requestTimeoutMilliseconds); let sourceBytes: Buffer | undefined; try { const providerRequest = buildRequest(request); const headers = new Headers({ authorization: `Bearer ${this.credential.toString("utf8")}` }); if (providerRequest.contentType) headers.set("content-type", providerRequest.contentType); const response = await this.fetchImpl(providerRequest.endpoint, { body: providerRequest.body, headers, method: "POST", redirect: "error", signal: controller.signal, }); if (!response.ok) throw mapHttpFailure(response.status); const parsed = await readBoundedJson(response); const extracted = providerRequest.parser(parsed); sourceBytes = extracted.bytes; const output = await normalizeOutput(sourceBytes, extracted.declaredMimeType, request.ratio); return { outputs: [output], status: "completed" }; } catch (error) { return failure(error); } finally { clearTimeout(timeout); sourceBytes?.fill(0); } } dispose() { if (this.disposed) return; this.disposed = true; this.credential.fill(0); } }