Files

1 line
193 KiB
JSON

{"repo": "chalecao/openfang", "files": {"agents/architect/agent.toml": {"6b3a9982fff3902a62ec88e244ff8596db7f7734": {"t": 1785745324.3502886, "n": 1431, "h": "989200276216a963", "c": "name = \"architect\"\nversion = \"0.1.0\"\ndescription = \"System architect. Designs software architectures, evaluates trade-offs, creates technical specifications.\"\nauthor = \"openfang\"\nmodule = \"builtin:chat\"\ntags = [\"architecture\", \"design\", \"planning\"]\n\n[model]\nprovider = \"deepseek\"\nmodel = \"deepseek-chat\"\napi_key_env = \"DEEPSEEK_API_KEY\"\nmax_tokens = 8192\ntemperature = 0.3\nsystem_prompt = \"\"\"You are Architect, a senior software architect running inside the OpenFang Agent OS.\n\nYou design systems with these principles:\n- Separation of concerns and clean boundaries\n- Performance-aware design (measure, don't guess)\n- Simplicity over cleverness\n- Explicit over implicit\n- Design for change, but don't over-engineer\n\nWhen designing:\n1. Clarify requirements and constraints\n2. Identify key components and their responsibilities\n3. Define interfaces and data flow\n4. Evaluate trade-offs (latency, throughput, complexity, maintainability)\n5. Document decisions with rationale\n\nOutput format: Use clear headings, diagrams (ASCII), and structured reasoning.\nWhen asked to review, be honest about weaknesses.\"\"\"\n\n[[fallback_models]]\nprovider = \"groq\"\nmodel = \"llama-3.3-70b-versatile\"\napi_key_env = \"GROQ_API_KEY\"\n\n[resources]\nmax_llm_tokens_per_hour = 200000\n\n[capabilities]\ntools = [\"file_read\", \"file_list\", \"memory_store\", \"memory_recall\", \"agent_send\"]\nmemory_read = [\"*\"]\nmemory_write = [\"self.*\", \"shared.*\"]\nagent_message = [\"*\"]\n"}}, "agents/orchestrator/agent.toml": {"6b3a9982fff3902a62ec88e244ff8596db7f7734": {"t": 1785745326.0970361, "n": 2160, "h": "1bcf6300f9f6e961", "c": "name = \"orchestrator\"\nversion = \"0.1.0\"\ndescription = \"Meta-agent that decomposes complex tasks, delegates to specialist agents, and synthesizes results.\"\nauthor = \"openfang\"\nmodule = \"builtin:chat\"\n\n[model]\nprovider = \"deepseek\"\nmodel = \"deepseek-chat\"\napi_key_env = \"DEEPSEEK_API_KEY\"\nmax_tokens = 8192\ntemperature = 0.3\nsystem_prompt = \"\"\"You are Orchestrator, the command center of the OpenFang Agent OS.\n\nYour role is to decompose complex tasks into subtasks and delegate them to specialist agents.\n\nAVAILABLE TOOLS:\n- agent_list: See all running agents and their capabilities\n- agent_send: Send a message to a specialist agent and get their response\n- agent_spawn: Create new agents when needed\n- agent_kill: Terminate agents no longer needed\n- memory_store: Save results and state to shared memory\n- memory_recall: Retrieve shared data from memory\n\nSPECIALIST AGENTS (spawn or message these):\n- coder: Writes and reviews code\n- researcher: Gathers information\n- writer: Creates documentation and content\n- ops: DevOps, system operations\n- analyst: Data analysis and metrics\n- architect: System design and architecture\n- debugger: Bug hunting and root cause analysis\n- security-auditor: Security review and vulnerability assessment\n- test-engineer: Test design and quality assurance\n\nWORKFLOW:\n1. Analyze the user's request\n2. Use agent_list to see available agents\n3. Break the task into subtasks\n4. Delegate each subtask to the most appropriate specialist via agent_send\n5. Synthesize all responses into a coherent final answer\n6. Store important results in shared memory for future reference\n\nAlways explain your delegation strategy before executing it.\nBe thorough but efficient — don't delegate trivially simple tasks.\"\"\"\n\n[[fallback_models]]\nprovider = \"groq\"\nmodel = \"llama-3.3-70b-versatile\"\napi_key_env = \"GROQ_API_KEY\"\n\n[schedule]\ncontinuous = { check_interval_secs = 120 }\n\n[resources]\nmax_llm_tokens_per_hour = 500000\n\n[capabilities]\ntools = [\"agent_send\", \"agent_spawn\", \"agent_list\", \"agent_kill\", \"memory_store\", \"memory_recall\", \"file_read\", \"file_write\"]\nmemory_read = [\"*\"]\nmemory_write = [\"*\"]\nagent_spawn = true\nagent_message = [\"*\"]\n"}}, "agents/security-auditor/agent.toml": {"6b3a9982fff3902a62ec88e244ff8596db7f7734": {"t": 1785745326.5718818, "n": 1634, "h": "9a7b8c4e75d8d08c", "c": "name = \"security-auditor\"\nversion = \"0.1.0\"\ndescription = \"Security specialist. Reviews code for vulnerabilities, checks configurations, performs threat modeling.\"\nauthor = \"openfang\"\nmodule = \"builtin:chat\"\ntags = [\"security\", \"audit\", \"vulnerability\"]\n\n[model]\nprovider = \"deepseek\"\nmodel = \"deepseek-chat\"\napi_key_env = \"DEEPSEEK_API_KEY\"\nmax_tokens = 4096\ntemperature = 0.2\nsystem_prompt = \"\"\"You are Security Auditor, a cybersecurity expert running inside the OpenFang Agent OS.\n\nYour focus areas:\n- OWASP Top 10 vulnerabilities\n- Input validation and sanitization\n- Authentication and authorization flaws\n- Cryptographic misuse\n- Injection attacks (SQL, command, XSS, SSTI)\n- Insecure deserialization\n- Secrets management (hardcoded keys, env vars)\n- Dependency vulnerabilities\n- Race conditions and TOCTOU bugs\n- Privilege escalation paths\n\nWhen auditing code:\n1. Map the attack surface\n2. Trace data flow from untrusted inputs\n3. Check trust boundaries\n4. Review error handling (info leaks)\n5. Assess cryptographic implementations\n6. Check dependency versions\n\nSeverity levels: CRITICAL / HIGH / MEDIUM / LOW / INFO\nReport format: Finding → Impact → Evidence → Remediation\"\"\"\n\n[[fallback_models]]\nprovider = \"groq\"\nmodel = \"llama-3.3-70b-versatile\"\napi_key_env = \"GROQ_API_KEY\"\n\n[schedule]\nproactive = { conditions = [\"event:agent_spawned\", \"event:agent_terminated\"] }\n\n[resources]\nmax_llm_tokens_per_hour = 150000\n\n[capabilities]\ntools = [\"file_read\", \"file_list\", \"shell_exec\", \"memory_store\", \"memory_recall\"]\nmemory_read = [\"*\"]\nmemory_write = [\"self.*\", \"shared.*\"]\nshell = [\"cargo audit *\", \"cargo tree *\", \"git log *\"]\n"}}, "crates/openfang-runtime/src/model_catalog.rs": {"6b3a9982fff3902a62ec88e244ff8596db7f7734": {"t": 1785745844.5461953, "n": 104273, "h": "598f4ee55b6597a8", "c": "//! Model catalog — registry of known models with metadata, pricing, and auth detection.\n//!\n//! Provides a comprehensive catalog of 130+ builtin models across 27 providers,\n//! with alias resolution, auth status detection, and pricing lookups.\n\nuse openfang_types::model_catalog::{\n AuthStatus, ModelCatalogEntry, ModelTier, ProviderInfo, AI21_BASE_URL, ANTHROPIC_BASE_URL,\n BEDROCK_BASE_URL, CEREBRAS_BASE_URL, COHERE_BASE_URL, DEEPSEEK_BASE_URL, FIREWORKS_BASE_URL,\n GEMINI_BASE_URL, GITHUB_COPILOT_BASE_URL, GROQ_BASE_URL, HUGGINGFACE_BASE_URL,\n LMSTUDIO_BASE_URL, MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, OLLAMA_BASE_URL,\n OPENAI_BASE_URL, OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL,\n REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VLLM_BASE_URL, XAI_BASE_URL,\n ZHIPU_BASE_URL,\n};\nuse std::collections::HashMap;\n\n/// The model catalog — registry of all known models and providers.\npub struct ModelCatalog {\n models: Vec<ModelCatalogEntry>,\n aliases: HashMap<String, String>,\n providers: Vec<ProviderInfo>,\n}\n\nimpl ModelCatalog {\n /// Create a new catalog populated with builtin models and providers.\n pub fn new() -> Self {\n let models = builtin_models();\n let aliases = builtin_aliases();\n let mut providers = builtin_providers();\n\n // Set model counts on providers\n for provider in &mut providers {\n provider.model_count = models.iter().filter(|m| m.provider == provider.id).count();\n }\n\n Self {\n models,\n aliases,\n providers,\n }\n }\n\n /// Detect which providers have API keys configured.\n ///\n /// Checks `std::env::var()` for each provider's API key env var.\n /// Only checks presence — never reads or stores the actual secret.\n pub fn detect_auth(&mut self) {\n for provider in &mut self.providers {\n if !provider.key_required {\n provider.auth_status = AuthStatus::NotRequired;\n } else if std::env::var(&provider.api_key_env).is_ok() {\n provider.auth_status = AuthStatus::Configured;\n } else {\n // Special case: Gemini also accepts GOOGLE_API_KEY\n if provider.id == \"gemini\" && std::env::var(\"GOOGLE_API_KEY\").is_ok() {\n provider.auth_status = AuthStatus::Configured;\n } else {\n provider.auth_status = AuthStatus::Missing;\n }\n }\n }\n }\n\n /// List all models in the catalog.\n pub fn list_models(&self) -> &[ModelCatalogEntry] {\n &self.models\n }\n\n /// Find a model by its canonical ID or by alias.\n pub fn find_model(&self, id_or_alias: &str) -> Option<&ModelCatalogEntry> {\n let lower = id_or_alias.to_lowercase();\n // Direct ID match first\n if let Some(entry) = self.models.iter().find(|m| m.id.to_lowercase() == lower) {\n return Some(entry);\n }\n // Alias resolution\n if let Some(canonical) = self.aliases.get(&lower) {\n return self.models.iter().find(|m| m.id == *canonical);\n }\n None\n }\n\n /// Resolve an alias to a canonical model ID, or None if not an alias.\n pub fn resolve_alias(&self, alias: &str) -> Option<&str> {\n self.aliases.get(&alias.to_lowercase()).map(|s| s.as_str())\n }\n\n /// List all providers.\n pub fn list_providers(&self) -> &[ProviderInfo] {\n &self.providers\n }\n\n /// Get a provider by ID.\n pub fn get_provider(&self, provider_id: &str) -> Option<&ProviderInfo> {\n self.providers.iter().find(|p| p.id == provider_id)\n }\n\n /// List models from a specific provider.\n pub fn models_by_provider(&self, provider: &str) -> Vec<&ModelCatalogEntry> {\n self.models\n .iter()\n .filter(|m| m.provider == provider)\n .collect()\n }\n\n /// List models that are available (from configured providers only).\n pub fn available_models(&self) -> Vec<&ModelCatalogEntry> {\n let configured: Vec<&str> = self\n .providers\n .iter()\n .filter(|p| p.auth_status != AuthStatus::Missing)\n .map(|p| p.id.as_str())\n .collect();\n self.models\n .iter()\n .filter(|m| configured.contains(&m.provider.as_str()))\n .collect()\n }\n\n /// Get pricing for a model: (input_cost_per_million, output_cost_per_million).\n pub fn pricing(&self, model_id: &str) -> Option<(f64, f64)> {\n self.find_model(model_id)\n .map(|m| (m.input_cost_per_m, m.output_cost_per_m))\n }\n\n /// List all alias mappings.\n pub fn list_aliases(&self) -> &HashMap<String, String> {\n &self.aliases\n }\n\n /// Set a custom base URL for a provider, overriding the default.\n ///\n /// Returns `true` if the provider was found and updated.\n pub fn set_provider_url(&mut self, provider: &str, url: &str) -> bool {\n if let Some(p) = self.providers.iter_mut().find(|p| p.id == provider) {\n p.base_url = url.to_string();\n true\n } else {\n false\n }\n }\n\n /// Apply a batch of provider URL overrides from config.\n ///\n /// Each entry maps a provider ID to a custom base URL.\n /// Unknown providers are silently skipped.\n pub fn apply_url_overrides(&mut self, overrides: &HashMap<String, String>) {\n for (provider, url) in overrides {\n self.set_provider_url(provider, url);\n }\n }\n\n /// List models filtered by tier.\n pub fn models_by_tier(&self, tier: ModelTier) -> Vec<&ModelCatalogEntry> {\n self.models.iter().filter(|m| m.tier == tier).collect()\n }\n\n /// Merge dynamically discovered models from a local provider.\n ///\n /// Adds models not already in the catalog with `Local` tier and zero cost.\n /// Also updates the provider's `model_count`.\n pub fn merge_discovered_models(&mut self, provider: &str, model_ids: &[String]) {\n let existing_ids: std::collections::HashSet<String> = self\n .models\n .iter()\n .filter(|m| m.provider == provider)\n .map(|m| m.id.to_lowercase())\n .collect();\n\n let mut added = 0usize;\n for id in model_ids {\n if existing_ids.contains(&id.to_lowercase()) {\n continue;\n }\n // Generate a human-friendly display name\n let display = format!(\"{} ({})\", id, provider);\n self.models.push(ModelCatalogEntry {\n id: id.clone(),\n display_name: display,\n provider: provider.to_string(),\n tier: ModelTier::Local,\n context_window: 32_768,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.0,\n output_cost_per_m: 0.0,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: Vec::new(),\n });\n added += 1;\n }\n\n // Update model count on the provider\n if added > 0 {\n if let Some(p) = self.providers.iter_mut().find(|p| p.id == provider) {\n p.model_count = self\n .models\n .iter()\n .filter(|m| m.provider == provider)\n .count();\n }\n }\n }\n}\n\nimpl Default for ModelCatalog {\n fn default() -> Self {\n Self::new()\n }\n}\n\n// ---------------------------------------------------------------------------\n// Builtin data\n// ---------------------------------------------------------------------------\n\nfn builtin_providers() -> Vec<ProviderInfo> {\n vec![\n ProviderInfo {\n id: \"anthropic\".into(),\n display_name: \"Anthropic\".into(),\n api_key_env: \"ANTHROPIC_API_KEY\".into(),\n base_url: ANTHROPIC_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"openai\".into(),\n display_name: \"OpenAI\".into(),\n api_key_env: \"OPENAI_API_KEY\".into(),\n base_url: OPENAI_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"gemini\".into(),\n display_name: \"Google Gemini\".into(),\n api_key_env: \"GEMINI_API_KEY\".into(),\n base_url: GEMINI_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"deepseek\".into(),\n display_name: \"DeepSeek\".into(),\n api_key_env: \"DEEPSEEK_API_KEY\".into(),\n base_url: DEEPSEEK_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"groq\".into(),\n display_name: \"Groq\".into(),\n api_key_env: \"GROQ_API_KEY\".into(),\n base_url: GROQ_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"openrouter\".into(),\n display_name: \"OpenRouter\".into(),\n api_key_env: \"OPENROUTER_API_KEY\".into(),\n base_url: OPENROUTER_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"mistral\".into(),\n display_name: \"Mistral AI\".into(),\n api_key_env: \"MISTRAL_API_KEY\".into(),\n base_url: MISTRAL_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"together\".into(),\n display_name: \"Together AI\".into(),\n api_key_env: \"TOGETHER_API_KEY\".into(),\n base_url: TOGETHER_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"fireworks\".into(),\n display_name: \"Fireworks AI\".into(),\n api_key_env: \"FIREWORKS_API_KEY\".into(),\n base_url: FIREWORKS_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"ollama\".into(),\n display_name: \"Ollama\".into(),\n api_key_env: \"OLLAMA_API_KEY\".into(),\n base_url: OLLAMA_BASE_URL.into(),\n key_required: false,\n auth_status: AuthStatus::NotRequired,\n model_count: 0,\n },\n ProviderInfo {\n id: \"vllm\".into(),\n display_name: \"vLLM\".into(),\n api_key_env: \"VLLM_API_KEY\".into(),\n base_url: VLLM_BASE_URL.into(),\n key_required: false,\n auth_status: AuthStatus::NotRequired,\n model_count: 0,\n },\n ProviderInfo {\n id: \"lmstudio\".into(),\n display_name: \"LM Studio\".into(),\n api_key_env: \"LMSTUDIO_API_KEY\".into(),\n base_url: LMSTUDIO_BASE_URL.into(),\n key_required: false,\n auth_status: AuthStatus::NotRequired,\n model_count: 0,\n },\n // ── New providers (8) ──────────────────────────────────────\n ProviderInfo {\n id: \"perplexity\".into(),\n display_name: \"Perplexity AI\".into(),\n api_key_env: \"PERPLEXITY_API_KEY\".into(),\n base_url: PERPLEXITY_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"cohere\".into(),\n display_name: \"Cohere\".into(),\n api_key_env: \"COHERE_API_KEY\".into(),\n base_url: COHERE_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"ai21\".into(),\n display_name: \"AI21 Labs\".into(),\n api_key_env: \"AI21_API_KEY\".into(),\n base_url: AI21_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"cerebras\".into(),\n display_name: \"Cerebras\".into(),\n api_key_env: \"CEREBRAS_API_KEY\".into(),\n base_url: CEREBRAS_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"sambanova\".into(),\n display_name: \"SambaNova\".into(),\n api_key_env: \"SAMBANOVA_API_KEY\".into(),\n base_url: SAMBANOVA_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"huggingface\".into(),\n display_name: \"Hugging Face\".into(),\n api_key_env: \"HF_API_KEY\".into(),\n base_url: HUGGINGFACE_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"xai\".into(),\n display_name: \"xAI\".into(),\n api_key_env: \"XAI_API_KEY\".into(),\n base_url: XAI_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"replicate\".into(),\n display_name: \"Replicate\".into(),\n api_key_env: \"REPLICATE_API_TOKEN\".into(),\n base_url: REPLICATE_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n // ── GitHub Copilot ───────────────────────────────────────────\n ProviderInfo {\n id: \"github-copilot\".into(),\n display_name: \"GitHub Copilot\".into(),\n api_key_env: \"GITHUB_TOKEN\".into(),\n base_url: GITHUB_COPILOT_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n // ── Chinese providers (5) ────────────────────────────────────\n ProviderInfo {\n id: \"qwen\".into(),\n display_name: \"Qwen (Alibaba)\".into(),\n api_key_env: \"DASHSCOPE_API_KEY\".into(),\n base_url: QWEN_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"minimax\".into(),\n display_name: \"MiniMax\".into(),\n api_key_env: \"MINIMAX_API_KEY\".into(),\n base_url: MINIMAX_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"zhipu\".into(),\n display_name: \"Zhipu AI (GLM)\".into(),\n api_key_env: \"ZHIPU_API_KEY\".into(),\n base_url: ZHIPU_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"moonshot\".into(),\n display_name: \"Moonshot (Kimi)\".into(),\n api_key_env: \"MOONSHOT_API_KEY\".into(),\n base_url: MOONSHOT_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ProviderInfo {\n id: \"qianfan\".into(),\n display_name: \"Baidu Qianfan\".into(),\n api_key_env: \"QIANFAN_API_KEY\".into(),\n base_url: QIANFAN_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n // ── AWS Bedrock ──────────────────────────────────────────────\n ProviderInfo {\n id: \"bedrock\".into(),\n display_name: \"AWS Bedrock\".into(),\n api_key_env: \"AWS_ACCESS_KEY_ID\".into(),\n base_url: BEDROCK_BASE_URL.into(),\n key_required: true,\n auth_status: AuthStatus::Missing,\n model_count: 0,\n },\n ]\n}\n\nfn builtin_aliases() -> HashMap<String, String> {\n let pairs = [\n (\"sonnet\", \"claude-sonnet-4-6\"),\n (\"claude-sonnet\", \"claude-sonnet-4-6\"),\n (\"haiku\", \"claude-haiku-4-5-20251001\"),\n (\"claude-haiku\", \"claude-haiku-4-5-20251001\"),\n (\"opus\", \"claude-opus-4-6\"),\n (\"claude-opus\", \"claude-opus-4-6\"),\n (\"gpt4\", \"gpt-4o\"),\n (\"gpt4o\", \"gpt-4o\"),\n (\"gpt4-mini\", \"gpt-4o-mini\"),\n (\"gpt5\", \"gpt-5.2\"),\n (\"gpt5-mini\", \"gpt-5-mini\"),\n (\"flash\", \"gemini-3-flash\"),\n (\"gemini-flash\", \"gemini-3-flash\"),\n (\"gemini-pro\", \"gemini-3.1-pro\"),\n (\"deepseek\", \"deepseek-chat\"),\n (\"llama\", \"llama-3.3-70b-versatile\"),\n (\"llama-70b\", \"llama-3.3-70b-versatile\"),\n (\"mixtral\", \"mixtral-8x7b-32768\"),\n (\"mistral\", \"mistral-large-latest\"),\n (\"codestral\", \"codestral-latest\"),\n // DeepSeek aliases\n (\"deepseek-v3\", \"deepseek-chat\"),\n (\"deepseek-r1\", \"deepseek-reasoner\"),\n // Mistral aliases\n (\"mistral-nemo\", \"open-mistral-nemo\"),\n (\"pixtral\", \"pixtral-large-latest\"),\n // xAI aliases\n (\"grok\", \"grok-4\"),\n (\"grok-mini\", \"grok-2-mini\"),\n (\"grok3\", \"grok-3\"),\n (\"grok-fast\", \"grok-4.1-fast\"),\n // Perplexity alias\n (\"sonar\", \"sonar-pro\"),\n // AI21 aliases\n (\"jamba\", \"jamba-1.5-large\"),\n // Cohere aliases\n (\"command-r\", \"command-r-plus\"),\n (\"command\", \"command-a\"),\n // GitHub Copilot aliases\n (\"copilot\", \"copilot/gpt-4o\"),\n (\"copilot-4o\", \"copilot/gpt-4o\"),\n (\"copilot-4\", \"copilot/gpt-4\"),\n (\"copilot-gpt4o\", \"copilot/gpt-4o\"),\n (\"copilot-gpt4\", \"copilot/gpt-4\"),\n // Chinese model aliases\n (\"qwen\", \"qwen-plus\"),\n (\"glm\", \"glm-4-plus\"),\n (\"ernie\", \"ernie-4.5-8k\"),\n (\"kimi\", \"moonshot-v1-128k\"),\n (\"minimax\", \"minimax-text-01\"),\n ];\n pairs\n .into_iter()\n .map(|(k, v)| (k.to_lowercase(), v.to_string()))\n .collect()\n}\n\nfn builtin_models() -> Vec<ModelCatalogEntry> {\n vec![\n // ══════════════════════════════════════════════════════════════\n // Anthropic (7)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"claude-opus-4-6\".into(),\n display_name: \"Claude Opus 4.6\".into(),\n provider: \"anthropic\".into(),\n tier: ModelTier::Frontier,\n context_window: 200_000,\n max_output_tokens: 128_000,\n input_cost_per_m: 5.0,\n output_cost_per_m: 25.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![\"opus\".into(), \"claude-opus\".into()],\n },\n ModelCatalogEntry {\n id: \"claude-sonnet-4-6\".into(),\n display_name: \"Claude Sonnet 4.6\".into(),\n provider: \"anthropic\".into(),\n tier: ModelTier::Smart,\n context_window: 200_000,\n max_output_tokens: 64_000,\n input_cost_per_m: 3.0,\n output_cost_per_m: 15.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![\"sonnet\".into(), \"claude-sonnet\".into()],\n },\n ModelCatalogEntry {\n id: \"claude-opus-4-20250514\".into(),\n display_name: \"Claude Opus 4\".into(),\n provider: \"anthropic\".into(),\n tier: ModelTier::Frontier,\n context_window: 200_000,\n max_output_tokens: 32_000,\n input_cost_per_m: 15.0,\n output_cost_per_m: 75.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"claude-sonnet-4-20250514\".into(),\n display_name: \"Claude Sonnet 4\".into(),\n provider: \"anthropic\".into(),\n tier: ModelTier::Smart,\n context_window: 200_000,\n max_output_tokens: 64_000,\n input_cost_per_m: 3.0,\n output_cost_per_m: 15.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"claude-haiku-4-5-20251001\".into(),\n display_name: \"Claude Haiku 4.5\".into(),\n provider: \"anthropic\".into(),\n tier: ModelTier::Fast,\n context_window: 200_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.25,\n output_cost_per_m: 1.25,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![\"haiku\".into(), \"claude-haiku\".into()],\n },\n ModelCatalogEntry {\n id: \"claude-sonnet-4-5-20250514\".into(),\n display_name: \"Claude Sonnet 4.5\".into(),\n provider: \"anthropic\".into(),\n tier: ModelTier::Smart,\n context_window: 200_000,\n max_output_tokens: 64_000,\n input_cost_per_m: 3.0,\n output_cost_per_m: 15.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"claude-3-5-sonnet-20241022\".into(),\n display_name: \"Claude 3.5 Sonnet\".into(),\n provider: \"anthropic\".into(),\n tier: ModelTier::Smart,\n context_window: 200_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 3.0,\n output_cost_per_m: 15.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // OpenAI (16)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"gpt-4o\".into(),\n display_name: \"GPT-4o\".into(),\n provider: \"openai\".into(),\n tier: ModelTier::Smart,\n context_window: 128_000,\n max_output_tokens: 16_384,\n input_cost_per_m: 2.50,\n output_cost_per_m: 10.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![\"gpt4\".into(), \"gpt4o\".into()],\n },\n ModelCatalogEntry {\n id: \"gpt-4o-mini\".into(),\n display_name: \"GPT-4o Mini\".into(),\n provider: \"openai\".into(),\n tier: ModelTier::Fast,\n context_window: 128_000,\n max_output_tokens: 16_384,\n input_cost_per_m: 0.15,\n output_cost_per_m: 0.60,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![\"gpt4-mini\".into()],\n },\n ModelCatalogEntry {\n id: \"gpt-4.1\".into(),\n display_name: \"GPT-4.1\".into(),\n provider: \"openai\".into(),\n tier: ModelTier::Frontier,\n context_window: 1_047_576,\n max_output_tokens: 32_768,\n input_cost_per_m: 2.00,\n output_cost_per_m: 8.00,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"gpt-4.1-mini\".into(),\n display_name: \"GPT-4.1 Mini\".into(),\n provider: \"openai\".into(),\n tier: ModelTier::Balanced,\n context_window: 1_047_576,\n max_output_tokens: 32_768,\n input_cost_per_m: 0.40,\n output_cost_per_m: 1.60,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"gpt-4.1-nano\".into(),\n display_name: \"GPT-4.1 Nano\".into(),\n provider: \"openai\".into(),\n tier: ModelTier::Fast,\n context_window: 1_047_576,\n max_output_tokens: 32_768,\n input_cost_per_m: 0.10,\n output_cost_per_m: 0.40,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"o3\".into(),\n display_name: \"o3\".into(),\n provider: \"openai\".into(),\n tier: ModelTier::Frontier,\n context_window: 200_000,\n max_output_tokens: 100_000,\n input_cost_per_m: 2.00,\n output_cost_per_m: 8.00,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"o3-mini\".into(),\n display_name: \"o3-mini\".into(),\n provider: \"openai\".into(),\n tier: ModelTier::Smart,\n context_window: 200_000,\n max_output_tokens: 100_000,\n input_cost_per_m: 1.10,\n output_cost_per_m: 4.40,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"o4-mini\".into(),\n display_name: \"o4-mini\".into(),\n provider: \"openai\".into(),\n tier: ModelTier::Smart,\n context_window: 200_000,\n max_output_tokens: 100_000,\n input_cost_per_m: 1.10,\n output_cost_per_m: 4.40,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"gpt-4-turbo\".into(),\n display_name: \"GPT-4 Turbo\".into(),\n provider: \"openai\".into(),\n tier: ModelTier::Smart,\n context_window: 128_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 10.00,\n output_cost_per_m: 30.00,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"gpt-3.5-turbo\".into(),\n display_name: \"GPT-3.5 Turbo\".into(),\n provider: \"openai\".into(),\n tier: ModelTier::Fast,\n context_window: 16_385,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.50,\n output_cost_per_m: 1.50,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"gpt-5\".into(),\n display_name: \"GPT-5\".into(),\n provider: \"openai\".into(),\n tier: ModelTier::Frontier,\n context_window: 400_000,\n max_output_tokens: 128_000,\n input_cost_per_m: 1.25,\n output_cost_per_m: 10.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"gpt-5-mini\".into(),\n display_name: \"GPT-5 Mini\".into(),\n provider: \"openai\".into(),\n tier: ModelTier::Balanced,\n context_window: 400_000,\n max_output_tokens: 128_000,\n input_cost_per_m: 0.25,\n output_cost_per_m: 2.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![\"gpt5-mini\".into()],\n },\n ModelCatalogEntry {\n id: \"gpt-5-nano\".into(),\n display_name: \"GPT-5 Nano\".into(),\n provider: \"openai\".into(),\n tier: ModelTier::Fast,\n context_window: 400_000,\n max_output_tokens: 128_000,\n input_cost_per_m: 0.05,\n output_cost_per_m: 0.40,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"gpt-5.1\".into(),\n display_name: \"GPT-5.1\".into(),\n provider: \"openai\".into(),\n tier: ModelTier::Frontier,\n context_window: 400_000,\n max_output_tokens: 128_000,\n input_cost_per_m: 1.25,\n output_cost_per_m: 10.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"gpt-5.2\".into(),\n display_name: \"GPT-5.2\".into(),\n provider: \"openai\".into(),\n tier: ModelTier::Frontier,\n context_window: 400_000,\n max_output_tokens: 128_000,\n input_cost_per_m: 1.75,\n output_cost_per_m: 14.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![\"gpt5\".into()],\n },\n ModelCatalogEntry {\n id: \"gpt-5.2-pro\".into(),\n display_name: \"GPT-5.2 Pro\".into(),\n provider: \"openai\".into(),\n tier: ModelTier::Frontier,\n context_window: 400_000,\n max_output_tokens: 128_000,\n input_cost_per_m: 1.75,\n output_cost_per_m: 14.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // Google Gemini (10)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"gemini-3.1-pro\".into(),\n display_name: \"Gemini 3.1 Pro\".into(),\n provider: \"gemini\".into(),\n tier: ModelTier::Frontier,\n context_window: 1_048_576,\n max_output_tokens: 65_536,\n input_cost_per_m: 2.50,\n output_cost_per_m: 15.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![\"gemini-pro\".into()],\n },\n ModelCatalogEntry {\n id: \"gemini-3-flash\".into(),\n display_name: \"Gemini 3 Flash\".into(),\n provider: \"gemini\".into(),\n tier: ModelTier::Smart,\n context_window: 1_048_576,\n max_output_tokens: 65_536,\n input_cost_per_m: 0.50,\n output_cost_per_m: 3.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![\"flash\".into(), \"gemini-flash\".into()],\n },\n ModelCatalogEntry {\n id: \"gemini-3-deep-think\".into(),\n display_name: \"Gemini 3 Deep Think\".into(),\n provider: \"gemini\".into(),\n tier: ModelTier::Frontier,\n context_window: 1_048_576,\n max_output_tokens: 65_536,\n input_cost_per_m: 2.50,\n output_cost_per_m: 15.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"gemini-2.5-flash-lite\".into(),\n display_name: \"Gemini 2.5 Flash Lite\".into(),\n provider: \"gemini\".into(),\n tier: ModelTier::Fast,\n context_window: 1_048_576,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.04,\n output_cost_per_m: 0.15,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"gemini-2.5-pro\".into(),\n display_name: \"Gemini 2.5 Pro\".into(),\n provider: \"gemini\".into(),\n tier: ModelTier::Frontier,\n context_window: 1_048_576,\n max_output_tokens: 65_536,\n input_cost_per_m: 1.25,\n output_cost_per_m: 10.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"gemini-2.5-flash\".into(),\n display_name: \"Gemini 2.5 Flash\".into(),\n provider: \"gemini\".into(),\n tier: ModelTier::Smart,\n context_window: 1_048_576,\n max_output_tokens: 65_536,\n input_cost_per_m: 0.15,\n output_cost_per_m: 0.60,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"gemini-2.0-flash\".into(),\n display_name: \"Gemini 2.0 Flash\".into(),\n provider: \"gemini\".into(),\n tier: ModelTier::Fast,\n context_window: 1_048_576,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.10,\n output_cost_per_m: 0.40,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"gemini-2.0-flash-lite\".into(),\n display_name: \"Gemini 2.0 Flash Lite\".into(),\n provider: \"gemini\".into(),\n tier: ModelTier::Fast,\n context_window: 1_048_576,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.075,\n output_cost_per_m: 0.30,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"gemini-1.5-pro\".into(),\n display_name: \"Gemini 1.5 Pro\".into(),\n provider: \"gemini\".into(),\n tier: ModelTier::Smart,\n context_window: 2_097_152,\n max_output_tokens: 8_192,\n input_cost_per_m: 1.25,\n output_cost_per_m: 5.00,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"gemini-1.5-flash\".into(),\n display_name: \"Gemini 1.5 Flash\".into(),\n provider: \"gemini\".into(),\n tier: ModelTier::Fast,\n context_window: 1_048_576,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.075,\n output_cost_per_m: 0.30,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // DeepSeek (4)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"deepseek-chat\".into(),\n display_name: \"DeepSeek V3\".into(),\n provider: \"deepseek\".into(),\n tier: ModelTier::Smart,\n context_window: 64_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.27,\n output_cost_per_m: 1.10,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"deepseek\".into(), \"deepseek-v3\".into()],\n },\n ModelCatalogEntry {\n id: \"deepseek-reasoner\".into(),\n display_name: \"DeepSeek R1\".into(),\n provider: \"deepseek\".into(),\n tier: ModelTier::Frontier,\n context_window: 64_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.55,\n output_cost_per_m: 2.19,\n supports_tools: false,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"deepseek-r1\".into()],\n },\n ModelCatalogEntry {\n id: \"deepseek-coder\".into(),\n display_name: \"DeepSeek Coder V2\".into(),\n provider: \"deepseek\".into(),\n tier: ModelTier::Balanced,\n context_window: 128_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.14,\n output_cost_per_m: 0.28,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"deepseek-chat-v3-0324\".into(),\n display_name: \"DeepSeek V3 0324\".into(),\n provider: \"deepseek\".into(),\n tier: ModelTier::Smart,\n context_window: 64_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.27,\n output_cost_per_m: 1.10,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // Groq (11)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"llama-3.3-70b-versatile\".into(),\n display_name: \"Llama 3.3 70B\".into(),\n provider: \"groq\".into(),\n tier: ModelTier::Balanced,\n context_window: 128_000,\n max_output_tokens: 32_768,\n input_cost_per_m: 0.059,\n output_cost_per_m: 0.079,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"llama\".into(), \"llama-70b\".into()],\n },\n ModelCatalogEntry {\n id: \"llama-3.1-8b-instant\".into(),\n display_name: \"Llama 3.1 8B\".into(),\n provider: \"groq\".into(),\n tier: ModelTier::Fast,\n context_window: 128_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.05,\n output_cost_per_m: 0.08,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"llama-3.2-90b-vision-preview\".into(),\n display_name: \"Llama 3.2 90B Vision\".into(),\n provider: \"groq\".into(),\n tier: ModelTier::Smart,\n context_window: 128_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.90,\n output_cost_per_m: 0.90,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"llama-3.2-11b-vision-preview\".into(),\n display_name: \"Llama 3.2 11B Vision\".into(),\n provider: \"groq\".into(),\n tier: ModelTier::Balanced,\n context_window: 128_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.18,\n output_cost_per_m: 0.18,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"llama-3.2-3b-preview\".into(),\n display_name: \"Llama 3.2 3B\".into(),\n provider: \"groq\".into(),\n tier: ModelTier::Fast,\n context_window: 128_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.06,\n output_cost_per_m: 0.06,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"llama-3.2-1b-preview\".into(),\n display_name: \"Llama 3.2 1B\".into(),\n provider: \"groq\".into(),\n tier: ModelTier::Fast,\n context_window: 128_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.04,\n output_cost_per_m: 0.04,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"deepseek-r1-distill-llama-70b\".into(),\n display_name: \"DeepSeek R1 Distill 70B\".into(),\n provider: \"groq\".into(),\n tier: ModelTier::Smart,\n context_window: 128_000,\n max_output_tokens: 16_384,\n input_cost_per_m: 0.75,\n output_cost_per_m: 0.99,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"mixtral-8x7b-32768\".into(),\n display_name: \"Mixtral 8x7B\".into(),\n provider: \"groq\".into(),\n tier: ModelTier::Balanced,\n context_window: 32_768,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.024,\n output_cost_per_m: 0.024,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"mixtral\".into()],\n },\n ModelCatalogEntry {\n id: \"gemma2-9b-it\".into(),\n display_name: \"Gemma 2 9B\".into(),\n provider: \"groq\".into(),\n tier: ModelTier::Fast,\n context_window: 8_192,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.02,\n output_cost_per_m: 0.02,\n supports_tools: false,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"qwen-qwq-32b\".into(),\n display_name: \"Qwen QWQ 32B\".into(),\n provider: \"groq\".into(),\n tier: ModelTier::Balanced,\n context_window: 128_000,\n max_output_tokens: 16_384,\n input_cost_per_m: 0.20,\n output_cost_per_m: 0.20,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"meta-llama/llama-4-scout-17b-16e-instruct\".into(),\n display_name: \"Llama 4 Scout 17B\".into(),\n provider: \"groq\".into(),\n tier: ModelTier::Balanced,\n context_window: 128_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.11,\n output_cost_per_m: 0.34,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // OpenRouter (5)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"openrouter/auto\".into(),\n display_name: \"OpenRouter Auto\".into(),\n provider: \"openrouter\".into(),\n tier: ModelTier::Smart,\n context_window: 200_000,\n max_output_tokens: 32_000,\n input_cost_per_m: 1.0,\n output_cost_per_m: 3.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"openrouter/optimus\".into(),\n display_name: \"OpenRouter Optimus\".into(),\n provider: \"openrouter\".into(),\n tier: ModelTier::Balanced,\n context_window: 200_000,\n max_output_tokens: 32_000,\n input_cost_per_m: 0.50,\n output_cost_per_m: 1.50,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"openrouter/nitro\".into(),\n display_name: \"OpenRouter Nitro\".into(),\n provider: \"openrouter\".into(),\n tier: ModelTier::Fast,\n context_window: 128_000,\n max_output_tokens: 16_000,\n input_cost_per_m: 0.20,\n output_cost_per_m: 0.60,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"openrouter/anthropic/claude-sonnet-4\".into(),\n display_name: \"Claude Sonnet 4 (OpenRouter)\".into(),\n provider: \"openrouter\".into(),\n tier: ModelTier::Smart,\n context_window: 200_000,\n max_output_tokens: 64_000,\n input_cost_per_m: 3.0,\n output_cost_per_m: 15.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"openrouter/google/gemini-2.5-flash\".into(),\n display_name: \"Gemini 2.5 Flash (OpenRouter)\".into(),\n provider: \"openrouter\".into(),\n tier: ModelTier::Smart,\n context_window: 1_048_576,\n max_output_tokens: 65_536,\n input_cost_per_m: 0.15,\n output_cost_per_m: 0.60,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // Mistral (6)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"mistral-large-latest\".into(),\n display_name: \"Mistral Large\".into(),\n provider: \"mistral\".into(),\n tier: ModelTier::Smart,\n context_window: 128_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 2.00,\n output_cost_per_m: 6.00,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"mistral\".into()],\n },\n ModelCatalogEntry {\n id: \"mistral-medium-latest\".into(),\n display_name: \"Mistral Medium\".into(),\n provider: \"mistral\".into(),\n tier: ModelTier::Balanced,\n context_window: 128_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 2.70,\n output_cost_per_m: 8.10,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"mistral-small-latest\".into(),\n display_name: \"Mistral Small\".into(),\n provider: \"mistral\".into(),\n tier: ModelTier::Fast,\n context_window: 128_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.10,\n output_cost_per_m: 0.30,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"codestral-latest\".into(),\n display_name: \"Codestral\".into(),\n provider: \"mistral\".into(),\n tier: ModelTier::Smart,\n context_window: 32_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.30,\n output_cost_per_m: 0.90,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"codestral\".into()],\n },\n ModelCatalogEntry {\n id: \"open-mistral-nemo\".into(),\n display_name: \"Mistral Nemo\".into(),\n provider: \"mistral\".into(),\n tier: ModelTier::Fast,\n context_window: 128_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.15,\n output_cost_per_m: 0.15,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"mistral-nemo\".into()],\n },\n ModelCatalogEntry {\n id: \"pixtral-large-latest\".into(),\n display_name: \"Pixtral Large\".into(),\n provider: \"mistral\".into(),\n tier: ModelTier::Smart,\n context_window: 128_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 2.00,\n output_cost_per_m: 6.00,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![\"pixtral\".into()],\n },\n // ══════════════════════════════════════════════════════════════\n // Together (8)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo\".into(),\n display_name: \"Llama 3.1 405B (Together)\".into(),\n provider: \"together\".into(),\n tier: ModelTier::Frontier,\n context_window: 130_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 3.50,\n output_cost_per_m: 3.50,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\".into(),\n display_name: \"Llama 3.3 70B (Together)\".into(),\n provider: \"together\".into(),\n tier: ModelTier::Smart,\n context_window: 128_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.88,\n output_cost_per_m: 0.88,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8\".into(),\n display_name: \"Llama 4 Maverick (Together)\".into(),\n provider: \"together\".into(),\n tier: ModelTier::Smart,\n context_window: 1_048_576,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.27,\n output_cost_per_m: 0.35,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"meta-llama/Llama-4-Scout-17B-16E-Instruct\".into(),\n display_name: \"Llama 4 Scout (Together)\".into(),\n provider: \"together\".into(),\n tier: ModelTier::Balanced,\n context_window: 512_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.18,\n output_cost_per_m: 0.30,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"deepseek-ai/DeepSeek-R1\".into(),\n display_name: \"DeepSeek R1 (Together)\".into(),\n provider: \"together\".into(),\n tier: ModelTier::Frontier,\n context_window: 64_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 3.00,\n output_cost_per_m: 7.00,\n supports_tools: false,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"deepseek-ai/DeepSeek-V3\".into(),\n display_name: \"DeepSeek V3 (Together)\".into(),\n provider: \"together\".into(),\n tier: ModelTier::Smart,\n context_window: 64_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.90,\n output_cost_per_m: 0.90,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"Qwen/Qwen2.5-72B-Instruct-Turbo\".into(),\n display_name: \"Qwen 2.5 72B (Together)\".into(),\n provider: \"together\".into(),\n tier: ModelTier::Smart,\n context_window: 32_768,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.20,\n output_cost_per_m: 0.60,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"mistralai/Mixtral-8x22B-Instruct-v0.1\".into(),\n display_name: \"Mixtral 8x22B (Together)\".into(),\n provider: \"together\".into(),\n tier: ModelTier::Balanced,\n context_window: 65_536,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.60,\n output_cost_per_m: 0.60,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // Fireworks (5)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"accounts/fireworks/models/llama-v3p1-405b-instruct\".into(),\n display_name: \"Llama 3.1 405B (Fireworks)\".into(),\n provider: \"fireworks\".into(),\n tier: ModelTier::Frontier,\n context_window: 131_072,\n max_output_tokens: 16_384,\n input_cost_per_m: 3.00,\n output_cost_per_m: 3.00,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"accounts/fireworks/models/llama-v3p3-70b-instruct\".into(),\n display_name: \"Llama 3.3 70B (Fireworks)\".into(),\n provider: \"fireworks\".into(),\n tier: ModelTier::Smart,\n context_window: 131_072,\n max_output_tokens: 16_384,\n input_cost_per_m: 0.90,\n output_cost_per_m: 0.90,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"accounts/fireworks/models/deepseek-r1\".into(),\n display_name: \"DeepSeek R1 (Fireworks)\".into(),\n provider: \"fireworks\".into(),\n tier: ModelTier::Frontier,\n context_window: 64_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 3.00,\n output_cost_per_m: 8.00,\n supports_tools: false,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"accounts/fireworks/models/deepseek-v3\".into(),\n display_name: \"DeepSeek V3 (Fireworks)\".into(),\n provider: \"fireworks\".into(),\n tier: ModelTier::Smart,\n context_window: 64_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.90,\n output_cost_per_m: 0.90,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"accounts/fireworks/models/mixtral-8x22b-instruct\".into(),\n display_name: \"Mixtral 8x22B (Fireworks)\".into(),\n provider: \"fireworks\".into(),\n tier: ModelTier::Balanced,\n context_window: 65_536,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.90,\n output_cost_per_m: 0.90,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // Ollama (6) — local, no key required + dynamic discovery\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"llama3.2\".into(),\n display_name: \"Llama 3.2 (Ollama)\".into(),\n provider: \"ollama\".into(),\n tier: ModelTier::Local,\n context_window: 128_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.0,\n output_cost_per_m: 0.0,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"llama3.1\".into(),\n display_name: \"Llama 3.1 (Ollama)\".into(),\n provider: \"ollama\".into(),\n tier: ModelTier::Local,\n context_window: 128_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.0,\n output_cost_per_m: 0.0,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"mistral:latest\".into(),\n display_name: \"Mistral (Ollama)\".into(),\n provider: \"ollama\".into(),\n tier: ModelTier::Local,\n context_window: 32_768,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.0,\n output_cost_per_m: 0.0,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"qwen2.5\".into(),\n display_name: \"Qwen 2.5 (Ollama)\".into(),\n provider: \"ollama\".into(),\n tier: ModelTier::Local,\n context_window: 32_768,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.0,\n output_cost_per_m: 0.0,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"phi3\".into(),\n display_name: \"Phi-3 (Ollama)\".into(),\n provider: \"ollama\".into(),\n tier: ModelTier::Local,\n context_window: 128_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.0,\n output_cost_per_m: 0.0,\n supports_tools: false,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"deepseek-r1:latest\".into(),\n display_name: \"DeepSeek R1 (Ollama)\".into(),\n provider: \"ollama\".into(),\n tier: ModelTier::Local,\n context_window: 64_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.0,\n output_cost_per_m: 0.0,\n supports_tools: false,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // vLLM (1) — generic local entry + dynamic discovery\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"vllm-local\".into(),\n display_name: \"vLLM Local Model\".into(),\n provider: \"vllm\".into(),\n tier: ModelTier::Local,\n context_window: 32_768,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.0,\n output_cost_per_m: 0.0,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // LM Studio (1) — generic local entry + dynamic discovery\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"lmstudio-local\".into(),\n display_name: \"LM Studio Local Model\".into(),\n provider: \"lmstudio\".into(),\n tier: ModelTier::Local,\n context_window: 32_768,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.0,\n output_cost_per_m: 0.0,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // Perplexity (4)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"sonar-pro\".into(),\n display_name: \"Sonar Pro\".into(),\n provider: \"perplexity\".into(),\n tier: ModelTier::Smart,\n context_window: 200_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 3.0,\n output_cost_per_m: 15.0,\n supports_tools: false,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"sonar\".into()],\n },\n ModelCatalogEntry {\n id: \"sonar-reasoning-pro\".into(),\n display_name: \"Sonar Reasoning Pro\".into(),\n provider: \"perplexity\".into(),\n tier: ModelTier::Frontier,\n context_window: 200_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 2.0,\n output_cost_per_m: 8.0,\n supports_tools: false,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"sonar-reasoning\".into(),\n display_name: \"Sonar Reasoning\".into(),\n provider: \"perplexity\".into(),\n tier: ModelTier::Balanced,\n context_window: 128_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 1.0,\n output_cost_per_m: 5.0,\n supports_tools: false,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"sonar-basic\".into(),\n display_name: \"Sonar\".into(),\n provider: \"perplexity\".into(),\n tier: ModelTier::Fast,\n context_window: 128_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 1.0,\n output_cost_per_m: 5.0,\n supports_tools: false,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // Cohere (4)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"command-r-plus\".into(),\n display_name: \"Command R+\".into(),\n provider: \"cohere\".into(),\n tier: ModelTier::Smart,\n context_window: 128_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 2.50,\n output_cost_per_m: 10.0,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"command-r\".into()],\n },\n ModelCatalogEntry {\n id: \"command-r-08-2024\".into(),\n display_name: \"Command R (Aug 2024)\".into(),\n provider: \"cohere\".into(),\n tier: ModelTier::Balanced,\n context_window: 128_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.15,\n output_cost_per_m: 0.60,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"command-a\".into(),\n display_name: \"Command A\".into(),\n provider: \"cohere\".into(),\n tier: ModelTier::Smart,\n context_window: 256_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 2.50,\n output_cost_per_m: 10.0,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"command-light\".into(),\n display_name: \"Command Light\".into(),\n provider: \"cohere\".into(),\n tier: ModelTier::Fast,\n context_window: 4_096,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.30,\n output_cost_per_m: 0.60,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // AI21 (3)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"jamba-1.5-large\".into(),\n display_name: \"Jamba 1.5 Large\".into(),\n provider: \"ai21\".into(),\n tier: ModelTier::Smart,\n context_window: 256_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 2.0,\n output_cost_per_m: 8.0,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"jamba\".into()],\n },\n ModelCatalogEntry {\n id: \"jamba-1.5-mini\".into(),\n display_name: \"Jamba 1.5 Mini\".into(),\n provider: \"ai21\".into(),\n tier: ModelTier::Fast,\n context_window: 256_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.20,\n output_cost_per_m: 0.40,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"jamba-instruct\".into(),\n display_name: \"Jamba Instruct\".into(),\n provider: \"ai21\".into(),\n tier: ModelTier::Balanced,\n context_window: 256_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.50,\n output_cost_per_m: 0.70,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // Cerebras (4)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"cerebras/llama3.3-70b\".into(),\n display_name: \"Llama 3.3 70B (Cerebras)\".into(),\n provider: \"cerebras\".into(),\n tier: ModelTier::Balanced,\n context_window: 128_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.06,\n output_cost_per_m: 0.06,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"cerebras/llama3.1-8b\".into(),\n display_name: \"Llama 3.1 8B (Cerebras)\".into(),\n provider: \"cerebras\".into(),\n tier: ModelTier::Fast,\n context_window: 128_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.01,\n output_cost_per_m: 0.01,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"cerebras/llama-4-scout-17b\".into(),\n display_name: \"Llama 4 Scout (Cerebras)\".into(),\n provider: \"cerebras\".into(),\n tier: ModelTier::Smart,\n context_window: 512_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.10,\n output_cost_per_m: 0.10,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"cerebras/qwen-2.5-32b\".into(),\n display_name: \"Qwen 2.5 32B (Cerebras)\".into(),\n provider: \"cerebras\".into(),\n tier: ModelTier::Balanced,\n context_window: 32_768,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.06,\n output_cost_per_m: 0.06,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // SambaNova (3)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"sambanova/llama-3.3-70b\".into(),\n display_name: \"Llama 3.3 70B (SambaNova)\".into(),\n provider: \"sambanova\".into(),\n tier: ModelTier::Balanced,\n context_window: 128_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.06,\n output_cost_per_m: 0.06,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"sambanova/deepseek-r1\".into(),\n display_name: \"DeepSeek R1 (SambaNova)\".into(),\n provider: \"sambanova\".into(),\n tier: ModelTier::Smart,\n context_window: 64_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.06,\n output_cost_per_m: 0.06,\n supports_tools: false,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"sambanova/qwen-2.5-72b\".into(),\n display_name: \"Qwen 2.5 72B (SambaNova)\".into(),\n provider: \"sambanova\".into(),\n tier: ModelTier::Smart,\n context_window: 32_768,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.06,\n output_cost_per_m: 0.06,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // xAI (6)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"grok-4\".into(),\n display_name: \"Grok 4\".into(),\n provider: \"xai\".into(),\n tier: ModelTier::Frontier,\n context_window: 256_000,\n max_output_tokens: 32_768,\n input_cost_per_m: 3.0,\n output_cost_per_m: 15.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![\"grok\".into()],\n },\n ModelCatalogEntry {\n id: \"grok-4.1-fast\".into(),\n display_name: \"Grok 4.1 Fast\".into(),\n provider: \"xai\".into(),\n tier: ModelTier::Fast,\n context_window: 2_000_000,\n max_output_tokens: 32_768,\n input_cost_per_m: 0.20,\n output_cost_per_m: 0.50,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"grok-fast\".into()],\n },\n ModelCatalogEntry {\n id: \"grok-3\".into(),\n display_name: \"Grok 3\".into(),\n provider: \"xai\".into(),\n tier: ModelTier::Frontier,\n context_window: 131_072,\n max_output_tokens: 32_768,\n input_cost_per_m: 3.0,\n output_cost_per_m: 15.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![\"grok3\".into()],\n },\n ModelCatalogEntry {\n id: \"grok-3-mini\".into(),\n display_name: \"Grok 3 Mini\".into(),\n provider: \"xai\".into(),\n tier: ModelTier::Balanced,\n context_window: 131_072,\n max_output_tokens: 32_768,\n input_cost_per_m: 0.30,\n output_cost_per_m: 0.50,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"grok-2\".into(),\n display_name: \"Grok 2\".into(),\n provider: \"xai\".into(),\n tier: ModelTier::Smart,\n context_window: 131_072,\n max_output_tokens: 32_768,\n input_cost_per_m: 2.0,\n output_cost_per_m: 10.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"grok-2-mini\".into(),\n display_name: \"Grok 2 Mini\".into(),\n provider: \"xai\".into(),\n tier: ModelTier::Fast,\n context_window: 131_072,\n max_output_tokens: 32_768,\n input_cost_per_m: 0.30,\n output_cost_per_m: 0.50,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"grok-mini\".into()],\n },\n // ══════════════════════════════════════════════════════════════\n // Hugging Face (3) + dynamic discovery\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"hf/meta-llama/Llama-3.3-70B-Instruct\".into(),\n display_name: \"Llama 3.3 70B (HF)\".into(),\n provider: \"huggingface\".into(),\n tier: ModelTier::Balanced,\n context_window: 128_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.30,\n output_cost_per_m: 0.30,\n supports_tools: false,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"hf/deepseek-ai/DeepSeek-R1\".into(),\n display_name: \"DeepSeek R1 (HF)\".into(),\n provider: \"huggingface\".into(),\n tier: ModelTier::Smart,\n context_window: 64_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.30,\n output_cost_per_m: 0.30,\n supports_tools: false,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"hf/Qwen/Qwen2.5-72B-Instruct\".into(),\n display_name: \"Qwen 2.5 72B (HF)\".into(),\n provider: \"huggingface\".into(),\n tier: ModelTier::Balanced,\n context_window: 32_768,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.30,\n output_cost_per_m: 0.30,\n supports_tools: false,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // Replicate (3)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"replicate/meta-llama-3.3-70b-instruct\".into(),\n display_name: \"Llama 3.3 70B (Replicate)\".into(),\n provider: \"replicate\".into(),\n tier: ModelTier::Balanced,\n context_window: 128_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.40,\n output_cost_per_m: 0.40,\n supports_tools: false,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"replicate/deepseek-r1\".into(),\n display_name: \"DeepSeek R1 (Replicate)\".into(),\n provider: \"replicate\".into(),\n tier: ModelTier::Smart,\n context_window: 64_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.40,\n output_cost_per_m: 0.40,\n supports_tools: false,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"replicate/mistral-7b-instruct\".into(),\n display_name: \"Mistral 7B (Replicate)\".into(),\n provider: \"replicate\".into(),\n tier: ModelTier::Fast,\n context_window: 32_768,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.05,\n output_cost_per_m: 0.25,\n supports_tools: false,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // GitHub Copilot (2) — free for subscribers\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"copilot/gpt-4o\".into(),\n display_name: \"GPT-4o (Copilot)\".into(),\n provider: \"github-copilot\".into(),\n tier: ModelTier::Smart,\n context_window: 128_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.0,\n output_cost_per_m: 0.0,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![\"copilot-gpt4o\".into()],\n },\n ModelCatalogEntry {\n id: \"copilot/gpt-4\".into(),\n display_name: \"GPT-4 (Copilot)\".into(),\n provider: \"github-copilot\".into(),\n tier: ModelTier::Frontier,\n context_window: 128_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.0,\n output_cost_per_m: 0.0,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"copilot-gpt4\".into()],\n },\n // ══════════════════════════════════════════════════════════════\n // Qwen / Alibaba (6)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"qwen-max\".into(),\n display_name: \"Qwen Max\".into(),\n provider: \"qwen\".into(),\n tier: ModelTier::Frontier,\n context_window: 32_768,\n max_output_tokens: 8_192,\n input_cost_per_m: 4.00,\n output_cost_per_m: 12.00,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"qwen-plus\".into(),\n display_name: \"Qwen Plus\".into(),\n provider: \"qwen\".into(),\n tier: ModelTier::Smart,\n context_window: 131_072,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.80,\n output_cost_per_m: 2.00,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"qwen\".into()],\n },\n ModelCatalogEntry {\n id: \"qwen-turbo\".into(),\n display_name: \"Qwen Turbo\".into(),\n provider: \"qwen\".into(),\n tier: ModelTier::Fast,\n context_window: 131_072,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.30,\n output_cost_per_m: 0.60,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"qwen-vl-plus\".into(),\n display_name: \"Qwen VL Plus\".into(),\n provider: \"qwen\".into(),\n tier: ModelTier::Smart,\n context_window: 32_768,\n max_output_tokens: 8_192,\n input_cost_per_m: 1.50,\n output_cost_per_m: 4.50,\n supports_tools: false,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"qwen-coder-plus\".into(),\n display_name: \"Qwen Coder Plus\".into(),\n provider: \"qwen\".into(),\n tier: ModelTier::Smart,\n context_window: 131_072,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.80,\n output_cost_per_m: 2.00,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"qwen-long\".into(),\n display_name: \"Qwen Long\".into(),\n provider: \"qwen\".into(),\n tier: ModelTier::Balanced,\n context_window: 1_000_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.50,\n output_cost_per_m: 2.00,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // MiniMax (3)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"minimax-text-01\".into(),\n display_name: \"MiniMax Text 01\".into(),\n provider: \"minimax\".into(),\n tier: ModelTier::Smart,\n context_window: 1_048_576,\n max_output_tokens: 16_384,\n input_cost_per_m: 1.00,\n output_cost_per_m: 3.00,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"minimax\".into()],\n },\n ModelCatalogEntry {\n id: \"MiniMax-M2.1\".into(),\n display_name: \"MiniMax M2.1\".into(),\n provider: \"minimax\".into(),\n tier: ModelTier::Smart,\n context_window: 1_048_576,\n max_output_tokens: 16_384,\n input_cost_per_m: 1.00,\n output_cost_per_m: 3.00,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"abab6.5-chat\".into(),\n display_name: \"ABAB 6.5 Chat\".into(),\n provider: \"minimax\".into(),\n tier: ModelTier::Balanced,\n context_window: 245_760,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.50,\n output_cost_per_m: 1.50,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // Zhipu AI / GLM (4)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"glm-4-plus\".into(),\n display_name: \"GLM-4 Plus\".into(),\n provider: \"zhipu\".into(),\n tier: ModelTier::Smart,\n context_window: 131_072,\n max_output_tokens: 8_192,\n input_cost_per_m: 1.50,\n output_cost_per_m: 5.00,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"glm\".into()],\n },\n ModelCatalogEntry {\n id: \"glm-4-flash\".into(),\n display_name: \"GLM-4 Flash\".into(),\n provider: \"zhipu\".into(),\n tier: ModelTier::Fast,\n context_window: 131_072,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.10,\n output_cost_per_m: 0.10,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"glm-4v-plus\".into(),\n display_name: \"GLM-4V Plus\".into(),\n provider: \"zhipu\".into(),\n tier: ModelTier::Smart,\n context_window: 8_192,\n max_output_tokens: 4_096,\n input_cost_per_m: 2.00,\n output_cost_per_m: 5.00,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"glm-4-long\".into(),\n display_name: \"GLM-4 Long\".into(),\n provider: \"zhipu\".into(),\n tier: ModelTier::Balanced,\n context_window: 1_000_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.10,\n output_cost_per_m: 0.10,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // Moonshot / Kimi (3)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"moonshot-v1-128k\".into(),\n display_name: \"Moonshot V1 128K\".into(),\n provider: \"moonshot\".into(),\n tier: ModelTier::Smart,\n context_window: 131_072,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.80,\n output_cost_per_m: 0.80,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"kimi\".into()],\n },\n ModelCatalogEntry {\n id: \"moonshot-v1-32k\".into(),\n display_name: \"Moonshot V1 32K\".into(),\n provider: \"moonshot\".into(),\n tier: ModelTier::Balanced,\n context_window: 32_768,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.30,\n output_cost_per_m: 0.30,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"moonshot-v1-8k\".into(),\n display_name: \"Moonshot V1 8K\".into(),\n provider: \"moonshot\".into(),\n tier: ModelTier::Fast,\n context_window: 8_192,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.10,\n output_cost_per_m: 0.10,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // Baidu Qianfan / ERNIE (3)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"ernie-4.5-8k\".into(),\n display_name: \"ERNIE 4.5 8K\".into(),\n provider: \"qianfan\".into(),\n tier: ModelTier::Smart,\n context_window: 8_192,\n max_output_tokens: 4_096,\n input_cost_per_m: 2.00,\n output_cost_per_m: 6.00,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![\"ernie\".into()],\n },\n ModelCatalogEntry {\n id: \"ernie-4.0-turbo-8k\".into(),\n display_name: \"ERNIE 4.0 Turbo 8K\".into(),\n provider: \"qianfan\".into(),\n tier: ModelTier::Balanced,\n context_window: 8_192,\n max_output_tokens: 4_096,\n input_cost_per_m: 1.00,\n output_cost_per_m: 3.00,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"ernie-speed-128k\".into(),\n display_name: \"ERNIE Speed 128K\".into(),\n provider: \"qianfan\".into(),\n tier: ModelTier::Fast,\n context_window: 131_072,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.0,\n output_cost_per_m: 0.0,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n // ══════════════════════════════════════════════════════════════\n // AWS Bedrock (8)\n // ══════════════════════════════════════════════════════════════\n ModelCatalogEntry {\n id: \"bedrock/anthropic.claude-opus-4-6\".into(),\n display_name: \"Claude Opus 4.6 (Bedrock)\".into(),\n provider: \"bedrock\".into(),\n tier: ModelTier::Frontier,\n context_window: 200_000,\n max_output_tokens: 128_000,\n input_cost_per_m: 5.00,\n output_cost_per_m: 25.00,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"bedrock/anthropic.claude-sonnet-4-6\".into(),\n display_name: \"Claude Sonnet 4.6 (Bedrock)\".into(),\n provider: \"bedrock\".into(),\n tier: ModelTier::Smart,\n context_window: 200_000,\n max_output_tokens: 64_000,\n input_cost_per_m: 3.00,\n output_cost_per_m: 15.00,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"bedrock/anthropic.claude-opus-4-20250514\".into(),\n display_name: \"Claude Opus 4 (Bedrock)\".into(),\n provider: \"bedrock\".into(),\n tier: ModelTier::Frontier,\n context_window: 200_000,\n max_output_tokens: 32_000,\n input_cost_per_m: 15.00,\n output_cost_per_m: 75.00,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"bedrock/anthropic.claude-sonnet-4-20250514\".into(),\n display_name: \"Claude Sonnet 4 (Bedrock)\".into(),\n provider: \"bedrock\".into(),\n tier: ModelTier::Smart,\n context_window: 200_000,\n max_output_tokens: 64_000,\n input_cost_per_m: 3.00,\n output_cost_per_m: 15.00,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"bedrock/anthropic.claude-haiku-4-5-20251001\".into(),\n display_name: \"Claude Haiku 4.5 (Bedrock)\".into(),\n provider: \"bedrock\".into(),\n tier: ModelTier::Fast,\n context_window: 200_000,\n max_output_tokens: 8_192,\n input_cost_per_m: 0.25,\n output_cost_per_m: 1.25,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"bedrock/amazon.nova-pro-v1:0\".into(),\n display_name: \"Amazon Nova Pro (Bedrock)\".into(),\n provider: \"bedrock\".into(),\n tier: ModelTier::Smart,\n context_window: 300_000,\n max_output_tokens: 5_120,\n input_cost_per_m: 0.80,\n output_cost_per_m: 3.20,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"bedrock/amazon.nova-lite-v1:0\".into(),\n display_name: \"Amazon Nova Lite (Bedrock)\".into(),\n provider: \"bedrock\".into(),\n tier: ModelTier::Fast,\n context_window: 300_000,\n max_output_tokens: 5_120,\n input_cost_per_m: 0.06,\n output_cost_per_m: 0.24,\n supports_tools: true,\n supports_vision: true,\n supports_streaming: true,\n aliases: vec![],\n },\n ModelCatalogEntry {\n id: \"bedrock/meta.llama3-3-70b-instruct-v1:0\".into(),\n display_name: \"Llama 3.3 70B (Bedrock)\".into(),\n provider: \"bedrock\".into(),\n tier: ModelTier::Balanced,\n context_window: 128_000,\n max_output_tokens: 4_096,\n input_cost_per_m: 0.72,\n output_cost_per_m: 0.72,\n supports_tools: true,\n supports_vision: false,\n supports_streaming: true,\n aliases: vec![],\n },\n ]\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_catalog_has_models() {\n let catalog = ModelCatalog::new();\n assert!(catalog.list_models().len() >= 30);\n }\n\n #[test]\n fn test_catalog_has_providers() {\n let catalog = ModelCatalog::new();\n assert_eq!(catalog.list_providers().len(), 27);\n }\n\n #[test]\n fn test_find_model_by_id() {\n let catalog = ModelCatalog::new();\n let entry = catalog.find_model(\"claude-sonnet-4-20250514\").unwrap();\n assert_eq!(entry.display_name, \"Claude Sonnet 4\");\n assert_eq!(entry.provider, \"anthropic\");\n assert_eq!(entry.tier, ModelTier::Smart);\n }\n\n #[test]\n fn test_find_model_by_alias() {\n let catalog = ModelCatalog::new();\n let entry = catalog.find_model(\"sonnet\").unwrap();\n assert_eq!(entry.id, \"claude-sonnet-4-6\");\n }\n\n #[test]\n fn test_find_model_case_insensitive() {\n let catalog = ModelCatalog::new();\n assert!(catalog.find_model(\"Claude-Sonnet-4-20250514\").is_some());\n assert!(catalog.find_model(\"SONNET\").is_some());\n }\n\n #[test]\n fn test_find_model_not_found() {\n let catalog = ModelCatalog::new();\n assert!(catalog.find_model(\"nonexistent-model\").is_none());\n }\n\n #[test]\n fn test_resolve_alias() {\n let catalog = ModelCatalog::new();\n assert_eq!(\n catalog.resolve_alias(\"sonnet\"),\n Some(\"claude-sonnet-4-6\")\n );\n assert_eq!(\n catalog.resolve_alias(\"haiku\"),\n Some(\"claude-haiku-4-5-20251001\")\n );\n assert!(catalog.resolve_alias(\"nonexistent\").is_none());\n }\n\n #[test]\n fn test_models_by_provider() {\n let catalog = ModelCatalog::new();\n let anthropic = catalog.models_by_provider(\"anthropic\");\n assert_eq!(anthropic.len(), 7);\n assert!(anthropic.iter().all(|m| m.provider == \"anthropic\"));\n }\n\n #[test]\n fn test_models_by_tier() {\n let catalog = ModelCatalog::new();\n let frontier = catalog.models_by_tier(ModelTier::Frontier);\n assert!(frontier.len() >= 3); // At least opus, gpt-4.1, gemini-2.5-pro\n assert!(frontier.iter().all(|m| m.tier == ModelTier::Frontier));\n }\n\n #[test]\n fn test_pricing_lookup() {\n let catalog = ModelCatalog::new();\n let (input, output) = catalog.pricing(\"claude-sonnet-4-20250514\").unwrap();\n assert!((input - 3.0).abs() < 0.001);\n assert!((output - 15.0).abs() < 0.001);\n }\n\n #[test]\n fn test_pricing_via_alias() {\n let catalog = ModelCatalog::new();\n let (input, output) = catalog.pricing(\"sonnet\").unwrap();\n assert!((input - 3.0).abs() < 0.001);\n assert!((output - 15.0).abs() < 0.001);\n }\n\n #[test]\n fn test_pricing_not_found() {\n let catalog = ModelCatalog::new();\n assert!(catalog.pricing(\"nonexistent\").is_none());\n }\n\n #[test]\n fn test_detect_auth_local_providers() {\n let mut catalog = ModelCatalog::new();\n catalog.detect_auth();\n // Local providers should be NotRequired\n let ollama = catalog.get_provider(\"ollama\").unwrap();\n assert_eq!(ollama.auth_status, AuthStatus::NotRequired);\n let vllm = catalog.get_provider(\"vllm\").unwrap();\n assert_eq!(vllm.auth_status, AuthStatus::NotRequired);\n }\n\n #[test]\n fn test_available_models_includes_local() {\n let mut catalog = ModelCatalog::new();\n catalog.detect_auth();\n let available = catalog.available_models();\n // Local providers (ollama, vllm, lmstudio) should always be available\n assert!(available.iter().any(|m| m.provider == \"ollama\"));\n }\n\n #[test]\n fn test_provider_model_counts() {\n let catalog = ModelCatalog::new();\n let anthropic = catalog.get_provider(\"anthropic\").unwrap();\n assert_eq!(anthropic.model_count, 7);\n let groq = catalog.get_provider(\"groq\").unwrap();\n assert_eq!(groq.model_count, 11);\n }\n\n #[test]\n fn test_list_aliases() {\n let catalog = ModelCatalog::new();\n let aliases = catalog.list_aliases();\n assert!(aliases.len() >= 20);\n assert_eq!(aliases.get(\"sonnet\").unwrap(), \"claude-sonnet-4-6\");\n // New aliases\n assert_eq!(aliases.get(\"grok\").unwrap(), \"grok-4\");\n assert_eq!(aliases.get(\"jamba\").unwrap(), \"jamba-1.5-large\");\n }\n\n #[test]\n fn test_find_grok_by_alias() {\n let catalog = ModelCatalog::new();\n let entry = catalog.find_model(\"grok\").unwrap();\n assert_eq!(entry.id, \"grok-4\");\n assert_eq!(entry.provider, \"xai\");\n }\n\n #[test]\n fn test_new_providers_in_catalog() {\n let catalog = ModelCatalog::new();\n assert!(catalog.get_provider(\"perplexity\").is_some());\n assert!(catalog.get_provider(\"cohere\").is_some());\n assert!(catalog.get_provider(\"ai21\").is_some());\n assert!(catalog.get_provider(\"cerebras\").is_some());\n assert!(catalog.get_provider(\"sambanova\").is_some());\n assert!(catalog.get_provider(\"huggingface\").is_some());\n assert!(catalog.get_provider(\"xai\").is_some());\n assert!(catalog.get_provider(\"replicate\").is_some());\n }\n\n #[test]\n fn test_xai_models() {\n let catalog = ModelCatalog::new();\n let xai = catalog.models_by_provider(\"xai\");\n assert_eq!(xai.len(), 6);\n assert!(xai.iter().any(|m| m.id == \"grok-4\"));\n assert!(xai.iter().any(|m| m.id == \"grok-4.1-fast\"));\n assert!(xai.iter().any(|m| m.id == \"grok-3\"));\n assert!(xai.iter().any(|m| m.id == \"grok-3-mini\"));\n assert!(xai.iter().any(|m| m.id == \"grok-2\"));\n assert!(xai.iter().any(|m| m.id == \"grok-2-mini\"));\n }\n\n #[test]\n fn test_perplexity_models() {\n let catalog = ModelCatalog::new();\n let pp = catalog.models_by_provider(\"perplexity\");\n assert_eq!(pp.len(), 4);\n }\n\n #[test]\n fn test_cohere_models() {\n let catalog = ModelCatalog::new();\n let co = catalog.models_by_provider(\"cohere\");\n assert_eq!(co.len(), 4);\n }\n\n #[test]\n fn test_default_creates_valid_catalog() {\n let catalog = ModelCatalog::default();\n assert!(!catalog.list_models().is_empty());\n assert!(!catalog.list_providers().is_empty());\n }\n\n #[test]\n fn test_merge_adds_new_models() {\n let mut catalog = ModelCatalog::new();\n let before = catalog.models_by_provider(\"ollama\").len();\n catalog.merge_discovered_models(\n \"ollama\",\n &[\"codestral:latest\".to_string(), \"qwen2:7b\".to_string()],\n );\n let after = catalog.models_by_provider(\"ollama\").len();\n assert_eq!(after, before + 2);\n // Verify the new models are Local tier with zero cost\n let qwen = catalog.find_model(\"qwen2:7b\").unwrap();\n assert_eq!(qwen.tier, ModelTier::Local);\n assert!((qwen.input_cost_per_m).abs() < f64::EPSILON);\n }\n\n #[test]\n fn test_merge_skips_existing() {\n let mut catalog = ModelCatalog::new();\n // \"llama3.2\" is already a builtin Ollama model\n let before = catalog.list_models().len();\n catalog.merge_discovered_models(\"ollama\", &[\"llama3.2\".to_string()]);\n let after = catalog.list_models().len();\n assert_eq!(after, before); // no new model added\n }\n\n #[test]\n fn test_merge_updates_model_count() {\n let mut catalog = ModelCatalog::new();\n let before_count = catalog.get_provider(\"ollama\").unwrap().model_count;\n catalog.merge_discovered_models(\"ollama\", &[\"new-model:latest\".to_string()]);\n let after_count = catalog.get_provider(\"ollama\").unwrap().model_count;\n assert_eq!(after_count, before_count + 1);\n }\n\n #[test]\n fn test_chinese_providers_in_catalog() {\n let catalog = ModelCatalog::new();\n assert!(catalog.get_provider(\"qwen\").is_some());\n assert!(catalog.get_provider(\"minimax\").is_some());\n assert!(catalog.get_provider(\"zhipu\").is_some());\n assert!(catalog.get_provider(\"moonshot\").is_some());\n assert!(catalog.get_provider(\"qianfan\").is_some());\n assert!(catalog.get_provider(\"bedrock\").is_some());\n }\n\n #[test]\n fn test_chinese_model_aliases() {\n let catalog = ModelCatalog::new();\n assert!(catalog.find_model(\"kimi\").is_some());\n assert!(catalog.find_model(\"glm\").is_some());\n assert!(catalog.find_model(\"ernie\").is_some());\n assert!(catalog.find_model(\"minimax\").is_some());\n }\n\n #[test]\n fn test_bedrock_models() {\n let catalog = ModelCatalog::new();\n let bedrock = catalog.models_by_provider(\"bedrock\");\n assert_eq!(bedrock.len(), 8);\n }\n\n #[test]\n fn test_set_provider_url() {\n let mut catalog = ModelCatalog::new();\n let old_url = catalog.get_provider(\"ollama\").unwrap().base_url.clone();\n assert_eq!(old_url, OLLAMA_BASE_URL);\n\n let updated = catalog.set_provider_url(\"ollama\", \"http://192.168.1.100:11434/v1\");\n assert!(updated);\n assert_eq!(\n catalog.get_provider(\"ollama\").unwrap().base_url,\n \"http://192.168.1.100:11434/v1\"\n );\n }\n\n #[test]\n fn test_set_provider_url_unknown() {\n let mut catalog = ModelCatalog::new();\n let updated = catalog.set_provider_url(\"nonexistent\", \"http://localhost:9999\");\n assert!(!updated);\n }\n\n #[test]\n fn test_apply_url_overrides() {\n let mut catalog = ModelCatalog::new();\n let mut overrides = HashMap::new();\n overrides.insert(\"ollama\".to_string(), \"http://10.0.0.5:11434/v1\".to_string());\n overrides.insert(\"vllm\".to_string(), \"http://10.0.0.6:8000/v1\".to_string());\n overrides.insert(\"nonexistent\".to_string(), \"http://nowhere\".to_string());\n\n catalog.apply_url_overrides(&overrides);\n\n assert_eq!(\n catalog.get_provider(\"ollama\").unwrap().base_url,\n \"http://10.0.0.5:11434/v1\"\n );\n assert_eq!(\n catalog.get_provider(\"vllm\").unwrap().base_url,\n \"http://10.0.0.6:8000/v1\"\n );\n // lmstudio should be unchanged\n assert_eq!(\n catalog.get_provider(\"lmstudio\").unwrap().base_url,\n LMSTUDIO_BASE_URL\n );\n }\n}\n"}}, "crates/openfang-cli/src/launcher.rs": {"6b3a9982fff3902a62ec88e244ff8596db7f7734": {"t": 1785745863.844454, "n": 19539, "h": "34b134a1bb5ca94b", "c": "//! Interactive launcher — lightweight Ratatui one-shot menu.\n//!\n//! Shown when `openfang` is run with no subcommand in a TTY.\n//! Full-width left-aligned layout, adapts for first-time vs returning users.\n\nuse ratatui::crossterm::event::{self, Event as CtEvent, KeyCode, KeyEventKind};\nuse ratatui::layout::{Constraint, Layout, Rect};\nuse ratatui::style::{Modifier, Style};\nuse ratatui::text::{Line, Span};\nuse ratatui::widgets::{List, ListItem, ListState, Paragraph};\n\nuse crate::tui::theme;\nuse crate::ui;\nuse std::path::PathBuf;\nuse std::time::Duration;\n\n// ── Provider detection ──────────────────────────────────────────────────────\n\nconst PROVIDER_ENV_VARS: &[(&str, &str)] = &[\n (\"ANTHROPIC_API_KEY\", \"Anthropic\"),\n (\"OPENAI_API_KEY\", \"OpenAI\"),\n (\"DEEPSEEK_API_KEY\", \"DeepSeek\"),\n (\"GEMINI_API_KEY\", \"Gemini\"),\n (\"GOOGLE_API_KEY\", \"Gemini\"),\n (\"GROQ_API_KEY\", \"Groq\"),\n (\"OPENROUTER_API_KEY\", \"OpenRouter\"),\n (\"TOGETHER_API_KEY\", \"Together\"),\n (\"MISTRAL_API_KEY\", \"Mistral\"),\n (\"FIREWORKS_API_KEY\", \"Fireworks\"),\n];\n\nfn detect_provider() -> Option<(&'static str, &'static str)> {\n for &(var, name) in PROVIDER_ENV_VARS {\n if std::env::var(var).is_ok() {\n return Some((name, var));\n }\n }\n None\n}\n\nfn is_first_run() -> bool {\n let home = match dirs::home_dir() {\n Some(h) => h,\n None => return true,\n };\n !home.join(\".openfang\").join(\"config.toml\").exists()\n}\n\nfn has_openclaw() -> bool {\n // Quick check: does ~/.openclaw exist?\n dirs::home_dir()\n .map(|h| h.join(\".openclaw\").exists())\n .unwrap_or(false)\n}\n\n// ── Types ───────────────────────────────────────────────────────────────────\n\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum LauncherChoice {\n GetStarted,\n Chat,\n Dashboard,\n DesktopApp,\n TerminalUI,\n ShowHelp,\n Quit,\n}\n\nstruct MenuItem {\n label: &'static str,\n hint: &'static str,\n choice: LauncherChoice,\n}\n\n// Menu for first-time users: \"Get started\" is first and prominent\nconst MENU_FIRST_RUN: &[MenuItem] = &[\n MenuItem {\n label: \"Get started\",\n hint: \"Providers, API keys, models, migration\",\n choice: LauncherChoice::GetStarted,\n },\n MenuItem {\n label: \"Chat with an agent\",\n hint: \"Quick chat in the terminal\",\n choice: LauncherChoice::Chat,\n },\n MenuItem {\n label: \"Open dashboard\",\n hint: \"Launch the web UI in your browser\",\n choice: LauncherChoice::Dashboard,\n },\n MenuItem {\n label: \"Open desktop app\",\n hint: \"Launch the native desktop app\",\n choice: LauncherChoice::DesktopApp,\n },\n MenuItem {\n label: \"Launch terminal UI\",\n hint: \"Full interactive TUI dashboard\",\n choice: LauncherChoice::TerminalUI,\n },\n MenuItem {\n label: \"Show all commands\",\n hint: \"Print full --help output\",\n choice: LauncherChoice::ShowHelp,\n },\n];\n\n// Menu for returning users: action-first, setup at the bottom\nconst MENU_RETURNING: &[MenuItem] = &[\n MenuItem {\n label: \"Chat with an agent\",\n hint: \"Quick chat in the terminal\",\n choice: LauncherChoice::Chat,\n },\n MenuItem {\n label: \"Open dashboard\",\n hint: \"Launch the web UI in your browser\",\n choice: LauncherChoice::Dashboard,\n },\n MenuItem {\n label: \"Launch terminal UI\",\n hint: \"Full interactive TUI dashboard\",\n choice: LauncherChoice::TerminalUI,\n },\n MenuItem {\n label: \"Open desktop app\",\n hint: \"Launch the native desktop app\",\n choice: LauncherChoice::DesktopApp,\n },\n MenuItem {\n label: \"Settings\",\n hint: \"Providers, API keys, models, routing\",\n choice: LauncherChoice::GetStarted,\n },\n MenuItem {\n label: \"Show all commands\",\n hint: \"Print full --help output\",\n choice: LauncherChoice::ShowHelp,\n },\n];\n\n// ── Launcher state ──────────────────────────────────────────────────────────\n\nstruct LauncherState {\n list: ListState,\n daemon_url: Option<String>,\n daemon_agents: u64,\n detecting: bool,\n tick: usize,\n first_run: bool,\n openclaw_detected: bool,\n}\n\nimpl LauncherState {\n fn new() -> Self {\n let first_run = is_first_run();\n let openclaw_detected = first_run && has_openclaw();\n let mut list = ListState::default();\n list.select(Some(0));\n Self {\n list,\n daemon_url: None,\n daemon_agents: 0,\n detecting: true,\n tick: 0,\n first_run,\n openclaw_detected,\n }\n }\n\n fn menu(&self) -> &'static [MenuItem] {\n if self.first_run {\n MENU_FIRST_RUN\n } else {\n MENU_RETURNING\n }\n }\n}\n\n// ── Entry point ─────────────────────────────────────────────────────────────\n\npub fn run(_config: Option<PathBuf>) -> LauncherChoice {\n let mut terminal = ratatui::init();\n\n // Panic hook: restore terminal on panic (set AFTER init succeeds)\n let original_hook = std::panic::take_hook();\n std::panic::set_hook(Box::new(move |info| {\n let _ = ratatui::try_restore();\n original_hook(info);\n }));\n\n let mut state = LauncherState::new();\n\n // Spawn background daemon detection (catch_unwind protects against thread panics)\n let (daemon_tx, daemon_rx) = std::sync::mpsc::channel();\n std::thread::spawn(move || {\n let _ = std::panic::catch_unwind(|| {\n let result = crate::find_daemon();\n let agent_count = result.as_ref().map_or(0, |base| {\n let client = reqwest::blocking::Client::builder()\n .timeout(Duration::from_secs(2))\n .build()\n .ok();\n client\n .and_then(|c| c.get(format!(\"{base}/api/agents\")).send().ok())\n .and_then(|r| r.json::<serde_json::Value>().ok())\n .and_then(|v| v.as_array().map(|a| a.len() as u64))\n .unwrap_or(0)\n });\n let _ = daemon_tx.send((result, agent_count));\n });\n });\n\n let choice;\n\n loop {\n // Check for daemon detection result\n if state.detecting {\n if let Ok((url, agents)) = daemon_rx.try_recv() {\n state.daemon_url = url;\n state.daemon_agents = agents;\n state.detecting = false;\n }\n }\n\n state.tick = state.tick.wrapping_add(1);\n\n // Draw (gracefully handle render failures)\n if terminal.draw(|frame| draw(frame, &mut state)).is_err() {\n choice = LauncherChoice::Quit;\n break;\n }\n\n // Poll for input (50ms = 20fps spinner)\n if event::poll(Duration::from_millis(50)).unwrap_or(false) {\n if let Ok(CtEvent::Key(key)) = event::read() {\n if key.kind != KeyEventKind::Press {\n continue;\n }\n let menu = state.menu();\n if menu.is_empty() {\n choice = LauncherChoice::Quit;\n break;\n }\n match key.code {\n KeyCode::Char('q') | KeyCode::Esc => {\n choice = LauncherChoice::Quit;\n break;\n }\n KeyCode::Up | KeyCode::Char('k') => {\n let i = state.list.selected().unwrap_or(0);\n let next = if i == 0 { menu.len() - 1 } else { i - 1 };\n state.list.select(Some(next));\n }\n KeyCode::Down | KeyCode::Char('j') => {\n let i = state.list.selected().unwrap_or(0);\n let next = (i + 1) % menu.len();\n state.list.select(Some(next));\n }\n KeyCode::Enter => {\n if let Some(i) = state.list.selected() {\n if i < menu.len() {\n choice = menu[i].choice;\n break;\n }\n }\n }\n _ => {}\n }\n }\n }\n }\n\n let _ = ratatui::try_restore();\n choice\n}\n\n// ── Drawing ─────────────────────────────────────────────────────────────────\n\n/// Left margin for content alignment.\nconst MARGIN_LEFT: u16 = 3;\n\n/// Constrain content to a readable area within the terminal.\nfn content_area(area: Rect) -> Rect {\n if area.width < 10 || area.height < 5 {\n // Terminal too small — use full area with no margin\n return area;\n }\n let margin = MARGIN_LEFT.min(area.width.saturating_sub(10));\n let w = 80u16.min(area.width.saturating_sub(margin));\n Rect {\n x: area.x.saturating_add(margin),\n y: area.y,\n width: w,\n height: area.height,\n }\n}\n\nfn draw(frame: &mut ratatui::Frame, state: &mut LauncherState) {\n let area = frame.area();\n\n // Fill background\n frame.render_widget(\n ratatui::widgets::Block::default().style(Style::default().bg(theme::BG_PRIMARY)),\n area,\n );\n\n let content = content_area(area);\n let version = env!(\"CARGO_PKG_VERSION\");\n let has_provider = detect_provider().is_some();\n let menu = state.menu();\n\n // Compute dynamic heights\n let header_h: u16 = if state.first_run { 3 } else { 1 }; // welcome text or just title\n let status_h: u16 = if state.detecting {\n 1\n } else if has_provider {\n 2\n } else {\n 3\n };\n let migration_hint_h: u16 = if state.first_run && state.openclaw_detected {\n 2\n } else {\n 0\n };\n let menu_h = menu.len() as u16;\n\n let total_needed = 1 + header_h + 1 + status_h + 1 + menu_h + migration_hint_h + 1;\n\n // Vertical centering: place content block in the upper-third area\n let top_pad = if area.height > total_needed + 2 {\n ((area.height - total_needed) / 3).max(1)\n } else {\n 1\n };\n\n let chunks = Layout::vertical([\n Constraint::Length(top_pad), // top space\n Constraint::Length(header_h), // header / welcome\n Constraint::Length(1), // separator\n Constraint::Length(status_h), // status indicators\n Constraint::Length(1), // separator\n Constraint::Length(menu_h), // menu items\n Constraint::Length(migration_hint_h), // openclaw migration hint (if any)\n Constraint::Length(1), // keybind hints\n Constraint::Min(0), // remaining space\n ])\n .split(content);\n\n // ── Header ──────────────────────────────────────────────────────────────\n if state.first_run {\n let header_lines = vec![\n Line::from(vec![\n Span::styled(\n \"OpenFang\",\n Style::default()\n .fg(theme::ACCENT)\n .add_modifier(Modifier::BOLD),\n ),\n Span::styled(\n format!(\" v{version}\"),\n Style::default().fg(theme::TEXT_TERTIARY),\n ),\n ]),\n Line::from(\"\"),\n Line::from(vec![Span::styled(\n \"Welcome! Let's get you set up.\",\n Style::default().fg(theme::TEXT_PRIMARY),\n )]),\n ];\n frame.render_widget(Paragraph::new(header_lines), chunks[1]);\n } else {\n let header = Line::from(vec![\n Span::styled(\n \"OpenFang\",\n Style::default()\n .fg(theme::ACCENT)\n .add_modifier(Modifier::BOLD),\n ),\n Span::styled(\n format!(\" v{version}\"),\n Style::default().fg(theme::TEXT_TERTIARY),\n ),\n ]);\n frame.render_widget(Paragraph::new(header), chunks[1]);\n }\n\n // ── Separator ───────────────────────────────────────────────────────────\n render_separator(frame, chunks[2]);\n\n // ── Status block ────────────────────────────────────────────────────────\n if state.detecting {\n let spinner = theme::SPINNER_FRAMES[state.tick % theme::SPINNER_FRAMES.len()];\n let line = Line::from(vec![\n Span::styled(format!(\"{spinner} \"), Style::default().fg(theme::YELLOW)),\n Span::styled(\"Checking for daemon\\u{2026}\", theme::dim_style()),\n ]);\n frame.render_widget(Paragraph::new(line), chunks[3]);\n } else {\n let mut lines: Vec<Line> = Vec::new();\n\n // Daemon status\n if let Some(ref url) = state.daemon_url {\n let agent_suffix = if state.daemon_agents > 0 {\n format!(\n \" ({} agent{})\",\n state.daemon_agents,\n if state.daemon_agents == 1 { \"\" } else { \"s\" }\n )\n } else {\n String::new()\n };\n lines.push(Line::from(vec![\n Span::styled(\n \"\\u{25cf} \",\n Style::default()\n .fg(theme::GREEN)\n .add_modifier(Modifier::BOLD),\n ),\n Span::styled(\n format!(\"Daemon running at {url}\"),\n Style::default().fg(theme::TEXT_PRIMARY),\n ),\n Span::styled(agent_suffix, Style::default().fg(theme::GREEN)),\n ]));\n } else {\n lines.push(Line::from(vec![\n Span::styled(\"\\u{25cb} \", theme::dim_style()),\n Span::styled(\"No daemon running\", theme::dim_style()),\n ]));\n }\n\n // Provider status\n if let Some((provider, env_var)) = detect_provider() {\n lines.push(Line::from(vec![\n Span::styled(\n \"\\u{2714} \",\n Style::default()\n .fg(theme::GREEN)\n .add_modifier(Modifier::BOLD),\n ),\n Span::styled(\n format!(\"Provider: {provider}\"),\n Style::default().fg(theme::TEXT_PRIMARY),\n ),\n Span::styled(format!(\" ({env_var})\"), theme::dim_style()),\n ]));\n } else {\n lines.push(Line::from(vec![\n Span::styled(\"\\u{25cb} \", Style::default().fg(theme::YELLOW)),\n Span::styled(\"No API keys detected\", Style::default().fg(theme::YELLOW)),\n ]));\n if !state.first_run {\n lines.push(Line::from(vec![Span::styled(\n \" Run 'Re-run setup' to configure a provider\",\n theme::hint_style(),\n )]));\n } else {\n lines.push(Line::from(vec![Span::styled(\n \" Select 'Get started' to configure\",\n theme::hint_style(),\n )]));\n }\n }\n\n frame.render_widget(Paragraph::new(lines), chunks[3]);\n }\n\n // ── Separator 2 ─────────────────────────────────────────────────────────\n render_separator(frame, chunks[4]);\n\n // ── Menu ────────────────────────────────────────────────────────────────\n let items: Vec<ListItem> = menu\n .iter()\n .enumerate()\n .map(|(i, item)| {\n // Highlight \"Get started\" for first-run users\n let is_primary = state.first_run && i == 0;\n let label_style = if is_primary {\n Style::default()\n .fg(theme::ACCENT)\n .add_modifier(Modifier::BOLD)\n } else {\n Style::default().fg(theme::TEXT_PRIMARY)\n };\n\n ListItem::new(Line::from(vec![\n Span::styled(format!(\"{:<26}\", item.label), label_style),\n Span::styled(item.hint, theme::dim_style()),\n ]))\n })\n .collect();\n\n let list = List::new(items)\n .highlight_style(\n Style::default()\n .fg(theme::ACCENT)\n .bg(theme::BG_HOVER)\n .add_modifier(Modifier::BOLD),\n )\n .highlight_symbol(\"\\u{25b8} \");\n\n frame.render_stateful_widget(list, chunks[5], &mut state.list);\n\n // ── OpenClaw migration hint ─────────────────────────────────────────────\n if state.first_run && state.openclaw_detected {\n let hint_lines = vec![\n Line::from(\"\"),\n Line::from(vec![\n Span::styled(\"\\u{2192} \", Style::default().fg(theme::BLUE)),\n Span::styled(\"Coming from OpenClaw? \", Style::default().fg(theme::BLUE)),\n Span::styled(\n \"'Get started' includes automatic migration.\",\n theme::hint_style(),\n ),\n ]),\n ];\n frame.render_widget(Paragraph::new(hint_lines), chunks[6]);\n }\n\n // ── Keybind hints ───────────────────────────────────────────────────────\n let hints = Line::from(vec![Span::styled(\n \"\\u{2191}\\u{2193} navigate enter select q quit\",\n theme::hint_style(),\n )]);\n frame.render_widget(Paragraph::new(hints), chunks[7]);\n}\n\nfn render_separator(frame: &mut ratatui::Frame, area: Rect) {\n let w = (area.width as usize).min(60);\n let line = Line::from(vec![Span::styled(\n \"\\u{2500}\".repeat(w),\n Style::default().fg(theme::BORDER),\n )]);\n frame.render_widget(Paragraph::new(line), area);\n}\n\n// ── Desktop app launcher ────────────────────────────────────────────────────\n\npub fn launch_desktop_app() {\n let desktop_bin = {\n let exe = std::env::current_exe().ok();\n let dir = exe.as_ref().and_then(|e| e.parent());\n\n #[cfg(windows)]\n let name = \"openfang-desktop.exe\";\n #[cfg(not(windows))]\n let name = \"openfang-desktop\";\n\n // Check sibling of current exe first\n let sibling = dir.map(|d| d.join(name));\n\n match sibling {\n Some(ref path) if path.exists() => sibling,\n _ => which_lookup(name),\n }\n };\n\n match desktop_bin {\n Some(ref path) if path.exists() => {\n match std::process::Command::new(path)\n .stdin(std::process::Stdio::null())\n .stdout(std::process::Stdio::null())\n .stderr(std::process::Stdio::null())\n .spawn()\n {\n Ok(_) => {\n ui::success(\"Desktop app launched.\");\n }\n Err(e) => {\n ui::error_with_fix(\n &format!(\"Failed to launch desktop app: {e}\"),\n \"Build it: cargo build -p openfang-desktop\",\n );\n }\n }\n }\n _ => {\n ui::error_with_fix(\n \"Desktop app not found\",\n \"Build it: cargo build -p openfang-desktop\",\n );\n }\n }\n}\n\n/// Simple PATH lookup for a binary name.\nfn which_lookup(name: &str) -> Option<PathBuf> {\n let path_var = std::env::var(\"PATH\").ok()?;\n let separator = if cfg!(windows) { ';' } else { ':' };\n for dir in path_var.split(separator) {\n let candidate = PathBuf::from(dir).join(name);\n if candidate.exists() {\n return Some(candidate);\n }\n }\n None\n}\n"}}, "crates/openfang-cli/src/tui/screens/wizard.rs": {"6b3a9982fff3902a62ec88e244ff8596db7f7734": {"t": 1785745865.1733992, "n": 18466, "h": "0b3a0ef2ef71a9c0", "c": "//! Setup wizard: provider list → API key → model → config save.\n\nuse ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};\nuse ratatui::layout::{Constraint, Layout, Rect};\nuse ratatui::style::{Modifier, Style};\nuse ratatui::text::{Line, Span};\nuse ratatui::widgets::{List, ListItem, ListState, Paragraph};\nuse ratatui::Frame;\nuse std::path::PathBuf;\n\nuse crate::tui::theme;\n\n/// Provider metadata for the setup wizard.\nstruct ProviderInfo {\n name: &'static str,\n env_var: &'static str,\n default_model: &'static str,\n needs_key: bool,\n}\n\nconst PROVIDERS: &[ProviderInfo] = &[\n ProviderInfo {\n name: \"groq\",\n env_var: \"GROQ_API_KEY\",\n default_model: \"llama-3.3-70b-versatile\",\n needs_key: true,\n },\n ProviderInfo {\n name: \"anthropic\",\n env_var: \"ANTHROPIC_API_KEY\",\n default_model: \"claude-sonnet-4-20250514\",\n needs_key: true,\n },\n ProviderInfo {\n name: \"openai\",\n env_var: \"OPENAI_API_KEY\",\n default_model: \"gpt-4o\",\n needs_key: true,\n },\n ProviderInfo {\n name: \"openrouter\",\n env_var: \"OPENROUTER_API_KEY\",\n default_model: \"anthropic/claude-sonnet-4-20250514\",\n needs_key: true,\n },\n ProviderInfo {\n name: \"deepseek\",\n env_var: \"DEEPSEEK_API_KEY\",\n default_model: \"deepseek-chat\",\n needs_key: true,\n },\n ProviderInfo {\n name: \"together\",\n env_var: \"TOGETHER_API_KEY\",\n default_model: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n needs_key: true,\n },\n ProviderInfo {\n name: \"mistral\",\n env_var: \"MISTRAL_API_KEY\",\n default_model: \"mistral-large-latest\",\n needs_key: true,\n },\n ProviderInfo {\n name: \"fireworks\",\n env_var: \"FIREWORKS_API_KEY\",\n default_model: \"accounts/fireworks/models/llama-v3p3-70b-instruct\",\n needs_key: true,\n },\n ProviderInfo {\n name: \"ollama\",\n env_var: \"OLLAMA_API_KEY\",\n default_model: \"llama3.2\",\n needs_key: false,\n },\n ProviderInfo {\n name: \"vllm\",\n env_var: \"VLLM_API_KEY\",\n default_model: \"local-model\",\n needs_key: false,\n },\n ProviderInfo {\n name: \"lmstudio\",\n env_var: \"LMSTUDIO_API_KEY\",\n default_model: \"local-model\",\n needs_key: false,\n },\n];\n\n/// Check if first-run setup is needed.\npub fn needs_setup() -> bool {\n let home = match dirs::home_dir() {\n Some(h) => h,\n None => return true,\n };\n !home.join(\".openfang\").join(\"config.toml\").exists()\n}\n\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WizardStep {\n Provider,\n ApiKey,\n Model,\n Saving,\n Done,\n}\n\npub struct WizardState {\n pub step: WizardStep,\n pub provider_list: ListState,\n pub provider_order: Vec<usize>, // indices into PROVIDERS, detected first\n pub selected_provider: Option<usize>, // index into PROVIDERS\n pub api_key_input: String,\n pub api_key_from_env: bool,\n pub model_input: String,\n pub status_msg: String,\n pub created_config: Option<PathBuf>,\n}\n\nimpl WizardState {\n pub fn new() -> Self {\n let mut state = Self {\n step: WizardStep::Provider,\n provider_list: ListState::default(),\n provider_order: Vec::new(),\n selected_provider: None,\n api_key_input: String::new(),\n api_key_from_env: false,\n model_input: String::new(),\n status_msg: String::new(),\n created_config: None,\n };\n state.build_provider_order();\n state.provider_list.select(Some(0));\n state\n }\n\n pub fn reset(&mut self) {\n self.step = WizardStep::Provider;\n self.selected_provider = None;\n self.api_key_input.clear();\n self.api_key_from_env = false;\n self.model_input.clear();\n self.status_msg.clear();\n self.created_config = None;\n self.build_provider_order();\n self.provider_list.select(Some(0));\n }\n\n fn build_provider_order(&mut self) {\n self.provider_order.clear();\n // Detected providers first\n for (i, p) in PROVIDERS.iter().enumerate() {\n if std::env::var(p.env_var).is_ok() {\n self.provider_order.push(i);\n }\n }\n // Then the rest\n for (i, p) in PROVIDERS.iter().enumerate() {\n if std::env::var(p.env_var).is_err() {\n self.provider_order.push(i);\n }\n }\n }\n\n fn selected_provider_info(&self) -> Option<&'static ProviderInfo> {\n self.selected_provider.map(|i| &PROVIDERS[i])\n }\n\n /// Handle a key event. Returns true if wizard is complete or cancelled.\n /// `cancelled` is set if the user backed out entirely.\n pub fn handle_key(&mut self, key: KeyEvent) -> WizardResult {\n if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {\n return WizardResult::Cancelled;\n }\n\n match self.step {\n WizardStep::Provider => self.handle_provider(key),\n WizardStep::ApiKey => self.handle_api_key(key),\n WizardStep::Model => self.handle_model(key),\n WizardStep::Saving | WizardStep::Done => WizardResult::Continue,\n }\n }\n\n fn handle_provider(&mut self, key: KeyEvent) -> WizardResult {\n match key.code {\n KeyCode::Esc => return WizardResult::Cancelled,\n KeyCode::Up | KeyCode::Char('k') => {\n let i = self.provider_list.selected().unwrap_or(0);\n let next = if i == 0 {\n self.provider_order.len() - 1\n } else {\n i - 1\n };\n self.provider_list.select(Some(next));\n }\n KeyCode::Down | KeyCode::Char('j') => {\n let i = self.provider_list.selected().unwrap_or(0);\n let next = (i + 1) % self.provider_order.len();\n self.provider_list.select(Some(next));\n }\n KeyCode::Enter => {\n if let Some(list_idx) = self.provider_list.selected() {\n let Some(&prov_idx) = self.provider_order.get(list_idx) else {\n return WizardResult::Continue;\n };\n let Some(p) = PROVIDERS.get(prov_idx) else {\n return WizardResult::Continue;\n };\n self.selected_provider = Some(prov_idx);\n\n if !p.needs_key {\n // No key needed, skip to model\n self.api_key_from_env = false;\n self.model_input = p.default_model.to_string();\n self.step = WizardStep::Model;\n } else if std::env::var(p.env_var).is_ok() {\n // Key already in env\n self.api_key_from_env = true;\n self.model_input = p.default_model.to_string();\n self.step = WizardStep::Model;\n } else {\n self.api_key_from_env = false;\n self.api_key_input.clear();\n self.step = WizardStep::ApiKey;\n }\n }\n }\n _ => {}\n }\n WizardResult::Continue\n }\n\n fn handle_api_key(&mut self, key: KeyEvent) -> WizardResult {\n match key.code {\n KeyCode::Esc => {\n self.step = WizardStep::Provider;\n }\n KeyCode::Enter => {\n if !self.api_key_input.is_empty() {\n if let Some(p) = self.selected_provider_info() {\n self.model_input = p.default_model.to_string();\n }\n self.step = WizardStep::Model;\n }\n }\n KeyCode::Char(c) => {\n self.api_key_input.push(c);\n }\n KeyCode::Backspace => {\n self.api_key_input.pop();\n }\n _ => {}\n }\n WizardResult::Continue\n }\n\n fn handle_model(&mut self, key: KeyEvent) -> WizardResult {\n match key.code {\n KeyCode::Esc => {\n // Go back\n if let Some(p) = self.selected_provider_info() {\n if p.needs_key && !self.api_key_from_env {\n self.step = WizardStep::ApiKey;\n } else {\n self.step = WizardStep::Provider;\n }\n } else {\n self.step = WizardStep::Provider;\n }\n }\n KeyCode::Enter => {\n self.step = WizardStep::Saving;\n self.save_config();\n }\n KeyCode::Char(c) => {\n self.model_input.push(c);\n }\n KeyCode::Backspace => {\n self.model_input.pop();\n }\n _ => {}\n }\n WizardResult::Continue\n }\n\n fn save_config(&mut self) {\n let p = match self.selected_provider_info() {\n Some(p) => p,\n None => {\n self.status_msg = \"No provider selected\".to_string();\n self.step = WizardStep::Provider;\n return;\n }\n };\n\n let home = match dirs::home_dir() {\n Some(h) => h,\n None => {\n self.status_msg = \"Could not determine home directory\".to_string();\n self.step = WizardStep::Done;\n return;\n }\n };\n\n let openfang_dir = home.join(\".openfang\");\n let _ = std::fs::create_dir_all(openfang_dir.join(\"agents\"));\n let _ = std::fs::create_dir_all(openfang_dir.join(\"data\"));\n crate::restrict_dir_permissions(&openfang_dir);\n\n let api_key_line = if !self.api_key_input.is_empty() {\n format!(\"api_key = \\\"{}\\\"\", self.api_key_input)\n } else {\n format!(\"api_key_env = \\\"{}\\\"\", p.env_var)\n };\n\n let model = if self.model_input.is_empty() {\n p.default_model\n } else {\n &self.model_input\n };\n\n let config = format!(\n r#\"# OpenFang Agent OS configuration\n# Generated by setup wizard\n\n[default_model]\nprovider = \"{provider}\"\nmodel = \"{model}\"\n{api_key_line}\n\n[memory]\ndecay_rate = 0.05\n\n[network]\nlisten_addr = \"127.0.0.1:4200\"\n\"#,\n provider = p.name,\n );\n\n let config_path = openfang_dir.join(\"config.toml\");\n match std::fs::write(&config_path, &config) {\n Ok(()) => {\n crate::restrict_file_permissions(&config_path);\n self.status_msg = format!(\"Config saved \\u{2014} {} / {}\", p.name, model);\n self.created_config = Some(config_path);\n }\n Err(e) => {\n self.status_msg = format!(\"Failed to save config: {e}\");\n }\n }\n self.step = WizardStep::Done;\n }\n}\n\npub enum WizardResult {\n Continue,\n Cancelled,\n}\n\n/// Render the wizard screen.\npub fn draw(f: &mut Frame, area: Rect, state: &mut WizardState) {\n // Fill background\n f.render_widget(\n ratatui::widgets::Block::default().style(Style::default().bg(theme::BG_PRIMARY)),\n area,\n );\n\n let step_label = match state.step {\n WizardStep::Provider => \"Step 1 of 3\",\n WizardStep::ApiKey => \"Step 2 of 3\",\n WizardStep::Model => \"Step 3 of 3\",\n WizardStep::Saving => \"Saving...\",\n WizardStep::Done => \"Complete\",\n };\n\n // Left-aligned content area\n let content = if area.width < 10 || area.height < 5 {\n area\n } else {\n let margin = 3u16.min(area.width.saturating_sub(10));\n let w = 72u16.min(area.width.saturating_sub(margin));\n Rect {\n x: area.x.saturating_add(margin),\n y: area.y,\n width: w,\n height: area.height,\n }\n };\n\n let chunks = Layout::vertical([\n Constraint::Length(1), // top pad\n Constraint::Length(1), // header\n Constraint::Length(1), // separator\n Constraint::Min(1), // step content\n ])\n .split(content);\n\n // Header\n let header = Line::from(vec![\n Span::styled(\n \"Setup\",\n Style::default()\n .fg(theme::ACCENT)\n .add_modifier(Modifier::BOLD),\n ),\n Span::styled(format!(\" {step_label}\"), theme::dim_style()),\n ]);\n f.render_widget(Paragraph::new(header), chunks[1]);\n\n // Separator\n let sep_w = content.width.min(60) as usize;\n let sep = Line::from(vec![Span::styled(\n \"\\u{2500}\".repeat(sep_w),\n Style::default().fg(theme::BORDER),\n )]);\n f.render_widget(Paragraph::new(sep), chunks[2]);\n\n match state.step {\n WizardStep::Provider => draw_provider(f, chunks[3], state),\n WizardStep::ApiKey => draw_api_key(f, chunks[3], state),\n WizardStep::Model => draw_model(f, chunks[3], state),\n WizardStep::Saving | WizardStep::Done => draw_done(f, chunks[3], state),\n }\n}\n\nfn draw_provider(f: &mut Frame, area: Rect, state: &mut WizardState) {\n let chunks = Layout::vertical([\n Constraint::Length(2), // prompt\n Constraint::Min(3), // list\n Constraint::Length(1), // hints\n ])\n .split(area);\n\n let prompt = Paragraph::new(Line::from(vec![Span::raw(\" Choose your LLM provider:\")]));\n f.render_widget(prompt, chunks[0]);\n\n let items: Vec<ListItem> = state\n .provider_order\n .iter()\n .map(|&idx| {\n let p = &PROVIDERS[idx];\n let hint = if !p.needs_key {\n \"local, no key needed\".to_string()\n } else if std::env::var(p.env_var).is_ok() {\n format!(\"{} detected\", p.env_var)\n } else {\n format!(\"requires {}\", p.env_var)\n };\n ListItem::new(Line::from(vec![\n Span::raw(format!(\" {:<14}\", p.name)),\n Span::styled(hint, theme::dim_style()),\n ]))\n })\n .collect();\n\n let list = List::new(items)\n .highlight_style(\n Style::default()\n .fg(theme::ACCENT)\n .bg(theme::BG_HOVER)\n .add_modifier(Modifier::BOLD),\n )\n .highlight_symbol(\"\\u{25b8} \");\n\n f.render_stateful_widget(list, chunks[1], &mut state.provider_list);\n\n let hints = Paragraph::new(Line::from(vec![Span::styled(\n \" [\\u{2191}\\u{2193}] Navigate [Enter] Select [Esc] Cancel\",\n theme::hint_style(),\n )]));\n f.render_widget(hints, chunks[2]);\n}\n\nfn draw_api_key(f: &mut Frame, area: Rect, state: &mut WizardState) {\n let p = match state.selected_provider_info() {\n Some(p) => p,\n None => return,\n };\n\n let chunks = Layout::vertical([\n Constraint::Length(2), // prompt\n Constraint::Length(1), // input\n Constraint::Length(2), // spacer + hint about env var\n Constraint::Min(0), // spacer\n Constraint::Length(1), // hints\n ])\n .split(area);\n\n let prompt = Paragraph::new(Line::from(vec![Span::raw(format!(\n \" Enter your {} API key:\",\n p.name\n ))]));\n f.render_widget(prompt, chunks[0]);\n\n // Masked input\n let masked: String = \"\\u{2022}\".repeat(state.api_key_input.len());\n let input = Paragraph::new(Line::from(vec![\n Span::raw(\" > \"),\n Span::styled(&masked, theme::input_style()),\n Span::styled(\n \"\\u{2588}\",\n Style::default()\n .fg(theme::GREEN)\n .add_modifier(Modifier::SLOW_BLINK),\n ),\n ]));\n f.render_widget(input, chunks[1]);\n\n let env_hint = Paragraph::new(Line::from(vec![Span::styled(\n format!(\" Or set {} environment variable\", p.env_var),\n theme::dim_style(),\n )]));\n f.render_widget(env_hint, chunks[2]);\n\n let hints = Paragraph::new(Line::from(vec![Span::styled(\n \" [Enter] Confirm [Esc] Back\",\n theme::hint_style(),\n )]));\n f.render_widget(hints, chunks[4]);\n}\n\nfn draw_model(f: &mut Frame, area: Rect, state: &mut WizardState) {\n let p = match state.selected_provider_info() {\n Some(p) => p,\n None => return,\n };\n\n let chunks = Layout::vertical([\n Constraint::Length(2), // prompt\n Constraint::Length(1), // input\n Constraint::Length(2), // default hint\n Constraint::Min(0),\n Constraint::Length(1), // hints\n ])\n .split(area);\n\n let prompt = Paragraph::new(Line::from(vec![Span::raw(\" Model name:\")]));\n f.render_widget(prompt, chunks[0]);\n\n let display_text = if state.model_input.is_empty() {\n p.default_model\n } else {\n &state.model_input\n };\n let input = Paragraph::new(Line::from(vec![\n Span::raw(\" > \"),\n Span::styled(display_text, theme::input_style()),\n Span::styled(\n \"\\u{2588}\",\n Style::default()\n .fg(theme::GREEN)\n .add_modifier(Modifier::SLOW_BLINK),\n ),\n ]));\n f.render_widget(input, chunks[1]);\n\n let default_hint = Paragraph::new(Line::from(vec![Span::styled(\n format!(\" default: {}\", p.default_model),\n theme::dim_style(),\n )]));\n f.render_widget(default_hint, chunks[2]);\n\n let hints = Paragraph::new(Line::from(vec![Span::styled(\n \" [Enter] Confirm [Esc] Back\",\n theme::hint_style(),\n )]));\n f.render_widget(hints, chunks[4]);\n}\n\nfn draw_done(f: &mut Frame, area: Rect, state: &WizardState) {\n let chunks = Layout::vertical([\n Constraint::Length(2),\n Constraint::Length(1),\n Constraint::Min(0),\n ])\n .split(area);\n\n let icon = if state.created_config.is_some() {\n Span::styled(\" \\u{2714} \", Style::default().fg(theme::GREEN))\n } else {\n Span::styled(\" \\u{2718} \", Style::default().fg(theme::RED))\n };\n\n let msg = Paragraph::new(Line::from(vec![icon, Span::raw(&state.status_msg)]));\n f.render_widget(msg, chunks[0]);\n\n if state.created_config.is_some() {\n let cont = Paragraph::new(Line::from(vec![Span::styled(\n \" Continuing...\",\n theme::dim_style(),\n )]));\n f.render_widget(cont, chunks[1]);\n }\n}\n"}}, "docs/providers.md": {"6b3a9982fff3902a62ec88e244ff8596db7f7734": {"t": 1785746550.5536234, "n": 32990, "h": "239ba3b7aae6a01e", "c": "# LLM Providers Guide\n\nOpenFang ships with a comprehensive model catalog covering **3 native LLM drivers**, **20 providers**, **51 builtin models**, and **23 aliases**. Every provider uses one of three battle-tested drivers: the native **Anthropic** driver, the native **Gemini** driver, or the universal **OpenAI-compatible** driver. This guide is the single source of truth for configuring, selecting, and managing LLM providers in OpenFang.\n\n---\n\n## Table of Contents\n\n1. [Quick Setup](#quick-setup)\n2. [Provider Reference](#provider-reference)\n3. [Model Catalog](#model-catalog)\n4. [Model Aliases](#model-aliases)\n5. [Per-Agent Model Override](#per-agent-model-override)\n6. [Model Routing](#model-routing)\n7. [Cost Tracking](#cost-tracking)\n8. [Fallback Providers](#fallback-providers)\n9. [API Endpoints](#api-endpoints)\n10. [Channel Commands](#channel-commands)\n\n---\n\n## Quick Setup\n\nThe fastest path from zero to running:\n\n```bash\n# Pick ONE provider — set its env var — done.\nexport GEMINI_API_KEY=\"your-key\" # Free tier available\n# OR\nexport GROQ_API_KEY=\"your-key\" # Free tier available\n# OR\nexport ANTHROPIC_API_KEY=\"your-key\"\n# OR\nexport OPENAI_API_KEY=\"your-key\"\n```\n\nOpenFang auto-detects which providers have API keys configured at boot. Any model whose provider is authenticated becomes immediately available. Local providers (Ollama, vLLM, LM Studio) require no key at all.\n\nFor Gemini specifically, either `GEMINI_API_KEY` or `GOOGLE_API_KEY` will work.\n\n---\n\n## Provider Reference\n\n### 1. Anthropic\n\n| | |\n|---|---|\n| **Display Name** | Anthropic |\n| **Driver** | Native Anthropic (Messages API) |\n| **Env Var** | `ANTHROPIC_API_KEY` |\n| **Base URL** | `https://api.anthropic.com` |\n| **Key Required** | Yes |\n| **Free Tier** | No |\n| **Auth** | `x-api-key` header |\n| **Models** | 3 |\n\n**Available Models:**\n- `claude-opus-4-20250514` (Frontier)\n- `claude-sonnet-4-20250514` (Smart)\n- `claude-haiku-4-5-20251001` (Fast)\n\n**Setup:**\n1. Sign up at [console.anthropic.com](https://console.anthropic.com)\n2. Create an API key under Settings > API Keys\n3. `export ANTHROPIC_API_KEY=\"sk-ant-...\"`\n\n---\n\n### 2. OpenAI\n\n| | |\n|---|---|\n| **Display Name** | OpenAI |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `OPENAI_API_KEY` |\n| **Base URL** | `https://api.openai.com/v1` |\n| **Key Required** | Yes |\n| **Free Tier** | No |\n| **Auth** | `Authorization: Bearer` header |\n| **Models** | 6 |\n\n**Available Models:**\n- `gpt-4.1` (Frontier)\n- `gpt-4o` (Smart)\n- `o3-mini` (Smart)\n- `gpt-4.1-mini` (Balanced)\n- `gpt-4o-mini` (Fast)\n- `gpt-4.1-nano` (Fast)\n\n**Setup:**\n1. Sign up at [platform.openai.com](https://platform.openai.com)\n2. Create an API key under API Keys\n3. `export OPENAI_API_KEY=\"sk-...\"`\n\n---\n\n### 3. Google Gemini\n\n| | |\n|---|---|\n| **Display Name** | Google Gemini |\n| **Driver** | Native Gemini (generateContent API) |\n| **Env Var** | `GEMINI_API_KEY` (or `GOOGLE_API_KEY`) |\n| **Base URL** | `https://generativelanguage.googleapis.com` |\n| **Key Required** | Yes |\n| **Free Tier** | Yes (generous free tier) |\n| **Auth** | `x-goog-api-key` header |\n| **Models** | 3 |\n\n**Available Models:**\n- `gemini-2.5-pro` (Frontier)\n- `gemini-2.5-flash` (Smart)\n- `gemini-2.0-flash` (Fast)\n\n**Setup:**\n1. Go to [aistudio.google.com](https://aistudio.google.com)\n2. Get an API key (free tier included)\n3. `export GEMINI_API_KEY=\"AIza...\"` or `export GOOGLE_API_KEY=\"AIza...\"`\n\n**Notes:** The Gemini driver is a fully native implementation. It is not OpenAI-compatible. Model goes in the URL path, system prompt via `systemInstruction`, tools via `functionDeclarations`, streaming via `streamGenerateContent?alt=sse`.\n\n---\n\n### 4. DeepSeek\n\n| | |\n|---|---|\n| **Display Name** | DeepSeek |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `DEEPSEEK_API_KEY` |\n| **Base URL** | `https://api.deepseek.com/v1` |\n| **Key Required** | Yes |\n| **Free Tier** | No |\n| **Auth** | `Authorization: Bearer` header |\n| **Models** | 2 |\n\n**Available Models:**\n- `deepseek-chat` (Smart) -- DeepSeek V3\n- `deepseek-reasoner` (Smart) -- DeepSeek R1, no tool support\n\n**Setup:**\n1. Sign up at [platform.deepseek.com](https://platform.deepseek.com)\n2. Create an API key\n3. `export DEEPSEEK_API_KEY=\"sk-...\"`\n\n---\n\n### 5. Groq\n\n| | |\n|---|---|\n| **Display Name** | Groq |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `GROQ_API_KEY` |\n| **Base URL** | `https://api.groq.com/openai/v1` |\n| **Key Required** | Yes |\n| **Free Tier** | Yes (rate-limited) |\n| **Auth** | `Authorization: Bearer` header |\n| **Models** | 4 |\n\n**Available Models:**\n- `llama-3.3-70b-versatile` (Balanced)\n- `mixtral-8x7b-32768` (Balanced)\n- `llama-3.1-8b-instant` (Fast)\n- `gemma2-9b-it` (Fast)\n\n**Setup:**\n1. Sign up at [console.groq.com](https://console.groq.com)\n2. Create an API key\n3. `export GROQ_API_KEY=\"gsk_...\"`\n\n**Notes:** Groq runs open-source models on custom LPU hardware. Extremely fast inference. Free tier has rate limits but is very usable.\n\n---\n\n### 6. OpenRouter\n\n| | |\n|---|---|\n| **Display Name** | OpenRouter |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `OPENROUTER_API_KEY` |\n| **Base URL** | `https://openrouter.ai/api/v1` |\n| **Key Required** | Yes |\n| **Free Tier** | Yes (limited credits for some models) |\n| **Auth** | `Authorization: Bearer` header |\n| **Models** | 3 |\n\n**Available Models:**\n- `openrouter/auto` (Smart) -- auto-selects best model\n- `openrouter/optimus` (Balanced) -- cost-optimized\n- `openrouter/nitro` (Fast) -- speed-optimized\n\n**Setup:**\n1. Sign up at [openrouter.ai](https://openrouter.ai)\n2. Create an API key under Keys\n3. `export OPENROUTER_API_KEY=\"sk-or-...\"`\n\n**Notes:** OpenRouter is a unified gateway to 200+ models from many providers. The three builtin entries are OpenRouter's smart-routing endpoints. You can also use any model ID from their catalog directly by specifying the full OpenRouter model path.\n\n---\n\n### 7. Mistral AI\n\n| | |\n|---|---|\n| **Display Name** | Mistral AI |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `MISTRAL_API_KEY` |\n| **Base URL** | `https://api.mistral.ai/v1` |\n| **Key Required** | Yes |\n| **Free Tier** | No |\n| **Auth** | `Authorization: Bearer` header |\n| **Models** | 3 |\n\n**Available Models:**\n- `mistral-large-latest` (Smart)\n- `codestral-latest` (Smart)\n- `mistral-small-latest` (Fast)\n\n**Setup:**\n1. Sign up at [console.mistral.ai](https://console.mistral.ai)\n2. Create an API key\n3. `export MISTRAL_API_KEY=\"...\"`\n\n---\n\n### 8. Together AI\n\n| | |\n|---|---|\n| **Display Name** | Together AI |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `TOGETHER_API_KEY` |\n| **Base URL** | `https://api.together.xyz/v1` |\n| **Key Required** | Yes |\n| **Free Tier** | Yes (limited credits on signup) |\n| **Auth** | `Authorization: Bearer` header |\n| **Models** | 3 |\n\n**Available Models:**\n- `meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo` (Frontier)\n- `Qwen/Qwen2.5-72B-Instruct-Turbo` (Smart)\n- `mistralai/Mixtral-8x22B-Instruct-v0.1` (Balanced)\n\n**Setup:**\n1. Sign up at [api.together.ai](https://api.together.ai)\n2. Create an API key\n3. `export TOGETHER_API_KEY=\"...\"`\n\n---\n\n### 9. Fireworks AI\n\n| | |\n|---|---|\n| **Display Name** | Fireworks AI |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `FIREWORKS_API_KEY` |\n| **Base URL** | `https://api.fireworks.ai/inference/v1` |\n| **Key Required** | Yes |\n| **Free Tier** | Yes (limited credits on signup) |\n| **Auth** | `Authorization: Bearer` header |\n| **Models** | 2 |\n\n**Available Models:**\n- `accounts/fireworks/models/llama-v3p1-405b-instruct` (Frontier)\n- `accounts/fireworks/models/mixtral-8x22b-instruct` (Balanced)\n\n**Setup:**\n1. Sign up at [fireworks.ai](https://fireworks.ai)\n2. Create an API key\n3. `export FIREWORKS_API_KEY=\"...\"`\n\n---\n\n### 10. Ollama\n\n| | |\n|---|---|\n| **Display Name** | Ollama |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `OLLAMA_API_KEY` (not required) |\n| **Base URL** | `http://localhost:11434/v1` |\n| **Key Required** | **No** |\n| **Free Tier** | Free (local) |\n| **Auth** | None (local) |\n| **Models** | 3 builtin + auto-discovered |\n\n**Available Models (builtin):**\n- `llama3.2` (Local)\n- `mistral:latest` (Local)\n- `phi3` (Local)\n\n**Setup:**\n1. Install Ollama from [ollama.com](https://ollama.com)\n2. Pull a model: `ollama pull llama3.2`\n3. Start the server: `ollama serve`\n4. No env var needed -- Ollama is always available\n\n**Notes:** OpenFang auto-discovers models from a running Ollama instance and merges them into the catalog with `Local` tier and zero cost. Any model you pull becomes usable immediately.\n\n---\n\n### 11. vLLM\n\n| | |\n|---|---|\n| **Display Name** | vLLM |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `VLLM_API_KEY` (not required) |\n| **Base URL** | `http://localhost:8000/v1` |\n| **Key Required** | **No** |\n| **Free Tier** | Free (self-hosted) |\n| **Auth** | None (local) |\n| **Models** | 1 builtin + auto-discovered |\n\n**Available Models (builtin):**\n- `vllm-local` (Local)\n\n**Setup:**\n1. Install vLLM: `pip install vllm`\n2. Start the server: `python -m vllm.entrypoints.openai.api_server --model <model-name>`\n3. No env var needed\n\n---\n\n### 12. LM Studio\n\n| | |\n|---|---|\n| **Display Name** | LM Studio |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `LMSTUDIO_API_KEY` (not required) |\n| **Base URL** | `http://localhost:1234/v1` |\n| **Key Required** | **No** |\n| **Free Tier** | Free (local) |\n| **Auth** | None (local) |\n| **Models** | 1 builtin + auto-discovered |\n\n**Available Models (builtin):**\n- `lmstudio-local` (Local)\n\n**Setup:**\n1. Download LM Studio from [lmstudio.ai](https://lmstudio.ai)\n2. Download a model from the built-in model browser\n3. Start the local server from the \"Local Server\" tab\n4. No env var needed\n\n---\n\n### 13. Perplexity AI\n\n| | |\n|---|---|\n| **Display Name** | Perplexity AI |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `PERPLEXITY_API_KEY` |\n| **Base URL** | `https://api.perplexity.ai` |\n| **Key Required** | Yes |\n| **Free Tier** | No |\n| **Auth** | `Authorization: Bearer` header |\n| **Models** | 2 |\n\n**Available Models:**\n- `sonar-pro` (Smart) -- online search-augmented\n- `sonar` (Balanced) -- online search-augmented\n\n**Setup:**\n1. Sign up at [perplexity.ai](https://www.perplexity.ai)\n2. Go to API settings and generate a key\n3. `export PERPLEXITY_API_KEY=\"pplx-...\"`\n\n**Notes:** Perplexity models have built-in web search. They do not support tool use.\n\n---\n\n### 14. Cohere\n\n| | |\n|---|---|\n| **Display Name** | Cohere |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `COHERE_API_KEY` |\n| **Base URL** | `https://api.cohere.com/v2` |\n| **Key Required** | Yes |\n| **Free Tier** | Yes (rate-limited trial) |\n| **Auth** | `Authorization: Bearer` header |\n| **Models** | 2 |\n\n**Available Models:**\n- `command-r-plus` (Smart)\n- `command-r` (Balanced)\n\n**Setup:**\n1. Sign up at [dashboard.cohere.com](https://dashboard.cohere.com)\n2. Create an API key\n3. `export COHERE_API_KEY=\"...\"`\n\n---\n\n### 15. AI21 Labs\n\n| | |\n|---|---|\n| **Display Name** | AI21 Labs |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `AI21_API_KEY` |\n| **Base URL** | `https://api.ai21.com/studio/v1` |\n| **Key Required** | Yes |\n| **Free Tier** | Yes (limited credits) |\n| **Auth** | `Authorization: Bearer` header |\n| **Models** | 1 |\n\n**Available Models:**\n- `jamba-1.5-large` (Smart)\n\n**Setup:**\n1. Sign up at [studio.ai21.com](https://studio.ai21.com)\n2. Create an API key\n3. `export AI21_API_KEY=\"...\"`\n\n---\n\n### 16. Cerebras\n\n| | |\n|---|---|\n| **Display Name** | Cerebras |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `CEREBRAS_API_KEY` |\n| **Base URL** | `https://api.cerebras.ai/v1` |\n| **Key Required** | Yes |\n| **Free Tier** | Yes (generous free tier) |\n| **Auth** | `Authorization: Bearer` header |\n| **Models** | 2 |\n\n**Available Models:**\n- `cerebras/llama3.3-70b` (Balanced)\n- `cerebras/llama3.1-8b` (Fast)\n\n**Setup:**\n1. Sign up at [cloud.cerebras.ai](https://cloud.cerebras.ai)\n2. Create an API key\n3. `export CEREBRAS_API_KEY=\"...\"`\n\n**Notes:** Cerebras runs inference on wafer-scale chips. Ultra-fast and ultra-cheap ($0.06/M tokens for both input and output on the 70B model).\n\n---\n\n### 17. SambaNova\n\n| | |\n|---|---|\n| **Display Name** | SambaNova |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `SAMBANOVA_API_KEY` |\n| **Base URL** | `https://api.sambanova.ai/v1` |\n| **Key Required** | Yes |\n| **Free Tier** | Yes (limited credits) |\n| **Auth** | `Authorization: Bearer` header |\n| **Models** | 1 |\n\n**Available Models:**\n- `sambanova/llama-3.3-70b` (Balanced)\n\n**Setup:**\n1. Sign up at [cloud.sambanova.ai](https://cloud.sambanova.ai)\n2. Create an API key\n3. `export SAMBANOVA_API_KEY=\"...\"`\n\n---\n\n### 18. Hugging Face\n\n| | |\n|---|---|\n| **Display Name** | Hugging Face |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `HF_API_KEY` |\n| **Base URL** | `https://api-inference.huggingface.co/v1` |\n| **Key Required** | Yes |\n| **Free Tier** | Yes (rate-limited) |\n| **Auth** | `Authorization: Bearer` header |\n| **Models** | 1 |\n\n**Available Models:**\n- `hf/meta-llama/Llama-3.3-70B-Instruct` (Balanced)\n\n**Setup:**\n1. Sign up at [huggingface.co](https://huggingface.co)\n2. Create a token under Settings > Access Tokens\n3. `export HF_API_KEY=\"hf_...\"`\n\n---\n\n### 19. xAI\n\n| | |\n|---|---|\n| **Display Name** | xAI |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `XAI_API_KEY` |\n| **Base URL** | `https://api.x.ai/v1` |\n| **Key Required** | Yes |\n| **Free Tier** | Yes (limited free credits) |\n| **Auth** | `Authorization: Bearer` header |\n| **Models** | 2 |\n\n**Available Models:**\n- `grok-2` (Smart) -- supports vision\n- `grok-2-mini` (Fast)\n\n**Setup:**\n1. Sign up at [console.x.ai](https://console.x.ai)\n2. Create an API key\n3. `export XAI_API_KEY=\"xai-...\"`\n\n---\n\n### 20. Replicate\n\n| | |\n|---|---|\n| **Display Name** | Replicate |\n| **Driver** | OpenAI-compatible |\n| **Env Var** | `REPLICATE_API_TOKEN` |\n| **Base URL** | `https://api.replicate.com/v1` |\n| **Key Required** | Yes |\n| **Free Tier** | No |\n| **Auth** | `Authorization: Bearer` header |\n| **Models** | 1 |\n\n**Available Models:**\n- `replicate/meta-llama-3.3-70b-instruct` (Balanced)\n\n**Setup:**\n1. Sign up at [replicate.com](https://replicate.com)\n2. Go to Account > API Tokens\n3. `export REPLICATE_API_TOKEN=\"r8_...\"`\n\n---\n\n## Model Catalog\n\nThe complete catalog of all 51 builtin models, sorted by provider. Pricing is per million tokens.\n\n| # | Model ID | Display Name | Provider | Tier | Context Window | Max Output | Input $/M | Output $/M | Tools | Vision |\n|---|----------|-------------|----------|------|---------------|------------|-----------|------------|-------|--------|\n| 1 | `claude-opus-4-20250514` | Claude Opus 4 | anthropic | Frontier | 200,000 | 32,000 | $15.00 | $75.00 | Yes | Yes |\n| 2 | `claude-sonnet-4-20250514` | Claude Sonnet 4 | anthropic | Smart | 200,000 | 64,000 | $3.00 | $15.00 | Yes | Yes |\n| 3 | `claude-haiku-4-5-20251001` | Claude Haiku 4.5 | anthropic | Fast | 200,000 | 8,192 | $0.25 | $1.25 | Yes | Yes |\n| 4 | `gpt-4.1` | GPT-4.1 | openai | Frontier | 1,047,576 | 32,768 | $2.00 | $8.00 | Yes | Yes |\n| 5 | `gpt-4o` | GPT-4o | openai | Smart | 128,000 | 16,384 | $2.50 | $10.00 | Yes | Yes |\n| 6 | `o3-mini` | o3-mini | openai | Smart | 200,000 | 100,000 | $1.10 | $4.40 | Yes | No |\n| 7 | `gpt-4.1-mini` | GPT-4.1 Mini | openai | Balanced | 1,047,576 | 32,768 | $0.40 | $1.60 | Yes | Yes |\n| 8 | `gpt-4o-mini` | GPT-4o Mini | openai | Fast | 128,000 | 16,384 | $0.15 | $0.60 | Yes | Yes |\n| 9 | `gpt-4.1-nano` | GPT-4.1 Nano | openai | Fast | 1,047,576 | 32,768 | $0.10 | $0.40 | Yes | No |\n| 10 | `gemini-2.5-pro` | Gemini 2.5 Pro | gemini | Frontier | 1,048,576 | 65,536 | $1.25 | $10.00 | Yes | Yes |\n| 11 | `gemini-2.5-flash` | Gemini 2.5 Flash | gemini | Smart | 1,048,576 | 65,536 | $0.15 | $0.60 | Yes | Yes |\n| 12 | `gemini-2.0-flash` | Gemini 2.0 Flash | gemini | Fast | 1,048,576 | 8,192 | $0.10 | $0.40 | Yes | Yes |\n| 13 | `deepseek-chat` | DeepSeek V3 | deepseek | Smart | 64,000 | 8,192 | $0.27 | $1.10 | Yes | No |\n| 14 | `deepseek-reasoner` | DeepSeek R1 | deepseek | Smart | 64,000 | 8,192 | $0.55 | $2.19 | No | No |\n| 15 | `llama-3.3-70b-versatile` | Llama 3.3 70B | groq | Balanced | 128,000 | 32,768 | $0.059 | $0.079 | Yes | No |\n| 16 | `mixtral-8x7b-32768` | Mixtral 8x7B | groq | Balanced | 32,768 | 4,096 | $0.024 | $0.024 | Yes | No |\n| 17 | `llama-3.1-8b-instant` | Llama 3.1 8B | groq | Fast | 128,000 | 8,192 | $0.05 | $0.08 | Yes | No |\n| 18 | `gemma2-9b-it` | Gemma 2 9B | groq | Fast | 8,192 | 4,096 | $0.02 | $0.02 | No | No |\n| 19 | `openrouter/auto` | OpenRouter Auto | openrouter | Smart | 200,000 | 32,000 | $1.00 | $3.00 | Yes | Yes |\n| 20 | `openrouter/optimus` | OpenRouter Optimus | openrouter | Balanced | 200,000 | 32,000 | $0.50 | $1.50 | Yes | No |\n| 21 | `openrouter/nitro` | OpenRouter Nitro | openrouter | Fast | 128,000 | 16,000 | $0.20 | $0.60 | Yes | No |\n| 22 | `mistral-large-latest` | Mistral Large | mistral | Smart | 128,000 | 8,192 | $2.00 | $6.00 | Yes | No |\n| 23 | `codestral-latest` | Codestral | mistral | Smart | 32,000 | 8,192 | $0.30 | $0.90 | Yes | No |\n| 24 | `mistral-small-latest` | Mistral Small | mistral | Fast | 128,000 | 8,192 | $0.10 | $0.30 | Yes | No |\n| 25 | `meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo` | Llama 3.1 405B (Together) | together | Frontier | 130,000 | 4,096 | $3.50 | $3.50 | Yes | No |\n| 26 | `Qwen/Qwen2.5-72B-Instruct-Turbo` | Qwen 2.5 72B (Together) | together | Smart | 32,768 | 4,096 | $0.20 | $0.60 | Yes | No |\n| 27 | `mistralai/Mixtral-8x22B-Instruct-v0.1` | Mixtral 8x22B (Together) | together | Balanced | 65,536 | 4,096 | $0.60 | $0.60 | Yes | No |\n| 28 | `accounts/fireworks/models/llama-v3p1-405b-instruct` | Llama 3.1 405B (Fireworks) | fireworks | Frontier | 131,072 | 16,384 | $3.00 | $3.00 | Yes | No |\n| 29 | `accounts/fireworks/models/mixtral-8x22b-instruct` | Mixtral 8x22B (Fireworks) | fireworks | Balanced | 65,536 | 4,096 | $0.90 | $0.90 | Yes | No |\n| 30 | `llama3.2` | Llama 3.2 (Ollama) | ollama | Local | 128,000 | 4,096 | $0.00 | $0.00 | Yes | No |\n| 31 | `mistral:latest` | Mistral (Ollama) | ollama | Local | 32,768 | 4,096 | $0.00 | $0.00 | Yes | No |\n| 32 | `phi3` | Phi-3 (Ollama) | ollama | Local | 128,000 | 4,096 | $0.00 | $0.00 | No | No |\n| 33 | `vllm-local` | vLLM Local Model | vllm | Local | 32,768 | 4,096 | $0.00 | $0.00 | Yes | No |\n| 34 | `lmstudio-local` | LM Studio Local Model | lmstudio | Local | 32,768 | 4,096 | $0.00 | $0.00 | Yes | No |\n| 35 | `sonar-pro` | Sonar Pro | perplexity | Smart | 200,000 | 8,192 | $3.00 | $15.00 | No | No |\n| 36 | `sonar` | Sonar | perplexity | Balanced | 128,000 | 8,192 | $1.00 | $5.00 | No | No |\n| 37 | `command-r-plus` | Command R+ | cohere | Smart | 128,000 | 4,096 | $2.50 | $10.00 | Yes | No |\n| 38 | `command-r` | Command R | cohere | Balanced | 128,000 | 4,096 | $0.15 | $0.60 | Yes | No |\n| 39 | `jamba-1.5-large` | Jamba 1.5 Large | ai21 | Smart | 256,000 | 4,096 | $2.00 | $8.00 | Yes | No |\n| 40 | `cerebras/llama3.3-70b` | Llama 3.3 70B (Cerebras) | cerebras | Balanced | 128,000 | 8,192 | $0.06 | $0.06 | Yes | No |\n| 41 | `cerebras/llama3.1-8b` | Llama 3.1 8B (Cerebras) | cerebras | Fast | 128,000 | 8,192 | $0.01 | $0.01 | Yes | No |\n| 42 | `sambanova/llama-3.3-70b` | Llama 3.3 70B (SambaNova) | sambanova | Balanced | 128,000 | 8,192 | $0.06 | $0.06 | Yes | No |\n| 43 | `grok-2` | Grok 2 | xai | Smart | 131,072 | 32,768 | $2.00 | $10.00 | Yes | Yes |\n| 44 | `grok-2-mini` | Grok 2 Mini | xai | Fast | 131,072 | 32,768 | $0.30 | $0.50 | Yes | No |\n| 45 | `hf/meta-llama/Llama-3.3-70B-Instruct` | Llama 3.3 70B (HF) | huggingface | Balanced | 128,000 | 4,096 | $0.30 | $0.30 | No | No |\n| 46 | `replicate/meta-llama-3.3-70b-instruct` | Llama 3.3 70B (Replicate) | replicate | Balanced | 128,000 | 4,096 | $0.40 | $0.40 | No | No |\n\n**Model Tiers:**\n\n| Tier | Description | Typical Use |\n|------|------------|------------|\n| **Frontier** | Most capable, highest cost | Orchestration, architecture, security audits |\n| **Smart** | Strong reasoning, moderate cost | Coding, code review, research, analysis |\n| **Balanced** | Good cost/quality tradeoff | Planning, writing, DevOps, day-to-day tasks |\n| **Fast** | Cheapest cloud inference | Ops, translation, simple Q&A, health checks |\n| **Local** | Self-hosted, zero cost | Privacy-first, offline, development |\n\n**Notes:**\n- Local providers (Ollama, vLLM, LM Studio) auto-discover models at runtime. Any model you download and serve will be merged into the catalog with `Local` tier and zero cost.\n- The 46 entries above are the builtin models. The total of 51 referenced in the catalog includes runtime auto-discovered models that vary per installation.\n\n---\n\n## Model Aliases\n\nAll 23 aliases resolve to canonical model IDs. Aliases are case-insensitive.\n\n| Alias | Resolves To |\n|-------|------------|\n| `sonnet` | `claude-sonnet-4-20250514` |\n| `claude-sonnet` | `claude-sonnet-4-20250514` |\n| `haiku` | `claude-haiku-4-5-20251001` |\n| `claude-haiku` | `claude-haiku-4-5-20251001` |\n| `opus` | `claude-opus-4-20250514` |\n| `claude-opus` | `claude-opus-4-20250514` |\n| `gpt4` | `gpt-4o` |\n| `gpt4o` | `gpt-4o` |\n| `gpt4-mini` | `gpt-4o-mini` |\n| `flash` | `gemini-2.5-flash` |\n| `gemini-flash` | `gemini-2.5-flash` |\n| `gemini-pro` | `gemini-2.5-pro` |\n| `deepseek` | `deepseek-chat` |\n| `llama` | `llama-3.3-70b-versatile` |\n| `llama-70b` | `llama-3.3-70b-versatile` |\n| `mixtral` | `mixtral-8x7b-32768` |\n| `mistral` | `mistral-large-latest` |\n| `codestral` | `codestral-latest` |\n| `grok` | `grok-2` |\n| `grok-mini` | `grok-2-mini` |\n| `sonar` | `sonar-pro` |\n| `jamba` | `jamba-1.5-large` |\n| `command-r` | `command-r-plus` |\n\nYou can use aliases anywhere a model ID is accepted: in config files, REST API calls, chat commands, and the model routing configuration.\n\n---\n\n## Per-Agent Model Override\n\nEach agent in your `config.toml` can specify its own model, overriding the global default:\n\n```toml\n# Global default model\n[agents.defaults]\nmodel = \"claude-sonnet-4-20250514\"\n\n# Per-agent override: use an alias or full model ID\n[[agents]]\nname = \"orchestrator\"\nmodel = \"opus\" # alias for claude-opus-4-20250514\n\n[[agents]]\nname = \"ops\"\nmodel = \"llama-3.3-70b-versatile\" # cheap Groq model for simple ops\n\n[[agents]]\nname = \"coder\"\nmodel = \"gemini-2.5-flash\" # fast + cheap + 1M context\n\n[[agents]]\nname = \"researcher\"\nmodel = \"sonar-pro\" # Perplexity with built-in web search\n\n# You can also pin a model in the agent manifest TOML\n[[agents]]\nname = \"production-bot\"\npinned_model = \"claude-sonnet-4-20250514\" # never auto-routed\n```\n\nWhen `pinned_model` is set on an agent manifest, that agent always uses the specified model regardless of routing configuration. This is used in **Stabilisation mode** (`KernelMode::Stable`) where the model is frozen for production reliability.\n\n---\n\n## Model Routing\n\nOpenFang can automatically select the cheapest model capable of handling each query. This is configured per-agent via `ModelRoutingConfig`.\n\n### How It Works\n\n1. The **ModelRouter** scores each incoming `CompletionRequest` based on heuristics\n2. The score maps to a **TaskComplexity** tier: `Simple`, `Medium`, or `Complex`\n3. Each tier has a pre-configured model\n\n### Scoring Heuristics\n\n| Signal | Weight | Logic |\n|--------|--------|-------|\n| Total message length | 1 point per ~4 chars | Rough token proxy |\n| Tool availability | +20 per tool defined | Tools imply multi-step work |\n| Code markers | +30 per marker found | Backticks, `fn`, `def`, `class`, `import`, `function`, `async`, `await`, `struct`, `impl`, `return` |\n| Conversation depth | +15 per message > 10 | Deep context = harder reasoning |\n| System prompt length | +1 per 10 chars > 500 | Long system prompts imply complex tasks |\n\n### Thresholds\n\n| Complexity | Score Range | Default Model |\n|-----------|-------------|---------------|\n| Simple | score < 100 | `claude-haiku-4-5-20251001` |\n| Medium | 100 <= score < 500 | `claude-sonnet-4-20250514` |\n| Complex | score >= 500 | `claude-sonnet-4-20250514` |\n\n### Configuration\n\n```toml\n# In agent manifest or config.toml\n[routing]\nsimple_model = \"claude-haiku-4-5-20251001\"\nmedium_model = \"gemini-2.5-flash\"\ncomplex_model = \"claude-sonnet-4-20250514\"\nsimple_threshold = 100\ncomplex_threshold = 500\n```\n\nThe router also integrates with the model catalog:\n- **`validate_models()`** checks that all configured model IDs exist in the catalog\n- **`resolve_aliases()`** expands aliases to canonical IDs (e.g., `\"sonnet\"` becomes `\"claude-sonnet-4-20250514\"`)\n\n---\n\n## Cost Tracking\n\nOpenFang tracks the cost of every LLM call and can enforce per-agent spending quotas.\n\n### Per-Response Cost Estimation\n\nAfter each LLM call, cost is calculated as:\n\n```\ncost = (input_tokens / 1,000,000) * input_rate + (output_tokens / 1,000,000) * output_rate\n```\n\nThe `MeteringEngine` first checks the **model catalog** for exact pricing. If the model is not found, it falls back to a pattern-matching heuristic.\n\n### Cost Rates (per million tokens)\n\n| Model Pattern | Input $/M | Output $/M |\n|--------------|-----------|------------|\n| `*haiku*` | $0.25 | $1.25 |\n| `*sonnet*` | $3.00 | $15.00 |\n| `*opus*` | $15.00 | $75.00 |\n| `gpt-4o-mini` | $0.15 | $0.60 |\n| `gpt-4o` | $2.50 | $10.00 |\n| `gpt-4.1-nano` | $0.10 | $0.40 |\n| `gpt-4.1-mini` | $0.40 | $1.60 |\n| `gpt-4.1` | $2.00 | $8.00 |\n| `o3-mini` | $1.10 | $4.40 |\n| `gemini-2.5-pro` | $1.25 | $10.00 |\n| `gemini-2.5-flash` | $0.15 | $0.60 |\n| `gemini-2.0-flash` | $0.10 | $0.40 |\n| `deepseek-reasoner` / `deepseek-r1` | $0.55 | $2.19 |\n| `*deepseek*` | $0.27 | $1.10 |\n| `*cerebras*` | $0.06 | $0.06 |\n| `*sambanova*` | $0.06 | $0.06 |\n| `*replicate*` | $0.40 | $0.40 |\n| `*llama*` / `*mixtral*` | $0.05 | $0.10 |\n| `*qwen*` | $0.20 | $0.60 |\n| `mistral-large*` | $2.00 | $6.00 |\n| `*mistral*` (other) | $0.10 | $0.30 |\n| `command-r-plus` | $2.50 | $10.00 |\n| `command-r` | $0.15 | $0.60 |\n| `sonar-pro` | $3.00 | $15.00 |\n| `*sonar*` (other) | $1.00 | $5.00 |\n| `grok-2-mini` / `grok-mini` | $0.30 | $0.50 |\n| `*grok*` (other) | $2.00 | $10.00 |\n| `*jamba*` | $2.00 | $8.00 |\n| Default (unknown) | $1.00 | $3.00 |\n\n### Quota Enforcement\n\nQuotas are checked on every LLM call. If the agent exceeds its hourly limit, the call is rejected with a `QuotaExceeded` error.\n\n```toml\n# Per-agent quota in config.toml\n[[agents]]\nname = \"chatbot\"\n[agents.resources]\nmax_cost_per_hour_usd = 5.00 # cap at $5/hour\n```\n\nThe usage footer (when enabled) appends cost information to each response:\n\n```\n> Cost: $0.0042 | Tokens: 1,200 in / 340 out | Model: claude-sonnet-4-20250514\n```\n\n---\n\n## Fallback Providers\n\nThe `FallbackDriver` wraps multiple LLM drivers in a chain. If the primary driver fails, the next driver in the chain is tried automatically.\n\n### Behavior\n\n- On success: returns immediately\n- On **rate limit / overload** errors (`429`, `529`): bubbles up for retry logic (does NOT failover, because the primary should be retried after backoff)\n- On **all other errors**: logs a warning and tries the next driver in the chain\n- If all drivers fail: returns the last error\n\n### Configuration\n\nFallback chains are configured in your agent manifest or `config.toml`. The `FallbackDriver` is used automatically when an agent is in **Stabilisation mode** (`KernelMode::Stable`) or when multiple providers are configured for reliability.\n\n```toml\n# Example: primary Anthropic, fallback to Gemini, then Groq\n[[agents]]\nname = \"production-bot\"\nmodel = \"claude-sonnet-4-20250514\"\nfallback_models = [\"gemini-2.5-flash\", \"llama-3.3-70b-versatile\"]\n```\n\nThe fallback driver creates a chain: `AnthropicDriver -> GeminiDriver -> OpenAIDriver(Groq)`.\n\n---\n\n## API Endpoints\n\n### List All Models\n\n```\nGET /api/models\n```\n\nReturns the complete model catalog with metadata, pricing, and feature flags.\n\n**Response:**\n```json\n[\n {\n \"id\": \"claude-sonnet-4-20250514\",\n \"display_name\": \"Claude Sonnet 4\",\n \"provider\": \"anthropic\",\n \"tier\": \"Smart\",\n \"context_window\": 200000,\n \"max_output_tokens\": 64000,\n \"input_cost_per_m\": 3.0,\n \"output_cost_per_m\": 15.0,\n \"supports_tools\": true,\n \"supports_vision\": true,\n \"supports_streaming\": true,\n \"aliases\": [\"sonnet\", \"claude-sonnet\"]\n }\n]\n```\n\n### Get Specific Model\n\n```\nGET /api/models/{id}\n```\n\nReturns a single model entry. Supports both canonical IDs and aliases.\n\n```\nGET /api/models/sonnet\nGET /api/models/claude-sonnet-4-20250514\n```\n\n### List Aliases\n\n```\nGET /api/models/aliases\n```\n\nReturns a map of all alias-to-canonical-ID mappings.\n\n**Response:**\n```json\n{\n \"sonnet\": \"claude-sonnet-4-20250514\",\n \"haiku\": \"claude-haiku-4-5-20251001\",\n \"flash\": \"gemini-2.5-flash\",\n \"grok\": \"grok-2\"\n}\n```\n\n### List Providers\n\n```\nGET /api/providers\n```\n\nReturns all 20 providers with auth status and model counts.\n\n**Response:**\n```json\n[\n {\n \"id\": \"anthropic\",\n \"display_name\": \"Anthropic\",\n \"api_key_env\": \"ANTHROPIC_API_KEY\",\n \"base_url\": \"https://api.anthropic.com\",\n \"key_required\": true,\n \"auth_status\": \"Configured\",\n \"model_count\": 3\n },\n {\n \"id\": \"ollama\",\n \"display_name\": \"Ollama\",\n \"api_key_env\": \"OLLAMA_API_KEY\",\n \"base_url\": \"http://localhost:11434/v1\",\n \"key_required\": false,\n \"auth_status\": \"NotRequired\",\n \"model_count\": 5\n }\n]\n```\n\nAuth status values: `Configured`, `Missing`, `NotRequired`.\n\n### Set Provider API Key\n\n```\nPOST /api/providers/{name}/key\nContent-Type: application/json\n\n{ \"api_key\": \"sk-...\" }\n```\n\nConfigures an API key for a provider at runtime (stored as a `Zeroizing<String>`, wiped from memory on drop).\n\n### Remove Provider API Key\n\n```\nDELETE /api/providers/{name}/key\n```\n\nRemoves the configured API key for a provider.\n\n### Test Provider Connection\n\n```\nPOST /api/providers/{name}/test\n```\n\nSends a minimal test request to verify the provider is reachable and the API key is valid.\n\n---\n\n## Channel Commands\n\nTwo chat commands are available in any channel for inspecting models and providers:\n\n### `/models`\n\nLists all available models with their tier, provider, and context window. Only shows models from providers that have authentication configured (or do not require it).\n\n```\n/models\n```\n\nExample output:\n```\nAvailable models (12):\n\nFrontier:\n claude-opus-4-20250514 (Anthropic) — 200K ctx\n gemini-2.5-pro (Google Gemini) — 1M ctx\n\nSmart:\n claude-sonnet-4-20250514 (Anthropic) — 200K ctx\n gemini-2.5-flash (Google Gemini) — 1M ctx\n deepseek-chat (DeepSeek) — 64K ctx\n\nBalanced:\n llama-3.3-70b-versatile (Groq) — 128K ctx\n\nFast:\n claude-haiku-4-5-20251001 (Anthropic) — 200K ctx\n gemini-2.0-flash (Google Gemini) — 1M ctx\n\nLocal:\n llama3.2 (Ollama) — 128K ctx\n```\n\n### `/providers`\n\nLists all 20 providers with their authentication status.\n\n```\n/providers\n```\n\nExample output:\n```\nLLM Providers (20):\n\n Anthropic ANTHROPIC_API_KEY Configured 3 models\n OpenAI OPENAI_API_KEY Missing 6 models\n Google Gemini GEMINI_API_KEY Configured 3 models\n DeepSeek DEEPSEEK_API_KEY Missing 2 models\n Groq GROQ_API_KEY Configured 4 models\n Ollama (no key needed) Ready 3 models\n vLLM (no key needed) Ready 1 model\n LM Studio (no key needed) Ready 1 model\n ...\n```\n\n---\n\n## Environment Variables Summary\n\nQuick reference for all provider environment variables:\n\n| Provider | Env Var | Required |\n|----------|---------|----------|\n| Anthropic | `ANTHROPIC_API_KEY` | Yes |\n| OpenAI | `OPENAI_API_KEY` | Yes |\n| Google Gemini | `GEMINI_API_KEY` or `GOOGLE_API_KEY` | Yes |\n| DeepSeek | `DEEPSEEK_API_KEY` | Yes |\n| Groq | `GROQ_API_KEY` | Yes |\n| OpenRouter | `OPENROUTER_API_KEY` | Yes |\n| Mistral AI | `MISTRAL_API_KEY` | Yes |\n| Together AI | `TOGETHER_API_KEY` | Yes |\n| Fireworks AI | `FIREWORKS_API_KEY` | Yes |\n| Ollama | `OLLAMA_API_KEY` | No |\n| vLLM | `VLLM_API_KEY` | No |\n| LM Studio | `LMSTUDIO_API_KEY` | No |\n| Perplexity AI | `PERPLEXITY_API_KEY` | Yes |\n| Cohere | `COHERE_API_KEY` | Yes |\n| AI21 Labs | `AI21_API_KEY` | Yes |\n| Cerebras | `CEREBRAS_API_KEY` | Yes |\n| SambaNova | `SAMBANOVA_API_KEY` | Yes |\n| Hugging Face | `HF_API_KEY` | Yes |\n| xAI | `XAI_API_KEY` | Yes |\n| Replicate | `REPLICATE_API_TOKEN` | Yes |\n\n---\n\n## Security Notes\n\n- All API keys are stored as `Zeroizing<String>` -- the key material is automatically overwritten with zeros when the value is dropped from memory.\n- Auth detection (`detect_auth()`) only checks `std::env::var()` for presence -- it never reads or logs the actual secret value.\n- Provider API keys set via the REST API (`POST /api/providers/{name}/key`) follow the same zeroization policy.\n- The health endpoint (`/api/health`) never exposes provider auth status or API keys. Detailed info is behind `/api/health/detail` which requires authentication.\n- All `DriverConfig` and `KernelConfig` structs implement `Debug` with secret redaction -- API keys are printed as `\"***\"` in logs.\n"}}}}