1use crate::{
4 ids::*, models::TokenUsage, FinishReason, Priority, ResponseCompletionEnvelope, SamplingParams,
5 TokenId,
6};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11pub const PROMPT_TOKENS_METADATA_KEY: &str = "ferrum_prompt_tokens";
12pub const DEFAULT_MAX_TOKENS_METADATA_KEY: &str = "ferrum_default_max_tokens";
13
14#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
19pub struct InferenceEvidenceRequest {
20 #[serde(default)]
21 pub capture_prompt_token_ids: bool,
22 #[serde(default)]
26 pub capture_engine_token_timing: bool,
27}
28
29#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
37#[serde(rename_all = "snake_case")]
38pub enum EngineDecodeStage {
39 DecodeScheduling,
41 DecodeExecution,
44 DecodePostprocess,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
52pub struct EngineDecodeStageInterval {
53 pub stage: EngineDecodeStage,
54 pub start_nanos_since_request_start: u64,
55 pub end_nanos_since_request_start: u64,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
59pub struct EngineTokenTimingEvidence {
60 pub clock_source: String,
61 pub wall_anchor_unix_nanos: i64,
62 pub wall_anchor_max_error_nanos: u64,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub decode_ready_nanos_since_request_start: Option<u64>,
65 pub token_commit_nanos_since_request_start: Vec<u64>,
66 #[serde(default, skip_serializing_if = "Vec::is_empty")]
68 pub decode_stage_intervals: Vec<EngineDecodeStageInterval>,
69}
70
71impl EngineTokenTimingEvidence {
72 pub fn validate(&self, output_tokens: usize) -> Result<(), String> {
73 if self.clock_source != "rust_std_instant" {
74 return Err("engine token timing clock_source must be rust_std_instant".to_string());
75 }
76 if self.wall_anchor_unix_nanos <= 0 {
77 return Err("engine token timing wall anchor must be positive".to_string());
78 }
79 if self.token_commit_nanos_since_request_start.len() != output_tokens {
80 return Err(format!(
81 "engine token timing has {} commits for {output_tokens} output tokens",
82 self.token_commit_nanos_since_request_start.len()
83 ));
84 }
85 if self
86 .token_commit_nanos_since_request_start
87 .windows(2)
88 .any(|window| window[1] < window[0])
89 {
90 return Err("engine token commit timestamps must be monotonic".to_string());
91 }
92 if self.decode_stage_intervals.iter().any(|interval| {
93 interval.end_nanos_since_request_start < interval.start_nanos_since_request_start
94 }) {
95 return Err("engine decode stage interval end precedes start".to_string());
96 }
97 if self.decode_stage_intervals.windows(2).any(|window| {
98 window[1].start_nanos_since_request_start < window[0].start_nanos_since_request_start
99 }) {
100 return Err("engine decode stage intervals must be ordered by start".to_string());
101 }
102 Ok(())
103 }
104
105 pub fn ttft_nanos(&self) -> Option<u64> {
106 self.token_commit_nanos_since_request_start.first().copied()
107 }
108
109 pub fn inter_token_nanos(&self) -> Vec<u64> {
110 self.token_commit_nanos_since_request_start
111 .windows(2)
112 .map(|window| window[1].saturating_sub(window[0]))
113 .collect()
114 }
115
116 pub fn decode_wall_nanos(&self) -> Option<u64> {
117 let start = self.decode_ready_nanos_since_request_start?;
118 let end = self
119 .token_commit_nanos_since_request_start
120 .last()
121 .copied()?;
122 (end >= start).then_some(end - start)
123 }
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
128pub struct InferenceExecutionEvidence {
129 #[serde(default)]
130 pub prompt_token_ids: Vec<TokenId>,
131 #[serde(default)]
135 pub output_token_ids: Vec<TokenId>,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub engine_token_timing: Option<EngineTokenTimingEvidence>,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct InferenceRequest {
143 pub id: RequestId,
145 pub prompt: String,
147 pub model_id: ModelId,
149 pub sampling_params: SamplingParams,
151 pub stream: bool,
153 pub priority: Priority,
155 pub client_id: Option<ClientId>,
157 pub session_id: Option<SessionId>,
159 pub created_at: DateTime<Utc>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub api_request: Option<ApiRequest>,
166 #[serde(default)]
169 pub evidence_request: InferenceEvidenceRequest,
170 pub metadata: HashMap<String, serde_json::Value>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
175#[serde(tag = "kind", rename_all = "snake_case")]
176pub enum ApiRequest {
177 Chat(ApiChatRequest),
178 Completion(ApiCompletionRequest),
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
182#[serde(tag = "kind", rename_all = "snake_case")]
183pub enum ApiResponse {
184 Chat(ApiChatResponse),
185 Completion(ApiCompletionResponse),
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
189pub struct ApiChatRequest {
190 pub messages: Vec<ApiChatMessage>,
191 #[serde(default, skip_serializing_if = "Vec::is_empty")]
192 pub tools: Vec<ApiTool>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub tool_choice: Option<ApiToolChoice>,
195 #[serde(default)]
198 pub tool_call_protocol: ApiToolCallProtocol,
199 #[serde(default, skip_serializing_if = "Vec::is_empty")]
200 pub legacy_functions: Vec<ApiFunction>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub legacy_function_call: Option<ApiFunctionCallChoice>,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub response_format: Option<ApiResponseFormat>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub stream_options: Option<ApiStreamOptions>,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
210pub struct ApiCompletionRequest {
211 pub prompt: String,
212 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub response_format: Option<ApiResponseFormat>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
217pub struct ApiChatResponse {
218 pub message: ApiChatMessage,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub finish_reason: Option<String>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
224pub struct ApiCompletionResponse {
225 pub text: String,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub finish_reason: Option<String>,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
231pub struct ApiChatMessage {
232 pub role: ApiMessageRole,
233 pub content: String,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub name: Option<String>,
236 #[serde(default, skip_serializing_if = "Vec::is_empty")]
237 pub tool_calls: Vec<ApiToolCall>,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub tool_call_id: Option<String>,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub function_call: Option<ApiFunctionCall>,
242}
243
244#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
245#[serde(rename_all = "lowercase")]
246pub enum ApiMessageRole {
247 System,
248 User,
249 Assistant,
250 Function,
251 Tool,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
255pub struct ApiTool {
256 #[serde(rename = "type")]
257 pub tool_type: String,
258 pub function: ApiFunction,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
262pub struct ApiFunction {
263 pub name: String,
264 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub description: Option<String>,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub parameters: Option<serde_json::Value>,
268 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub strict: Option<bool>,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
273#[serde(untagged)]
274pub enum ApiToolChoice {
275 Mode(String),
276 Function {
277 #[serde(rename = "type")]
278 tool_type: String,
279 function: ApiToolChoiceFunction,
280 },
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
284pub struct ApiToolChoiceFunction {
285 pub name: String,
286}
287
288#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
289#[serde(rename_all = "snake_case")]
290pub enum ApiToolCallProtocol {
291 #[default]
292 Json,
293 FunctionParameterXml,
294}
295
296impl ApiToolCallProtocol {
297 pub const fn generated_control_token_texts(self) -> &'static [&'static str] {
303 match self {
304 Self::Json => &[],
305 Self::FunctionParameterXml => &["<tool_call>", "</tool_call>"],
306 }
307 }
308
309 pub fn generated_response_envelope(self) -> Option<ResponseCompletionEnvelope> {
311 match self {
312 Self::Json => None,
313 Self::FunctionParameterXml => Some(ResponseCompletionEnvelope {
314 open_token_text: "<tool_call>".to_string(),
315 close_token_text: "</tool_call>".to_string(),
316 max_envelopes: MAX_PARALLEL_TOOL_CALLS_PER_RESPONSE,
317 }),
318 }
319 }
320}
321
322impl ApiChatRequest {
323 pub fn generated_control_token_texts(&self) -> &'static [&'static str] {
326 if self.tools.is_empty() || api_tool_choice_is_none(self) {
327 return &[];
328 }
329 self.tool_call_protocol.generated_control_token_texts()
330 }
331
332 pub fn generated_response_envelope(&self) -> Option<ResponseCompletionEnvelope> {
334 if self.tools.is_empty() || api_tool_choice_is_none(self) {
335 return None;
336 }
337 self.tool_call_protocol.generated_response_envelope()
338 }
339}
340
341impl ApiRequest {
342 pub fn generated_control_token_texts(&self) -> &'static [&'static str] {
343 match self {
344 Self::Chat(request) => request.generated_control_token_texts(),
345 Self::Completion(_) => &[],
346 }
347 }
348
349 pub fn generated_response_envelope(&self) -> Option<ResponseCompletionEnvelope> {
350 match self {
351 Self::Chat(request) => request.generated_response_envelope(),
352 Self::Completion(_) => None,
353 }
354 }
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
358#[serde(untagged)]
359pub enum ApiFunctionCallChoice {
360 Mode(String),
361 Function { name: String },
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
365pub struct ApiToolCall {
366 pub id: String,
367 #[serde(rename = "type")]
368 pub tool_type: String,
369 pub function: ApiFunctionCall,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
373pub struct ApiFunctionCall {
374 pub name: String,
375 pub arguments: String,
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
379pub struct ApiResponseFormat {
380 #[serde(rename = "type")]
381 pub format_type: String,
382 #[serde(default, skip_serializing_if = "Option::is_none")]
383 pub json_schema: Option<ApiJsonSchema>,
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
387pub struct ApiJsonSchema {
388 #[serde(default, skip_serializing_if = "Option::is_none")]
389 pub name: Option<String>,
390 pub schema: serde_json::Value,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub strict: Option<bool>,
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
396pub struct ApiStreamOptions {
397 #[serde(default, skip_serializing_if = "Option::is_none")]
398 pub include_usage: Option<bool>,
399}
400
401const MAX_PARALLEL_TOOL_CALLS_PER_RESPONSE: usize = 32;
402
403pub fn api_response_from_generated_text(
404 request: &InferenceRequest,
405 text: &str,
406 finish_reason: FinishReason,
407) -> Option<ApiResponse> {
408 let ApiRequest::Chat(chat_request) = request.api_request.as_ref()? else {
409 return None;
410 };
411 chat_api_response_from_generated_text(chat_request, text, finish_reason).map(ApiResponse::Chat)
412}
413
414pub fn chat_api_may_emit_tool_or_function_call(chat_request: &ApiChatRequest) -> bool {
415 (!chat_request.tools.is_empty() && !api_tool_choice_is_none(chat_request))
416 || (!chat_request.legacy_functions.is_empty()
417 && !api_function_call_choice_is_none(chat_request))
418}
419
420pub fn chat_api_response_from_generated_text(
421 chat_request: &ApiChatRequest,
422 text: &str,
423 finish_reason: FinishReason,
424) -> Option<ApiChatResponse> {
425 if !matches!(finish_reason, FinishReason::Stop | FinishReason::EOS) {
426 return None;
427 }
428
429 if !chat_request.tools.is_empty() && !api_tool_choice_is_none(chat_request) {
430 if let Some(tool_calls) = parse_tool_calls_from_generated_text(text, chat_request) {
431 return Some(ApiChatResponse {
432 message: ApiChatMessage {
433 role: ApiMessageRole::Assistant,
434 content: String::new(),
435 name: None,
436 tool_calls,
437 tool_call_id: None,
438 function_call: None,
439 },
440 finish_reason: Some("tool_calls".to_string()),
441 });
442 }
443 }
444
445 if !chat_request.legacy_functions.is_empty() && !api_function_call_choice_is_none(chat_request)
446 {
447 if let Some(function_call) =
448 parse_legacy_function_call_from_generated_text(text, chat_request)
449 {
450 return Some(ApiChatResponse {
451 message: ApiChatMessage {
452 role: ApiMessageRole::Assistant,
453 content: String::new(),
454 name: None,
455 tool_calls: Vec::new(),
456 tool_call_id: None,
457 function_call: Some(function_call),
458 },
459 finish_reason: Some("function_call".to_string()),
460 });
461 }
462 }
463
464 None
465}
466
467fn api_tool_choice_is_none(chat_request: &ApiChatRequest) -> bool {
468 matches!(
469 chat_request.tool_choice.as_ref(),
470 Some(ApiToolChoice::Mode(mode)) if mode.eq_ignore_ascii_case("none")
471 )
472}
473
474fn api_function_call_choice_is_none(chat_request: &ApiChatRequest) -> bool {
475 matches!(
476 chat_request.legacy_function_call.as_ref(),
477 Some(ApiFunctionCallChoice::Mode(mode)) if mode.eq_ignore_ascii_case("none")
478 )
479}
480
481fn parse_tool_calls_from_generated_text(
482 text: &str,
483 chat_request: &ApiChatRequest,
484) -> Option<Vec<ApiToolCall>> {
485 if chat_request.tool_call_protocol == ApiToolCallProtocol::FunctionParameterXml {
486 if let Some(calls) = parse_function_parameter_xml_tool_calls(text, chat_request) {
487 return validate_parsed_tool_calls(calls);
488 }
489 }
490
491 let value = parse_json_value_from_generated_text(text)?;
492 if let Some(calls) = value.get("tool_calls").and_then(|value| value.as_array()) {
493 if calls.len() > MAX_PARALLEL_TOOL_CALLS_PER_RESPONSE {
494 return None;
495 }
496 let parsed = calls
497 .iter()
498 .enumerate()
499 .map(|(index, value)| parse_tool_call_value(value, index, chat_request))
500 .collect::<Option<Vec<_>>>()?;
501 return validate_parsed_tool_calls(parsed);
502 }
503 if let Some(tool_call) = value.get("tool_call") {
504 return parse_tool_call_value(tool_call, 0, chat_request)
505 .and_then(|call| validate_parsed_tool_calls(vec![call]));
506 }
507 if let Some(tool_call) = parse_wrapped_tool_call_value(&value, 0, chat_request) {
508 return validate_parsed_tool_calls(vec![tool_call]);
509 }
510 parse_tool_call_value(&value, 0, chat_request)
511 .or_else(|| parse_forced_tool_arguments_value(&value, 0, chat_request))
512 .and_then(|call| validate_parsed_tool_calls(vec![call]))
513}
514
515fn validate_parsed_tool_calls(calls: Vec<ApiToolCall>) -> Option<Vec<ApiToolCall>> {
516 if calls.is_empty() || calls.len() > MAX_PARALLEL_TOOL_CALLS_PER_RESPONSE {
517 return None;
518 }
519 for (index, call) in calls.iter().enumerate() {
520 if calls[..index].iter().any(|previous| {
521 previous.function.name == call.function.name
522 && previous.function.arguments == call.function.arguments
523 }) {
524 return None;
525 }
526 }
527 Some(calls)
528}
529
530fn parse_function_parameter_xml_tool_calls(
531 text: &str,
532 chat_request: &ApiChatRequest,
533) -> Option<Vec<ApiToolCall>> {
534 const TOOL_START: &str = "<tool_call>";
535 const TOOL_END: &str = "</tool_call>";
536 const FUNCTION_START: &str = "<function=";
537 const FUNCTION_END: &str = "</function>";
538
539 let mut remaining = text;
540 let mut calls = Vec::new();
541 while let Some(tool_start) = remaining.find(TOOL_START) {
542 if calls.len() == MAX_PARALLEL_TOOL_CALLS_PER_RESPONSE {
543 return None;
544 }
545 remaining = &remaining[tool_start + TOOL_START.len()..];
546 let tool_end = remaining.find(TOOL_END)?;
547 let block = &remaining[..tool_end];
548 remaining = &remaining[tool_end + TOOL_END.len()..];
549
550 let Some(function_start) = block.find(FUNCTION_START) else {
551 return None;
552 };
553 if !block[..function_start].trim().is_empty() {
554 return None;
555 }
556 let function = &block[function_start + FUNCTION_START.len()..];
557 let Some(name_end) = function.find('>') else {
558 return None;
559 };
560 let name = function[..name_end].trim();
561 if !api_tool_name_allowed(chat_request, name) {
562 return None;
563 }
564 let arguments_end = function[name_end + 1..]
565 .find(FUNCTION_END)
566 .map(|offset| name_end + 1 + offset)?;
567 if !function[arguments_end + FUNCTION_END.len()..]
568 .trim()
569 .is_empty()
570 {
571 return None;
572 }
573 let arguments =
574 parse_function_parameter_xml_arguments(&function[name_end + 1..arguments_end])?;
575 let arguments = serde_json::to_string(&arguments).ok()?;
576 calls.push(ApiToolCall {
577 id: format!("call_{}", calls.len()),
578 tool_type: "function".to_string(),
579 function: ApiFunctionCall {
580 name: name.to_string(),
581 arguments,
582 },
583 });
584 }
585
586 (!calls.is_empty()).then_some(calls)
587}
588
589fn parse_function_parameter_xml_arguments(
590 text: &str,
591) -> Option<serde_json::Map<String, serde_json::Value>> {
592 const PARAMETER_START: &str = "<parameter=";
593 const PARAMETER_END: &str = "</parameter>";
594
595 let mut arguments = serde_json::Map::new();
596 let mut remaining = text;
597 while let Some(parameter_start) = remaining.find(PARAMETER_START) {
598 remaining = &remaining[parameter_start + PARAMETER_START.len()..];
599 let Some(name_end) = remaining.find('>') else {
600 return None;
601 };
602 let name = remaining[..name_end].trim();
603 remaining = &remaining[name_end + 1..];
604 if name.is_empty() {
605 return None;
606 }
607 let value_end = remaining.find(PARAMETER_END)?;
608 if arguments.contains_key(name) {
609 return None;
610 }
611 arguments.insert(
612 name.to_string(),
613 serde_json::Value::String(remaining[..value_end].trim().to_string()),
614 );
615 remaining = &remaining[value_end + PARAMETER_END.len()..];
616 }
617 Some(arguments)
618}
619
620fn parse_wrapped_tool_call_value(
621 value: &serde_json::Value,
622 index: usize,
623 chat_request: &ApiChatRequest,
624) -> Option<ApiToolCall> {
625 for key in ["auto", "tool", "tool_call", "auto_tool_response"] {
626 if let Some(wrapped) = value.get(key) {
627 if let Some(call) = parse_tool_call_value(wrapped, index, chat_request) {
628 return Some(call);
629 }
630 }
631 }
632 None
633}
634
635fn parse_tool_call_value(
636 value: &serde_json::Value,
637 index: usize,
638 chat_request: &ApiChatRequest,
639) -> Option<ApiToolCall> {
640 let tool_type = value
641 .get("type")
642 .and_then(|value| value.as_str())
643 .unwrap_or("function");
644 if tool_type != "function" {
645 return None;
646 }
647 let function = value.get("function").unwrap_or(value);
648 let name = function
649 .as_str()
650 .or_else(|| function.get("name").and_then(|value| value.as_str()))
651 .or_else(|| function.get("tool").and_then(|value| value.as_str()))
652 .or_else(|| value.get("name").and_then(|value| value.as_str()))?;
653 if !api_tool_name_allowed(chat_request, name) {
654 return None;
655 }
656 let arguments = api_arguments_to_string(
657 function
658 .get("arguments")
659 .or_else(|| function.get("parameters"))
660 .or_else(|| value.get("arguments"))
661 .or_else(|| value.get("parameters")),
662 );
663 let id = value
664 .get("id")
665 .and_then(|value| value.as_str())
666 .map(str::to_string)
667 .unwrap_or_else(|| format!("call_{index}"));
668
669 Some(ApiToolCall {
670 id,
671 tool_type: "function".to_string(),
672 function: ApiFunctionCall {
673 name: name.to_string(),
674 arguments,
675 },
676 })
677}
678
679fn parse_forced_tool_arguments_value(
680 value: &serde_json::Value,
681 index: usize,
682 chat_request: &ApiChatRequest,
683) -> Option<ApiToolCall> {
684 let tool = unwrapped_tool_arguments_target(chat_request, value)?;
685 if value.get("tool_calls").is_some()
686 || value.get("tool_call").is_some()
687 || value.get("function").is_some()
688 || value.get("name").is_some()
689 {
690 return None;
691 }
692
693 Some(ApiToolCall {
694 id: format!("call_{index}"),
695 tool_type: "function".to_string(),
696 function: ApiFunctionCall {
697 name: tool.function.name.clone(),
698 arguments: serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()),
699 },
700 })
701}
702
703fn unwrapped_tool_arguments_target<'a>(
704 chat_request: &'a ApiChatRequest,
705 value: &serde_json::Value,
706) -> Option<&'a ApiTool> {
707 if let Some(name) = forced_tool_choice_name(chat_request) {
708 return chat_request
709 .tools
710 .iter()
711 .find(|tool| tool.tool_type == "function" && tool.function.name == name);
712 }
713
714 if matches!(
715 chat_request.tool_choice.as_ref(),
716 Some(ApiToolChoice::Mode(mode)) if !mode.eq_ignore_ascii_case("auto")
717 ) {
718 return None;
719 }
720
721 let tool = single_function_tool(chat_request)?;
722 if !value_looks_like_tool_arguments(value, tool) {
723 return None;
724 }
725 Some(tool)
726}
727
728fn value_looks_like_tool_arguments(value: &serde_json::Value, tool: &ApiTool) -> bool {
729 let Some(arguments) = value.as_object() else {
730 return false;
731 };
732 if arguments.is_empty() {
733 return false;
734 }
735 let Some(properties) = tool
736 .function
737 .parameters
738 .as_ref()
739 .and_then(|parameters| parameters.get("properties"))
740 .and_then(|properties| properties.as_object())
741 else {
742 return false;
743 };
744 arguments.keys().all(|key| properties.contains_key(key))
745}
746
747fn forced_tool_choice_name(chat_request: &ApiChatRequest) -> Option<&str> {
748 match chat_request.tool_choice.as_ref() {
749 Some(ApiToolChoice::Function {
750 tool_type,
751 function,
752 }) if tool_type == "function" && api_tool_name_allowed(chat_request, &function.name) => {
753 Some(function.name.as_str())
754 }
755 Some(ApiToolChoice::Mode(mode)) if mode.eq_ignore_ascii_case("required") => {
756 single_function_tool(chat_request).map(|tool| tool.function.name.as_str())
757 }
758 _ => None,
759 }
760}
761
762fn single_function_tool(chat_request: &ApiChatRequest) -> Option<&ApiTool> {
763 let mut tools = chat_request
764 .tools
765 .iter()
766 .filter(|tool| tool.tool_type == "function");
767 let tool = tools.next()?;
768 tools.next().is_none().then_some(tool)
769}
770
771fn parse_legacy_function_call_from_generated_text(
772 text: &str,
773 chat_request: &ApiChatRequest,
774) -> Option<ApiFunctionCall> {
775 let value = parse_json_value_from_generated_text(text)?;
776 let function = value.get("function_call").unwrap_or(&value);
777 let name = function.get("name").and_then(|value| value.as_str())?;
778 if !api_function_name_allowed(chat_request, name) {
779 return None;
780 }
781 Some(ApiFunctionCall {
782 name: name.to_string(),
783 arguments: api_arguments_to_string(function.get("arguments")),
784 })
785}
786
787fn api_tool_name_allowed(chat_request: &ApiChatRequest, name: &str) -> bool {
788 match chat_request.tool_choice.as_ref() {
789 Some(ApiToolChoice::Mode(mode)) if mode.eq_ignore_ascii_case("none") => false,
790 Some(ApiToolChoice::Function {
791 tool_type,
792 function,
793 }) => {
794 tool_type == "function"
795 && function.name == name
796 && chat_request
797 .tools
798 .iter()
799 .any(|tool| tool.function.name == name)
800 }
801 _ => chat_request
802 .tools
803 .iter()
804 .any(|tool| tool.function.name == name),
805 }
806}
807
808fn api_function_name_allowed(chat_request: &ApiChatRequest, name: &str) -> bool {
809 match chat_request.legacy_function_call.as_ref() {
810 Some(ApiFunctionCallChoice::Mode(mode)) if mode.eq_ignore_ascii_case("none") => false,
811 Some(ApiFunctionCallChoice::Function { name: selected }) => {
812 selected == name
813 && chat_request
814 .legacy_functions
815 .iter()
816 .any(|function| function.name == name)
817 }
818 _ => chat_request
819 .legacy_functions
820 .iter()
821 .any(|function| function.name == name),
822 }
823}
824
825fn parse_json_value_from_generated_text(text: &str) -> Option<serde_json::Value> {
826 let trimmed = strip_single_json_fence(text.trim());
827 serde_json::from_str(trimmed).ok().or_else(|| {
828 let start = trimmed.find('{')?;
829 let end = trimmed.rfind('}')?;
830 (start <= end)
831 .then(|| serde_json::from_str(&trimmed[start..=end]).ok())
832 .flatten()
833 })
834}
835
836fn strip_single_json_fence(text: &str) -> &str {
837 let Some(rest) = text.strip_prefix("```") else {
838 return text;
839 };
840 let rest = rest.strip_prefix("json").unwrap_or(rest).trim_start();
841 rest.strip_suffix("```").map(str::trim).unwrap_or(text)
842}
843
844fn api_arguments_to_string(arguments: Option<&serde_json::Value>) -> String {
845 match arguments {
846 Some(serde_json::Value::String(raw)) => raw.clone(),
847 Some(value) => serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()),
848 None => "{}".to_string(),
849 }
850}
851
852impl InferenceRequest {
853 pub fn new(prompt: impl Into<String>, model_id: impl Into<ModelId>) -> Self {
855 Self {
856 id: RequestId::new(),
857 prompt: prompt.into(),
858 model_id: model_id.into(),
859 sampling_params: SamplingParams::default(),
860 stream: false,
861 priority: Priority::default(),
862 client_id: None,
863 session_id: None,
864 created_at: Utc::now(),
865 api_request: None,
866 evidence_request: InferenceEvidenceRequest::default(),
867 metadata: HashMap::new(),
868 }
869 }
870
871 pub fn with_sampling_params(mut self, params: SamplingParams) -> Self {
873 self.sampling_params = params;
874 self
875 }
876
877 pub fn with_stream(mut self, stream: bool) -> Self {
879 self.stream = stream;
880 self
881 }
882
883 pub fn with_priority(mut self, priority: Priority) -> Self {
885 self.priority = priority;
886 self
887 }
888
889 pub fn with_client_id(mut self, client_id: impl Into<ClientId>) -> Self {
891 self.client_id = Some(client_id.into());
892 self
893 }
894
895 pub fn with_session_id(mut self, session_id: SessionId) -> Self {
897 self.session_id = Some(session_id);
898 self
899 }
900
901 pub fn with_api_request(mut self, api_request: ApiRequest) -> Self {
903 self.api_request = Some(api_request);
904 self
905 }
906
907 pub fn with_prompt_token_evidence(mut self) -> Self {
909 self.evidence_request.capture_prompt_token_ids = true;
910 self
911 }
912
913 pub fn with_engine_token_timing_evidence(mut self) -> Self {
915 self.evidence_request.capture_engine_token_timing = true;
916 self
917 }
918
919 pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
921 self.metadata.insert(key.into(), value);
922 self
923 }
924}
925
926#[derive(Debug, Clone, Serialize, Deserialize)]
928pub struct InferenceResponse {
929 pub request_id: RequestId,
931 pub text: String,
933 pub tokens: Vec<TokenId>,
935 pub finish_reason: FinishReason,
937 pub usage: TokenUsage,
939 pub latency_ms: u64,
941 pub created_at: DateTime<Utc>,
943 pub metadata: HashMap<String, serde_json::Value>,
945 #[serde(default, skip_serializing_if = "Option::is_none")]
949 pub api_response: Option<ApiResponse>,
950 #[serde(default, skip_serializing_if = "Option::is_none")]
952 pub execution_evidence: Option<InferenceExecutionEvidence>,
953}
954
955#[derive(Debug, Clone, Serialize, Deserialize)]
957pub struct StreamChunk {
958 pub request_id: RequestId,
960 pub text: String,
962 pub token: Option<TokenId>,
964 pub finish_reason: Option<FinishReason>,
966 pub usage: Option<TokenUsage>,
968 pub created_at: DateTime<Utc>,
970 pub metadata: HashMap<String, serde_json::Value>,
972 #[serde(default, skip_serializing_if = "Option::is_none")]
976 pub api_response: Option<ApiResponse>,
977 #[serde(default, skip_serializing_if = "Option::is_none")]
979 pub execution_evidence: Option<InferenceExecutionEvidence>,
980}
981
982#[derive(Debug, Clone, Serialize, Deserialize)]
984pub struct BatchRequest {
985 pub batch_id: BatchId,
987 pub requests: Vec<InferenceRequest>,
989 pub max_sequence_length: usize,
991 pub created_at: DateTime<Utc>,
993}
994
995impl BatchRequest {
996 pub fn new(requests: Vec<InferenceRequest>) -> Self {
998 let max_sequence_length = requests
999 .iter()
1000 .map(|r| r.sampling_params.max_tokens)
1001 .max()
1002 .unwrap_or(512);
1003
1004 Self {
1005 batch_id: BatchId::new(),
1006 requests,
1007 max_sequence_length,
1008 created_at: Utc::now(),
1009 }
1010 }
1011
1012 pub fn size(&self) -> usize {
1014 self.requests.len()
1015 }
1016
1017 pub fn is_empty(&self) -> bool {
1019 self.requests.is_empty()
1020 }
1021}
1022
1023#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1025pub enum RequestState {
1026 Waiting,
1028 Running,
1030 Preempted,
1032 Completed,
1034 Failed,
1036 Cancelled,
1038}
1039
1040#[derive(Debug, Clone)]
1042pub struct ScheduledRequest {
1043 pub request: InferenceRequest,
1045 pub state: RequestState,
1047 pub allocated_blocks: Vec<crate::BlockId>,
1049 pub tokens_processed: usize,
1051 pub estimated_completion: Option<DateTime<Utc>>,
1053}
1054
1055impl ScheduledRequest {
1056 pub fn new(request: InferenceRequest) -> Self {
1058 Self {
1059 request,
1060 state: RequestState::Waiting,
1061 allocated_blocks: Vec::new(),
1062 tokens_processed: 0,
1063 estimated_completion: None,
1064 }
1065 }
1066
1067 pub fn set_state(&mut self, state: RequestState) {
1069 self.state = state;
1070 }
1071
1072 pub fn add_blocks(&mut self, blocks: Vec<crate::BlockId>) {
1074 self.allocated_blocks.extend(blocks);
1075 }
1076
1077 pub fn update_progress(&mut self, tokens_processed: usize) {
1079 self.tokens_processed = tokens_processed;
1080 }
1081}