1use std::sync::Arc;
2use std::sync::Mutex;
3
4use anyhow::{Context as _, Result};
5use async_openai::types::ChatCompletionStreamOptions;
6use async_openai::types::{
7 ChatCompletionMessageToolCall, ChatCompletionRequestAssistantMessageArgs,
8 ChatCompletionRequestSystemMessageArgs, ChatCompletionRequestToolMessageArgs,
9 ChatCompletionRequestUserMessageArgs, ChatCompletionTool, ChatCompletionToolArgs,
10 ChatCompletionToolType, FunctionCall, FunctionObjectArgs,
11};
12use async_trait::async_trait;
13use futures_util::StreamExt as _;
14use futures_util::stream;
15use itertools::Itertools;
16use serde::Serialize;
17use serde_json::json;
18use swiftide_core::ChatCompletionStream;
19use swiftide_core::chat_completion::{
20 ChatCompletion, ChatCompletionRequest, ChatCompletionResponse, ChatMessage, ToolCall, ToolSpec,
21 errors::LanguageModelError,
22};
23use swiftide_core::chat_completion::{Usage, UsageBuilder};
24#[cfg(feature = "metrics")]
25use swiftide_core::metrics::emit_usage;
26
27use super::GenericOpenAI;
28use super::openai_error_to_language_model_error;
29use super::responses_api::{
30 build_responses_request_from_chat, response_to_chat_completion, responses_stream_adapter,
31};
32use super::{
33 ensure_tool_schema_additional_properties_false, ensure_tool_schema_required_matches_properties,
34};
35use tracing_futures::Instrument;
36
37#[async_trait]
38impl<
39 C: async_openai::config::Config
40 + std::default::Default
41 + Sync
42 + Send
43 + std::fmt::Debug
44 + Clone
45 + 'static,
46> ChatCompletion for GenericOpenAI<C>
47{
48 #[cfg_attr(not(feature = "langfuse"), tracing::instrument(skip_all, err))]
49 #[cfg_attr(
50 feature = "langfuse",
51 tracing::instrument(skip_all, err, fields(langfuse.type = "GENERATION"))
52 )]
53 async fn complete(
54 &self,
55 request: &ChatCompletionRequest,
56 ) -> Result<ChatCompletionResponse, LanguageModelError> {
57 if self.is_responses_api_enabled() {
58 return self.complete_via_responses_api(request).await;
59 }
60
61 let model = self
62 .default_options
63 .prompt_model
64 .as_ref()
65 .context("Model not set")?;
66
67 let messages = request
68 .messages()
69 .iter()
70 .map(message_to_openai)
71 .collect::<Result<Vec<_>>>()?;
72
73 let mut openai_request = self
75 .chat_completion_request_defaults()
76 .model(model)
77 .messages(messages)
78 .to_owned();
79
80 if !request.tools_spec.is_empty() {
81 openai_request
82 .tools(
83 request
84 .tools_spec()
85 .iter()
86 .map(tools_to_openai)
87 .collect::<Result<Vec<_>>>()?,
88 )
89 .tool_choice("auto");
90 if let Some(par) = self.default_options.parallel_tool_calls {
91 openai_request.parallel_tool_calls(par);
92 }
93 }
94
95 let request = openai_request
96 .build()
97 .map_err(openai_error_to_language_model_error)?;
98
99 tracing::trace!(model, ?request, "Sending request to OpenAI");
100
101 let response = self
102 .client
103 .chat()
104 .create(request.clone())
105 .await
106 .map_err(openai_error_to_language_model_error)?;
107
108 tracing::trace!(?response, "[ChatCompletion] Full response from OpenAI");
109 let mut builder = ChatCompletionResponse::builder()
112 .maybe_message(
113 response
114 .choices
115 .first()
116 .and_then(|choice| choice.message.content.clone()),
117 )
118 .maybe_tool_calls(
119 response
120 .choices
121 .first()
122 .and_then(|choice| choice.message.tool_calls.clone())
123 .map(|tool_calls| {
124 tool_calls
125 .iter()
126 .map(|tool_call| {
127 ToolCall::builder()
128 .id(tool_call.id.clone())
129 .args(tool_call.function.arguments.clone())
130 .name(tool_call.function.name.clone())
131 .build()
132 .expect("infallible")
133 })
134 .collect_vec()
135 }),
136 )
137 .to_owned();
138
139 if let Some(usage) = &response.usage {
140 let usage = UsageBuilder::default()
141 .prompt_tokens(usage.prompt_tokens)
142 .completion_tokens(usage.completion_tokens)
143 .total_tokens(usage.total_tokens)
144 .build()
145 .map_err(LanguageModelError::permanent)?;
146
147 builder.usage(usage);
148 }
149
150 let our_response = builder.build().map_err(LanguageModelError::from)?;
151
152 self.track_completion(
153 model,
154 our_response.usage.as_ref(),
155 Some(&request),
156 Some(&our_response),
157 );
158
159 Ok(our_response)
160 }
161
162 #[tracing::instrument(skip_all)]
163 async fn complete_stream(&self, request: &ChatCompletionRequest) -> ChatCompletionStream {
164 if self.is_responses_api_enabled() {
165 return self.complete_stream_via_responses_api(request).await;
166 }
167
168 let Some(model_name) = self.default_options.prompt_model.clone() else {
169 return LanguageModelError::permanent("Model not set").into();
170 };
171
172 #[cfg(not(any(feature = "metrics", feature = "langfuse")))]
173 let _ = &model_name;
174
175 let messages = match request
176 .messages()
177 .iter()
178 .map(message_to_openai)
179 .collect::<Result<Vec<_>>>()
180 {
181 Ok(messages) => messages,
182 Err(e) => return LanguageModelError::from(e).into(),
183 };
184
185 let mut openai_request = self
187 .chat_completion_request_defaults()
188 .model(&model_name)
189 .messages(messages)
190 .stream_options(ChatCompletionStreamOptions {
191 include_usage: true,
192 })
193 .to_owned();
194
195 if !request.tools_spec.is_empty() {
196 openai_request
197 .tools(
198 match request
199 .tools_spec()
200 .iter()
201 .map(tools_to_openai)
202 .collect::<Result<Vec<_>>>()
203 {
204 Ok(tools) => tools,
205 Err(e) => {
206 return LanguageModelError::from(e).into();
207 }
208 },
209 )
210 .tool_choice("auto");
211 if let Some(par) = self.default_options.parallel_tool_calls {
212 openai_request.parallel_tool_calls(par);
213 }
214 }
215
216 let request = match openai_request.build() {
217 Ok(request) => request,
218 Err(e) => {
219 return openai_error_to_language_model_error(e).into();
220 }
221 };
222
223 tracing::trace!(model = %model_name, ?request, "Sending request to OpenAI");
224
225 let response = match self.client.chat().create_stream(request.clone()).await {
226 Ok(response) => response,
227 Err(e) => return openai_error_to_language_model_error(e).into(),
228 };
229
230 let accumulating_response = Arc::new(Mutex::new(ChatCompletionResponse::default()));
231 let final_response = accumulating_response.clone();
232 let stream_full = self.stream_full;
233
234 let span = if cfg!(feature = "langfuse") {
235 tracing::info_span!(
236 "stream",
237 langfuse.type = "GENERATION",
238 )
239 } else {
240 tracing::info_span!("stream")
241 };
242
243 let self_for_stream = self.clone();
244 let stream = response
245 .map(move |chunk| match chunk {
246 Ok(chunk) => {
247 let accumulating_response = Arc::clone(&accumulating_response);
248
249 let delta_message = chunk
250 .choices
251 .first()
252 .and_then(|d| d.delta.content.as_deref());
253 let delta_tool_calls = chunk
254 .choices
255 .first()
256 .and_then(|d| d.delta.tool_calls.as_deref());
257 let usage = chunk.usage.as_ref();
258
259 let chat_completion_response = {
260 let mut lock = accumulating_response.lock().unwrap();
261 lock.append_message_delta(delta_message);
262
263 if let Some(delta_tool_calls) = delta_tool_calls {
264 for tc in delta_tool_calls {
265 lock.append_tool_call_delta(
266 tc.index as usize,
267 tc.id.as_deref(),
268 tc.function.as_ref().and_then(|f| f.name.as_deref()),
269 tc.function.as_ref().and_then(|f| f.arguments.as_deref()),
270 );
271 }
272 }
273
274 if let Some(usage) = usage {
275 lock.append_usage_delta(
276 usage.prompt_tokens,
277 usage.completion_tokens,
278 usage.total_tokens,
279 );
280 }
281
282 if stream_full {
283 lock.clone()
284 } else {
285 ChatCompletionResponse {
289 id: lock.id,
290 message: None,
291 tool_calls: None,
292 usage: None,
293 delta: lock.delta.clone(),
294 }
295 }
296 };
297
298 Ok(chat_completion_response)
299 }
300 Err(e) => Err(openai_error_to_language_model_error(e)),
301 })
302 .chain(
303 stream::iter(vec![final_response]).map(move |accumulating_response| {
304 let lock = accumulating_response.lock().unwrap();
305
306 self_for_stream.track_completion(
307 &model_name,
308 lock.usage.as_ref(),
309 Some(&request),
310 Some(&*lock),
311 );
312
313 Ok(lock.clone())
314 }),
315 );
316
317 let stream = tracing_futures::Instrument::instrument(stream, span);
318
319 Box::pin(stream)
320 }
321}
322
323impl<
324 C: async_openai::config::Config
325 + std::default::Default
326 + Sync
327 + Send
328 + std::fmt::Debug
329 + Clone
330 + 'static,
331> GenericOpenAI<C>
332{
333 async fn complete_via_responses_api(
334 &self,
335 request: &ChatCompletionRequest,
336 ) -> Result<ChatCompletionResponse, LanguageModelError> {
337 let model = self
338 .default_options
339 .prompt_model
340 .as_ref()
341 .context("Model not set")?;
342
343 let create_request = build_responses_request_from_chat(self, request)?;
344
345 let response = self
346 .client
347 .responses()
348 .create(create_request.clone())
349 .await
350 .map_err(openai_error_to_language_model_error)?;
351
352 let completion = response_to_chat_completion(&response)?;
353
354 self.track_completion(
355 model,
356 completion.usage.as_ref(),
357 Some(&create_request),
358 Some(&completion),
359 );
360
361 Ok(completion)
362 }
363
364 #[allow(clippy::too_many_lines)]
365 async fn complete_stream_via_responses_api(
366 &self,
367 request: &ChatCompletionRequest,
368 ) -> ChatCompletionStream {
369 #[allow(unused_variables)]
370 let Some(model_name) = self.default_options.prompt_model.clone() else {
371 return LanguageModelError::permanent("Model not set").into();
372 };
373
374 let mut create_request = match build_responses_request_from_chat(self, request) {
375 Ok(req) => req,
376 Err(err) => return err.into(),
377 };
378
379 create_request.stream = Some(true);
380
381 let stream = match self
382 .client
383 .responses()
384 .create_stream(create_request.clone())
385 .await
386 {
387 Ok(stream) => stream,
388 Err(err) => return openai_error_to_language_model_error(err).into(),
389 };
390
391 let stream_full = self.stream_full;
392
393 let span = if cfg!(feature = "langfuse") {
394 tracing::info_span!("responses_stream", langfuse.type = "GENERATION")
395 } else {
396 tracing::info_span!("responses_stream")
397 };
398
399 let mapped_stream = responses_stream_adapter(stream, stream_full);
400
401 let this = self.clone();
402 let tracked_request = create_request.clone();
403
404 let mapped_stream = mapped_stream.map(move |result| match result {
405 Ok(item) => {
406 if item.finished {
407 this.track_completion(
408 &model_name,
409 item.response.usage.as_ref(),
410 Some(&tracked_request),
411 Some(&item.response),
412 );
413 }
414
415 Ok(item.response)
416 }
417 Err(err) => Err(err),
418 });
419
420 Box::pin(Instrument::instrument(mapped_stream, span))
421 }
422 #[allow(unused_variables)]
423 pub(crate) fn track_completion<R, S>(
424 &self,
425 model: &str,
426 usage: Option<&Usage>,
427 request: Option<&R>,
428 response: Option<&S>,
429 ) where
430 R: Serialize + ?Sized,
431 S: Serialize + ?Sized,
432 {
433 if let Some(usage) = usage {
434 let cb_usage = usage.clone();
435 if let Some(callback) = &self.on_usage {
436 let callback = callback.clone();
437 tokio::spawn(async move {
438 if let Err(err) = callback(&cb_usage).await {
439 tracing::error!("Error in on_usage callback: {err}");
440 }
441 });
442 }
443
444 #[cfg(feature = "metrics")]
445 emit_usage(
446 model,
447 usage.prompt_tokens.into(),
448 usage.completion_tokens.into(),
449 usage.total_tokens.into(),
450 self.metric_metadata.as_ref(),
451 );
452 }
453
454 #[cfg(feature = "langfuse")]
455 tracing::debug!(
456 langfuse.model = model,
457 langfuse.input = request.and_then(langfuse_json).unwrap_or_default(),
458 langfuse.output = response.and_then(langfuse_json).unwrap_or_default(),
459 langfuse.usage = usage.and_then(langfuse_json).unwrap_or_default(),
460 );
461 }
462}
463
464#[cfg(feature = "langfuse")]
465pub(crate) fn langfuse_json<T: Serialize + ?Sized>(value: &T) -> Option<String> {
466 serde_json::to_string_pretty(value).ok()
467}
468
469#[cfg(not(feature = "langfuse"))]
470#[allow(dead_code)]
471pub(crate) fn langfuse_json<T>(_value: &T) -> Option<String> {
472 None
473}
474
475pub(crate) fn usage_from_counts(
476 prompt_tokens: u32,
477 completion_tokens: u32,
478 total_tokens: u32,
479) -> Usage {
480 Usage {
481 prompt_tokens,
482 completion_tokens,
483 total_tokens,
484 }
485}
486
487fn tools_to_openai(spec: &ToolSpec) -> Result<ChatCompletionTool> {
488 let mut parameters = match &spec.parameters_schema {
489 Some(schema) => serde_json::to_value(schema)?,
490 None => json!({
491 "type": "object",
492 "properties": {},
493 "required": [],
494 "additionalProperties": false,
495 }),
496 };
497
498 ensure_tool_schema_additional_properties_false(&mut parameters)
499 .context("tool schema must allow no additional properties")?;
500 ensure_tool_schema_required_matches_properties(&mut parameters)
501 .context("tool schema must list required properties")?;
502 tracing::debug!(
503 parameters = serde_json::to_string_pretty(¶meters).unwrap(),
504 tool = %spec.name,
505 "Tool parameters schema"
506 );
507
508 ChatCompletionToolArgs::default()
509 .r#type(ChatCompletionToolType::Function)
510 .function(
511 FunctionObjectArgs::default()
512 .name(&spec.name)
513 .description(&spec.description)
514 .strict(true)
515 .parameters(parameters)
516 .build()?,
517 )
518 .build()
519 .map_err(anyhow::Error::from)
520}
521
522fn message_to_openai(
523 message: &ChatMessage,
524) -> Result<async_openai::types::ChatCompletionRequestMessage> {
525 let openai_message = match message {
526 ChatMessage::User(msg) => ChatCompletionRequestUserMessageArgs::default()
527 .content(msg.as_str())
528 .build()?
529 .into(),
530 ChatMessage::System(msg) => ChatCompletionRequestSystemMessageArgs::default()
531 .content(msg.as_str())
532 .build()?
533 .into(),
534 ChatMessage::Summary(msg) => ChatCompletionRequestAssistantMessageArgs::default()
535 .content(msg.as_str())
536 .build()?
537 .into(),
538 ChatMessage::ToolOutput(tool_call, tool_output) => {
539 let Some(content) = tool_output.content() else {
540 return Ok(ChatCompletionRequestToolMessageArgs::default()
541 .tool_call_id(tool_call.id())
542 .build()?
543 .into());
544 };
545
546 ChatCompletionRequestToolMessageArgs::default()
547 .content(content)
548 .tool_call_id(tool_call.id())
549 .build()?
550 .into()
551 }
552 ChatMessage::Assistant(msg, tool_calls) => {
553 let mut builder = ChatCompletionRequestAssistantMessageArgs::default();
554
555 if let Some(msg) = msg {
556 builder.content(msg.as_str());
557 }
558
559 if let Some(tool_calls) = tool_calls {
560 builder.tool_calls(
561 tool_calls
562 .iter()
563 .map(|tool_call| ChatCompletionMessageToolCall {
564 id: tool_call.id().to_string(),
565 r#type: ChatCompletionToolType::Function,
566 function: FunctionCall {
567 name: tool_call.name().to_string(),
568 arguments: tool_call.args().unwrap_or_default().to_string(),
569 },
570 })
571 .collect::<Vec<_>>(),
572 );
573 }
574
575 builder.build()?.into()
576 }
577 };
578
579 Ok(openai_message)
580}
581
582#[cfg(test)]
583mod tests {
584 use crate::openai::{OpenAI, Options};
585
586 use super::*;
587 use wiremock::matchers::{method, path};
588 use wiremock::{Mock, MockServer, ResponseTemplate};
589
590 #[allow(dead_code)]
591 #[derive(schemars::JsonSchema)]
592 struct WeatherArgs {
593 city: String,
594 }
595
596 #[test]
597 fn test_tools_to_openai_sets_additional_properties_false() {
598 let spec = ToolSpec::builder()
599 .name("get_weather")
600 .description("Retrieve weather data")
601 .parameters_schema(schemars::schema_for!(WeatherArgs))
602 .build()
603 .unwrap();
604
605 let tool = tools_to_openai(&spec).expect("tool conversion succeeds");
606
607 assert_eq!(tool.r#type, ChatCompletionToolType::Function);
608
609 let additional_properties = tool
610 .function
611 .parameters
612 .as_ref()
613 .and_then(serde_json::Value::as_object)
614 .and_then(|obj| obj.get("additionalProperties"))
615 .cloned();
616
617 assert_eq!(
618 additional_properties,
619 Some(serde_json::Value::Bool(false)),
620 "Chat Completions require additionalProperties=false for tool parameters, got {}",
621 serde_json::to_string_pretty(&tool.function.parameters).unwrap()
622 );
623 }
624
625 #[test_log::test(tokio::test)]
626 async fn test_complete() {
627 let mock_server = MockServer::start().await;
628
629 let response_body = json!({
631 "id": "chatcmpl-B9MBs8CjcvOU2jLn4n570S5qMJKcT",
632 "object": "chat.completion",
633 "created": 123,
634 "model": "gpt-4o",
635 "choices": [
636 {
637 "index": 0,
638 "message": {
639 "role": "assistant",
640 "content": "Hello, world!",
641 "refusal": null,
642 "annotations": []
643 },
644 "logprobs": null,
645 "finish_reason": "stop"
646 }
647 ],
648 "usage": {
649 "prompt_tokens": 19,
650 "completion_tokens": 10,
651 "total_tokens": 29,
652 "prompt_tokens_details": {
653 "cached_tokens": 0,
654 "audio_tokens": 0
655 },
656 "completion_tokens_details": {
657 "reasoning_tokens": 0,
658 "audio_tokens": 0,
659 "accepted_prediction_tokens": 0,
660 "rejected_prediction_tokens": 0
661 }
662 },
663 "service_tier": "default"
664 });
665 Mock::given(method("POST"))
666 .and(path("/chat/completions"))
667 .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
668 .mount(&mock_server)
669 .await;
670
671 let config = async_openai::config::OpenAIConfig::new().with_api_base(mock_server.uri());
673 let async_openai = async_openai::Client::with_config(config);
674
675 let openai = OpenAI::builder()
676 .client(async_openai)
677 .default_prompt_model("gpt-4o")
678 .build()
679 .expect("Can create OpenAI client.");
680
681 let request = ChatCompletionRequest::builder()
683 .messages(vec![ChatMessage::User("Hi".to_string())])
684 .build()
685 .unwrap();
686
687 let response = openai.complete(&request).await.unwrap();
689
690 assert_eq!(response.message(), Some("Hello, world!"));
692
693 let usage = response.usage.unwrap();
695 assert_eq!(usage.prompt_tokens, 19);
696 assert_eq!(usage.completion_tokens, 10);
697 assert_eq!(usage.total_tokens, 29);
698 }
699
700 #[test_log::test(tokio::test)]
701 #[allow(clippy::items_after_statements)]
702 async fn test_complete_responses_api() {
703 use serde_json::Value;
704 use wiremock::{Request, Respond};
705
706 let mock_server = MockServer::start().await;
707
708 use async_openai::types::responses::{
709 CompletionTokensDetails, Content, OutputContent, OutputMessage, OutputStatus,
710 OutputText, PromptTokensDetails, Response as ResponsesResponse, Role, Status,
711 Usage as ResponsesUsage,
712 };
713
714 let response = ResponsesResponse {
715 created_at: 123,
716 error: None,
717 id: "resp_123".into(),
718 incomplete_details: None,
719 instructions: None,
720 max_output_tokens: None,
721 metadata: None,
722 model: "gpt-4.1-mini".into(),
723 object: "response".into(),
724 output: vec![OutputContent::Message(OutputMessage {
725 content: vec![Content::OutputText(OutputText {
726 annotations: Vec::new(),
727 text: "Hello via responses".into(),
728 })],
729 id: "msg_1".into(),
730 role: Role::Assistant,
731 status: OutputStatus::Completed,
732 })],
733 output_text: Some("Hello via responses".into()),
734 parallel_tool_calls: None,
735 previous_response_id: None,
736 reasoning: None,
737 store: None,
738 service_tier: None,
739 status: Status::Completed,
740 temperature: None,
741 text: None,
742 tool_choice: None,
743 tools: None,
744 top_p: None,
745 truncation: None,
746 usage: Some(ResponsesUsage {
747 input_tokens: 5,
748 input_tokens_details: PromptTokensDetails {
749 audio_tokens: Some(0),
750 cached_tokens: Some(0),
751 },
752 output_tokens: 3,
753 output_tokens_details: CompletionTokensDetails {
754 accepted_prediction_tokens: Some(0),
755 audio_tokens: Some(0),
756 reasoning_tokens: Some(0),
757 rejected_prediction_tokens: Some(0),
758 },
759 total_tokens: 8,
760 }),
761 user: None,
762 };
763
764 let response_body = serde_json::to_value(&response).unwrap();
765
766 struct ValidateResponsesRequest {
767 expected_model: &'static str,
768 response: Value,
769 }
770
771 impl Respond for ValidateResponsesRequest {
772 fn respond(&self, request: &Request) -> ResponseTemplate {
773 let body: Value = serde_json::from_slice(&request.body).unwrap();
774 assert_eq!(body["model"], self.expected_model);
775 let input = body["input"].as_array().expect("input array");
776 assert_eq!(input.len(), 1);
777 assert_eq!(input[0]["role"], "user");
778 assert_eq!(input[0]["content"], "Hello via prompt");
779
780 let _: async_openai::types::responses::Response =
781 serde_json::from_value(self.response.clone()).unwrap();
782
783 ResponseTemplate::new(200).set_body_json(self.response.clone())
784 }
785 }
786
787 Mock::given(method("POST"))
788 .and(path("/responses"))
789 .respond_with(ValidateResponsesRequest {
790 expected_model: "gpt-4.1-mini",
791 response: response_body,
792 })
793 .mount(&mock_server)
794 .await;
795
796 let config = async_openai::config::OpenAIConfig::new().with_api_base(mock_server.uri());
797 let async_openai = async_openai::Client::with_config(config);
798
799 let openai = OpenAI::builder()
800 .client(async_openai)
801 .default_prompt_model("gpt-4.1-mini")
802 .use_responses_api(true)
803 .build()
804 .expect("Can create OpenAI client.");
805
806 let request = ChatCompletionRequest::builder()
807 .messages(vec![ChatMessage::User("Hello via prompt".to_string())])
808 .build()
809 .unwrap();
810
811 let response = openai.complete(&request).await.unwrap();
812
813 assert_eq!(response.message(), Some("Hello via responses"));
814
815 let usage = response.usage.expect("usage present");
816 assert_eq!(usage.prompt_tokens, 5);
817 assert_eq!(usage.completion_tokens, 3);
818 assert_eq!(usage.total_tokens, 8);
819 }
820
821 #[test_log::test(tokio::test)]
822 #[allow(clippy::items_after_statements)]
823 async fn test_complete_with_all_default_settings() {
824 use serde_json::Value;
825 use wiremock::{Request, Respond, ResponseTemplate};
826
827 let mock_server = wiremock::MockServer::start().await;
828
829 struct ValidateAllSettings;
831
832 impl Respond for ValidateAllSettings {
833 fn respond(&self, request: &Request) -> ResponseTemplate {
834 let v: Value = serde_json::from_slice(&request.body).unwrap();
835
836 assert_eq!(v["model"], "gpt-4-turbo");
838 let arr = v["messages"].as_array().unwrap();
839 assert_eq!(arr.len(), 1);
840 assert_eq!(arr[0]["content"], "Test");
841
842 assert_eq!(v["parallel_tool_calls"], true);
843 assert_eq!(v["max_completion_tokens"], 77);
844 assert!((v["temperature"].as_f64().unwrap() - 0.42).abs() < 1e-5);
845 assert_eq!(v["reasoning_effort"], "low");
846 assert_eq!(v["seed"], 42);
847 assert!((v["presence_penalty"].as_f64().unwrap() - 1.1).abs() < 1e-5);
848
849 assert_eq!(v["metadata"], serde_json::json!({"key": "value"}));
851 assert_eq!(v["user"], "test-user");
852 ResponseTemplate::new(200).set_body_json(serde_json::json!({
853 "id": "chatcmpl-xxx",
854 "object": "chat.completion",
855 "created": 123,
856 "model": "gpt-4-turbo",
857 "choices": [{
858 "index": 0,
859 "message": {
860 "role": "assistant",
861 "content": "All settings validated",
862 "refusal": null,
863 "annotations": []
864 },
865 "logprobs": null,
866 "finish_reason": "stop"
867 }],
868 "usage": {
869 "prompt_tokens": 19,
870 "completion_tokens": 10,
871 "total_tokens": 29,
872 "prompt_tokens_details": {"cached_tokens": 0, "audio_tokens": 0},
873 "completion_tokens_details": {"reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0}
874 },
875 "service_tier": "default"
876 }))
877 }
878 }
879
880 wiremock::Mock::given(wiremock::matchers::method("POST"))
881 .and(wiremock::matchers::path("/chat/completions"))
882 .respond_with(ValidateAllSettings)
883 .mount(&mock_server)
884 .await;
885
886 let config = async_openai::config::OpenAIConfig::new().with_api_base(mock_server.uri());
887 let async_openai = async_openai::Client::with_config(config);
888
889 let openai = crate::openai::OpenAI::builder()
890 .client(async_openai)
891 .default_prompt_model("gpt-4-turbo")
892 .default_embed_model("not-used")
893 .parallel_tool_calls(Some(true))
894 .default_options(
895 Options::builder()
896 .max_completion_tokens(77)
897 .temperature(0.42)
898 .reasoning_effort(async_openai::types::ReasoningEffort::Low)
899 .seed(42)
900 .presence_penalty(1.1)
901 .metadata(serde_json::json!({"key": "value"}))
902 .user("test-user"),
903 )
904 .build()
905 .expect("Can create OpenAI client.");
906
907 let request = swiftide_core::chat_completion::ChatCompletionRequest::builder()
908 .messages(vec![swiftide_core::chat_completion::ChatMessage::User(
909 "Test".to_string(),
910 )])
911 .build()
912 .unwrap();
913
914 let response = openai.complete(&request).await.unwrap();
915
916 assert_eq!(response.message(), Some("All settings validated"));
917 }
918}