codetether_agent/session/helper/
provider.rs1use 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
149pub 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}