tokenmiser_providers/
lib.rs1use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8use tokenmiser_config::ProviderConfig;
9
10pub mod anthropic;
11pub mod ollama;
12pub mod openai;
13pub mod registry;
14
15pub use registry::ProviderRegistry;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ChatRequest {
20 pub model: String,
21 pub messages: Vec<ChatMessage>,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub temperature: Option<f32>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub max_tokens: Option<u32>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub top_p: Option<f32>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub stream: Option<bool>,
30 #[serde(flatten)]
32 pub extra: serde_json::Map<String, serde_json::Value>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ChatMessage {
37 pub role: String,
38 #[serde(default)]
39 pub content: serde_json::Value,
40 #[serde(flatten)]
41 pub extra: serde_json::Map<String, serde_json::Value>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ChatResponse {
47 pub id: String,
48 #[serde(default = "default_object")]
49 pub object: String,
50 #[serde(default)]
51 pub created: u64,
52 pub model: String,
53 pub choices: Vec<ChatChoice>,
54 pub usage: Usage,
55 #[serde(flatten, default)]
57 pub extra: serde_json::Map<String, serde_json::Value>,
58}
59
60fn default_object() -> String {
61 "chat.completion".into()
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct ChatChoice {
66 pub index: u32,
67 pub message: ChatMessage,
68 #[serde(default)]
69 pub finish_reason: Option<String>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub logprobs: Option<serde_json::Value>,
74}
75
76pub fn response_visible_content_empty(resp: &ChatResponse) -> bool {
80 let Some(choice) = resp.choices.first() else {
81 return true;
82 };
83 match &choice.message.content {
84 serde_json::Value::String(s) => s.trim().is_empty(),
85 serde_json::Value::Null => true,
86 serde_json::Value::Array(arr) => !arr.iter().any(|item| {
87 item.get("text")
88 .and_then(|t| t.as_str())
89 .map(|s| !s.trim().is_empty())
90 .unwrap_or(false)
91 }),
92 _ => false,
93 }
94}
95
96#[derive(Debug, Clone, Default, Serialize, Deserialize)]
97pub struct Usage {
98 #[serde(default)]
99 pub prompt_tokens: u64,
100 #[serde(default)]
101 pub completion_tokens: u64,
102 #[serde(default)]
103 pub total_tokens: u64,
104}
105
106#[derive(Debug, Error)]
107pub enum ProviderError {
108 #[error("provider {name} not registered")]
109 NotFound { name: String },
110 #[error("unknown model `{model}`: no alias, no `provider:model` prefix, no family heuristic match. Known providers: {known_providers}. Hint: use a `provider:model` prefix (e.g. `ollama:llama3.2`) or set `routing.default_provider` in config.")]
111 UnknownModel {
112 model: String,
113 known_providers: String,
114 },
115 #[error("missing api key env var: {0}")]
116 MissingApiKey(String),
117 #[error("upstream http error: {0}")]
118 Http(#[from] reqwest::Error),
119 #[error("upstream returned status {status}: {body}")]
120 Upstream { status: u16, body: String },
121 #[error("response parse error: {0}")]
122 Parse(#[from] serde_json::Error),
123 #[error("provider returned malformed response: {0}")]
124 Malformed(String),
125}
126
127#[derive(Debug, Clone)]
129pub enum StreamChunk {
130 Sse(bytes::Bytes),
132 Done,
134}
135
136#[async_trait]
137pub trait Provider: Send + Sync {
138 fn name(&self) -> &str;
139 fn config(&self) -> &ProviderConfig;
140 async fn complete(&self, req: &ChatRequest) -> Result<ChatResponse, ProviderError>;
141
142 async fn stream(
146 &self,
147 req: &ChatRequest,
148 ) -> Result<
149 futures::stream::BoxStream<'static, Result<StreamChunk, ProviderError>>,
150 ProviderError,
151 > {
152 let resp = self.complete(req).await?;
154 let json = serde_json::to_vec(&resp)?;
155 let mut sse = b"data: ".to_vec();
156 sse.extend_from_slice(&json);
157 sse.extend_from_slice(b"\n\n");
158 let chunk = StreamChunk::Sse(bytes::Bytes::from(sse));
159 let done = StreamChunk::Sse(bytes::Bytes::from_static(b"data: [DONE]\n\n"));
160 use futures::stream::StreamExt;
161 let s = futures::stream::iter(vec![Ok(chunk), Ok(done), Ok(StreamChunk::Done)]);
162 Ok(s.boxed())
163 }
164}