Spaces:
Sleeping
Sleeping
| /** | |
| * 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 }; | |
| } | |