systemprompt_models/profile/gateway/
route.rs1use std::collections::HashMap;
12use std::collections::hash_map::DefaultHasher;
13use std::hash::{Hash, Hasher};
14
15use serde::{Deserialize, Serialize};
16use systemprompt_identifiers::{ProviderId, RouteId};
17
18use super::super::providers::{ProviderEntry, ProviderRegistry};
19use crate::services::ai::ModelPricing;
20
21fn default_route_id() -> RouteId {
22 RouteId::new("")
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
26#[serde(deny_unknown_fields)]
27pub struct GatewayRoute {
28 #[serde(default = "default_route_id")]
29 pub id: RouteId,
30 pub model_pattern: String,
31 pub provider: ProviderId,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub upstream_model: Option<String>,
34 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
35 pub extra_headers: HashMap<String, String>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub pricing: Option<ModelPricing>,
38}
39
40impl GatewayRoute {
41 pub fn matches(&self, model: &str) -> bool {
42 match_pattern(&self.model_pattern, model)
43 }
44
45 pub fn effective_upstream_model<'a>(&'a self, requested: &'a str) -> &'a str {
46 self.upstream_model.as_deref().unwrap_or(requested)
47 }
48
49 pub fn ensure_id(&mut self) {
50 if self.id.as_str().trim().is_empty() {
51 self.id = synthesize_route_id(&self.model_pattern, self.provider.as_str());
52 }
53 }
54
55 pub fn resolve<'a>(&self, registry: &'a ProviderRegistry) -> Option<&'a ProviderEntry> {
56 registry.find_provider(self.provider.as_str())
57 }
58}
59
60#[must_use]
64pub fn slugify_pattern(pattern: &str) -> String {
65 let mut out = String::with_capacity(pattern.len());
66 let mut last_dash = false;
67 for ch in pattern.chars() {
68 if ch == '*' {
69 out.push_str("star");
70 last_dash = false;
71 } else if ch.is_ascii_alphanumeric() {
72 for lc in ch.to_lowercase() {
73 out.push(lc);
74 }
75 last_dash = false;
76 } else if !last_dash && !out.is_empty() {
77 out.push('-');
78 last_dash = true;
79 }
80 }
81 while out.ends_with('-') {
82 out.pop();
83 }
84 while out.starts_with('-') {
85 out.remove(0);
86 }
87 if out.is_empty() {
88 out.push_str("route");
89 }
90 out
91}
92
93#[must_use]
98pub fn synthesize_route_id(model_pattern: &str, provider: &str) -> RouteId {
99 let mut hasher = DefaultHasher::new();
100 model_pattern.hash(&mut hasher);
101 provider.hash(&mut hasher);
102 let h = hasher.finish();
103 let hash6: String = format!("{h:016x}").chars().take(6).collect();
104 RouteId::new(format!("{}-{}", slugify_pattern(model_pattern), hash6))
105}
106
107fn match_pattern(pattern: &str, model: &str) -> bool {
108 if pattern == "*" {
109 return true;
110 }
111 if let Some(prefix) = pattern.strip_suffix('*') {
112 return model.starts_with(prefix);
113 }
114 if let Some(suffix) = pattern.strip_prefix('*') {
115 return model.ends_with(suffix);
116 }
117 pattern == model
118}