Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 25 additions & 16 deletions supabase/functions/ai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,12 @@ async function callAIOnce(
);
}

// Supabase answers 504 when a function has not responded within 150 s. Every
// provider call shares one budget below that, so a slow model can use the time
// research left instead of a fixed 60 s. The margin covers the post-generation
// source-usage check (its own 8 s Jev timeout), logging, and saving.
const REQUEST_BUDGET_MS = 130_000;

async function callOpenRouter(
messages: Array<{ role: string; content: string }>,
model: string,
Expand All @@ -762,7 +768,7 @@ async function callOpenRouter(
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
body: JSON.stringify(payload),
}, 60000, signal);
}, REQUEST_BUDGET_MS, signal);
if (!response.ok) throw new Error(`OpenRouter error: ${errorBody ?? ""}`);
if (!data?.choices || !Array.isArray(data.choices) || data.choices.length === 0) throw new Error("OpenRouter returned no choices");
const usage = normalizeProviderUsage(data.usage) ?? missingOpenRouterUsage("response");
Expand Down Expand Up @@ -839,6 +845,7 @@ export async function handleRequest(req: Request): Promise<Response> {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
const workSignal = AbortSignal.any([req.signal, AbortSignal.timeout(REQUEST_BUDGET_MS)]);
if (req.method !== "POST") {
return new Response("Method Not Allowed", { status: 405, headers: corsHeaders });
}
Expand Down Expand Up @@ -1054,7 +1061,7 @@ export async function handleRequest(req: Request): Promise<Response> {
const evaluated = await evaluateCrossfireExchange({
transcript: crossfireInput.transcript,
reply: crossfireInput.reply,
signal: req.signal,
signal: workSignal,
});
const signalUsage = evaluated.jev?.usage ?? evaluated.jevUsage;
if (signalUsage) await logUsage(userId, "jev_crossfire_signal", provider, JEV_MODEL, signalUsage, undefined, evaluated.jevUsage ? "error" : "success", evaluated.jevUsage ? "jev_incomplete" : undefined);
Expand Down Expand Up @@ -1235,7 +1242,7 @@ export async function handleRequest(req: Request): Promise<Response> {
canAccessProModels: Boolean(rateLimit.canAccessProModels),
reasoningEffort: requestedReasoningEffort,
sourceCount: Array.isArray(rawSources) ? rawSources.length : 0,
signal: req.signal,
signal: workSignal,
});
model = resolution.resolvedModel;
modelEntry = resolution.modelEntry;
Expand Down Expand Up @@ -1285,7 +1292,7 @@ export async function handleRequest(req: Request): Promise<Response> {
feature,
requestText: JSON.stringify(sanitizedInput),
sources: sourceDecisionCandidates,
signal: req.signal,
signal: workSignal,
});
sourceAssessments = ranked.assessments;
sources = ranked.sources.map(({ title, content, origin, type }) => ({ title, content, ...(origin ? { origin } : {}), type }));
Expand Down Expand Up @@ -1314,7 +1321,7 @@ export async function handleRequest(req: Request): Promise<Response> {
provider,
model,
reasoningEffort,
signal: req.signal,
signal: workSignal,
onTimeout: () => {
researchAccountingIncomplete = true;
},
Expand Down Expand Up @@ -1376,7 +1383,7 @@ export async function handleRequest(req: Request): Promise<Response> {
const systemPrompt = await buildSystemPromptFor(feature);
const userPrompt = buildUserPrompt(feature, sparringRequest as unknown as Record<string, unknown>, "");
const messages = [{ role: "user", content: userPrompt }];
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, req.signal));
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal));
const output = cleanGeneratedText(aiResult.output);
const normalizedOutput = normalizeSparringOutput(output, sparringRequest.action);
const parsedOutput = parseSparringProviderOutput(normalizedOutput, sparringRequest.action);
Expand Down Expand Up @@ -1445,7 +1452,7 @@ export async function handleRequest(req: Request): Promise<Response> {
[{ role: "user", content: userPrompt }],
temperature,
reasoningEffort,
req.signal,
workSignal,
));
const cleanedOutput = cleanGeneratedText(aiResult.output);
const output = crossfireInput.operation === "next-turn"
Expand Down Expand Up @@ -1487,7 +1494,7 @@ export async function handleRequest(req: Request): Promise<Response> {
const userPrompt = buildUserPrompt(feature, sanitizedInput, searchContext);
const messages = [{ role: "user", content: userPrompt }];

const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, req.signal));
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal));
const output = cleanGeneratedText(aiResult.output);

