1use async_trait::async_trait;
5use futures_util::Stream;
6use serde::Deserialize;
7use serde_json::json;
8use std::pin::Pin;
9
10use super::OpenAIConfig;
11use lc_callbacks::{RunTree, RunType};
12use lc_core::language_models::{BaseChatModel, BaseLanguageModel, LLMResult, TokenUsage};
13use lc_core::runnables::Runnable;
14use lc_core::tools::{StructuredOutput, ToolDefinition};
15use lc_core::RunnableConfig;
16use lc_schema::Message;
17use schemars::JsonSchema;
18use serde::de::DeserializeOwned;
19use std::marker::PhantomData;
20
21#[derive(Clone)]
23pub struct OpenAIChat {
24 pub(crate) config: OpenAIConfig,
25 pub(crate) client: reqwest::Client,
26}
27
28impl OpenAIChat {
29 pub fn new(config: OpenAIConfig) -> Self {
31 Self {
32 config,
33 client: reqwest::Client::new(),
34 }
35 }
36
37 #[deprecated(
39 since = "0.5.0",
40 note = "Use from_env_result() which returns Result<Self, OpenAIError>"
41 )]
42 #[allow(deprecated)]
43 pub fn from_env() -> Self {
44 Self::new(OpenAIConfig::from_env())
45 }
46
47 pub fn from_env_result() -> Result<Self, OpenAIError> {
49 let config = OpenAIConfig::from_env_result()?;
50 Ok(Self::new(config))
51 }
52
53 fn message_to_openai_format(message: &Message) -> serde_json::Value {
55 match &message.message_type {
56 lc_schema::MessageType::System => json!({
57 "role": "system",
58 "content": message.content,
59 }),
60 lc_schema::MessageType::Human => {
61 if message.has_images() {
62 let mut content = vec![json!({"type": "text", "text": &message.content})];
63 for img in &message.images {
64 content.push(json!({"type": "image_url", "image_url": {"url": &img.url}}));
65 }
66 json!({"role": "user", "content": content})
67 } else {
68 json!({"role": "user", "content": &message.content})
69 }
70 }
71 lc_schema::MessageType::AI => {
72 let mut msg = json!({
73 "role": "assistant",
74 "content": message.content,
75 });
76 if let Some(tool_calls) = &message.tool_calls {
77 msg["tool_calls"] =
78 serde_json::to_value(tool_calls).unwrap_or(serde_json::Value::Null);
79 }
80 msg
81 }
82 lc_schema::MessageType::Tool { tool_call_id } => json!({
83 "role": "tool",
84 "tool_call_id": tool_call_id,
85 "content": message.content,
86 }),
87 }
88 }
89
90 fn build_request_body(&self, messages: Vec<Message>, stream: bool) -> serde_json::Value {
92 let openai_messages: Vec<serde_json::Value> = messages
93 .iter()
94 .map(Self::message_to_openai_format)
95 .collect();
96
97 let mut body = json!({
98 "model": self.config.model,
99 "messages": openai_messages,
100 "stream": stream,
101 });
102
103 if let Some(temp) = self.config.temperature {
104 body["temperature"] = json!(temp);
105 }
106
107 if let Some(max) = self.config.max_tokens {
108 body["max_tokens"] = json!(max);
109 }
110
111 if let Some(top_p) = self.config.top_p {
112 body["top_p"] = json!(top_p);
113 }
114
115 if let Some(tools) = &self.config.tools {
116 body["tools"] = serde_json::to_value(tools).unwrap_or(serde_json::Value::Null);
117 }
118
119 if let Some(tool_choice) = &self.config.tool_choice {
120 body["tool_choice"] = json!(tool_choice);
121 }
122
123 body
124 }
125
126 pub fn bind_tools(&self, tools: Vec<ToolDefinition>) -> Self {
128 let config = OpenAIConfig {
129 tools: Some(tools),
130 ..self.config.clone()
131 };
132 Self {
133 config,
134 client: self.client.clone(),
135 }
136 }
137
138 pub fn with_tool_choice(mut self, choice: impl Into<String>) -> Self {
140 self.config.tool_choice = Some(choice.into());
141 self
142 }
143
144 pub fn with_structured_output<T: DeserializeOwned + JsonSchema>(
146 &self,
147 ) -> StructuredOutputMethod<T> {
148 use schemars::schema_for;
149 let schema = serde_json::to_value(schema_for!(T)).unwrap_or_else(|_| {
150 serde_json::json!({"type": "object", "properties": {}})
152 });
153
154 let tool = ToolDefinition::new("structured_output", "Return structured JSON output")
155 .with_parameters(schema)
156 .with_strict(true);
157
158 let config = OpenAIConfig {
159 tools: Some(vec![tool]),
160 tool_choice: Some("auto".to_string()),
161 ..self.config.clone()
162 };
163
164 StructuredOutputMethod {
165 config,
166 client: self.client.clone(),
167 _phantom: PhantomData,
168 }
169 }
170}
171
172pub struct StructuredOutputMethod<T: DeserializeOwned + JsonSchema> {
174 config: OpenAIConfig,
175 client: reqwest::Client,
176 _phantom: PhantomData<T>,
177}
178
179impl<T: DeserializeOwned + JsonSchema> StructuredOutputMethod<T> {
180 pub async fn invoke(&self, messages: Vec<Message>) -> Result<T, OpenAIError> {
181 let chat = OpenAIChat {
182 config: self.config.clone(),
183 client: self.client.clone(),
184 };
185
186 let result = chat.chat_internal(messages).await?;
187 let structured = StructuredOutput::<T>::new(result);
188 structured
189 .parse()
190 .map_err(|e| OpenAIError::Parse(e.to_string()))
191 }
192}
193
194#[async_trait]
195impl Runnable<Vec<Message>, LLMResult> for OpenAIChat {
196 type Error = OpenAIError;
197
198 async fn invoke(
199 &self,
200 input: Vec<Message>,
201 _config: Option<RunnableConfig>,
202 ) -> Result<LLMResult, Self::Error> {
203 self.chat(input, _config).await
204 }
205
206 async fn stream(
207 &self,
208 input: Vec<Message>,
209 _config: Option<RunnableConfig>,
210 ) -> Result<Pin<Box<dyn Stream<Item = Result<LLMResult, Self::Error>> + Send>>, Self::Error>
211 {
212 use futures_util::StreamExt;
213
214 let model = self.config.model.clone();
215 let token_stream = self.stream_chat_internal(input).await?;
216
217 let stream = token_stream.map(move |token_result| match token_result {
220 Ok(token) => Ok(LLMResult {
221 content: token,
222 model: model.clone(),
223 token_usage: None,
224 tool_calls: None,
225 thinking_content: None,
226 }),
227 Err(e) => Err(e),
228 });
229
230 Ok(Box::pin(stream))
231 }
232}
233
234#[async_trait]
235impl BaseLanguageModel<Vec<Message>, LLMResult> for OpenAIChat {
236 fn model_name(&self) -> &str {
237 &self.config.model
238 }
239
240 fn get_num_tokens(&self, text: &str) -> usize {
241 lc_core::token_counter::count_tokens(text)
242 }
243
244 fn temperature(&self) -> Option<f32> {
245 self.config.temperature
246 }
247
248 fn max_tokens(&self) -> Option<usize> {
249 self.config.max_tokens
250 }
251
252 fn with_temperature(mut self, temp: f32) -> Self {
253 self.config.temperature = Some(temp);
254 self
255 }
256
257 fn with_max_tokens(mut self, max: usize) -> Self {
258 self.config.max_tokens = Some(max);
259 self
260 }
261}
262
263#[async_trait]
264impl BaseChatModel for OpenAIChat {
265 async fn chat(
266 &self,
267 messages: Vec<Message>,
268 config: Option<RunnableConfig>,
269 ) -> Result<LLMResult, Self::Error> {
270 let run_name = config
271 .as_ref()
272 .and_then(|c| c.run_name.clone())
273 .unwrap_or_else(|| format!("{}:chat", self.config.model));
274
275 let mut run = RunTree::new(
276 run_name,
277 RunType::Llm,
278 json!({
279 "messages": messages.iter().map(|m| m.content.clone()).collect::<Vec<_>>(),
280 "model": self.config.model,
281 }),
282 );
283
284 if let Some(ref cfg) = config {
285 for tag in &cfg.tags {
286 run = run.with_tag(tag.clone());
287 }
288 for (key, value) in &cfg.metadata {
289 run = run.with_metadata(key.clone(), value.clone());
290 }
291 }
292
293 if let Some(ref cfg) = config {
294 if let Some(ref callbacks) = cfg.callbacks {
295 for handler in callbacks.handlers() {
296 handler.on_llm_start(&run, &messages).await;
297 }
298 }
299 }
300
301 let result = self.chat_internal(messages.clone()).await;
302
303 match result {
304 Ok(response) => {
305 run.end(json!({
306 "content": &response.content,
307 "model": &response.model,
308 "token_usage": &response.token_usage,
309 }));
310
311 if let Some(ref cfg) = config {
312 if let Some(ref callbacks) = cfg.callbacks {
313 for handler in callbacks.handlers() {
314 handler.on_llm_end(&run, &response.content).await;
315 }
316 }
317 }
318
319 Ok(response)
320 }
321 Err(e) => {
322 run.end_with_error(e.to_string());
323
324 if let Some(ref cfg) = config {
325 if let Some(ref callbacks) = cfg.callbacks {
326 for handler in callbacks.handlers() {
327 handler.on_llm_error(&run, &e.to_string()).await;
328 }
329 }
330 }
331
332 Err(e)
333 }
334 }
335 }
336
337 async fn stream_chat(
338 &self,
339 messages: Vec<Message>,
340 config: Option<RunnableConfig>,
341 ) -> Result<Pin<Box<dyn Stream<Item = Result<String, Self::Error>> + Send>>, Self::Error> {
342 use futures_util::StreamExt;
343
344 let run_name = config
345 .as_ref()
346 .and_then(|c| c.run_name.clone())
347 .unwrap_or_else(|| format!("{}:stream", self.config.model));
348
349 let run = RunTree::new(
350 run_name,
351 RunType::Llm,
352 json!({
353 "messages": messages.len(),
354 "model": self.config.model,
355 }),
356 );
357
358 if let Some(ref cfg) = config {
359 if let Some(ref callbacks) = cfg.callbacks {
360 for handler in callbacks.handlers() {
361 handler.on_llm_start(&run, &messages).await;
362 }
363 }
364 }
365
366 let stream = self.stream_chat_internal(messages).await?;
367
368 let callbacks = config.and_then(|c| c.callbacks);
369 let stream = stream.then(move |token_result| {
370 let cbs = callbacks.clone();
371 let run = run.clone();
372 async move {
373 if let Some(ref cbs) = cbs {
374 if let Ok(ref token) = token_result {
375 for handler in cbs.handlers() {
376 handler.on_llm_new_token(&run, token).await;
377 }
378 }
379 }
380 token_result
381 }
382 });
383
384 Ok(Box::pin(stream))
385 }
386}
387
388impl OpenAIChat {
389 async fn chat_internal(&self, messages: Vec<Message>) -> Result<LLMResult, OpenAIError> {
390 let url = format!("{}/chat/completions", self.config.base_url);
391 let body = self.build_request_body(messages, false);
392
393 let response = self
394 .client
395 .post(&url)
396 .header("Authorization", format!("Bearer {}", self.config.api_key))
397 .header("Content-Type", "application/json")
398 .json(&body)
399 .send()
400 .await
401 .map_err(|e| OpenAIError::Http(e.to_string()))?;
402
403 let status = response.status();
404 if !status.is_success() {
405 let error_text = response.text().await.unwrap_or_default();
406 return Err(OpenAIError::Api(format!("HTTP {}: {}", status, error_text)));
407 }
408
409 let chat_response: OpenAIChatResponse = response
410 .json()
411 .await
412 .map_err(|e| OpenAIError::Parse(e.to_string()))?;
413
414 let choice = chat_response
415 .choices
416 .first()
417 .ok_or_else(|| OpenAIError::Api("No choices in response".to_string()))?;
418 let message = &choice.message;
419
420 let content = message
424 .content
425 .clone()
426 .filter(|c| !c.is_empty())
427 .unwrap_or_default();
428
429 let thinking_content = message.reasoning_content.clone().filter(|c| !c.is_empty());
430
431 let content = if content.is_empty() {
433 message.reasoning_content.clone().unwrap_or_default()
434 } else {
435 content
436 };
437
438 Ok(LLMResult {
439 content,
440 model: chat_response.model,
441 token_usage: chat_response.usage.map(|u| TokenUsage {
442 prompt_tokens: u.prompt_tokens,
443 completion_tokens: u.completion_tokens,
444 total_tokens: u.total_tokens,
445 }),
446 tool_calls: message.tool_calls.clone(),
447 thinking_content,
448 })
449 }
450
451 async fn stream_chat_internal(
452 &self,
453 messages: Vec<Message>,
454 ) -> Result<Pin<Box<dyn Stream<Item = Result<String, OpenAIError>> + Send>>, OpenAIError> {
455 use super::sse::SSEParser;
456 use std::sync::{Arc, Mutex};
457
458 let url = format!("{}/chat/completions", self.config.base_url);
459 let body = self.build_request_body(messages, true);
460
461 let response = self
462 .client
463 .post(&url)
464 .header("Authorization", format!("Bearer {}", self.config.api_key))
465 .header("Content-Type", "application/json")
466 .json(&body)
467 .send()
468 .await
469 .map_err(|e| OpenAIError::Http(e.to_string()))?;
470
471 let status = response.status();
472 if !status.is_success() {
473 let error_text = response.text().await.unwrap_or_default();
474 return Err(OpenAIError::Api(format!("HTTP {}: {}", status, error_text)));
475 }
476
477 let byte_stream = response.bytes_stream();
478
479 let parser = Arc::new(Mutex::new(SSEParser::new()));
480
481 let parser_clone = parser.clone();
482 let (tx, rx) = tokio::sync::mpsc::channel::<Result<String, OpenAIError>>(64);
484
485 tokio::spawn(async move {
486 use futures_util::StreamExt;
487 let mut byte_stream = byte_stream;
488 while let Some(chunk_result) = byte_stream.next().await {
489 let chunk_bytes = match chunk_result {
492 Ok(bytes) => bytes,
493 Err(e) => {
494 let _ = tx.send(Err(OpenAIError::Http(e.to_string()))).await;
495 return;
496 }
497 };
498
499 let events = {
500 let mut parser_guard = parser_clone.lock().unwrap_or_else(|e| e.into_inner());
501 let chunk_str = String::from_utf8_lossy(&chunk_bytes);
502 parser_guard.parse(&chunk_str)
503 };
504 for event in events {
507 if event.is_done() {
508 break;
509 }
510 if let Ok(Some(chunk)) = event.parse_openai_chunk() {
511 if let Some(choice) = chunk.choices.first() {
512 if let Some(content) = &choice.delta.content {
513 if tx.send(Ok(content.clone())).await.is_err() {
514 return;
515 }
516 }
517 }
518 }
519 }
520 }
521 });
522 let stream = tokio_stream::wrappers::ReceiverStream::new(rx);
523
524 Ok(Box::pin(stream))
525 }
526}
527
528#[derive(Debug)]
530pub enum OpenAIError {
531 Http(String),
532 Api(String),
533 Parse(String),
534}
535
536impl std::fmt::Display for OpenAIError {
537 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
538 match self {
539 OpenAIError::Http(msg) => write!(f, "HTTP 错误: {}", msg),
540 OpenAIError::Api(msg) => write!(f, "API 错误: {}", msg),
541 OpenAIError::Parse(msg) => write!(f, "解析错误: {}", msg),
542 }
543 }
544}
545
546impl std::error::Error for OpenAIError {}
547
548impl From<String> for OpenAIError {
549 fn from(s: String) -> Self {
550 OpenAIError::Api(s)
551 }
552}
553
554#[cfg(test)]
555mod tests_env {
556 use super::*;
557
558 use std::env;
559
560 fn save_and_set(key: &str, value: &str) -> Option<String> {
561 let old = env::var(key).ok();
562 env::set_var(key, value);
563 old
564 }
565
566 fn restore(key: &str, old: Option<String>) {
567 match old {
568 Some(v) => env::set_var(key, v),
569 None => env::remove_var(key),
570 }
571 }
572
573 #[test]
574 fn test_from_env_result_ok_when_key_set() {
575 let _lock = crate::ENV_TEST_LOCK
576 .lock()
577 .unwrap_or_else(|e| e.into_inner());
578 let old = save_and_set("OPENAI_API_KEY", "test-key-123");
579 assert!(OpenAIChat::from_env_result().is_ok());
580 restore("OPENAI_API_KEY", old);
581 }
582
583 #[test]
584 fn test_from_env_result_err_when_key_missing() {
585 let _lock = crate::ENV_TEST_LOCK
586 .lock()
587 .unwrap_or_else(|e| e.into_inner());
588 let old = env::var("OPENAI_API_KEY").ok();
589 env::remove_var("OPENAI_API_KEY");
590 assert!(OpenAIChat::from_env_result().is_err());
591 restore("OPENAI_API_KEY", old);
592 }
593}
594
595#[derive(Debug, Deserialize)]
597#[allow(dead_code)]
598struct OpenAIChatResponse {
599 id: String,
600 object: String,
601 created: i64,
602 model: String,
603 choices: Vec<OpenAIChoice>,
604 usage: Option<OpenAIUsage>,
605}
606
607#[derive(Debug, Deserialize)]
608#[allow(dead_code)]
609struct OpenAIChoice {
610 index: i32,
611 message: OpenAIMessage,
612 finish_reason: Option<String>,
613}
614
615#[derive(Debug, Deserialize)]
616#[allow(dead_code)]
617struct OpenAIMessage {
618 role: String,
619 content: Option<String>,
620 reasoning_content: Option<String>,
622 tool_calls: Option<Vec<lc_core::tools::ToolCall>>,
623}
624
625#[derive(Debug, Deserialize)]
626#[allow(dead_code)]
627struct OpenAIUsage {
628 prompt_tokens: usize,
629 completion_tokens: usize,
630 total_tokens: usize,
631}