/**
 * src/models/ollama.ts
 *
 * Local-first Ollama provider. Uses Ollama's OpenAI-compatible endpoint
 * at http://localhost:11434/v1/chat/completions — no API key, no quota,
 * no leak risk, works offline. Cross-platform: Ollama installs the same
 * way on macOS, Windows, and Linux and always exposes the same port.
 *
 * Role in the failover chain (see failover.ts):
 *   primary  → bundled NIM (or user-supplied NVIDIA_NIM_API_KEY)
 *   fallback → THIS (Ollama, if a daemon is running locally)
 *
 * Why a fallback and not the primary: NIM is fast and uses big models
 * (qwen3-next-80b). Local Ollama tops out at whatever fits on the user's
 * RAM — 7B/8B models are common. We use it as the "nothing else worked"
 * safety net so the agent never returns a bare E400 to the customer.
 *
 * Model selection: lazy-resolved on first use. We probe /api/tags and
 * pick the best installed model from a preferred list (Arabic-capable
 * + tool-calling first). If none of the preferred models is installed,
 * we fall back to whatever IS installed. If the daemon isn't running,
 * the chat() call throws a ModelError which the failover layer treats
 * as "skip this provider."
 */
import { createChildLogger } from '../utils/logger.js';
import { callOpenAICompatible, ModelError, } from './provider.js';
const log = createChildLogger('models:ollama');
const OLLAMA_HOST = process.env.OLLAMA_HOST?.replace(/\/$/, '') || 'http://localhost:11434';
const OLLAMA_BASE_URL = `${OLLAMA_HOST}/v1`;
// Ranked: best Arabic + tool-calling first, then general-purpose, then small/fast.
// On a fresh box `ollama pull qwen2.5:7b` gives us the top choice. We don't
// pull automatically — that would silently consume gigabytes; we only USE
// what the user already has.
const PREFERRED_MODELS = [
    'qwen2.5:7b',
    'qwen2.5:7b-instruct',
    'qwen2.5:14b',
    'qwen2.5:3b',
    'llama3.1:8b',
    'llama3.1:latest',
    'llama3.2:3b',
    'llama3.2:latest',
    'mistral:7b',
    'mistral:latest',
];
let cached;
/**
 * One-shot probe. Result is cached for the process lifetime so failover
 * doesn't re-check on every brain call. Call clearOllamaProbeCache() if
 * you want to re-run (e.g. after the user pulls a new model).
 */
export async function probeOllama() {
    if (cached)
        return cached;
    try {
        const r = await fetch(`${OLLAMA_HOST}/api/tags`, {
            signal: AbortSignal.timeout(2_000),
        });
        if (!r.ok) {
            cached = { available: false, model: null, installedCount: 0, suggestedPull: null };
            return cached;
        }
        const data = (await r.json());
        const installed = new Set((data.models ?? []).map(m => m.name));
        let chosen = null;
        for (const candidate of PREFERRED_MODELS) {
            if (installed.has(candidate)) {
                chosen = candidate;
                break;
            }
        }
        // Last-ditch fallback: take whatever is installed.
        if (!chosen && data.models?.length) {
            chosen = data.models[0].name;
        }
        cached = {
            available: true,
            model: chosen,
            installedCount: data.models?.length ?? 0,
            suggestedPull: chosen ? null : 'ollama pull qwen2.5:7b',
        };
        return cached;
    }
    catch (err) {
        log.debug({ err: err instanceof Error ? err.message : String(err) }, 'Ollama probe failed (daemon not running?)');
        cached = { available: false, model: null, installedCount: 0, suggestedPull: null };
        return cached;
    }
}
export function clearOllamaProbeCache() {
    cached = undefined;
}
export class OllamaProvider {
    name = 'ollama';
    resolvedModel = null;
    get model() {
        return this.resolvedModel ?? 'ollama:auto';
    }
    async chat(messages, options = {}) {
        if (!this.resolvedModel) {
            const probe = await probeOllama();
            if (!probe.available) {
                throw new ModelError('Ollama daemon not reachable at ' + OLLAMA_HOST + ' (start with: ollama serve)', 503, 'ollama');
            }
            if (!probe.model) {
                throw new ModelError('Ollama is running but has no models installed (try: ' + (probe.suggestedPull ?? 'ollama pull qwen2.5:7b') + ')', 503, 'ollama');
            }
            this.resolvedModel = probe.model;
            log.info({ model: this.resolvedModel, installedCount: probe.installedCount }, 'Ollama provider resolved');
        }
        return callOpenAICompatible(OLLAMA_BASE_URL, 
        // Ollama's OpenAI-compat layer ignores the bearer token. Send a
        // placeholder so the shared caller doesn't need a special branch.
        'ollama-local-no-auth', this.resolvedModel, messages, {
            temperature: options.temperature ?? 0.7,
            // Ollama context window varies by model; keep this conservative
            // so 7B models on 16GB RAM machines don't OOM mid-turn.
            maxTokens: options.maxTokens ?? 2048,
            ...options,
        });
    }
}
export const ollama = new OllamaProvider();
//# sourceMappingURL=ollama.js.map