fix(studio): allow sixty seconds for AI generation

This commit is contained in:
Clintchiz committed 2026-10-19 14:29:15 +05:30
1 parent fac1c4461f
commit 27dcb561db
7 files changed
+60 -12

No files matched your search

+2 -2
View File
@@ -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
+3 -1
View File
@@ -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) {
+2 -2
View File
@@ -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=="],
+4 -4
View File
@@ -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",
+1 -1
View File
@@ -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": {
+3 -1
View File
@@ -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.",
+45 -1
View File
@@ -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(