// SF Extension — Dynamic Model Router
// Maps complexity tiers to models, enforcing downgrade-only semantics.
// The user's configured model is always the ceiling.
import { getProviderCapabilities } from "@singularity-forge/ai";
import { getToolCompatibility } from "@singularity-forge/coding-agent";
import { tierOrdinal } from "./complexity-classifier.js";
import { lookupModelCost } from "./model-cost-table.js";
import { tierFor } from "./model-registry.js";
// ─── Known Model Tiers ───────────────────────────────────────────────────────
// MIGRATED: this table moved to model-registry.ts as the TIER map. Kept here
// as a deprecated re-export shim so external callers that import
// MODEL_CAPABILITY_TIER directly don't break during the transition. Swarm C
// will remove this shim once metrics + remaining consumers are migrated.
/** @deprecated Use tierFor() from model-registry.js instead. */
export const MODEL_CAPABILITY_TIER = {
	// Light-tier models (cheapest)
	"claude-haiku-4-5": "light",
	"claude-3-5-haiku-latest": "light",
	"claude-3-haiku-20240307": "light",
	"gpt-4o-mini": "light",
	"gpt-4.1-mini": "light",
	"gpt-4.1-nano": "light",
	"gpt-5-mini": "light",
	"gpt-5-nano": "light",
	"gpt-5.1-codex-mini": "light",
	"gpt-5.3-codex-spark": "light",
	"gemini-2.0-flash": "light",
	"gemini-flash-2.0": "light",
	"gemini-3.1-flash-lite-preview": "light",
	"gemini-2.5-flash-lite": "light",
	"glm-4.7-flash": "light",
	"glm-4.7-flashx": "light",
	"ministral-3b-latest": "light",
	"ministral-8b-latest": "light",
	"devstral-small-2505": "light",
	"devstral-small-2507": "light",
	"labs-devstral-small-2512": "light",
	// Standard-tier models
	"claude-sonnet-4-6": "standard",
	"claude-sonnet-4-5-20250514": "standard",
	"claude-3-5-sonnet-latest": "standard",
	"gpt-4o": "standard",
	"gpt-4.1": "standard",
	"gpt-5.1-codex-max": "standard",
	"gemini-2.5-pro": "standard",
	"gemini-3-flash-preview": "standard",
	"gemini-2.5-flash": "standard",
	"deepseek-chat": "standard",
	"glm-4.7": "standard",
	"qwen3-coder:480b": "standard",
	"qwen3-coder-next": "standard",
	"kimi-k2.6": "standard",
	"kimi-for-coding": "standard",
	"MiniMax-M2.7": "standard",
	"MiniMax-M2.7-highspeed": "standard",
	"codestral-latest": "standard",
	"devstral-2512": "standard",
	"devstral-medium-2507": "standard",
	"devstral-medium-latest": "standard",
	"magistral-small": "standard",
	"mistral-medium-2505": "standard",
	"mistral-medium-2508": "standard",
	"mistral-medium-latest": "standard",
	"mistral-nemo": "standard",
	"mistral-small-2506": "standard",
	"mistral-small-2603": "standard",
	"mistral-small-latest": "standard",
	"pixtral-12b": "standard",
	// Heavy-tier models (most capable)
	"claude-opus-4-6": "heavy",
	"claude-3-opus-latest": "heavy",
	"gpt-4-turbo": "heavy",
	"gpt-5": "heavy",
	"gpt-5-pro": "heavy",
	"gpt-5.1": "heavy",
	"gpt-5.2": "heavy",
	"gpt-5.2-codex": "heavy",
	"gpt-5.3-codex": "heavy",
	"gpt-5.4": "heavy",
	"gpt-5.4-mini": "standard",
	"gpt-5.5": "heavy",
	o1: "heavy",
	o3: "heavy",
	"o4-mini": "heavy",
	"o4-mini-deep-research": "heavy",
	"gemini-3.1-pro-preview": "heavy",
	"gemini-3-pro-preview": "heavy",
	"kimi-k2-thinking": "heavy",
	"qwen3-next:80b": "heavy",
	"glm-5": "heavy",
	"glm-5-turbo": "heavy",
	"glm-5.1": "heavy",
	"glm-5v-turbo": "heavy",
	"magistral-medium-latest": "heavy",
	"mistral-large-2411": "heavy",
	"mistral-large-2512": "heavy",
	"mistral-large-latest": "heavy",
	"open-mixtral-8x22b": "heavy",
	"pixtral-large-latest": "heavy",
};
// ─── Capability Profiles Data Table ──────────────────────────────────────────
// Per-model capability profiles (0–100 scale). Used for capability-aware
// model selection within an eligible tier set.
export const MODEL_CAPABILITY_PROFILES = {
	// ── Anthropic ──────────────────────────────────────────────────────────────
	"claude-opus-4-6": {
		coding: 95,
		debugging: 90,
		research: 85,
		reasoning: 95,
		speed: 30,
		longContext: 80,
		instruction: 90,
		// Agentic: Claude Opus is built around extended tool-use loops.
		agentic: 95,
	},
	"claude-sonnet-4-6": {
		coding: 85,
		debugging: 80,
		research: 75,
		reasoning: 80,
		speed: 60,
		longContext: 75,
		instruction: 85,
		agentic: 92,
	},
	"claude-sonnet-4-5-20250514": {
		coding: 85,
		debugging: 80,
		research: 75,
		reasoning: 80,
		speed: 60,
		longContext: 75,
		instruction: 85,
		agentic: 90,
	},
	"claude-3-5-sonnet-latest": {
		coding: 82,
		debugging: 78,
		research: 72,
		reasoning: 78,
		speed: 62,
		longContext: 70,
		instruction: 82,
		agentic: 85,
	},
	"claude-haiku-4-5": {
		coding: 60,
		debugging: 50,
		research: 45,
		reasoning: 50,
		speed: 95,
		longContext: 50,
		instruction: 75,
		// Haiku follows tool-use contracts but is less reliable than Sonnet on
		// long agentic loops.
		agentic: 75,
	},
	"claude-3-5-haiku-latest": {
		coding: 60,
		debugging: 50,
		research: 45,
		reasoning: 50,
		speed: 95,
		longContext: 50,
		instruction: 75,
		agentic: 75,
	},
	"claude-3-haiku-20240307": {
		coding: 50,
		debugging: 40,
		research: 35,
		reasoning: 40,
		speed: 95,
		longContext: 40,
		instruction: 65,
		agentic: 35,
	},
	"claude-3-opus-latest": {
		agentic: 88,
		coding: 90,
		debugging: 85,
		research: 82,
		reasoning: 90,
		speed: 35,
		longContext: 75,
		instruction: 88,
	},
	// ── OpenAI GPT ─────────────────────────────────────────────────────────────
	"gpt-4o": {
		coding: 80,
		debugging: 75,
		research: 70,
		reasoning: 75,
		speed: 65,
		longContext: 70,
		instruction: 80,
		agentic: 65,
	},
	"gpt-4o-mini": {
		coding: 55,
		debugging: 45,
		research: 40,
		reasoning: 45,
		speed: 90,
		longContext: 45,
		instruction: 70,
		agentic: 50,
	},
	"gpt-4-turbo": {
		coding: 78,
		debugging: 72,
		research: 68,
		reasoning: 72,
		speed: 50,
		longContext: 65,
		instruction: 78,
		agentic: 60,
	},
	"gpt-4.1": {
		coding: 82,
		debugging: 78,
		research: 72,
		reasoning: 78,
		speed: 62,
		longContext: 72,
		instruction: 82,
		agentic: 70,
	},
	"gpt-4.1-mini": {
		coding: 58,
		debugging: 48,
		research: 42,
		reasoning: 48,
		speed: 88,
		longContext: 48,
		instruction: 72,
		agentic: 55,
	},
	"gpt-4.1-nano": {
		coding: 40,
		debugging: 30,
		research: 25,
		reasoning: 30,
		speed: 95,
		longContext: 30,
		instruction: 60,
		agentic: 35,
	},
	"gpt-5": {
		coding: 92,
		debugging: 88,
		research: 85,
		reasoning: 92,
		speed: 40,
		longContext: 85,
		instruction: 90,
		// GPT-5 family is strongly agentic per OpenAI's tool-use evals.
		agentic: 92,
	},
	"gpt-5-mini": {
		coding: 62,
		debugging: 52,
		research: 48,
		reasoning: 52,
		speed: 88,
		longContext: 52,
		instruction: 74,
		agentic: 75,
	},
	"gpt-5-nano": {
		coding: 42,
		debugging: 32,
		research: 28,
		reasoning: 32,
		speed: 95,
		longContext: 32,
		instruction: 62,
		agentic: 60,
	},
	"gpt-5-pro": {
		coding: 94,
		debugging: 90,
		research: 88,
		reasoning: 94,
		speed: 35,
		longContext: 88,
		instruction: 92,
		agentic: 94,
	},
	"gpt-5.1": {
		coding: 93,
		debugging: 89,
		research: 86,
		reasoning: 93,
		speed: 42,
		longContext: 86,
		instruction: 91,
		agentic: 92,
	},
	"gpt-5.1-codex-max": {
		coding: 90,
		debugging: 85,
		research: 70,
		reasoning: 85,
		speed: 55,
		longContext: 75,
		instruction: 85,
		// Codex-tuned models are agentic-capable but not as reliable as the
		// flagship gpt-5/5.x lineup for long tool-use loops.
		agentic: 80,
	},
	"gpt-5.1-codex-mini": {
		coding: 65,
		debugging: 55,
		research: 40,
		reasoning: 50,
		speed: 88,
		longContext: 48,
		instruction: 72,
		agentic: 55,
	},
	"gpt-5.2": {
		coding: 93,
		debugging: 90,
		research: 87,
		reasoning: 93,
		speed: 42,
		longContext: 87,
		instruction: 91,
		agentic: 92,
	},
	"gpt-5.2-codex": {
		coding: 93,
		debugging: 90,
		research: 72,
		reasoning: 88,
		speed: 50,
		longContext: 78,
		instruction: 88,
		agentic: 82,
	},
	"gpt-5.3-codex": {
		coding: 94,
		debugging: 91,
		research: 74,
		reasoning: 89,
		speed: 50,
		longContext: 80,
		instruction: 89,
		agentic: 84,
	},
	"gpt-5.3-codex-spark": {
		coding: 68,
		debugging: 58,
		research: 42,
		reasoning: 52,
		speed: 90,
		longContext: 50,
		instruction: 74,
		agentic: 55,
	},
	"gpt-5.4": {
		coding: 95,
		debugging: 92,
		research: 88,
		reasoning: 94,
		speed: 42,
		longContext: 88,
		instruction: 92,
		agentic: 94,
	},
	"gpt-5.4-mini": {
		coding: 80,
		debugging: 75,
		research: 70,
		reasoning: 78,
		speed: 72,
		longContext: 72,
		instruction: 80,
		agentic: 80,
	},
	// GPT-5.5 scores are relative to the existing gpt-5.4 profile and backed by
	// OpenAI's 2026-04-23 published eval deltas across coding, tool use, and long context.
	// Source: https://openai.com/index/introducing-gpt-5-5/
	"gpt-5.5": {
		coding: 96,
		debugging: 93,
		research: 89,
		reasoning: 95,
		speed: 42,
		longContext: 90,
		instruction: 93,
		agentic: 95,
	},
	// ── OpenAI o-series (reasoning-first) ──────────────────────────────────────
	o1: {
		coding: 78,
		debugging: 82,
		research: 78,
		reasoning: 90,
		speed: 20,
		longContext: 65,
		instruction: 82,
		agentic: 65,
	},
	o3: {
		coding: 80,
		debugging: 85,
		research: 80,
		reasoning: 92,
		speed: 25,
		longContext: 70,
		instruction: 85,
		agentic: 72,
	},
	"o4-mini": {
		coding: 75,
		debugging: 80,
		research: 72,
		reasoning: 88,
		speed: 60,
		longContext: 65,
		instruction: 80,
		agentic: 70,
	},
	"o4-mini-deep-research": {
		coding: 75,
		debugging: 80,
		research: 85,
		reasoning: 88,
		speed: 30,
		longContext: 80,
		instruction: 80,
		agentic: 65,
	},
	// ── Google ─────────────────────────────────────────────────────────────────
	"gemini-2.5-pro": {
		coding: 75,
		debugging: 70,
		research: 85,
		reasoning: 75,
		speed: 55,
		longContext: 90,
		instruction: 75,
		agentic: 70,
	},
	"gemini-3.1-pro-preview": {
		coding: 82,
		debugging: 78,
		research: 92,
		reasoning: 84,
		speed: 48,
		longContext: 98,
		instruction: 82,
		agentic: 85,
	},
	"gemini-3-pro-preview": {
		coding: 82,
		debugging: 78,
		research: 90,
		reasoning: 84,
		speed: 50,
		longContext: 96,
		instruction: 82,
		agentic: 85,
	},
	"gemini-3-flash-preview": {
		coding: 62,
		debugging: 55,
		research: 70,
		reasoning: 60,
		speed: 88,
		longContext: 88,
		instruction: 72,
		// Gemini Flash follows tool contracts but is occasionally chatty in
		// agentic loops; mid-tier so it doesn't dominate execute-task vs
		// a Sonnet/Opus/K2.6 alternative.
		agentic: 70,
	},
	"gemini-3.1-flash-lite-preview": {
		coding: 55,
		debugging: 48,
		research: 62,
		reasoning: 52,
		speed: 96,
		longContext: 85,
		instruction: 68,
		agentic: 60,
	},
	"gemini-2.5-flash": {
		coding: 60,
		debugging: 52,
		research: 68,
		reasoning: 58,
		speed: 92,
		longContext: 85,
		instruction: 70,
		agentic: 60,
	},
	"gemini-2.5-flash-lite": {
		coding: 52,
		debugging: 45,
		research: 58,
		reasoning: 48,
		speed: 97,
		longContext: 78,
		instruction: 65,
		agentic: 50,
	},
	"gemini-2.0-flash": {
		coding: 50,
		debugging: 40,
		research: 50,
		reasoning: 40,
		speed: 95,
		longContext: 60,
		instruction: 65,
		agentic: 55,
	},
	"gemini-flash-2.0": {
		coding: 50,
		debugging: 40,
		research: 50,
		reasoning: 40,
		speed: 95,
		longContext: 60,
		instruction: 65,
		agentic: 55,
	},
	// ── DeepSeek ───────────────────────────────────────────────────────────────
	"deepseek-chat": {
		coding: 75,
		debugging: 65,
		research: 55,
		reasoning: 70,
		speed: 70,
		longContext: 55,
		instruction: 65,
		agentic: 55,
	},
	// ── Mistral AI ─────────────────────────────────────────────────────────────
	"mistral-large-latest": {
		coding: 85,
		debugging: 80,
		research: 75,
		reasoning: 80,
		speed: 50,
		longContext: 75,
		instruction: 85,
		agentic: 60,
	},
	"mistral-large-2411": {
		coding: 85,
		debugging: 80,
		research: 75,
		reasoning: 80,
		speed: 50,
		longContext: 75,
		instruction: 85,
		agentic: 55,
	},
	"mistral-large-2512": {
		coding: 88,
		debugging: 82,
		research: 78,
		reasoning: 82,
		speed: 52,
		longContext: 78,
		instruction: 88,
		agentic: 65,
	},
	"pixtral-large-latest": {
		coding: 85,
		debugging: 80,
		research: 85,
		reasoning: 80,
		speed: 45,
		longContext: 80,
		instruction: 85,
		agentic: 50,
	},
	"mistral-medium-latest": {
		coding: 75,
		debugging: 70,
		research: 65,
		reasoning: 70,
		speed: 60,
		longContext: 65,
		instruction: 75,
		agentic: 55,
	},
	"mistral-medium-2505": {
		coding: 75,
		debugging: 70,
		research: 65,
		reasoning: 70,
		speed: 60,
		longContext: 65,
		instruction: 75,
		agentic: 50,
	},
	"mistral-medium-2508": {
		coding: 78,
		debugging: 72,
		research: 68,
		reasoning: 72,
		speed: 62,
		longContext: 68,
		instruction: 78,
		agentic: 55,
	},
	"mistral-small-latest": {
		coding: 65,
		debugging: 60,
		research: 55,
		reasoning: 60,
		speed: 80,
		longContext: 55,
		instruction: 70,
		agentic: 40,
	},
	"mistral-small-2506": {
		coding: 65,
		debugging: 60,
		research: 55,
		reasoning: 60,
		speed: 80,
		longContext: 55,
		instruction: 70,
		agentic: 40,
	},
	"mistral-small-2603": {
		coding: 68,
		debugging: 62,
		research: 58,
		reasoning: 62,
		speed: 82,
		longContext: 58,
		instruction: 72,
		agentic: 40,
	},
	"codestral-latest": {
		coding: 85,
		debugging: 75,
		research: 50,
		reasoning: 70,
		speed: 70,
		longContext: 60,
		instruction: 80,
		// Agentic: code-completion tuning. Refuses agentic tasks with "I'm sorry,
		// I don't have the necessary tools" (M001-6377a4/S04/T02, 2026-05-12).
		// Should not be routed to execute-task without explicit operator pin.
		agentic: 25,
	},
	"ministral-8b-latest": {
		coding: 55,
		debugging: 45,
		research: 40,
		reasoning: 45,
		speed: 90,
		longContext: 45,
		instruction: 70,
		agentic: 30,
	},
	"ministral-3b-latest": {
		coding: 45,
		debugging: 35,
		research: 30,
		reasoning: 35,
		speed: 95,
		longContext: 35,
		instruction: 60,
		agentic: 25,
	},
	"open-mixtral-8x22b": {
		coding: 75,
		debugging: 70,
		research: 70,
		reasoning: 70,
		speed: 40,
		longContext: 70,
		instruction: 75,
		agentic: 40,
	},
	"pixtral-12b": {
		coding: 60,
		debugging: 55,
		research: 65,
		reasoning: 55,
		speed: 75,
		longContext: 60,
		instruction: 65,
		agentic: 35,
	},
	"mistral-nemo": {
		coding: 60,
		debugging: 55,
		research: 55,
		reasoning: 55,
		speed: 85,
		longContext: 60,
		instruction: 65,
		agentic: 35,
	},
	"magistral-medium-latest": {
		coding: 80,
		debugging: 75,
		research: 75,
		reasoning: 75,
		speed: 55,
		longContext: 75,
		instruction: 80,
		agentic: 65,
	},
	"magistral-small": {
		coding: 70,
		debugging: 65,
		research: 65,
		reasoning: 65,
		speed: 75,
		longContext: 65,
		instruction: 70,
		agentic: 50,
	},
	"devstral-2512": {
		coding: 82,
		debugging: 75,
		research: 60,
		reasoning: 70,
		speed: 65,
		longContext: 65,
		instruction: 80,
		// Agentic: Devstral series is coding-completion-tuned; tool-use is not
		// the design target. Penalize so execute-task routing avoids it.
		agentic: 30,
	},
	"devstral-medium-latest": {
		coding: 78,
		debugging: 70,
		research: 55,
		reasoning: 65,
		speed: 75,
		longContext: 60,
		instruction: 75,
		agentic: 30,
	},
	"devstral-medium-2507": {
		coding: 78,
		debugging: 70,
		research: 55,
		reasoning: 65,
		speed: 75,
		longContext: 60,
		instruction: 75,
		agentic: 30,
	},
	"devstral-small-2505": {
		coding: 60,
		debugging: 55,
		research: 45,
		reasoning: 50,
		speed: 90,
		longContext: 45,
		instruction: 65,
		agentic: 30,
	},
	"devstral-small-2507": {
		coding: 60,
		debugging: 55,
		research: 45,
		reasoning: 50,
		speed: 90,
		longContext: 45,
		instruction: 65,
		agentic: 30,
	},
	"labs-devstral-small-2512": {
		coding: 65,
		debugging: 58,
		research: 45,
		reasoning: 55,
		speed: 88,
		longContext: 60,
		instruction: 68,
		agentic: 30,
	},
	// ── Zhipu AI (GLM) ─────────────────────────────────────────────────────────
	"glm-5": {
		coding: 90,
		debugging: 85,
		research: 80,
		reasoning: 90,
		speed: 35,
		longContext: 80,
		instruction: 88,
		agentic: 75,
	},
	"glm-5-turbo": {
		coding: 85,
		debugging: 80,
		research: 75,
		reasoning: 80,
		speed: 65,
		longContext: 75,
		instruction: 85,
		agentic: 70,
	},
	"glm-5.1": {
		coding: 92,
		debugging: 87,
		research: 82,
		reasoning: 91,
		speed: 38,
		longContext: 82,
		instruction: 89,
		agentic: 75,
	},
	"glm-5v-turbo": {
		coding: 82,
		debugging: 78,
		research: 85,
		reasoning: 78,
		speed: 60,
		longContext: 75,
		instruction: 82,
		agentic: 65,
	},
	"glm-4.7": {
		coding: 80,
		debugging: 75,
		research: 70,
		reasoning: 75,
		speed: 60,
		longContext: 70,
		instruction: 80,
		agentic: 55,
	},
	"glm-4.7-flash": {
		coding: 50,
		debugging: 40,
		research: 40,
		reasoning: 40,
		speed: 95,
		longContext: 50,
		instruction: 65,
		agentic: 50,
	},
	"glm-4.7-flashx": {
		coding: 45,
		debugging: 35,
		research: 35,
		reasoning: 35,
		speed: 98,
		longContext: 45,
		instruction: 60,
		agentic: 50,
	},
	// ── Qwen / Ollama Cloud compatible tags ──────────────────────────────────
	"qwen3-coder:480b": {
		coding: 84,
		debugging: 78,
		research: 62,
		reasoning: 76,
		speed: 58,
		longContext: 86,
		instruction: 78,
		// Agentic: qwen3-coder is tuned for code completion, not tool-use loops.
		agentic: 40,
	},
	"qwen3-coder-next": {
		coding: 82,
		debugging: 76,
		research: 60,
		reasoning: 74,
		speed: 70,
		longContext: 86,
		instruction: 76,
		agentic: 40,
	},
	"qwen3-next:80b": {
		coding: 70,
		debugging: 68,
		research: 76,
		reasoning: 80,
		speed: 62,
		longContext: 86,
		instruction: 74,
		agentic: 55,
	},
	// ── Moonshot / Kimi ───────────────────────────────────────────────────────
	"kimi-k2.6": {
		coding: 88,
		debugging: 84,
		research: 72,
		reasoning: 82,
		speed: 55,
		longContext: 86,
		instruction: 84,
		// Agentic: K2.6 is the pinned default for the autonomous-solver role
		// (ADR-0079) — refusal-resistant and follows tool-use contracts.
		agentic: 90,
	},
	"kimi-for-coding": {
		coding: 88,
		debugging: 84,
		research: 72,
		reasoning: 82,
		speed: 55,
		longContext: 86,
		instruction: 84,
		// `kimi-for-coding` is an alias for K2.6 on the Kimi Code provider
		// (memory: bayesian-blender/benchmark-selector both canonicalize it).
		agentic: 90,
	},
	"kimi-k2-thinking": {
		coding: 86,
		debugging: 88,
		research: 78,
		reasoning: 92,
		speed: 30,
		longContext: 86,
		instruction: 84,
		agentic: 88,
	},
	// ── MiniMax ───────────────────────────────────────────────────────────────
	// Profiles ordered by generation. Older M2.1 generation gets distinctly
	// lower agentic + capability scores: the M2.1 stuck-checkpoint loop on
	// 2026-05-13 (infra repo) traced back to M2.1 being aliased to M2.7's
	// profile, winning execute-task on cost, then failing to follow the
	// checkpoint contract reliably across 60+ tool calls. (See
	// self-feedback sf-mp37kjmo-1mfuru.)
	"MiniMax-M2.7": {
		coding: 84,
		debugging: 80,
		research: 78,
		reasoning: 84,
		speed: 52,
		longContext: 84,
		instruction: 82,
		agentic: 78,
	},
	"MiniMax-M2.7-highspeed": {
		coding: 82,
		debugging: 78,
		research: 76,
		reasoning: 80,
		speed: 72,
		longContext: 84,
		instruction: 80,
		agentic: 76,
	},
	"MiniMax-M2.5": {
		// Distinct profile (previously aliased to M2.7 — overclaimed).
		coding: 78,
		debugging: 74,
		research: 72,
		reasoning: 78,
		speed: 55,
		longContext: 82,
		instruction: 76,
		// Mid agentic — better than coding-completion-only models but
		// noticeably less reliable than current-gen agentic models.
		agentic: 60,
	},
	"MiniMax-M2.1": {
		// Distinct profile (previously aliased to M2.7 — overclaimed).
		// M2.1 has demonstrated unreliable tool-use loops in production
		// (M001-6377a4 / 1-ci-build-pipeline parallel-research, 2026-05-13:
		// 60+ checkpoint calls with shifting unitId claims). Penalize the
		// agentic axis so execute-task routing avoids it absent operator
		// override.
		coding: 72,
		debugging: 66,
		research: 64,
		reasoning: 70,
		speed: 60,
		longContext: 78,
		instruction: 72,
		agentic: 40,
	},
	"MiniMax-M2": {
		// Earliest of the M2.x line — older still.
		coding: 68,
		debugging: 60,
		research: 60,
		reasoning: 66,
		speed: 62,
		longContext: 76,
		instruction: 68,
		agentic: 35,
	},
};
const MODEL_CAPABILITY_ALIASES = {
	"deepseek-v3.1": "deepseek-chat",
	"deepseek-v3.2": "deepseek-chat",
	"deepseek-v4-flash": "deepseek-chat",
	"deepseek-v4-pro": "deepseek-chat",
	"devstral-latest": "devstral-medium-latest",
	"devstral-2:123b": "devstral-2512",
	"mistral.devstral-2-123b": "devstral-2512",
	"devstral-small-2:24b": "devstral-small-2507",
	"mistral.devstral-small-2-24b": "labs-devstral-small-2512",
	"mistral.mistral-large-3-675b-instruct": "mistral-large-latest",
	"mistral.ministral-3-14b-instruct": "mistral-small-latest",
	"mistral.ministral-3-3b-instruct": "ministral-3b-latest",
	"mistral.ministral-3-8b-instruct": "ministral-8b-latest",
	"gemini-3-flash-preview": "gemini-3-flash-preview",
	"glm-4.6": "glm-4.7",
	"gpt-oss:120b": "gpt-4o",
	"gpt-oss:20b": "gpt-4o-mini",
	"kimi-k2:1t": "kimi-k2.6",
	// NOTE: "kimi-k2.5" → "kimi-k2.6" alias REMOVED. K2.5 is a distinct
	// generation from K2.6; aliasing them caused K2.5 to silently inherit
	// K2.6's tier (latent downgrade-conflation bug). K2.5 now resolves via
	// tierFor("kimi-k2.5") in the registry directly.
	"kimi-for-coding": "kimi-k2.6",
	"kimi-k2.6:cloud": "kimi-k2.6",
	"kimi-k2.6-cloud": "kimi-k2.6",
	// Each MiniMax generation now has its own profile — previously they all
	// aliased to MiniMax-M2.7, which let older/weaker models inherit current
	// capability scores and win cost tie-breaks on execute-task. The aliases
	// below normalize provider-prefixed and casing variants to the canonical
	// per-generation profile, NOT to the current generation.
	"minimax-m2": "MiniMax-M2",
	"minimax/MiniMax-M2": "MiniMax-M2",
	"minimax/minimax-m2": "MiniMax-M2",
	"minimax-m2.1": "MiniMax-M2.1",
	"minimax/MiniMax-M2.1": "MiniMax-M2.1",
	"minimax/minimax-m2.1": "MiniMax-M2.1",
	"minimax-m2.5": "MiniMax-M2.5",
	"minimax/MiniMax-M2.5": "MiniMax-M2.5",
	"minimax/minimax-m2.5": "MiniMax-M2.5",
	"minimax-m2.7": "MiniMax-M2.7",
	"minimax/MiniMax-M2.7": "MiniMax-M2.7",
	"minimax/minimax-m2.7": "MiniMax-M2.7",
	"mistral-large-3:675b": "mistral-large-latest",
	"ministral-3:3b": "ministral-3b-latest",
	"ministral-3:8b": "ministral-8b-latest",
	"ministral-3:14b": "mistral-small-latest",
	"nemotron-3-nano:30b": "gpt-4o-mini",
	"nemotron-3-super": "gpt-4o",
	"qwen3-coder-480b-a35b-v1:0": "qwen3-coder:480b",
	"qwen3-coder-480b-a35b": "qwen3-coder:480b",
	"qwen3-coder": "qwen3-coder:480b",
	"qwen3-coder:free": "qwen3-coder:480b",
	"qwen3-coder-30b-a3b-instruct": "qwen3-coder-next",
	"qwen3-coder-flash": "qwen3-coder-next",
	"qwen3-next-80b-a3b": "qwen3-next:80b",
	"qwen3-next-80b-a3b-instruct": "qwen3-next:80b",
	"qwen3-next-80b-a3b-instruct:free": "qwen3-next:80b",
	"qwen3-next-80b-a3b-thinking": "qwen3-next:80b",
};
// ─── Base Task Requirements Data Table ───────────────────────────────────────
// Per-unit-type base requirement vectors. Weights indicate how important each
// capability dimension is for this unit type.
//
// The `agentic` dimension represents the model's reliability at multi-turn
// tool-use loops (does it follow the tool-use contract? does it refuse the
// task? does it call the checkpoint tool when asked?). It is weighted high
// for any unit type that actually uses tools at runtime — execute-task most
// of all. See ADR-0079 for the motivation: a Codestral-style refusal on
// execute-task in M001-6377a4/S04/T02 (2026-05-12) traced back to the router
// having no agentic axis, so a coding-completion model out-scored agentic
// alternatives on coding/instruction.
export const BASE_REQUIREMENTS = {
	"execute-task": {
		coding: 0.9,
		instruction: 0.7,
		speed: 0.3,
		agentic: 0.85,
	},
	"research-milestone": { research: 0.9, longContext: 0.7, reasoning: 0.5 },
	"research-slice": { research: 0.9, longContext: 0.7, reasoning: 0.5 },
	"plan-milestone": { reasoning: 0.9, coding: 0.5, agentic: 0.6 },
	"plan-slice": { reasoning: 0.9, coding: 0.5, agentic: 0.6 },
	"replan-slice": { reasoning: 0.9, debugging: 0.6, coding: 0.5, agentic: 0.6 },
	"reassess-roadmap": { reasoning: 0.9, research: 0.5, agentic: 0.4 },
	"complete-slice": { instruction: 0.8, speed: 0.7, agentic: 0.6 },
	"run-uat": { instruction: 0.7, speed: 0.8, agentic: 0.6 },
	"discuss-milestone": { reasoning: 0.6, instruction: 0.7, agentic: 0.4 },
	"complete-milestone": { instruction: 0.8, reasoning: 0.5, agentic: 0.5 },
	// Self-feedback triage is an agentic task: read open entries, reason
	// about decisions (fix/promote/close), optionally call resolve_issue.
	// Higher agentic weight than execute-task because triage decisions
	// involve significant judgment + structured tool calls; lower coding
	// weight because the model isn't writing implementation code in this
	// dispatch.
	"self-feedback-triage": {
		coding: 0.4,
		instruction: 0.8,
		reasoning: 0.8,
		agentic: 0.9,
	},
};
// ─── Public API ──────────────────────────────────────────────────────────────
/**
 * Score a model's suitability for a task given a requirement vector.
 * Returns a weighted average of capability dimensions (0–100).
 * Returns 50 if requirements are empty (neutral score).
 */
