1use secrecy::{ExposeSecret, SecretString};
2
3use crate::{
4 azure_chatbot::{
5 InputItem, LLMRequest, LLMRequestParams, MistralParams, NonThinkingParams, OutputItem,
6 Reasoning, ReasoningOutput, Response as AzureResponse, ResponseError, SummaryType,
7 ThinkingParams,
8 },
9 chatbot_error::ChatbotResult,
10 prelude::*,
11};
12use core::default::Default;
13use headless_lms_base::config::ApplicationConfiguration;
14use headless_lms_models::{
15 chatbot_configurations::{ChatbotConfiguration, ReasoningEffortLevel},
16 chatbot_configurations_models::ModelType,
17 chatbot_conversation_message_messages::{ChatbotConversationMessageMessage, MessageRole},
18 chatbot_conversation_message_reasoning::ChatbotConversationMessageReasoning,
19 chatbot_conversation_message_tool_calls::{ChatbotConversationMessageToolCall, ToolKind},
20 chatbot_conversation_message_tool_outputs::ChatbotConversationMessageToolOutput,
21 chatbot_conversation_messages::{ChatbotConversationMessage, Message},
22};
23use reqwest::Response;
24use reqwest::header::HeaderMap;
25use serde::{Deserialize, Serialize};
26use tracing::{debug, error, instrument, trace, warn};
27
28#[derive(Serialize, Deserialize, Debug, Clone)]
30pub struct APIOutputMessage {
31 #[serde(flatten)]
32 pub message_type: OutputItem,
33}
34
35#[derive(Serialize, Deserialize, Debug, Clone)]
37pub struct APIInputMessage {
38 #[serde(flatten)]
39 pub message_type: InputItem,
40}
41
42impl From<APIOutputMessage> for APIInputMessage {
43 fn from(message: APIOutputMessage) -> Self {
44 match message.message_type {
45 OutputItem::Message { role, content, .. } => APIInputMessage {
46 message_type: InputItem::Message { role, content },
47 },
48 OutputItem::FunctionCall {
49 call_id,
50 tool_name,
51 arguments,
52 ..
53 } => APIInputMessage {
54 message_type: InputItem::FunctionCall {
55 call_id,
56 tool_name,
57 arguments,
58 },
59 },
60 OutputItem::FunctionCallOutput {
61 call_id, output, ..
62 } => APIInputMessage {
63 message_type: InputItem::FunctionCallOutput { call_id, output },
64 },
65 OutputItem::AzureAiSearchCall {
66 call_id, arguments, ..
67 } => APIInputMessage {
68 message_type: InputItem::FunctionCall {
69 call_id,
70 tool_name: "azure_ai_search".to_string(),
71 arguments,
72 },
73 },
74 OutputItem::AzureAiSearchCallOutput {
75 call_id, output, ..
76 } => APIInputMessage {
77 message_type: InputItem::FunctionCallOutput { call_id, output },
78 },
79 OutputItem::Reasoning { id, summary, .. } => APIInputMessage {
80 message_type: InputItem::Reasoning { id, summary },
81 },
82 }
83 }
84}
85
86impl TryFrom<ChatbotConversationMessage> for APIInputMessage {
87 type Error = ChatbotError;
88
89 fn try_from(message: ChatbotConversationMessage) -> Result<Self, Self::Error> {
90 let res = match message.message {
91 Message::Text(text_message) => match text_message.message_role {
92 MessageRole::User | MessageRole::Assistant => APIInputMessage {
93 message_type: InputItem::Message {
94 role: text_message.message_role,
95 content: MessageContent::Text(text_message.text),
96 },
97 },
98 _ => {
99 return Err(chatbot_err!(
100 InvalidMessageShape,
101 "A 'role: system' or 'role: developer' type text-variant ChatbotConversationMessage shouldn't be saved into the database."
102 ));
103 }
104 },
105 Message::ToolCall(tool_call) => match tool_call.tool_kind {
106 ToolKind::Function => APIInputMessage {
107 message_type: InputItem::FunctionCall {
108 call_id: tool_call.tool_call_id,
109 tool_name: tool_call.tool_name,
110 arguments: serde_json::to_string(&tool_call.tool_arguments)?,
111 },
112 },
113 ToolKind::AzureAiSearch => APIInputMessage {
114 message_type: InputItem::FunctionCall {
115 call_id: tool_call.tool_call_id,
116 tool_name: "azure_ai_search".to_string(),
117 arguments: serde_json::to_string(&tool_call.tool_arguments)?,
118 },
119 },
120 },
121 Message::ToolOutput(tool_output) => match tool_output.tool_kind {
122 ToolKind::Function => APIInputMessage {
123 message_type: InputItem::FunctionCallOutput {
124 call_id: tool_output.tool_call_id,
125 output: tool_output.output,
126 },
127 },
128 ToolKind::AzureAiSearch => APIInputMessage {
129 message_type: InputItem::FunctionCallOutput {
130 call_id: tool_output.tool_call_id,
131 output: tool_output.output,
132 },
133 },
134 },
135 Message::Reasoning(ChatbotConversationMessageReasoning {
136 reasoning_id,
137 summary,
138 ..
139 }) => {
140 let summ = if let Some(text) = summary {
141 vec![ReasoningOutput {
142 output_type: "summary_text".to_string(),
143 text,
144 }]
145 } else {
146 vec![]
147 };
148 APIInputMessage {
149 message_type: InputItem::Reasoning {
150 id: reasoning_id,
151 summary: summ,
152 },
153 }
154 }
155 };
156 Result::Ok(res)
157 }
158}
159
160#[derive(Serialize, Deserialize, Debug, Clone)]
161#[serde(untagged)]
162pub enum MessageContent {
163 Text(String),
164 OutputText(Vec<MessageContentItem>),
165 Refusal(Vec<RefusalContentItem>),
166}
167
168#[derive(Serialize, Deserialize, Debug, Clone)]
169pub struct MessageContentItem {
170 pub text: String,
171}
172
173#[derive(Serialize, Deserialize, Debug, Clone)]
174pub struct RefusalContentItem {
175 pub refusal: String,
176}
177
178impl MessageContent {
179 pub fn get_content_text(self) -> String {
180 match self {
181 MessageContent::Text(msg_text) => msg_text,
182 MessageContent::OutputText(output) => output
183 .iter()
184 .map(|x| x.text.to_owned())
185 .collect::<Vec<String>>()
186 .join(""),
187 MessageContent::Refusal(refusal) => refusal
188 .iter()
189 .map(|x| x.refusal.to_owned())
190 .collect::<Vec<String>>()
191 .join(""),
192 }
193 }
194}
195
196impl APIOutputMessage {
197 pub fn to_chatbot_conversation_message(
202 &self,
203 conversation_id: Uuid,
204 ) -> ChatbotResult<ChatbotConversationMessage> {
205 let res = match self.message_type.clone() {
206 OutputItem::Message {
207 role,
208 content,
209 response_id,
210 ..
211 } => {
212 let text = content.get_content_text();
213 let used_tokens = estimate_tokens(&text);
214
215 ChatbotConversationMessage {
216 conversation_id,
217 message: Message::Text(ChatbotConversationMessageMessage {
218 text,
219 message_role: role,
220 message_is_complete: true,
221 used_tokens,
222 response_id: if role == MessageRole::User {
223 None
224 } else {
225 Some(response_id)
226 },
227 ..Default::default()
228 }),
229 ..Default::default()
230 }
231 }
232 OutputItem::FunctionCall {
233 call_id,
234 tool_name,
235 arguments,
236 response_id,
237 } => ChatbotConversationMessage {
238 conversation_id,
239 message: Message::ToolCall(ChatbotConversationMessageToolCall {
240 tool_name,
241 tool_arguments: serde_json::to_value(arguments)?,
242 tool_call_id: call_id,
243 tool_kind: ToolKind::Function,
244 response_id,
245 ..Default::default()
246 }),
247 ..Default::default()
248 },
249 OutputItem::FunctionCallOutput {
250 call_id,
251 output,
252 response_id,
253 } => ChatbotConversationMessage {
254 conversation_id,
255 message: Message::ToolOutput(ChatbotConversationMessageToolOutput {
256 output,
257 tool_call_id: call_id,
258 tool_kind: ToolKind::Function,
259 response_id,
260 ..Default::default()
261 }),
262 ..Default::default()
263 },
264 OutputItem::AzureAiSearchCall {
265 call_id,
266 arguments,
267 response_id,
268 } => ChatbotConversationMessage {
269 conversation_id,
270 message: Message::ToolCall(ChatbotConversationMessageToolCall {
271 tool_arguments: serde_json::to_value(arguments)?,
272 tool_call_id: call_id,
273 tool_kind: ToolKind::AzureAiSearch,
274 tool_name: "azure_ai_search".to_string(),
275 response_id,
276 ..Default::default()
277 }),
278 ..Default::default()
279 },
280 OutputItem::AzureAiSearchCallOutput {
281 call_id,
282 output,
283 response_id,
284 } => ChatbotConversationMessage {
285 conversation_id,
286 message: Message::ToolOutput(ChatbotConversationMessageToolOutput {
287 tool_call_id: call_id,
288 tool_kind: ToolKind::AzureAiSearch,
289 output,
290 response_id,
291 ..Default::default()
292 }),
293 ..Default::default()
294 },
295 OutputItem::Reasoning {
296 summary,
297 response_id,
298 id,
299 } => {
300 let text = if !summary.is_empty() {
301 Some(
302 summary
303 .iter()
304 .map(|i| i.text.to_owned())
305 .collect::<Vec<String>>()
306 .join(" "),
307 )
308 } else {
309 None
310 };
311 ChatbotConversationMessage {
312 conversation_id,
313 message: Message::Reasoning(ChatbotConversationMessageReasoning {
314 summary: text,
315 response_id,
316 reasoning_id: id,
317 ..Default::default()
318 }),
319 ..Default::default()
320 }
321 }
322 };
323 Result::Ok(res)
324 }
325}
326
327impl TryFrom<ChatbotConversationMessage> for APIOutputMessage {
328 type Error = ChatbotError;
329
330 fn try_from(message: ChatbotConversationMessage) -> ChatbotResult<Self> {
331 let res = match message.message {
332 Message::Text(text_message) => match text_message.message_role {
333 MessageRole::User | MessageRole::Assistant => APIOutputMessage {
334 message_type: OutputItem::Message {
335 role: text_message.message_role,
336 content: MessageContent::Text(text_message.text),
337 response_id: if text_message.message_role == MessageRole::User {
338 "".to_string()
339 } else {
340 text_message.response_id.ok_or(chatbot_err!(
341 Other,
342 "Can't convert ChatbotConversationMessage into APIOutputMessage: a role='assistant' message should have a response_id, but it's missing"
343 ))?
344 },
345 phase: None,
346 },
347 },
348 _ => {
349 return Err(chatbot_err!(
350 InvalidMessageShape,
351 "A 'role: system' or 'role: developer' type text-variant ChatbotConversationMessage shouldn't be saved into the database."
352 ));
353 }
354 },
355 Message::ToolCall(tool_call) => match tool_call.tool_kind {
356 ToolKind::Function => APIOutputMessage {
357 message_type: OutputItem::FunctionCall {
358 call_id: tool_call.tool_call_id,
359 tool_name: tool_call.tool_name,
360 arguments: serde_json::to_string(&tool_call.tool_arguments)?,
361 response_id: tool_call.response_id,
362 },
363 },
364 ToolKind::AzureAiSearch => APIOutputMessage {
365 message_type: OutputItem::AzureAiSearchCall {
366 call_id: tool_call.tool_call_id,
367 arguments: serde_json::to_string(&tool_call.tool_arguments)?,
368 response_id: tool_call.response_id,
369 },
370 },
371 },
372 Message::ToolOutput(tool_output) => match tool_output.tool_kind {
373 ToolKind::Function => APIOutputMessage {
374 message_type: OutputItem::FunctionCallOutput {
375 call_id: tool_output.tool_call_id,
376 output: tool_output.output,
377 response_id: tool_output.response_id,
378 },
379 },
380 ToolKind::AzureAiSearch => APIOutputMessage {
381 message_type: OutputItem::AzureAiSearchCallOutput {
382 call_id: tool_output.tool_call_id,
383 output: tool_output.output,
384 response_id: tool_output.response_id,
385 },
386 },
387 },
388 Message::Reasoning(reasoning) => {
389 if let Some(text) = reasoning.summary {
390 APIOutputMessage {
391 message_type: OutputItem::Reasoning {
392 summary: vec![ReasoningOutput {
393 output_type: "summary_text".to_string(),
394 text,
395 }],
396 response_id: reasoning.response_id,
397 id: reasoning.reasoning_id,
398 },
399 }
400 } else {
401 APIOutputMessage {
402 message_type: OutputItem::Reasoning {
403 summary: vec![],
404 response_id: reasoning.response_id,
405 id: reasoning.reasoning_id,
406 },
407 }
408 }
409 }
410 };
411 Result::Ok(res)
412 }
413}
414
415impl From<ChatbotConversationMessageToolOutput> for APIOutputMessage {
416 fn from(value: ChatbotConversationMessageToolOutput) -> Self {
417 match value.tool_kind {
418 ToolKind::Function => APIOutputMessage {
419 message_type: OutputItem::FunctionCallOutput {
420 call_id: value.tool_call_id,
421 output: value.output,
422 response_id: value.response_id,
423 },
424 },
425 ToolKind::AzureAiSearch => APIOutputMessage {
426 message_type: OutputItem::AzureAiSearchCallOutput {
427 response_id: value.response_id,
428 call_id: value.tool_call_id,
429 output: value.output,
430 },
431 },
432 }
433 }
434}
435
436impl TryFrom<APIOutputMessage> for ChatbotConversationMessageToolOutput {
437 type Error = ChatbotError;
438 fn try_from(value: APIOutputMessage) -> ChatbotResult<Self> {
439 match value.message_type {
440 OutputItem::FunctionCallOutput {
441 call_id,
442 output,
443 response_id,
444 } => Ok(ChatbotConversationMessageToolOutput {
445 output,
446 tool_call_id: call_id,
447 response_id,
448 ..Default::default()
449 }),
450 OutputItem::AzureAiSearchCallOutput {
451 response_id,
452 call_id,
453 output,
454 } => Ok(ChatbotConversationMessageToolOutput {
455 output,
456 tool_call_id: call_id,
457 response_id,
458 ..Default::default()
459 }),
460 _ => Err(chatbot_err!(
461 Other,
462 "Can't convert APIMessage to ChatbotConversationMessageToolOutput: APIMessage type is not OutputItem::FunctionCallOutput"
463 )),
464 }
465 }
466}
467
468#[derive(Serialize, Deserialize, Debug, Clone)]
470pub struct APIToolCall {
471 pub function: APITool,
472 pub id: String,
473 #[serde(rename = "type")]
474 pub tool_type: ToolKind,
475}
476
477impl From<ChatbotConversationMessageToolCall> for APIToolCall {
478 fn from(value: ChatbotConversationMessageToolCall) -> Self {
479 APIToolCall {
480 function: APITool {
481 arguments: value.tool_arguments.to_string(),
482 name: value.tool_name,
483 },
484 id: value.tool_call_id,
485 tool_type: value.tool_kind,
486 }
487 }
488}
489
490impl TryFrom<APIToolCall> for ChatbotConversationMessageToolCall {
491 type Error = ChatbotError;
492 fn try_from(value: APIToolCall) -> ChatbotResult<Self> {
493 Ok(ChatbotConversationMessageToolCall {
494 tool_name: value.function.name,
495 tool_arguments: serde_json::from_str(&value.function.arguments)?,
496 tool_call_id: value.id,
497 tool_kind: value.tool_type,
498 ..Default::default()
499 })
500 }
501}
502
503#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
504pub struct APITool {
505 pub arguments: String,
506 pub name: String,
507}
508
509#[derive(Serialize, Deserialize, Debug)]
512pub struct AzureCompletionRequest {
513 #[serde(flatten)]
514 pub base: LLMRequest,
515 pub stream: bool,
516}
517
518#[derive(Deserialize, Debug)]
520pub struct LLMResponse {
521 pub id: String,
522 pub output: Vec<APIOutputMessage>,
523}
524
525#[instrument(skip(api_key), fields(api_key_length = api_key.expose_secret().len()))]
527pub fn build_llm_headers(api_key: &SecretString) -> ChatbotResult<HeaderMap> {
528 trace!("Building LLM request headers");
529 let mut headers = HeaderMap::new();
530 headers.insert(
531 "api-key",
532 api_key.expose_secret().parse().map_err(|_e| {
534 error!("Failed to parse API key");
535 chatbot_err!(AzureRequestBuildError, "Invalid API key")
536 })?,
537 );
538 headers.insert(
539 "content-type",
540 "application/json".parse().map_err(|_e| {
541 error!("Failed to parse content-type header");
542 chatbot_err!(AzureRequestBuildError, "Internal error")
543 })?,
544 );
545 trace!("Successfully built headers");
546 Ok(headers)
547}
548
549#[instrument(skip(text), fields(text_length = text.len()))]
551pub fn estimate_tokens(text: &str) -> i32 {
552 trace!("Estimating tokens for text");
553 let text_length = text.chars().fold(0, |acc, c| {
554 let mut len = c.len_utf8() as i32;
555 if len > 1 {
556 len *= 2;
558 }
559 if c.is_ascii_punctuation() {
560 len *= 2;
562 }
563 acc + len
564 });
565 let estimated_tokens = text_length / 4;
567 trace!("Estimated {} tokens for text", estimated_tokens);
568 estimated_tokens
569}
570
571#[instrument(skip(chat_request, endpoint, api_key), fields(
573 num_messages = chat_request.input.len(),
574 temperature,
575 max_tokens,
576 endpoint = %endpoint
577))]
578async fn make_llm_request(
579 chat_request: LLMRequest,
580 endpoint: &url::Url,
581 api_key: &SecretString,
582) -> ChatbotResult<LLMResponse> {
583 debug!(
584 "Preparing LLM request with {} messages",
585 chat_request.input.len()
586 );
587
588 trace!("Base request: {:?}", chat_request);
589
590 let request = AzureCompletionRequest {
591 base: chat_request,
592 stream: false,
593 };
594
595 let headers = build_llm_headers(api_key)?;
596 debug!("Sending request to LLM endpoint: {}", endpoint);
597
598 let response = REQWEST_CLIENT
599 .post(endpoint.clone())
600 .headers(headers)
601 .json(&request)
602 .send()
603 .await?;
604
605 trace!("Received response from LLM");
606 process_llm_response(response).await
607}
608
609#[instrument(skip(response), fields(status = %response.status()))]
611async fn process_llm_response(response: Response) -> ChatbotResult<LLMResponse> {
612 if !response.status().is_success() {
613 let status = response.status();
614 let error_text = response.text().await?;
615 error!(
616 status = %status,
617 error = %error_text,
618 "Error calling LLM API"
619 );
620 let azure_response = serde_json::from_str::<AzureResponse>(&error_text);
622 let error = match azure_response {
623 Ok(response) => {
624 let azure_error: Option<ResponseError> = response.error;
625 let mut error = chatbot_err!(
627 FailedAzureResponse,
628 format!(
629 "Error calling LLM API: Status: {}. Error: {}",
630 status,
631 &azure_error
632 .as_ref()
633 .and_then(|e| e.code.to_owned())
634 .or_else(|| azure_error.as_ref().and_then(|e| e.error_type.to_owned()))
635 .unwrap_or(error_text)
636 )
637 );
638 if let Some(e) = azure_error {
639 error.add_azure_source(e);
640 };
641 error
642 }
643 Err(_) => chatbot_err!(
645 FailedAzureResponse,
646 format!(
647 "Error calling LLM API: Status: {}. Error: {}",
648 status, &error_text
649 )
650 ),
651 };
652
653 return Err(error);
654 }
655
656 trace!("Processing successful LLM response");
657 let completion: LLMResponse = response.json().await?;
659 debug!(
660 "Successfully processed LLM response with {} choices",
661 completion.output.len()
662 );
663 Ok(completion)
664}
665
666#[instrument(skip(chat_request, app_config), fields(
668 num_messages = chat_request.input.len(),
669 temperature,
670 max_tokens
671))]
672pub async fn make_streaming_llm_request(
673 chat_request: LLMRequest,
674 app_config: &ApplicationConfiguration,
675) -> ChatbotResult<Response> {
676 debug!(
677 "Preparing streaming LLM request with {} messages",
678 chat_request.input.len()
679 );
680 let azure_config = app_config.azure_configuration.as_ref().ok_or_else(|| {
681 error!("Azure configuration missing");
682 chatbot_err!(
683 AzureRequestBuildError,
684 "Azure configuration is missing from the application configuration"
685 )
686 })?;
687
688 let chatbot_config = azure_config.chatbot_config.as_ref().ok_or_else(|| {
689 error!("Chatbot configuration missing");
690 chatbot_err!(
691 AzureRequestBuildError,
692 "Chatbot configuration is missing from the Azure configuration"
693 )
694 })?;
695
696 let request = AzureCompletionRequest {
697 base: chat_request,
698 stream: true,
699 };
700
701 let headers = build_llm_headers(&chatbot_config.api_key)?;
702 let api_endpoint = chatbot_config.api_endpoint.to_owned();
703 debug!(
704 "Sending streaming request to LLM endpoint: {}",
705 api_endpoint
706 );
707
708 let response = REQWEST_CLIENT
709 .post(api_endpoint)
710 .headers(headers)
711 .json(&request)
712 .send()
713 .await?;
714
715 if !response.status().is_success() {
716 let status = response.status();
717 let error_text = response.text().await?;
718 error!(
719 status = %status,
720 error = %error_text,
721 "Error calling streaming LLM API"
722 );
723 let azure_response = serde_json::from_str::<AzureResponse>(&error_text);
725 let error = match azure_response {
726 Ok(response) => {
727 let azure_error: Option<ResponseError> = response.error;
728 let mut error = chatbot_err!(
730 FailedAzureResponse,
731 format!(
732 "Error calling LLM API: Status: {}. Error: {}",
733 status,
734 &azure_error
735 .as_ref()
736 .and_then(|e| e.code.to_owned())
737 .or_else(|| azure_error.as_ref().and_then(|e| e.error_type.to_owned()))
738 .unwrap_or(error_text)
739 )
740 );
741 if let Some(e) = azure_error {
742 error.add_azure_source(e);
743 };
744 error
745 }
746 Err(_) => chatbot_err!(
748 FailedAzureResponse,
749 format!(
750 "Error calling LLM API: Status: {}. Error: {}",
751 status, &error_text
752 )
753 ),
754 };
755
756 return Err(error);
757 }
758
759 debug!("Successfully initiated streaming response");
760 Ok(response)
761}
762
763#[instrument(skip(chat_request, app_config), fields(
765 num_messages = chat_request.input.len(),
766 temperature,
767 max_tokens
768))]
769pub async fn make_blocking_llm_request(
770 chat_request: LLMRequest,
771 app_config: &ApplicationConfiguration,
772) -> ChatbotResult<LLMResponse> {
773 debug!(
774 "Preparing blocking LLM request with {} messages",
775 chat_request.input.len()
776 );
777 let azure_config = app_config.azure_configuration.as_ref().ok_or_else(|| {
778 error!("Azure configuration missing");
779 chatbot_err!(
780 AzureRequestBuildError,
781 "Azure configuration is missing from the application configuration"
782 )
783 })?;
784
785 let chatbot_config = azure_config.chatbot_config.as_ref().ok_or_else(|| {
786 error!("Chatbot configuration missing");
787 chatbot_err!(
788 AzureRequestBuildError,
789 "Chatbot configuration is missing from the Azure configuration"
790 )
791 })?;
792
793 let api_endpoint = chatbot_config.api_endpoint.to_owned();
794
795 trace!("Making LLM request to endpoint: {}", api_endpoint);
796 make_llm_request(chat_request, &api_endpoint, &chatbot_config.api_key).await
797}
798
799pub fn parse_text_completion(completion: LLMResponse) -> ChatbotResult<String> {
802 let res =
803 completion
804 .output
805 .into_iter()
806 .map(|x| match x.message_type {
807 OutputItem::Message { content , ..} => Ok(content.get_content_text()),
808 OutputItem::Reasoning { .. } => Ok("".to_string()),
809 _ => Err(chatbot_err!( InvalidMessageShape, "It was assumed this LLM response contains only text, but a tool call or tool response was detected.")),
810 })
811 .collect::<ChatbotResult<Vec<String>>>()?
812 .join("");
813 if res.is_empty() {
814 return Err(chatbot_err!(
815 InvalidMessageShape,
816 "No content returned from LLM"
817 ));
818 };
819 Ok(res)
820}
821
822pub fn get_params_for_model(
823 model_name: &str,
824 model_type: &ModelType,
825 configuration: Option<&ChatbotConfiguration>,
826) -> LLMRequestParams {
827 if model_name == "gpt-5.2-chat" {
828 return LLMRequestParams::GPTThinking(ThinkingParams {
829 reasoning: Some(Reasoning {
830 effort: ReasoningEffortLevel::Medium,
831 summary: Some(SummaryType::Detailed),
832 }),
833 });
834 }
835 match model_type {
836 ModelType::GPTNonThinking => {
837 if let Some(conf) = configuration {
838 LLMRequestParams::GPTNonThinking(NonThinkingParams {
839 temperature: Some(conf.temperature),
840 top_p: Some(conf.top_p),
841 frequency_penalty: Some(conf.frequency_penalty),
842 presence_penalty: Some(conf.presence_penalty),
843 })
844 } else {
845 LLMRequestParams::GPTNonThinking(NonThinkingParams {
846 temperature: None,
847 top_p: None,
848 frequency_penalty: None,
849 presence_penalty: None,
850 })
851 }
852 }
853 ModelType::GPTHardThinking => {
854 let effort = if let Some(conf) = configuration {
856 if conf.reasoning_effort == ReasoningEffortLevel::Minimal {
857 ReasoningEffortLevel::Low
858 } else {
859 conf.reasoning_effort
860 }
861 } else {
862 ReasoningEffortLevel::None
863 };
864 LLMRequestParams::GPTThinking(ThinkingParams {
865 reasoning: Some(Reasoning {
866 effort,
867 summary: Some(SummaryType::Detailed),
868 }),
869 })
870 }
871 ModelType::GPTThinking => {
872 let effort = if let Some(conf) = configuration {
874 if conf.reasoning_effort == ReasoningEffortLevel::None {
875 ReasoningEffortLevel::Minimal
876 } else if conf.reasoning_effort == ReasoningEffortLevel::Xhigh {
877 ReasoningEffortLevel::High
878 } else {
879 conf.reasoning_effort
880 }
881 } else {
882 ReasoningEffortLevel::Minimal
883 };
884 LLMRequestParams::GPTThinking(ThinkingParams {
885 reasoning: Some(Reasoning {
886 effort,
887 summary: Some(SummaryType::Detailed),
888 }),
889 })
890 }
891 ModelType::Mistral => LLMRequestParams::Mistral(MistralParams { test: true }),
892 }
893}
894
895pub fn model_is_thinking(model_type: ModelType) -> bool {
898 matches!(
899 model_type,
900 ModelType::GPTHardThinking | ModelType::GPTThinking
901 )
902}
903
904#[cfg(test)]
905mod tests {
906 use super::*;
907
908 #[test]
909 fn test_estimate_tokens() {
910 assert_eq!(estimate_tokens("Hello, world!"), 3);
912 assert_eq!(estimate_tokens(""), 0);
913 assert_eq!(
915 estimate_tokens("This is a longer sentence with several words."),
916 11
917 );
918 assert_eq!(estimate_tokens("Hyvää päivää!"), 7);
920 assert_eq!(estimate_tokens("トークンは楽しい"), 12);
922 assert_eq!(
924 estimate_tokens("🙂🙃😀😃😄😁😆😅😂🤣😊😇🙂🙃😀😃😄😁😆😅😂🤣😊😇"),
925 48
926 );
927 assert_eq!(estimate_tokens("ฉันใช้โทเค็นทุกวัน"), 27);
929 assert_eq!(estimate_tokens("Жетони роблять мене щасливим"), 25);
931 }
932}