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