import type { GenerationAdapterRequest, NormalizedGenerationOutput } from "./ai-adapter-contract.js"; import { normalizeImageOutputToRatio } from "./image-output-normalizer.mjs"; import { AdapterContractError, type AdapterStartResult, type AdapterTransport, balanceSignalFromResponse, classifyAdapterError, dimensionsForRatio, mockPngBytes, validateAdapterRequest, validateMockContract, type ModelAdapter, supportedAdapterRatios, } from "./model-adapter.js"; const expectedContract = { endpoint: "https://mock.invalid/openai/images", fixture_id: "gpt-image-mock-v1", mode: "sync", protocol_version: "gpt-image-v1", response_shape: "data.b64_json", supports_reference_images: true, supported_ratios: [...supportedAdapterRatios], }; function defaultTransport(): AdapterTransport { return { async start(request) { const dimensions = dimensionsForRatio(request.ratio ?? "3:4"); return { data: [{ b64_json: mockPngBytes("gpt-image").toString("base64"), ...dimensions }] }; }, async poll() { throw new AdapterContractError("unexpected_poll"); }, }; } export class GptImageAdapter implements ModelAdapter { readonly contractVersion = "gpt-image-v1"; readonly fixtureId = "gpt-image-mock-v1"; readonly modelId = "gpt-image-2"; private readonly transport: AdapterTransport; constructor(input: { transport?: AdapterTransport } = {}) { this.transport = input.transport ?? defaultTransport(); } async start(request: GenerationAdapterRequest): Promise { try { validateAdapterRequest(request, this.modelId); const response = await this.transport.start({ operation: "start", modelId: this.modelId, prompt: request.prompt, ratio: request.ratio, referenceAssetIds: request.referenceAssetIds }); if (response && typeof response === "object" && "status" in response && response.status === "failed") { const classified = this.classifyError("error" in response ? response.error : undefined, (request.configSnapshot.error_mapping_profile as Record | undefined) ?? {}); return { ...classified, status: "failed" }; } const output = this.normalizeOutput(response); return { outputs: [await normalizeImageOutputToRatio({ ...output, ratio: request.ratio })], status: "completed" }; } catch (error) { const classified = error instanceof AdapterContractError ? { category: "gateway_contract_invalid" as const, sourceCategory: error.sourceCategory } : this.classifyError(error, (request.configSnapshot.error_mapping_profile as Record | undefined) ?? {}); return { ...classified, status: "failed" }; } } async poll(upstreamJobReference: string): Promise { void upstreamJobReference; return { category: "gateway_contract_invalid", sourceCategory: "unexpected_poll", status: "failed" }; } normalizeOutput(response: unknown): NormalizedGenerationOutput { if (!response || typeof response !== "object" || !("data" in response) || !Array.isArray(response.data) || response.data.length !== 1) throw new AdapterContractError("response_single_image_required"); const image = response.data[0]; if (!image || typeof image !== "object") throw new AdapterContractError("response_shape_invalid"); const data = "b64_json" in image && typeof image.b64_json === "string" ? Buffer.from(image.b64_json, "base64") : undefined; const pixelHeight = "pixelHeight" in image && typeof image.pixelHeight === "number" ? image.pixelHeight : undefined; const pixelWidth = "pixelWidth" in image && typeof image.pixelWidth === "number" ? image.pixelWidth : undefined; if (!data || !data.length || !pixelHeight || !pixelWidth) throw new AdapterContractError("response_media_invalid"); return { bytes: data, mimeType: "image/png", pixelHeight, pixelWidth }; } classifyError(error: unknown, mappingProfile: Readonly>) { return classifyAdapterError(error, mappingProfile); } checkBalanceSignal(response: unknown) { return balanceSignalFromResponse(response); } validateContract(routeProfile: Readonly>) { return validateMockContract(routeProfile, expectedContract, this.modelId, this.fixtureId, this.contractVersion); } } export { expectedContract as gptImageMockContract };