1pub mod anthropic;
4pub mod bedrock;
5pub mod classify;
6mod codex_websocket;
7pub mod google;
8pub mod images;
9pub mod mistral;
10pub mod openai_completions;
11pub mod openai_responses;
12pub mod pi_messages;
13
14use std::sync::Arc;
15
16pub use codex_websocket::{
17 DebugStats as CodexWebSocketStats, close_sessions as close_codex_websockets,
18 debug_stats as codex_websocket_stats, reset_debug_stats as reset_codex_websocket_stats,
19};
20
21use futures_util::future::BoxFuture;
22use indexmap::IndexMap;
23use yapi_types::classify::{AssistantImages, ClassifierContext, ClassifierResult, ImagesContext};
24use yapi_types::message::{Content, ContentBlock, Message};
25use yapi_types::model::{ClassifierModel, ImageModel};
26
27use crate::http::Headers;
28use crate::stream::{EventStream, Provider, Request, new_output, now_ms, send_error};
29
30#[derive(Clone, Copy, Debug)]
33enum Builtin {
34 Anthropic,
35 Completions,
36 Responses,
37 AzureResponses,
38 CodexResponses,
39 Gemini,
40 Vertex,
41 Mistral,
42 Bedrock,
43 PiMessages,
44}
45
46impl Builtin {
47 const ALL: [Builtin; 10] = [
48 Builtin::Anthropic,
49 Builtin::Completions,
50 Builtin::Responses,
51 Builtin::AzureResponses,
52 Builtin::CodexResponses,
53 Builtin::Gemini,
54 Builtin::Vertex,
55 Builtin::Mistral,
56 Builtin::Bedrock,
57 Builtin::PiMessages,
58 ];
59}
60
61impl Provider for Builtin {
62 fn api(&self) -> &str {
63 match self {
64 Builtin::Anthropic => "anthropic-messages",
65 Builtin::Completions => "openai-completions",
66 Builtin::Responses => "openai-responses",
67 Builtin::AzureResponses => "azure-openai-responses",
68 Builtin::CodexResponses => "openai-codex-responses",
69 Builtin::Gemini => "google-generative-ai",
70 Builtin::Vertex => "google-vertex",
71 Builtin::Mistral => "mistral-conversations",
72 Builtin::Bedrock => "bedrock-converse-stream",
73 Builtin::PiMessages => "pi-messages",
74 }
75 }
76
77 fn stream(&self, request: Request) -> EventStream {
78 use google::Flavor as Google;
79 use openai_responses::Flavor as Responses;
80 let (sender, stream) = EventStream::channel();
81 match self {
82 Builtin::Anthropic => tokio::spawn(anthropic::run(request, sender)),
83 Builtin::Completions => tokio::spawn(openai_completions::run(request, sender)),
84 Builtin::Responses => {
85 tokio::spawn(openai_responses::run(request, sender, Responses::OpenAi))
86 }
87 Builtin::AzureResponses => {
88 tokio::spawn(openai_responses::run(request, sender, Responses::Azure))
89 }
90 Builtin::CodexResponses => {
91 tokio::spawn(openai_responses::run(request, sender, Responses::Codex))
92 }
93 Builtin::Gemini => tokio::spawn(google::run(request, sender, Google::Gemini)),
94 Builtin::Vertex => tokio::spawn(google::run(request, sender, Google::Vertex)),
95 Builtin::Mistral => tokio::spawn(mistral::run(request, sender)),
96 Builtin::Bedrock => tokio::spawn(bedrock::run(request, sender)),
97 Builtin::PiMessages => tokio::spawn(pi_messages::run(request, sender)),
98 };
99 stream
100 }
101}
102
103pub fn builtin(api: &str) -> Option<Arc<dyn Provider>> {
105 let builtin = Builtin::ALL
106 .into_iter()
107 .find(|builtin| builtin.api() == api)?;
108 Some(Arc::new(builtin))
109}
110
111pub(crate) fn normalize_tool_call_id(id: &str) -> String {
114 sanitize_id_part(id).chars().take(64).collect()
115}
116
117pub(crate) fn sanitize_id_part(text: &str) -> String {
121 text.chars()
122 .flat_map(|c| {
123 let keep = c.is_ascii_alphanumeric() || c == '_' || c == '-';
124 let (c, count) = if keep { (c, 1) } else { ('_', c.len_utf16()) };
125 std::iter::repeat_n(c, count)
126 })
127 .collect()
128}
129
130pub fn failed_stream(
132 model: &yapi_types::model::Model,
133 cancel: &tokio_util::sync::CancellationToken,
134 message: String,
135) -> EventStream {
136 let (sender, stream) = EventStream::channel();
137 send_error(&sender, new_output(model, now_ms()), cancel, message);
138 stream
139}
140
141pub(crate) fn openai_headers(
144 api_key: &str,
145 model_headers: Option<&IndexMap<String, String>>,
146) -> Headers {
147 let mut headers = Headers::default();
148 headers.set("authorization", Some(format!("Bearer {api_key}")));
149 headers.set("content-type", Some("application/json"));
150 headers.set("accept", Some("application/json"));
151 headers.extend_model(model_headers);
152 headers
153}
154
155pub(crate) fn copilot_headers(messages: &[Message]) -> Vec<(&'static str, String)> {
159 let initiator = match messages.last() {
160 Some(Message::User(_)) | None => "user",
161 Some(_) => "agent",
162 };
163 let has_image = |blocks: &[ContentBlock]| {
164 blocks
165 .iter()
166 .any(|block| matches!(block, ContentBlock::Image(_)))
167 };
168 let vision = messages.iter().any(|message| match message {
169 Message::User(user) => {
170 matches!(&user.content, Content::Blocks(blocks) if has_image(blocks))
171 }
172 Message::ToolResult(result) => has_image(&result.content),
173 _ => false,
174 });
175 let mut headers = vec![
176 ("X-Initiator", initiator.to_owned()),
177 ("Openai-Intent", "conversation-edits".to_owned()),
178 ];
179 if vision {
180 headers.push(("Copilot-Vision-Request", "true".to_owned()));
181 }
182 headers
183}
184
185pub trait ModelOperations: Send + Sync {
188 fn classify<'a>(
190 &'a self,
191 model: &'a ClassifierModel,
192 context: &'a ClassifierContext,
193 options: &'a classify::ClassifyOptions,
194 ) -> BoxFuture<'a, ClassifierResult>;
195
196 fn generate_images<'a>(
198 &'a self,
199 model: &'a ImageModel,
200 context: &'a ImagesContext,
201 options: &'a images::ImagesOptions,
202 ) -> BoxFuture<'a, AssistantImages>;
203}
204
205#[derive(Clone, Default)]
207pub struct Apis {
208 registered: IndexMap<String, Arc<dyn Provider>>,
209 providers: IndexMap<(String, String), Arc<dyn Provider>>,
212}
213
214impl Apis {
215 pub fn register(&mut self, provider: Arc<dyn Provider>) {
217 self.registered.insert(provider.api().to_owned(), provider);
218 }
219
220 pub fn register_for(&mut self, provider: &str, implementation: Arc<dyn Provider>) {
225 let key = (provider.to_owned(), implementation.api().to_owned());
226 self.providers.insert(key, implementation);
227 }
228
229 pub fn unregister_for(&mut self, provider: &str) {
231 self.providers.retain(|(owner, _), _| owner != provider);
232 }
233
234 pub fn get(&self, api: &str) -> Option<Arc<dyn Provider>> {
236 self.registered.get(api).cloned().or_else(|| builtin(api))
237 }
238
239 pub fn stream(&self, mut request: Request) -> EventStream {
242 if matches!(request.model.provider.as_str(), "opencode" | "opencode-go")
243 && let Some(session_id) = request.options.session_id.clone()
244 && !request
245 .options
246 .headers
247 .keys()
248 .any(|name| name.eq_ignore_ascii_case("x-opencode-session"))
249 {
250 request
251 .options
252 .headers
253 .insert("x-opencode-session".into(), Some(session_id));
254 }
255 if matches!(
256 request.model.provider.as_str(),
257 "cloudflare-ai-gateway" | "cloudflare-workers-ai"
258 ) {
259 request.model.base_url = crate::key_auth::cloudflare_base_url(
260 &request.model.base_url,
261 request.options.env.as_ref(),
262 );
263 }
264 let key = (request.model.provider.clone(), request.model.api.clone());
265 let own = self.providers.get(&key).cloned();
266 match own.or_else(|| self.get(&request.model.api)) {
267 Some(provider) => provider.stream(request),
268 None => {
269 let message = format!("No API provider registered for api: {}", request.model.api);
270 failed_stream(&request.model, &request.options.cancel, message)
271 }
272 }
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[tokio::test]
282 async fn streams_serve_own_models() {
283 let mut apis = Apis::default();
284 apis.register_for(
285 "echo",
286 Arc::new(crate::faux::Faux::new([crate::faux::Response::text("hi")])),
287 );
288 let request = |provider: &str| Request {
289 model: serde_json::from_value(serde_json::json!({
290 "id": "m", "name": "M", "api": "faux", "provider": provider, "baseUrl": "",
291 "reasoning": false, "input": ["text"],
292 "cost": {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0},
293 "contextWindow": 1000, "maxTokens": 100,
294 }))
295 .unwrap(),
296 messages: Vec::new(),
297 options: crate::stream::StreamOptions::default(),
298 };
299 let own = apis.stream(request("echo")).result().await.unwrap();
300 assert_eq!(own.error_message, None);
301 let other = apis.stream(request("other")).result().await.unwrap();
302 assert_eq!(
303 other.error_message.as_deref(),
304 Some("No API provider registered for api: faux")
305 );
306 }
307
308 #[test]
309 fn tool_call_ids_utf16() {
310 assert_eq!(normalize_tool_call_id("call|a😀b"), "call_a__b");
312 assert_eq!(normalize_tool_call_id(&"x".repeat(70)), "x".repeat(64));
313 }
314}