Skip to main content

langchain_rust/llm/openai/
mod.rs

1use std::pin::Pin;
2
3pub use async_openai::config::{AzureConfig, Config, OpenAIConfig};
4
5use async_openai::types::chat::{
6    ChatChoiceStream, ChatCompletionMessageToolCalls, ChatCompletionRequestAssistantMessageArgs,
7    ChatCompletionRequestMessage, ChatCompletionRequestMessageContentPartImageArgs,
8    ChatCompletionRequestMessageContentPartTextArgs, ChatCompletionRequestSystemMessageArgs,
9    ChatCompletionRequestToolMessageArgs, ChatCompletionRequestUserMessageArgs,
10    ChatCompletionRequestUserMessageContent, ChatCompletionRequestUserMessageContentPart,
11    ChatCompletionStreamOptions, ChatCompletionToolChoiceOption, ChatCompletionTools,
12    CreateChatCompletionRequest, CreateChatCompletionRequestArgs,
13    CreateChatCompletionStreamResponse, ImageDetail, ImageUrl, ResponseFormat, StopConfiguration,
14};
15use async_openai::{error::OpenAIError, Client};
16use async_trait::async_trait;
17use futures::{Stream, StreamExt};
18
19use crate::observability::{traced_event_stream, traced_generate};
20use crate::schemas::convert::{LangchainIntoOpenAI, TryLangchainIntoOpenAI};
21use crate::{
22    language_models::{
23        box_llm_events, llm::LLM, options::CallOptions, GenerateResult, LLMError, LLMEvent,
24        LLMEventStream, TokenUsage,
25    },
26    schemas::{
27        messages::{ImageContent, Message, MessageType},
28        StreamData, ToolCall,
29    },
30};
31
32#[derive(Clone)]
33pub enum OpenAIModel {
34    Gpt35,
35    Gpt4,
36    Gpt4Turbo,
37    Gpt4o,
38    Gpt4oMini,
39}
40
41impl ToString for OpenAIModel {
42    fn to_string(&self) -> String {
43        match self {
44            OpenAIModel::Gpt35 => "gpt-3.5-turbo".to_string(),
45            OpenAIModel::Gpt4 => "gpt-4".to_string(),
46            OpenAIModel::Gpt4Turbo => "gpt-4-turbo-preview".to_string(),
47            OpenAIModel::Gpt4o => "gpt-4o".to_string(),
48            OpenAIModel::Gpt4oMini => "gpt-4o-mini".to_string(),
49        }
50    }
51}
52
53impl Into<String> for OpenAIModel {
54    fn into(self) -> String {
55        self.to_string()
56    }
57}
58
59#[derive(Clone)]
60pub struct OpenAI<C: Config> {
61    config: C,
62    options: CallOptions,
63    model: String,
64}
65
66/// `provider` field of the `llm.generate` span. OpenAI-compatible servers report as "openai".
67const PROVIDER: &str = "openai";
68
69impl<C: Config> OpenAI<C> {
70    pub fn new(config: C) -> Self {
71        Self {
72            config,
73            options: CallOptions::default(),
74            model: OpenAIModel::Gpt4oMini.to_string(),
75        }
76    }
77
78    pub fn with_model<S: Into<String>>(mut self, model: S) -> Self {
79        self.model = model.into();
80        self
81    }
82
83    pub fn with_config(mut self, config: C) -> Self {
84        self.config = config;
85        self
86    }
87
88    pub fn with_options(mut self, options: CallOptions) -> Self {
89        self.options = options;
90        self
91    }
92}
93
94impl Default for OpenAI<OpenAIConfig> {
95    fn default() -> Self {
96        Self::new(OpenAIConfig::default())
97    }
98}
99
100impl<C: Config + Clone + Send + Sync + 'static> OpenAI<C> {
101    async fn generate_untraced(&self, prompt: &[Message]) -> Result<GenerateResult, LLMError> {
102        let client = Client::with_config(self.config.clone());
103        let request = self.generate_request(prompt, self.options.streaming_func.is_some())?;
104        match &self.options.streaming_func {
105            Some(func) => {
106                let mut stream = client.chat().create_stream(request).await?;
107                let mut generate_result = GenerateResult::default();
108                while let Some(result) = stream.next().await {
109                    match result {
110                        Ok(response) => {
111                            if let Some(usage) = response.usage {
112                                generate_result.tokens = Some(TokenUsage {
113                                    prompt_tokens: usage.prompt_tokens,
114                                    completion_tokens: usage.completion_tokens,
115                                    total_tokens: usage.total_tokens,
116                                });
117                            }
118                            for chat_choice in response.choices.iter() {
119                                let chat_choice: ChatChoiceStream = chat_choice.clone();
120                                {
121                                    let mut func = func.lock().await;
122                                    let _ = func(
123                                        serde_json::to_string(&chat_choice).unwrap_or("".into()),
124                                    )
125                                    .await;
126                                }
127                                if let Some(content) = chat_choice.delta.content {
128                                    generate_result.generation.push_str(&content);
129                                }
130                            }
131                        }
132                        Err(err) => {
133                            // Used to be printed and dropped, returning a truncated answer.
134                            return Err(err.into());
135                        }
136                    }
137                }
138                Ok(generate_result)
139            }
140            None => {
141                let response = client.chat().create(request).await?;
142                let mut generate_result = GenerateResult::default();
143
144                if let Some(usage) = response.usage {
145                    generate_result.tokens = Some(TokenUsage {
146                        prompt_tokens: usage.prompt_tokens,
147                        completion_tokens: usage.completion_tokens,
148                        total_tokens: usage.total_tokens,
149                    });
150                }
151
152                if let Some(choice) = &response.choices.first() {
153                    generate_result.generation = choice.message.content.clone().unwrap_or_default();
154                    if let Some(calls) = &choice.message.tool_calls {
155                        let typed: Vec<ToolCall> = calls
156                            .iter()
157                            .filter_map(|c| match c {
158                                ChatCompletionMessageToolCalls::Function(f) => Some(ToolCall::new(
159                                    &f.id,
160                                    &f.function.name,
161                                    &f.function.arguments,
162                                )),
163                                _ => None,
164                            })
165                            .collect();
166                        if typed.is_empty() {
167                            // Only non-function (custom) tool calls: keep the raw JSON.
168                            generate_result.generation =
169                                serde_json::to_string(&calls).unwrap_or_default();
170                        } else {
171                            generate_result = generate_result.with_tool_calls(typed);
172                        }
173                    }
174                } else {
175                    generate_result.generation = "".to_string();
176                }
177
178                Ok(generate_result)
179            }
180        }
181    }
182}
183
184#[async_trait]
185impl<C: Config + Clone + Send + Sync + 'static> LLM for OpenAI<C> {
186    async fn generate(&self, prompt: &[Message]) -> Result<GenerateResult, LLMError> {
187        traced_generate(PROVIDER, &self.model, self.generate_untraced(prompt)).await
188    }
189
190    async fn invoke(&self, prompt: &str) -> Result<String, LLMError> {
191        self.generate(&[Message::new_human_message(prompt)])
192            .await
193            .map(|res| res.generation)
194    }
195
196    async fn stream(
197        &self,
198        messages: &[Message],
199    ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamData, LLMError>> + Send>>, LLMError> {
200        let client = Client::with_config(self.config.clone());
201        let request = self.generate_request(messages, true)?;
202
203        let original_stream = client.chat().create_stream(request).await?;
204
205        let new_stream = original_stream.map(|result| match result {
206            Ok(completion) => {
207                let value_completion = serde_json::to_value(completion).map_err(LLMError::from)?;
208                let tokens = match value_completion.pointer("/usage") {
209                    Some(usage) if !usage.is_null() => Some(
210                        serde_json::from_value::<TokenUsage>(usage.clone())
211                            .map_err(LLMError::from)?,
212                    ),
213                    _ => None,
214                };
215                // Some OpenAI-compatible providers send usage together with the last
216                // content delta (#290), so the content is read even when usage is present.
217                let content = value_completion.pointer("/choices/0/delta/content");
218                if tokens.is_none() && content.is_none() {
219                    return Err(LLMError::ContentNotFound(
220                        "/choices/0/delta/content".to_string(),
221                    ));
222                }
223                let content = content
224                    .and_then(serde_json::Value::as_str)
225                    .unwrap_or("")
226                    .to_string();
227
228                Ok(StreamData::new(value_completion, tokens, content))
229            }
230            Err(e) => Err(LLMError::from(e)),
231        });
232
233        Ok(Box::pin(new_stream))
234    }
235
236    fn generate_stream<'a>(&'a self, messages: &'a [Message]) -> LLMEventStream<'a> {
237        let events = box_llm_events(async_stream::try_stream! {
238            let client = Client::with_config(self.config.clone());
239            let request = self.generate_request(messages, true)?;
240            let mut stream = client.chat().create_stream(request).await?;
241
242            let mut acc = StreamAccumulator::default();
243            while let Some(chunk) = stream.next().await {
244                if let Some(text) = acc.push(chunk?) {
245                    yield LLMEvent::Text(text);
246                }
247            }
248            yield LLMEvent::Done(acc.finish());
249        });
250        traced_event_stream(PROVIDER, &self.model, events)
251    }
252
253    fn add_options(&mut self, options: CallOptions) {
254        self.options.merge_options(options)
255    }
256}
257
258/// Rebuilds a [`GenerateResult`] from OpenAI stream chunks. Tool calls arrive as
259/// fragments keyed by `index`: the first carries `id` and `name`, the rest append to
260/// `arguments`.
261#[derive(Default)]
262struct StreamAccumulator {
263    text: String,
264    calls: std::collections::BTreeMap<u32, ToolCall>,
265    tokens: Option<TokenUsage>,
266}
267
268impl StreamAccumulator {
269    /// Applies one chunk; returns its text delta, if any. Only the first choice is used.
270    fn push(&mut self, chunk: CreateChatCompletionStreamResponse) -> Option<String> {
271        if let Some(usage) = chunk.usage {
272            self.tokens = Some(TokenUsage {
273                prompt_tokens: usage.prompt_tokens,
274                completion_tokens: usage.completion_tokens,
275                total_tokens: usage.total_tokens,
276            });
277        }
278        let delta = chunk.choices.into_iter().find(|c| c.index == 0)?.delta;
279        for part in delta.tool_calls.unwrap_or_default() {
280            let call = self.calls.entry(part.index).or_default();
281            if let Some(id) = part.id {
282                call.id.push_str(&id);
283            }
284            if let Some(function) = part.function {
285                call.name
286                    .push_str(function.name.as_deref().unwrap_or_default());
287                call.arguments
288                    .push_str(function.arguments.as_deref().unwrap_or_default());
289            }
290        }
291        let text = delta.content.filter(|t| !t.is_empty())?;
292        self.text.push_str(&text);
293        Some(text)
294    }
295
296    fn finish(self) -> GenerateResult {
297        GenerateResult {
298            tokens: self.tokens,
299            generation: self.text,
300            ..Default::default()
301        }
302        .with_tool_calls(self.calls.into_values().collect())
303    }
304}
305
306/// An image as an OpenAI `image_url` part: http(s) URLs pass through, base64 data
307/// becomes a `data:` URL, and `detail` maps to OpenAI's levels.
308fn openai_image_url(image: &ImageContent) -> Result<ImageUrl, OpenAIError> {
309    let detail = match image.detail.as_deref().map(str::to_ascii_lowercase) {
310        None => None,
311        Some(d) if d == "auto" => Some(ImageDetail::Auto),
312        Some(d) if d == "low" => Some(ImageDetail::Low),
313        Some(d) if d == "high" => Some(ImageDetail::High),
314        Some(other) => {
315            return Err(OpenAIError::InvalidArgument(format!(
316                "unknown image detail {other:?}, expected auto, low or high"
317            )))
318        }
319    };
320    Ok(ImageUrl {
321        url: image.source().to_url(),
322        detail,
323    })
324}
325
326impl<C: Config> OpenAI<C> {
327    fn to_openai_messages(
328        &self,
329        messages: &[Message],
330    ) -> Result<Vec<ChatCompletionRequestMessage>, LLMError> {
331        let mut openai_messages: Vec<ChatCompletionRequestMessage> = Vec::new();
332        for m in messages {
333            match m.message_type {
334                MessageType::AIMessage => openai_messages.push(match &m.tool_calls {
335                    Some(value) => {
336                        let function: Vec<ChatCompletionMessageToolCalls> =
337                            serde_json::from_value(value.clone())?;
338                        ChatCompletionRequestAssistantMessageArgs::default()
339                            .tool_calls(function)
340                            .content(m.content.clone())
341                            .build()?
342                            .into()
343                    }
344                    None => ChatCompletionRequestAssistantMessageArgs::default()
345                        .content(m.content.clone())
346                        .build()?
347                        .into(),
348                }),
349                MessageType::HumanMessage => {
350                    let content: ChatCompletionRequestUserMessageContent = match &m.images {
351                        Some(images) if !images.is_empty() => {
352                            let mut parts: Vec<ChatCompletionRequestUserMessageContentPart> =
353                                Vec::with_capacity(images.len() + 1);
354                            if !m.content.is_empty() {
355                                parts.push(
356                                    ChatCompletionRequestMessageContentPartTextArgs::default()
357                                        .text(m.content.clone())
358                                        .build()?
359                                        .into(),
360                                );
361                            }
362                            for image in images {
363                                parts.push(
364                                    ChatCompletionRequestMessageContentPartImageArgs::default()
365                                        .image_url(openai_image_url(image)?)
366                                        .build()?
367                                        .into(),
368                                );
369                            }
370                            parts.into()
371                        }
372                        _ => m.content.clone().into(),
373                    };
374
375                    openai_messages.push(
376                        ChatCompletionRequestUserMessageArgs::default()
377                            .content(content)
378                            .build()?
379                            .into(),
380                    )
381                }
382                MessageType::SystemMessage => openai_messages.push(
383                    ChatCompletionRequestSystemMessageArgs::default()
384                        .content(m.content.clone())
385                        .build()?
386                        .into(),
387                ),
388                MessageType::ToolMessage => {
389                    openai_messages.push(
390                        ChatCompletionRequestToolMessageArgs::default()
391                            .content(m.content.clone())
392                            .tool_call_id(m.id.clone().unwrap_or_default())
393                            .build()?
394                            .into(),
395                    );
396                }
397            }
398        }
399        Ok(openai_messages)
400    }
401
402    fn generate_request(
403        &self,
404        messages: &[Message],
405        stream: bool,
406    ) -> Result<CreateChatCompletionRequest, LLMError> {
407        let messages: Vec<ChatCompletionRequestMessage> = self.to_openai_messages(messages)?;
408        let mut request_builder = CreateChatCompletionRequestArgs::default();
409        if let Some(temperature) = self.options.temperature {
410            request_builder.temperature(temperature);
411        }
412        if let Some(max_tokens) = self.options.max_tokens {
413            request_builder.max_tokens(max_tokens);
414        }
415        if stream {
416            request_builder.stream(true);
417            if let Some(include_usage) = self.options.stream_usage {
418                request_builder.stream_options(ChatCompletionStreamOptions {
419                    include_usage: Some(include_usage),
420                    include_obfuscation: None,
421                });
422            }
423        }
424        request_builder.model(self.model.to_string());
425        if let Some(stop_words) = &self.options.stop_words {
426            request_builder.stop(StopConfiguration::StringArray(stop_words.clone()));
427        }
428
429        if let Some(functions) = &self.options.functions {
430            let functions: Result<Vec<ChatCompletionTools>, OpenAIError> = functions
431                .clone()
432                .into_iter()
433                .map(|f| f.try_into_openai())
434                .collect();
435            request_builder.tools(functions?);
436        }
437
438        if let Some(behavior) = &self.options.function_call_behavior {
439            request_builder
440                .tool_choice::<ChatCompletionToolChoiceOption>(behavior.clone().into_openai());
441        }
442
443        if let Some(response_format) = &self.options.response_format {
444            request_builder
445                .response_format::<ResponseFormat>(response_format.clone().into_openai());
446        }
447
448        request_builder.messages(messages);
449        Ok(request_builder.build()?)
450    }
451}
452
453#[cfg(test)]
454mod mock_tests;
455
456#[cfg(test)]
457mod tests {
458    use crate::schemas::FunctionDefinition;
459
460    use super::*;
461
462    use base64::prelude::*;
463    use serde_json::json;
464    use std::sync::Arc;
465    use tokio::sync::Mutex;
466    use tokio::test;
467
468    #[test]
469    #[ignore]
470    async fn test_invoke() {
471        let message_complete = Arc::new(Mutex::new(String::new()));
472
473        // Define the streaming function
474        // This function will append the content received from the stream to `message_complete`
475        let streaming_func = {
476            let message_complete = message_complete.clone();
477            move |content: String| {
478                let message_complete = message_complete.clone();
479                async move {
480                    let mut message_complete_lock = message_complete.lock().await;
481                    println!("Content: {:?}", content);
482                    message_complete_lock.push_str(&content);
483                    Ok(())
484                }
485            }
486        };
487        let options = CallOptions::new().with_streaming_func(streaming_func);
488        // Setup the OpenAI client with the necessary options
489        let open_ai = OpenAI::new(OpenAIConfig::default())
490            .with_model(OpenAIModel::Gpt35.to_string()) // You can change the model as needed
491            .with_options(options);
492
493        // Define a set of messages to send to the generate function
494
495        // Call the generate function
496        match open_ai.invoke("hola").await {
497            Ok(result) => {
498                // Print the response from the generate function
499                println!("Generate Result: {:?}", result);
500                println!("Message Complete: {:?}", message_complete.lock().await);
501            }
502            Err(e) => {
503                // Handle any errors
504                eprintln!("Error calling generate: {:?}", e);
505            }
506        }
507    }
508
509    #[test]
510    #[ignore]
511    async fn test_generate_function() {
512        let message_complete = Arc::new(Mutex::new(String::new()));
513
514        // Define the streaming function
515        // This function will append the content received from the stream to `message_complete`
516        let streaming_func = {
517            let message_complete = message_complete.clone();
518            move |content: String| {
519                let message_complete = message_complete.clone();
520                async move {
521                    let content = serde_json::from_str::<ChatChoiceStream>(&content).unwrap();
522                    if content.finish_reason.is_some() {
523                        return Ok(());
524                    }
525                    let mut message_complete_lock = message_complete.lock().await;
526                    println!("Content: {:?}", content);
527                    message_complete_lock.push_str(&content.delta.content.unwrap());
528                    Ok(())
529                }
530            }
531        };
532        // Define the streaming function as an async block without capturing external references directly
533        let options = CallOptions::new().with_streaming_func(streaming_func);
534        // Setup the OpenAI client with the necessary options
535        let open_ai = OpenAI::new(OpenAIConfig::default())
536            .with_model(OpenAIModel::Gpt35.to_string()) // You can change the model as needed
537            .with_options(options);
538
539        // Define a set of messages to send to the generate function
540        let messages = vec![Message::new_human_message("Hello, how are you?")];
541
542        // Call the generate function
543        match open_ai.generate(&messages).await {
544            Ok(result) => {
545                // Print the response from the generate function
546                println!("Generate Result: {:?}", result);
547                println!("Message Complete: {:?}", message_complete.lock().await);
548            }
549            Err(e) => {
550                // Handle any errors
551                eprintln!("Error calling generate: {:?}", e);
552            }
553        }
554    }
555
556    #[test]
557    #[ignore]
558    async fn test_openai_stream() {
559        // Setup the OpenAI client with the necessary options
560        let open_ai = OpenAI::default().with_model(OpenAIModel::Gpt35.to_string());
561
562        // Define a set of messages to send to the generate function
563        let messages = vec![Message::new_human_message("Hello, how are you?")];
564
565        open_ai
566            .stream(&messages)
567            .await
568            .unwrap()
569            .for_each(|result| async {
570                match result {
571                    Ok(stream_data) => {
572                        println!("Stream Data: {:?}", stream_data.content);
573                    }
574                    Err(e) => {
575                        eprintln!("Error calling generate: {:?}", e);
576                    }
577                }
578            })
579            .await;
580    }
581
582    #[test]
583    #[ignore]
584    async fn test_function() {
585        let mut functions = Vec::new();
586        functions.push(FunctionDefinition {
587            name: "cli".to_string(),
588            description: "Use the Ubuntu command line to preform any action you wish.".to_string(),
589            parameters: json!({
590                "type": "object",
591                "properties": {
592                    "command": {
593                        "type": "string",
594                        "description": "The raw command you want executed"
595                    }
596                },
597                "required": ["command"]
598            }),
599        });
600
601        let llm = OpenAI::default()
602            .with_model(OpenAIModel::Gpt35)
603            .with_config(OpenAIConfig::new())
604            .with_options(CallOptions::new().with_functions(functions));
605        let response = llm
606            .invoke("Use the command line to create a new rust project. Execute the first command.")
607            .await
608            .unwrap();
609        println!("{}", response)
610    }
611
612    #[test]
613    #[ignore]
614    async fn test_generate_with_image_message() {
615        // Setup the OpenAI client with the necessary options
616        let open_ai =
617            OpenAI::new(OpenAIConfig::default()).with_model(OpenAIModel::Gpt4o.to_string());
618
619        // Convert image to base64
620        let image = std::fs::read("./src/llm/test_data/example.jpg").unwrap();
621        let image_base64 = BASE64_STANDARD.encode(image);
622
623        // Define a set of messages to send to the generate function
624        let image_urls = vec![format!("data:image/jpeg;base64,{image_base64}")];
625        let messages = vec![
626            Message::new_human_message("Describe this image"),
627            Message::new_human_message_with_images(image_urls),
628        ];
629
630        // Call the generate function
631        let response = open_ai.generate(&messages).await.unwrap();
632        println!("Response: {:?}", response);
633    }
634}