Files
tyx_AI_xhs/apps/worker/src/ai-adapter-gemini-flash.ts
T

82 lines
4.5 KiB
TypeScript

import type { GenerationAdapterRequest, NormalizedGenerationOutput } from "./ai-adapter-contract.js";
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/gemini/flash",
fixture_id: "gemini-flash-mock-v1",
mode: "sync",
protocol_version: "gemini-flash-v1",
response_shape: "candidates.inline_data",
supports_reference_images: true,
supported_ratios: [...supportedAdapterRatios],
};
function defaultTransport(): AdapterTransport {
return {
async start(request) {
const dimensions = dimensionsForRatio(request.ratio ?? "3:4");
return { candidates: [{ inline_data: { data: mockPngBytes("flash").toString("base64"), mime_type: "image/png" }, ...dimensions }] };
},
async poll() { throw new AdapterContractError("unexpected_poll"); },
};
}
export class GeminiFlashAdapter implements ModelAdapter {
readonly contractVersion = "gemini-flash-v1";
readonly fixtureId = "gemini-flash-mock-v1";
readonly modelId = "gemini-3.1-flash-image-preview";
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" };
}
return { outputs: [this.normalizeOutput(response)], 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" || !("candidates" in response) || !Array.isArray(response.candidates) || response.candidates.length !== 1) {
throw new AdapterContractError("response_single_image_required");
}
const candidate = response.candidates[0];
if (!candidate || typeof candidate !== "object" || !("inline_data" in candidate) || !candidate.inline_data || typeof candidate.inline_data !== "object") {
throw new AdapterContractError("response_shape_invalid");
}
const data = "data" in candidate.inline_data && typeof candidate.inline_data.data === "string" ? Buffer.from(candidate.inline_data.data, "base64") : undefined;
const mimeType = "mime_type" in candidate.inline_data && candidate.inline_data.mime_type === "image/png" ? "image/png" : undefined;
const pixelHeight = "pixelHeight" in candidate && typeof candidate.pixelHeight === "number" ? candidate.pixelHeight : undefined;
const pixelWidth = "pixelWidth" in candidate && typeof candidate.pixelWidth === "number" ? candidate.pixelWidth : undefined;
if (!data || !data.length || !mimeType || !pixelHeight || !pixelWidth) throw new AdapterContractError("response_media_invalid");
return { bytes: data, mimeType, 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 geminiFlashMockContract };