export function scoreModel(model, requirements) {
	let weightedSum = 0;
	let weightSum = 0;
	for (const [dim, weight] of Object.entries(requirements)) {
		const capability = model[dim] ?? 50;
		weightedSum += weight * capability;
		weightSum += weight;
	}
	return weightSum > 0 ? weightedSum / weightSum : 50;
}
/**
 * Compute dynamic task requirements from unit type and optional task metadata.
 * Returns a requirement vector refined by task-specific signals.
 */
export function computeTaskRequirements(unitType, metadata) {
	const base = BASE_REQUIREMENTS[unitType] ?? { reasoning: 0.5 };
	if (unitType === "execute-task" && metadata) {
		if (
			metadata.tags?.some((t) =>
				/^(docs?|readme|comment|config|typo|rename)$/i.test(t),
			)
		) {
			return { ...base, instruction: 0.9, coding: 0.3, speed: 0.7 };
		}
		if (
			metadata.complexityKeywords?.some(
				(k) => k === "concurrency" || k === "compatibility",
			)
		) {
			return { ...base, debugging: 0.9, reasoning: 0.8 };
		}
		if (
			metadata.complexityKeywords?.some(
				(k) => k === "migration" || k === "architecture",
			)
		) {
			return { ...base, reasoning: 0.9, coding: 0.8 };
		}
		if (
			(metadata.fileCount ?? 0) >= 6 ||
			(metadata.estimatedLines ?? 0) >= 500
		) {
			return { ...base, coding: 0.9, reasoning: 0.7 };
		}
	}
	return base;
}
/**
 * Score all eligible models against a requirement vector and return them
 * sorted by score descending. Within 2 points: prefer cheaper; equal cost:
 * lexicographic tie-break by model ID.
 */
