Skip to main content

ferrum_types/
requests.rs

1//! Request and response types for inference
2
3use 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/// Explicit request for execution evidence that is expensive or sensitive to retain.
15///
16/// The default keeps the inference hot path unchanged. Product entrypoints opt in
17/// only when the user enables a diagnostic artifact such as a request replay dump.
18#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
19pub struct InferenceEvidenceRequest {
20    #[serde(default)]
21    pub capture_prompt_token_ids: bool,
22    /// Capture engine token-commit timestamps from the request's monotonic
23    /// clock. Disabled by default so ordinary inference does not allocate or
24    /// read an additional clock in the token hot path.
25    #[serde(default)]
26    pub capture_engine_token_timing: bool,
27}
28
29/// Engine-boundary timing for one inference request.
30///
31/// Every token timestamp is an offset from the same `std::time::Instant`
32/// captured at request admission. The wall anchor exists only to correlate
33/// this monotonic domain with profile events; durations and ITL must be
34/// computed from `token_commit_nanos_since_request_start`, never by
35/// subtracting wall-clock timestamps.
36#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
37#[serde(rename_all = "snake_case")]
38pub enum EngineDecodeStage {
39    /// Host scheduling work that selected this request for a decode wave.
40    DecodeScheduling,
41    /// The model-executor call for one decode wave, including preparation,
42    /// device submission/completion, readback, and executor-side retirement.
43    DecodeExecution,
44    /// Host-side validation, sampling, and state commit after device execution.
45    DecodePostprocess,
46}
47
48/// A measured request-local engine decode stage in the request's monotonic
49/// clock domain. These are explicit producer boundaries, not intervals inferred
50/// by filling gaps between other profile events.
51#[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    /// Opt-in decode-stage evidence captured only with engine token timing.
67    #[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/// Evidence captured at the engine execution boundary.
127#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
128pub struct InferenceExecutionEvidence {
129    #[serde(default)]
130    pub prompt_token_ids: Vec<TokenId>,
131    /// Complete generated-token history at the engine boundary. Streaming
132    /// chunks cannot reconstruct this reliably because special tokens and
133    /// deferred multi-byte pieces may not produce a visible text delta.
134    #[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/// Inference request
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct InferenceRequest {
143    /// Unique request identifier
144    pub id: RequestId,
145    /// Input prompt text
146    pub prompt: String,
147    /// Model to use for inference
148    pub model_id: ModelId,
149    /// Sampling parameters
150    pub sampling_params: SamplingParams,
151    /// Whether to stream response
152    pub stream: bool,
153    /// Request priority
154    pub priority: Priority,
155    /// Client identifier
156    pub client_id: Option<ClientId>,
157    /// Session identifier for stateful interactions
158    pub session_id: Option<SessionId>,
159    /// Request creation timestamp
160    pub created_at: DateTime<Utc>,
161    /// Structured product/API request context. `prompt` remains the rendered
162    /// model input for current engines; this carries the original semantic
163    /// request boundary for API features such as tools and response formats.
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub api_request: Option<ApiRequest>,
166    /// Explicitly requested execution evidence. Disabled by default so normal
167    /// inference does not retain or copy prompt token IDs after completion.
168    #[serde(default)]
169    pub evidence_request: InferenceEvidenceRequest,
170    /// Additional metadata
171    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    /// Wire protocol emitted by the model's chat template for automatic tool calls.
196    /// Forced/named calls may still use a hard argument grammar and the JSON fallback.
197    #[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    /// Atomic tokenizer entries that this wire protocol may need to emit.
298    ///
299    /// The sampling layer resolves these texts against the loaded tokenizer
300    /// and allows only exact matches. Other added-vocabulary tokens remain
301    /// masked.
302    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    /// Lexical envelope that may satisfy a pending response boundary.
310    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    /// Protocol controls are generatable only when this request can emit a
324    /// modern tool call. `tool_choice: none` must not widen token sampling.
325    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    /// Alternate complete response envelope available to this request.
333    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    /// Create a new inference request
854    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    /// Set sampling parameters
872    pub fn with_sampling_params(mut self, params: SamplingParams) -> Self {
873        self.sampling_params = params;
874        self
875    }
876
877    /// Enable streaming
878    pub fn with_stream(mut self, stream: bool) -> Self {
879        self.stream = stream;
880        self
881    }
882
883    /// Set priority
884    pub fn with_priority(mut self, priority: Priority) -> Self {
885        self.priority = priority;
886        self
887    }
888
889    /// Set client ID
890    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    /// Set session ID
896    pub fn with_session_id(mut self, session_id: SessionId) -> Self {
897        self.session_id = Some(session_id);
898        self
899    }
900
901    /// Set structured product/API request context.
902    pub fn with_api_request(mut self, api_request: ApiRequest) -> Self {
903        self.api_request = Some(api_request);
904        self
905    }
906
907    /// Request prompt-token evidence from the execution boundary.
908    pub fn with_prompt_token_evidence(mut self) -> Self {
909        self.evidence_request.capture_prompt_token_ids = true;
910        self
911    }
912
913    /// Request engine token-commit timing evidence.
914    pub fn with_engine_token_timing_evidence(mut self) -> Self {
915        self.evidence_request.capture_engine_token_timing = true;
916        self
917    }
918
919    /// Add metadata
920    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/// Inference response
927#[derive(Debug, Clone, Serialize, Deserialize)]
928pub struct InferenceResponse {
929    /// Request ID this response corresponds to
930    pub request_id: RequestId,
931    /// Generated text
932    pub text: String,
933    /// Generated token IDs
934    pub tokens: Vec<TokenId>,
935    /// Reason for completion
936    pub finish_reason: FinishReason,
937    /// Token usage statistics
938    pub usage: TokenUsage,
939    /// Total latency in milliseconds
940    pub latency_ms: u64,
941    /// Response creation timestamp
942    pub created_at: DateTime<Utc>,
943    /// Additional response metadata
944    pub metadata: HashMap<String, serde_json::Value>,
945    /// Structured product/API response context. Engines that can produce
946    /// product-native outputs, such as assistant tool calls, can populate
947    /// this without overloading plain text or ad hoc metadata.
948    #[serde(default, skip_serializing_if = "Option::is_none")]
949    pub api_response: Option<ApiResponse>,
950    /// Optional engine-boundary evidence requested by the caller.
951    #[serde(default, skip_serializing_if = "Option::is_none")]
952    pub execution_evidence: Option<InferenceExecutionEvidence>,
953}
954
955/// Streaming response chunk
956#[derive(Debug, Clone, Serialize, Deserialize)]
957pub struct StreamChunk {
958    /// Request ID this chunk corresponds to
959    pub request_id: RequestId,
960    /// Text delta for this chunk
961    pub text: String,
962    /// Token ID for this chunk (if available)
963    pub token: Option<TokenId>,
964    /// Finish reason if this is the final chunk
965    pub finish_reason: Option<FinishReason>,
966    /// Token usage (typically only in final chunk)
967    pub usage: Option<TokenUsage>,
968    /// Chunk creation timestamp
969    pub created_at: DateTime<Utc>,
970    /// Chunk metadata
971    pub metadata: HashMap<String, serde_json::Value>,
972    /// Structured product/API response context for final streaming chunks.
973    /// This mirrors `InferenceResponse::api_response` so streaming endpoints
974    /// can return native tool/function-call payloads without reparsing text.
975    #[serde(default, skip_serializing_if = "Option::is_none")]
976    pub api_response: Option<ApiResponse>,
977    /// Optional engine-boundary evidence, emitted on the final chunk.
978    #[serde(default, skip_serializing_if = "Option::is_none")]
979    pub execution_evidence: Option<InferenceExecutionEvidence>,
980}
981
982/// Batch request for processing multiple requests together
983#[derive(Debug, Clone, Serialize, Deserialize)]
984pub struct BatchRequest {
985    /// Batch identifier
986    pub batch_id: BatchId,
987    /// Requests in this batch
988    pub requests: Vec<InferenceRequest>,
989    /// Maximum sequence length for this batch
990    pub max_sequence_length: usize,
991    /// Batch creation timestamp
992    pub created_at: DateTime<Utc>,
993}
994
995impl BatchRequest {
996    /// Create a new batch request
997    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    /// Get the number of requests in this batch
1013    pub fn size(&self) -> usize {
1014        self.requests.len()
1015    }
1016
1017    /// Check if batch is empty
1018    pub fn is_empty(&self) -> bool {
1019        self.requests.is_empty()
1020    }
1021}
1022
1023/// Request state in the scheduler
1024#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1025pub enum RequestState {
1026    /// Request is waiting in queue
1027    Waiting,
1028    /// Request is being processed
1029    Running,
1030    /// Request was preempted and is waiting to resume
1031    Preempted,
1032    /// Request completed successfully
1033    Completed,
1034    /// Request failed with error
1035    Failed,
1036    /// Request was cancelled
1037    Cancelled,
1038}
1039
1040/// Scheduled request with additional state information
1041#[derive(Debug, Clone)]
1042pub struct ScheduledRequest {
1043    /// The original request
1044    pub request: InferenceRequest,
1045    /// Current state in scheduler
1046    pub state: RequestState,
1047    /// Allocated cache blocks
1048    pub allocated_blocks: Vec<crate::BlockId>,
1049    /// Number of tokens processed so far
1050    pub tokens_processed: usize,
1051    /// Estimated completion time
1052    pub estimated_completion: Option<DateTime<Utc>>,
1053}
1054
1055impl ScheduledRequest {
1056    /// Create a new scheduled request
1057    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    /// Update request state
1068    pub fn set_state(&mut self, state: RequestState) {
1069        self.state = state;
1070    }
1071
1072    /// Add allocated cache blocks
1073    pub fn add_blocks(&mut self, blocks: Vec<crate::BlockId>) {
1074        self.allocated_blocks.extend(blocks);
1075    }
1076
1077    /// Update tokens processed
1078    pub fn update_progress(&mut self, tokens_processed: usize) {
1079        self.tokens_processed = tokens_processed;
1080    }
1081}