fix(preview): stop spec generation timing out behind the edge proxy
All checks were successful
Deploy to Production / deploy (push) Successful in 50s
All checks were successful
Deploy to Production / deploy (push) Successful in 50s
The /v1/servers/preview route ran claude-opus-4-7 synchronously; full spec generation routinely exceeded Cloudflare's ~100s proxy cap, so the browser received a headerless 524 and reported it as a CORS failure. - preview now uses claude-sonnet-4-6 with a 45s per-attempt timeout and one retry — comfortably inside the proxy budget - generateSpec maps an exhausted timeout to SpecTimeoutError; the route returns a clean 504 (with CORS headers) instead of a stalled connection - analyze step: live elapsed-seconds counter as freeze-proof, plus a reduced-motion exception so the loading spinner keeps spinning (a status indicator, which WCAG exempts from reduced-motion) - textarea resize grip restyled to dark theme (light hatch on dark square) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,19 +54,43 @@ export interface GenerateOptions {
|
||||
apiKey?: string;
|
||||
model?: string;
|
||||
maxTokens?: number;
|
||||
/** Per-attempt request timeout in ms. Omit to use the SDK default. */
|
||||
timeoutMs?: number;
|
||||
/** SDK retry count. Omit to use the SDK default. */
|
||||
maxRetries?: number;
|
||||
}
|
||||
|
||||
export async function generateSpec(prompt: string, opts: GenerateOptions = {}): Promise<GenerationResult> {
|
||||
export async function generateSpec(
|
||||
prompt: string,
|
||||
opts: GenerateOptions = {},
|
||||
): Promise<GenerationResult> {
|
||||
if (!opts.apiKey) {
|
||||
return { spec: mockSpec(prompt), source: 'mock' };
|
||||
}
|
||||
const client = new Anthropic({ apiKey: opts.apiKey });
|
||||
const response = await client.messages.create({
|
||||
model: opts.model ?? 'claude-opus-4-7',
|
||||
max_tokens: opts.maxTokens ?? 8192,
|
||||
system: SYSTEM_PROMPT,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
});
|
||||
const requestOptions: { timeout?: number; maxRetries?: number } = {};
|
||||
if (opts.timeoutMs !== undefined) requestOptions.timeout = opts.timeoutMs;
|
||||
if (opts.maxRetries !== undefined) requestOptions.maxRetries = opts.maxRetries;
|
||||
|
||||
const response = await client.messages
|
||||
.create(
|
||||
{
|
||||
model: opts.model ?? 'claude-opus-4-7',
|
||||
max_tokens: opts.maxTokens ?? 8192,
|
||||
system: SYSTEM_PROMPT,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
},
|
||||
requestOptions,
|
||||
)
|
||||
.catch((err: unknown) => {
|
||||
// A per-attempt timeout surfaces as APIConnectionTimeoutError once the
|
||||
// SDK exhausts retries. Map it to a typed error so the API layer returns
|
||||
// a clean 504 instead of letting the edge proxy time out headerless.
|
||||
if (err instanceof Anthropic.APIConnectionTimeoutError) {
|
||||
throw new SpecTimeoutError('spec generation exceeded the time budget');
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
const text = response.content
|
||||
.filter((b): b is { type: 'text'; text: string } => b.type === 'text')
|
||||
.map((b) => b.text)
|
||||
@@ -88,6 +112,10 @@ export class BannedPatternError extends Error {
|
||||
override readonly name = 'BannedPatternError';
|
||||
}
|
||||
|
||||
export class SpecTimeoutError extends Error {
|
||||
override readonly name = 'SpecTimeoutError';
|
||||
}
|
||||
|
||||
function extractJson(text: string): unknown {
|
||||
const trimmed = text.trim();
|
||||
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
|
||||
|
||||
Reference in New Issue
Block a user