export function scoreEligibleModels(
	eligibleModelIds,
	requirements,
	capabilityOverrides,
) {
	const scored = eligibleModelIds.map((modelId) => {
		const canonicalModelId = canonicalCapabilityModelId(modelId);
		const builtin = MODEL_CAPABILITY_PROFILES[canonicalModelId];
		const override =
			capabilityOverrides?.[modelId] ?? capabilityOverrides?.[canonicalModelId];
		const profile = builtin
			? override
				? { ...builtin, ...override }
				: builtin
			: {
					coding: 50,
					debugging: 50,
					research: 50,
					reasoning: 50,
					speed: 50,
					longContext: 50,
					instruction: 50,
				};
		return { modelId, score: scoreModel(profile, requirements) };
	});
	scored.sort((a, b) => {
		const scoreDiff = b.score - a.score;
		if (Math.abs(scoreDiff) > 2) return scoreDiff;
		const costA = lookupModelCost(a.modelId)?.inputPer1k ?? Infinity;
		const costB = lookupModelCost(b.modelId)?.inputPer1k ?? Infinity;
		if (costA !== costB) return costA - costB;
		return a.modelId.localeCompare(b.modelId);
	});
	return scored;
}
function canonicalCapabilityModelId(modelId) {
	const bareId = modelId.includes("/") ? modelId.split("/").pop() : modelId;
	const normalizedId = bareId.replace(/:cloud$/i, "").replace(/-cloud$/i, "");
	const aliased =
		resolveCapabilityAlias(bareId) ?? resolveCapabilityAlias(normalizedId);
	if (aliased) return aliased;
	if (MODEL_CAPABILITY_PROFILES[normalizedId]) return normalizedId;
	for (const knownId of Object.keys(MODEL_CAPABILITY_PROFILES)) {
		if (normalizedId.includes(knownId) || knownId.includes(normalizedId)) {
			return knownId;
		}
	}
	return normalizedId;
}
function resolveCapabilityAlias(modelId) {
	const direct = MODEL_CAPABILITY_ALIASES[modelId];
	if (direct) return direct;
	const lower = modelId.toLowerCase();
	return Object.entries(MODEL_CAPABILITY_ALIASES).find(
		([alias]) => alias.toLowerCase() === lower,
	)?.[1];
}
/**
 * Return all models eligible for a given tier, sorted cheapest first.
 * If routingConfig.tier_models[tier] is set and available, returns only that
 * model. Otherwise filters availableModelIds by tier from MODEL_CAPABILITY_TIER.
 */
