Skip to content

Commit e4670dd

Browse files
Programcodeclaude
andcommitted
fix(ai): give provider calls the time research leaves, not a fixed 60 s
Claude Opus 5 needs about 68 s to write a case, so every Opus case generation hit the 60 s provider timeout and returned 500. Each call now shares one 140 s request budget, which stays under Supabase's 150 s response limit. Research, the main call, and its JSON repair retry all use what is left. The model matrix reproduces the failure and proves the fix. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent de4264d commit e4670dd

1 file changed

Lines changed: 23 additions & 17 deletions

File tree

‎supabase/functions/ai/index.ts‎

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,11 @@ async function callAIOnce(
737737
);
738738
}
739739

740+
// Supabase answers 504 when a function has not responded within 150 s. Every
741+
// provider call shares one budget below that, so a slow model can use the time
742+
// research left instead of a fixed 60 s. The margin covers logging and saving.
743+
const REQUEST_BUDGET_MS = 140_000;
744+
740745
async function callOpenRouter(
741746
messages: Array<{ role: string; content: string }>,
742747
model: string,
@@ -762,7 +767,7 @@ async function callOpenRouter(
762767
method: "POST",
763768
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
764769
body: JSON.stringify(payload),
765-
}, 60000, signal);
770+
}, REQUEST_BUDGET_MS, signal);
766771
if (!response.ok) throw new Error(`OpenRouter error: ${errorBody ?? ""}`);
767772
if (!data?.choices || !Array.isArray(data.choices) || data.choices.length === 0) throw new Error("OpenRouter returned no choices");
768773
const usage = normalizeProviderUsage(data.usage) ?? missingOpenRouterUsage("response");
@@ -839,6 +844,7 @@ export async function handleRequest(req: Request): Promise<Response> {
839844
if (req.method === "OPTIONS") {
840845
return new Response("ok", { headers: corsHeaders });
841846
}
847+
const workSignal = AbortSignal.any([req.signal, AbortSignal.timeout(REQUEST_BUDGET_MS)]);
842848
if (req.method !== "POST") {
843849
return new Response("Method Not Allowed", { status: 405, headers: corsHeaders });
844850
}
@@ -1054,7 +1060,7 @@ export async function handleRequest(req: Request): Promise<Response> {
10541060
const evaluated = await evaluateCrossfireExchange({
10551061
transcript: crossfireInput.transcript,
10561062
reply: crossfireInput.reply,
1057-
signal: req.signal,
1063+
signal: workSignal,
10581064
});
10591065
const signalUsage = evaluated.jev?.usage ?? evaluated.jevUsage;
10601066
if (signalUsage) await logUsage(userId, "jev_crossfire_signal", provider, JEV_MODEL, signalUsage, undefined, evaluated.jevUsage ? "error" : "success", evaluated.jevUsage ? "jev_incomplete" : undefined);
@@ -1235,7 +1241,7 @@ export async function handleRequest(req: Request): Promise<Response> {
12351241
canAccessProModels: Boolean(rateLimit.canAccessProModels),
12361242
reasoningEffort: requestedReasoningEffort,
12371243
sourceCount: Array.isArray(rawSources) ? rawSources.length : 0,
1238-
signal: req.signal,
1244+
signal: workSignal,
12391245
});
12401246
model = resolution.resolvedModel;
12411247
modelEntry = resolution.modelEntry;
@@ -1285,7 +1291,7 @@ export async function handleRequest(req: Request): Promise<Response> {
12851291
feature,
12861292
requestText: JSON.stringify(sanitizedInput),
12871293
sources: sourceDecisionCandidates,
1288-
signal: req.signal,
1294+
signal: workSignal,
12891295
});
12901296
sourceAssessments = ranked.assessments;
12911297
sources = ranked.sources.map(({ title, content, origin, type }) => ({ title, content, ...(origin ? { origin } : {}), type }));
@@ -1314,7 +1320,7 @@ export async function handleRequest(req: Request): Promise<Response> {
13141320
provider,
13151321
model,
13161322
reasoningEffort,
1317-
signal: req.signal,
1323+
signal: workSignal,
13181324
onTimeout: () => {
13191325
researchAccountingIncomplete = true;
13201326
},
@@ -1376,7 +1382,7 @@ export async function handleRequest(req: Request): Promise<Response> {
13761382
const systemPrompt = await buildSystemPromptFor(feature);
13771383
const userPrompt = buildUserPrompt(feature, sparringRequest as unknown as Record<string, unknown>, "");
13781384
const messages = [{ role: "user", content: userPrompt }];
1379-
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, req.signal));
1385+
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal));
13801386
const output = cleanGeneratedText(aiResult.output);
13811387
const normalizedOutput = normalizeSparringOutput(output, sparringRequest.action);
13821388
const parsedOutput = parseSparringProviderOutput(normalizedOutput, sparringRequest.action);
@@ -1445,7 +1451,7 @@ export async function handleRequest(req: Request): Promise<Response> {
14451451
[{ role: "user", content: userPrompt }],
14461452
temperature,
14471453
reasoningEffort,
1448-
req.signal,
1454+
workSignal,
14491455
));
14501456
const cleanedOutput = cleanGeneratedText(aiResult.output);
14511457
const output = crossfireInput.operation === "next-turn"
@@ -1487,7 +1493,7 @@ export async function handleRequest(req: Request): Promise<Response> {
14871493
const userPrompt = buildUserPrompt(feature, sanitizedInput, searchContext);
14881494
const messages = [{ role: "user", content: userPrompt }];
14891495

1490-
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, req.signal));
1496+
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal));
14911497
const output = cleanGeneratedText(aiResult.output);
14921498

