From 27dcb561db249ec6b33931b2c51dbca9f99106b0 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Mon, 19 Oct 2026 14:29:15 +0530 Subject: [PATCH] fix(studio): allow sixty seconds for AI generation --- README.md | 4 ++-- app/lib/studio-ai.ts | 4 +++- bun.lock | 4 ++-- package-lock.json | 8 +++---- package.json | 2 +- scripts/verify-deployment.ts | 4 +++- test/studio-ai.test.ts | 46 +++++++++++++++++++++++++++++++++++- 7 files changed, 60 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 16dd7b1..437b59d 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ AI generation sends the current design, brief and allowed component capabilities to APIMart when you request generation. The engine validates proposals and locks before offering them for review. Failed requests leave the draft unchanged. Requests allow two concurrent generations and ten starts per minute per server -process, with two attempts, 4096 output tokens per attempt and a 20-second deadline. +process, with two attempts, 4096 output tokens per attempt and a 60-second total deadline. Without APIMART_API_KEY, generation uses local rules and reports that in the UI; local rules do not interpret arbitrary product descriptions. Drafts remain in browser storage; export before clearing browser data or moving to another origin. @@ -167,7 +167,7 @@ load failures instead of leaving the editor permanently at Checking. ### Linux component previews -Studio is pinned to 0.1.1, which resolves component files through the UI registry +Studio is pinned to 0.1.2, which resolves component files through the UI registry instead of assuming a filename from the public component name. This fixes Linux lookups such as Input -> input.wrn. Install the committed lockfile before rebuilding and restarting; restarting a deployment still using Studio 0.1.0 diff --git a/app/lib/studio-ai.ts b/app/lib/studio-ai.ts index 563efbb..b61abaf 100644 --- a/app/lib/studio-ai.ts +++ b/app/lib/studio-ai.ts @@ -4,6 +4,7 @@ import type { } from "@wrnexus/design-studio"; export const APIMART_MODEL = "gpt-6-luna"; +export const AI_TIMEOUT_MS = 60_000; export function apimartProvider( apiKey: string, transport: typeof fetch = fetch, @@ -117,7 +118,7 @@ export function studioAI(apiKey?: string, transport: typeof fetch = fetch) { templateVersion: 1, maxAttempts: 2, maxOutputTokens: 4096, - timeoutMs: 20000, + timeoutMs: AI_TIMEOUT_MS, }); if (!url.pathname.endsWith("/generate") || request.method !== "POST") return json({ error: "Method not allowed." }, 405); @@ -151,6 +152,7 @@ export function studioAI(apiKey?: string, transport: typeof fetch = fetch) { provider: apimartProvider(apiKey, transport), model: APIMART_MODEL, signal: request.signal, + timeoutMs: AI_TIMEOUT_MS, }); return json(result); } catch (error) { diff --git a/bun.lock b/bun.lock index b3823ed..3284cf1 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "component-showcase", "dependencies": { "@wrnexus/core": "0.8.25", - "@wrnexus/design-studio": "0.1.1", + "@wrnexus/design-studio": "0.1.2", "@wrnexus/design-system": "0.1.0", "@wrnexus/dev-server": "0.8.83", "@wrnexus/helpers": "0.8.10", @@ -164,7 +164,7 @@ "@wrnexus/db": ["@wrnexus/db@0.8.24", "https://registry.workroot.in/repository/npm/@wrnexus/db/-/db-0.8.24.tgz", {}, "sha512-zLT7e+C1nWhYs3BnqEjEO0QSli0JNl5sSnSHObjSX0C+wty30ASWD1WyBnfu9xFjOFn1eucZGthzo1xdGGQ1TQ=="], - "@wrnexus/design-studio": ["@wrnexus/design-studio@0.1.1", "https://registry.workroot.in/repository/npm/@wrnexus/design-studio/-/design-studio-0.1.1.tgz", { "dependencies": { "@wrnexus/ai": "^0.8.10", "@wrnexus/compiler": "^0.8.44", "@wrnexus/core": "^0.8.25", "@wrnexus/csr": "^0.8.74", "@wrnexus/design-system": "^0.1.0", "@wrnexus/ssr": "^0.8.20", "@wrnexus/styles": "^0.8.27", "@wrnexus/ui": "^0.8.118" }, "bin": { "wrnexus-design-studio": "dist/cli.js" } }, "sha512-BV8O34W/ey+izIgGuFevq+c0c3vjxB75KqHPhVaALt97X8I1FUvALzzODIhUyIMOSMlHGq0qaTW+2LQvbeUYyg=="], + "@wrnexus/design-studio": ["@wrnexus/design-studio@0.1.2", "https://registry.workroot.in/repository/npm/@wrnexus/design-studio/-/design-studio-0.1.2.tgz", { "dependencies": { "@wrnexus/ai": "^0.8.10", "@wrnexus/compiler": "^0.8.44", "@wrnexus/core": "^0.8.25", "@wrnexus/csr": "^0.8.74", "@wrnexus/design-system": "^0.1.0", "@wrnexus/ssr": "^0.8.20", "@wrnexus/styles": "^0.8.27", "@wrnexus/ui": "^0.8.118" }, "bin": { "wrnexus-design-studio": "dist/cli.js" } }, "sha512-4ftDwf9dMXw+00123pE/dbXFRhCTNTwDbskubbk6R7yw+YRPJA6TSAAV0VAqmjdd9M8QV3DoEpKBJZ/yCCVlGw=="], "@wrnexus/design-system": ["@wrnexus/design-system@0.1.0", "https://registry.workroot.in/repository/npm/@wrnexus/design-system/-/design-system-0.1.0.tgz", {}, "sha512-gNH/B+kYYZy+Pvlvl3gXoQZlTS6iP/teXct2MoClmsGp6M1vKa8IEptVXKs7po3E83zj5kSh3FIXwcBJHtZqLg=="], diff --git a/package-lock.json b/package-lock.json index 3dddaa7..97a7e07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.8.8", "dependencies": { "@wrnexus/core": "0.8.25", - "@wrnexus/design-studio": "0.1.1", + "@wrnexus/design-studio": "0.1.2", "@wrnexus/design-system": "0.1.0", "@wrnexus/dev-server": "0.8.83", "@wrnexus/helpers": "0.8.10", @@ -1186,9 +1186,9 @@ } }, "node_modules/@wrnexus/design-studio": { - "version": "0.1.1", - "resolved": "https://registry.workroot.in/repository/npm/@wrnexus/design-studio/-/design-studio-0.1.1.tgz", - "integrity": "sha512-BV8O34W/ey+izIgGuFevq+c0c3vjxB75KqHPhVaALt97X8I1FUvALzzODIhUyIMOSMlHGq0qaTW+2LQvbeUYyg==", + "version": "0.1.2", + "resolved": "https://registry.workroot.in/repository/npm/@wrnexus/design-studio/-/design-studio-0.1.2.tgz", + "integrity": "sha512-4ftDwf9dMXw+00123pE/dbXFRhCTNTwDbskubbk6R7yw+YRPJA6TSAAV0VAqmjdd9M8QV3DoEpKBJZ/yCCVlGw==", "license": "MIT", "dependencies": { "@wrnexus/ai": "^0.8.10", diff --git a/package.json b/package.json index f45e0f7..0ed5e39 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@wrnexus/styles": "0.8.27", "@wrnexus/syntax": "0.8.20", "@wrnexus/ui": "0.8.118", - "@wrnexus/design-studio": "0.1.1", + "@wrnexus/design-studio": "0.1.2", "@wrnexus/design-system": "0.1.0" }, "devDependencies": { diff --git a/scripts/verify-deployment.ts b/scripts/verify-deployment.ts index 4787e40..cc01f8e 100644 --- a/scripts/verify-deployment.ts +++ b/scripts/verify-deployment.ts @@ -83,7 +83,9 @@ try { ).json(); if ( process.env.APIMART_API_KEY?.trim() && - (generation.provider !== "APIMart" || generation.model !== "gpt-6-luna") + (generation.provider !== "APIMart" || + generation.model !== "gpt-6-luna" || + generation.timeoutMs !== 60_000) ) throw Error( "The compiled app did not pick up its server-side AI configuration.", diff --git a/test/studio-ai.test.ts b/test/studio-ai.test.ts index d21592a..5720d06 100644 --- a/test/studio-ai.test.ts +++ b/test/studio-ai.test.ts @@ -1,7 +1,11 @@ import { expect, test } from "bun:test"; import { CORE_DESIGN_SYSTEMS } from "@wrnexus/design-system"; import { designRevision, DEFAULT_DESIGN_BRIEF } from "@wrnexus/design-studio"; -import { apimartProvider, studioAI } from "../app/lib/studio-ai.ts"; +import { + AI_TIMEOUT_MS, + apimartProvider, + studioAI, +} from "../app/lib/studio-ai.ts"; import { addStudioGuide } from "../app/lib/studio-guide.ts"; const proposals = ["2px", "4px", "8px"].map((value, index) => ({ @@ -110,6 +114,8 @@ test("metadata and errors never contain the key or raw upstream response", async ); const text = await meta!.text(); expect(text).toContain("gpt-6-luna"); + expect(JSON.parse(text).timeoutMs).toBe(AI_TIMEOUT_MS); + expect(AI_TIMEOUT_MS).toBe(60_000); expect(text).not.toContain("test-secret"); const failure = await handler(await request()); const body = await failure!.text(); @@ -118,6 +124,44 @@ test("metadata and errors never contain the key or raw upstream response", async expect(body).toContain("WRN-STUDIO-PROVIDER"); }); +test("a completion beyond the old 20-second deadline produces three validated alternatives", async () => { + const handler = studioAI( + "test-secret", + mockFetch(async (_url, init) => { + await Bun.sleep(21_000); + expect(init?.signal?.aborted).toBe(false); + return completion(); + }), + ); + const original = JSON.stringify(system); + const response = await handler(await request()); + expect(response!.status).toBe(200); + const batch = await response!.json(); + expect(batch.alternatives).toHaveLength(3); + expect(batch.provenance.provider).toBe("APIMart"); + expect(JSON.stringify(system)).toBe(original); +}, 30_000); + +test("cancellation still aborts the upstream request and keeps the draft unchanged", async () => { + const controller = new AbortController(); + const handler = studioAI( + "test-secret", + mockFetch(async (_url, init) => { + controller.abort(); + expect(init?.signal?.aborted).toBe(true); + throw new DOMException("Cancelled", "AbortError"); + }), + ); + const original = JSON.stringify(system); + const base = await request(); + const response = await handler( + new Request(base, { signal: controller.signal }), + ); + expect(response!.status).toBe(408); + expect(await response!.text()).toContain("draft is unchanged"); + expect(JSON.stringify(system)).toBe(original); +}); + test("same-origin checks, body bounds and local fallback remain enforced", async () => { let calls = 0; const handler = studioAI(