export function getEligibleModels(tier, availableModelIds, routingConfig) {
	// 1. Check explicit tier_models config
	const explicitModel = routingConfig.tier_models?.[tier];
	if (explicitModel) {
		// Exact match
		if (availableModelIds.includes(explicitModel)) return [explicitModel];
		// Provider-prefix-stripped match
		const match = availableModelIds.find((id) => {
			const bareAvail = id.includes("/") ? id.split("/").pop() : id;
			const bareExplicit = explicitModel.includes("/")
				? explicitModel.split("/").pop()
				: explicitModel;
			return bareAvail === bareExplicit;
		});
		if (match) return [match];
	}
	// 2. Auto-detect: filter by tier, sort cheapest first
	return availableModelIds
		.filter((id) => getModelTier(id) === tier)
		.sort((a, b) => {
			const costA =
				lookupModelCost(canonicalCapabilityModelId(a))?.inputPer1k ?? 999;
			const costB =
				lookupModelCost(canonicalCapabilityModelId(b))?.inputPer1k ?? 999;
			return costA - costB;
		});
}
/**
 * Build a fallback chain for a selected model: [selectedModel, ...configuredFallbacks, configuredPrimary]
 * Deduplicates entries while preserving order.
 */
function buildFallbackChain(selectedModelId, phaseConfig) {
	return [
		...phaseConfig.fallbacks.filter((f) => f !== selectedModelId),
		phaseConfig.primary,
	].filter((f) => f !== selectedModelId);
}
/**
 * Load capability overrides from user preferences' modelOverrides section.
 * Returns a map of model ID → partial capability overrides to deep-merge with built-in profiles.
 *
 * Per D-17: partial capability overrides via models.json modelOverrides, deep-merged with defaults.
 */