14931499
// Post-process: quality floor. Only citations whose URL matches fetched,
@@ -1545,7 +1551,7 @@ export async function handleRequest(req: Request): Promise<Response> {
15451551
let urlContext = "";
15461552
if (urls.size > 0) {
15471553
const contents = await Promise.all(Array.from(urls).slice(0, 3).map(async (u) => {
1548-
const content = await exaGetContents(u, req.signal);
1554+
const content = await exaGetContents(u, workSignal);
15491555
return content.trim()
15501556
? `--- Content from ${u} ---\n${content.slice(0, 8000)}\n--- End of ${u} ---`
15511557
: "";
@@ -1600,7 +1606,7 @@ export async function handleRequest(req: Request): Promise<Response> {
16001606
const systemPrompt = await buildSystemPromptFor(feature, effectiveResearch);
16011607
const prompt = `${basePrompt}${urlContext}${webSearchContext}`;
16021608
const messages = [...history, { role: "user", content: prompt }];
1603-
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, req.signal));
1609+
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal));
16041610
const output = cleanGeneratedText(aiResult.output);
16051611

16061612
if (output && typeof output === "object") {
@@ -1688,7 +1694,7 @@ export async function handleRequest(req: Request): Promise<Response> {
16881694
feature,
16891695
requestText: JSON.stringify(sanitizedInput),
16901696
sources: discoveredCandidates,
1691-
signal: req.signal,
1697+
signal: workSignal,
16921698
});
16931699
const discoveredRankingUsage = discoveredRanking.jev?.usage ?? discoveredRanking.jevUsage;
16941700
if (discoveredRankingUsage) await logUsage(userId, "jev_source_ranking", provider, JEV_MODEL, discoveredRankingUsage, undefined, discoveredRanking.jevUsage ? "error" : "success", discoveredRanking.jevUsage ? "jev_incomplete" : undefined);
@@ -1717,7 +1723,7 @@ export async function handleRequest(req: Request): Promise<Response> {
17171723
const systemPrompt = await buildSystemPromptFor(feature, research);
17181724
const userPrompt = buildUserPrompt(feature, sanitizedInput, searchContext);
17191725
const messages = [{ role: "user", content: userPrompt }];
1720-
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, req.signal));
1726+
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal));
17211727
const output = cleanGeneratedText(aiResult.output);
17221728

17231729
// Preserve model evidence warnings when research exists. If research
@@ -1765,7 +1771,7 @@ export async function handleRequest(req: Request): Promise<Response> {
17651771
const caseUsage = await identifyCaseSourceUsage({
17661772
caseText: JSON.stringify(output),
17671773
sources: usageCandidates,
1768-
signal: req.signal,
1774+
signal: workSignal,
17691775
});
17701776
const caseUsageCost = caseUsage.jev?.usage ?? caseUsage.jevUsage;
17711777
if (caseUsageCost) await logUsage(userId, "jev_source_usage", provider, JEV_MODEL, caseUsageCost, undefined, caseUsage.jevUsage ? "error" : "success", caseUsage.jevUsage ? "jev_incomplete" : undefined);
@@ -1812,7 +1818,7 @@ export async function handleRequest(req: Request): Promise<Response> {
18121818
const systemPrompt = await buildSystemPromptFor(feature, research);
18131819
const userPrompt = buildUserPrompt(feature, sanitizedInput, searchContext);
18141820
const messages = [{ role: "user", content: userPrompt }];
1815-
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, req.signal));
1821+
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal));
18161822
const output = cleanGeneratedText(aiResult.output);
18171823
enforceBrainstormControls(output, side, outputMode);
18181824

@@ -1840,7 +1846,7 @@ export async function handleRequest(req: Request): Promise<Response> {
18401846
const systemPrompt = await buildSystemPromptFor(feature, research);
18411847
const userPrompt = buildUserPrompt(feature, sanitizedInput, searchContext);
18421848
const messages = [{ role: "user", content: userPrompt }];
1843-
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, req.signal));
1849+
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal));
18441850
const output = cleanGeneratedText(aiResult.output);
18451851
enforceAttackControls(output, detectFallacies, analyzeWeighing);
18461852

@@ -1856,7 +1862,7 @@ export async function handleRequest(req: Request): Promise<Response> {
18561862
const systemPrompt = await buildSystemPromptFor(feature);
18571863
const userPrompt = buildUserPrompt(feature, sanitizedInput, "");
18581864
const messages = [{ role: "user", content: userPrompt }];
1859-
const fallbackResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, req.signal));
1865+
const fallbackResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal));
18601866
const output = cleanGeneratedText(fallbackResult.output);
18611867

18621868
const latencyMs = Date.now() - startTime;
@@ -1890,7 +1896,7 @@ export async function handleRequest(req: Request): Promise<Response> {
18901896
: undefined;
18911897
// A successful main call takes precedence over usage attached to a later
18921898
// post-processing error. This avoids counting its repair attempts twice.
1893-
const mainErrorUsage = completedMainUsage ?? selectObservedUsage(usageError, req.signal?.aborted === true);
1899+
const mainErrorUsage = completedMainUsage ?? selectObservedUsage(usageError, workSignal?.aborted === true);
18941900
await logUsage(
18951901
userId,
18961902
feature,

0 commit comments

Comments
 (0)