AUDIT / src /lib /agentLoop /apiCallRetryBlock.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
6.82 kB
/**
* apiCallRetryBlock.ts — S508: extracted from agentLoop.ts
*
* Esegue la chiamata API LLM con retry esponenziale (3×) e gestisce:
* 1. LLM call timeout (60s per tentativo) via AbortController
* 2. Rate-limit (429) + unavailable (503): retry con backoff
* 3. Network/timeout failure: streamFallback + return
*
* Ritorna:
* - { action: "ok", response: Response } → il caller prosegue con la risposta
* - { action: "return" } → streamFallback già eseguito, caller deve return
*
* @module apiCallRetryBlock
*/
import { callWithFallback } from "../providerChain";
import { streamFallback } from "./streamHelpers";
import type { ApiMsg } from "./networkTools";
import { recordOutcome as recordTaskOutcome } from "../agent/SuccessRateMonitor";
import { AGENT_TOOLS } from "./toolDefinitions";
import type { StreamOptions as AgentOptions } from "../types";
import type { TaskType } from "../taskClassifier";
// ─── Interfaces ────────────────────────────────────────────────────────────────
export interface ApiCallRetryCtx {
token: string;
safeMsgs: ApiMsg[];
options: AgentOptions | undefined;
signal: AbortSignal | undefined;
loopMessages: ApiMsg[];
initialMessages: Array<{ role: string; content: string }>;
realDataInjected: boolean;
preferredModel: string | undefined;
maxTokensBudget: number;
preCls: { type: string } | null;
onStatus: (msg: string) => void;
onChunk: (chunk: string) => void;
}
export type ApiCallRetryResult =
| { action: "ok"; response: Response }
| { action: "return" };
// ─── Main export ───────────────────────────────────────────────────────────────
/**
* Chiama il provider LLM con retry esponenziale (max 3 tentativi).
* Se tutte le retry falliscono → streamFallback e ritorna action="return".
* Se la chiamata riesce → ritorna action="ok" con la Response grezza.
*/
export async function callLlmWithRetry(ctx: ApiCallRetryCtx): Promise<ApiCallRetryResult> {
const {
token, safeMsgs, options, signal, loopMessages, initialMessages,
realDataInjected, preferredModel, maxTokensBudget, preCls,
onStatus, onChunk,
} = ctx;
const _API_DELAYS = [600, 1_500, 3_500]; // ms — 600 · 1500 · 3500
let _apiAttempt = 0;
let res!: Response;
// eslint-disable-next-line no-constant-condition
while (true) {
try {
// LLM call timeout: 60s per attempt — prevents hanging on frozen providers
const _llmCtrl = new AbortController();
const _llmTimer = setTimeout(() => _llmCtrl.abort(), 60_000);
const _llmSig: AbortSignal = (typeof AbortSignal !== "undefined" && "any" in AbortSignal)
? (AbortSignal as { any(s: AbortSignal[]): AbortSignal }).any([...(signal ? [signal] : []), _llmCtrl.signal])
: _llmCtrl.signal;
try {
res = await callWithFallback(token, {
messages: safeMsgs as unknown as import("../api").ChatMessage[],
tools: AGENT_TOOLS,
tool_choice: "auto",
max_tokens: Math.min(options?.maxTokens ?? 8192, 8192),
// Fix A (S380): temperatura task-aware — deterministico per code/math, espressivo per chat
temperature: (() => {
const _TASK_TEMP: Record<string, number> = {
code_generation: 0.15, code_fix: 0.15, debug: 0.15,
math: 0.10, data: 0.30, search_and_report: 0.40,
text: 0.55, fetch: 0.45,
};
const _taskDefault = _TASK_TEMP[preCls?.type ?? ""] ?? 0.50;
return options?.temperature !== undefined ? Math.min(options.temperature, 0.90) : _taskDefault;
})(),
stream: false,
// S694-fix: passa preferredModel al payload — era ignorato da callWithFallback (HF routing)
...(preferredModel ? { preferredModel } : {}),
}, _llmSig);
} finally {
clearTimeout(_llmTimer);
}
} catch (err) {
if (err instanceof Error && err.name === "AbortError" && signal?.aborted) throw err; // user Stop
if (err instanceof Error && err.name === "AbortError") { onStatus("LLM timeout (60s) — riprovo…"); }
if (_apiAttempt < _API_DELAYS.length) {
const delay = _API_DELAYS[_apiAttempt++];
onStatus(`Riconnessione API… (tentativo ${_apiAttempt}/${_API_DELAYS.length + 1})`);
await new Promise(r => setTimeout(r, delay));
continue;
}
// All retries exhausted → streamFallback
console.warn("[AgentLoop] callWithFallback esaurita dopo", _apiAttempt, "tentativi:", err);
onStatus("Risposta diretta…");
// S255: usa loopMessages se ha dati pre-iniettati (pre-exec riuscito), altrimenti initialMessages
// S375: usa loopMessages (ha il format directive + system prompt assemblato) quando pre-exec ha iniettato dati
if (realDataInjected) {
await streamFallback(token, loopMessages as Array<{ role: string; content: string }>, onChunk, signal, { ...options, ...(preferredModel ? { model: preferredModel } : {}), maxTokens: maxTokensBudget });
} else {
await streamFallback(token, initialMessages, onChunk, signal, { ...options, ...(preferredModel ? { model: preferredModel } : {}), maxTokens: maxTokensBudget });
}
// S-GAP5: retry chain esaurita → streamFallback è degradazione del servizio, NON success.
// "success" qui infiava SuccessRateMonitor → iterBudget sbagliato per quel task type.
try { recordTaskOutcome((preCls?.type ?? "unknown") as TaskType, "failure"); } catch { /* non-blocking */ }
return { action: "return" };
}
// ── Rate-limit (429) and unavailable (503): wait and retry ────────
if (!res.ok && [429, 503].includes(res.status) && _apiAttempt < _API_DELAYS.length) {
// S-LIVE-3: 503 sintetico da callWithFallback ("All providers exhausted") → retry inutile, vai subito a streamFallback
if (res.status === 503) {
try { const _b503 = await res.clone().text(); if (_b503.includes("providers exhausted") || _b503.includes("All providers")) break; } catch { /* ignore */ }
}
const delay = res.status === 429
? _API_DELAYS[_apiAttempt++] * 3 // longer backoff for rate-limit
: _API_DELAYS[_apiAttempt++];
onStatus(`API ${res.status} — riprovo tra ${Math.round(delay / 1000)}s (${_apiAttempt}/3)…`);
await new Promise(r => setTimeout(r, delay));
continue;
}
break; // success or non-retryable error — exit retry loop
}
return { action: "ok", response: res };
}