export function loadCapabilityOverrides(prefs) {
	const result = {};
	if (!prefs.modelOverrides) return result;
	for (const [modelId, overrideEntry] of Object.entries(prefs.modelOverrides)) {
		if (overrideEntry.capabilities) {
			result[modelId] = overrideEntry.capabilities;
		}
	}
	return result;
}
/**
 * Resolve the model to use for a given complexity tier.
 *
 * Downgrade-only: the returned model is always equal to or cheaper than
 * the user's configured primary model. Never upgrades beyond configuration.
 *
 * STEP 1: Filter to eligible models for the requested tier.
 * STEP 2: Capability scoring — ranks eligible models by task-capability match
 *         when capability_routing is enabled and multiple eligible models exist.
 * STEP 3: Fallback chain assembly.
 *
 * @param classification      The complexity classification result
 * @param phaseConfig         The user's configured model for this phase (ceiling)
 * @param routingConfig       Dynamic routing configuration
 * @param availableModelIds   List of available model IDs (from registry)
 * @param unitType            The unit type for capability requirement computation (optional)
 * @param taskMetadata        Task metadata for refined requirement vectors (optional)
 * @param capabilityOverrides User-provided capability overrides (deep-merged with built-in profiles, optional)
 */
export function resolveModelForComplexity(
	classification,
	phaseConfig,
	routingConfig,
	availableModelIds,
	unitType,
	taskMetadata,
	capabilityOverrides,
	stickyHint,
) {
	// If no phase config or routing disabled, pass through
	if (!phaseConfig || !routingConfig.enabled) {
		return {
			modelId: phaseConfig?.primary ?? "",
			fallbacks: phaseConfig?.fallbacks ?? [],
			tier: classification.tier,
			wasDowngraded: false,
			reason: "dynamic routing disabled or no phase config",
			selectionMethod: "tier-only",
		};
	}
	const configuredPrimary = phaseConfig.primary;
	const configuredTier = getModelTier(configuredPrimary);
	const requestedTier = classification.tier;
	// If the configured model is unknown (not in MODEL_CAPABILITY_TIER),
	// honor the user's explicit choice — don't downgrade based on a guess.
	// Unknown models default to "heavy" in getModelTier, which makes every
	// standard/light unit get downgraded to tier_models, silently ignoring
	// the user's configuration. (#2192)
	if (!isKnownModel(configuredPrimary)) {
		return {
			modelId: configuredPrimary,
			fallbacks: phaseConfig.fallbacks,
			tier: requestedTier,
			wasDowngraded: false,
			reason: `configured model "${configuredPrimary}" is not in the known tier map — honoring explicit config`,
			selectionMethod: "tier-only",
		};
	}
	// Downgrade-only: if requested tier >= configured tier, no change
	if (tierOrdinal(requestedTier) >= tierOrdinal(configuredTier)) {
		return {
			modelId: configuredPrimary,
			fallbacks: phaseConfig.fallbacks,
			tier: requestedTier,
			wasDowngraded: false,
			reason: `tier ${requestedTier} >= configured ${configuredTier}`,
			selectionMethod: "tier-only",
		};
	}
	// STEP 1: Get all eligible models for the requested tier
	const eligible = getEligibleModels(
		requestedTier,
		availableModelIds,
		routingConfig,
	);
	if (eligible.length === 0) {
		// No suitable model found — use configured primary
		return {
			modelId: configuredPrimary,
			fallbacks: phaseConfig.fallbacks,
			tier: requestedTier,
			wasDowngraded: false,
			reason: `no ${requestedTier}-tier model available`,
			selectionMethod: "tier-only",
		};
	}
	// STEP 2: Capability scoring (when enabled and multiple eligible models exist)
	if (
		routingConfig.capability_routing !== false &&
		eligible.length > 1 &&
		unitType
	) {
		const requirements = computeTaskRequirements(unitType, taskMetadata);
		const scored = scoreEligibleModels(
			eligible,
			requirements,
			capabilityOverrides,
		);
		const winner = scored[0];
		if (winner) {
			const capScores = {};
			for (const s of scored) capScores[s.modelId] = s.score;
			// Slice-sticky preference: if a model previously succeeded on a
			// sibling unit in this slice AND it is still eligible in the
			// current tier AND its capability score is within STICKY_WINDOW of
			// the winner, prefer it. Stops within-slice routing thrash where
			// T01 → gemini-flash and T02 → codestral on the same slice.
			const STICKY_WINDOW_POINTS = 8;
			const stickyId = (() => {
				if (!stickyHint?.id) return null;
				const stickyKey = stickyHint.provider
					? `${stickyHint.provider}/${stickyHint.id}`
					: stickyHint.id;
				// Match either "provider/model" or bare model id in the eligible list.
				const found = scored.find(
					(s) =>
						s.modelId === stickyKey || s.modelId.endsWith(`/${stickyHint.id}`),
				);
				if (!found) return null;
				if (winner.score - found.score > STICKY_WINDOW_POINTS) return null;
				return found.modelId;
			})();
			const selectedId = stickyId ?? winner.modelId;
			const selectedScore = (
				scored.find((s) => s.modelId === selectedId) ?? winner
			).score;
			const fallbacks = buildFallbackChain(selectedId, phaseConfig);
			return {
				modelId: selectedId,
				fallbacks,
				tier: requestedTier,
				wasDowngraded: true,
				reason: stickyId
					? `slice-sticky: ${selectedId} (${selectedScore.toFixed(1)}, within ${STICKY_WINDOW_POINTS}pt of capability winner) for ${unitType}`
					: `capability-scored: ${selectedId} (${selectedScore.toFixed(1)}) for ${unitType}`,
				capabilityScores: capScores,
				taskRequirements: requirements,
				selectionMethod: stickyId ? "slice-sticky" : "capability-scored",
			};
		}
	}
	// STEP 3: Fallback — use first eligible model (cheapest in tier, or single eligible)
	const targetModelId = eligible[0];
	// Build fallback chain: [downgraded_model, ...configured_fallbacks, configured_primary]
	const fallbacks = buildFallbackChain(targetModelId, phaseConfig);
	return {
		modelId: targetModelId,
		fallbacks,
		tier: requestedTier,
		wasDowngraded: true,
		reason: classification.reason,
		selectionMethod: "tier-only",
	};
}
/**
 * Escalate to the next tier after a failure.
 * Returns the new tier, or null if already at heavy (max).
 */
