/**
 * LiteLLMAdapter — OpenAI-compatible adapter for LiteLLM proxy.
 *
 * Uses native fetch (Node 18+). Retries 429 responses with
 * exponential back-off + jitter (max 3 attempts). No axios.
 * Cost extracted from usage field when available.
 *
 * BYOM: defaultModel is a constructor param default only.
 * No model string is hardcoded at module scope.
 */
import { SpanStatusCode, trace } from "@opentelemetry/api";
const ADAPTER_TRACER = trace.getTracer("vauban-agent-sdk.llm.litellm", "0.1.0");
// Tokens per USD — approximate LiteLLM defaults for cost estimation.
const INPUT_TOKENS_PER_USD = 500_000;
const OUTPUT_TOKENS_PER_USD = 250_000;
const MAX_RETRIES = 3;
function jitter(baseMs) {
    // ±25% jitter
    return baseMs * (0.75 + Math.random() * 0.5);
}
async function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
function estimateInputTokens(req) {
    // Rough approximation: 4 chars ≈ 1 token
    const chars = req.messages.reduce((sum, m) => sum + m.content.length, 0);
    return Math.ceil(chars / 4);
}
function mapFinishReason(raw) {
    switch (raw) {
        case "stop":
            return "stop";
        case "length":
        case "max_tokens":
            return "length";
        case "tool_calls":
        case "function_call":
            return "tool";
        default:
            return "error";
    }
}
export class LiteLLMAdapter {
    baseUrl;
    apiKey;
    defaultModel;
    constructor(config) {
        this.baseUrl = config.baseUrl.replace(/\/$/, "");
        this.apiKey = config.apiKey;
        this.defaultModel = config.defaultModel ?? "gpt-3.5-turbo";
    }
    estimateCost(req) {
        const inputTokens = estimateInputTokens(req);
        const maxOutputTokens = req.maxTokens ?? 512;
        const inputCost = inputTokens / INPUT_TOKENS_PER_USD;
        const outputCost = maxOutputTokens / OUTPUT_TOKENS_PER_USD;
        return { usd: inputCost + outputCost };
    }
    async complete(req) {
        return ADAPTER_TRACER.startActiveSpan("llm-provider.complete", {
            attributes: {
                "gen_ai.system": "litellm",
                "gen_ai.request.model": req.model ?? this.defaultModel,
                "gen_ai.request.message_count": req.messages.length,
                "vauban.port.name": "llm-provider",
                "vauban.port.impl": "LiteLLMAdapter",
            },
        }, async (span) => {
            try {
                const response = await this._doComplete(req);
                span.setAttributes({
                    "gen_ai.usage.input_tokens": response.usage.inputTokens,
                    "gen_ai.usage.output_tokens": response.usage.outputTokens,
                    "gen_ai.response.model": response.model,
                    "gen_ai.response.finish_reasons": [response.finishReason],
                });
                if (response.usage.costUsd !== undefined) {
                    span.setAttribute("gen_ai.usage.cost_usd", response.usage.costUsd);
                }
                span.setStatus({ code: SpanStatusCode.OK });
                return response;
            }
            catch (_err) {
                span.setStatus({
                    code: SpanStatusCode.ERROR,
                    message: "span wrapper caught exception",
                });
                return {
                    content: "",
                    usage: { inputTokens: 0, outputTokens: 0 },
                    model: req.model ?? this.defaultModel,
                    finishReason: "error",
                };
            }
            finally {
                span.end();
            }
        });
    }
    /** Internal completion logic — extracted from complete() for span instrumentation. */
    async _doComplete(req) {
        const model = req.model ?? this.defaultModel;
        const body = {
            model,
            messages: req.messages,
            temperature: req.temperature,
            max_tokens: req.maxTokens,
            // Pass tenant metadata as user field for tracking
            user: req.metadata?.tenantId,
        };
        const headers = {
            "Content-Type": "application/json",
        };
        if (this.apiKey) {
            headers["Authorization"] = `Bearer ${this.apiKey}`;
        }
        let lastError = null;
        for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
            if (req.abortSignal?.aborted) {
                return {
                    content: "",
                    usage: { inputTokens: 0, outputTokens: 0 },
                    model,
                    finishReason: "error",
                };
            }
            let response;
            try {
                response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
                    method: "POST",
                    headers,
                    body: JSON.stringify(body),
                    signal: req.abortSignal,
                });
            }
            catch (err) {
                // AbortError or network error
                if (req.abortSignal?.aborted) {
                    return {
                        content: "",
                        usage: { inputTokens: 0, outputTokens: 0 },
                        model,
                        finishReason: "error",
                    };
                }
                lastError = err instanceof Error ? err : new Error(String(err));
                if (attempt < MAX_RETRIES - 1) {
                    await sleep(jitter(500 * 2 ** attempt));
                }
                continue;
            }
            if (response.status === 429) {
                if (attempt < MAX_RETRIES - 1) {
                    await sleep(jitter(1000 * 2 ** attempt));
                    continue;
                }
                // Exhausted retries — return error finishReason
                return {
                    content: "",
                    usage: { inputTokens: 0, outputTokens: 0 },
                    model,
                    finishReason: "error",
                };
            }
            if (!response.ok) {
                if (response.status >= 500 && attempt < MAX_RETRIES - 1) {
                    await sleep(jitter(500 * 2 ** attempt));
                    continue;
                }
                return {
                    content: "",
                    usage: { inputTokens: 0, outputTokens: 0 },
                    model,
                    finishReason: "error",
                };
            }
            let json;
            try {
                json = (await response.json());
            }
            catch {
                return {
                    content: "",
                    usage: { inputTokens: 0, outputTokens: 0 },
                    model,
                    finishReason: "error",
                };
            }
            const choice = json.choices?.[0];
            const rawUsage = json.usage;
            const usage = {
                inputTokens: rawUsage?.prompt_tokens ?? estimateInputTokens(req),
                outputTokens: rawUsage?.completion_tokens ?? 0,
                costUsd: rawUsage
                    ? rawUsage.prompt_tokens / INPUT_TOKENS_PER_USD +
                        rawUsage.completion_tokens / OUTPUT_TOKENS_PER_USD
                    : undefined,
            };
            return {
                content: choice?.message?.content ?? "",
                usage,
                model: json.model ?? model,
                finishReason: mapFinishReason(choice?.finish_reason),
            };
        }
        // Should not reach here but TypeScript requires a return
        if (lastError) {
            return {
                content: "",
                usage: { inputTokens: 0, outputTokens: 0 },
                model,
                finishReason: "error",
            };
        }
        return {
            content: "",
            usage: { inputTokens: 0, outputTokens: 0 },
            model,
            finishReason: "error",
        };
    }
    async *stream(req) {
        const model = req.model ?? this.defaultModel;
        const body = {
            model,
            messages: req.messages,
            temperature: req.temperature,
            max_tokens: req.maxTokens,
            stream: true,
            user: req.metadata?.tenantId,
        };
        const headers = {
            "Content-Type": "application/json",
        };
        if (this.apiKey) {
            headers["Authorization"] = `Bearer ${this.apiKey}`;
        }
        const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
            method: "POST",
            headers,
            body: JSON.stringify(body),
            signal: req.abortSignal,
        });
        if (!response.ok || !response.body) {
            yield { delta: "", usage: { inputTokens: 0, outputTokens: 0 } };
            return;
        }
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = "";
        try {
            while (true) {
                const { done, value } = await reader.read();
                if (done)
                    break;
                buffer += decoder.decode(value, { stream: true });
                const lines = buffer.split("\n");
                buffer = lines.pop() ?? "";
                for (const line of lines) {
                    const trimmed = line.trim();
                    if (!trimmed.startsWith("data:"))
                        continue;
                    const data = trimmed.slice(5).trim();
                    if (data === "[DONE]")
                        return;
                    try {
                        const chunk = JSON.parse(data);
                        const delta = chunk.choices?.[0]?.delta?.content ?? "";
                        if (delta)
                            yield { delta };
                    }
                    catch {
                        // skip malformed chunk
                    }
                }
            }
        }
        finally {
            reader.releaseLock();
        }
    }
}
//# sourceMappingURL=litellm.js.map