1use axum::{extract::State, response::IntoResponse, Json};
8use std::collections::HashMap;
9use std::sync::Arc;
10
11use crate::api_types::{
12 ChoiceLogprobs, ExtendedChatRequest, ExtendedChatResponse, ExtendedChoice, UsageInfo,
13};
14use crate::engine::InferenceEngine;
15use crate::sampling::SamplingParams;
16use crate::server::{AppState, ChatMessage};
17
18pub async fn extended_chat_completions(
26 State(state): State<Arc<AppState>>,
27 Json(req): Json<ExtendedChatRequest>,
28) -> impl IntoResponse {
29 let n = req.n.unwrap_or(1).clamp(1, 4);
30 let max_tokens = req.max_tokens;
31 let temperature = req.temperature.unwrap_or(0.7);
32 let seed = req.seed.unwrap_or(42);
33 let want_logprobs = req.logprobs.unwrap_or(false);
34 let top_logprobs_k = req.top_logprobs.unwrap_or(0).clamp(0, 20);
35 let response_format = req.response_format.clone();
36 let tools = req.tools.clone();
37 let frequency_penalty = req.frequency_penalty.unwrap_or(0.0);
38 let presence_penalty = req.presence_penalty.unwrap_or(0.0);
39
40 let stop_checker = match req.stop {
42 Some(ref seqs) => StopChecker::new(seqs.as_slice().to_vec()),
43 None => StopChecker::new(vec![]),
44 };
45
46 let prompt_text = build_extended_prompt(&req.messages);
48
49 let prompt_tokens = {
51 let tokenizer = state.tokenizer();
52 if let Some(tok) = tokenizer {
53 match tok.encode(&prompt_text) {
54 Ok(tokens) => tokens,
55 Err(e) => {
56 tracing::error!(error = %e, "tokenization failed");
57 return (
58 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
59 Json(serde_json::json!({"error": "tokenization failed"})),
60 )
61 .into_response();
62 }
63 }
64 } else {
65 vec![151644u32]
66 }
67 };
68
69 let prompt_len = prompt_tokens.len();
70
71 let sampling_params = SamplingParams {
73 temperature,
74 top_k: 40,
75 top_p: req.top_p.unwrap_or(0.9),
76 repetition_penalty: 1.1,
77 ..SamplingParams::default()
78 };
79
80 let mut engine = match state.acquire_engine().await {
83 Ok(lease) => lease,
84 Err(e) => {
85 tracing::error!(error = %e, "engine pool acquire failed");
86 return (
87 axum::http::StatusCode::SERVICE_UNAVAILABLE,
88 Json(serde_json::json!({"error": "engine pool unavailable"})),
89 )
90 .into_response();
91 }
92 };
93
94 let raw_completions: Vec<String> = {
95 let mut results = Vec::with_capacity(n);
96 for i in 0..n {
97 let run_seed = seed.wrapping_add(i as u64);
98 engine.reset();
99
100 let output_tokens = match engine.generate_with_seed(
101 &prompt_tokens,
102 max_tokens,
103 run_seed,
104 &sampling_params,
105 ) {
106 Ok(toks) => toks,
107 Err(e) => {
108 tracing::error!(error = %e, "generation failed for completion {i}");
109 return (
110 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
111 Json(serde_json::json!({"error": "generation failed"})),
112 )
113 .into_response();
114 }
115 };
116
117 let _ = frequency_penalty;
120 let _ = presence_penalty;
121
122 let text = if let Some(tok) = state.tokenizer() {
124 tok.decode(&output_tokens)
125 .unwrap_or_else(|_| format!("{output_tokens:?}"))
126 } else {
127 format!("{output_tokens:?}")
128 };
129
130 results.push(text);
131 }
132 results
133 };
134
135 let json_enforcer = JsonModeEnforcer::new();
137 let is_json_mode = response_format
138 .as_ref()
139 .map(|rf| rf.format_type == "json_object" || rf.format_type == "json_schema")
140 .unwrap_or(false);
141
142 let total_completion_tokens: usize;
143 let choices: Vec<ExtendedChoice> = {
144 let mut comp_tokens = 0usize;
145 let choices_out: Vec<ExtendedChoice> = raw_completions
146 .into_iter()
147 .enumerate()
148 .map(|(idx, raw_text)| {
149 let (truncated, hit_stop) = stop_checker.truncate_at_stop(&raw_text);
150 let finish_reason = "stop".to_string();
151 let _ = hit_stop;
152
153 let final_text = if is_json_mode {
155 json_enforcer.enforce(&truncated)
156 } else {
157 truncated.clone()
158 };
159
160 let tool_calls = if tools.is_some() {
162 let call_id = crate::api_types::generate_tool_call_id();
163 crate::api_types::parse_tool_call(&final_text, &call_id).map(|tc| vec![tc])
164 } else {
165 None
166 };
167
168 let logprobs = if want_logprobs && top_logprobs_k > 0 {
170 Some(ChoiceLogprobs {
172 content: Some(vec![]),
173 })
174 } else if want_logprobs {
175 Some(ChoiceLogprobs {
176 content: Some(vec![]),
177 })
178 } else {
179 None
180 };
181
182 let approx_tokens = final_text.split_whitespace().count().max(1);
184 comp_tokens += approx_tokens;
185
186 ExtendedChoice {
187 index: idx,
188 message: ChatMessage {
189 role: "assistant".to_string(),
190 content: Some(final_text),
191 tool_calls: None,
192 tool_call_id: None,
193 },
194 finish_reason,
195 logprobs,
196 tool_calls,
197 }
198 })
199 .collect();
200 total_completion_tokens = comp_tokens;
201 choices_out
202 };
203
204 let system_fingerprint = Some(crate::api_types::fingerprint_from_config("bonsai-8b"));
206
207 let created = std::time::SystemTime::now()
208 .duration_since(std::time::UNIX_EPOCH)
209 .unwrap_or_default()
210 .as_secs();
211
212 let response = ExtendedChatResponse {
213 id: format!("chatcmpl-ext-{}", rand_ext_id()),
214 object: "chat.completion".to_string(),
215 created,
216 model: "bonsai-8b".to_string(),
217 choices,
218 usage: UsageInfo {
219 prompt_tokens: prompt_len,
220 completion_tokens: total_completion_tokens,
221 total_tokens: prompt_len + total_completion_tokens,
222 },
223 system_fingerprint,
224 };
225
226 Json(response).into_response()
227}
228
229fn build_extended_prompt(messages: &[ChatMessage]) -> String {
233 let mut prompt = String::new();
234 for msg in messages {
235 let text = match msg.content.as_deref() {
236 Some(t) => t,
237 None => continue,
238 };
239 match msg.role.as_str() {
240 "system" => {
241 prompt.push_str("<|im_start|>system\n");
242 prompt.push_str(text);
243 prompt.push_str("<|im_end|>\n");
244 }
245 "user" => {
246 prompt.push_str("<|im_start|>user\n");
247 prompt.push_str(text);
248 prompt.push_str("<|im_end|>\n");
249 }
250 "assistant" => {
251 prompt.push_str("<|im_start|>assistant\n");
252 prompt.push_str(text);
253 prompt.push_str("<|im_end|>\n");
254 }
255 _ => {
256 prompt.push_str(text);
257 prompt.push('\n');
258 }
259 }
260 }
261 prompt.push_str("<|im_start|>assistant\n");
262 prompt
263}
264
265fn rand_ext_id() -> String {
266 let ts = std::time::SystemTime::now()
267 .duration_since(std::time::UNIX_EPOCH)
268 .unwrap_or_default()
269 .as_nanos();
270 format!("{ts:x}")
271}
272
273pub struct JsonModeEnforcer {
282 pub max_retries: usize,
284}
285
286impl JsonModeEnforcer {
287 pub fn new() -> Self {
289 Self { max_retries: 3 }
290 }
291
292 pub fn enforce(&self, text: &str) -> String {
295 if crate::api_types::is_valid_json(text) {
297 return text.to_string();
298 }
299
300 if let Some(extracted) = extract_json_substring(text) {
302 if crate::api_types::is_valid_json(&extracted) {
303 return extracted;
304 }
305 }
306
307 let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
309 format!(r#"{{"response": "{escaped}"}}"#)
310 }
311}
312
313impl Default for JsonModeEnforcer {
314 fn default() -> Self {
315 Self::new()
316 }
317}
318
319fn extract_json_substring(text: &str) -> Option<String> {
321 if let Some(obj) = extract_balanced(text, '{', '}') {
323 return Some(obj);
324 }
325 if let Some(arr) = extract_balanced(text, '[', ']') {
327 return Some(arr);
328 }
329 None
330}
331
332fn extract_balanced(text: &str, open: char, close: char) -> Option<String> {
335 let start = text.find(open)?;
336 let substr = &text[start..];
337 let mut depth = 0i32;
338 let mut end_idx = None;
339
340 for (i, ch) in substr.char_indices() {
341 if ch == open {
342 depth += 1;
343 } else if ch == close {
344 depth -= 1;
345 if depth == 0 {
346 end_idx = Some(i + ch.len_utf8());
347 break;
348 }
349 }
350 }
351
352 end_idx.map(|e| substr[..e].to_string())
353}
354
355pub struct StopChecker {
359 sequences: Vec<String>,
360}
361
362impl StopChecker {
363 pub fn new(sequences: Vec<String>) -> Self {
365 Self { sequences }
366 }
367
368 pub fn check<'a>(&'a self, text: &str) -> Option<&'a str> {
370 for seq in &self.sequences {
371 if text.contains(seq.as_str()) {
372 return Some(seq.as_str());
373 }
374 }
375 None
376 }
377
378 pub fn truncate_at_stop(&self, text: &str) -> (String, bool) {
382 let mut earliest: Option<(usize, &str)> = None;
383 for seq in &self.sequences {
384 if let Some(pos) = text.find(seq.as_str()) {
385 match earliest {
386 None => earliest = Some((pos, seq.as_str())),
387 Some((prev_pos, _)) if pos < prev_pos => {
388 earliest = Some((pos, seq.as_str()));
389 }
390 _ => {}
391 }
392 }
393 }
394
395 match earliest {
396 Some((pos, _)) => (text[..pos].to_string(), true),
397 None => (text.to_string(), false),
398 }
399 }
400
401 pub fn is_empty(&self) -> bool {
403 self.sequences.is_empty()
404 }
405}
406
407pub fn generate_n_completions(
414 engine: &mut InferenceEngine<'_>,
415 prompt: &str,
416 params: &SamplingParams,
417 n: usize,
418 base_seed: u64,
419) -> Vec<String> {
420 let prompt_tokens: Vec<u32> = {
421 prompt
423 .split_whitespace()
424 .enumerate()
425 .map(|(i, _)| (i as u32).wrapping_add(1000))
426 .collect()
427 };
428
429 let mut results = Vec::with_capacity(n);
430 for i in 0..n {
431 engine.reset();
432 let seed = base_seed.wrapping_add(i as u64);
433 let text = engine
434 .generate_with_seed(&prompt_tokens, 64, seed, params)
435 .map(|toks| format!("{toks:?}"))
436 .unwrap_or_else(|_| String::new());
437 results.push(text);
438 }
439 results
440}
441
442pub fn apply_frequency_penalty(
450 logits: &mut [f32],
451 token_counts: &HashMap<u32, usize>,
452 frequency_penalty: f32,
453 presence_penalty: f32,
454) {
455 for (&token_id, &count) in token_counts {
456 if let Some(logit) = logits.get_mut(token_id as usize) {
457 *logit -= frequency_penalty * count as f32;
458 *logit -= presence_penalty;
459 }
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 #[test]
468 fn json_mode_enforcer_valid_passthrough() {
469 let enforcer = JsonModeEnforcer::new();
470 let json = r#"{"key": "value"}"#;
471 assert_eq!(enforcer.enforce(json), json);
472 }
473
474 #[test]
475 fn json_mode_enforcer_extracts_substring() {
476 let enforcer = JsonModeEnforcer::new();
477 let text = r#"Here is some text {"key": "value"} and more"#;
478 let result = enforcer.enforce(text);
479 assert!(
480 crate::api_types::is_valid_json(&result),
481 "result should be valid JSON, got: {result}"
482 );
483 }
484
485 #[test]
486 fn json_mode_enforcer_wraps_invalid() {
487 let enforcer = JsonModeEnforcer::new();
488 let text = "not json at all";
489 let result = enforcer.enforce(text);
490 assert!(
491 crate::api_types::is_valid_json(&result),
492 "result should be valid JSON, got: {result}"
493 );
494 let v: serde_json::Value = serde_json::from_str(&result).expect("should parse as json");
495 assert!(v.get("response").is_some(), "should have 'response' key");
496 }
497
498 #[test]
499 fn stop_checker_finds_sequence() {
500 let checker = StopChecker::new(vec!["STOP".to_string(), "END".to_string()]);
501 assert_eq!(checker.check("Hello STOP world"), Some("STOP"));
502 assert_eq!(checker.check("No match here"), None);
503 }
504
505 #[test]
506 fn stop_checker_truncates_correctly() {
507 let checker = StopChecker::new(vec!["<end>".to_string()]);
508 let (truncated, hit) = checker.truncate_at_stop("Hello world<end>more text");
509 assert_eq!(truncated, "Hello world");
510 assert!(hit);
511 }
512
513 #[test]
514 fn stop_checker_no_match() {
515 let checker = StopChecker::new(vec!["nope".to_string()]);
516 let (truncated, hit) = checker.truncate_at_stop("Hello world");
517 assert_eq!(truncated, "Hello world");
518 assert!(!hit);
519 }
520
521 #[test]
522 fn stop_checker_is_empty() {
523 let empty = StopChecker::new(vec![]);
524 assert!(empty.is_empty());
525 let non_empty = StopChecker::new(vec!["x".to_string()]);
526 assert!(!non_empty.is_empty());
527 }
528
529 #[test]
530 fn apply_frequency_penalty_reduces_seen() {
531 let mut logits = vec![1.0f32, 2.0, 3.0];
532 let mut counts = HashMap::new();
533 counts.insert(1u32, 2usize); apply_frequency_penalty(&mut logits, &counts, 0.5, 0.0);
535 assert!(
537 (logits[1] - 1.0).abs() < 1e-5,
538 "expected 1.0, got {}",
539 logits[1]
540 );
541 assert!((logits[0] - 1.0).abs() < 1e-5);
543 assert!((logits[2] - 3.0).abs() < 1e-5);
544 }
545
546 #[test]
547 fn apply_presence_penalty_reduces_seen() {
548 let mut logits = vec![1.0f32, 2.0, 3.0];
549 let mut counts = HashMap::new();
550 counts.insert(0u32, 1usize);
551 apply_frequency_penalty(&mut logits, &counts, 0.0, 1.0);
552 assert!(
553 (logits[0] - 0.0).abs() < 1e-5,
554 "expected 0.0, got {}",
555 logits[0]
556 );
557 assert!((logits[1] - 2.0).abs() < 1e-5);
558 }
559
560 #[test]
561 fn extract_balanced_object() {
562 let text = r#"prefix {"a":1} suffix"#;
563 let result = extract_balanced(text, '{', '}');
564 assert_eq!(result.as_deref(), Some(r#"{"a":1}"#));
565 }
566
567 #[test]
568 fn extract_balanced_array() {
569 let text = r#"pre [1,2,3] post"#;
570 let result = extract_balanced(text, '[', ']');
571 assert_eq!(result.as_deref(), Some("[1,2,3]"));
572 }
573}