fix(studio): allow sixty seconds for AI generation
This commit is contained in:
1 parent
fac1c4461f
commit
27dcb561db
7 files changed
+60
-12
No files matched your search
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
|
||||
Generated
+4
-4
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user