77 lines
4.2 KiB
TypeScript
77 lines
4.2 KiB
TypeScript
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<AdapterStartResult> {
|
|
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<string, string> | 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<string, string> | undefined) ?? {});
|
|
return { ...classified, status: "failed" };
|
|
}
|
|
}
|
|
|
|
async poll(upstreamJobReference: string): Promise<AdapterStartResult> {
|
|
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<Record<string, string>>) { return classifyAdapterError(error, mappingProfile); }
|
|
checkBalanceSignal(response: unknown) { return balanceSignalFromResponse(response); }
|
|
validateContract(routeProfile: Readonly<Record<string, unknown>>) { return validateMockContract(routeProfile, expectedContract, this.modelId, this.fixtureId, this.contractVersion); }
|
|
}
|
|
|
|
export { expectedContract as gptImageMockContract };
|