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
|
to APIMart when you request generation. The engine validates proposals and locks
|
||||||
before offering them for review. Failed requests leave the draft unchanged.
|
before offering them for review. Failed requests leave the draft unchanged.
|
||||||
Requests allow two concurrent generations and ten starts per minute per server
|
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;
|
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
|
local rules do not interpret arbitrary product descriptions. Drafts remain in
|
||||||
browser storage; export before clearing browser data or moving to another origin.
|
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
|
### 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
|
instead of assuming a filename from the public component name. This fixes Linux
|
||||||
lookups such as Input -> input.wrn. Install the committed lockfile before
|
lookups such as Input -> input.wrn. Install the committed lockfile before
|
||||||
rebuilding and restarting; restarting a deployment still using Studio 0.1.0
|
rebuilding and restarting; restarting a deployment still using Studio 0.1.0
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
} from "@wrnexus/design-studio";
|
} from "@wrnexus/design-studio";
|
||||||
|
|
||||||
export const APIMART_MODEL = "gpt-6-luna";
|
export const APIMART_MODEL = "gpt-6-luna";
|
||||||
|
export const AI_TIMEOUT_MS = 60_000;
|
||||||
export function apimartProvider(
|
export function apimartProvider(
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
transport: typeof fetch = fetch,
|
transport: typeof fetch = fetch,
|
||||||
@@ -117,7 +118,7 @@ export function studioAI(apiKey?: string, transport: typeof fetch = fetch) {
|
|||||||
templateVersion: 1,
|
templateVersion: 1,
|
||||||
maxAttempts: 2,
|
maxAttempts: 2,
|
||||||
maxOutputTokens: 4096,
|
maxOutputTokens: 4096,
|
||||||
timeoutMs: 20000,
|
timeoutMs: AI_TIMEOUT_MS,
|
||||||
});
|
});
|
||||||
if (!url.pathname.endsWith("/generate") || request.method !== "POST")
|
if (!url.pathname.endsWith("/generate") || request.method !== "POST")
|
||||||
return json({ error: "Method not allowed." }, 405);
|
return json({ error: "Method not allowed." }, 405);
|
||||||
@@ -151,6 +152,7 @@ export function studioAI(apiKey?: string, transport: typeof fetch = fetch) {
|
|||||||
provider: apimartProvider(apiKey, transport),
|
provider: apimartProvider(apiKey, transport),
|
||||||
model: APIMART_MODEL,
|
model: APIMART_MODEL,
|
||||||
signal: request.signal,
|
signal: request.signal,
|
||||||
|
timeoutMs: AI_TIMEOUT_MS,
|
||||||
});
|
});
|
||||||
return json(result);
|
return json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"name": "component-showcase",
|
"name": "component-showcase",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "0.8.25",
|
"@wrnexus/core": "0.8.25",
|
||||||
"@wrnexus/design-studio": "0.1.1",
|
"@wrnexus/design-studio": "0.1.2",
|
||||||
"@wrnexus/design-system": "0.1.0",
|
"@wrnexus/design-system": "0.1.0",
|
||||||
"@wrnexus/dev-server": "0.8.83",
|
"@wrnexus/dev-server": "0.8.83",
|
||||||
"@wrnexus/helpers": "0.8.10",
|
"@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/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=="],
|
"@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",
|
"version": "0.8.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "0.8.25",
|
"@wrnexus/core": "0.8.25",
|
||||||
"@wrnexus/design-studio": "0.1.1",
|
"@wrnexus/design-studio": "0.1.2",
|
||||||
"@wrnexus/design-system": "0.1.0",
|
"@wrnexus/design-system": "0.1.0",
|
||||||
"@wrnexus/dev-server": "0.8.83",
|
"@wrnexus/dev-server": "0.8.83",
|
||||||
"@wrnexus/helpers": "0.8.10",
|
"@wrnexus/helpers": "0.8.10",
|
||||||
@@ -1186,9 +1186,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wrnexus/design-studio": {
|
"node_modules/@wrnexus/design-studio": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.workroot.in/repository/npm/@wrnexus/design-studio/-/design-studio-0.1.1.tgz",
|
"resolved": "https://registry.workroot.in/repository/npm/@wrnexus/design-studio/-/design-studio-0.1.2.tgz",
|
||||||
"integrity": "sha512-BV8O34W/ey+izIgGuFevq+c0c3vjxB75KqHPhVaALt97X8I1FUvALzzODIhUyIMOSMlHGq0qaTW+2LQvbeUYyg==",
|
"integrity": "sha512-4ftDwf9dMXw+00123pE/dbXFRhCTNTwDbskubbk6R7yw+YRPJA6TSAAV0VAqmjdd9M8QV3DoEpKBJZ/yCCVlGw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/ai": "^0.8.10",
|
"@wrnexus/ai": "^0.8.10",
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@
|
|||||||
"@wrnexus/styles": "0.8.27",
|
"@wrnexus/styles": "0.8.27",
|
||||||
"@wrnexus/syntax": "0.8.20",
|
"@wrnexus/syntax": "0.8.20",
|
||||||
"@wrnexus/ui": "0.8.118",
|
"@wrnexus/ui": "0.8.118",
|
||||||
"@wrnexus/design-studio": "0.1.1",
|
"@wrnexus/design-studio": "0.1.2",
|
||||||
"@wrnexus/design-system": "0.1.0"
|
"@wrnexus/design-system": "0.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -83,7 +83,9 @@ try {
|
|||||||
).json();
|
).json();
|
||||||
if (
|
if (
|
||||||
process.env.APIMART_API_KEY?.trim() &&
|
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(
|
throw Error(
|
||||||
"The compiled app did not pick up its server-side AI configuration.",
|
"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 { expect, test } from "bun:test";
|
||||||
import { CORE_DESIGN_SYSTEMS } from "@wrnexus/design-system";
|
import { CORE_DESIGN_SYSTEMS } from "@wrnexus/design-system";
|
||||||
import { designRevision, DEFAULT_DESIGN_BRIEF } from "@wrnexus/design-studio";
|
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";
|
import { addStudioGuide } from "../app/lib/studio-guide.ts";
|
||||||
|
|
||||||
const proposals = ["2px", "4px", "8px"].map((value, index) => ({
|
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();
|
const text = await meta!.text();
|
||||||
expect(text).toContain("gpt-6-luna");
|
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");
|
expect(text).not.toContain("test-secret");
|
||||||
const failure = await handler(await request());
|
const failure = await handler(await request());
|
||||||
const body = await failure!.text();
|
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");
|
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 () => {
|
test("same-origin checks, body bounds and local fallback remain enforced", async () => {
|
||||||
let calls = 0;
|
let calls = 0;
|
||||||
const handler = studioAI(
|
const handler = studioAI(
|
||||||
|
|||||||
Reference in new issue
Block a user