1use futures_util::future::BoxFuture;
4use indexmap::IndexMap;
5use tokio::sync::mpsc;
6use tokio_util::sync::CancellationToken;
7use yapi_types::event::AssistantMessageEvent;
8use yapi_types::message::{
9 AssistantMessage, ContentBlock, Message, StopReason, ThinkingLevel, Usage,
10};
11use yapi_types::model::Model;
12pub use yapi_types::settings::Transport;
13
14use crate::credentials::{ProviderEnv, provider_env_value};
15
16pub trait Provider: Send + Sync {
22 fn api(&self) -> &str;
24
25 fn stream(&self, request: Request) -> EventStream;
27}
28
29#[derive(Clone, Debug)]
31pub struct Request {
32 pub model: Model,
34 pub messages: Vec<Message>,
36 pub options: StreamOptions,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum CacheRetention {
43 None,
45 Short,
47 Long,
49}
50
51impl CacheRetention {
52 pub fn as_str(self) -> &'static str {
54 match self {
55 CacheRetention::None => "none",
56 CacheRetention::Short => "short",
57 CacheRetention::Long => "long",
58 }
59 }
60
61 pub fn parse(name: &str) -> Option<CacheRetention> {
63 [
64 CacheRetention::None,
65 CacheRetention::Short,
66 CacheRetention::Long,
67 ]
68 .into_iter()
69 .find(|retention| retention.as_str() == name)
70 }
71}
72
73#[derive(Clone, Debug, Default)]
75pub struct StreamOptions {
76 pub api_key: Option<String>,
78 pub headers: IndexMap<String, Option<String>>,
80 pub reasoning: Option<ThinkingLevel>,
82 pub max_tokens: Option<u64>,
84 pub temperature: Option<f64>,
86 pub session_id: Option<String>,
88 pub transport: Option<Transport>,
91 pub websocket_connect_timeout_ms: Option<u64>,
94 pub timeout_ms: Option<f64>,
99 pub cache_retention: Option<CacheRetention>,
101 pub thinking_budgets: ThinkingBudgets,
103 pub max_retries: u32,
105 pub max_retry_delay_ms: Option<u64>,
107 pub cancel: CancellationToken,
109 pub env: Option<ProviderEnv>,
112 pub hooks: RequestHooks,
114}
115
116pub type RequestHeaders = IndexMap<String, Option<String>>;
118
119pub type Hook<A, R> = std::sync::Arc<dyn Fn(A) -> BoxFuture<'static, R> + Send + Sync>;
121
122#[derive(Clone, Debug, PartialEq, Eq)]
124pub struct ProviderResponse {
125 pub status: u16,
127 pub headers: IndexMap<String, String>,
129}
130
131#[derive(Clone, Default)]
135pub struct RequestHooks {
136 pub payload: Option<Hook<serde_json::Value, serde_json::Value>>,
139 pub headers: Option<Hook<RequestHeaders, RequestHeaders>>,
142 pub response: Option<Hook<ProviderResponse, ()>>,
144 pub stream_event: Option<Hook<serde_json::Value, ()>>,
146}
147
148impl RequestHooks {
149 pub async fn payload(&self, payload: serde_json::Value) -> serde_json::Value {
151 match &self.payload {
152 Some(hook) => hook(payload).await,
153 None => payload,
154 }
155 }
156
157 pub async fn prepare(request: &mut Request) {
160 if let Some(hook) = request.options.hooks.headers.clone() {
161 request.options.headers = hook(std::mem::take(&mut request.options.headers)).await;
162 }
163 }
164
165 pub async fn response(&self, response: &reqwest::Response) {
167 if let Some(hook) = &self.response {
168 let headers = response
169 .headers()
170 .iter()
171 .filter_map(|(name, value)| {
172 Some((name.as_str().to_owned(), value.to_str().ok()?.to_owned()))
173 })
174 .collect();
175 hook(ProviderResponse {
176 status: response.status().as_u16(),
177 headers,
178 })
179 .await;
180 }
181 }
182
183 pub async fn stream_event(&self, data: &serde_json::Value) {
185 if let Some(hook) = &self.stream_event {
186 hook(data.clone()).await;
187 }
188 }
189}
190
191impl std::fmt::Debug for RequestHooks {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 f.debug_struct("RequestHooks")
194 .field("payload", &self.payload.is_some())
195 .field("headers", &self.headers.is_some())
196 .field("response", &self.response.is_some())
197 .field("stream_event", &self.stream_event.is_some())
198 .finish()
199 }
200}
201
202impl StreamOptions {
203 pub fn resolved_cache_retention(&self) -> CacheRetention {
206 self.requested_cache_retention()
207 .unwrap_or(CacheRetention::Short)
208 }
209
210 pub fn requested_cache_retention(&self) -> Option<CacheRetention> {
213 self.cache_retention.or_else(|| {
214 (provider_env_value("PI_CACHE_RETENTION", self.env.as_ref()).as_deref() == Some("long"))
215 .then_some(CacheRetention::Long)
216 })
217 }
218
219 pub fn has_header(&self, name: &str) -> bool {
221 self.headers.iter().any(|(key, value)| {
222 key.eq_ignore_ascii_case(name)
223 && value
224 .as_deref()
225 .is_some_and(|value| !value.trim().is_empty())
226 })
227 }
228}
229
230#[derive(Clone, Debug, Default, PartialEq, Eq)]
232pub struct ThinkingBudgets {
233 pub minimal: Option<u64>,
235 pub low: Option<u64>,
237 pub medium: Option<u64>,
239 pub high: Option<u64>,
241}
242
243#[derive(Clone, Debug, PartialEq)]
245pub enum StreamEvent {
246 Start(AssistantMessage),
248 Update {
250 event: AssistantMessageEvent,
252 usage: Usage,
254 },
255 Done(AssistantMessage),
257 Error(AssistantMessage),
259}
260
261#[derive(Debug)]
263pub struct EventStream {
264 receiver: mpsc::UnboundedReceiver<StreamEvent>,
265}
266
267impl EventStream {
268 pub fn channel() -> (EventSender, EventStream) {
270 let (sender, receiver) = mpsc::unbounded_channel();
271 (EventSender { sender }, EventStream { receiver })
272 }
273
274 pub async fn next(&mut self) -> Option<StreamEvent> {
276 self.receiver.recv().await
277 }
278
279 pub async fn result(mut self) -> Option<AssistantMessage> {
281 while let Some(event) = self.next().await {
282 if let StreamEvent::Done(message) | StreamEvent::Error(message) = event {
283 return Some(message);
284 }
285 }
286 None
287 }
288}
289
290#[derive(Clone, Debug)]
293pub struct EventSender {
294 sender: mpsc::UnboundedSender<StreamEvent>,
295}
296
297impl EventSender {
298 pub fn send(&self, event: StreamEvent) {
300 let _ = self.sender.send(event);
301 }
302
303 pub fn update(&self, message: &AssistantMessage, event: AssistantMessageEvent) {
305 self.send(StreamEvent::Update {
306 event,
307 usage: message.usage.clone(),
308 });
309 }
310
311 pub(crate) fn start(&self, output: &mut AssistantMessage, block: ContentBlock) -> usize {
313 let content_index = output.content.len();
314 let event = match &block {
315 ContentBlock::Text(_) => Some(AssistantMessageEvent::TextStart { content_index }),
316 ContentBlock::Thinking(_) => {
317 Some(AssistantMessageEvent::ThinkingStart { content_index })
318 }
319 ContentBlock::ToolCall(call) => Some(AssistantMessageEvent::ToolcallStart {
320 content_index,
321 id: call.id.clone(),
322 tool_name: call.name.clone(),
323 }),
324 ContentBlock::Image(_) => None,
325 };
326 output.content.push(block);
327 if let Some(event) = event {
328 self.update(output, event);
329 }
330 content_index
331 }
332
333 pub(crate) fn delta(&self, output: &mut AssistantMessage, content_index: usize, delta: &str) {
337 let event = match output.content.get_mut(content_index) {
338 Some(ContentBlock::Text(text)) => {
339 text.text.push_str(delta);
340 AssistantMessageEvent::TextDelta {
341 content_index,
342 delta: delta.to_owned(),
343 }
344 }
345 Some(ContentBlock::Thinking(thinking)) => {
346 thinking.thinking.push_str(delta);
347 AssistantMessageEvent::ThinkingDelta {
348 content_index,
349 delta: delta.to_owned(),
350 }
351 }
352 Some(ContentBlock::ToolCall(_)) => AssistantMessageEvent::ToolcallDelta {
353 content_index,
354 delta: delta.to_owned(),
355 },
356 Some(ContentBlock::Image(_)) | None => return,
357 };
358 self.update(output, event);
359 }
360
361 pub(crate) fn end(&self, output: &AssistantMessage, content_index: usize) {
364 let event = match output.content.get(content_index) {
365 Some(ContentBlock::Text(text)) => AssistantMessageEvent::TextEnd {
366 content_index,
367 content: text.text.clone(),
368 },
369 Some(ContentBlock::Thinking(thinking)) => AssistantMessageEvent::ThinkingEnd {
370 content_index,
371 content: thinking.thinking.clone(),
372 },
373 Some(ContentBlock::ToolCall(call)) => AssistantMessageEvent::ToolcallEnd {
374 content_index,
375 tool_call: call.clone(),
376 },
377 Some(ContentBlock::Image(_)) | None => return,
378 };
379 self.update(output, event);
380 }
381}
382
383pub fn new_output(model: &Model, now_ms: u64) -> AssistantMessage {
385 AssistantMessage {
386 content: Vec::new(),
387 api: model.api.clone(),
388 provider: model.provider.clone(),
389 model: model.id.clone(),
390 provider_thinking_level: None,
391 usage: Usage {
392 total_tokens: Some(0),
393 ..Usage::default()
394 },
395 stop_reason: StopReason::Pending,
396 timestamp: now_ms,
397 response_id: None,
398 response_model: None,
399 raw_stop_reason: None,
400 deferred: None,
401 error_message: None,
402 diagnostics: None,
403 end_turn: None,
404 thinking_level: None,
405 }
406}
407
408pub fn send_error(
410 sender: &EventSender,
411 mut output: AssistantMessage,
412 cancel: &CancellationToken,
413 message: String,
414) {
415 output.stop_reason = if cancel.is_cancelled() {
416 StopReason::Aborted
417 } else {
418 StopReason::Error
419 };
420 output.error_message = Some(message);
421 sender.send(StreamEvent::Error(output));
422}
423
424pub(crate) fn check_complete(
428 output: &AssistantMessage,
429 cancel: &CancellationToken,
430 pending: &str,
431) -> Result<(), String> {
432 if cancel.is_cancelled() {
433 return Err(crate::http::ABORTED_DURING_STREAM.to_owned());
434 }
435 match output.stop_reason {
436 StopReason::Pending => Err(pending.to_owned()),
437 StopReason::Aborted | StopReason::Error => Err(output
438 .error_message
439 .clone()
440 .filter(|message| !message.is_empty())
441 .unwrap_or_else(|| "An unknown error occurred".to_owned())),
442 _ => Ok(()),
443 }
444}
445
446pub use yapi_types::time::now_ms;