// Post-process: quality floor. Only citations whose URL matches fetched,
Expand Down Expand Up @@ -1545,7 +1552,7 @@ export async function handleRequest(req: Request): Promise<Response> {
let urlContext = "";
if (urls.size > 0) {
const contents = await Promise.all(Array.from(urls).slice(0, 3).map(async (u) => {
const content = await exaGetContents(u, req.signal);
const content = await exaGetContents(u, workSignal);
return content.trim()
? `--- Content from ${u} ---\n${content.slice(0, 8000)}\n--- End of ${u} ---`
: "";
Expand Down Expand Up @@ -1600,7 +1607,7 @@ export async function handleRequest(req: Request): Promise<Response> {
const systemPrompt = await buildSystemPromptFor(feature, effectiveResearch);
const prompt = `${basePrompt}${urlContext}${webSearchContext}`;
const messages = [...history, { role: "user", content: prompt }];
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, req.signal));
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal));
const output = cleanGeneratedText(aiResult.output);

if (output && typeof output === "object") {
Expand Down Expand Up @@ -1688,7 +1695,7 @@ export async function handleRequest(req: Request): Promise<Response> {
feature,
requestText: JSON.stringify(sanitizedInput),
sources: discoveredCandidates,
signal: req.signal,
signal: workSignal,
});
const discoveredRankingUsage = discoveredRanking.jev?.usage ?? discoveredRanking.jevUsage;
if (discoveredRankingUsage) await logUsage(userId, "jev_source_ranking", provider, JEV_MODEL, discoveredRankingUsage, undefined, discoveredRanking.jevUsage ? "error" : "success", discoveredRanking.jevUsage ? "jev_incomplete" : undefined);
Expand Down Expand Up @@ -1717,7 +1724,7 @@ export async function handleRequest(req: Request): Promise<Response> {
const systemPrompt = await buildSystemPromptFor(feature, research);
const userPrompt = buildUserPrompt(feature, sanitizedInput, searchContext);
const messages = [{ role: "user", content: userPrompt }];
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, req.signal));
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal));
const output = cleanGeneratedText(aiResult.output);

// Preserve model evidence warnings when research exists. If research
Expand Down Expand Up @@ -1762,6 +1769,8 @@ export async function handleRequest(req: Request): Promise<Response> {
strength: discoveredAssessmentByKey.get(source.key)?.strength ?? 0,
})),
].slice(0, 10);
// The case is already complete, so this optional check must not inherit
// an almost spent request budget and turn a finished case into a 500.
const caseUsage = await identifyCaseSourceUsage({
caseText: JSON.stringify(output),
sources: usageCandidates,
Expand Down Expand Up @@ -1812,7 +1821,7 @@ export async function handleRequest(req: Request): Promise<Response> {
const systemPrompt = await buildSystemPromptFor(feature, research);
const userPrompt = buildUserPrompt(feature, sanitizedInput, searchContext);
const messages = [{ role: "user", content: userPrompt }];
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, req.signal));
const aiResult = recordMainResult(await callAI(provider, model, systemPrompt, messages, temperature, reasoningEffort, workSignal));
const output = cleanGeneratedText(aiResult.output);
enforceBrainstormControls(output, side, outputMode);

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

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

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