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 generated_control_token_texts(&self) -> &'static [&'static str] {
329 if self.tools.is_empty() || api_tool_choice_is_none(self) {
330 return &[];
331 }
332 self.tool_call_protocol.generated_control_token_texts()
333 }
334
335 pub fn generated_response_envelope(&self) -> Option<ResponseCompletionEnvelope> {
337 if self.tools.is_empty() || api_tool_choice_is_none(self) {
338 return None;
339 }
340 self.tool_call_protocol.generated_response_envelope()
341 }
342}
343
344impl ApiRequest {
345 pub fn generated_control_token_texts(&self) -> &'static [&'static str] {
346 match self {
347 Self::Chat(request) => request.generated_control_token_texts(),
348 Self::Completion(_) => &[],
349 }
350 }
351
352 pub fn generated_response_envelope(&self) -> Option<ResponseCompletionEnvelope> {
353 match self {
354 Self::Chat(request) => request.generated_response_envelope(),
355 Self::Completion(_) => None,
356 }
357 }
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
361#[serde(untagged)]
362pub enum ApiFunctionCallChoice {
363 Mode(String),
364 Function { name: String },
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
368pub struct ApiToolCall {
369 pub id: String,
370 #[serde(rename = "type")]
371 pub tool_type: String,
372 pub function: ApiFunctionCall,
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
376pub struct ApiFunctionCall {
377 pub name: String,
378 pub arguments: String,
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
382pub struct ApiResponseFormat {
383 #[serde(rename = "type")]
384 pub format_type: String,
385 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub json_schema: Option<ApiJsonSchema>,
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
390pub struct ApiJsonSchema {
391 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub name: Option<String>,
393 pub schema: serde_json::Value,
394 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub strict: Option<bool>,
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
399pub struct ApiStreamOptions {
400 #[serde(default, skip_serializing_if = "Option::is_none")]
401 pub include_usage: Option<bool>,
402}
403
404const MAX_PARALLEL_TOOL_CALLS_PER_RESPONSE: usize = 32;
405
406pub fn api_response_from_generated_text(
407 request: &InferenceRequest,
408 text: &str,
409 finish_reason: FinishReason,
410) -> Option<ApiResponse> {
411 let ApiRequest::Chat(chat_request) = request.api_request.as_ref()? else {
412 return None;
413 };
414 chat_api_response_from_generated_text(chat_request, text, finish_reason).map(ApiResponse::Chat)
415}
416
417pub fn chat_api_may_emit_tool_or_function_call(chat_request: &ApiChatRequest) -> bool {
418 (!chat_request.tools.is_empty() && !api_tool_choice_is_none(chat_request))
419 || (!chat_request.legacy_functions.is_empty()
420 && !api_function_call_choice_is_none(chat_request))
421}
422
423pub fn chat_api_response_from_generated_text(
424 chat_request: &ApiChatRequest,
425 text: &str,
426 finish_reason: FinishReason,
427) -> Option<ApiChatResponse> {
428 if !matches!(finish_reason, FinishReason::Stop | FinishReason::EOS) {
429 return None;
430 }
431
432 if !chat_request.tools.is_empty() && !api_tool_choice_is_none(chat_request) {
433 if let Some(tool_calls) = parse_tool_calls_from_generated_text(text, chat_request) {
434 return Some(ApiChatResponse {
435 message: ApiChatMessage {
436 role: ApiMessageRole::Assistant,
437 content: String::new(),
438 name: None,
439 tool_calls,
440 tool_call_id: None,
441 function_call: None,
442 },
443 finish_reason: Some("tool_calls".to_string()),
444 });
445 }
446 }
447
448 if !chat_request.legacy_functions.is_empty() && !api_function_call_choice_is_none(chat_request)
449 {
450 if let Some(function_call) =
451 parse_legacy_function_call_from_generated_text(text, chat_request)
452 {
453 return Some(ApiChatResponse {
454 message: ApiChatMessage {
455 role: ApiMessageRole::Assistant,
456 content: String::new(),
457 name: None,
458 tool_calls: Vec::new(),
459 tool_call_id: None,
460 function_call: Some(function_call),
461 },
462 finish_reason: Some("function_call".to_string()),
463 });
464 }
465 }
466
467 None
468}
469
470fn api_tool_choice_is_none(chat_request: &ApiChatRequest) -> bool {
471 matches!(
472 chat_request.tool_choice.as_ref(),
473 Some(ApiToolChoice::Mode(mode)) if mode.eq_ignore_ascii_case("none")
474 )
475}
476
477fn api_function_call_choice_is_none(chat_request: &ApiChatRequest) -> bool {
478 matches!(
479 chat_request.legacy_function_call.as_ref(),
480 Some(ApiFunctionCallChoice::Mode(mode)) if mode.eq_ignore_ascii_case("none")
481 )
482}
483
484fn parse_tool_calls_from_generated_text(
485 text: &str,
486 chat_request: &ApiChatRequest,
487) -> Option<Vec<ApiToolCall>> {
488 if chat_request.tool_call_protocol == ApiToolCallProtocol::FunctionParameterXml {
489 if let Some(calls) = parse_function_parameter_xml_tool_calls(text, chat_request) {
490 return validate_parsed_tool_calls(calls);
491 }
492 }
493
494 let value = parse_json_value_from_generated_text(text)?;
495 if let Some(calls) = value.get("tool_calls").and_then(|value| value.as_array()) {
496 if calls.len() > MAX_PARALLEL_TOOL_CALLS_PER_RESPONSE {
497 return None;
498 }
499 let parsed = calls
500 .iter()
501 .enumerate()
502 .map(|(index, value)| parse_tool_call_value(value, index, chat_request))
503 .collect::<Option<Vec<_>>>()?;
504 return validate_parsed_tool_calls(parsed);
505 }
506 if let Some(tool_call) = value.get("tool_call") {
507 return parse_tool_call_value(tool_call, 0, chat_request)
508 .and_then(|call| validate_parsed_tool_calls(vec![call]));
509 }
510 if let Some(tool_call) = parse_wrapped_tool_call_value(&value, 0, chat_request) {
511 return validate_parsed_tool_calls(vec![tool_call]);
512 }
513 parse_tool_call_value(&value, 0, chat_request)
514 .or_else(|| parse_forced_tool_arguments_value(&value, 0, chat_request))
515 .and_then(|call| validate_parsed_tool_calls(vec![call]))
516}
517
518fn validate_parsed_tool_calls(calls: Vec<ApiToolCall>) -> Option<Vec<ApiToolCall>> {
519 if calls.is_empty() || calls.len() > MAX_PARALLEL_TOOL_CALLS_PER_RESPONSE {
520 return None;
521 }
522 for (index, call) in calls.iter().enumerate() {
523 if calls[..index].iter().any(|previous| {
524 previous.function.name == call.function.name
525 && previous.function.arguments == call.function.arguments
526 }) {
527 return None;
528 }
529 }
530 Some(calls)
531}
532
533fn parse_function_parameter_xml_tool_calls(
534 text: &str,
535 chat_request: &ApiChatRequest,
536) -> Option<Vec<ApiToolCall>> {
537 const TOOL_START: &str = "<tool_call>";
538 const TOOL_END: &str = "</tool_call>";
539 const FUNCTION_START: &str = "<function=";
540 const FUNCTION_END: &str = "</function>";
541
542 let mut remaining = text;
543 let mut calls = Vec::new();
544 while let Some(tool_start) = remaining.find(TOOL_START) {
545 if calls.len() == MAX_PARALLEL_TOOL_CALLS_PER_RESPONSE {
546 return None;
547 }
548 remaining = &remaining[tool_start + TOOL_START.len()..];
549 let tool_end = remaining.find(TOOL_END)?;
550 let block = &remaining[..tool_end];
551 remaining = &remaining[tool_end + TOOL_END.len()..];
552
553 let Some(function_start) = block.find(FUNCTION_START) else {
554 return None;
555 };
556 if !block[..function_start].trim().is_empty() {
557 return None;
558 }
559 let function = &block[function_start + FUNCTION_START.len()..];
560 let Some(name_end) = function.find('>') else {
561 return None;
562 };
563 let name = function[..name_end].trim();
564 if !api_tool_name_allowed(chat_request, name) {
565 return None;
566 }
567 let arguments_end = function[name_end + 1..]
568 .find(FUNCTION_END)
569 .map(|offset| name_end + 1 + offset)?;
570 if !function[arguments_end + FUNCTION_END.len()..]
571 .trim()
572 .is_empty()
573 {
574 return None;
575 }
576 let parameter_schema = chat_request
577 .tools
578 .iter()
579 .find(|tool| tool.tool_type == "function" && tool.function.name == name)
580 .and_then(|tool| tool.function.parameters.as_ref());
581 let arguments = parse_function_parameter_xml_arguments(
582 &function[name_end + 1..arguments_end],
583 parameter_schema,
584 )?;
585 let arguments = serde_json::to_string(&arguments).ok()?;
586 calls.push(ApiToolCall {
587 id: format!("call_{}", calls.len()),
588 tool_type: "function".to_string(),
589 function: ApiFunctionCall {
590 name: name.to_string(),
591 arguments,
592 },
593 });
594 }
595
596 (!calls.is_empty()).then_some(calls)
597}
598
599fn parse_function_parameter_xml_arguments(
600 text: &str,
601 parameter_schema: Option<&serde_json::Value>,
602) -> Option<serde_json::Map<String, serde_json::Value>> {
603 const PARAMETER_START: &str = "<parameter=";
604 const PARAMETER_END: &str = "</parameter>";
605
606 let mut arguments = serde_json::Map::new();
607 let mut schema_probe = parameter_schema.map(XmlParameterSchemaProbe::new);
608 let mut remaining = text;
609 while let Some(parameter_start) = remaining.find(PARAMETER_START) {
610 remaining = &remaining[parameter_start + PARAMETER_START.len()..];
611 let Some(name_end) = remaining.find('>') else {
612 return None;
613 };
614 let name = remaining[..name_end].trim();
615 remaining = &remaining[name_end + 1..];
616 if name.is_empty() {
617 return None;
618 }
619 let value_end = remaining.find(PARAMETER_END)?;
620 if arguments.contains_key(name) {
621 return None;
622 }
623 let value = strip_xml_parameter_wrapper_newlines(&remaining[..value_end]);
624 let value = schema_probe.as_mut().map_or_else(
625 || serde_json::Value::String(value.to_string()),
626 |probe| probe.decode(name, value),
627 );
628 arguments.insert(name.to_string(), value);
629 remaining = &remaining[value_end + PARAMETER_END.len()..];
630 }
631 Some(arguments)
632}
633
634fn strip_xml_parameter_wrapper_newlines(value: &str) -> &str {
639 if let Some(value) = value.strip_prefix("\r\n") {
640 return value.strip_suffix("\r\n").unwrap_or(value);
641 }
642 if let Some(value) = value.strip_prefix('\n') {
643 if value.ends_with("\r\n") {
644 return value;
645 }
646 return value.strip_suffix('\n').unwrap_or(value);
647 }
648 value
649}
650
651fn parse_wrapped_tool_call_value(
652 value: &serde_json::Value,
653 index: usize,
654 chat_request: &ApiChatRequest,
655) -> Option<ApiToolCall> {
656 for key in ["auto", "tool", "tool_call", "auto_tool_response"] {
657 if let Some(wrapped) = value.get(key) {
658 if let Some(call) = parse_tool_call_value(wrapped, index, chat_request) {
659 return Some(call);
660 }
661 }
662 }
663 None
664}
665
666fn parse_tool_call_value(
667 value: &serde_json::Value,
668 index: usize,
669 chat_request: &ApiChatRequest,
670) -> Option<ApiToolCall> {
671 let tool_type = value
672 .get("type")
673 .and_then(|value| value.as_str())
674 .unwrap_or("function");
675 if tool_type != "function" {
676 return None;
677 }
678 let function = value.get("function").unwrap_or(value);
679 let name = function
680 .as_str()
681 .or_else(|| function.get("name").and_then(|value| value.as_str()))
682 .or_else(|| function.get("tool").and_then(|value| value.as_str()))
683 .or_else(|| value.get("name").and_then(|value| value.as_str()))?;
684 if !api_tool_name_allowed(chat_request, name) {
685 return None;
686 }
687 let arguments = api_arguments_to_string(
688 function
689 .get("arguments")
690 .or_else(|| function.get("parameters"))
691 .or_else(|| value.get("arguments"))
692 .or_else(|| value.get("parameters")),
693 );
694 let id = value
695 .get("id")
696 .and_then(|value| value.as_str())
697 .map(str::to_string)
698 .unwrap_or_else(|| format!("call_{index}"));
699
700 Some(ApiToolCall {
701 id,
702 tool_type: "function".to_string(),
703 function: ApiFunctionCall {
704 name: name.to_string(),
705 arguments,
706 },
707 })
708}
709
710fn parse_forced_tool_arguments_value(
711 value: &serde_json::Value,
712 index: usize,
713 chat_request: &ApiChatRequest,
714) -> Option<ApiToolCall> {
715 let tool = unwrapped_tool_arguments_target(chat_request, value)?;
716 if value.get("tool_calls").is_some()
717 || value.get("tool_call").is_some()
718 || value.get("function").is_some()
719 || value.get("name").is_some()
720 {
721 return None;
722 }
723
724 Some(ApiToolCall {
725 id: format!("call_{index}"),
726 tool_type: "function".to_string(),
727 function: ApiFunctionCall {
728 name: tool.function.name.clone(),
729 arguments: serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()),
730 },
731 })
732}
733
734fn unwrapped_tool_arguments_target<'a>(
735 chat_request: &'a ApiChatRequest,
736 value: &serde_json::Value,
737) -> Option<&'a ApiTool> {
738 if let Some(name) = forced_tool_choice_name(chat_request) {
739 return chat_request
740 .tools
741 .iter()
742 .find(|tool| tool.tool_type == "function" && tool.function.name == name);
743 }
744
745 if matches!(
746 chat_request.tool_choice.as_ref(),
747 Some(ApiToolChoice::Mode(mode)) if !mode.eq_ignore_ascii_case("auto")
748 ) {
749 return None;
750 }
751
752 let tool = single_function_tool(chat_request)?;
753 if !value_looks_like_tool_arguments(value, tool) {
754 return None;
755 }
756 Some(tool)
757}
758
759fn value_looks_like_tool_arguments(value: &serde_json::Value, tool: &ApiTool) -> bool {
760 let Some(arguments) = value.as_object() else {
761 return false;
762 };
763 if arguments.is_empty() {
764 return false;
765 }
766 let Some(properties) = tool
767 .function
768 .parameters
769 .as_ref()
770 .and_then(|parameters| parameters.get("properties"))
771 .and_then(|properties| properties.as_object())
772 else {
773 return false;
774 };
775 arguments.keys().all(|key| properties.contains_key(key))
776}
777
778fn forced_tool_choice_name(chat_request: &ApiChatRequest) -> Option<&str> {
779 match chat_request.tool_choice.as_ref() {
780 Some(ApiToolChoice::Function {
781 tool_type,
782 function,
783 }) if tool_type == "function" && api_tool_name_allowed(chat_request, &function.name) => {
784 Some(function.name.as_str())
785 }
786 Some(ApiToolChoice::Mode(mode)) if mode.eq_ignore_ascii_case("required") => {
787 single_function_tool(chat_request).map(|tool| tool.function.name.as_str())
788 }
789 _ => None,
790 }
791}
792
793fn single_function_tool(chat_request: &ApiChatRequest) -> Option<&ApiTool> {
794 let mut tools = chat_request
795 .tools
796 .iter()
797 .filter(|tool| tool.tool_type == "function");
798 let tool = tools.next()?;
799 tools.next().is_none().then_some(tool)
800}
801
802fn parse_legacy_function_call_from_generated_text(
803 text: &str,
804 chat_request: &ApiChatRequest,
805) -> Option<ApiFunctionCall> {
806 let value = parse_json_value_from_generated_text(text)?;
807 let function = value.get("function_call").unwrap_or(&value);
808 let name = function.get("name").and_then(|value| value.as_str())?;
809 if !api_function_name_allowed(chat_request, name) {
810 return None;
811 }
812 Some(ApiFunctionCall {
813 name: name.to_string(),
814 arguments: api_arguments_to_string(function.get("arguments")),
815 })
816}
817
818fn api_tool_name_allowed(chat_request: &ApiChatRequest, name: &str) -> bool {
819 match chat_request.tool_choice.as_ref() {
820 Some(ApiToolChoice::Mode(mode)) if mode.eq_ignore_ascii_case("none") => false,
821 Some(ApiToolChoice::Function {
822 tool_type,
823 function,
824 }) => {
825 tool_type == "function"
826 && function.name == name
827 && chat_request
828 .tools
829 .iter()
830 .any(|tool| tool.function.name == name)
831 }
832 _ => chat_request
833 .tools
834 .iter()
835 .any(|tool| tool.function.name == name),
836 }
837}
838
839fn api_function_name_allowed(chat_request: &ApiChatRequest, name: &str) -> bool {
840 match chat_request.legacy_function_call.as_ref() {
841 Some(ApiFunctionCallChoice::Mode(mode)) if mode.eq_ignore_ascii_case("none") => false,
842 Some(ApiFunctionCallChoice::Function { name: selected }) => {
843 selected == name
844 && chat_request
845 .legacy_functions
846 .iter()
847 .any(|function| function.name == name)
848 }
849 _ => chat_request
850 .legacy_functions
851 .iter()
852 .any(|function| function.name == name),
853 }
854}
855
856fn parse_json_value_from_generated_text(text: &str) -> Option<serde_json::Value> {
857 let trimmed = strip_single_json_fence(text.trim());
858 serde_json::from_str(trimmed).ok().or_else(|| {
859 let start = trimmed.find('{')?;
860 let end = trimmed.rfind('}')?;
861 (start <= end)
862 .then(|| serde_json::from_str(&trimmed[start..=end]).ok())
863 .flatten()
864 })
865}
866
867fn strip_single_json_fence(text: &str) -> &str {
868 let Some(rest) = text.strip_prefix("```") else {
869 return text;
870 };
871 let rest = rest.strip_prefix("json").unwrap_or(rest).trim_start();
872 rest.strip_suffix("```").map(str::trim).unwrap_or(text)
873}
874
875fn api_arguments_to_string(arguments: Option<&serde_json::Value>) -> String {
876 match arguments {
877 Some(serde_json::Value::String(raw)) => raw.clone(),
878 Some(value) => serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()),
879 None => "{}".to_string(),
880 }
881}
882
883impl InferenceRequest {
884 pub fn new(prompt: impl Into<String>, model_id: impl Into<ModelId>) -> Self {
886 Self {
887 id: RequestId::new(),
888 prompt: prompt.into(),
889 model_id: model_id.into(),
890 sampling_params: SamplingParams::default(),
891 stream: false,
892 priority: Priority::default(),
893 client_id: None,
894 session_id: None,
895 created_at: Utc::now(),
896 api_request: None,
897 evidence_request: InferenceEvidenceRequest::default(),
898 metadata: HashMap::new(),
899 }
900 }
901
902 pub fn with_sampling_params(mut self, params: SamplingParams) -> Self {
904 self.sampling_params = params;
905 self
906 }
907
908 pub fn with_stream(mut self, stream: bool) -> Self {
910 self.stream = stream;
911 self
912 }
913
914 pub fn with_priority(mut self, priority: Priority) -> Self {
916 self.priority = priority;
917 self
918 }
919
920 pub fn with_client_id(mut self, client_id: impl Into<ClientId>) -> Self {
922 self.client_id = Some(client_id.into());
923 self
924 }
925
926 pub fn with_session_id(mut self, session_id: SessionId) -> Self {
928 self.session_id = Some(session_id);
929 self
930 }
931
932 pub fn with_api_request(mut self, api_request: ApiRequest) -> Self {
934 self.api_request = Some(api_request);
935 self
936 }
937
938 pub fn with_prompt_token_evidence(mut self) -> Self {
940 self.evidence_request.capture_prompt_token_ids = true;
941 self
942 }
943
944 pub fn with_engine_token_timing_evidence(mut self) -> Self {
946 self.evidence_request.capture_engine_token_timing = true;
947 self
948 }
949
950 pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
952 self.metadata.insert(key.into(), value);
953 self
954 }
955}
956
957#[derive(Debug, Clone, Serialize, Deserialize)]
959pub struct InferenceResponse {
960 pub request_id: RequestId,
962 pub text: String,
964 pub tokens: Vec<TokenId>,
966 pub finish_reason: FinishReason,
968 pub usage: TokenUsage,
970 pub latency_ms: u64,
972 pub created_at: DateTime<Utc>,
974 pub metadata: HashMap<String, serde_json::Value>,
976 #[serde(default, skip_serializing_if = "Option::is_none")]
980 pub api_response: Option<ApiResponse>,
981 #[serde(default, skip_serializing_if = "Option::is_none")]
983 pub execution_evidence: Option<InferenceExecutionEvidence>,
984}
985
986#[derive(Debug, Clone, Serialize, Deserialize)]
988pub struct StreamChunk {
989 pub request_id: RequestId,
991 pub text: String,
993 pub token: Option<TokenId>,
995 pub finish_reason: Option<FinishReason>,
997 pub usage: Option<TokenUsage>,
999 pub created_at: DateTime<Utc>,
1001 pub metadata: HashMap<String, serde_json::Value>,
1003 #[serde(default, skip_serializing_if = "Option::is_none")]
1007 pub api_response: Option<ApiResponse>,
1008 #[serde(default, skip_serializing_if = "Option::is_none")]
1010 pub execution_evidence: Option<InferenceExecutionEvidence>,
1011}
1012
1013#[derive(Debug, Clone, Serialize, Deserialize)]
1015pub struct BatchRequest {
1016 pub batch_id: BatchId,
1018 pub requests: Vec<InferenceRequest>,
1020 pub max_sequence_length: usize,
1022 pub created_at: DateTime<Utc>,
1024}
1025
1026impl BatchRequest {
1027 pub fn new(requests: Vec<InferenceRequest>) -> Self {
1029 let max_sequence_length = requests
1030 .iter()
1031 .map(|r| r.sampling_params.max_tokens)
1032 .max()
1033 .unwrap_or(512);
1034
1035 Self {
1036 batch_id: BatchId::new(),
1037 requests,
1038 max_sequence_length,
1039 created_at: Utc::now(),
1040 }
1041 }
1042
1043 pub fn size(&self) -> usize {
1045 self.requests.len()
1046 }
1047
1048 pub fn is_empty(&self) -> bool {
1050 self.requests.is_empty()
1051 }
1052}
1053
1054#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1056pub enum RequestState {
1057 Waiting,
1059 Running,
1061 Preempted,
1063 Completed,
1065 Failed,
1067 Cancelled,
1069}
1070
1071#[derive(Debug, Clone)]
1073pub struct ScheduledRequest {
1074 pub request: InferenceRequest,
1076 pub state: RequestState,
1078 pub allocated_blocks: Vec<crate::BlockId>,
1080 pub tokens_processed: usize,
1082 pub estimated_completion: Option<DateTime<Utc>>,
1084}
1085
1086impl ScheduledRequest {
1087 pub fn new(request: InferenceRequest) -> Self {
1089 Self {
1090 request,
1091 state: RequestState::Waiting,
1092 allocated_blocks: Vec::new(),
1093 tokens_processed: 0,
1094 estimated_completion: None,
1095 }
1096 }
1097
1098 pub fn set_state(&mut self, state: RequestState) {
1100 self.state = state;
1101 }
1102
1103 pub fn add_blocks(&mut self, blocks: Vec<crate::BlockId>) {
1105 self.allocated_blocks.extend(blocks);
1106 }
1107
1108 pub fn update_progress(&mut self, tokens_processed: usize) {
1110 self.tokens_processed = tokens_processed;
1111 }
1112}