export function escalateTier(currentTier) {
	switch (currentTier) {
		case "light":
			return "standard";
		case "standard":
			return "heavy";
		case "heavy":
			return null;
	}
}
/**
 * Get the default routing config (all features enabled).
 */
export function defaultRoutingConfig() {
	return {
		enabled: true,
		capability_routing: true,
		escalate_on_failure: true,
		budget_pressure: true,
		cross_provider: true,
		hooks: true,
	};
}
// ─── Internal ────────────────────────────────────────────────────────────────
export function getModelTier(modelId) {
	// Prefer registry lookup using canonical id — this eliminates the K2.5→K2.6
	// downgrade alias that was in MODEL_CAPABILITY_ALIASES.
	const canonicalId = canonicalCapabilityModelId(modelId);
	const registryTier = tierFor(canonicalId);
	if (registryTier) return registryTier;
	// Fall back to the local table for models not yet in the registry.
	if (MODEL_CAPABILITY_TIER[canonicalId])
		return MODEL_CAPABILITY_TIER[canonicalId];
	const sizeTier = inferTierFromModelSize(canonicalId);
	if (sizeTier) return sizeTier;
	// Check if any known model ID is a prefix/suffix match
	for (const [knownId, tier] of Object.entries(MODEL_CAPABILITY_TIER)) {
		if (canonicalId.includes(knownId) || knownId.includes(canonicalId))
			return tier;
	}
	// Unknown models are assumed standard (per D-15: avoids silently ignoring user config)
	return "standard";
}
function inferTierFromModelSize(modelId) {
	const normalized = modelId.toLowerCase();
	const match = normalized.match(/(?:^|[-_.])(\d+(?:\.\d+)?)b(?:$|[-_.])/);
	if (!match) return null;
	const billions = Number(match[1]);
	if (!Number.isFinite(billions)) return null;
	if (billions <= 8) return "light";
	return null;
}
/** Check if a model ID has a known capability tier mapping. (#2192) */
function isKnownModel(modelId) {
	const canonicalId = canonicalCapabilityModelId(modelId);
	if (MODEL_CAPABILITY_TIER[canonicalId]) return true;
	for (const knownId of Object.keys(MODEL_CAPABILITY_TIER)) {
		if (canonicalId.includes(knownId) || knownId.includes(canonicalId))
			return true;
	}
	return false;
}
// ─── Tool Compatibility Filter (ADR-005 Phase 3) ───────────────────────────
/**
 * Check if a tool is compatible with a provider's capabilities.
 * Returns true if the tool can be used with the provider.
 */
