mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 08:59:40 +02:00
Create a shared LLM client package that provides a unified interface to the mana-llm service, replacing 9 individual fetch-based integrations with consistent error handling, retry logic, and JSON extraction. Package (@manacore/shared-llm): - LlmModule with forRoot/forRootAsync (NestJS dynamic module) - LlmClientService: chat, json, vision, visionJson, embed, stream - LlmClient standalone class for non-NestJS consumers - extractJson utility (consolidates 3 markdown-stripping implementations) - retryFetch with exponential backoff (429, 5xx, network errors) - 44 unit tests (json-extractor, retry, llm-client) Migrated backends: - mana-core-auth: raw fetch → llm.json() - planta: raw fetch + vision → llm.visionJson() - nutriphi: raw fetch + regex → llm.visionJson() + llm.json() - chat: custom OllamaService (175 LOC) → llm.chatMessages() - context: raw fetch → llm.chat() (keeps token tracking) - traces: 2x raw fetch → llm.chat() - manadeck: @google/genai SDK → llm.json() + llm.visionJson() - bot-services: raw Ollama API → LlmClient standalone - matrix-ollama-bot: raw fetch → llm.chatMessages() + llm.vision() New credit operations: - AI_PLANT_ANALYSIS (2 credits, planta) - AI_GUIDE_GENERATION (5 credits, traces) - AI_CONTEXT_GENERATION (2 credits, context) - AI_BOT_CHAT (0.1 credits, matrix) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
51 lines
1.4 KiB
TypeScript
51 lines
1.4 KiB
TypeScript
/**
|
|
* Fetch wrapper with exponential backoff retry for transient failures.
|
|
*
|
|
* Retries on: 429 (rate limit), 502, 503, 504 (server errors), network errors.
|
|
* Does NOT retry on: 400, 401, 403, 404 (client errors).
|
|
*/
|
|
|
|
const RETRYABLE_STATUS_CODES = new Set([429, 502, 503, 504]);
|
|
|
|
export interface RetryOptions {
|
|
maxRetries: number;
|
|
/** Base delay in ms (doubles each retry). Default: 200 */
|
|
baseDelay?: number;
|
|
}
|
|
|
|
export async function retryFetch(
|
|
url: string,
|
|
init: RequestInit,
|
|
options: RetryOptions
|
|
): Promise<Response> {
|
|
const { maxRetries, baseDelay = 200 } = options;
|
|
let lastError: Error | undefined;
|
|
|
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
try {
|
|
const response = await fetch(url, init);
|
|
|
|
if (response.ok || !RETRYABLE_STATUS_CODES.has(response.status)) {
|
|
return response;
|
|
}
|
|
|
|
// Retryable status code
|
|
lastError = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
} catch (error) {
|
|
// Network error (connection refused, timeout, etc.)
|
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
}
|
|
|
|
// Don't sleep after the last attempt
|
|
if (attempt < maxRetries) {
|
|
const delay = baseDelay * Math.pow(2, attempt);
|
|
await sleep(delay);
|
|
}
|
|
}
|
|
|
|
throw lastError ?? new Error('retryFetch exhausted all retries');
|
|
}
|
|
|
|
function sleep(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|