Skip to main content

llm/catalog/
transport.rs

1use std::fmt::Write as _;
2
3use thiserror::Error;
4
5/// Executable per-model transport strategy.
6///
7/// A model without an override uses its provider's default transport. Each
8/// variant here is a complete routing decision implemented by a runtime provider.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum ModelTransport {
11    /// `OpenAI` Responses API at a dedicated endpoint template.
12    OpenAiResponses { base_url_template: &'static str },
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, Error)]
16pub enum TemplateError {
17    #[error("template variable '{name}' could not be resolved in '{template}'")]
18    UnresolvedVariable { name: String, template: String },
19    #[error("template '{template}' has an unterminated '${{' placeholder")]
20    UnterminatedPlaceholder { template: String },
21}
22
23/// Expand `${VAR}` placeholders in an endpoint template.
24///
25/// `lookup` resolves a variable name to its value; returning `None` fails the
26/// expansion rather than substituting an empty string, so a misconfigured
27/// environment surfaces as an error instead of a malformed URL.
28pub fn expand_api_template(template: &str, lookup: impl Fn(&str) -> Option<String>) -> Result<String, TemplateError> {
29    let mut out = String::with_capacity(template.len());
30    let mut rest = template;
31
32    while let Some(start) = rest.find("${") {
33        out.push_str(&rest[..start]);
34        let after = &rest[start + 2..];
35        let Some(end) = after.find('}') else {
36            return Err(TemplateError::UnterminatedPlaceholder { template: template.to_string() });
37        };
38        let name = &after[..end];
39        let value = lookup(name).ok_or_else(|| TemplateError::UnresolvedVariable {
40            name: name.to_string(),
41            template: template.to_string(),
42        })?;
43        let _ = out.write_str(&value);
44        rest = &after[end + 1..];
45    }
46
47    out.push_str(rest);
48    Ok(out)
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    fn lookup<'a>(vars: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
56        move |name| vars.iter().find(|(k, _)| *k == name).map(|(_, v)| (*v).to_string())
57    }
58
59    #[test]
60    fn expands_a_single_variable() {
61        let expanded = expand_api_template(
62            "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1",
63            lookup(&[("AWS_REGION", "us-west-2")]),
64        )
65        .unwrap();
66
67        assert_eq!(expanded, "https://bedrock-mantle.us-west-2.api.aws/openai/v1");
68    }
69
70    #[test]
71    fn leaves_templates_without_placeholders_untouched() {
72        let expanded = expand_api_template("https://bedrock-mantle.us-east-1.api.aws/v1", lookup(&[])).unwrap();
73
74        assert_eq!(expanded, "https://bedrock-mantle.us-east-1.api.aws/v1");
75    }
76
77    #[test]
78    fn expands_multiple_variables() {
79        let expanded = expand_api_template(
80            "https://${HOST}/v1/projects/${PROJECT}/end",
81            lookup(&[("HOST", "example.com"), ("PROJECT", "p-1")]),
82        )
83        .unwrap();
84
85        assert_eq!(expanded, "https://example.com/v1/projects/p-1/end");
86    }
87
88    #[test]
89    fn unresolved_variable_is_an_error() {
90        let error = expand_api_template("https://x.${AWS_REGION}.aws/v1", lookup(&[])).unwrap_err();
91
92        assert_eq!(
93            error,
94            TemplateError::UnresolvedVariable {
95                name: "AWS_REGION".to_string(),
96                template: "https://x.${AWS_REGION}.aws/v1".to_string(),
97            }
98        );
99    }
100
101    #[test]
102    fn unterminated_placeholder_is_an_error() {
103        let error = expand_api_template("https://x.${AWS_REGION/v1", lookup(&[])).unwrap_err();
104
105        assert!(matches!(error, TemplateError::UnterminatedPlaceholder { .. }));
106    }
107}