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, 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}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct ToolCall {
43 pub id: String,
44 pub name: String,
45 pub args: serde_json::Value,
46}
47
48#[derive(Debug, Clone, Default, Serialize, Deserialize)]
49pub struct Usage {
50 pub input_tokens: u32,
51 pub output_tokens: u32,
52 pub cached_input_tokens: u32,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57#[non_exhaustive]
58pub enum StopReason {
59 EndTurn,
60 ToolUse,
61 MaxTokens,
62 StopSequence,
63 Other,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68#[non_exhaustive]
69pub enum ModelDelta {
70 Text(String),
71 ToolCallStart {
72 id: String,
73 name: String,
74 },
75 ToolCallArgs {
76 id: String,
77 partial_json: String,
78 },
79 ToolCallEnd {
80 id: String,
81 },
82 Usage(Usage),
83 Stop(StopReason),
84 Reasoning(String),
90}
91
92#[async_trait]
93pub trait Model: Send + Sync + 'static {
94 async fn complete(&self, ctx: &Context) -> Result<ModelOutput, ModelError>;
95
96 async fn search_web(&self, query: &str) -> Option<Result<String, ModelError>> {
112 let _ = query;
113 None
114 }
115
116 async fn stream(
118 &self,
119 ctx: &Context,
120 ) -> Result<futures::stream::BoxStream<'static, Result<ModelDelta, ModelError>>, ModelError>
121 {
122 let out = self.complete(ctx).await?;
123 let deltas: Vec<Result<ModelDelta, ModelError>> = out
124 .text
125 .into_iter()
126 .map(|t| Ok(ModelDelta::Text(t)))
127 .chain(std::iter::once(Ok(ModelDelta::Stop(out.stop_reason))))
128 .collect();
129 Ok(Box::pin(futures::stream::iter(deltas)))
130 }
131
132 fn info(&self) -> ModelInfo;
133}
134
135pub struct DynModel(pub std::sync::Arc<dyn Model>);
146
147#[async_trait]
148impl Model for DynModel {
149 async fn complete(&self, ctx: &Context) -> Result<ModelOutput, ModelError> {
150 self.0.complete(ctx).await
151 }
152 async fn stream(
153 &self,
154 ctx: &Context,
155 ) -> Result<futures::stream::BoxStream<'static, Result<ModelDelta, ModelError>>, ModelError>
156 {
157 self.0.stream(ctx).await
158 }
159 fn info(&self) -> ModelInfo {
160 self.0.info()
161 }
162}
163
164#[cfg(test)]
165mod arc_model_tests {
166 use super::*;
167 use std::sync::Arc;
168
169 struct Dummy;
170
171 #[async_trait]
172 impl Model for Dummy {
173 async fn complete(&self, _ctx: &Context) -> Result<ModelOutput, ModelError> {
174 Ok(ModelOutput {
175 text: Some("ok".into()),
176 tool_calls: vec![],
177 usage: Usage::default(),
178 stop_reason: StopReason::EndTurn,
179 reasoning: None,
180 })
181 }
182 fn info(&self) -> ModelInfo {
183 ModelInfo {
184 handle: "dummy".into(),
185 provider: "test".into(),
186 model: "dummy".into(),
187 context_window: 8192,
188 input_cost_usd_per_million_tokens: None,
189 output_cost_usd_per_million_tokens: None,
190 supports_tool_use: false,
191 supports_streaming: false,
192 supports_web_grounding: false,
193 }
194 }
195 }
196
197 fn assert_is_model<M: Model>(_m: &M) {}
198
199 #[tokio::test]
200 async fn dyn_model_wrapper_is_a_model() {
201 let m: Arc<dyn Model> = Arc::new(Dummy);
202 let wrapped = DynModel(m);
203 assert_is_model(&wrapped); let out = wrapped
205 .complete(&Context::new(crate::Task {
206 description: "x".into(),
207 source: None,
208 deadline: None,
209 }))
210 .await
211 .unwrap();
212 assert_eq!(out.text.as_deref(), Some("ok"));
213 }
214}