1mod xml_parameter_schema;
4
5use crate::{
6 ids::*, models::TokenUsage, FinishReason, Priority, ResponseCompletionEnvelope, SamplingParams,
7 TokenId,
8};
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use xml_parameter_schema::XmlParameterSchemaProbe;
13
14pub const PROMPT_TOKENS_METADATA_KEY: &str = "ferrum_prompt_tokens";
15pub const DEFAULT_MAX_TOKENS_METADATA_KEY: &str = "ferrum_default_max_tokens";
16
17#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
22pub struct InferenceEvidenceRequest {
23 #[serde(default)]
24 pub capture_prompt_token_ids: bool,
25 #[serde(default)]
29 pub capture_engine_token_timing: bool,
30}
31
32#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
40#[serde(rename_all = "snake_case")]
41pub enum EngineDecodeStage {
42 DecodeScheduling,
44 DecodeExecution,
47 DecodePostprocess,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
55pub struct EngineDecodeStageInterval {
56 pub stage: EngineDecodeStage,
57 pub start_nanos_since_request_start: u64,
58 pub end_nanos_since_request_start: u64,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
62pub struct EngineTokenTimingEvidence {
63 pub clock_source: String,
64 pub wall_anchor_unix_nanos: i64,
65 pub wall_anchor_max_error_nanos: u64,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub decode_ready_nanos_since_request_start: Option<u64>,
68 pub token_commit_nanos_since_request_start: Vec<u64>,
69 #[serde(default, skip_serializing_if = "Vec::is_empty")]
71 pub decode_stage_intervals: Vec<EngineDecodeStageInterval>,
72}
73
74impl EngineTokenTimingEvidence {
75 pub fn validate(&self, output_tokens: usize) -> Result<(), String> {
76 if self.clock_source != "rust_std_instant" {
77 return Err("engine token timing clock_source must be rust_std_instant".to_string());
78 }
79 if self.wall_anchor_unix_nanos <= 0 {
80 return Err("engine token timing wall anchor must be positive".to_string());
81 }
82 if self.token_commit_nanos_since_request_start.len() != output_tokens {
83 return Err(format!(
84 "engine token timing has {} commits for {output_tokens} output tokens",
85 self.token_commit_nanos_since_request_start.len()
86 ));
87 }
88 if self
89 .token_commit_nanos_since_request_start
90 .windows(2)
91 .any(|window| window[1] < window[0])
92 {
93 return Err("engine token commit timestamps must be monotonic".to_string());
94 }
95 if self.decode_stage_intervals.iter().any(|interval| {
96 interval.end_nanos_since_request_start < interval.start_nanos_since_request_start
97 }) {
98 return Err("engine decode stage interval end precedes start".to_string());
99 }
100 if self.decode_stage_intervals.windows(2).any(|window| {
101 window[1].start_nanos_since_request_start < window[0].start_nanos_since_request_start
102 }) {
103 return Err("engine decode stage intervals must be ordered by start".to_string());
104 }
105 Ok(())
106 }
107
108 pub fn ttft_nanos(&self) -> Option<u64> {
109 self.token_commit_nanos_since_request_start.first().copied()
110 }
111
112 pub fn inter_token_nanos(&self) -> Vec<u64> {
113 self.token_commit_nanos_since_request_start
114 .windows(2)
115 .map(|window| window[1].saturating_sub(window[0]))
116 .collect()
117 }
118
119 pub fn decode_wall_nanos(&self) -> Option<u64> {
120 let start = self.decode_ready_nanos_since_request_start?;
121 let end = self
122 .token_commit_nanos_since_request_start
123 .last()
124 .copied()?;
125 (end >= start).then_some(end - start)
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
131pub struct InferenceExecutionEvidence {
132 #[serde(default)]
133 pub prompt_token_ids: Vec<TokenId>,
134 #[serde(default)]
138 pub output_token_ids: Vec<TokenId>,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub engine_token_timing: Option<EngineTokenTimingEvidence>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct InferenceRequest {
146 pub id: RequestId,
148 pub prompt: String,
150 pub model_id: ModelId,
152 pub sampling_params: SamplingParams,
154 pub stream: bool,
156 pub priority: Priority,
158 pub client_id: Option<ClientId>,
160 pub session_id: Option<SessionId>,
162 pub created_at: DateTime<Utc>,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub api_request: Option<ApiRequest>,
169 #[serde(default)]
172 pub evidence_request: InferenceEvidenceRequest,
173 pub metadata: HashMap<String, serde_json::Value>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
178#[serde(tag = "kind", rename_all = "snake_case")]
179pub enum ApiRequest {
180 Chat(ApiChatRequest),
181 Completion(ApiCompletionRequest),
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
185#[serde(tag = "kind", rename_all = "snake_case")]
186pub enum ApiResponse {
187 Chat(ApiChatResponse),
188 Completion(ApiCompletionResponse),
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
192pub struct ApiChatRequest {
193 pub messages: Vec<ApiChatMessage>,
194 #[serde(default, skip_serializing_if = "Vec::is_empty")]
195 pub tools: Vec<ApiTool>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub tool_choice: Option<ApiToolChoice>,
198 #[serde(default)]
201 pub tool_call_protocol: ApiToolCallProtocol,
202 #[serde(default, skip_serializing_if = "Vec::is_empty")]
203 pub legacy_functions: Vec<ApiFunction>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub legacy_function_call: Option<ApiFunctionCallChoice>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub response_format: Option<ApiResponseFormat>,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub stream_options: Option<ApiStreamOptions>,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
213pub struct ApiCompletionRequest {
214 pub prompt: String,
215 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub response_format: Option<ApiResponseFormat>,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
220pub struct ApiChatResponse {
221 pub message: ApiChatMessage,
222 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub finish_reason: Option<String>,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
227pub struct ApiCompletionResponse {
228 pub text: String,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub finish_reason: Option<String>,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
234pub struct ApiChatMessage {
235 pub role: ApiMessageRole,
236 pub content: String,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub name: Option<String>,
239 #[serde(default, skip_serializing_if = "Vec::is_empty")]
240 pub tool_calls: Vec<ApiToolCall>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub tool_call_id: Option<String>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub function_call: Option<ApiFunctionCall>,
245}
246
247#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
248#[serde(rename_all = "lowercase")]
249pub enum ApiMessageRole {
250 System,
251 User,
252 Assistant,
253 Function,
254 Tool,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
258pub struct ApiTool {
259 #[serde(rename = "type")]
260 pub tool_type: String,
261 pub function: ApiFunction,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
265pub struct ApiFunction {
266 pub name: String,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub description: Option<String>,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
270 pub parameters: Option<serde_json::Value>,
271 #[serde(default, skip_serializing_if = "Option::is_none")]
272 pub strict: Option<bool>,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
276#[serde(untagged)]
277pub enum ApiToolChoice {
278 Mode(String),
279 Function {
280 #[serde(rename = "type")]
281 tool_type: String,
282 function: ApiToolChoiceFunction,
283 },
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
287pub struct ApiToolChoiceFunction {
288 pub name: String,
289}
290
291#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
292#[serde(rename_all = "snake_case")]
293pub enum ApiToolCallProtocol {
294 #[default]
295 Json,
296 FunctionParameterXml,
297}
298
299impl ApiToolCallProtocol {
300 pub const fn generated_control_token_texts(self) -> &'static [&'static str] {
306 match self {
307 Self::Json => &[],
308 Self::FunctionParameterXml => &["<tool_call>", "</tool_call>"],
309 }
310 }
311
312 pub fn generated_response_envelope(self) -> Option<ResponseCompletionEnvelope> {
314 match self {
315 Self::Json => None,
316 Self::FunctionParameterXml => Some(ResponseCompletionEnvelope {
317 open_token_text: "<tool_call>".to_string(),
318 close_token_text: "</tool_call>".to_string(),
319 max_envelopes: MAX_PARALLEL_TOOL_CALLS_PER_RESPONSE,
320 }),
321 }
322 }
323}
324
325impl ApiChatRequest {
326 pub fn automatic_tools_with_hard_response_format(&self) -> bool {
330 !self.tools.is_empty()
331 && self.tool_choice.as_ref().is_none_or(|choice| {
332 matches!(choice, ApiToolChoice::Mode(mode) if mode.eq_ignore_ascii_case("auto"))
333 })
334 && self.response_format.as_ref().is_some_and(|format| {
335 format.format_type == "json_object"
336 || (format.format_type == "json_schema"
337 && format
338 .json_schema
339 .as_ref()
340 .is_some_and(|schema| schema.strict == Some(true)))
341 })
342 }
343
344 pub fn generated_control_token_texts(&self) -> &'static [&'static str] {
347 if self.tools.is_empty() || api_tool_choice_is_none(self) {
348 return &[];
349 }
350 if self.automatic_tools_with_hard_response_format() {
351 return &["<tool_call>", "</tool_call>"];
352 }
353 self.tool_call_protocol.generated_control_token_texts()
354 }
355
356 pub fn generated_response_envelope(&self) -> Option<ResponseCompletionEnvelope> {
358 if self.tools.is_empty() || api_tool_choice_is_none(self) {
359 return None;
360 }
361 if self.automatic_tools_with_hard_response_format() {
362 return Some(ResponseCompletionEnvelope {
363 open_token_text: "<tool_call>".to_string(),
364 close_token_text: "</tool_call>".to_string(),
365 max_envelopes: MAX_PARALLEL_TOOL_CALLS_PER_RESPONSE,
366 });
367 }
368 self.tool_call_protocol.generated_response_envelope()
369 }
370}
371
372impl ApiRequest {
373 pub fn generated_control_token_texts(&self) -> &'static [&'static str] {
374 match self {
375 Self::Chat(request) => request.generated_control_token_texts(),
376 Self::Completion(_) => &[],
377 }
378 }
379
380 pub fn generated_response_envelope(&self) -> Option<ResponseCompletionEnvelope> {
381 match self {
382 Self::Chat(request) => request.generated_response_envelope(),
383 Self::Completion(_) => None,
384 }
385 }
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
389#[serde(untagged)]
390pub enum ApiFunctionCallChoice {
391 Mode(String),
392 Function { name: String },
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
396pub struct ApiToolCall {
397 pub id: String,
398 #[serde(rename = "type")]
399 pub tool_type: String,
400 pub function: ApiFunctionCall,
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
404pub struct ApiFunctionCall {
405 pub name: String,
406 pub arguments: String,
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
410pub struct ApiResponseFormat {
411 #[serde(rename = "type")]
412 pub format_type: String,
413 #[serde(default, skip_serializing_if = "Option::is_none")]
414 pub json_schema: Option<ApiJsonSchema>,
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
418pub struct ApiJsonSchema {
419 #[serde(default, skip_serializing_if = "Option::is_none")]
420 pub name: Option<String>,
421 pub schema: serde_json::Value,
422 #[serde(default, skip_serializing_if = "Option::is_none")]
423 pub strict: Option<bool>,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
427pub struct ApiStreamOptions {
428 #[serde(default, skip_serializing_if = "Option::is_none")]
429 pub include_usage: Option<bool>,
430}
431
432const MAX_PARALLEL_TOOL_CALLS_PER_RESPONSE: usize = 32;
433
434pub fn api_response_from_generated_text(
435 request: &InferenceRequest,
436 text: &str,
437 finish_reason: FinishReason,
438) -> Option<ApiResponse> {
439 let ApiRequest::Chat(chat_request) = request.api_request.as_ref()? else {
440 return None;
441 };
442 chat_api_response_from_generated_text(chat_request, text, finish_reason).map(ApiResponse::Chat)
443}
444
445#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub enum StructuredOutputBranch {
450 Final,
451 ToolCall,
452}
453
454pub fn api_response_from_classified_generated_text(
466 request: &InferenceRequest,
467 text: &str,
468 finish_reason: FinishReason,
469 branch: StructuredOutputBranch,
470) -> crate::Result<Option<ApiResponse>> {
471 let Some(ApiRequest::Chat(chat_request)) = request.api_request.as_ref() else {
472 return Ok(None);
473 };
474 if !matches!(finish_reason, FinishReason::Stop | FinishReason::EOS) {
475 return Ok(None);
476 }
477 if !chat_request.automatic_tools_with_hard_response_format() {
478 return Err(crate::FerrumError::invalid_request(
479 "classified structured output requires automatic tools with a hard response format",
480 ));
481 }
482
483 let (content, tool_calls, wire_finish_reason) = match branch {
484 StructuredOutputBranch::Final => (text.to_string(), Vec::new(), "stop"),
485 StructuredOutputBranch::ToolCall => {
486 let calls = parse_explicit_tool_call_envelopes(text, chat_request)
487 .or_else(|| parse_classified_named_json_call(text, chat_request))
488 .filter(|calls| {
489 calls.iter().all(|call| {
490 serde_json::from_str::<serde_json::Value>(&call.function.arguments)
491 .is_ok_and(|arguments| arguments.is_object())
492 })
493 })
494 .ok_or_else(|| {
495 crate::FerrumError::invalid_format(
496 "classified tool output is not a complete declared function call",
497 )
498 })?;
499 (String::new(), calls, "tool_calls")
500 }
501 };
502 Ok(Some(ApiResponse::Chat(ApiChatResponse {
503 message: ApiChatMessage {
504 role: ApiMessageRole::Assistant,
505 content,
506 name: None,
507 tool_calls,
508 tool_call_id: None,
509 function_call: None,
510 },
511 finish_reason: Some(wire_finish_reason.to_string()),
512 })))
513}
514
515fn parse_classified_named_json_call(
516 text: &str,
517 chat_request: &ApiChatRequest,
518) -> Option<Vec<ApiToolCall>> {
519 if chat_request.tool_call_protocol != ApiToolCallProtocol::Json {
520 return None;
521 }
522 let value: serde_json::Value = serde_json::from_str(text).ok()?;
525 let name = value.get("name")?.as_str()?;
526 if !api_tool_name_allowed(chat_request, name) {
527 return None;
528 }
529 let arguments = match (value.get("arguments"), value.get("parameters")) {
530 (Some(arguments), None) | (None, Some(arguments)) => arguments,
531 _ => return None,
532 };
533 if !arguments.is_object() {
534 return None;
535 }
536 let call = ApiToolCall {
537 id: "call_0".to_string(),
538 tool_type: "function".to_string(),
539 function: ApiFunctionCall {
540 name: name.to_string(),
541 arguments: serde_json::to_string(arguments).ok()?,
542 },
543 };
544 validate_parsed_tool_calls(vec![call])
545}
546
547pub fn chat_api_may_emit_tool_or_function_call(chat_request: &ApiChatRequest) -> bool {
548 (!chat_request.tools.is_empty() && !api_tool_choice_is_none(chat_request))
549 || (!chat_request.legacy_functions.is_empty()
550 && !api_function_call_choice_is_none(chat_request))
551}
552
553pub fn chat_api_response_from_generated_text(
554 chat_request: &ApiChatRequest,
555 text: &str,
556 finish_reason: FinishReason,
557) -> Option<ApiChatResponse> {
558 if !matches!(finish_reason, FinishReason::Stop | FinishReason::EOS) {
559 return None;
560 }
561
562 if !chat_request.tools.is_empty() && !api_tool_choice_is_none(chat_request) {
563 if let Some(tool_calls) = parse_tool_calls_from_generated_text(text, chat_request) {
564 return Some(ApiChatResponse {
565 message: ApiChatMessage {
566 role: ApiMessageRole::Assistant,
567 content: String::new(),
568 name: None,
569 tool_calls,
570 tool_call_id: None,
571 function_call: None,
572 },
573 finish_reason: Some("tool_calls".to_string()),
574 });
575 }
576 }
577
578 if !chat_request.legacy_functions.is_empty() && !api_function_call_choice_is_none(chat_request)
579 {
580 if let Some(function_call) =
581 parse_legacy_function_call_from_generated_text(text, chat_request)
582 {
583 return Some(ApiChatResponse {
584 message: ApiChatMessage {
585 role: ApiMessageRole::Assistant,
586 content: String::new(),
587 name: None,
588 tool_calls: Vec::new(),
589 tool_call_id: None,
590 function_call: Some(function_call),
591 },
592 finish_reason: Some("function_call".to_string()),
593 });
594 }
595 }
596
597 None
598}
599
600fn api_tool_choice_is_none(chat_request: &ApiChatRequest) -> bool {
601 matches!(
602 chat_request.tool_choice.as_ref(),
603 Some(ApiToolChoice::Mode(mode)) if mode.eq_ignore_ascii_case("none")
604 )
605}
606
607fn api_function_call_choice_is_none(chat_request: &ApiChatRequest) -> bool {
608 matches!(
609 chat_request.legacy_function_call.as_ref(),
610 Some(ApiFunctionCallChoice::Mode(mode)) if mode.eq_ignore_ascii_case("none")
611 )
612}
613
614fn parse_tool_calls_from_generated_text(
615 text: &str,
616 chat_request: &ApiChatRequest,
617) -> Option<Vec<ApiToolCall>> {
618 if chat_request.automatic_tools_with_hard_response_format() {
619 return parse_explicit_tool_call_envelopes(text, chat_request);
620 }
621 if chat_request.tool_call_protocol == ApiToolCallProtocol::FunctionParameterXml {
622 if let Some(calls) = parse_function_parameter_xml_tool_calls(text, chat_request, false) {
623 return validate_parsed_tool_calls(calls);
624 }
625 }
626
627 let value = parse_json_value_from_generated_text(text)?;
628 parse_json_tool_call_value(&value, chat_request, 0, true)
629}
630
631fn parse_json_tool_call_value(
632 value: &serde_json::Value,
633 chat_request: &ApiChatRequest,
634 index_offset: usize,
635 allow_unwrapped_arguments: bool,
636) -> Option<Vec<ApiToolCall>> {
637 if let Some(calls) = value.get("tool_calls").and_then(|value| value.as_array()) {
638 if calls.len() > MAX_PARALLEL_TOOL_CALLS_PER_RESPONSE {
639 return None;
640 }
641 let parsed = calls
642 .iter()
643 .enumerate()
644 .map(|(index, value)| parse_tool_call_value(value, index_offset + index, chat_request))
645 .collect::<Option<Vec<_>>>()?;
646 return validate_parsed_tool_calls(parsed);
647 }
648 if let Some(tool_call) = value.get("tool_call") {
649 return parse_tool_call_value(tool_call, index_offset, chat_request)
650 .and_then(|call| validate_parsed_tool_calls(vec![call]));
651 }
652 if let Some(tool_call) = parse_wrapped_tool_call_value(value, index_offset, chat_request) {
653 return validate_parsed_tool_calls(vec![tool_call]);
654 }
655 parse_tool_call_value(value, index_offset, chat_request)
656 .or_else(|| {
657 allow_unwrapped_arguments
658 .then(|| parse_forced_tool_arguments_value(value, index_offset, chat_request))
659 .flatten()
660 })
661 .and_then(|call| validate_parsed_tool_calls(vec![call]))
662}
663
664fn parse_explicit_tool_call_envelopes(
667 text: &str,
668 chat_request: &ApiChatRequest,
669) -> Option<Vec<ApiToolCall>> {
670 const OPEN: &str = "<tool_call>";
671 const CLOSE: &str = "</tool_call>";
672 let mut remaining = text.trim();
673 let mut calls = Vec::new();
674 while !remaining.is_empty() {
675 if calls.len() >= MAX_PARALLEL_TOOL_CALLS_PER_RESPONSE {
676 return None;
677 }
678 let payload = remaining.strip_prefix(OPEN)?.trim_start();
679 let parsed = match chat_request.tool_call_protocol {
680 ApiToolCallProtocol::Json => {
681 let mut values =
684 serde_json::Deserializer::from_str(payload).into_iter::<serde_json::Value>();
685 let value = values.next()?.ok()?;
686 remaining = payload[values.byte_offset()..]
687 .trim_start()
688 .strip_prefix(CLOSE)?
689 .trim_start();
690 parse_json_tool_call_value(&value, chat_request, calls.len(), false)?
691 }
692 ApiToolCallProtocol::FunctionParameterXml => {
693 let end = remaining.find(CLOSE)? + CLOSE.len();
694 let envelope = &remaining[..end];
695 remaining = remaining[end..].trim_start();
696 let mut parsed =
697 parse_function_parameter_xml_tool_calls(envelope, chat_request, true)?;
698 for (index, call) in parsed.iter_mut().enumerate() {
699 call.id = format!("call_{}", calls.len() + index);
700 }
701 parsed
702 }
703 };
704 if calls.len() + parsed.len() > MAX_PARALLEL_TOOL_CALLS_PER_RESPONSE {
705 return None;
706 }
707 for call in parsed {
708 if calls
709 .iter()
710 .any(|previous: &ApiToolCall| previous.id == call.id)
711 {
712 return None;
713 }
714 calls.push(call);
715 }
716 }
717 validate_parsed_tool_calls(calls)
718}
719
720fn validate_parsed_tool_calls(calls: Vec<ApiToolCall>) -> Option<Vec<ApiToolCall>> {
721 if calls.is_empty() || calls.len() > MAX_PARALLEL_TOOL_CALLS_PER_RESPONSE {
722 return None;
723 }
724 for (index, call) in calls.iter().enumerate() {
725 if calls[..index].iter().any(|previous| {
726 previous.function.name == call.function.name
727 && previous.function.arguments == call.function.arguments
728 }) {
729 return None;
730 }
731 }
732 Some(calls)
733}
734
735fn parse_function_parameter_xml_tool_calls(
736 text: &str,
737 chat_request: &ApiChatRequest,
738 require_complete_parameters: bool,
739) -> Option<Vec<ApiToolCall>> {
740 const TOOL_START: &str = "<tool_call>";
741 const TOOL_END: &str = "</tool_call>";
742 const FUNCTION_START: &str = "<function=";
743 const FUNCTION_END: &str = "</function>";
744
745 let mut remaining = text;
746 let mut calls = Vec::new();
747 while let Some(tool_start) = remaining.find(TOOL_START) {
748 if calls.len() == MAX_PARALLEL_TOOL_CALLS_PER_RESPONSE {
749 return None;
750 }
751 remaining = &remaining[tool_start + TOOL_START.len()..];
752 let tool_end = remaining.find(TOOL_END)?;
753 let block = &remaining[..tool_end];
754 remaining = &remaining[tool_end + TOOL_END.len()..];
755
756 let Some(function_start) = block.find(FUNCTION_START) else {
757 return None;
758 };
759 if !block[..function_start].trim().is_empty() {
760 return None;
761 }
762 let function = &block[function_start + FUNCTION_START.len()..];
763 let Some(name_end) = function.find('>') else {
764 return None;
765 };
766 let name = function[..name_end].trim();
767 if !api_tool_name_allowed(chat_request, name) {
768 return None;
769 }
770 let arguments_end = function[name_end + 1..]
771 .find(FUNCTION_END)
772 .map(|offset| name_end + 1 + offset)?;
773 if !function[arguments_end + FUNCTION_END.len()..]
774 .trim()
775 .is_empty()
776 {
777 return None;
778 }
779 let parameter_schema = chat_request
780 .tools
781 .iter()
782 .find(|tool| tool.tool_type == "function" && tool.function.name == name)
783 .and_then(|tool| tool.function.parameters.as_ref());
784 let arguments = parse_function_parameter_xml_arguments(
785 &function[name_end + 1..arguments_end],
786 parameter_schema,
787 require_complete_parameters,
788 )?;
789 let arguments = serde_json::to_string(&arguments).ok()?;
790 calls.push(ApiToolCall {
791 id: format!("call_{}", calls.len()),
792 tool_type: "function".to_string(),
793 function: ApiFunctionCall {
794 name: name.to_string(),
795 arguments,
796 },
797 });
798 }
799
800 (!calls.is_empty()).then_some(calls)
801}
802
803fn parse_function_parameter_xml_arguments(
804 text: &str,
805 parameter_schema: Option<&serde_json::Value>,
806 require_complete_parameters: bool,
807) -> Option<serde_json::Map<String, serde_json::Value>> {
808 const PARAMETER_START: &str = "<parameter=";
809 const PARAMETER_END: &str = "</parameter>";
810
811 let mut arguments = serde_json::Map::new();
812 let mut schema_probe = parameter_schema.map(XmlParameterSchemaProbe::new);
813 let mut remaining = text;
814 while let Some(parameter_start) = remaining.find(PARAMETER_START) {
815 if require_complete_parameters && !remaining[..parameter_start].trim().is_empty() {
816 return None;
817 }
818 remaining = &remaining[parameter_start + PARAMETER_START.len()..];
819 let Some(name_end) = remaining.find('>') else {
820 return None;
821 };
822 let name = remaining[..name_end].trim();
823 remaining = &remaining[name_end + 1..];
824 if name.is_empty() {
825 return None;
826 }
827 let value_end = remaining.find(PARAMETER_END)?;
828 if arguments.contains_key(name) {
829 return None;
830 }
831 let value = strip_xml_parameter_wrapper_newlines(&remaining[..value_end]);
832 let value = schema_probe.as_mut().map_or_else(
833 || serde_json::Value::String(value.to_string()),
834 |probe| probe.decode(name, value),
835 );
836 arguments.insert(name.to_string(), value);
837 remaining = &remaining[value_end + PARAMETER_END.len()..];
838 }
839 if require_complete_parameters && !remaining.trim().is_empty() {
840 return None;
841 }
842 Some(arguments)
843}
844
845fn strip_xml_parameter_wrapper_newlines(value: &str) -> &str {
850 if let Some(value) = value.strip_prefix("\r\n") {
851 return value.strip_suffix("\r\n").unwrap_or(value);
852 }
853 if let Some(value) = value.strip_prefix('\n') {
854 if value.ends_with("\r\n") {
855 return value;
856 }
857 return value.strip_suffix('\n').unwrap_or(value);
858 }
859 value
860}
861
862fn parse_wrapped_tool_call_value(
863 value: &serde_json::Value,
864 index: usize,
865 chat_request: &ApiChatRequest,
866) -> Option<ApiToolCall> {
867 for key in ["auto", "tool", "tool_call", "auto_tool_response"] {
868 if let Some(wrapped) = value.get(key) {
869 if let Some(call) = parse_tool_call_value(wrapped, index, chat_request) {
870 return Some(call);
871 }
872 }
873 }
874 None
875}
876
877fn parse_tool_call_value(
878 value: &serde_json::Value,
879 index: usize,
880 chat_request: &ApiChatRequest,
881) -> Option<ApiToolCall> {
882 let tool_type = value
883 .get("type")
884 .and_then(|value| value.as_str())
885 .unwrap_or("function");
886 if tool_type != "function" {
887 return None;
888 }
889 let function = value.get("function").unwrap_or(value);
890 let name = function
891 .as_str()
892 .or_else(|| function.get("name").and_then(|value| value.as_str()))
893 .or_else(|| function.get("tool").and_then(|value| value.as_str()))
894 .or_else(|| value.get("name").and_then(|value| value.as_str()))?;
895 if !api_tool_name_allowed(chat_request, name) {
896 return None;
897 }
898 let arguments = api_arguments_to_string(
899 function
900 .get("arguments")
901 .or_else(|| function.get("parameters"))
902 .or_else(|| value.get("arguments"))
903 .or_else(|| value.get("parameters")),
904 );
905 let id = value
906 .get("id")
907 .and_then(|value| value.as_str())
908 .map(str::to_string)
909 .unwrap_or_else(|| format!("call_{index}"));
910
911 Some(ApiToolCall {
912 id,
913 tool_type: "function".to_string(),
914 function: ApiFunctionCall {
915 name: name.to_string(),
916 arguments,
917 },
918 })
919}
920
921fn parse_forced_tool_arguments_value(
922 value: &serde_json::Value,
923 index: usize,
924 chat_request: &ApiChatRequest,
925) -> Option<ApiToolCall> {
926 let tool = unwrapped_tool_arguments_target(chat_request, value)?;
927 if value.get("tool_calls").is_some()
928 || value.get("tool_call").is_some()
929 || value.get("function").is_some()
930 || value.get("name").is_some()
931 {
932 return None;
933 }
934
935 Some(ApiToolCall {
936 id: format!("call_{index}"),
937 tool_type: "function".to_string(),
938 function: ApiFunctionCall {
939 name: tool.function.name.clone(),
940 arguments: serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()),
941 },
942 })
943}
944
945fn unwrapped_tool_arguments_target<'a>(
946 chat_request: &'a ApiChatRequest,
947 value: &serde_json::Value,
948) -> Option<&'a ApiTool> {
949 if let Some(name) = forced_tool_choice_name(chat_request) {
950 return chat_request
951 .tools
952 .iter()
953 .find(|tool| tool.tool_type == "function" && tool.function.name == name);
954 }
955
956 if matches!(
957 chat_request.tool_choice.as_ref(),
958 Some(ApiToolChoice::Mode(mode)) if !mode.eq_ignore_ascii_case("auto")
959 ) {
960 return None;
961 }
962
963 let tool = single_function_tool(chat_request)?;
964 if !value_looks_like_tool_arguments(value, tool) {
965 return None;
966 }
967 Some(tool)
968}
969
970fn value_looks_like_tool_arguments(value: &serde_json::Value, tool: &ApiTool) -> bool {
971 let Some(arguments) = value.as_object() else {
972 return false;
973 };
974 if arguments.is_empty() {
975 return false;
976 }
977 let Some(properties) = tool
978 .function
979 .parameters
980 .as_ref()
981 .and_then(|parameters| parameters.get("properties"))
982 .and_then(|properties| properties.as_object())
983 else {
984 return false;
985 };
986 arguments.keys().all(|key| properties.contains_key(key))
987}
988
989fn forced_tool_choice_name(chat_request: &ApiChatRequest) -> Option<&str> {
990 match chat_request.tool_choice.as_ref() {
991 Some(ApiToolChoice::Function {
992 tool_type,
993 function,
994 }) if tool_type == "function" && api_tool_name_allowed(chat_request, &function.name) => {
995 Some(function.name.as_str())
996 }
997 Some(ApiToolChoice::Mode(mode)) if mode.eq_ignore_ascii_case("required") => {
998 single_function_tool(chat_request).map(|tool| tool.function.name.as_str())
999 }
1000 _ => None,
1001 }
1002}
1003
1004fn single_function_tool(chat_request: &ApiChatRequest) -> Option<&ApiTool> {
1005 let mut tools = chat_request
1006 .tools
1007 .iter()
1008 .filter(|tool| tool.tool_type == "function");
1009 let tool = tools.next()?;
1010 tools.next().is_none().then_some(tool)
1011}
1012
1013fn parse_legacy_function_call_from_generated_text(
1014 text: &str,
1015 chat_request: &ApiChatRequest,
1016) -> Option<ApiFunctionCall> {
1017 let value = parse_json_value_from_generated_text(text)?;
1018 let function = value.get("function_call").unwrap_or(&value);
1019 let name = function.get("name").and_then(|value| value.as_str())?;
1020 if !api_function_name_allowed(chat_request, name) {
1021 return None;
1022 }
1023 Some(ApiFunctionCall {
1024 name: name.to_string(),
1025 arguments: api_arguments_to_string(function.get("arguments")),
1026 })
1027}
1028
1029fn api_tool_name_allowed(chat_request: &ApiChatRequest, name: &str) -> bool {
1030 match chat_request.tool_choice.as_ref() {
1031 Some(ApiToolChoice::Mode(mode)) if mode.eq_ignore_ascii_case("none") => false,
1032 Some(ApiToolChoice::Function {
1033 tool_type,
1034 function,
1035 }) => {
1036 tool_type == "function"
1037 && function.name == name
1038 && chat_request
1039 .tools
1040 .iter()
1041 .any(|tool| tool.function.name == name)
1042 }
1043 _ => chat_request
1044 .tools
1045 .iter()
1046 .any(|tool| tool.function.name == name),
1047 }
1048}
1049
1050fn api_function_name_allowed(chat_request: &ApiChatRequest, name: &str) -> bool {
1051 match chat_request.legacy_function_call.as_ref() {
1052 Some(ApiFunctionCallChoice::Mode(mode)) if mode.eq_ignore_ascii_case("none") => false,
1053 Some(ApiFunctionCallChoice::Function { name: selected }) => {
1054 selected == name
1055 && chat_request
1056 .legacy_functions
1057 .iter()
1058 .any(|function| function.name == name)
1059 }
1060 _ => chat_request
1061 .legacy_functions
1062 .iter()
1063 .any(|function| function.name == name),
1064 }
1065}
1066
1067fn parse_json_value_from_generated_text(text: &str) -> Option<serde_json::Value> {
1068 let trimmed = strip_single_json_fence(text.trim());
1069 serde_json::from_str(trimmed).ok().or_else(|| {
1070 let start = trimmed.find('{')?;
1071 let end = trimmed.rfind('}')?;
1072 (start <= end)
1073 .then(|| serde_json::from_str(&trimmed[start..=end]).ok())
1074 .flatten()
1075 })
1076}
1077
1078fn strip_single_json_fence(text: &str) -> &str {
1079 let Some(rest) = text.strip_prefix("```") else {
1080 return text;
1081 };
1082 let rest = rest.strip_prefix("json").unwrap_or(rest).trim_start();
1083 rest.strip_suffix("```").map(str::trim).unwrap_or(text)
1084}
1085
1086fn api_arguments_to_string(arguments: Option<&serde_json::Value>) -> String {
1087 match arguments {
1088 Some(serde_json::Value::String(raw)) => raw.clone(),
1089 Some(value) => serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()),
1090 None => "{}".to_string(),
1091 }
1092}
1093
1094impl InferenceRequest {
1095 pub fn new(prompt: impl Into<String>, model_id: impl Into<ModelId>) -> Self {
1097 Self {
1098 id: RequestId::new(),
1099 prompt: prompt.into(),
1100 model_id: model_id.into(),
1101 sampling_params: SamplingParams::default(),
1102 stream: false,
1103 priority: Priority::default(),
1104 client_id: None,
1105 session_id: None,
1106 created_at: Utc::now(),
1107 api_request: None,
1108 evidence_request: InferenceEvidenceRequest::default(),
1109 metadata: HashMap::new(),
1110 }
1111 }
1112
1113 pub fn with_sampling_params(mut self, params: SamplingParams) -> Self {
1115 self.sampling_params = params;
1116 self
1117 }
1118
1119 pub fn with_stream(mut self, stream: bool) -> Self {
1121 self.stream = stream;
1122 self
1123 }
1124
1125 pub fn with_priority(mut self, priority: Priority) -> Self {
1127 self.priority = priority;
1128 self
1129 }
1130
1131 pub fn with_client_id(mut self, client_id: impl Into<ClientId>) -> Self {
1133 self.client_id = Some(client_id.into());
1134 self
1135 }
1136
1137 pub fn with_session_id(mut self, session_id: SessionId) -> Self {
1139 self.session_id = Some(session_id);
1140 self
1141 }
1142
1143 pub fn with_api_request(mut self, api_request: ApiRequest) -> Self {
1145 self.api_request = Some(api_request);
1146 self
1147 }
1148
1149 pub fn with_prompt_token_evidence(mut self) -> Self {
1151 self.evidence_request.capture_prompt_token_ids = true;
1152 self
1153 }
1154
1155 pub fn with_engine_token_timing_evidence(mut self) -> Self {
1157 self.evidence_request.capture_engine_token_timing = true;
1158 self
1159 }
1160
1161 pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
1163 self.metadata.insert(key.into(), value);
1164 self
1165 }
1166}
1167
1168#[derive(Debug, Clone, Serialize, Deserialize)]
1170pub struct InferenceResponse {
1171 pub request_id: RequestId,
1173 pub text: String,
1175 pub tokens: Vec<TokenId>,
1177 pub finish_reason: FinishReason,
1179 pub usage: TokenUsage,
1181 pub latency_ms: u64,
1183 pub created_at: DateTime<Utc>,
1185 pub metadata: HashMap<String, serde_json::Value>,
1187 #[serde(default, skip_serializing_if = "Option::is_none")]
1191 pub api_response: Option<ApiResponse>,
1192 #[serde(default, skip_serializing_if = "Option::is_none")]
1194 pub execution_evidence: Option<InferenceExecutionEvidence>,
1195}
1196
1197#[derive(Debug, Clone, Serialize, Deserialize)]
1199pub struct StreamChunk {
1200 pub request_id: RequestId,
1202 pub text: String,
1204 pub token: Option<TokenId>,
1206 pub finish_reason: Option<FinishReason>,
1208 pub usage: Option<TokenUsage>,
1210 pub created_at: DateTime<Utc>,
1212 pub metadata: HashMap<String, serde_json::Value>,
1214 #[serde(default, skip_serializing_if = "Option::is_none")]
1218 pub api_response: Option<ApiResponse>,
1219 #[serde(default, skip_serializing_if = "Option::is_none")]
1221 pub execution_evidence: Option<InferenceExecutionEvidence>,
1222}
1223
1224#[derive(Debug, Clone, Serialize, Deserialize)]
1226pub struct BatchRequest {
1227 pub batch_id: BatchId,
1229 pub requests: Vec<InferenceRequest>,
1231 pub max_sequence_length: usize,
1233 pub created_at: DateTime<Utc>,
1235}
1236
1237impl BatchRequest {
1238 pub fn new(requests: Vec<InferenceRequest>) -> Self {
1240 let max_sequence_length = requests
1241 .iter()
1242 .map(|r| r.sampling_params.max_tokens)
1243 .max()
1244 .unwrap_or(512);
1245
1246 Self {
1247 batch_id: BatchId::new(),
1248 requests,
1249 max_sequence_length,
1250 created_at: Utc::now(),
1251 }
1252 }
1253
1254 pub fn size(&self) -> usize {
1256 self.requests.len()
1257 }
1258
1259 pub fn is_empty(&self) -> bool {
1261 self.requests.is_empty()
1262 }
1263}
1264
1265#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1267pub enum RequestState {
1268 Waiting,
1270 Running,
1272 Preempted,
1274 Completed,
1276 Failed,
1278 Cancelled,
1280}
1281
1282#[derive(Debug, Clone)]
1284pub struct ScheduledRequest {
1285 pub request: InferenceRequest,
1287 pub state: RequestState,
1289 pub allocated_blocks: Vec<crate::BlockId>,
1291 pub tokens_processed: usize,
1293 pub estimated_completion: Option<DateTime<Utc>>,
1295}
1296
1297impl ScheduledRequest {
1298 pub fn new(request: InferenceRequest) -> Self {
1300 Self {
1301 request,
1302 state: RequestState::Waiting,
1303 allocated_blocks: Vec::new(),
1304 tokens_processed: 0,
1305 estimated_completion: None,
1306 }
1307 }
1308
1309 pub fn set_state(&mut self, state: RequestState) {
1311 self.state = state;
1312 }
1313
1314 pub fn add_blocks(&mut self, blocks: Vec<crate::BlockId>) {
1316 self.allocated_blocks.extend(blocks);
1317 }
1318
1319 pub fn update_progress(&mut self, tokens_processed: usize) {
1321 self.tokens_processed = tokens_processed;
1322 }
1323}