use std::collections::HashSet;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use anyllm_translate::openai::{
ChatCompletionRequest, ChatMessage, ChatRole, ChatTool, ChatToolChoice, Stop, ToolCall,
};
use runtime::GenerationConfig;
use selection::ShapingConfig;
use tokenizer::{
ContentPart, FunctionDefinition, HistoricalToolCall, JsonValue, Message, ReasoningEffort, Role,
};
use crate::model::ChatModel;
pub type AppState = Arc<dyn ChatModel>;
pub(crate) fn now_unix() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
static REQUEST_ID_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
pub(crate) fn request_id(prefix: &str) -> String {
let n = REQUEST_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
format!("{prefix}{}-{n}", now_unix())
}
fn role_from(role: &ChatRole) -> Role {
match role {
ChatRole::System => Role::System,
ChatRole::Assistant => Role::Assistant,
ChatRole::Tool | ChatRole::Function => Role::Tool,
ChatRole::Developer => Role::Developer,
ChatRole::User => Role::User,
}
}
pub(crate) fn stop_strings(stop: Option<&Stop>) -> Vec<String> {
match stop {
Some(Stop::Single(s)) => vec![s.clone()],
Some(Stop::Multiple(v)) => v.clone(),
None => Vec::new(),
}
}
pub(crate) fn build_shaping(
temperature: Option<f32>,
top_p: Option<f32>,
presence_penalty: Option<f32>,
frequency_penalty: Option<f32>,
extra: &serde_json::Map<String, serde_json::Value>,
) -> Result<ShapingConfig, String> {
let top_k = match extra.get("top_k") {
None => 64,
Some(v) => v
.as_u64()
.and_then(|n| u32::try_from(n).ok())
.ok_or_else(|| format!("top_k must be a non-negative integer, got {v}"))?,
};
let repetition_penalty = extra
.get("repetition_penalty")
.and_then(|v| v.as_f64())
.unwrap_or(1.0);
let mut shaping = ShapingConfig::new(
temperature.map(f64::from).unwrap_or(1.0),
top_k,
top_p.map(f64::from),
repetition_penalty,
extra.get("seed").and_then(|v| v.as_u64()),
)
.map_err(|e| e.to_string())?;
if let Some(p) = presence_penalty {
shaping = shaping
.with_presence_penalty(p as f64)
.map_err(|e| e.to_string())?;
}
if let Some(f) = frequency_penalty {
shaping = shaping
.with_frequency_penalty(f as f64)
.map_err(|e| e.to_string())?;
}
if let Some(m) = extra.get("min_p").and_then(|v| v.as_f64()) {
shaping = shaping.with_min_p(m).map_err(|e| e.to_string())?;
}
Ok(shaping)
}
fn build_config(request: &ChatCompletionRequest) -> Result<GenerationConfig, String> {
let shaping = build_shaping(
request.temperature,
request.top_p,
request.presence_penalty,
request.frequency_penalty,
&request.extra,
)?;
if request.max_tokens == Some(0) || request.max_completion_tokens == Some(0) {
return Err("max_tokens must be greater than 0".to_string());
}
if request.max_tokens == Some(0) || request.max_completion_tokens == Some(0) {
return Err("max_tokens must be greater than 0".to_string());
}
Ok(GenerationConfig {
shaping,
max_new_tokens: request
.max_tokens
.or(request.max_completion_tokens)
.unwrap_or(256),
stop_strings: stop_strings(request.stop.as_ref()),
extra_stop_tokens: Vec::new(),
rate: Default::default(),
})
}
fn visible_text(message: &ChatMessage) -> Option<String> {
let mut stripped = message.clone();
stripped.reasoning_content = None;
stripped.effective_text()
}
fn historical_call(call: &ToolCall) -> HistoricalToolCall {
HistoricalToolCall {
id: call.id.clone(),
name: call.function.name.clone(),
arguments: JsonValue::parse(&call.function.arguments)
.unwrap_or_else(|_| JsonValue::String(call.function.arguments.clone())),
}
}
fn to_message(message: &ChatMessage) -> Option<Message> {
let content = visible_text(message);
let tool_calls: Vec<HistoricalToolCall> = message
.tool_calls
.iter()
.flatten()
.map(historical_call)
.collect();
if content.is_none() && tool_calls.is_empty() {
return None;
}
Some(Message {
role: role_from(&message.role),
content,
content_parts: Vec::new(),
tool_calls,
tool_call_id: message.tool_call_id.clone(),
name: message.name.clone(),
})
}
fn with_images(message: Message, count: usize) -> Message {
let mut parts: Vec<ContentPart> = (0..count).map(|_| ContentPart::Image).collect();
if let Some(text) = message.content.clone() {
if !text.is_empty() {
parts.push(ContentPart::Text(text));
}
}
Message::with_parts(message.role, parts)
}
fn tool_definition(tool: &ChatTool) -> FunctionDefinition {
FunctionDefinition {
name: tool.function.name.clone(),
description: tool.function.description.clone().unwrap_or_default(),
parameters: tool
.function
.parameters
.as_ref()
.and_then(|p| JsonValue::parse(&p.to_string()).ok())
.unwrap_or(JsonValue::Null),
}
}
pub(crate) fn tool_names(request: &ChatCompletionRequest) -> HashSet<String> {
match &request.tool_choice {
Some(ChatToolChoice::Simple(s)) if s == "none" => HashSet::new(),
Some(ChatToolChoice::Named(n)) => {
let mut set = HashSet::new();
set.insert(n.function.name.clone());
set
}
_ => request
.tools
.iter()
.flatten()
.map(|t| t.function.name.clone())
.collect(),
}
}
pub(crate) fn reasoning_effort(
request: &ChatCompletionRequest,
default: ReasoningEffort,
) -> Result<ReasoningEffort, String> {
let Some(value) = request.extra.get("reasoning_effort") else {
return Ok(default);
};
let Some(text) = value.as_str() else {
return Err(format!("reasoning_effort must be a string, got {value}"));
};
ReasoningEffort::parse(text).ok_or_else(|| {
format!("unknown reasoning_effort {text:?}: expected off, low, medium, high or xhigh")
})
}
pub(crate) fn openai_request_warnings(request: &ChatCompletionRequest) -> Option<String> {
let mut warnings = anyllm_translate::TranslationWarnings::default();
if request
.response_format
.as_ref()
.is_some_and(|f| f.format_type != "text")
{
warnings.add("response_format");
}
if request.extra.get("n").and_then(|v| v.as_u64()).unwrap_or(1) > 1 {
warnings.add("n");
}
if request.extra.contains_key("logprobs") {
warnings.add("logprobs");
}
if request.extra.contains_key("top_logprobs") {
warnings.add("top_logprobs");
}
if request.extra.contains_key("logit_bias") {
warnings.add("logit_bias");
}
warnings.as_header_value()
}
pub(crate) fn merge_degradation(warnings: Option<String>, note: Option<String>) -> Option<String> {
match (&warnings, ¬e) {
(_, None) => warnings,
(None, Some(n)) => Some(n.clone()),
(Some(existing), Some(n)) => Some(format!("{existing}; {n}")),
}
}
pub(crate) struct Planned {
pub(crate) prompt_ids: Vec<foundation::TokenId>,
pub(crate) config: GenerationConfig,
pub(crate) images: Option<crate::vision::RequestImages>,
pub(crate) dropped_images: Option<String>,
}
pub(crate) fn plan(model: &AppState, request: &ChatCompletionRequest) -> Result<Planned, String> {
let mut messages: Vec<Message> = request.messages.iter().filter_map(to_message).collect();
let carries_images = request.messages.iter().any(crate::vision::has_images);
let vision_info = if carries_images { model.vision() } else { None };
let mut dropped_images = None;
let mut per_message: Vec<usize> = Vec::new();
let mut encoded: Vec<Vec<u8>> = Vec::new();
if carries_images {
let mut total_images = 0usize;
for message in &request.messages {
crate::vision::validate_urls(message)?;
total_images += crate::vision::image_count(message);
}
if total_images > crate::vision::MAX_IMAGES_PER_REQUEST {
return Err(format!(
"too many images: {total_images} exceeds the {}-image limit per request",
crate::vision::MAX_IMAGES_PER_REQUEST
));
}
match &vision_info {
Some(_) => {
for message in &request.messages {
let bytes = crate::vision::image_bytes(message)?;
per_message.push(bytes.len());
encoded.extend(bytes);
}
}
None => {
dropped_images = Some(
"this server's model has no vision tower, so image content parts were \
ignored; run it against an install whose checkpoint carries one"
.to_string(),
);
}
}
}
let is_none_choice =
matches!(&request.tool_choice, Some(ChatToolChoice::Simple(s)) if s == "none");
let tools: Vec<FunctionDefinition> = if is_none_choice {
Vec::new()
} else if let Some(ChatToolChoice::Named(n)) = &request.tool_choice {
request
.tools
.iter()
.flatten()
.filter(|t| t.function.name == n.function.name)
.map(tool_definition)
.collect()
} else {
request
.tools
.iter()
.flatten()
.map(tool_definition)
.collect()
};
if vision_info.is_some() && !per_message.is_empty() {
let mut counts = per_message.iter().copied();
let mut rebuilt = Vec::with_capacity(messages.len());
let mut planned = messages.into_iter();
for source in &request.messages {
let count = counts.next().unwrap_or(0);
match to_message(source) {
Some(_) => {
let message = planned.next().expect("one planned message per kept source");
rebuilt.push(if count > 0 {
with_images(message, count)
} else {
message
});
}
None if count > 0 => rebuilt.push(Message::with_parts(
role_from(&source.role),
(0..count).map(|_| ContentPart::Image).collect(),
)),
None => {}
}
}
if planned.next().is_some() {
return Err(
"internal: a message was inserted before the image splice, so images \
would attach to the wrong turn"
.to_string(),
);
}
messages = rebuilt;
}
if let Some(system) = model.default_system() {
let caller_sent_one = request
.messages
.iter()
.any(|m| matches!(m.role, ChatRole::System | ChatRole::Developer));
if !caller_sent_one {
messages.insert(0, Message::new(Role::System, system));
}
}
let reasoning = reasoning_effort(request, model.default_reasoning())?;
let mut prompt_ids = if tools.is_empty() {
let prompt = model
.tokenizer()
.apply_chat_template_with_reasoning(&messages, reasoning)
.map_err(|e| e.to_string())?;
model.tokenizer().encode(&prompt, false)
} else {
model
.tokenizer()
.encode_generic_tool_chat(&messages, &tools, reasoning)
.map_err(|e| e.to_string())?
};
let mut config = build_config(request)?;
config.rate = model.rate_control();
let images = match (&vision_info, encoded.is_empty()) {
(Some(info), false) => {
let grids = crate::vision::preflight_all(&encoded, info)?;
let spliced = turbospark_vision_io::splice_and_walk(
&prompt_ids,
&grids,
info.specials,
info.params.merge_size,
)
.map_err(|e| format!("cannot place {} image(s): {e}", grids.len()))?;
prompt_ids = spliced.ids;
let required = prompt_ids.len() as u64 + config.max_new_tokens as u64;
if required > model.max_context() as u64 {
return Err(format!(
"prompt needs {} tokens plus {} new tokens, exceeding the {}-token context",
prompt_ids.len(),
config.max_new_tokens,
model.max_context()
));
}
let preprocessed = crate::vision::preprocess_all(&encoded, info)?;
Some(crate::vision::RequestImages {
images: preprocessed,
positions: spliced.positions,
})
}
_ => None,
};
Ok(Planned {
prompt_ids,
config,
images,
dropped_images,
})
}