//#region ../core/dist/memory/llm-backend-resolver.js
/**
* LLM backend resolver for the transcript extraction pipeline.
*
* Resolves the best available LLM backend using a priority-ordered fallback
* chain. Warm-tier prefers local inference (Ollama → transformers.js) before
* escalating to the cloud (Claude Sonnet). Cold-tier uses Claude Sonnet
* exclusively (owner-mandated, NOT Haiku).
*
* ## Unified resolver convergence (T11757)
*
* Before walking the legacy fallback chain, `resolveLlmBackend` FIRST consults
* the unified role/profile resolver (`resolveLLMForRole('extraction')` in
* `../llm/role-resolver.ts`). This honours a pinned `extraction`-role profile —
* e.g. an openai-compatible provider (`openai`/`ollama`, optionally with a
* `baseUrl` override) or `anthropic` — so the sentient loop uses the SAME
* provider/credential machinery as the rest of CLEO. When the unified resolver
* yields nothing usable (no pinned profile, no credential, or a client that
* cannot be constructed), the call falls through to the legacy warm/cold chain
* below — no behavioural regression for unconfigured installs.
*
* Fallback chain (warm):
*   0. Unified resolver (`extraction` role profile) — anthropic OR openai-compatible
*   1. Ollama daemon running at localhost:11434 (gemma4:e4b-it or fallback model)
*   2. @huggingface/transformers (already installed; ONNX pipeline, zero extra deps)
*   3. Claude Sonnet via Anthropic API (cold escalation)
*   4. null — no backend; caller must skip extraction
*
* Fallback chain (cold):
*   0. Unified resolver (`extraction` role profile)
*   1. Claude Sonnet via Anthropic API (ANTHROPIC_API_KEY required)
*   2. null — no API key; caller must skip extraction
*
* OWNER OVERRIDE 2026-04-15: Cold tier MUST use claude-sonnet-4-6, NOT Haiku.
*
* Research basis: `.cleo/agent-outputs/T750-research-local-llm.md`
* Spec reference: `docs/specs/memory-architecture-spec.md` §7
*
* @task T730
* @task T11757
* @epic T726
*/
/**
* Ollama model priority list.
*
* We try these in order on the running Ollama daemon. The first model that
* is present in the model list is used. This allows users who have already
* pulled a different local model to use it without re-pulling gemma4.
*
* Priority rationale (T752 2026-04-15):
* - `gemma4:e4b-it`: instruction-tuned 4B — 90% schema compliance, ~5 GB Q4 VRAM
* - `gemma4:e2b-it`: instruction-tuned 2B fallback — fits 3.2 GB VRAM
* - `gemma4:e2b`: base model last-resort — no instruction tuning, expect more re-prompts
*/
var OLLAMA_MODEL_PRIORITY = [
	"gemma4:e4b-it",
	"gemma4:e2b-it",
	"gemma4:e2b",
	"phi4-mini",
	"llama3.2:3b",
	"llama3.2"
];
/** Cold-tier model — MUST be Sonnet per owner override 2026-04-15. */
var COLD_TIER_MODEL = "claude-sonnet-4-6";
/** Transformers.js fallback model (ONNX, ~300MB downloaded on first use). */
var TRANSFORMERS_FALLBACK_MODEL = "onnx-community/Qwen2.5-0.5B-Instruct";
/**
* Resolve the best available LLM backend for transcript extraction.
*
* Returns a `ResolvedBackend` with a Vercel AI SDK `LanguageModel` ready for
* `generateObject()`, or `null` if no backend is available and the caller
* should skip extraction.
*
* Never throws — all errors are caught internally.
*
* @param tier - Extraction tier: `warm` (prefer local) or `cold` (cloud).
* @returns Resolved backend descriptor, or `null` if nothing is available.
*
* @example
* ```ts
* const backend = await resolveLlmBackend('warm');
* if (!backend) { return; } // no backend available
* const { object } = await generateObject({ model: backend.model, ... });
* ```
*/
async function resolveLlmBackend(tier) {
	const unifiedBackend = await tryUnifiedResolver();
	if (unifiedBackend) return unifiedBackend;
	if (tier === "warm") {
		const ollamaBackend = await tryOllama();
		if (ollamaBackend) return ollamaBackend;
		const transformersBackend = await tryTransformers();
		if (transformersBackend) return transformersBackend;
		return resolveLlmBackend("cold");
	}
	if (tier === "cold") return await tryAnthropic(COLD_TIER_MODEL);
	return null;
}
/**
* Attempt to resolve an Ollama backend.
*
* Probes the daemon, lists available models, and selects the first model
* from `OLLAMA_MODEL_PRIORITY` that is present. Falls back to the first
* available model if none of the preferred models are installed.
*/
async function tryOllama() {
	try {
		const res = await fetch("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(2e3) });
		if (!res.ok) return null;
		const availableModels = ((await res.json()).models ?? []).map((m) => m.name ?? m.model ?? "").filter(Boolean);
		if (availableModels.length === 0) return null;
		let selectedModel;
		for (const preferred of OLLAMA_MODEL_PRIORITY) if (availableModels.some((m) => m === preferred || m.startsWith(preferred.split(":")[0]))) {
			selectedModel = preferred;
			break;
		}
		if (!selectedModel) selectedModel = availableModels[0];
		const { createOpenAICompatible } = await import('@ai-sdk/openai-compatible');
		return {
			model: createOpenAICompatible({
				baseURL: "http://localhost:11434/v1",
				name: "ollama"
			})(selectedModel),
			name: "ollama",
			modelId: selectedModel
		};
	} catch {
		return null;
	}
}
/**
* Attempt to resolve a transformers.js backend.
*
* Uses @huggingface/transformers (already a CLEO dependency) with a small
* ONNX model. The model is downloaded on first use (~300MB) and cached in
* `~/.cache/huggingface/hub/` — same as the embedding model.
*
* NOTE: @huggingface/transformers v4 does not expose an OpenAI-compatible HTTP
* server, so it cannot be wired as a Vercel AI SDK `LanguageModel` provider
* without implementing the full `LanguageModelV3` interface (complex). Instead,
* this function detects that transformers.js CAN run a generation model and
* returns a marker. The actual generation in the warm tier will use
* `generateTransformersText()` — a direct call path that bypasses the AI SDK
* but still stores results through the same brain APIs.
*
* For the caller (`extractTranscript`), if `name === 'transformers'` then
* `model` is a stub — the caller must use `runTransformersExtraction()` instead
* of `generateObject()`.
*/
async function tryTransformers() {
	try {
		const { env } = await import('./transformers.node-_xbxmK5p.js');
		if (typeof env === "undefined") return null;
		return {
			model: null,
			name: "transformers",
			modelId: TRANSFORMERS_FALLBACK_MODEL
		};
	} catch {
		return null;
	}
}
/**
* Attempt to resolve the Anthropic cold-tier backend.
*
* Checks for ANTHROPIC_API_KEY in the environment. Returns null if the key
* is not present — never throws.
*
* @param modelId - Anthropic model to use (MUST be claude-sonnet-4-6 for cold tier).
*/
async function tryAnthropic(modelId) {
	try {
		const { resolveCredentials } = await import('./credentials-DMWnF6hI.js');
		const apiKey = resolveCredentials("anthropic").apiKey;
		if (!apiKey) return null;
		const { createAnthropic } = await import('@ai-sdk/anthropic');
		return {
			model: createAnthropic({ apiKey })(modelId),
			name: "anthropic",
			modelId
		};
	} catch {
		return null;
	}
}
/**
* Default OpenAI-compatible base URL for the local Ollama daemon (T11757).
*
* Ollama exposes an OpenAI-compatible surface under `/v1`. The provider-registry
* profile records the bare host (`http://localhost:11434`); the unified backend
* path appends `/v1` so `createOpenAICompatible` can speak the OpenAI wire shape.
*/
var OLLAMA_OPENAI_COMPAT_BASE_URL = "http://localhost:11434/v1";
/**
* Resolve the OpenAI-compatible base URL for a provider resolved by the unified
* role/profile resolver.
*
* Looks up the provider-registry profile (which carries the canonical `baseUrl`)
* and normalises it for the OpenAI-compatible client. For `ollama` we guarantee
* the `/v1` shim suffix; other openai-family providers use their registered
* base URL verbatim (already a `/v1`-style endpoint) and fall back to the OpenAI
* default when no profile is registered.
*
* @param provider - The provider id resolved by the unified resolver.
* @returns A base URL suitable for `createOpenAICompatible`, or `undefined`
*          when none can be determined (caller skips the openai-compatible path).
*/
async function resolveOpenAiCompatibleBaseUrl(provider) {
	if (provider === "ollama") return OLLAMA_OPENAI_COMPAT_BASE_URL;
	try {
		const { getProviderProfile } = await import('./provider-registry2-B0lKDoCm.js');
		const profile = await getProviderProfile(provider);
		if (profile?.baseUrl) return profile.baseUrl;
	} catch {}
	if (provider === "openai") return "https://api.openai.com/v1";
}
/**
* Attempt to resolve an extraction backend via the E9 resolution chokepoint
* (`resolveLLMForSystem({ kind: 'role', id: 'extraction' })`) — the T11757
* convergence point, folded onto the single chokepoint by T11750.
*
* This is consulted BEFORE the legacy warm/cold chain so a pinned `extraction`
* profile (or the unscoped default) drives the sentient loop with the same
* provider/credential machinery as every other CLEO LLM call-site:
*
*  - `anthropic` provider → build via `createAnthropic` (returns `name: 'anthropic'`).
*  - `openai` / `ollama` (openai-compatible) → build via `createOpenAICompatible`
*    against the resolved base URL (returns `name: 'ollama'` for ollama, else
*    `name: 'openai'`). An openai-family client whose key later 401s simply
*    fails the downstream `generateObject` call and the caller degrades — it is
*    NOT this resolver's job to validate the credential live.
*
* Returns `null` (so the caller falls through to the legacy chain) when:
*  - the resolver throws or yields no usable provider, OR
*  - no credential is reachable for the resolved provider, OR
*  - the provider is not one we can wire here (e.g. a transport-only provider).
*
* ## Why the chokepoint, not a bare `resolveLLMForRole` (T11750 · AC2)
*
* Routing through `resolveLLMForSystem` (rather than calling `resolveLLMForRole`
* directly) folds the Ollama→transformers→Anthropic fallback chain BEHIND the
* one E9 chokepoint: the legacy local-backend chain below is now strictly the
* *fall-through* path when the chokepoint yields no usable cloud credential, and
* the role/profile/credential resolution itself happens in exactly ONE place.
* `skipCatalogDefault: true` is passed because this path only consults the
* resolved `provider` + `sealedCredential` — it builds its own AI-SDK model and
* does not need the chokepoint's provider-registry default-model upgrade.
*
* NOTE: codex's ChatGPT-backend transport is intentionally NOT handled here
* (tracked separately as T11767). A codex/openai profile resolves through the
* openai-compatible path; if its client cannot authenticate it falls through to
* the warm chain rather than crashing.
*
* @task T11757
* @task T11750
*/
async function tryUnifiedResolver() {
	try {
		const { resolveLLMForSystem } = await import('./system-resolver2-CSKl6faZ.js');
		const resolved = await resolveLLMForSystem({
			kind: "role",
			id: "extraction"
		}, { skipCatalogDefault: true });
		const apiKey = resolved.sealedCredential ? (await resolved.sealedCredential.fetch()).value : null;
		if (resolved.provider === "anthropic") {
			if (!apiKey) return null;
			const { createAnthropic } = await import('@ai-sdk/anthropic');
			return {
				model: createAnthropic({ apiKey })(resolved.model),
				name: "anthropic",
				modelId: resolved.model
			};
		}
		if (resolved.provider === "openai" || resolved.provider === "ollama") {
			const baseURL = await resolveOpenAiCompatibleBaseUrl(resolved.provider);
			if (!baseURL) return null;
			const { createOpenAICompatible } = await import('@ai-sdk/openai-compatible');
			return {
				model: createOpenAICompatible({
					baseURL,
					name: resolved.provider,
					...apiKey ? { apiKey } : {}
				})(resolved.model),
				name: resolved.provider === "ollama" ? "ollama" : "openai",
				modelId: resolved.model
			};
		}
		return null;
	} catch {
		return null;
	}
}

export { resolveLlmBackend };
//# sourceMappingURL=llm-backend-resolver-BPbvttS4.js.map
