/** * Cross-provider provisioning-aware LLM selection (DHQ-081 · T11978). * * ## Problem * * `selectProviderModel` in `role-resolver.ts` (tier 8) falls back to * `IMPLICIT_FALLBACK_PROVIDER = 'anthropic'` unconditionally when all 7 * config tiers miss. This means a machine with only `OPENAI_API_KEY` set * still resolves to `anthropic` and returns `credential: null`, causing a * silent 401 / graceful-degradation miss. * * ## Fix * * When all config tiers miss, instead of immediately returning the hardcoded * `anthropic` fallback, `selectBestProvisioned` is called. It: * * 1. Enumerates all {@link BUILTIN_PROVIDER_IDS} (11 providers). * 2. **Refresh-on-use (T11986 · DHQ-087)**: for any provider with an * expired-but-refreshable OAuth credential, attempts a refresh BEFORE * the provisioning probe so the selector sees a valid token instead of * silently filtering the provider as "not-provisioned". * 3. Probes which are provisioned (have a non-exhausted, non-expired * credential in the cred store OR a non-empty env var). * 4. Ranks provisioned providers by `scoreProvider(provider, taskTier)`. * 5. Returns the winner's `SelectedProviderModel`, or `null` if nothing * is provisioned (caller falls through to the anthropic implicit fallback, * preserving backward compat). * * ## Gate-13 compliance * * This module: * - MUST NOT construct any transport or SDK client. * - MUST NOT read `process.env.*_API_KEY` directly — delegates to the * credential layer (`resolveCredentials`, `pickCredentialForProviderSync`). * - MUST NOT define a new exported `resolveLLMFor*` function. * - MUST NOT hardcode model-id literals outside the provider-registry SSoT. * * @module llm/cross-provider-selector * @task T11978 * @task T11986 * @epic T11679 */ import type { ResolutionSource } from '@cleocode/contracts'; import type { ProviderTier } from '@cleocode/contracts/llm/provider-profile.js'; import type { ModelTransport } from './types-config.js'; /** * The canonical set of builtin provider IDs that the cross-provider selector * enumerates. Mirrors `BuiltinProviderId` (contracts/src/llm/provider-id.ts). * Defined here as a runtime array so we can iterate without importing the * union type. * * @task T11978 */ export declare const BUILTIN_PROVIDER_IDS: ReadonlyArray; /** * Bias added for frontier/standard cloud providers that hold a valid credential. * This ensures a provisioned frontier cloud provider (anthropic, openai) outranks * a merely-running local ollama for frontier-tier tasks. * * Rationale (Q1 ratification): +60 on top of frontier's TIER_BASE=100 → score * 160 ≥ local's TIER_BASE=120 + LOCALITY_BONUS=30 = 150. * * @task T11978 */ export declare const PROVISIONED_CLOUD_BIAS = 60; /** * Internal shape returned by {@link selectBestProvisioned}. * Mirrors the `SelectedProviderModel` interface in `role-resolver.ts`. * Kept internal — callers receive `SelectedProviderModel` via the role-resolver. * * @internal */ export interface SelectedProviderModel { provider: ModelTransport; model: string; credentialLabel: string | undefined; source: ResolutionSource; } /** * Provisioning state and score for a single provider. * * @task T11978 */ export interface ProviderEnumeration { /** Provider canonical ID. */ id: ModelTransport; /** Human-readable display name (from provider registry, or the id string). */ displayName: string; /** Capability tier (from provider registry). */ tier: ProviderTier; /** Whether the provider has at least one usable credential. */ provisioningState: 'provisioned' | 'not-provisioned'; /** * Credential reachability. * `'auth-reachable'` — has a non-expired, non-invalid, non-exhausted credential. * `'no-credential'` — no credential at all. * `'credential-expired'` — credential exists but is expired. * `'credential-invalid'` — credential exists but is marked invalid/exhausted. */ reachabilityState: 'auth-reachable' | 'no-credential' | 'credential-expired' | 'credential-invalid'; /** * Whether the local ollama daemon is reachable. * `null` for non-local providers (tier !== 'local'). */ machineRunnable: boolean | null; /** Number of credentials in the store for this provider. */ credentialCount: number; /** Score from the cross-provider scoring function (null if not provisioned). */ resolverScore: number | null; /** * The model that would be selected by the catalog resolver for this provider. * `null` if no catalog is available. */ wouldPickModel: string | null; /** Reason for selection. */ wouldPickReason: 'cross-provider-best' | 'implicit-fallback' | 'not-selected'; } /** * Reset ollama probe cache for testing. * @internal */ export declare function _resetOllamaProbeCache(): void; /** * Check whether the ollama daemon is listening at `baseUrl`. * * Uses a lightweight HTTP GET with a 200ms timeout. Result is cached in-process * for {@link OLLAMA_PROBE_TTL_MS} (30 seconds) to avoid hammering localhost on * every resolve call. * * Gate-13 note: this is a vanilla `fetch` call to probe localhost, NOT a * transport/SDK client construction — Gate-13 does not cover network utility calls. * * @param baseUrl - Ollama base URL (default: `http://localhost:11434`). * @returns `true` if the daemon responded with HTTP 200, `false` otherwise. * * @task T11978 */ export declare function probeOllamaAlive(baseUrl: string): Promise; /** * Select the appropriate ollama default model based on available RAM (DHQ-081 Q2). * * RAM gating uses `os.totalmem()` (Node.js built-in, no native addon required). * VRAM detection is out-of-scope (deferred to T11982 wizard task). * * Resolution: * - RAM ≥ 8 GB → `gemma4:e4b` for frontier/standard; `gemma4:e2b` for fast/local. * - 4 GB ≤ RAM < 8 GB → `gemma4:e2b` for all tiers. * - RAM < 4 GB → `qwen2:0.5b` (proof-of-life only; logged as WARNING). * * Note: `qwen2:0.5b` MUST NOT be selected as a resolver default. It is a * last-resort proof-of-life floor for under-resourced machines. * * These string literals live here (the one allowed location outside * `ollama.ts`) because they are DEFAULTS within the selector logic. The * profile's `defaultModel`/`defaultAuxModel` fields are the primary SSoT; * the selector reads them via `getProviderProfile('ollama')` when available * and only falls back to these when the profile is unavailable (defensive). * * Tags live-verified 2026-06-11 on ollama.com/library/gemma4: * - gemma4:e2b = 7.2 GB (Q4_K_M), edge 2B effective params, ≥ 4 GB RAM * - gemma4:e4b = 9.6 GB (Q4_K_M), edge 4B effective params, ≥ 8 GB RAM * If the catalog does not yet have a `gemma4` family entry, the resolver * logs a hint to run `cleo llm refresh-catalog`. * * @param tier - The task tier for which a model is being selected. * @param ramBytesOverride - Override `os.totalmem()` for testing. * @returns The model ID string to use for ollama. * * @task T11978 */ export declare function ollamaDefaultModelForTier(tier: ProviderTier, ramBytesOverride?: number): string; /** * Score a provisioned provider for a given task tier. * * Higher score = preferred. Tie-break by credential priority (lower priority * value = higher user-assigned trust) is handled in {@link selectBestProvisioned}. * * Scoring formula: * ``` * score = TIER_BASE[tier] * + TASK_MATCH_BONUS (if tier aligns with taskTier) * + LOCALITY_BONUS (local + daemon alive) * + PROVISIONED_CLOUD_BIAS (frontier/standard cloud providers with valid cred) * - COST_PENALTY (capped at MAX_COST_PENALTY) * ``` * * @param provider - The provider transport ID. * @param providerTier - The capability tier of the provider. * @param taskTier - The capability tier required by the current task. * @param ollamaAlive - Whether the ollama daemon is currently reachable. * @param costInputPerMillion - Cost per 1M input tokens (from catalog), or 0. * @returns The numeric score (higher = preferred). * * @task T11978 */ export declare function scoreProvider(provider: ModelTransport, providerTier: ProviderTier, taskTier: ProviderTier, ollamaAlive: boolean, costInputPerMillion: number): number; /** * Map a role name to a task tier for scoring purposes. * * @param role - The LLM role name. * @returns The task tier for the role. * * @task T11978 */ export declare function roleTierFor(role: string): ProviderTier; /** * Enumerate all builtin providers with their provisioning state and scores. * * Used by `cleo llm providers` and `cleo llm health` subcommands to display * the full provisioning landscape to the user. * * @param taskTier - The task tier to score against (defaults to `'frontier'`). * @returns Array of provider enumeration records. * * @task T11978 */ export declare function enumerateProvisionedProviders(taskTier?: ProviderTier): Promise; /** * Select the best provisioned provider for the given role. * * Called at tier 8 of `selectProviderModel` in `role-resolver.ts`, replacing * the unconditional `IMPLICIT_FALLBACK_PROVIDER = 'anthropic'` hardcode. * * ## Algorithm * * 1. Enumerate all {@link BUILTIN_PROVIDER_IDS}. * 2. Filter to those that are provisioned (cred store or env var). * 3. Score each provisioned provider. * 4. Pick the highest scorer. Tie-break: lowest credential priority value. * 5. Resolve the model via catalog (`resolveProviderDefaultModel`) or * provider profile `defaultModel`. * 6. For ollama: gate model selection on `os.totalmem()`. * * ## When nothing is provisioned * * Returns `null`. The caller falls through to the hardcoded anthropic fallback, * preserving backward compatibility. * * ## Gate-13 compliance * * Does NOT construct any transport or SDK client. Does NOT read * `process.env.*_API_KEY` directly (delegates to credential layer). * Does NOT define a new `resolveLLMFor*` function. * * @param role - The LLM role name (used to derive `taskTier`). * @param opts - Options including `projectRoot`. * @returns The best {@link SelectedProviderModel}, or `null` if nothing is provisioned. * * @task T11978 */ export declare function selectBestProvisioned(role: string, opts: { projectRoot: string; }): Promise; //# sourceMappingURL=cross-provider-selector.d.ts.map