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)
226 lines
5.4 KiB
TypeScript
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),
|
|
};
|
|
};
|