1use crate::LlmError;
2use crate::LlmModel;
3use crate::ProviderConnectionConfig;
4use crate::Result as LlmResult;
5use std::future::Future;
6use std::pin::Pin;
7use tokio_stream::{Stream, StreamExt};
8
9use super::{Context, LlmResponse};
10
11pub type LlmResponseStream = Pin<Box<dyn Stream<Item = LlmResult<LlmResponse>> + Send>>;
18
19#[doc = include_str!("docs/provider_factory.md")]
20pub trait ProviderFactory: Sized {
21 fn from_env() -> impl Future<Output = LlmResult<Self>> + Send;
23
24 fn from_env_with_connection(connection: ProviderConnectionConfig) -> impl Future<Output = LlmResult<Self>> + Send {
26 async move {
27 let _ = connection;
28 Self::from_env().await
29 }
30 }
31
32 fn with_model(self, model: &str) -> Self;
34}
35
36#[doc = include_str!("docs/streaming_model_provider.md")]
37pub trait StreamingModelProvider: Send + Sync {
38 fn stream_response(&self, context: &Context) -> LlmResponseStream;
39 fn display_name(&self) -> String;
40
41 fn context_window(&self) -> Option<u32>;
44
45 fn model(&self) -> Option<LlmModel> {
49 None
50 }
51}
52
53pub fn get_context_window(provider: &str, model_id: &str) -> Option<u32> {
57 let key = format!("{provider}:{model_id}");
58 key.parse::<LlmModel>().ok().and_then(|m| m.context_window())
59}
60
61pub(crate) fn stream_from<T, S>(
67 open: impl Future<Output = LlmResult<T>> + Send + 'static,
68 process: impl FnOnce(T) -> S + Send + 'static,
69) -> LlmResponseStream
70where
71 T: Send,
72 S: Stream<Item = LlmResult<LlmResponse>> + Send + 'static,
73{
74 Box::pin(async_stream::stream! {
75 match open.await {
76 Ok(opened) => {
77 let mut stream = Box::pin(process(opened));
78 while let Some(item) = stream.next().await {
79 yield item;
80 }
81 }
82 Err(error) => yield Err(error),
83 }
84 })
85}
86
87pub(crate) fn error_stream(error: LlmError) -> LlmResponseStream {
89 Box::pin(tokio_stream::once(Err(error)))
90}
91
92impl StreamingModelProvider for Box<dyn StreamingModelProvider> {
93 fn stream_response(&self, context: &Context) -> LlmResponseStream {
94 (**self).stream_response(context)
95 }
96
97 fn display_name(&self) -> String {
98 (**self).display_name()
99 }
100
101 fn context_window(&self) -> Option<u32> {
102 (**self).context_window()
103 }
104
105 fn model(&self) -> Option<LlmModel> {
106 (**self).model()
107 }
108}
109
110impl<T: StreamingModelProvider + ?Sized> StreamingModelProvider for std::sync::Arc<T> {
111 fn stream_response(&self, context: &Context) -> LlmResponseStream {
112 (**self).stream_response(context)
113 }
114
115 fn display_name(&self) -> String {
116 (**self).display_name()
117 }
118
119 fn context_window(&self) -> Option<u32> {
120 (**self).context_window()
121 }
122
123 fn model(&self) -> Option<LlmModel> {
124 (**self).model()
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn lookup_context_window_known_model() {
134 assert_eq!(get_context_window("anthropic", "claude-opus-4-6"), Some(1_000_000));
135 }
136
137 #[test]
138 fn lookup_context_window_openrouter_model() {
139 let result = get_context_window("openrouter", "anthropic/claude-opus-4");
141 assert_eq!(result, Some(200_000));
142 }
143
144 #[test]
145 fn lookup_context_window_unknown_model() {
146 assert_eq!(get_context_window("anthropic", "unknown-model-xyz"), None);
147 }
148
149 #[test]
150 fn lookup_context_window_unknown_provider() {
151 assert_eq!(get_context_window("unknown-provider", "some-model"), None);
152 }
153}