Skip to main content

codetether_agent/provider/
parse.rs

1//! Model-string parsing and provider URL normalization helpers.
2//!
3//! # Examples
4//!
5//! ```rust
6//! use codetether_agent::provider::parse_model_string;
7//!
8//! assert_eq!(parse_model_string("openai/gpt-4o"), (Some("openai"), "gpt-4o"));
9//! assert_eq!(parse_model_string("gpt-4o"), (None, "gpt-4o"));
10//! assert_eq!(
11//!     parse_model_string("openrouter/openai/gpt-5-codex"),
12//!     (Some("openrouter"), "openai/gpt-5-codex")
13//! );
14//! ```
15
16/// Parse a model string into `(provider, model)`.
17///
18/// Supports:
19/// - `provider/model` → (`provider`, `model`)
20/// - nested provider namespaces like `openai-codex/gpt-5.4`
21/// - OpenRouter-style paths like `openrouter/openai/gpt-5-codex`
22///
23/// For three-or-more path segments, the first segment is the provider and
24/// the remainder is the model ID.
25pub fn parse_model_string(s: &str) -> (Option<&str>, &str) {
26    let mut parts = s.split('/');
27    let Some(first) = parts.next() else {
28        return (None, s);
29    };
30
31    let remainder_start = first.len();
32    if remainder_start >= s.len() {
33        return (None, s);
34    }
35
36    let remainder = &s[remainder_start + 1..];
37    (Some(first), remainder)
38}
39
40/// Normalize MiniMax base URLs to the Anthropic-compatible endpoint.
41///
42/// MiniMax's docs recommend using the `/anthropic` path for their
43/// Anthropic-compatible harness. This rewrites the legacy `/v1` path.
44pub(crate) fn normalize_minimax_anthropic_base_url(base_url: &str) -> String {
45    let trimmed = base_url.trim().trim_end_matches('/');
46    if trimmed.eq_ignore_ascii_case("https://api.minimax.io/v1") {
47        "https://api.minimax.io/anthropic".to_string()
48    } else {
49        trimmed.to_string()
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn parses_provider_model() {
59        assert_eq!(
60            parse_model_string("openai/gpt-4o"),
61            (Some("openai"), "gpt-4o")
62        );
63    }
64
65    #[test]
66    fn parses_bare_model() {
67        assert_eq!(parse_model_string("gpt-4o"), (None, "gpt-4o"));
68    }
69
70    #[test]
71    fn parses_nested_provider() {
72        assert_eq!(
73            parse_model_string("openrouter/openai/gpt-5-codex"),
74            (Some("openrouter"), "openai/gpt-5-codex")
75        );
76    }
77
78    #[test]
79    fn normalizes_minimax_url() {
80        assert_eq!(
81            normalize_minimax_anthropic_base_url("https://api.minimax.io/v1"),
82            "https://api.minimax.io/anthropic"
83        );
84        assert_eq!(
85            normalize_minimax_anthropic_base_url("https://custom.host/custom"),
86            "https://custom.host/custom"
87        );
88    }
89}