Files
codetyper.cli/src/services/model-routing.ts
Carlos Gutierrez c839fc4d68 Add BRAIN_DISABLED flag and fix Ollama tool call formatting
Features:
  - Add BRAIN_DISABLED feature flag to hide all Brain functionality
  - When enabled, hides Brain banner, status indicator, menu, and commands
  - Flag location: src/constants/brain.ts

  Fixes:
  - Fix Ollama 400 error by properly formatting tool_calls in messages
  - Update OllamaMessage type to include tool_calls field
  - Fix Brain menu keyboard not working (add missing modes to isMenuOpen)

  UI Changes:
  - Remove "^Tab toggle mode" hint from status bar
  - Remove "ctrl+t to hide todos" hint from status bar

  Files modified:
  - src/constants/brain.ts (add BRAIN_DISABLED flag)
  - src/types/ollama.ts (add tool_calls to OllamaMessage)
  - src/providers/ollama/chat.ts (format tool_calls in messages)
  - src/tui-solid/components/header.tsx (hide Brain UI when disabled)
  - src/tui-solid/components/status-bar.tsx (remove hints)
  - src/tui-solid/components/command-menu.tsx (filter brain command)
  - src/tui-solid/components/input-area.tsx (fix isMenuOpen modes)
  - src/tui-solid/routes/session.tsx (skip brain menu when disabled)
  - src/services/brain.ts (early return when disabled)
  - src/services/chat-tui/initialize.ts (skip brain init when disabled)
2026-02-02 13:25:38 -05:00

226 lines
5.4 KiB
TypeScript

/**
* Model Routing Service
*
* Maps agent tiers to appropriate models based on task complexity.
* Following Claude Code's multi-model strategy:
* - fast: Quick screening, filtering (like Haiku)
* - balanced: Detailed analysis, general tasks (like Sonnet)
* - thorough: Complex reasoning, bug hunting (like Opus)
*/
import { getModelContextSize } from "@constants/copilot";
import type { AgentConfig } from "@/types/agent-config";
/**
* Model tier for routing decisions
*/
export type ModelTier = "fast" | "balanced" | "thorough";
/**
* Model tier mapping to Copilot models
* These are the default mappings - can be overridden by agent config
*/
export const MODEL_TIER_MAPPING: Record<ModelTier, string[]> = {
// Fast tier: Low cost, quick responses (0x or 0.33x multiplier)
fast: [
"gpt-5-mini",
"gpt-4o-mini",
"claude-haiku-4.5",
"gemini-3-flash-preview",
"grok-code-fast-1",
],
// Balanced tier: Good quality, moderate cost (1x multiplier)
balanced: [
"claude-sonnet-4.5",
"claude-sonnet-4",
"gpt-5",
"gpt-5.1",
"gemini-2.5-pro",
"gpt-4.1",
],
// Thorough tier: Best quality, higher cost (3x multiplier)
thorough: [
"claude-opus-4.5",
"gpt-5.2-codex",
"gpt-5.1-codex-max",
],
};
/**
* Tier aliases for agent frontmatter
*/
const TIER_ALIASES: Record<string, ModelTier> = {
haiku: "fast",
fast: "fast",
quick: "fast",
sonnet: "balanced",
balanced: "balanced",
default: "balanced",
opus: "thorough",
thorough: "thorough",
deep: "thorough",
};
/**
* Agent type to default tier mapping
*/
const AGENT_TYPE_TIERS: Record<string, ModelTier> = {
explorer: "fast",
explore: "fast",
filter: "fast",
screen: "fast",
architect: "balanced",
planner: "balanced",
plan: "balanced",
coder: "balanced",
general: "balanced",
reviewer: "balanced",
review: "balanced",
"code-reviewer": "balanced",
"bug-hunter": "thorough",
bugs: "thorough",
security: "thorough",
compaction: "fast",
summary: "fast",
title: "fast",
};
/**
* Resolve model tier from string (tier name or model ID)
*/
export const resolveTier = (modelOrTier: string): ModelTier | null => {
const lower = modelOrTier.toLowerCase();
// Check if it's a tier alias
if (lower in TIER_ALIASES) {
return TIER_ALIASES[lower];
}
// Check if it's already a model ID in one of the tiers
for (const [tier, models] of Object.entries(MODEL_TIER_MAPPING)) {
if (models.some((m) => m.toLowerCase() === lower)) {
return tier as ModelTier;
}
}
return null;
};
/**
* Get the best available model for a tier
* Returns the first model in the tier's list (assumed to be preference order)
*/
export const getModelForTier = (
tier: ModelTier,
availableModels?: string[],
): string => {
const tierModels = MODEL_TIER_MAPPING[tier];
if (availableModels && availableModels.length > 0) {
// Find first available model from tier
for (const model of tierModels) {
if (availableModels.includes(model)) {
return model;
}
}
// Fallback to first tier model if none available
return tierModels[0];
}
return tierModels[0];
};
/**
* Infer tier from agent type/name
*/
export const inferTierFromAgent = (agent: AgentConfig): ModelTier => {
const idLower = agent.id.toLowerCase();
const nameLower = agent.name.toLowerCase();
// Check agent type mapping
for (const [type, tier] of Object.entries(AGENT_TYPE_TIERS)) {
if (idLower.includes(type) || nameLower.includes(type)) {
return tier;
}
}
// Default to balanced
return "balanced";
};
/**
* Resolve the model to use for an agent
*
* Priority:
* 1. Explicit model in agent config (full model ID)
* 2. Tier specified in agent config (fast/balanced/thorough)
* 3. Inferred from agent type/name
* 4. Default model passed in
*/
export const resolveAgentModel = (
agent: AgentConfig,
defaultModel: string,
availableModels?: string[],
): { model: string; tier: ModelTier; source: string } => {
// 1. Check explicit model in agent config
if (agent.model) {
// Check if it's a tier name
const tier = resolveTier(agent.model);
if (tier) {
const model = getModelForTier(tier, availableModels);
return { model, tier, source: "agent-tier" };
}
// Otherwise use as model ID
return {
model: agent.model,
tier: resolveTier(agent.model) ?? "balanced",
source: "agent-model",
};
}
// 2. Infer from agent type
const inferredTier = inferTierFromAgent(agent);
if (inferredTier !== "balanced") {
const model = getModelForTier(inferredTier, availableModels);
return { model, tier: inferredTier, source: "agent-inferred" };
}
// 3. Use default
const defaultTier = resolveTier(defaultModel) ?? "balanced";
return { model: defaultModel, tier: defaultTier, source: "default" };
};
/**
* Get model context size for routing decisions
*/
export const getRouteContextSize = (modelId: string): number => {
return getModelContextSize(modelId).input;
};
/**
* Model routing decision
*/
export interface ModelRoutingDecision {
model: string;
tier: ModelTier;
source: string;
contextSize: number;
}
/**
* Make routing decision for an agent
*/
export const routeAgent = (
agent: AgentConfig,
defaultModel: string,
availableModels?: string[],
): ModelRoutingDecision => {
const resolution = resolveAgentModel(agent, defaultModel, availableModels);
return {
...resolution,
contextSize: getRouteContextSize(resolution.model),
};
};