#![allow(unused_imports, dead_code)]
#[cfg(test)]
mod cancel_guard_tests;
mod ctx;
mod handle_done;
mod handle_error;
mod handle_token;
mod state;
mod strip;
mod token_ids;
mod tool_dispatch;
mod tool_handlers;
mod vllm_shape;
#[cfg(test)]
mod vllm_shape_tests;
use axum::http::StatusCode;
use axum::response::sse::{Event, KeepAlive};
use axum::response::{IntoResponse, Response, Sse};
use futures::StreamExt;
use std::sync::Arc;
use tokio_stream::wrappers::ReceiverStream;
use crate::AppState;
use crate::tool_parser;
use super::inference_types::{GrammarSpec, InferenceRequest, StreamEvent};
use ctx::StreamCtx;
use state::StreamState;
#[allow(clippy::too_many_arguments)]
pub(crate) async fn run_chat_stream(
state: Arc<AppState>,
prompt_tokens: Vec<u32>,
session_hash: u64,
lookup_tenant: Option<crate::auth::LookupTenant>,
adapter_slot: i32,
src_lang_id: u32,
tgt_lang_id: u32,
num_beams: u32,
length_penalty: f32,
early_stopping: bool,
image_pixels: Vec<metrale_model_layers::VisionItem>,
max_tokens: usize,
min_tokens: usize,
temperature: f32,
top_k: u32,
top_p: f32,
top_n_sigma: f32,
min_p: f32,
repetition_penalty: f32,
presence_penalty: f32,
frequency_penalty: f32,
dry_multiplier: f32,
dry_base: f32,
dry_allowed_length: u32,
lz_penalty: f32,
logit_bias: Vec<(u32, f32)>,
enable_thinking: bool,
thinking_budget: Option<u32>,
repetition_detection: Option<crate::api::inference_types::RepetitionDetectionParams>,
tools_active: bool,
tool_choice_required: bool,
suppress_tool_call: bool,
tool_defs: Vec<tool_parser::ToolDefinition>,
cwd_hint: Option<String>,
stop_tokens: Vec<u32>,
grammar_spec: Option<GrammarSpec>,
seed: Option<u64>,
top_logprobs: Option<u8>,
timeout_at: Option<std::time::Instant>,
stop_strings: Vec<String>,
req_return_token_ids: bool,
req_ctx: Option<crate::rate_limiter::RequestContext>,
dump_seq: Option<u64>,
active_guard: crate::metrics::ActiveRequestGuard,
) -> Result<crate::ir::DeltaStream, (StatusCode, String)> {
let (token_tx, token_rx) = tokio::sync::mpsc::channel::<StreamEvent>(1024);
let prompt_len = prompt_tokens.len();
let cancel_flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let scheduler_thinking = enable_thinking;
let prompt_tokens = std::sync::Arc::new(prompt_tokens);
let prompt_tokens_for_retry = prompt_tokens.clone();
let grammar_spec_for_retry = grammar_spec.clone();
let request = InferenceRequest::Streaming {
prompt_tokens,
session_hash,
lookup_tenant,
adapter_slot,
src_lang_id,
tgt_lang_id,
num_beams,
length_penalty,
early_stopping,
image_pixels,
max_tokens,
min_tokens,
temperature,
top_k,
top_p,
top_n_sigma,
min_p,
repetition_penalty,
presence_penalty,
frequency_penalty,
dry_multiplier,
dry_base,
dry_allowed_length,
lz_penalty,
logit_bias,
stop_tokens,
enable_thinking: scheduler_thinking,
thinking_budget,
repetition_detection,
require_tool_call: tool_choice_required,
tools_present: tools_active,
suppress_tool_call,
disable_mtp: false,
grammar_spec,
seed,
top_logprobs,
prompt_logprobs: None,
echo: false,
timeout_at,
token_tx,
cancel_flag: cancel_flag.clone(),
};
state.request_tx.send(request).await.map_err(|_| {
(
StatusCode::SERVICE_UNAVAILABLE,
"Scheduler queue full".to_string(),
)
})?;
let chunk_id = format!("chatcmpl-{}", crate::ids::uuid_v4());
let model_name = state.model_name.clone();
let leak_markers: tool_parser::LeakMarkers = state
.tool_call_parser
.as_ref()
.map(|p| p.leak_markers())
.unwrap_or(tool_parser::LeakMarkers::EMPTY);
let max_tool_calls_per_response: usize = std::env::var("METRALE_MAX_TOOL_CALLS_PER_RESPONSE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(12);
let stop_string_buffer_len: usize = stop_strings
.iter()
.map(|s| s.len())
.max()
.map(|m| m.saturating_sub(1))
.unwrap_or(0);
let prompt_vocab: Arc<std::collections::HashSet<String>> =
Arc::new(std::collections::HashSet::new());
let call_policy = state
.tool_call_parser
.as_ref()
.map_or(tool_parser::CallPolicy::Repairing, |p| p.call_policy());
let ctx = StreamCtx {
state: state.clone(),
model: model_name.clone(),
id: chunk_id.clone(),
prompt_len,
enable_thinking,
tool_defs_for_backfill: tool_defs,
cwd_for_normalize: cwd_hint,
stop_strings,
stop_string_buffer_len,
leak_markers,
wants_typed_arguments: state
.tool_call_parser
.as_ref()
.is_some_and(|p| p.wants_typed_arguments()),
call_policy,
tool_call_closes_reasoning: state
.reasoning_parser
.as_ref()
.is_some_and(|p| p.tool_call_closes_reasoning()),
max_tool_calls_per_response,
req_return_token_ids,
req_ctx,
dump_seq,
tool_retry_enabled: false,
prompt_tokens: prompt_tokens_for_retry,
prompt_vocab,
grammar_spec: grammar_spec_for_retry,
max_tokens,
timeout_at,
_active_guard: active_guard,
};
let mut stream_state = StreamState::new(
tools_active,
enable_thinking,
cancel_flag.clone(),
ctx.tool_defs_for_backfill.clone(),
);
if let Some(detector) = stream_state.detector.as_mut() {
detector.set_promote_bare_names(
state
.tool_call_parser
.as_ref()
.is_some_and(|p| p.promotes_bare_call_names()),
);
detector.set_fail_closed(matches!(
call_policy,
tool_parser::CallPolicy::FailClosed { .. }
));
}
let mut vllm_shape = state
.reasoning_parser
.as_ref()
.is_some_and(|p| p.shapes_stream_whitespace())
.then(vllm_shape::VllmDeltaShape::default);
let events = super::stream_terminal::terminated(ReceiverStream::new(token_rx));
let token_stream = events.flat_map(move |event| {
use futures::StreamExt;
let deltas = match event {
StreamEvent::Token(tok) | StreamEvent::TokenWithLogprobs(tok, _) => {
handle_token::handle_token(&mut stream_state, &ctx, tok)
}
StreamEvent::PromptLogprobs(_) => Vec::new(),
StreamEvent::Done {
finish_reason,
prompt_tokens: _,
completion_tokens,
time_to_first_token_ms,
decode_time_ms,
reasoning_tokens,
cached_prompt_tokens,
accepted_prediction_tokens,
guard_stop,
} => {
stream_state.guard_stop = stream_state.guard_stop.or(guard_stop);
handle_done::handle_done(
&mut stream_state,
&ctx,
finish_reason,
completion_tokens,
time_to_first_token_ms,
decode_time_ms,
reasoning_tokens,
cached_prompt_tokens,
accepted_prediction_tokens,
)
}
StreamEvent::Error(msg) => handle_error::handle_error(&ctx, msg),
};
let deltas = match vllm_shape.as_mut() {
Some(shape) => shape.apply(deltas),
None => deltas,
};
futures::stream::iter(deltas).boxed()
});
Ok(Box::pin(token_stream))
}