Spaces:
Sleeping
Sleeping
File size: 6,820 Bytes
cc11e77 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | /**
* 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 };
}
|