1use std::sync::Arc;
8use std::time::Instant;
9
10use axum::http::{HeaderMap, HeaderValue};
11use indexmap::IndexMap;
12use serde_json::Value;
13use sokuto_core::gating::{SystemRoutingSummary, evaluate_response_routing};
14use sokuto_core::schema::{Answer, QuestionType, SystemOneRequest, Usage};
15use sokuto_runtime::engine::StateCacheManager;
16use sokuto_runtime::hierarchical::CoarseToFineRouter;
17
18use crate::error::ServerError;
19use crate::metrics::{
20 record_confidence, record_gating_route, record_hierarchical_early_exit,
21 record_hierarchical_soft_beam, record_inference_metrics, record_question_type,
22};
23use crate::state::AppState;
24
25#[derive(Debug, Clone)]
27pub struct InferenceOutput {
28 pub answers: IndexMap<String, Answer>,
30 pub usage: Usage,
32 pub routing_summary: Option<SystemRoutingSummary>,
34 pub cache_status: &'static str,
36 pub cache_hit: bool,
38 pub state_key: Option<u64>,
40}
41
42impl InferenceOutput {
43 pub fn to_headers(&self) -> HeaderMap {
45 let mut headers = HeaderMap::new();
46 if let Some(key) = self.state_key
47 && let Ok(hv) = HeaderValue::from_str(&format!("{:016x}", key))
48 {
49 headers.insert("X-State-Hash", hv);
50 }
51 let cache_status_val = HeaderValue::from_static(self.cache_status);
52 headers.insert("X-Cache-Status", cache_status_val.clone());
53 headers.insert("X-State-Cache", cache_status_val.clone());
54 headers.insert("X-Cache", cache_status_val);
55 headers
56 }
57}
58
59#[derive(Clone)]
61pub struct InferenceService {
62 state: Arc<AppState>,
63}
64
65impl InferenceService {
66 pub fn new(state: Arc<AppState>) -> Self {
68 Self { state }
69 }
70
71 pub async fn execute(
83 &self,
84 mut req: SystemOneRequest,
85 client_hash_str: Option<String>,
86 ) -> Result<InferenceOutput, ServerError> {
87 let original_questions = req.questions.clone();
88
89 self.state.guardrail_pipeline.process(&mut req, None)?;
91
92 let question_count = req.questions.len();
93
94 for question in req.questions.values() {
96 match question.question_type {
97 QuestionType::Choice => record_question_type("choice"),
98 QuestionType::Score => record_question_type("score"),
99 QuestionType::Noul => record_question_type("noul"),
100 }
101 }
102
103 let state_text = match &req.state {
105 Value::String(s) => s.clone(),
106 Value::Null => String::new(),
107 other => other.to_string(),
108 };
109
110 let is_state_empty = state_text.trim().is_empty();
111
112 let (state_key, _state_cache_hit_initial) = if is_state_empty {
113 if let Some(ref h_str) = client_hash_str {
114 let key = u64::from_str_radix(h_str.trim().trim_start_matches("0x"), 16).map_err(
115 |e| ServerError::BadRequest(format!("無効な State ハッシュです: {e}。")),
116 )?;
117 let has_cache = self
118 .state
119 .cache_manager
120 .as_ref()
121 .map(|cm| cm.contains_key(key))
122 .unwrap_or(false);
123 if !has_cache {
124 return Err(ServerError::StateCacheMiss(
125 "指定された State ハッシュのキャッシュが見つかりません。State 本文を含めて再送してください。".to_string(),
126 ));
127 }
128 (Some(key), true)
129 } else {
130 return Err(ServerError::BadRequest(
131 "State 本文が空で、有効な State ハッシュも指定されていません。".to_string(),
132 ));
133 }
134 } else {
135 let key = StateCacheManager::compute_key(&state_text);
136 let has_cache = self
137 .state
138 .cache_manager
139 .as_ref()
140 .map(|cm| cm.contains_key(key))
141 .unwrap_or(false);
142 (Some(key), has_cache)
143 };
144
145 let engine = Arc::clone(&self.state.engine);
146 let dual_engine = self.state.dual_engine.clone();
147 let cache_manager = self.state.cache_manager.clone();
148 let tokenizer = Arc::clone(&self.state.tokenizer);
149 let calib_config = Arc::clone(&self.state.calib_config);
150 let coarse_config = self.state.coarse_config.clone();
151 let guardrail_pipeline = self.state.guardrail_pipeline.clone();
152 let chunk_size = self.state.chunk_size;
153 let questions = req.questions;
154 let gating_config = req
155 .gating
156 .unwrap_or_else(|| self.state.gating_config.clone());
157 let auto_hierarchical = req
158 .auto_hierarchical
159 .unwrap_or(self.state.auto_hierarchical);
160 let hierarchical_mapping = self.state.hierarchical_mapping.clone();
161 let hierarchical_config = self.state.hierarchical_config.clone();
162
163 let inference_start = Instant::now();
164
165 let (answers, prompt_tokens, routing_summary, route_records, cache_hit, cache_status_str) =
167 tokio::task::spawn_blocking(move || {
168 let state_tokens = if !is_state_empty {
169 tokenizer
170 .encode_state(&state_text)
171 .map(|toks| toks.len())
172 .unwrap_or(0)
173 } else {
174 0
175 };
176 let questions_tokens: usize = questions
177 .values()
178 .map(|q| {
179 sokuto_runtime::tokenizer::prompt::format_prompt(q)
180 .and_then(|f| {
181 tokenizer
182 .inner()
183 .encode(f.suffix.as_str(), false)
184 .map_err(|e| {
185 sokuto_runtime::error::RuntimeError::TokenizerEncodeError(
186 e.to_string(),
187 )
188 })
189 })
190 .map(|toks| toks.len())
191 .unwrap_or(0)
192 })
193 .sum();
194 let total_prompt_tokens = state_tokens + questions_tokens;
195
196 let has_hierarchical_target = if let Some(ref mapping) = hierarchical_mapping {
198 questions.values().any(|q| {
199 q.question_type == QuestionType::Choice
200 && q.criteria
201 .as_ref()
202 .and_then(|c| c.as_map())
203 .map(|m| {
204 let k = m.len();
205 (auto_hierarchical && k > 8 || k > 16)
206 && m.keys()
207 .any(|key| mapping.fine_to_coarse.contains_key(key))
208 })
209 .unwrap_or(false)
210 })
211 } else {
212 false
213 };
214
215 let (mut answers, cache_hit, status_str) = if let (Some(de), Some(cm), Some(key)) =
216 (&dual_engine, &cache_manager, state_key)
217 {
218 let (cached_state, hit, status) = if let Some(cached) = cm.get(key) {
219 (cached, true, "HIT")
220 } else {
221 let (s_ids, s_mask) = tokenizer.encode_state_tensor(&state_text)?;
222 let cached = de.encode_and_cache_state(
223 &state_text,
224 &s_ids,
225 &s_mask,
226 [1, s_ids.len()],
227 )?;
228 (cached, false, "MISS")
229 };
230
231 if has_hierarchical_target {
232 let mapping = hierarchical_mapping
233 .as_ref()
234 .expect("マッピングが存在すること。");
235 let router = CoarseToFineRouter::new_dual(
236 de,
237 &cached_state,
238 &tokenizer,
239 mapping.as_ref(),
240 &calib_config,
241 hierarchical_config,
242 )?;
243
244 let mut combined_answers =
245 indexmap::IndexMap::with_capacity(questions.len());
246 let mut fallback_questions = indexmap::IndexMap::new();
247
248 let effective_state = if is_state_empty {
249 cached_state.state_text.as_deref().unwrap_or(&state_text)
250 } else {
251 &state_text
252 };
253
254 for (qid, question) in &questions {
255 let is_hierarchical = question.question_type == QuestionType::Choice
256 && question
257 .criteria
258 .as_ref()
259 .and_then(|c| c.as_map())
260 .map(|m| {
261 let k = m.len();
262 (auto_hierarchical && k > 8 || k > 16)
263 && m.keys()
264 .any(|key| mapping.fine_to_coarse.contains_key(key))
265 })
266 .unwrap_or(false);
267
268 if is_hierarchical {
269 let res = router.route(
270 effective_state,
271 &question.instructions,
272 Some(&gating_config),
273 )?;
274 if res.trace().soft_beam_triggered {
275 record_hierarchical_soft_beam();
276 }
277 if res.trace().early_exit_triggered {
278 record_hierarchical_early_exit();
279 }
280 combined_answers.insert(qid.clone(), res.into_answer());
281 } else {
282 fallback_questions.insert(qid.clone(), question.clone());
283 }
284 }
285
286 if !fallback_questions.is_empty() {
287 let fallback_answers = de.evaluate_batch_questions_with_cache(
288 &tokenizer,
289 &cached_state,
290 if is_state_empty {
291 None
292 } else {
293 Some(&state_text)
294 },
295 &fallback_questions,
296 &calib_config,
297 Some(&gating_config),
298 chunk_size,
299 )?;
300 for (qid, answer) in fallback_answers {
301 combined_answers.insert(qid, answer);
302 }
303 }
304
305 (combined_answers, hit, status)
306 } else {
307 let ans = de.evaluate_batch_questions_with_cache(
308 &tokenizer,
309 &cached_state,
310 if is_state_empty {
311 None
312 } else {
313 Some(&state_text)
314 },
315 &questions,
316 &calib_config,
317 Some(&gating_config),
318 chunk_size,
319 )?;
320
321 (ans, hit, status)
322 }
323 } else if has_hierarchical_target {
324 let mapping = hierarchical_mapping
325 .as_ref()
326 .expect("マッピングが存在すること。");
327 let router = CoarseToFineRouter::new(
328 &engine,
329 &tokenizer,
330 mapping.as_ref(),
331 &calib_config,
332 hierarchical_config,
333 )?;
334
335 let mut combined_answers = indexmap::IndexMap::with_capacity(questions.len());
336 let mut fallback_questions = indexmap::IndexMap::new();
337
338 for (qid, question) in &questions {
339 let is_hierarchical = question.question_type == QuestionType::Choice
340 && question
341 .criteria
342 .as_ref()
343 .and_then(|c| c.as_map())
344 .map(|m| {
345 let k = m.len();
346 (auto_hierarchical && k > 8 || k > 16)
347 && m.keys()
348 .any(|key| mapping.fine_to_coarse.contains_key(key))
349 })
350 .unwrap_or(false);
351
352 if is_hierarchical {
353 let res = router.route(
354 &state_text,
355 &question.instructions,
356 Some(&gating_config),
357 )?;
358 if res.trace().soft_beam_triggered {
359 record_hierarchical_soft_beam();
360 }
361 if res.trace().early_exit_triggered {
362 record_hierarchical_early_exit();
363 }
364 combined_answers.insert(qid.clone(), res.into_answer());
365 } else {
366 fallback_questions.insert(qid.clone(), question.clone());
367 }
368 }
369
370 if !fallback_questions.is_empty() {
371 let fallback_answers = engine
372 .evaluate_batch_questions_coarse_to_fine_chunked_with_gating(
373 &tokenizer,
374 &state_text,
375 &fallback_questions,
376 &calib_config,
377 &coarse_config,
378 chunk_size,
379 Some(&gating_config),
380 )?;
381 for (qid, answer) in fallback_answers {
382 combined_answers.insert(qid, answer);
383 }
384 }
385
386 (combined_answers, false, "BYPASS")
387 } else {
388 let ans = engine.evaluate_batch_questions_coarse_to_fine_chunked_with_gating(
389 &tokenizer,
390 &state_text,
391 &questions,
392 &calib_config,
393 &coarse_config,
394 chunk_size,
395 Some(&gating_config),
396 )?;
397 (ans, false, "BYPASS")
398 };
399
400 guardrail_pipeline.post_process(&mut answers, &original_questions);
402
403 let (routing_summary, route_records) = if gating_config.enabled {
405 let mut records = Vec::with_capacity(answers.len());
406 for (qid, answer) in answers.iter() {
407 let q_def = questions.get(qid);
408 let q_type_str = q_def
409 .map(|q| match q.question_type {
410 QuestionType::Choice => "choice",
411 QuestionType::Score => "score",
412 QuestionType::Noul => "noul",
413 })
414 .unwrap_or("unknown");
415
416 if let Some(ref meta) = answer.gating {
417 records.push((
418 meta.route.as_str(),
419 q_type_str,
420 qid.clone(),
421 meta.route,
422 meta.confidence,
423 meta.reason.clone(),
424 ));
425 }
426 }
427 let summary = evaluate_response_routing(&answers);
428 (Some(summary), records)
429 } else {
430 (None, Vec::new())
431 };
432
433 Ok::<_, ServerError>((
434 answers,
435 total_prompt_tokens,
436 routing_summary,
437 route_records,
438 cache_hit,
439 status_str,
440 ))
441 })
442 .await
443 .map_err(|join_err| {
444 ServerError::Internal(format!(
445 "ブロッキング推論タスクが異常終了しました: {join_err}。"
446 ))
447 })??;
448
449 let inference_duration = inference_start.elapsed().as_secs_f64();
450
451 record_inference_metrics(inference_duration, question_count);
453
454 for answer in answers.values() {
455 if let Some(conf) = answer.effective_confidence() {
456 record_confidence(conf);
457 }
458 }
459
460 for (route_str, q_type_str, qid, route, confidence, reason) in route_records {
461 record_gating_route(route_str, q_type_str);
462 tracing::info!(
463 question_id = %qid,
464 route = %route,
465 confidence = confidence,
466 reason = %reason,
467 "確信度ゲーティング判定を適用しました。"
468 );
469 }
470
471 let usage = Usage::new(prompt_tokens);
472
473 Ok(InferenceOutput {
474 answers,
475 usage,
476 routing_summary,
477 cache_status: cache_status_str,
478 cache_hit,
479 state_key,
480 })
481 }
482}