1#[cfg(feature = "openai")]
4mod openai;
5
6#[cfg(feature = "openai")]
7#[allow(unused_imports)]
8pub use openai::{OpenAIProvider, OpenAiApiMode};
9
10use crate::error::AgentResult;
11use async_trait::async_trait;
12use serde::{Deserialize, Serialize};
13
14#[async_trait]
16pub trait LLMProvider: Send + Sync {
17 async fn complete(
19 &self,
20 messages: Vec<Message>,
21 options: &CompletionOptions,
22 client: &reqwest::Client,
23 ) -> AgentResult<CompletionResponse>;
24
25 fn provider_name(&self) -> &'static str;
27
28 fn is_configured(&self) -> bool;
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Message {
35 pub role: String,
37 pub content: MessageContent,
39}
40
41impl Message {
42 pub fn system(content: impl Into<String>) -> Self {
44 Self {
45 role: "system".to_string(),
46 content: MessageContent::Text(content.into()),
47 }
48 }
49
50 pub fn user(content: impl Into<String>) -> Self {
52 Self {
53 role: "user".to_string(),
54 content: MessageContent::Text(content.into()),
55 }
56 }
57
58 pub fn assistant(content: impl Into<String>) -> Self {
60 Self {
61 role: "assistant".to_string(),
62 content: MessageContent::Text(content.into()),
63 }
64 }
65
66 pub fn user_with_image(text: impl Into<String>, image_base64: impl Into<String>) -> Self {
68 Self {
69 role: "user".to_string(),
70 content: MessageContent::MultiPart(vec![
71 ContentPart::Text { text: text.into() },
72 ContentPart::ImageUrl {
73 image_url: ImageUrl {
74 url: format!("data:image/png;base64,{}", image_base64.into()),
75 },
76 },
77 ]),
78 }
79 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(untagged)]
85pub enum MessageContent {
86 Text(String),
88 MultiPart(Vec<ContentPart>),
90}
91
92impl MessageContent {
93 pub fn as_text(&self) -> &str {
97 match self {
98 Self::Text(s) => s,
99 Self::MultiPart(parts) => {
100 for part in parts {
102 if let ContentPart::Text { text } = part {
103 return text;
104 }
105 }
106 ""
107 }
108 }
109 }
110
111 pub fn full_text(&self) -> String {
113 match self {
114 Self::Text(s) => s.clone(),
115 Self::MultiPart(parts) => parts
116 .iter()
117 .filter_map(|p| {
118 if let ContentPart::Text { text } = p {
119 Some(text.as_str())
120 } else {
121 None
122 }
123 })
124 .collect::<Vec<_>>()
125 .join(" "),
126 }
127 }
128
129 pub fn is_text(&self) -> bool {
131 matches!(self, Self::Text(_))
132 }
133
134 pub fn has_images(&self) -> bool {
136 match self {
137 Self::Text(_) => false,
138 Self::MultiPart(parts) => parts
139 .iter()
140 .any(|p| matches!(p, ContentPart::ImageUrl { .. })),
141 }
142 }
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147#[serde(tag = "type", rename_all = "snake_case")]
148pub enum ContentPart {
149 Text { text: String },
151 ImageUrl { image_url: ImageUrl },
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct ImageUrl {
158 pub url: String,
160}
161
162#[derive(Debug, Clone)]
164pub struct CompletionOptions {
165 pub temperature: f32,
167 pub max_tokens: u16,
169 pub json_mode: bool,
171}
172
173impl Default for CompletionOptions {
174 fn default() -> Self {
175 Self {
176 temperature: 0.1,
177 max_tokens: 4096,
178 json_mode: true,
179 }
180 }
181}
182
183#[derive(Debug, Clone)]
185pub struct CompletionResponse {
186 pub content: String,
188 pub usage: TokenUsage,
190}
191
192#[derive(Debug, Clone, Default, Serialize, Deserialize)]
194pub struct TokenUsage {
195 pub prompt_tokens: u32,
197 pub completion_tokens: u32,
199 pub total_tokens: u32,
201}
202
203impl TokenUsage {
204 pub fn accumulate(&mut self, other: &TokenUsage) {
206 self.prompt_tokens += other.prompt_tokens;
207 self.completion_tokens += other.completion_tokens;
208 self.total_tokens += other.total_tokens;
209 }
210}