/**
 * Minimal OpenAI-compatible chat client: streaming chat completions with tool
 * calling, plus the model catalog. No provider SDK; plain fetch + SSE parsing.
 *
 * Requests go to the CheapCode API (which meters usage against your account
 * and handles provider pinning server-side). A legacy OpenRouter key
 * (sk-or-...) still routes directly to OpenRouter for BYO-key use.
 */
const CHEAPCODE_URL = 'https://api.cheapcode.ai/v1';
const OPENROUTER_URL = 'https://openrouter.ai/api/v1';
export function baseUrlFor(apiKey) {
    if (process.env.CHEAPCODE_API_URL)
        return process.env.CHEAPCODE_API_URL.replace(/\/$/, '');
    return apiKey.startsWith('sk-or-') ? OPENROUTER_URL : CHEAPCODE_URL;
}
export class OpenRouterError extends Error {
    status;
    constructor(message, status) {
        super(message);
        this.status = status;
        this.name = 'OpenRouterError';
    }
}
const MAX_ATTEMPTS = 3;
function isRetryable(err) {
    if (err instanceof OpenRouterError) {
        return err.status === 429 || (err.status !== undefined && err.status >= 500);
    }
    // Undici network failures (ECONNRESET, DNS, etc.) surface as TypeError from fetch.
    return err instanceof TypeError;
}
/**
 * Anthropic models support prompt caching via cache_control breakpoints.
 * Marking the system prompt (the largest stable prefix in an agent loop)
 * makes every subsequent call in the session a cache hit on those tokens.
 * Other providers either cache implicitly (OpenAI, Google) or ignore this.
 */
function withCacheControl(messages, model) {
    if (!/^anthropic\//.test(model))
        return messages;
    return messages.map((m) => m.role === 'system'
        ? {
            role: 'system',
            content: [{ type: 'text', text: m.content, cache_control: { type: 'ephemeral' } }],
        }
        : m);
}
export async function streamChat(opts, onContentDelta) {
    let lastError;
    for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
        // Only safe to retry if nothing has been streamed to the caller yet;
        // retrying a half-delivered response would duplicate output.
        let deliveredAnything = false;
        try {
            return await streamChatOnce(opts, (delta) => {
                deliveredAnything = true;
                onContentDelta?.(delta);
            });
        }
        catch (err) {
            lastError = err;
            if (opts.signal?.aborted || deliveredAnything || !isRetryable(err) || attempt === MAX_ATTEMPTS) {
                throw err;
            }
            const delay = 1000 * 2 ** (attempt - 1) * (0.5 + Math.random());
            await new Promise((r) => setTimeout(r, delay));
        }
    }
    throw lastError; // unreachable, satisfies the compiler
}
async function streamChatOnce(opts, onContentDelta) {
    const body = {
        model: opts.model,
        messages: withCacheControl(opts.messages, opts.model),
        stream: true,
        usage: { include: true },
    };
    if (opts.provider)
        body.provider = opts.provider;
    if (opts.tools && opts.tools.length > 0) {
        body.tools = opts.tools.map((t) => ({
            type: 'function',
            function: { name: t.name, description: t.description, parameters: t.parameters },
        }));
    }
    const res = await fetch(`${baseUrlFor(opts.apiKey)}/chat/completions`, {
        method: 'POST',
        headers: {
            Authorization: `Bearer ${opts.apiKey}`,
            'Content-Type': 'application/json',
            'HTTP-Referer': 'https://cheapcode.ai',
            'X-Title': 'CheapCode',
        },
        body: JSON.stringify(body),
        signal: opts.signal,
    });
    if (!res.ok || !res.body) {
        let detail = '';
        try {
            const err = (await res.json());
            detail = err?.error?.message ?? '';
        }
        catch {
            /* non-JSON error body */
        }
        throw new OpenRouterError(`API request failed (${res.status})${detail ? `: ${detail}` : ''}`, res.status);
    }
    let content = '';
    let finishReason = null;
    let usage = null;
    // Tool call deltas arrive keyed by index; accumulate them.
    const toolAcc = new Map();
    const decoder = new TextDecoder();
    let buffer = '';
    const reader = res.body.getReader();
    const handleLine = (line) => {
        if (!line.startsWith('data: '))
            return;
        const payload = line.slice(6).trim();
        if (payload === '[DONE]')
            return;
        let json;
        try {
            json = JSON.parse(payload);
        }
        catch {
            return; // ignore keep-alive comments / partial junk
        }
        if (json.error) {
            throw new OpenRouterError(json.error.message ?? 'stream error');
        }
        if (json.usage) {
            usage = {
                promptTokens: json.usage.prompt_tokens ?? 0,
                completionTokens: json.usage.completion_tokens ?? 0,
                cost: typeof json.usage.cost === 'number' ? json.usage.cost : null,
            };
        }
        const choice = json.choices?.[0];
        if (!choice)
            return;
        if (choice.finish_reason)
            finishReason = choice.finish_reason;
        const delta = choice.delta ?? {};
        if (typeof delta.content === 'string' && delta.content.length > 0) {
            content += delta.content;
            onContentDelta?.(delta.content);
        }
        if (Array.isArray(delta.tool_calls)) {
            for (const tc of delta.tool_calls) {
                const idx = tc.index ?? 0;
                const acc = toolAcc.get(idx) ?? { id: '', name: '', arguments: '' };
                if (tc.id)
                    acc.id = tc.id;
                if (tc.function?.name)
                    acc.name += tc.function.name;
                if (tc.function?.arguments)
                    acc.arguments += tc.function.arguments;
                toolAcc.set(idx, acc);
            }
        }
    };
    for (;;) {
        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)
            handleLine(line.trimEnd());
    }
    if (buffer.trim())
        handleLine(buffer.trimEnd());
    const toolCalls = [...toolAcc.entries()]
        .sort((a, b) => a[0] - b[0])
        .map(([, v], i) => ({
        id: v.id || `call_${i}`,
        name: v.name,
        arguments: v.arguments || '{}',
    }));
    return { content, toolCalls, finishReason, usage };
}
let modelCache = null;
export async function listModels(apiKey) {
    if (modelCache)
        return modelCache;
    const res = await fetch(`${baseUrlFor(apiKey)}/models`, {
        headers: { Authorization: `Bearer ${apiKey}` },
    });
    if (!res.ok)
        throw new OpenRouterError(`failed to list models (${res.status})`, res.status);
    const json = (await res.json());
    modelCache = json.data.map((m) => ({
        id: m.id,
        name: m.name ?? m.id,
        contextLength: m.context_length ?? 0,
    }));
    return modelCache;
}
