1use crate::{Context, error::ModelError};
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ModelInfo {
12 pub handle: String,
13 pub provider: String,
14 pub model: String,
15 pub context_window: u32,
16 pub input_cost_usd_per_million_tokens: Option<f64>,
17 pub output_cost_usd_per_million_tokens: Option<f64>,
18 pub supports_tool_use: bool,
19 pub supports_streaming: bool,
20 #[serde(default)]
25 pub supports_web_grounding: bool,
26}
27
28#[derive(Debug, Clone, Default, Serialize, Deserialize)]
29pub struct ModelOutput {
30 pub text: Option<String>,
31 pub tool_calls: Vec<ToolCall>,
32 pub usage: Usage,
33 pub stop_reason: StopReason,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub reasoning: Option<String>,
39 #[serde(default, skip_serializing_if = "Vec::is_empty")]
47 pub images: Vec<crate::image::GeneratedImage>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ToolCall {
52 pub id: String,
53 pub name: String,
54 pub args: serde_json::Value,
55}
56
57#[derive(Debug, Clone, Default, Serialize, Deserialize)]
58pub struct Usage {
59 pub input_tokens: u32,
60 pub output_tokens: u32,
61 pub cached_input_tokens: u32,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66#[non_exhaustive]
67pub enum StopReason {
68 #[default]
69 EndTurn,
70 ToolUse,
71 MaxTokens,
72 StopSequence,
73 Other,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78#[non_exhaustive]
79pub enum ModelDelta {
80 Text(String),
81 ToolCallStart {
82 id: String,
83 name: String,
84 },
85 ToolCallArgs {
86 id: String,
87 partial_json: String,
88 },
89 ToolCallEnd {
90 id: String,
91 },
92 Usage(Usage),
93 Stop(StopReason),
94 Reasoning(String),
100}
101
102#[async_trait]
103pub trait Model: Send + Sync + 'static {
104 async fn complete(&self, ctx: &Context) -> Result<ModelOutput, ModelError>;
105
106 async fn search_web(&self, query: &str) -> Option<Result<String, ModelError>> {
122 let _ = query;
123 None
124 }
125
126 async fn stream(
128 &self,
129 ctx: &Context,
130 ) -> Result<futures::stream::BoxStream<'static, Result<ModelDelta, ModelError>>, ModelError>
131 {
132 let out = self.complete(ctx).await?;
133 let deltas: Vec<Result<ModelDelta, ModelError>> = out
134 .text
135 .into_iter()
136 .map(|t| Ok(ModelDelta::Text(t)))
137 .chain(std::iter::once(Ok(ModelDelta::Stop(out.stop_reason))))
138 .collect();
139 Ok(Box::pin(futures::stream::iter(deltas)))
140 }
141
142 fn info(&self) -> ModelInfo;
143}
144
145pub struct DynModel(pub std::sync::Arc<dyn Model>);
156
157#[async_trait]
158impl Model for DynModel {
159 async fn complete(&self, ctx: &Context) -> Result<ModelOutput, ModelError> {
160 self.0.complete(ctx).await
161 }
162 async fn stream(
163 &self,
164 ctx: &Context,
165 ) -> Result<futures::stream::BoxStream<'static, Result<ModelDelta, ModelError>>, ModelError>
166 {
167 self.0.stream(ctx).await
168 }
169 fn info(&self) -> ModelInfo {
170 self.0.info()
171 }
172}
173
174#[cfg(test)]
175mod arc_model_tests {
176 use super::*;
177 use std::sync::Arc;
178
179 struct Dummy;
180
181 #[async_trait]
182 impl Model for Dummy {
183 async fn complete(&self, _ctx: &Context) -> Result<ModelOutput, ModelError> {
184 Ok(ModelOutput {
185 text: Some("ok".into()),
186 ..Default::default()
187 })
188 }
189 fn info(&self) -> ModelInfo {
190 ModelInfo {
191 handle: "dummy".into(),
192 provider: "test".into(),
193 model: "dummy".into(),
194 context_window: 8192,
195 input_cost_usd_per_million_tokens: None,
196 output_cost_usd_per_million_tokens: None,
197 supports_tool_use: false,
198 supports_streaming: false,
199 supports_web_grounding: false,
200 }
201 }
202 }
203
204 fn assert_is_model<M: Model>(_m: &M) {}
205
206 #[tokio::test]
207 async fn dyn_model_wrapper_is_a_model() {
208 let m: Arc<dyn Model> = Arc::new(Dummy);
209 let wrapped = DynModel(m);
210 assert_is_model(&wrapped); let out = wrapped
212 .complete(&Context::new(crate::Task {
213 description: "x".into(),
214 source: None,
215 deadline: None,
216 }))
217 .await
218 .unwrap();
219 assert_eq!(out.text.as_deref(), Some("ok"));
220 }
221}