bitrouter_core/routers/
routing_table.rs1use std::fmt;
2use std::future::Future;
3use std::sync::Arc;
4
5use serde::{Deserialize, Serialize};
6
7use crate::errors::Result;
8use crate::routers::content::RouteContext;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum ApiProtocol {
23 Openai,
25 Anthropic,
26 Google,
27 Mcp,
29 Rest,
30 Acp,
32}
33
34impl fmt::Display for ApiProtocol {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 f.write_str(match self {
37 Self::Openai => "openai",
38 Self::Anthropic => "anthropic",
39 Self::Google => "google",
40 Self::Mcp => "mcp",
41 Self::Rest => "rest",
42 Self::Acp => "acp",
43 })
44 }
45}
46
47pub struct RoutingTarget {
51 pub provider_name: String,
53 pub service_id: String,
55 pub api_protocol: ApiProtocol,
57}
58
59#[derive(Debug, Clone)]
61pub struct RouteEntry {
62 pub name: String,
64 pub provider: String,
66 pub protocol: ApiProtocol,
68}
69
70#[derive(Debug, Clone, Default, Serialize, Deserialize)]
72pub struct InputTokenPricing {
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub no_cache: Option<f64>,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub cache_read: Option<f64>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub cache_write: Option<f64>,
82}
83
84impl InputTokenPricing {
85 fn is_empty(&self) -> bool {
86 self.no_cache.is_none() && self.cache_read.is_none() && self.cache_write.is_none()
87 }
88}
89
90#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92pub struct OutputTokenPricing {
93 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub text: Option<f64>,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub reasoning: Option<f64>,
99}
100
101impl OutputTokenPricing {
102 fn is_empty(&self) -> bool {
103 self.text.is_none() && self.reasoning.is_none()
104 }
105}
106
107#[derive(Debug, Clone, Default, Serialize, Deserialize)]
109pub struct ModelPricing {
110 #[serde(default, skip_serializing_if = "InputTokenPricing::is_empty")]
111 pub input_tokens: InputTokenPricing,
112 #[serde(default, skip_serializing_if = "OutputTokenPricing::is_empty")]
113 pub output_tokens: OutputTokenPricing,
114}
115
116impl ModelPricing {
117 pub fn is_empty(&self) -> bool {
119 self.input_tokens.is_empty() && self.output_tokens.is_empty()
120 }
121}
122
123pub trait RoutingTable: Send + Sync {
127 fn route(
132 &self,
133 incoming_name: &str,
134 context: &RouteContext,
135 ) -> impl Future<Output = Result<RoutingTarget>> + Send;
136
137 fn list_routes(&self) -> Vec<RouteEntry> {
139 Vec::new()
140 }
141}
142
143impl<T: RoutingTable> RoutingTable for Arc<T> {
144 async fn route(&self, incoming_name: &str, context: &RouteContext) -> Result<RoutingTarget> {
145 (**self).route(incoming_name, context).await
146 }
147
148 fn list_routes(&self) -> Vec<RouteEntry> {
149 (**self).list_routes()
150 }
151}
152
153pub fn strip_ansi_escapes(input: &str) -> String {
163 let bytes = input.as_bytes();
164 let len = bytes.len();
165 let mut out = String::with_capacity(len);
166 let mut i = 0;
167
168 while i < len {
169 if bytes[i] == 0x1b && i + 1 < len && bytes[i + 1] == b'[' {
170 i += 2;
173 while i < len && !(0x40..=0x7E).contains(&bytes[i]) {
174 i += 1;
175 }
176 if i < len {
177 i += 1; }
179 } else {
180 let ch = input[i..].chars().next().unwrap_or_default();
183 out.push(ch);
184 i += ch.len_utf8();
185 }
186 }
187
188 out
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn strip_ansi_removes_bold() {
197 assert_eq!(
198 strip_ansi_escapes("claude-opus-4-6\x1b[1m"),
199 "claude-opus-4-6"
200 );
201 }
202
203 #[test]
204 fn strip_ansi_removes_bold_prefix() {
205 assert_eq!(
206 strip_ansi_escapes("\x1b[1mclaude-opus-4-6\x1b[0m"),
207 "claude-opus-4-6"
208 );
209 }
210
211 #[test]
212 fn strip_ansi_noop_clean_string() {
213 assert_eq!(strip_ansi_escapes("gpt-4o"), "gpt-4o");
214 }
215
216 #[test]
217 fn strip_ansi_removes_color_codes() {
218 assert_eq!(
219 strip_ansi_escapes("\x1b[32mmodel-name\x1b[0m"),
220 "model-name"
221 );
222 }
223
224 #[test]
225 fn strip_ansi_handles_empty_string() {
226 assert_eq!(strip_ansi_escapes(""), "");
227 }
228
229 #[test]
230 fn strip_ansi_preserves_brackets_without_esc() {
231 assert_eq!(strip_ansi_escapes("model[v2]"), "model[v2]");
233 }
234}