Skip to main content

codetether_agent/session/helper/
provider.rs

1use crate::provider::ToolDefinition;
2use anyhow::Result;
3
4pub fn provider_has_flaky_native_tool_calling(provider_name: &str, model: &str) -> bool {
5    let provider = provider_name.to_ascii_lowercase();
6    let model = model.to_ascii_lowercase();
7    provider == "minimax"
8        || provider == "minimax-credits"
9        || model.contains("minimax")
10        || model.contains("m2.5")
11}
12
13pub fn assistant_claims_imminent_tool_use(text: &str, tool_definitions: &[ToolDefinition]) -> bool {
14    let lower = text.trim().to_ascii_lowercase();
15    if lower.is_empty() {
16        return false;
17    }
18
19    let explicit_markers = [
20        "tool call",
21        "tool calls",
22        "use a tool",
23        "use tools",
24        "call the",
25        "invoke the",
26        "let me use",
27        "i'll use",
28        "i will use",
29        "i'm going to use",
30        "i am going to use",
31    ];
32    if explicit_markers.iter().any(|m| lower.contains(m)) {
33        return true;
34    }
35
36    let action_markers = [
37        "let me check",
38        "let me inspect",
39        "let me search",
40        "let me read",
41        "let me open",
42        "first, i'll",
43        "first i will",
44        "i'll check",
45        "i'll inspect",
46        "i'll search",
47        "i'll read",
48        "i'll open",
49        "i'll look through",
50        "i'll examine",
51        "i will check",
52        "i will inspect",
53        "i will search",
54        "i will read",
55        "i will open",
56        "i will examine",
57    ];
58    if action_markers.iter().any(|m| lower.contains(m))
59        && tool_definitions.iter().any(|tool| {
60            let name = tool.name.to_ascii_lowercase();
61            lower.contains(&name)
62                || matches!(
63                    name.as_str(),
64                    "bash"
65                        | "read"
66                        | "write"
67                        | "edit"
68                        | "advanced_edit"
69                        | "multiedit"
70                        | "codesearch"
71                        | "glob"
72                        | "grep"
73                        | "ls"
74                        | "cat"
75                        | "lsp"
76                )
77        })
78    {
79        return true;
80    }
81
82    false
83}
84
85pub fn should_retry_missing_native_tool_call(
86    provider_name: &str,
87    model: &str,
88    retry_count: u8,
89    tool_definitions: &[ToolDefinition],
90    assistant_text: &str,
91    has_tool_calls: bool,
92    native_tool_promise_retry_max_retries: u8,
93) -> bool {
94    if retry_count >= native_tool_promise_retry_max_retries
95        || has_tool_calls
96        || tool_definitions.is_empty()
97        || !provider_has_flaky_native_tool_calling(provider_name, model)
98    {
99        return false;
100    }
101
102    assistant_claims_imminent_tool_use(assistant_text, tool_definitions)
103}
104
105pub fn choose_default_provider<'a>(providers: &'a [&'a str]) -> Option<&'a str> {
106    let preferred = [
107        "openai",
108        "anthropic",
109        "github-copilot",
110        "openai-codex",
111        "zai",
112        "minimax",
113        "openrouter",
114        "novita",
115        "moonshotai",
116        "google",
117    ];
118    for name in preferred {
119        if let Some(found) = providers.iter().copied().find(|p| *p == name) {
120            return Some(found);
121        }
122    }
123    providers.first().copied()
124}
125
126pub fn resolve_provider_for_session_request<'a>(
127    providers: &'a [&'a str],
128    explicit_provider: Option<&str>,
129) -> Result<&'a str> {
130    if let Some(explicit) = explicit_provider {
131        if let Some(found) = providers.iter().copied().find(|p| *p == explicit) {
132            return Ok(found);
133        }
134        anyhow::bail!(
135            "Provider '{}' selected explicitly but is unavailable. Available providers: {}",
136            explicit,
137            providers.join(", ")
138        );
139    }
140
141    choose_default_provider(providers).ok_or_else(|| anyhow::anyhow!("No providers available"))
142}
143
144pub fn prefers_temperature_one(model: &str) -> bool {
145    let normalized = model.to_ascii_lowercase();
146    normalized.contains("kimi-k2") || normalized.contains("glm-") || normalized.contains("minimax")
147}
148
149/// Returns true for models where the `temperature` parameter is deprecated
150/// and must not be sent (causes a 400 Bad Request error).
151/// Claude Opus 4.7 removed temperature support in favor of adaptive reasoning.
152pub fn temperature_is_deprecated(model: &str) -> bool {
153    let normalized = model.to_ascii_lowercase();
154    normalized.contains("opus-4-7")
155        || normalized.contains("opus-4.7")
156        || normalized.contains("4.7-opus")
157        || normalized.contains("4-7-opus")
158        || normalized.contains("opus_4_7")
159        || normalized.contains("opus_47")
160}
161
162#[cfg(test)]
163mod tests {
164    use super::temperature_is_deprecated;
165
166    #[test]
167    fn detects_opus_47_aliases() {
168        assert!(temperature_is_deprecated("claude-opus-4-7"));
169        assert!(temperature_is_deprecated("claude-opus-4.7"));
170        assert!(temperature_is_deprecated("claude-4.7-opus"));
171        assert!(temperature_is_deprecated("claude-4-7-opus"));
172        assert!(temperature_is_deprecated("claude-opus_4_7"));
173        assert!(temperature_is_deprecated("claude-opus_47"));
174        assert!(temperature_is_deprecated("us.anthropic.claude-opus-4-7"));
175    }
176
177    #[test]
178    fn non_deprecated_models_return_false() {
179        assert!(!temperature_is_deprecated("claude-sonnet-4"));
180        assert!(!temperature_is_deprecated("claude-opus-4-6"));
181        assert!(!temperature_is_deprecated("gpt-4o"));
182    }
183}