1use crate::agent::provider::{Provider, StreamEvent, ToolDef};
2use crate::agent::types::{AgentMessage, Role, ToolCall};
3use crate::auth::AuthStorage;
4use async_trait::async_trait;
5use futures::{Stream, StreamExt};
6use genai::chat::{
7 ChatMessage, ChatOptions, ChatRequest, ContentPart, MessageContent, ReasoningEffort, Tool,
8 ToolCall as GenaiToolCall, ToolResponse,
9};
10use genai::resolver::{AuthData, AuthResolver};
11use std::pin::Pin;
12use std::sync::RwLock;
13
14fn build_reqwest_client() -> reqwest::Client {
18 let mut root_store = rustls::RootCertStore::empty();
19 root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
20 let tls_config = rustls::ClientConfig::builder()
21 .with_root_certificates(root_store)
22 .with_no_client_auth();
23 reqwest::Client::builder()
24 .tls_backend_preconfigured(tls_config)
25 .timeout(std::time::Duration::from_secs(300)) .connect_timeout(std::time::Duration::from_secs(30))
27 .build()
28 .expect("Failed to build reqwest client")
29}
30
31pub struct GenaiProvider {
32 client: genai::Client,
33 model_prefix: String,
34 reasoning_effort: RwLock<Option<ReasoningEffort>>,
35}
36
37impl GenaiProvider {
38 pub fn new(auth: &AuthStorage, thinking_level: Option<&str>) -> anyhow::Result<Self> {
39 let api_key = auth
40 .api_key("opencode-go")
41 .ok_or_else(|| anyhow::anyhow!("No API key found for opencode_go in auth.json"))?;
42
43 let auth_resolver = AuthResolver::from_resolver_fn(move |_model_iden: genai::ModelIden| {
44 Ok(Some(AuthData::from_single(api_key.clone())))
45 });
46
47 let reqwest_client = build_reqwest_client();
48 let client = genai::Client::builder()
49 .with_reqwest(reqwest_client)
50 .with_auth_resolver(auth_resolver)
51 .build();
52
53 let reasoning_effort = match thinking_level {
63 Some("off" | "none") => None,
64 Some("minimal" | "low") => Some(ReasoningEffort::Low),
65 Some("medium") => Some(ReasoningEffort::Medium),
66 _ => Some(ReasoningEffort::High), };
68
69 Ok(Self {
70 client,
71 model_prefix: "opencode_go::".into(),
72 reasoning_effort: RwLock::new(reasoning_effort),
73 })
74 }
75
76 fn full_model(&self, model: &str) -> String {
77 if model.contains("::") {
78 model.to_string()
79 } else {
80 format!("{}{}", self.model_prefix, model)
81 }
82 }
83
84 fn thinking_level_to_effort(level: Option<&str>) -> Option<ReasoningEffort> {
86 match level {
87 Some("off" | "none") => None,
88 Some("minimal" | "low") => Some(ReasoningEffort::Low),
89 Some("medium") => Some(ReasoningEffort::Medium),
90 _ => Some(ReasoningEffort::High), }
92 }
93
94 fn convert_messages(messages: &[AgentMessage]) -> Vec<ChatMessage> {
95 messages
96 .iter()
97 .map(|m| match m.role {
98 Role::User => ChatMessage::user(&m.content),
99 Role::Assistant => {
100 let mut parts: Vec<ContentPart> = Vec::new();
101
102 if !m.content.is_empty() {
105 parts.push(ContentPart::from_text(&m.content));
106 }
107
108 for tc in &m.tool_calls {
109 parts.push(ContentPart::ToolCall(GenaiToolCall {
110 call_id: tc.id.clone(),
111 fn_name: tc.name.clone(),
112 fn_arguments: tc.arguments.clone(),
113 thought_signatures: None,
114 }));
115 }
116
117 ChatMessage::assistant(MessageContent::from_parts(parts))
118 }
119 Role::ToolResult => ChatMessage::from(ToolResponse::new(
120 m.tool_call_id.clone().unwrap_or_default(),
121 &m.content,
122 )),
123 })
124 .collect()
125 }
126
127 fn convert_tools(tools: &[ToolDef]) -> Vec<Tool> {
128 tools
129 .iter()
130 .map(|t| {
131 Tool::new(&t.name)
132 .with_description(&t.description)
133 .with_schema(t.parameters.clone())
134 })
135 .collect()
136 }
137}
138
139#[async_trait]
140impl Provider for GenaiProvider {
141 async fn stream(
142 &self,
143 model: &str,
144 system_prompt: &str,
145 messages: &[AgentMessage],
146 tools: &[ToolDef],
147 ) -> anyhow::Result<Pin<Box<dyn Stream<Item = StreamEvent> + Send>>> {
148 let full_model = self.full_model(model);
149 let chat_messages = Self::convert_messages(messages);
150 let genai_tools = Self::convert_tools(tools);
151
152 let mut req = ChatRequest::new(chat_messages).with_system(system_prompt);
153 if !genai_tools.is_empty() {
154 req = req.with_tools(genai_tools);
155 }
156
157 let mut options = ChatOptions::default()
158 .with_capture_usage(true)
159 .with_capture_content(true)
160 .with_capture_tool_calls(true);
161
162 if let Ok(guard) = self.reasoning_effort.read()
163 && let Some(ref effort) = *guard
164 {
165 options = options.with_reasoning_effort(effort.clone());
166 }
167
168 let genai_response = self
169 .client
170 .exec_chat_stream(&full_model, req, Some(&options))
171 .await?;
172
173 let mut genai_stream = genai_response.stream;
174
175 let stream = async_stream::stream! {
176 while let Some(result) = genai_stream.next().await {
177 match result {
178 Ok(event) => {
179 match event {
180 genai::chat::ChatStreamEvent::Start => {},
181 genai::chat::ChatStreamEvent::Chunk(chunk) => {
182 yield StreamEvent::TextDelta { text: chunk.content };
183 }
184 genai::chat::ChatStreamEvent::ReasoningChunk(chunk) => {
185 yield StreamEvent::ThinkingDelta { text: chunk.content };
186 }
187 genai::chat::ChatStreamEvent::ThoughtSignatureChunk(_) => {},
188 genai::chat::ChatStreamEvent::ToolCallChunk(tool_chunk) => {
189 let tc = &tool_chunk.tool_call;
190 yield StreamEvent::ToolCall {
191 id: tc.call_id.clone(),
192 name: tc.fn_name.clone(),
193 arguments: serde_json::to_string(&tc.fn_arguments)
194 .unwrap_or_default(),
195 };
196 }
197 genai::chat::ChatStreamEvent::End(end) => {
198 let text = end.captured_first_text().unwrap_or("").to_string();
199 let tool_calls: Vec<ToolCall> = end
200 .captured_tool_calls()
201 .into_iter()
202 .flatten()
203 .map(|tc| ToolCall {
204 id: tc.call_id.clone(),
205 name: tc.fn_name.clone(),
206 arguments: tc.fn_arguments.clone(),
207 })
208 .collect();
209
210 let usage = crate::agent::types::Usage {
211 input_tokens: end.captured_usage.as_ref()
212 .and_then(|u| u.prompt_tokens),
213 output_tokens: end.captured_usage.as_ref()
214 .and_then(|u| u.completion_tokens),
215 cache_tokens: None,
216 };
217
218 let stop_reason = match &end.captured_stop_reason {
219 Some(genai::chat::StopReason::Completed(_)) => crate::agent::provider::StopReason::EndTurn,
220 Some(genai::chat::StopReason::ToolCall(_)) => crate::agent::provider::StopReason::ToolUse,
221 Some(genai::chat::StopReason::MaxTokens(_)) => crate::agent::provider::StopReason::MaxTokens,
222 _ => crate::agent::provider::StopReason::EndTurn,
223 };
224
225 yield StreamEvent::Done {
226 text,
227 usage,
228 stop_reason,
229 tool_calls,
230 };
231 }
232 }
233 }
234 Err(e) => {
235 yield StreamEvent::Error {
236 message: format!("{:#}", e),
237 };
238 }
239 }
240 }
241 };
242
243 Ok(Box::pin(stream))
244 }
245
246 fn set_reasoning_effort(&self, level: Option<&str>) {
247 let effort = Self::thinking_level_to_effort(level);
248 if let Ok(mut guard) = self.reasoning_effort.write() {
249 *guard = effort;
250 }
251 }
252}