import type { GenerationAdapterRequest, NormalizedGenerationOutput } from "./ai-adapter-contract.js"; import type { GenerationErrorCategory } from "./generation-error-registry.js"; export type AdapterStartResult = | { outputs: readonly NormalizedGenerationOutput[]; status: "completed" } | { status: "pending"; upstreamJobReference: string } | { category: GenerationErrorCategory; sourceCategory: string; status: "failed" }; export interface AdapterTransportRequest { operation: "start" | "poll"; modelId: string; prompt?: string; ratio?: GenerationAdapterRequest["ratio"]; referenceAssetIds?: readonly string[]; upstreamJobReference?: string; } export interface AdapterTransport { start(request: AdapterTransportRequest): Promise; poll(request: AdapterTransportRequest): Promise; } export interface ContractValidationResult { contractVersion: string; evidenceRef: string; fixtureId: string; modelId: string; reason?: "contract_profile_mismatch"; status: "blocked" | "verified"; } export interface AdapterBalanceSignal { gatewayAccountRef: string; impactScope: "model" | "account" | "unknown"; } export interface ModelAdapter { readonly contractVersion: string; readonly fixtureId: string; readonly modelId: string; checkBalanceSignal(response: unknown): AdapterBalanceSignal | undefined; classifyError(error: unknown, mappingProfile: Readonly>): { category: GenerationErrorCategory; sourceCategory: string }; normalizeOutput(response: unknown): NormalizedGenerationOutput; poll(upstreamJobReference: string): Promise; start(request: GenerationAdapterRequest): Promise; validateContract(routeProfile: Readonly>): ContractValidationResult; } export class AdapterContractError extends Error { constructor(readonly sourceCategory: string, message = sourceCategory) { super(message); this.name = "AdapterContractError"; } } export const supportedAdapterRatios = ["3:4", "1:1", "4:3", "9:16"] as const; export function dimensionsForRatio(ratio: GenerationAdapterRequest["ratio"]) { return ratio === "3:4" ? { pixelHeight: 1440, pixelWidth: 1080 } : ratio === "1:1" ? { pixelHeight: 1080, pixelWidth: 1080 } : ratio === "4:3" ? { pixelHeight: 1080, pixelWidth: 1440 } : { pixelHeight: 1920, pixelWidth: 1080 }; } export function mockPngBytes(seed: string) { const body = Buffer.from(`DADA-MOCK-${seed}`, "ascii"); return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), body]); } export function validateAdapterRequest(request: GenerationAdapterRequest, modelId: string) { if (request.modelId !== modelId) throw new AdapterContractError("model_id_mismatch"); if (!supportedAdapterRatios.includes(request.ratio)) throw new AdapterContractError("ratio_unsupported"); if (request.referenceAssetIds.length > 2) throw new AdapterContractError("reference_count_exceeded"); if (!request.prompt.trim()) throw new AdapterContractError("prompt_empty"); } const errorCategories = new Set([ "upstream_timeout", "upstream_failed", "safety_rejected", "model_disabled", "gateway_balance_insufficient", "gateway_contract_invalid", "reference_invalid", "unknown_retryable", "unknown_non_retryable", ]); export function classifyAdapterError(error: unknown, mappingProfile: Readonly>) { const code = error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : "unknown"; const mapped = mappingProfile[code] ?? (code === "timeout" ? "upstream_timeout" : code === "safety" ? "safety_rejected" : "unknown_retryable"); const category = errorCategories.has(mapped as GenerationErrorCategory) ? mapped as GenerationErrorCategory : "unknown_retryable"; const sourceCategory = /^[a-z][a-z0-9_]{0,79}$/.test(code) ? code : "adapter_error"; return { category, sourceCategory }; } export function balanceSignalFromResponse(response: unknown): AdapterBalanceSignal | undefined { if (!response || typeof response !== "object" || !("balance_signal" in response)) return undefined; const signal = response.balance_signal; if (!signal || typeof signal !== "object") return undefined; const gatewayAccountRef = "gateway_account_ref" in signal && typeof signal.gateway_account_ref === "string" ? signal.gateway_account_ref : ""; const impactScope = "impact_scope" in signal && (signal.impact_scope === "model" || signal.impact_scope === "account" || signal.impact_scope === "unknown") ? signal.impact_scope : undefined; return gatewayAccountRef && impactScope ? { gatewayAccountRef, impactScope } : undefined; } export function validateMockContract( routeProfile: Readonly>, expected: Readonly>, modelId: string, fixtureId: string, contractVersion: string, ): ContractValidationResult { const matches = Object.entries(expected).every(([key, value]) => JSON.stringify(routeProfile[key]) === JSON.stringify(value)); return matches ? { contractVersion, evidenceRef: `${fixtureId}:mock`, fixtureId, modelId, status: "verified" } : { contractVersion, evidenceRef: `${fixtureId}:mock`, fixtureId, modelId, reason: "contract_profile_mismatch", status: "blocked" }; }