export function isToolCompatibleWithProvider(toolName, providerCaps) {
	const compat = getToolCompatibility(toolName);
	if (!compat) return true; // no metadata = always compatible
	// Hard filter: provider doesn't support image tool results
	if (compat.producesImages && !providerCaps.imageToolResults) return false;
	// Hard filter: tool uses schema features provider doesn't support
	if (
		compat.schemaFeatures?.some((f) =>
			providerCaps.unsupportedSchemaFeatures.includes(f),
		)
	) {
		return false;
	}
	return true;
}
/**
 * Filter a list of tool names to only those compatible with a provider.
 * Used by the routing pipeline to adjust tool sets when switching providers.
 */
export function filterToolsForProvider(toolNames, providerApi) {
	const providerCaps = getProviderCapabilities(providerApi);
	// Provider doesn't support tool calling at all
	if (!providerCaps.toolCalling) {
		return { compatible: [], filtered: toolNames };
	}
	const compatible = [];
	const filtered = [];
	for (const name of toolNames) {
		if (isToolCompatibleWithProvider(name, providerCaps)) {
			compatible.push(name);
		} else {
			filtered.push(name);
		}
	}
	return { compatible, filtered };
}
/**
 * Adjust the active tool set for a selected model's provider capabilities.
 * Returns tool names that should be active — removes incompatible tools.
 *
 * This is a hard filter only — it removes tools that would fail at the
 * provider level. It does NOT remove tools based on soft heuristics.
 */
export function adjustToolSet(activeToolNames, selectedModelApi) {
	const { compatible, filtered } = filterToolsForProvider(
		activeToolNames,
		selectedModelApi,
	);
	return { toolNames: compatible, removedTools: filtered };
}
