Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/chatshellapp_chatshell-desktop.json

1 line
62 KiB
JSON

{"repo": "chatshellapp/chatshell-desktop", "files": {"src-tauri/src/llm/deepseek.rs": {"63aab8d2537e9f0c02e06a0878032bf3c6974282": {"t": 1785746222.710642, "n": 210, "h": "2b72487f73c41c9a", "c": "//! DeepSeek provider constants\n//!\n//! The actual client creation and streaming is handled by agent_builder.rs\n\n/// Default DeepSeek API base URL\npub const DEFAULT_BASE_URL: &str = \"https://api.deepseek.com\";\n"}, "873405ffd8a0dfe373d95884a7460f11df5366e0": {"t": 1785760745.9076798, "n": 210, "h": "2b72487f73c41c9a", "c": "//! DeepSeek provider constants\n//!\n//! The actual client creation and streaming is handled by agent_builder.rs\n\n/// Default DeepSeek API base URL\npub const DEFAULT_BASE_URL: &str = \"https://api.deepseek.com\";\n"}}, "src/components/provider-settings-dialog/constants.ts": {"63aab8d2537e9f0c02e06a0878032bf3c6974282": {"t": 1785846963.4169908, "n": 11884, "h": "a39ca0059dc1dcbe", "c": "import type { LLMProvider, ModelItem } from './types'\n\nexport const CUSTOM_PROVIDER: LLMProvider = {\n id: 'custom',\n name: 'Custom Provider',\n baseUrl: '',\n isCustom: true,\n}\n\nexport const BUILTIN_PROVIDERS: LLMProvider[] = [\n { id: 'openai', name: 'OpenAI', baseUrl: 'https://api.openai.com/v1' },\n { id: 'anthropic', name: 'Anthropic', baseUrl: 'https://api.anthropic.com' },\n {\n id: 'gemini',\n name: 'Google Gemini',\n baseUrl: 'https://generativelanguage.googleapis.com',\n },\n { id: 'openrouter', name: 'OpenRouter', baseUrl: 'https://openrouter.ai/api/v1' },\n { id: 'azure', name: 'Azure OpenAI', baseUrl: '' },\n { id: 'deepseek', name: 'DeepSeek', baseUrl: 'https://api.deepseek.com' },\n { id: 'groq', name: 'Groq', baseUrl: 'https://api.groq.com/openai/v1' },\n { id: 'mistral', name: 'Mistral', baseUrl: 'https://api.mistral.ai' },\n { id: 'perplexity', name: 'Perplexity', baseUrl: 'https://api.perplexity.ai' },\n { id: 'together', name: 'Together AI', baseUrl: 'https://api.together.xyz' },\n { id: 'xai', name: 'xAI', baseUrl: 'https://api.x.ai' },\n { id: 'cohere', name: 'Cohere', baseUrl: 'https://api.cohere.ai' },\n { id: 'moonshot', name: 'Moonshot', baseUrl: 'https://api.moonshot.cn/v1' },\n { id: 'hyperbolic', name: 'Hyperbolic', baseUrl: 'https://api.hyperbolic.xyz' },\n { id: 'galadriel', name: 'Galadriel', baseUrl: 'https://api.galadriel.com/v1/verified' },\n { id: 'minimax', name: 'MiniMax', baseUrl: 'https://api.minimax.io/v1' },\n { id: 'minimax_cn', name: 'MiniMax CN', baseUrl: 'https://api.minimaxi.com/v1' },\n { id: 'mira', name: 'Mira', baseUrl: 'https://api.mira.network' },\n // International\n { id: 'github_models', name: 'GitHub Models', baseUrl: 'https://models.inference.ai.azure.com' },\n { id: 'fireworks', name: 'Fireworks AI', baseUrl: 'https://api.fireworks.ai/inference/v1' },\n { id: 'nvidia', name: 'NVIDIA NIM', baseUrl: 'https://integrate.api.nvidia.com/v1' },\n { id: 'huggingface', name: 'Hugging Face', baseUrl: 'https://api-inference.huggingface.co/v1' },\n { id: 'cerebras', name: 'Cerebras', baseUrl: 'https://api.cerebras.ai/v1' },\n // Chinese AI\n { id: 'zhipu', name: 'Zhipu AI', baseUrl: 'https://open.bigmodel.cn/api/paas/v4' },\n { id: 'yi', name: '01.AI', baseUrl: 'https://api.lingyiwanwu.com/v1' },\n { id: 'baichuan', name: 'Baichuan', baseUrl: 'https://api.baichuan-ai.com/v1' },\n {\n id: 'dashscope',\n name: 'Alibaba Qwen',\n baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n },\n { id: 'stepfun', name: 'StepFun', baseUrl: 'https://api.stepfun.com/v1' },\n { id: 'doubao', name: 'Doubao', baseUrl: 'https://ark.cn-beijing.volces.com/api/v3' },\n { id: 'hunyuan', name: 'Tencent Hunyuan', baseUrl: 'https://api.hunyuan.cloud.tencent.com/v1' },\n {\n id: 'tencent_cloud_ti',\n name: 'Tencent Cloud TI',\n baseUrl: 'https://api.lkeap.cloud.tencent.com/v1',\n },\n { id: 'baidu_cloud', name: 'Baidu Cloud', baseUrl: 'https://qianfan.baidubce.com/v2' },\n { id: 'siliconflow', name: 'SiliconFlow', baseUrl: 'https://api.siliconflow.cn/v1' },\n { id: 'modelscope', name: 'ModelScope', baseUrl: 'https://api-inference.modelscope.cn/v1' },\n { id: 'xirang', name: 'Xirang', baseUrl: 'https://wishub-x1.ctyun.cn/v1' },\n { id: 'mimo', name: 'Xiaomi MiMo', baseUrl: 'https://api.xiaomimimo.com/v1' },\n // Local\n { id: 'ollama', name: 'Ollama', baseUrl: 'http://localhost:11434' },\n { id: 'lmstudio', name: 'LM Studio', baseUrl: 'http://localhost:1234/v1' },\n { id: 'gpustack', name: 'GPUStack', baseUrl: 'http://localhost:80/v1' },\n { id: 'ovms', name: 'OVMS', baseUrl: 'http://localhost:8000/v1' },\n]\n\n/**\n * Default models pre-populated for each provider so users don't have to\n * configure them manually. Users can still delete or add more models.\n * Aggregators like OpenRouter have more defaults.\n */\nexport const DEFAULT_PROVIDER_MODELS: Record<\n string,\n Array<{ modelId: string; displayName: string }>\n> = {\n openai: [\n { modelId: 'gpt-5.2', displayName: 'GPT-5.2' },\n { modelId: 'gpt-5-mini', displayName: 'GPT-5 Mini' },\n { modelId: 'gpt-5-nano', displayName: 'GPT-5 Nano' },\n ],\n azure: [\n { modelId: 'gpt-5.2', displayName: 'GPT-5.2' },\n { modelId: 'gpt-5-mini', displayName: 'GPT-5 Mini' },\n { modelId: 'gpt-5-nano', displayName: 'GPT-5 Nano' },\n ],\n anthropic: [\n { modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6' },\n { modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6' },\n { modelId: 'claude-haiku-4-5', displayName: 'Claude Haiku 4.5' },\n ],\n gemini: [\n { modelId: 'gemini-3.1-pro-preview', displayName: 'Gemini 3.1 Pro Preview' },\n { modelId: 'gemini-3-flash-preview', displayName: 'Gemini 3 Flash Preview' },\n ],\n openrouter: [\n { modelId: 'openai/gpt-5.2', displayName: 'OpenAI: GPT-5.2' },\n { modelId: 'openai/gpt-5-mini', displayName: 'OpenAI: GPT-5 Mini' },\n { modelId: 'openai/gpt-5-nano', displayName: 'OpenAI: GPT-5 Nano' },\n { modelId: 'anthropic/claude-opus-4-6', displayName: 'Anthropic: Claude Opus 4.6' },\n { modelId: 'anthropic/claude-sonnet-4-6', displayName: 'Anthropic: Claude Sonnet 4.6' },\n { modelId: 'anthropic/claude-haiku-4-5', displayName: 'Anthropic: Claude Haiku 4.5' },\n { modelId: 'google/gemini-3.1-pro-preview', displayName: 'Google: Gemini 3.1 Pro Preview' },\n { modelId: 'google/gemini-3-flash-preview', displayName: 'Google: Gemini 3 Flash Preview' },\n { modelId: 'minimax/minimax-m2.5', displayName: 'MiniMax: MiniMax M2.5' },\n { modelId: 'moonshotai/kimi-k2.5', displayName: 'MoonshotAI: Kimi K2.5' },\n { modelId: 'deepseek/deepseek-v3.2', displayName: 'DeepSeek: DeepSeek V3.2' },\n { modelId: 'deepseek/deepseek-r1', displayName: 'DeepSeek: DeepSeek R1' },\n ],\n deepseek: [\n { modelId: 'deepseek-chat', displayName: 'DeepSeek Chat' },\n { modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner' },\n ],\n groq: [\n { modelId: 'groq/compound', displayName: 'Groq: Compound' },\n { modelId: 'openai/gpt-oss-120b', displayName: 'OpenAI: GPT-OSS 120B' },\n ],\n mistral: [\n { modelId: 'mistral-large-latest', displayName: 'Mistral Large' },\n { modelId: 'mistral-small-latest', displayName: 'Mistral Small' },\n { modelId: 'codestral-latest', displayName: 'Codestral' },\n ],\n perplexity: [\n { modelId: 'sonar', displayName: 'Sonar' },\n { modelId: 'sonar-pro', displayName: 'Sonar Pro' },\n { modelId: 'sonar-reasoning', displayName: 'Sonar Reasoning' },\n ],\n together: [\n { modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B Turbo' },\n { modelId: 'Qwen/Qwen2.5-72B-Instruct-Turbo', displayName: 'Qwen 2.5 72B Turbo' },\n { modelId: 'deepseek-ai/DeepSeek-V3', displayName: 'DeepSeek V3' },\n ],\n xai: [\n { modelId: 'grok-4-1-fast-reasoning', displayName: 'Grok 4.1 Fast Reasoning' },\n { modelId: 'grok-4-1-fast-non-reasoning', displayName: 'Grok 4.1 Fast Non-Reasoning' },\n ],\n cohere: [\n { modelId: 'command-r-plus', displayName: 'Command R+' },\n { modelId: 'command-r', displayName: 'Command R' },\n ],\n moonshot: [{ modelId: 'kimi-k2.5', displayName: 'Kimi K2.5' }],\n hyperbolic: [\n { modelId: 'meta-llama/Llama-3.3-70B-Instruct', displayName: 'Llama 3.3 70B' },\n { modelId: 'deepseek-ai/DeepSeek-V3', displayName: 'DeepSeek V3' },\n ],\n galadriel: [{ modelId: 'llama3.3:70b', displayName: 'Llama 3.3 70B' }],\n minimax: [{ modelId: 'minimax-m2.5', displayName: 'MiniMax M2.5' }],\n minimax_cn: [{ modelId: 'MiniMax-M2.5', displayName: 'MiniMax M2.5' }],\n mira: [{ modelId: 'mira/llama-3.1-70b', displayName: 'Llama 3.1 70B' }],\n // International\n github_models: [{ modelId: 'gpt-4o', displayName: 'GPT-4o' }],\n fireworks: [\n {\n modelId: 'accounts/fireworks/models/llama-v3-70b-instruct',\n displayName: 'Llama 3 70B Instruct',\n },\n ],\n nvidia: [{ modelId: 'meta/llama-3.1-405b-instruct', displayName: 'Llama 3.1 405B Instruct' }],\n cerebras: [\n { modelId: 'gpt-oss-120b', displayName: 'GPT OSS 120B' },\n { modelId: 'qwen-3-235b-a22b-instruct-2507', displayName: 'Qwen 3 235B Instruct' },\n ],\n // Chinese AI\n zhipu: [\n { modelId: 'glm-5', displayName: 'GLM-5' },\n { modelId: 'glm-4.7', displayName: 'GLM-4.7' },\n { modelId: 'glm-4.6', displayName: 'GLM-4.6' },\n { modelId: 'glm-4.5-flash', displayName: 'GLM-4.5 Flash' },\n ],\n yi: [{ modelId: 'yi-lightning', displayName: 'Yi Lightning' }],\n baichuan: [\n { modelId: 'Baichuan4', displayName: 'Baichuan 4' },\n { modelId: 'Baichuan4-Turbo', displayName: 'Baichuan 4 Turbo' },\n { modelId: 'Baichuan3-Turbo', displayName: 'Baichuan 3 Turbo' },\n ],\n dashscope: [\n { modelId: 'qwen3.5-plus', displayName: 'Qwen 3.5 Plus' },\n { modelId: 'qwen3-max', displayName: 'Qwen 3 Max' },\n { modelId: 'qwen-max', displayName: 'Qwen Max' },\n { modelId: 'qwen-plus', displayName: 'Qwen Plus' },\n { modelId: 'qwen-flash', displayName: 'Qwen Flash' },\n ],\n stepfun: [\n { modelId: 'step-1-8k', displayName: 'Step 1 8K' },\n { modelId: 'step-1-flash', displayName: 'Step 1 Flash' },\n ],\n doubao: [\n { modelId: 'doubao-seed-1-8-251228', displayName: 'Doubao Seed 1.8' },\n { modelId: 'doubao-1-5-pro-32k-250115', displayName: 'Doubao 1.5 Pro 32K' },\n { modelId: 'doubao-1-5-lite-32k-250115', displayName: 'Doubao 1.5 Lite 32K' },\n { modelId: 'deepseek-v3-250324', displayName: 'DeepSeek V3' },\n ],\n hunyuan: [\n { modelId: 'hunyuan-turbo', displayName: 'Hunyuan Turbo' },\n { modelId: 'hunyuan-pro', displayName: 'Hunyuan Pro' },\n { modelId: 'hunyuan-standard', displayName: 'Hunyuan Standard' },\n { modelId: 'hunyuan-lite', displayName: 'Hunyuan Lite' },\n ],\n tencent_cloud_ti: [\n { modelId: 'deepseek-r1', displayName: 'DeepSeek R1' },\n { modelId: 'deepseek-v3', displayName: 'DeepSeek V3' },\n ],\n baidu_cloud: [\n { modelId: 'ernie-4.0-8k-latest', displayName: 'ERNIE 4.0' },\n { modelId: 'ernie-4.0-turbo-8k-latest', displayName: 'ERNIE 4.0 Turbo' },\n { modelId: 'deepseek-r1', displayName: 'DeepSeek R1' },\n { modelId: 'deepseek-v3', displayName: 'DeepSeek V3' },\n ],\n siliconflow: [\n { modelId: 'deepseek-ai/DeepSeek-V3.2', displayName: 'DeepSeek V3.2' },\n { modelId: 'Qwen/Qwen3-8B', displayName: 'Qwen 3 8B' },\n ],\n modelscope: [\n { modelId: 'Qwen/Qwen2.5-72B-Instruct', displayName: 'Qwen 2.5 72B Instruct' },\n { modelId: 'deepseek-ai/DeepSeek-R1', displayName: 'DeepSeek R1' },\n { modelId: 'deepseek-ai/DeepSeek-V3', displayName: 'DeepSeek V3' },\n ],\n mimo: [{ modelId: 'mimo-v2-flash', displayName: 'MiMo V2 Flash' }],\n}\n\n/**\n * Get default ModelItem list for a provider type.\n * Each item gets a unique timestamp-based ID for the UI.\n */\nexport function getDefaultModelsForProvider(providerType: string): ModelItem[] {\n const defaults = DEFAULT_PROVIDER_MODELS[providerType]\n if (!defaults || defaults.length === 0) return []\n return defaults.map((m, i) => ({\n id: `default-${Date.now()}-${i}`,\n displayName: m.displayName,\n modelId: m.modelId,\n }))\n}\n\nexport const LLM_PROVIDERS: LLMProvider[] = [...BUILTIN_PROVIDERS]\n\n// Providers that support auto-fetching models via API\nconst FETCH_SUPPORTED_PROVIDERS = new Set([\n 'openai',\n 'openrouter',\n 'ollama',\n 'deepseek',\n 'groq',\n 'together',\n 'xai',\n 'moonshot',\n 'perplexity',\n 'hyperbolic',\n 'mistral',\n 'minimax',\n 'minimax_cn',\n 'mira',\n 'galadriel',\n 'cohere',\n 'custom',\n 'custom_openai',\n // International\n 'github_models',\n 'fireworks',\n 'nvidia',\n 'huggingface',\n 'cerebras',\n // Chinese AI\n 'zhipu',\n 'yi',\n 'baichuan',\n 'dashscope',\n 'stepfun',\n 'doubao',\n 'hunyuan',\n 'tencent_cloud_ti',\n 'baidu_cloud',\n 'siliconflow',\n 'modelscope',\n 'xirang',\n 'mimo',\n // Local\n 'lmstudio',\n 'gpustack',\n 'ovms',\n])\n\nexport function isSupportedFetchProvider(id: string): boolean {\n return FETCH_SUPPORTED_PROVIDERS.has(id)\n}\n"}}, "src-tauri/src/llm/agent_builder.rs": {"63aab8d2537e9f0c02e06a0878032bf3c6974282": {"t": 1785846964.9587667, "n": 48637, "h": "50d68e59aa071952", "c": "//! Agent builder module for unified LLM agent creation across all providers.\n//!\n//! This module provides a unified interface for creating rig agents with full configuration\n//! (preamble, temperature, max_tokens, etc.) regardless of the underlying provider.\n\nuse std::path::PathBuf;\nuse std::sync::Arc;\n\nuse anyhow::Result;\nuse rig::OneOrMany;\nuse rig::agent::{Agent, AgentBuilder};\nuse rig::client::{CompletionClient, Nothing};\nuse rig::completion::{CompletionModel, Message};\nuse rig::message::AssistantContent;\nuse rig::providers::{\n anthropic, azure, cohere, deepseek, galadriel, gemini, groq, hyperbolic, mira, mistral,\n moonshot, ollama, openai, openrouter, perplexity, together, xai,\n};\nuse tokio_util::sync::CancellationToken;\n\nuse crate::llm::ChatResponse;\nuse crate::llm::agent_streaming;\nuse crate::llm::common::{StreamChunkType, build_user_content, create_http_client};\nuse crate::llm::tool_registry::ToolRegistry;\nuse crate::llm::tools::bash::{SharedBashSession, TempFileList};\nuse crate::llm::tools::{\n BashTool, EditTool, GlobTool, GrepTool, KillShellTool, McpSchemaTool, McpToolUseTool, ReadTool,\n SkillTool, WebFetchTool, WebSearchTool, WriteTool,\n};\nuse crate::llm::{\n anthropic as anthropic_provider, azure as azure_provider, cohere as cohere_provider,\n deepseek as deepseek_provider, galadriel as galadriel_provider, gemini as gemini_provider,\n groq as groq_provider, hyperbolic as hyperbolic_provider, minimax as minimax_provider,\n minimax_cn as minimax_cn_provider, mira as mira_provider, mistral as mistral_provider,\n moonshot as moonshot_provider, ollama as ollama_provider, openai as openai_provider,\n openrouter as openrouter_provider, perplexity as perplexity_provider,\n together as together_provider, xai as xai_provider,\n};\nuse crate::models::ModelParameters;\n\n/// Configuration for building an agent.\n/// Combines system prompt with model parameters and tool registry.\n#[derive(Clone, Default)]\npub struct AgentConfig {\n /// System prompt (preamble) for the agent\n pub system_prompt: Option<String>,\n /// Model parameters (temperature, max_tokens, etc.)\n pub model_params: ModelParameters,\n /// Optional tool registry for function calling\n pub tool_registry: Option<ToolRegistry>,\n /// Optional list of specific tool names to enable (if None, all tools are enabled)\n pub enabled_tools: Option<Vec<String>>,\n /// Enable built-in web search tool\n pub enable_web_search: bool,\n /// Enable built-in web fetch tool\n pub enable_web_fetch: bool,\n /// Enable built-in bash tool\n pub enable_bash: bool,\n /// Default working directory for bash tool\n pub bash_working_directory: Option<String>,\n /// Enable built-in file read tool\n pub enable_read: bool,\n /// Enable built-in file edit tool\n pub enable_edit: bool,\n /// Enable built-in file write tool\n pub enable_write: bool,\n /// Enable built-in grep (content search) tool\n pub enable_grep: bool,\n /// Enable built-in glob (file pattern matching) tool\n pub enable_glob: bool,\n /// Enable built-in kill_shell tool\n pub enable_kill_shell: bool,\n /// Default working directory for grep tool\n pub grep_working_directory: Option<String>,\n /// Default working directory for glob tool\n pub glob_working_directory: Option<String>,\n /// Shared bash session handle (for conversation-level persistence)\n pub bash_session: Option<SharedBashSession>,\n /// Abort notification handle for cooperative bash cancellation\n pub bash_abort_notify: Option<Arc<tokio::sync::Notify>>,\n /// Shared temp file tracker for bash output truncation cleanup\n pub bash_temp_files: Option<TempFileList>,\n /// MCP schema lookup tool with embedded catalog\n pub mcp_schema_tool: Option<McpSchemaTool>,\n /// MCP tool execution tool\n pub mcp_tool_use: Option<McpToolUseTool>,\n /// Skill tool with embedded catalog\n pub skill_tool: Option<SkillTool>,\n /// Project root directory for path security enforcement\n pub project_root: Option<PathBuf>,\n}\n\nimpl AgentConfig {\n pub fn new() -> Self {\n Self::default()\n }\n\n pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {\n self.system_prompt = Some(prompt.into());\n self\n }\n\n pub fn with_model_params(mut self, params: ModelParameters) -> Self {\n self.model_params = params;\n self\n }\n\n pub fn with_temperature(mut self, temp: f64) -> Self {\n self.model_params.temperature = Some(temp);\n self\n }\n\n pub fn with_max_tokens(mut self, tokens: i64) -> Self {\n self.model_params.max_tokens = Some(tokens);\n self\n }\n\n pub fn with_additional_params(mut self, params: serde_json::Value) -> Self {\n self.model_params.additional_params = Some(params);\n self\n }\n\n pub fn with_tool_registry(mut self, registry: ToolRegistry) -> Self {\n self.tool_registry = Some(registry);\n self\n }\n\n pub fn with_enabled_tools(mut self, tools: Vec<String>) -> Self {\n self.enabled_tools = Some(tools);\n self\n }\n\n pub fn with_default_tools(mut self) -> Self {\n self.tool_registry = Some(ToolRegistry::with_defaults());\n self\n }\n\n /// Set the MCP schema lookup tool with embedded catalog\n pub fn with_mcp_schema_tool(mut self, tool: McpSchemaTool) -> Self {\n self.mcp_schema_tool = Some(tool);\n self\n }\n\n /// Set the MCP tool execution tool\n pub fn with_mcp_tool_use(mut self, tool: McpToolUseTool) -> Self {\n self.mcp_tool_use = Some(tool);\n self\n }\n\n /// Enable the built-in web search tool\n pub fn with_web_search(mut self) -> Self {\n self.enable_web_search = true;\n self\n }\n\n /// Enable the built-in web fetch tool\n pub fn with_web_fetch(mut self) -> Self {\n self.enable_web_fetch = true;\n self\n }\n\n /// Enable the built-in bash tool\n pub fn with_bash(mut self) -> Self {\n self.enable_bash = true;\n self\n }\n\n /// Set the default working directory for bash tool\n pub fn with_bash_working_directory(mut self, dir: String) -> Self {\n self.bash_working_directory = Some(dir);\n self\n }\n\n /// Enable the built-in file read tool\n pub fn with_read(mut self) -> Self {\n self.enable_read = true;\n self\n }\n\n /// Enable the built-in file edit tool\n pub fn with_edit(mut self) -> Self {\n self.enable_edit = true;\n self\n }\n\n /// Enable the built-in file write tool\n pub fn with_write(mut self) -> Self {\n self.enable_write = true;\n self\n }\n\n /// Enable the built-in grep (content search) tool\n pub fn with_grep(mut self) -> Self {\n self.enable_grep = true;\n self\n }\n\n /// Enable the built-in glob (file pattern matching) tool\n pub fn with_glob(mut self) -> Self {\n self.enable_glob = true;\n self\n }\n\n /// Set the default working directory for grep tool\n pub fn with_grep_working_directory(mut self, dir: String) -> Self {\n self.grep_working_directory = Some(dir);\n self\n }\n\n /// Set the default working directory for glob tool\n pub fn with_glob_working_directory(mut self, dir: String) -> Self {\n self.glob_working_directory = Some(dir);\n self\n }\n\n /// Enable the built-in kill_shell tool\n pub fn with_kill_shell(mut self) -> Self {\n self.enable_kill_shell = true;\n self\n }\n\n /// Set the project root directory for path security enforcement.\n /// Write operations are restricted to this directory; sensitive paths\n /// outside it are blocked for reads.\n pub fn with_project_root(mut self, root: PathBuf) -> Self {\n self.project_root = Some(root);\n self\n }\n\n /// Set a shared bash session handle for conversation-level persistence\n pub fn with_bash_session(mut self, session: SharedBashSession) -> Self {\n self.bash_session = Some(session);\n self\n }\n\n /// Set the abort notification handle for cooperative bash cancellation\n pub fn with_bash_abort_notify(mut self, notify: Arc<tokio::sync::Notify>) -> Self {\n self.bash_abort_notify = Some(notify);\n self\n }\n\n /// Set a shared temp file tracker for bash output truncation cleanup\n pub fn with_bash_temp_files(mut self, files: TempFileList) -> Self {\n self.bash_temp_files = Some(files);\n self\n }\n\n /// Set the skill tool with embedded catalog\n pub fn with_skill_tool(mut self, tool: SkillTool) -> Self {\n self.skill_tool = Some(tool);\n self\n }\n\n /// Enable all built-in tools\n pub fn with_builtin_tools(mut self) -> Self {\n self.enable_web_search = true;\n self.enable_web_fetch = true;\n self.enable_bash = true;\n self.enable_read = true;\n self.enable_edit = true;\n self.enable_write = true;\n self.enable_grep = true;\n self.enable_glob = true;\n self\n }\n}\n\n// Type aliases for the completion model types used by each provider\ntype AnthropicCompletionModel = anthropic::completion::CompletionModel;\ntype AzureCompletionModel = azure::CompletionModel;\ntype CohereCompletionModel = cohere::CompletionModel;\ntype DeepSeekCompletionModel = deepseek::CompletionModel;\ntype GaladrielCompletionModel = galadriel::CompletionModel;\ntype GeminiCompletionModel = gemini::completion::CompletionModel;\ntype GroqCompletionModel = groq::CompletionModel;\ntype HyperbolicCompletionModel = hyperbolic::CompletionModel;\n// MiniMax uses OpenAI-compatible API with string content format\ntype MiniMaxCompletionModel = crate::llm::openai_compat::CompletionModel;\ntype MiraCompletionModel = mira::CompletionModel;\ntype MistralCompletionModel = mistral::CompletionModel;\ntype MoonshotCompletionModel = moonshot::CompletionModel;\ntype OllamaCompletionModel = ollama::CompletionModel;\ntype OpenAICompatCompletionModel = crate::llm::openai_compat::CompletionModel;\n// OpenAI uses ResponsesCompletionModel by default for agent()\ntype OpenAICompletionModel = openai::responses_api::ResponsesCompletionModel;\ntype OpenRouterCompletionModel = openrouter::CompletionModel;\ntype PerplexityCompletionModel = perplexity::CompletionModel;\ntype TogetherCompletionModel = together::CompletionModel;\ntype XAICompletionModel = xai::completion::CompletionModel;\n\n/// Enum to hold different provider agent types.\n/// This allows us to handle all providers uniformly while maintaining type safety.\npub enum ProviderAgent {\n Anthropic(Agent<AnthropicCompletionModel>),\n Azure(Agent<AzureCompletionModel>),\n Cohere(Agent<CohereCompletionModel>),\n DeepSeek(Agent<DeepSeekCompletionModel>),\n Galadriel(Agent<GaladrielCompletionModel>),\n Gemini(Agent<GeminiCompletionModel>),\n Groq(Agent<GroqCompletionModel>),\n Hyperbolic(Agent<HyperbolicCompletionModel>),\n MiniMax(Agent<MiniMaxCompletionModel>),\n MiniMaxCn(Agent<MiniMaxCompletionModel>),\n Mira(Agent<MiraCompletionModel>),\n OpenAICompat(Agent<OpenAICompatCompletionModel>),\n Mistral(Agent<MistralCompletionModel>),\n Moonshot(Agent<MoonshotCompletionModel>),\n Ollama(Agent<OllamaCompletionModel>),\n OpenAI(Agent<OpenAICompletionModel>),\n OpenRouter(Agent<OpenRouterCompletionModel>),\n Perplexity(Agent<PerplexityCompletionModel>),\n Together(Agent<TogetherCompletionModel>),\n XAI(Agent<XAICompletionModel>),\n}\n\n/// Create an OpenAI agent with full configuration\n/// Adds default reasoning: {\"effort\": \"medium\"} parameter for gpt-5 models (extended thinking support)\npub fn create_openai_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<OpenAICompletionModel>> {\n let http_client = create_http_client();\n let client = openai::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url.unwrap_or(openai_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n // Add default reasoning param for gpt-5 models if user hasn't set additional_params\n let mut openai_config = config.clone();\n if openai_config.model_params.additional_params.is_none() && model_id.starts_with(\"gpt-5\") {\n openai_config.model_params.additional_params = Some(serde_json::json!({\n \"reasoning\": { \"effort\": \"medium\" }\n }));\n }\n\n Ok(build_agent(client.agent(model_id), &openai_config))\n}\n\n/// Create an agent for custom OpenAI-compatible providers using the Chat Completions API.\n/// Uses openai_compat::CompletionModel which serializes content as plain strings for\n/// maximum compatibility with providers that don't support structured content arrays.\nfn create_custom_openai_chat_completions_agent(\n api_key: &str,\n base_url: &str,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<OpenAICompatCompletionModel>> {\n let http_client = create_http_client();\n let client = moonshot::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url)\n .http_client(http_client)\n .build()?;\n\n let model = crate::llm::openai_compat::CompletionModel::new(client, model_id);\n Ok(build_agent(rig::agent::AgentBuilder::new(model), config))\n}\n\n/// Create an OpenRouter agent with full configuration\n/// Uses the dedicated OpenRouter provider for better compatibility\n/// Adds default reasoning: {\"effort\": \"medium\"} parameter for extended thinking support\npub fn create_openrouter_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<OpenRouterCompletionModel>> {\n let http_client = create_http_client();\n let client = openrouter::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url.unwrap_or(openrouter_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n let mut openrouter_config = config.clone();\n if openrouter_config.model_params.additional_params.is_none() {\n // Extended thinking (reasoning.effort) can cause \"Invalid signature in thinking\n // block\" errors during multi-turn tool use if the streaming provider doesn't\n // supply reasoning signatures. Only enable by default for tool-free agents.\n let has_tools = config.enable_web_search\n || config.enable_web_fetch\n || config.enable_bash\n || config.enable_read\n || config.enable_edit\n || config.enable_write\n || config.enable_grep\n || config.enable_glob\n || config.enable_kill_shell\n || config.mcp_schema_tool.is_some()\n || config.mcp_tool_use.is_some()\n || config.skill_tool.is_some();\n\n if !has_tools {\n openrouter_config.model_params.additional_params = Some(serde_json::json!({\n \"reasoning\": { \"effort\": \"medium\" }\n }));\n }\n }\n\n // Auto-inject modalities for image-capable models (e.g. gemini-3.1-flash-image-preview)\n if model_id.contains(\"image-preview\") || model_id.contains(\"image-generation\") {\n let mut params = openrouter_config\n .model_params\n .additional_params\n .unwrap_or(serde_json::json!({}));\n if let Some(obj) = params.as_object_mut() {\n obj.insert(\n \"modalities\".to_string(),\n serde_json::json!([\"image\", \"text\"]),\n );\n }\n openrouter_config.model_params.additional_params = Some(params);\n }\n\n Ok(build_agent(client.agent(model_id), &openrouter_config))\n}\n\n/// Create an Ollama agent with full configuration\npub fn create_ollama_agent(\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<OllamaCompletionModel>> {\n let http_client = create_http_client();\n let client = ollama::Client::<reqwest::Client>::builder()\n .api_key(Nothing)\n .base_url(base_url.unwrap_or(ollama_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n Ok(build_agent(client.agent(model_id), config))\n}\n\n/// Default max_tokens for Anthropic (required by the API, unlike OpenAI)\nconst ANTHROPIC_DEFAULT_MAX_TOKENS: i64 = 8192;\n\n/// Create an Anthropic agent with full configuration\npub fn create_anthropic_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<AnthropicCompletionModel>> {\n let http_client = create_http_client();\n let client = anthropic::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url.unwrap_or(anthropic_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n let mut anthropic_config = config.clone();\n if anthropic_config.model_params.max_tokens.is_none() {\n anthropic_config.model_params.max_tokens = Some(ANTHROPIC_DEFAULT_MAX_TOKENS);\n }\n\n let model = client.completion_model(model_id).with_prompt_caching();\n Ok(build_agent(AgentBuilder::new(model), &anthropic_config))\n}\n\n/// Create an Azure OpenAI agent with full configuration.\n/// The base_url is used as the Azure endpoint.\npub fn create_azure_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<AzureCompletionModel>> {\n let http_client = create_http_client();\n let endpoint = base_url.ok_or_else(|| {\n anyhow::anyhow!(\"Azure OpenAI requires an endpoint URL (set as base URL)\")\n })?;\n let client = azure::Client::<reqwest::Client>::builder()\n .api_key(azure::AzureOpenAIAuth::ApiKey(api_key.to_string()))\n .azure_endpoint(endpoint.to_string())\n .api_version(azure_provider::DEFAULT_API_VERSION)\n .http_client(http_client)\n .build()?;\n\n Ok(build_agent(client.agent(model_id), config))\n}\n\n/// Create a Cohere agent with full configuration\npub fn create_cohere_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<CohereCompletionModel>> {\n let http_client = create_http_client();\n let client = cohere::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url.unwrap_or(cohere_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n Ok(build_agent(client.agent(model_id), config))\n}\n\n/// Create a DeepSeek agent with full configuration\npub fn create_deepseek_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<DeepSeekCompletionModel>> {\n let http_client = create_http_client();\n let client = deepseek::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url.unwrap_or(deepseek_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n Ok(build_agent(client.agent(model_id), config))\n}\n\n/// Create a Galadriel agent with full configuration\npub fn create_galadriel_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<GaladrielCompletionModel>> {\n let http_client = create_http_client();\n let client = galadriel::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url.unwrap_or(galadriel_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n Ok(build_agent(client.agent(model_id), config))\n}\n\n/// Create a Google Gemini agent with full configuration\npub fn create_gemini_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<GeminiCompletionModel>> {\n let http_client = create_http_client();\n let client = gemini::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url.unwrap_or(gemini_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n Ok(build_agent(client.agent(model_id), config))\n}\n\n/// Create a Groq agent with full configuration\npub fn create_groq_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<GroqCompletionModel>> {\n let http_client = create_http_client();\n let client = groq::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url.unwrap_or(groq_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n Ok(build_agent(client.agent(model_id), config))\n}\n\n/// Create a Hyperbolic agent with full configuration\npub fn create_hyperbolic_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<HyperbolicCompletionModel>> {\n let http_client = create_http_client();\n let client = hyperbolic::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url.unwrap_or(hyperbolic_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n Ok(build_agent(client.agent(model_id), config))\n}\n\n/// Create a MiniMax agent with full configuration (international version)\n/// Uses openai_compat::CompletionModel for proper string content serialization.\npub fn create_minimax_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<MiniMaxCompletionModel>> {\n let http_client = create_http_client();\n let client = moonshot::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url.unwrap_or(minimax_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n let model = crate::llm::openai_compat::CompletionModel::new(client, model_id);\n Ok(build_agent(rig::agent::AgentBuilder::new(model), config))\n}\n\n/// Create a MiniMax CN agent with full configuration (China version)\n/// Uses openai_compat::CompletionModel for proper string content serialization.\npub fn create_minimax_cn_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<MiniMaxCompletionModel>> {\n let http_client = create_http_client();\n let client = moonshot::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url.unwrap_or(minimax_cn_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n let model = crate::llm::openai_compat::CompletionModel::new(client, model_id);\n Ok(build_agent(rig::agent::AgentBuilder::new(model), config))\n}\n\n/// Create a Mira agent with full configuration\npub fn create_mira_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<MiraCompletionModel>> {\n let http_client = create_http_client();\n let client = mira::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url.unwrap_or(mira_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n Ok(build_agent(client.agent(model_id), config))\n}\n\n/// Create a Mistral agent with full configuration\npub fn create_mistral_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<MistralCompletionModel>> {\n let http_client = create_http_client();\n let client = mistral::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url.unwrap_or(mistral_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n Ok(build_agent(client.agent(model_id), config))\n}\n\n/// Create a Moonshot agent with full configuration\npub fn create_moonshot_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<MoonshotCompletionModel>> {\n let http_client = create_http_client();\n let client = moonshot::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url.unwrap_or(moonshot_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n Ok(build_agent(client.agent(model_id), config))\n}\n\n/// Create a Perplexity agent with full configuration\npub fn create_perplexity_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<PerplexityCompletionModel>> {\n let http_client = create_http_client();\n let client = perplexity::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url.unwrap_or(perplexity_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n Ok(build_agent(client.agent(model_id), config))\n}\n\n/// Create a Together AI agent with full configuration\npub fn create_together_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<TogetherCompletionModel>> {\n let http_client = create_http_client();\n let client = together::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url.unwrap_or(together_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n Ok(build_agent(client.agent(model_id), config))\n}\n\n/// Create an xAI agent with full configuration\npub fn create_xai_agent(\n api_key: &str,\n base_url: Option<&str>,\n model_id: &str,\n config: &AgentConfig,\n) -> Result<Agent<XAICompletionModel>> {\n let http_client = create_http_client();\n let client = xai::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url.unwrap_or(xai_provider::DEFAULT_BASE_URL))\n .http_client(http_client)\n .build()?;\n\n Ok(build_agent(client.agent(model_id), config))\n}\n\n/// Helper function to apply common configuration to any agent builder\nfn build_agent<M: CompletionModel>(\n mut builder: rig::agent::AgentBuilder<M>,\n config: &AgentConfig,\n) -> Agent<M> {\n // Apply system prompt (preamble)\n if let Some(ref preamble) = config.system_prompt {\n builder = builder.preamble(preamble);\n }\n\n // Apply model parameters\n let params = &config.model_params;\n\n // Apply temperature\n if let Some(temp) = params.temperature {\n builder = builder.temperature(temp);\n }\n\n // Apply max tokens (convert from i64 to u64)\n if let Some(tokens) = params.max_tokens {\n builder = builder.max_tokens(tokens as u64);\n }\n\n // Apply additional params\n if let Some(ref additional) = params.additional_params {\n builder = builder.additional_params(additional.clone());\n }\n\n // Apply tools if a tool registry is provided\n // Note: Tool registration is prepared here but actual integration with rig\n // requires provider-specific implementation. This is a placeholder for future\n // enhancement when rig adds full tool support for all providers.\n if let Some(ref registry) = config.tool_registry {\n // Get the list of tools to register\n let tools = if let Some(ref enabled) = config.enabled_tools {\n registry.to_openai_functions_by_names(enabled)\n } else {\n registry.to_openai_functions()\n };\n\n // Log tool registration (actual registration depends on provider support)\n if !tools.is_empty() {\n tracing::info!(\"🔧 Tool registry prepared with {} tool(s)\", tools.len());\n // TODO: Apply tools when rig library supports it\n // builder = builder.tools(tools);\n }\n }\n\n // Check if we need to add any native tools or MCP tools\n // Note: .tool() transforms AgentBuilder into AgentBuilderSimple,\n // so we need to handle the transition carefully\n let has_tools = config.enable_web_search\n || config.enable_web_fetch\n || config.enable_bash\n || config.enable_read\n || config.enable_edit\n || config.enable_write\n || config.enable_grep\n || config.enable_glob\n || config.mcp_schema_tool.is_some()\n || config.mcp_tool_use.is_some()\n || config.skill_tool.is_some();\n\n if has_tools {\n return build_agent_with_tools(builder, config);\n }\n\n builder.build()\n}\n\n/// Build agent with native tools and/or MCP tools.\n///\n/// In rig-core 0.32, `AgentBuilder` uses a typestate pattern: the first `.tool()` call\n/// transitions from `NoToolConfig` to `WithBuilderTools`, and subsequent `.tool()` calls\n/// return `Self`. This eliminates the cascading if-blocks previously needed.\nfn build_agent_with_tools<M: CompletionModel>(\n builder: rig::agent::AgentBuilder<M>,\n config: &AgentConfig,\n) -> Agent<M> {\n let create_bash_tool = || -> BashTool {\n let mut tool = if let Some(ref session) = config.bash_session {\n tracing::info!(\"🖥️ Bash tool using shared conversation session\");\n BashTool::with_session(session.clone(), config.bash_working_directory.clone())\n } else if let Some(ref dir) = config.bash_working_directory {\n tracing::info!(\"🖥️ Bash tool configured with working directory: {}\", dir);\n BashTool::with_working_directory(dir.clone())\n } else {\n BashTool::new()\n };\n if let Some(ref notify) = config.bash_abort_notify {\n tool = tool.with_abort_notify(notify.clone());\n }\n if let Some(ref tracker) = config.bash_temp_files {\n tool = tool.with_temp_file_tracker(tracker.clone());\n }\n if let Some(ref root) = config.project_root {\n tool = tool.with_project_root(root.clone());\n }\n tool\n };\n\n let create_read_tool = || -> ReadTool {\n if let Some(ref root) = config.project_root {\n ReadTool::with_project_root(root.clone())\n } else {\n ReadTool::new()\n }\n };\n\n let create_edit_tool = || -> EditTool {\n if let Some(ref root) = config.project_root {\n EditTool::with_project_root(root.clone())\n } else {\n EditTool::new()\n }\n };\n\n let create_write_tool = || -> WriteTool {\n if let Some(ref root) = config.project_root {\n WriteTool::with_project_root(root.clone())\n } else {\n WriteTool::new()\n }\n };\n\n let create_grep_tool = || -> GrepTool {\n let tool = if let Some(ref dir) = config.grep_working_directory {\n tracing::info!(\"🔎 Grep tool configured with working directory: {}\", dir);\n GrepTool::with_working_directory(dir.clone())\n } else {\n GrepTool::new()\n };\n if let Some(ref root) = config.project_root {\n tool.with_project_root(root.clone())\n } else {\n tool\n }\n };\n\n let create_glob_tool = || -> GlobTool {\n let tool = if let Some(ref dir) = config.glob_working_directory {\n tracing::info!(\"📂 Glob tool configured with working directory: {}\", dir);\n GlobTool::with_working_directory(dir.clone())\n } else {\n GlobTool::new()\n };\n if let Some(ref root) = config.project_root {\n tool.with_project_root(root.clone())\n } else {\n tool\n }\n };\n\n // Create bash tool once (if enabled) so kill_shell can share its session\n let bash_tool_instance: Option<BashTool> = if config.enable_bash {\n Some(create_bash_tool())\n } else {\n None\n };\n\n // Transition AgentBuilder from NoToolConfig -> WithBuilderTools using the\n // highest-priority enabled tool, then chain all remaining tools.\n macro_rules! first_tool {\n ($builder:expr) => {{\n match first_added(config) {\n FirstTool::WebSearch => $builder.tool(WebSearchTool::new()),\n FirstTool::WebFetch => $builder.tool(WebFetchTool::new()),\n FirstTool::Bash => $builder.tool(bash_tool_instance.as_ref().unwrap().clone()),\n FirstTool::Read => $builder.tool(create_read_tool()),\n FirstTool::Edit => $builder.tool(create_edit_tool()),\n FirstTool::Write => $builder.tool(create_write_tool()),\n FirstTool::Grep => $builder.tool(create_grep_tool()),\n FirstTool::Glob => $builder.tool(create_glob_tool()),\n FirstTool::McpSchema => $builder.tool(config.mcp_schema_tool.clone().unwrap()),\n FirstTool::McpToolUse => $builder.tool(config.mcp_tool_use.clone().unwrap()),\n FirstTool::Skill => $builder.tool(config.skill_tool.clone().unwrap()),\n }\n }};\n }\n\n let first = first_added(config);\n let mut sb = first_tool!(builder);\n\n if config.enable_web_fetch && first != FirstTool::WebFetch {\n tracing::info!(\"🌐 Adding web_fetch tool to agent\");\n sb = sb.tool(WebFetchTool::new());\n }\n if let Some(ref bash) = bash_tool_instance {\n if first != FirstTool::Bash {\n tracing::info!(\"🖥️ Adding bash tool to agent\");\n sb = sb.tool(bash.clone());\n }\n if config.enable_kill_shell {\n tracing::info!(\"🔪 Adding kill_shell tool to agent\");\n sb = sb.tool(KillShellTool::new(bash.session_handle()));\n }\n }\n if config.enable_read && first != FirstTool::Read {\n tracing::info!(\"📖 Adding read tool to agent\");\n sb = sb.tool(create_read_tool());\n }\n if config.enable_edit && first != FirstTool::Edit {\n tracing::info!(\"✏️ Adding edit tool to agent\");\n sb = sb.tool(create_edit_tool());\n }\n if config.enable_write && first != FirstTool::Write {\n tracing::info!(\"📝 Adding write tool to agent\");\n sb = sb.tool(create_write_tool());\n }\n if config.enable_grep && first != FirstTool::Grep {\n tracing::info!(\"🔎 Adding grep tool to agent\");\n sb = sb.tool(create_grep_tool());\n }\n if config.enable_glob && first != FirstTool::Glob {\n tracing::info!(\"📂 Adding glob tool to agent\");\n sb = sb.tool(create_glob_tool());\n }\n if config.mcp_schema_tool.is_some() && first != FirstTool::McpSchema {\n tracing::info!(\"📋 Adding mcp_schema tool to agent\");\n sb = sb.tool(config.mcp_schema_tool.clone().unwrap());\n }\n if config.mcp_tool_use.is_some() && first != FirstTool::McpToolUse {\n tracing::info!(\"🔌 Adding mcp_tool_use tool to agent\");\n sb = sb.tool(config.mcp_tool_use.clone().unwrap());\n }\n if config.skill_tool.is_some() && first != FirstTool::Skill {\n tracing::info!(\"📋 Adding skill tool to agent\");\n sb = sb.tool(config.skill_tool.clone().unwrap());\n }\n\n sb.build()\n}\n\n/// Identifies which tool was used to bootstrap the `NoToolConfig -> WithBuilderTools` transition.\n#[derive(PartialEq)]\nenum FirstTool {\n WebSearch,\n WebFetch,\n Bash,\n Read,\n Edit,\n Write,\n Grep,\n Glob,\n McpSchema,\n McpToolUse,\n Skill,\n}\n\nfn first_added(config: &AgentConfig) -> FirstTool {\n if config.enable_web_search {\n FirstTool::WebSearch\n } else if config.enable_web_fetch {\n FirstTool::WebFetch\n } else if config.enable_bash {\n FirstTool::Bash\n } else if config.enable_read {\n FirstTool::Read\n } else if config.enable_edit {\n FirstTool::Edit\n } else if config.enable_write {\n FirstTool::Write\n } else if config.enable_grep {\n FirstTool::Grep\n } else if config.enable_glob {\n FirstTool::Glob\n } else if config.mcp_schema_tool.is_some() {\n FirstTool::McpSchema\n } else if config.mcp_tool_use.is_some() {\n FirstTool::McpToolUse\n } else {\n FirstTool::Skill\n }\n}\n\n/// Create a provider agent based on provider type.\n/// `api_style` is only used for `custom_openai` to choose between Responses API and Chat Completions API.\npub fn create_provider_agent(\n provider_type: &str,\n model_id: &str,\n api_key: Option<&str>,\n base_url: Option<&str>,\n api_style: Option<&str>,\n config: &AgentConfig,\n) -> Result<ProviderAgent> {\n macro_rules! require_key {\n ($name:expr) => {\n api_key.ok_or_else(|| anyhow::anyhow!(\"{} API key required\", $name))?\n };\n }\n\n match provider_type {\n \"anthropic\" => Ok(ProviderAgent::Anthropic(create_anthropic_agent(\n require_key!(\"Anthropic\"),\n base_url,\n model_id,\n config,\n )?)),\n \"azure\" => Ok(ProviderAgent::Azure(create_azure_agent(\n require_key!(\"Azure OpenAI\"),\n base_url,\n model_id,\n config,\n )?)),\n \"cohere\" => Ok(ProviderAgent::Cohere(create_cohere_agent(\n require_key!(\"Cohere\"),\n base_url,\n model_id,\n config,\n )?)),\n \"deepseek\" => Ok(ProviderAgent::DeepSeek(create_deepseek_agent(\n require_key!(\"DeepSeek\"),\n base_url,\n model_id,\n config,\n )?)),\n \"galadriel\" => Ok(ProviderAgent::Galadriel(create_galadriel_agent(\n require_key!(\"Galadriel\"),\n base_url,\n model_id,\n config,\n )?)),\n \"gemini\" => Ok(ProviderAgent::Gemini(create_gemini_agent(\n require_key!(\"Google Gemini\"),\n base_url,\n model_id,\n config,\n )?)),\n \"groq\" => Ok(ProviderAgent::Groq(create_groq_agent(\n require_key!(\"Groq\"),\n base_url,\n model_id,\n config,\n )?)),\n \"hyperbolic\" => Ok(ProviderAgent::Hyperbolic(create_hyperbolic_agent(\n require_key!(\"Hyperbolic\"),\n base_url,\n model_id,\n config,\n )?)),\n \"minimax\" => Ok(ProviderAgent::MiniMax(create_minimax_agent(\n require_key!(\"MiniMax\"),\n base_url,\n model_id,\n config,\n )?)),\n \"minimax_cn\" => Ok(ProviderAgent::MiniMaxCn(create_minimax_cn_agent(\n require_key!(\"MiniMax CN\"),\n base_url,\n model_id,\n config,\n )?)),\n \"mira\" => Ok(ProviderAgent::Mira(create_mira_agent(\n require_key!(\"Mira\"),\n base_url,\n model_id,\n config,\n )?)),\n \"mistral\" => Ok(ProviderAgent::Mistral(create_mistral_agent(\n require_key!(\"Mistral\"),\n base_url,\n model_id,\n config,\n )?)),\n \"moonshot\" => Ok(ProviderAgent::Moonshot(create_moonshot_agent(\n require_key!(\"Moonshot\"),\n base_url,\n model_id,\n config,\n )?)),\n \"ollama\" => Ok(ProviderAgent::Ollama(create_ollama_agent(\n base_url, model_id, config,\n )?)),\n \"openai\" => Ok(ProviderAgent::OpenAI(create_openai_agent(\n require_key!(\"OpenAI\"),\n base_url,\n model_id,\n config,\n )?)),\n \"openrouter\" => Ok(ProviderAgent::OpenRouter(create_openrouter_agent(\n require_key!(\"OpenRouter\"),\n base_url,\n model_id,\n config,\n )?)),\n \"perplexity\" => Ok(ProviderAgent::Perplexity(create_perplexity_agent(\n require_key!(\"Perplexity\"),\n base_url,\n model_id,\n config,\n )?)),\n \"together\" => Ok(ProviderAgent::Together(create_together_agent(\n require_key!(\"Together AI\"),\n base_url,\n model_id,\n config,\n )?)),\n \"xai\" => Ok(ProviderAgent::XAI(create_xai_agent(\n require_key!(\"xAI\"),\n base_url,\n model_id,\n config,\n )?)),\n \"custom_openai\" => {\n let key = require_key!(\"Custom OpenAI-compatible\");\n let url = base_url.ok_or_else(|| {\n anyhow::anyhow!(\"Base URL is required for custom OpenAI-compatible providers\")\n })?;\n if api_style == Some(\"chat_completions\") {\n Ok(ProviderAgent::OpenAICompat(\n create_custom_openai_chat_completions_agent(key, url, model_id, config)?,\n ))\n } else {\n // Responses API (default)\n Ok(ProviderAgent::OpenAI(create_openai_agent(\n key,\n Some(url),\n model_id,\n config,\n )?))\n }\n }\n \"custom_anthropic\" => {\n let key = require_key!(\"Custom Anthropic-compatible\");\n let url = base_url.ok_or_else(|| {\n anyhow::anyhow!(\"Base URL is required for custom Anthropic-compatible providers\")\n })?;\n Ok(ProviderAgent::Anthropic(create_anthropic_agent(\n key,\n Some(url),\n model_id,\n config,\n )?))\n }\n _ => {\n if let Some(default_url) = openai_compat_default_url(provider_type) {\n let is_local = matches!(provider_type, \"lmstudio\" | \"gpustack\" | \"ovms\");\n let key = if is_local {\n api_key.unwrap_or(\"no-key\")\n } else {\n require_key!(provider_type)\n };\n let url = base_url.unwrap_or(default_url);\n Ok(ProviderAgent::OpenAICompat(create_openai_compat_agent(\n key,\n url,\n model_id,\n config,\n provider_type,\n )?))\n } else {\n Err(anyhow::anyhow!(\"Unknown provider: {}\", provider_type))\n }\n }\n }\n}\n\n/// Default base URLs for OpenAI-compatible providers.\n/// Returns `Some(url)` if the provider is known, `None` otherwise.\nfn openai_compat_default_url(provider_type: &str) -> Option<&'static str> {\n match provider_type {\n // International\n \"github_models\" => Some(\"https://models.inference.ai.azure.com\"),\n \"fireworks\" => Some(\"https://api.fireworks.ai/inference/v1\"),\n \"nvidia\" => Some(\"https://integrate.api.nvidia.com/v1\"),\n \"huggingface\" => Some(\"https://api-inference.huggingface.co/v1\"),\n \"cerebras\" => Some(\"https://api.cerebras.ai/v1\"),\n \"lmstudio\" => Some(\"http://localhost:1234/v1\"),\n \"gpustack\" => Some(\"http://localhost:80/v1\"),\n \"ovms\" => Some(\"http://localhost:8000/v1\"),\n // Chinese AI\n \"zhipu\" => Some(\"https://open.bigmodel.cn/api/paas/v4\"),\n \"yi\" => Some(\"https://api.lingyiwanwu.com/v1\"),\n \"baichuan\" => Some(\"https://api.baichuan-ai.com/v1\"),\n \"dashscope\" => Some(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n \"stepfun\" => Some(\"https://api.stepfun.com/v1\"),\n \"doubao\" => Some(\"https://ark.cn-beijing.volces.com/api/v3\"),\n \"hunyuan\" => Some(\"https://api.hunyuan.cloud.tencent.com/v1\"),\n \"tencent_cloud_ti\" => Some(\"https://api.lkeap.cloud.tencent.com/v1\"),\n \"baidu_cloud\" => Some(\"https://qianfan.baidubce.com/v2\"),\n \"siliconflow\" => Some(\"https://api.siliconflow.cn/v1\"),\n \"modelscope\" => Some(\"https://api-inference.modelscope.cn/v1\"),\n \"xirang\" => Some(\"https://wishub-x1.ctyun.cn/v1\"),\n \"mimo\" => Some(\"https://api.xiaomimimo.com/v1\"),\n _ => None,\n }\n}\n\nconst STRING_CONTENT_ONLY_PROVIDERS: &[&str] =\n &[\"deepseek\", \"baichuan\", \"minimax\", \"minimax_cn\", \"xirang\"];\n\nfn create_openai_compat_agent(\n api_key: &str,\n base_url: &str,\n model_id: &str,\n config: &AgentConfig,\n provider_type: &str,\n) -> Result<Agent<OpenAICompatCompletionModel>> {\n let http_client = create_http_client();\n let client = moonshot::Client::<reqwest::Client>::builder()\n .api_key(api_key)\n .base_url(base_url)\n .http_client(http_client)\n .build()?;\n\n let model = crate::llm::openai_compat::CompletionModel::new(client, model_id);\n let model = if STRING_CONTENT_ONLY_PROVIDERS.contains(&provider_type) {\n model.with_string_content_only()\n } else {\n model\n };\n Ok(build_agent(rig::agent::AgentBuilder::new(model), config))\n}\n\n/// Stream chat with an agent, handling all provider types uniformly.\n/// Returns the complete response after streaming.\npub async fn stream_chat_with_agent(\n agent: ProviderAgent,\n prompt: Message,\n chat_history: Vec<Message>,\n cancel_token: CancellationToken,\n callback: impl FnMut(String, StreamChunkType) -> bool + Send,\n log_prefix: &str,\n) -> Result<ChatResponse> {\n macro_rules! stream {\n ($agent:expr) => {\n agent_streaming::stream_agent(\n $agent,\n prompt,\n chat_history,\n cancel_token,\n callback,\n log_prefix,\n )\n .await\n };\n }\n\n match agent {\n ProviderAgent::Anthropic(a) => stream!(a),\n ProviderAgent::Azure(a) => stream!(a),\n ProviderAgent::Cohere(a) => stream!(a),\n ProviderAgent::DeepSeek(a) => stream!(a),\n ProviderAgent::Galadriel(a) => stream!(a),\n ProviderAgent::Gemini(a) => stream!(a),\n ProviderAgent::Groq(a) => stream!(a),\n ProviderAgent::Hyperbolic(a) => stream!(a),\n ProviderAgent::MiniMax(a) => stream!(a),\n ProviderAgent::MiniMaxCn(a) => stream!(a),\n ProviderAgent::Mira(a) => stream!(a),\n ProviderAgent::Mistral(a) => stream!(a),\n ProviderAgent::Moonshot(a) => stream!(a),\n ProviderAgent::Ollama(a) => stream!(a),\n ProviderAgent::OpenAI(a) => stream!(a),\n ProviderAgent::OpenAICompat(a) => stream!(a),\n ProviderAgent::OpenRouter(a) => stream!(a),\n ProviderAgent::Perplexity(a) => stream!(a),\n ProviderAgent::Together(a) => stream!(a),\n ProviderAgent::XAI(a) => stream!(a),\n }\n}\n\n/// Helper to convert chat messages to rig Message format\n/// Uses build_user_content from common.rs for consistency\npub fn build_user_message(\n text: &str,\n images: &[crate::llm::ImageData],\n files: &[crate::llm::FileData],\n) -> Message {\n Message::User {\n content: build_user_content(text, images, files),\n }\n}\n\n/// Helper to build assistant message\npub fn build_assistant_message(text: &str, reasoning: Option<&str>) -> Message {\n let mut content_items: Vec<AssistantContent> = vec![];\n if let Some(r) = reasoning {\n if !r.is_empty() {\n content_items.push(AssistantContent::reasoning(r));\n }\n }\n content_items.push(AssistantContent::Text(text.to_string().into()));\n Message::Assistant {\n id: None,\n content: OneOrMany::many(content_items)\n .expect(\"content_items should have at least one item\"),\n }\n}\n\n/// Build an assistant message that includes tool calls alongside optional text.\npub fn build_assistant_message_with_tool_calls(\n text: &str,\n tool_calls: &[crate::llm::ToolCallData],\n reasoning: Option<&str>,\n) -> Message {\n let mut content_items: Vec<AssistantContent> = vec![];\n if let Some(r) = reasoning {\n if !r.is_empty() {\n content_items.push(AssistantContent::reasoning(r));\n }\n }\n if !text.is_empty() {\n content_items.push(AssistantContent::Text(text.to_string().into()));\n }\n for tc in tool_calls {\n let args: serde_json::Value = serde_json::from_str(&tc.tool_input)\n .unwrap_or(serde_json::Value::String(tc.tool_input.clone()));\n content_items.push(if let Some(ref call_id) = tc.call_id {\n AssistantContent::tool_call_with_call_id(&tc.id, call_id.clone(), &tc.tool_name, args)\n } else {\n AssistantContent::tool_call(&tc.id, &tc.tool_name, args)\n });\n }\n if content_items.is_empty() {\n content_items.push(AssistantContent::Text(String::new().into()));\n }\n Message::Assistant {\n id: None,\n content: OneOrMany::many(content_items)\n .expect(\"content_items should have at least one item\"),\n }\n}\n\n/// Build a tool result message (sent as a User message with ToolResult content).\npub fn build_tool_result_message(\n tool_call_id: &str,\n call_id: Option<&str>,\n output: &str,\n) -> Message {\n use rig::message::{ToolResultContent, UserContent};\n let content = OneOrMany::one(ToolResultContent::text(output));\n Message::User {\n content: OneOrMany::one(if let Some(call_id) = call_id {\n UserContent::tool_result_with_call_id(tool_call_id, call_id.to_string(), content)\n } else {\n UserContent::tool_result(tool_call_id, content)\n }),\n }\n}\n"}}}}