import type { GenerationProvider, GenerationRequest, } from "@wrnexus/design-studio"; export const APIMART_MODEL = "gpt-5-mini"; export const AI_TIMEOUT_MS = 60_000; export const AI_OUTPUT_TOKENS = 8192; type FailureKind = | "auth" | "quota" | "model" | "rate" | "unavailable" | "network" | "truncated" | "filtered" | "response" | "request"; const failureMessages: Record = { auth: "APIMart rejected the server API key. Check APIMART_API_KEY and its permissions, then restart the app.", quota: "APIMart reports insufficient credit or quota. Check the provider account balance and limits.", model: "APIMart could not access the configured model. Check that this model is enabled for the server API key.", rate: "APIMart is rate limiting requests. Wait a minute and generate again.", unavailable: "APIMart is temporarily unavailable. Try generating again shortly.", network: "The server could not reach APIMart. Check the VM's outbound HTTPS connection and try again.", truncated: "APIMart stopped before completing all design alternatives. Try a shorter brief or generate again.", filtered: "APIMart declined this generation. Revise the brief and try again.", response: "APIMart returned an empty or unreadable completion. Try generating again.", request: "APIMart rejected the completion request. Check the provider's model configuration.", }; class APIMartFailure extends Error { constructor( readonly kind: FailureKind, readonly status?: number, ) { super(failureMessages[kind]); } get retryable() { return ["rate", "unavailable", "network", "truncated", "response"].includes( this.kind, ); } } function failureFor(status: number, code?: unknown) { // Only recognized codes influence the category; upstream text never leaves this adapter. const known = typeof code === "string" ? code.toLowerCase() : ""; const kind: FailureKind = status === 401 || status === 403 || known === "invalid_api_key" ? "auth" : status === 402 || ["insufficient_quota", "insufficient_balance"].includes(known) ? "quota" : status === 404 || known === "model_not_found" ? "model" : status === 429 || known === "rate_limit_exceeded" ? "rate" : status >= 500 ? "unavailable" : "request"; return new APIMartFailure(kind, status); } export function apimartProvider( apiKey: string, transport: typeof fetch = fetch, model = APIMART_MODEL, ): GenerationProvider { return { name: "APIMart", async generate(prompt, options = {}) { let response: Response; try { response = await transport( "https://api.apimart.ai/v1/chat/completions", { method: "POST", signal: options.signal, redirect: "error", headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json", }, body: JSON.stringify({ model, stream: false, max_tokens: options.maxTokens ?? AI_OUTPUT_TOKENS, messages: [ { role: "system", content: options.system ?? "Return valid design proposal JSON only.", }, { role: "user", content: prompt }, ], }), }, ); } catch { options.signal?.throwIfAborted(); throw new APIMartFailure("network"); } // Never forward upstream bodies: they may echo credentials or submitted context. if (!response.ok) { // Read a bounded envelope only to recognize documented machine codes. let code: unknown; try { code = JSON.parse(await readBounded(response, 200_000)).error?.code; } catch { /* Ignore raw error text. */ } throw failureFor(response.status, code); } let result; try { result = JSON.parse(await readBounded(response, 200_000)); } catch { options.signal?.throwIfAborted(); throw new APIMartFailure("response"); } if (result?.error) throw failureFor(400, result.error.code); const choice = result?.choices?.[0]; if (choice?.finish_reason === "length") throw new APIMartFailure("truncated"); if (choice?.finish_reason === "content_filter") throw new APIMartFailure("filtered"); if ( typeof choice?.message?.content !== "string" || !choice.message.content.trim() ) throw new APIMartFailure("response"); return { value: choice.message.content, provider: "APIMart", model, }; }, }; } async function readBounded(source: Request | Response, limit: number) { const reader = source.body?.getReader(); const decoder = new TextDecoder(); let text = "", bytes = 0; if (!reader) return text; try { while (true) { const chunk = await reader.read(); if (chunk.done) break; bytes += chunk.value.byteLength; if (bytes > limit) { await reader.cancel(); throw Error("Request or response is too large."); } text += decoder.decode(chunk.value, { stream: true }); } return text + decoder.decode(); } finally { reader.releaseLock(); } } function synchronousStudioAI( apiKey?: string, transport: typeof fetch = fetch, model = APIMART_MODEL, ) { let active = 0, windowStart = Date.now(), count = 0; const json = (value: unknown, status = 200) => new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json", "cache-control": "no-store", }, }); return async (request: Request): Promise => { const url = new URL(request.url); if ( !apiKey || ![ "/design-studio/api/generation", "/design-studio/api/generate", ].includes(url.pathname) ) return; const scheme = request.headers.get("x-forwarded-proto"); if (scheme === "https" || scheme === "http") url.protocol = scheme + ":"; if ( request.headers.get("origin") && request.headers.get("origin") !== url.origin ) return json({ error: "Same-origin requests only." }, 403); if (url.pathname.endsWith("/generation") && request.method === "GET") return json({ provider: "APIMart", model, templateVersion: 1, delivery: "polling", maxAttempts: 2, maxOutputTokens: AI_OUTPUT_TOKENS, timeoutMs: AI_TIMEOUT_MS, }); if (!url.pathname.endsWith("/generate") || request.method !== "POST") return json({ error: "Method not allowed." }, 405); if ( !(request.headers.get("content-type") ?? "").startsWith( "application/json", ) ) return json({ error: "JSON required." }, 415); if (Date.now() - windowStart >= 60000) { windowStart = Date.now(); count = 0; } if (active >= 2 || count >= 10) return json( { error: "AI generation is busy. Try again in a minute." }, 429, ); active++; try { const input = JSON.parse( await readBounded(request, 2_000_000), ) as GenerationRequest; count++; // Preserve the installed engine's validation, locks, revision checks and repair budget. const module = import.meta.resolve("@wrnexus/design-studio"); const { generateDesigns } = (await import( module )) as typeof import("@wrnexus/design-studio"); let calls = 0; let exhausted = false; let failure: APIMartFailure | undefined; const upstream = apimartProvider(apiKey, transport, model); const provider: GenerationProvider = { name: upstream.name, async generate(prompt, options) { // The repair pass and transport retry share one two-call budget. if (calls >= 2) { exhausted = true; throw failure ?? new APIMartFailure("response"); } calls++; failure = undefined; try { return await upstream.generate(prompt, options); } catch (error) { options?.signal?.throwIfAborted(); failure = error instanceof APIMartFailure ? error : new APIMartFailure("response"); throw failure; } }, }; const deadline = AbortSignal.any([ request.signal, AbortSignal.timeout(AI_TIMEOUT_MS), ]); const generate = () => generateDesigns(input, { provider, model, signal: deadline, timeoutMs: AI_TIMEOUT_MS, maxOutputTokens: AI_OUTPUT_TOKENS, }); let result = await generate(); if ( !result.alternatives.length && failure?.retryable && calls < 2 && !deadline.aborted ) result = await generate(); result.provenance.attempts = calls; if (exhausted && !failure && !result.alternatives.length) { result.diagnostics = [ { code: "WRN-STUDIO-PROPOSAL", path: "$", message: "APIMart returned a design that did not meet the schema after both provider requests were used. Generate again. Your draft and previous alternatives are unchanged.", }, ]; } if ( failure && result.diagnostics.some((d) => d.code === "WRN-STUDIO-PROVIDER") ) { const reference = crypto.randomUUID(); console.warn( JSON.stringify({ event: "studio-ai-provider-failure", reference, category: failure.kind, status: failure.status, attempts: calls, }), ); result.diagnostics = [ { code: "WRN-STUDIO-PROVIDER", path: "$", message: `${failure.message} Your draft and previous alternatives are unchanged. Reference: ${reference}`, }, ]; } return json(result); } catch (error) { const message = error instanceof Error ? error.message : "Invalid generation request."; if (message.includes("WRN-STUDIO-CONFLICT")) return json( { error: "Your draft changed. Generate again from the current draft.", }, 409, ); if (message.includes("too large")) return json({ error: "Request is too large." }, 413); if ( request.signal.aborted || (error instanceof Error && ["AbortError", "TimeoutError"].includes(error.name)) ) return json( { error: "AI generation timed out or was cancelled. Your draft is unchanged.", }, 408, ); return json( { error: "Could not generate a design. Check the brief and server AI configuration. Your draft is unchanged.", }, 400, ); } finally { active--; } }; } /** Short HTTP requests keep the deployment proxy out of the provider wait. */ export function studioAI( apiKey?: string, transport: typeof fetch = fetch, model = APIMART_MODEL, ) { const run = synchronousStudioAI(apiKey, transport, model); type Job = { status: "pending" | "complete" | "failed" | "cancelled"; controller: AbortController; expires: number; result?: unknown; error?: string; }; const jobs = new Map(); const json = (value: unknown, status = 200) => Response.json(value, { status, headers: { "cache-control": "no-store" } }); return async (request: Request): Promise => { const url = new URL(request.url); const match = /^\/design-studio\/api\/generate\/jobs\/([a-f0-9-]{36})$/.exec( url.pathname, ); const enqueue = url.pathname === "/design-studio/api/generate" && request.method === "POST" && request.headers.get("prefer")?.includes("respond-async"); if (!apiKey || (!match && !enqueue)) return run(request); const scheme = request.headers.get("x-forwarded-proto"); if (scheme === "http" || scheme === "https") url.protocol = scheme + ":"; if ( request.headers.get("origin") && request.headers.get("origin") !== url.origin ) return json({ error: "Same-origin requests only." }, 403); for (const [id, job] of jobs) if (job.expires < Date.now()) { job.controller.abort(); jobs.delete(id); } if (match) { if (!["GET", "DELETE"].includes(request.method)) return json({ error: "Method not allowed." }, 405); const job = jobs.get(match[1]!); if (!job) return json( { error: "This generation job expired or the server restarted. Your draft is preserved; generate again.", }, 410, ); if (request.method === "DELETE" && job.status === "pending") { job.status = "cancelled"; job.error = "Generation cancelled. Your draft is unchanged."; job.controller.abort(); } return json({ status: job.status, ...(job.result ? { result: job.result } : {}), ...(job.error ? { error: job.error } : {}), }); } if ( !(request.headers.get("content-type") ?? "").startsWith( "application/json", ) ) return json({ error: "JSON required." }, 415); if ( [...jobs.values()].filter((job) => job.status === "pending").length >= 2 ) return json({ error: "AI generation is busy. Try again shortly." }, 429); let body: string; try { body = await readBounded(request, 2_000_000); JSON.parse(body); } catch { return json({ error: "Invalid or oversized generation request." }, 400); } // Recheck after reading the body so concurrent uploads cannot bypass the limit. if ( [...jobs.values()].filter((job) => job.status === "pending").length >= 2 ) return json({ error: "AI generation is busy. Try again shortly." }, 429); if (jobs.size >= 20) { const old = [...jobs].find(([, job]) => job.status !== "pending"); if (old) jobs.delete(old[0]); } const id = crypto.randomUUID(); const job: Job = { status: "pending", controller: new AbortController(), expires: Date.now() + 5 * 60_000, }; jobs.set(id, job); const headers = new Headers(request.headers); headers.delete("prefer"); const task = new Request(request.url, { method: "POST", headers, body, signal: job.controller.signal, }); void run(task) .then(async (response) => { if (job.status !== "pending") return; if (!response) throw Error("Generation is not configured."); const result = await response.json(); if (response.ok) { job.status = "complete"; job.result = result; } else { job.status = "failed"; job.error = typeof result.error === "string" ? result.error : "Generation did not complete."; } }) .catch(() => { if (job.status === "pending") { job.status = "failed"; job.error = "Generation could not complete. Your draft is preserved; try again."; } }); return json({ job: { id, status: "pending" } }, 202); }; }