Skip to main content

atomcode_core/ctx/
resolver.rs

1//! [`for_provider`] — 按 provider 配置选 [`CtxBuilder`] 实现的唯一入口。
2//!
3//! 添加新模型的 ctx 管理策略 **只需两步**:
4//!
5//! 1. 新建文件 `crates/atomcode-core/src/ctx/<name>.rs`,定义
6//!    `pub struct XxxCtx` 并 `impl CtxBuilder for XxxCtx`
7//! 2. 在 [`for_provider`] 里插一条 `if` 分支,匹配
8//!    `provider.provider_type` 或 `provider.model` 前缀。
9//!
10//! 未命中任何规则 → [`DefaultCtx`],保留 atomcode 当前的上下文行为
11//! 不变。
12//!
13//! 规则表是 **按顺序匹配**:靠上的规则优先生效。当一个 provider
14//! 同时匹配多个规则(例如 `provider_type == "ollama"` 且
15//! `model.starts_with("claude-")`,虽然现实中极少见),靠前的赢。
16
17use std::sync::Arc;
18
19use super::{CtxBuilder, DefaultCtx};
20use crate::config::provider::ProviderConfig;
21
22/// 给定 provider config 返回对应的 [`CtxBuilder`]。
23///
24/// 由 [`crate::agent::AgentLoop::new`] 在会话开始时调用一次,
25/// 以及由 `AgentCommand::ReloadConfig` 在用户切模型时重新调用。
26///
27/// 返回 `Arc` 而非 `Box`:AgentLoop 与它持有的 `TurnRunner` 共享
28/// 同一个 ctx 实例,确保 datalog build_messages 和 runner 实际发送
29/// 走同一条 ctx 路径(不会因为一边走 trait 派发、另一边走自由函数
30/// 而漂移)。ReloadConfig 重建时两处一起更新。
31pub fn for_provider(provider: &ProviderConfig) -> Arc<dyn CtxBuilder> {
32    // ── 规则表(按注册顺序匹配)─────────────────────────
33
34    // Ollama: 本地小窗口模型
35    if provider.provider_type == "ollama" {
36        return Arc::new(super::ollama::OllamaCtx::new(provider));
37    }
38
39    // (未来其它模型在此插入 —— Claude / GPT-4 / 自定义微调等)
40    //
41    // 示例:
42    //   if provider.model.starts_with("claude-") {
43    //       return Arc::new(super::claude::ClaudeCtx::new(provider));
44    //   }
45
46    // ── Fallback ────────────────────────────────────────
47    Arc::new(DefaultCtx::new(provider))
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    fn provider(ptype: &str, model: &str, ctx: usize) -> ProviderConfig {
55        ProviderConfig {
56            provider_type: ptype.into(),
57            api_key: None,
58            model: model.into(),
59            base_url: None,
60            system_prompt: None,
61            user_agent: None,
62            context_window: ctx,
63            max_tokens: None,
64            thinking_type: None,
65            thinking_keep: None,
66            reasoning_history: None,
67            thinking_enabled: None,
68            thinking_budget: None,
69            skip_tls_verify: false,
70            ephemeral: false,
71
72}
73    }
74
75    #[test]
76    fn resolves_ollama_by_provider_type() {
77        let p = provider("ollama", "llama3", 8_000);
78        assert_eq!(for_provider(&p).name(), "ollama");
79    }
80
81    #[test]
82    fn resolves_default_for_unknown_provider() {
83        let p = provider("openai", "gpt-4o", 128_000);
84        assert_eq!(for_provider(&p).name(), "default");
85
86        let p = provider("anthropic", "claude-3-5-sonnet", 200_000);
87        assert_eq!(for_provider(&p).name(), "default");
88
89        let p = provider("", "", 0);
90        assert_eq!(for_provider(&p).name(), "default");
91    }
92
93    #[test]
94    fn ollama_rule_matches_any_model_under_ollama_provider() {
95        // 不管 model 字段是什么,provider_type == "ollama" 就走 Ollama
96        for model in ["llama3-8b", "qwen2.5-coder", "deepseek-coder-v2", ""] {
97            let p = provider("ollama", model, 8_000);
98            assert_eq!(for_provider(&p).name(), "ollama", "model={}", model);
99        }
100    }
101}