1use std::sync::Arc;
4
5use anyhow::{bail, Context, Result};
6use axum::{
7 body::Bytes,
8 extract::State,
9 http::{header::AUTHORIZATION, HeaderMap, StatusCode},
10 response::{IntoResponse, Response},
11 routing::{get, post},
12 Json, Router,
13};
14use serde::Serialize;
15use serde_json::{json, Value};
16use tokio::sync::Mutex;
17
18use crate::prompt::{
19 binary_messages, direct_messages, direct_messages_reusing, evidence_json, prompt_sha256,
20 softmax, BinarySlots, ChatTemplate, LocalComponents, RuntimeClient, BINARY_PROMPT_VERSION,
21 LETTERS, PROMPT_VERSION,
22};
23use crate::strategy::{
24 build_body_with_candidates, candidate_ladder, candidates_restricted_to, input_tokens,
25 parse_batch_candidates, parse_candidates, parse_logprobs, request_url,
26 slot_logprobs_from_candidates, truncated, Candidate, CompletionResponse, Probe,
27};
28use crate::wire::{
29 request_batch, response_from_results, ConfidenceMethod, OrderedMap, Question, ReadoutStatus,
30 Row, ScoredAnswer, PROBABILITY_STATUS,
31};
32use std::sync::atomic::{AtomicU64, Ordering};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
36pub enum Scoring {
37 #[default]
40 Letters,
41 Binary,
44}
45
46pub struct Bridge {
48 client: reqwest::Client,
49 template: ChatTemplate,
50 probe: Probe,
51 upstream_model: String,
52 upstream_key: Option<String>,
53 openai_base: String,
54 native_base: String,
55 slots: Vec<u32>,
56 scoring: Scoring,
58 binary_slots: Option<BinarySlots>,
60 confidence: ConfidenceMethod,
62 served_model: String,
63 description: String,
64 release_date: String,
65 max_input_tokens: Option<usize>,
66 accept_any_model: bool,
68 readout: ReadoutStatus,
70 batch_fallbacks: AtomicU64,
74 lock: Mutex<()>,
76}
77
78struct Readout {
80 logprobs: Vec<f64>,
82 input_tokens: u64,
87}
88
89#[derive(Debug)]
97pub struct UpstreamStatus {
98 pub url: String,
100 pub status: u16,
102 pub body: String,
104}
105
106impl UpstreamStatus {
107 pub fn is_shape_rejection(&self) -> bool {
110 matches!(self.status, 400 | 404 | 405 | 415 | 422)
111 }
112}
113
114impl std::fmt::Display for UpstreamStatus {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 write!(f, "POST {} returned HTTP {}: {}", self.url, self.status, self.body)
117 }
118}
119
120impl std::error::Error for UpstreamStatus {}
121
122pub struct BridgeConfig {
124 pub openai_base: String,
126 pub native_base: String,
128 pub upstream_model: String,
130 pub upstream_key: Option<String>,
132 pub served_model: String,
134 pub description: String,
136 pub release_date: String,
138 pub chat_template_kwargs: Value,
140 pub max_input_tokens: Option<usize>,
142 pub local: LocalComponents,
145 pub accept_any_model: bool,
154 pub readout: ReadoutStatus,
160 pub scoring: Scoring,
162 pub confidence: ConfidenceMethod,
164}
165
166#[derive(Debug, Clone)]
169pub struct DetailedScore {
170 pub answer: ScoredAnswer,
172 pub option_logprobs: Vec<f64>,
174 pub prompt_sha256: String,
176}
177
178impl Bridge {
179 pub async fn connect(client: reqwest::Client, mut config: BridgeConfig) -> Result<Self> {
181 let runtime = RuntimeClient::new(
182 client.clone(),
183 config.native_base.clone(),
184 config.upstream_model.clone(),
185 config.upstream_key.clone(),
186 );
187 let template = ChatTemplate::new(
188 runtime,
189 std::mem::take(&mut config.local),
190 config.chat_template_kwargs.clone(),
191 );
192
193 let slots = template
194 .resolve_slots()
195 .await
196 .context("resolving the single-token answer slots failed")?;
197 let binary_slots = if config.scoring == Scoring::Binary {
198 Some(
199 template
200 .resolve_binary_slots()
201 .await
202 .context("resolving the yes/no answer slots failed")?,
203 )
204 } else {
205 None
206 };
207
208 let probe_options: Vec<crate::wire::RowOption> = LETTERS
211 .chars()
212 .map(|letter| crate::wire::RowOption {
213 id: letter.to_string(),
214 description: format!("Probe option {letter}"),
215 })
216 .collect();
217 let messages = direct_messages(&json!("probe"), "probe", &probe_options);
218 let prompt = template
219 .render(&messages)
220 .await
221 .context("rendering the probe prompt failed")?;
222 template
223 .verify_boundary(&prompt, &slots)
224 .await
225 .context("verifying the answer-token boundary failed")?;
226
227 let letters: Vec<char> = LETTERS.chars().collect();
228 let mut chosen = None;
229 let mut failures = Vec::new();
230 for probe in crate::strategy::CANDIDATES {
231 match try_probe(
232 &client,
233 probe,
234 &config,
235 &prompt,
236 &slots,
237 &letters,
238 )
239 .await
240 {
241 Ok(()) => {
242 chosen = Some(probe);
243 break;
244 }
245 Err(error) => failures.push(format!("{}: {error:#}", probe.name())),
246 }
247 }
248 let probe = chosen.ok_or_else(|| {
249 anyhow::anyhow!(
250 "no supported transport answered the probe:\n {}",
251 failures.join("\n ")
252 )
253 })?;
254
255 Ok(Self {
256 client,
257 template,
258 probe,
259 upstream_model: config.upstream_model,
260 upstream_key: config.upstream_key,
261 openai_base: config.openai_base,
262 native_base: config.native_base,
263 slots,
264 scoring: config.scoring,
265 binary_slots,
266 confidence: config.confidence,
267 served_model: config.served_model,
268 description: config.description,
269 release_date: config.release_date,
270 max_input_tokens: config.max_input_tokens,
271 accept_any_model: config.accept_any_model,
272 readout: config.readout,
273 batch_fallbacks: AtomicU64::new(0),
274 lock: Mutex::new(()),
275 })
276 }
277
278 pub fn probe(&self) -> Probe {
280 self.probe
281 }
282
283 pub fn readout(&self) -> &ReadoutStatus {
285 &self.readout
286 }
287
288 pub fn accepts_any_model(&self) -> bool {
291 self.accept_any_model
292 }
293
294 pub fn batch_fallbacks(&self) -> u64 {
297 self.batch_fallbacks.load(Ordering::Relaxed)
298 }
299
300 pub async fn run_readout_check(&self) -> Result<crate::readout::ReadoutCheck> {
308 let questions = crate::readout::questions();
309 let scored = self
310 .score_questions(&json!(crate::readout::PROBE_STATE), &questions)
311 .await
312 .context("the readout self-check could not be scored")?;
313 let results = crate::readout::PROBES
314 .iter()
315 .zip(&scored)
316 .map(|(probe, score)| {
317 crate::readout::outcome(probe, &score.answer.option_ids, &score.answer.probabilities)
318 })
319 .collect();
320 Ok(crate::readout::ReadoutCheck::from_results(results))
321 }
322
323 pub fn slots(&self) -> &[u32] {
325 &self.slots
326 }
327
328 pub fn confidence_method(&self) -> ConfidenceMethod {
330 self.confidence
331 }
332
333 pub fn upstream_model(&self) -> &str {
335 &self.upstream_model
336 }
337
338 pub fn chat_template_kwargs(&self) -> Value {
340 self.template.chat_template_kwargs()
341 }
342
343 pub fn renders_locally(&self) -> bool {
345 self.template.renders_locally()
346 }
347
348 pub fn tokenizes_locally(&self) -> bool {
350 self.template.tokenizes_locally()
351 }
352
353 pub async fn score_row(&self, row: &Row) -> Result<DetailedScore> {
355 let question = Question {
356 id: row.id.clone(),
357 question: row.question.clone(),
358 options: row.options.clone(),
359 };
360 let mut scored = self.score_questions(&row.state, &[question]).await?;
361 Ok(scored.remove(0))
362 }
363
364 pub async fn score_questions(
374 &self,
375 state: &Value,
376 questions: &[Question],
377 ) -> Result<Vec<DetailedScore>> {
378 let _guard = self.lock.lock().await;
379 if self.scoring == Scoring::Binary {
380 return self.score_binary(state, questions).await;
381 }
382
383 let evidence = evidence_json(state);
386 let mut prompts = Vec::with_capacity(questions.len());
387 for question in questions {
388 let messages =
389 direct_messages_reusing(&evidence, &question.question, &question.options);
390 let prompt = self
391 .template
392 .render(&messages)
393 .await
394 .with_context(|| format!("question {:?}", question.id))?;
395 prompts.push(prompt);
396 }
397
398 let readouts = if questions.len() > 1 && self.probe.supports_batch() {
399 match self.read_batch(questions, &prompts).await {
400 Ok(readouts) => readouts,
401 Err(error) => {
402 if error
406 .downcast_ref::<UpstreamStatus>()
407 .is_some_and(UpstreamStatus::is_shape_rejection)
408 {
409 self.batch_fallbacks.fetch_add(1, Ordering::Relaxed);
410 eprintln!(
411 "the upstream rejected the batched readout shape ({error}); falling \
412 back to {} sequential requests (no shared prefill on this endpoint)",
413 questions.len()
414 );
415 self.read_one_by_one(questions, &prompts).await?
416 } else {
417 return Err(error);
418 }
419 }
420 }
421 } else {
422 self.read_one_by_one(questions, &prompts).await?
423 };
424
425 Ok(readouts
427 .into_iter()
428 .zip(questions)
429 .zip(&prompts)
430 .map(|((readout, question), prompt)| DetailedScore {
431 answer: ScoredAnswer {
432 id: question.id.clone(),
433 option_ids: question
434 .options
435 .iter()
436 .map(|option| option.id.clone())
437 .collect(),
438 probabilities: softmax(&readout.logprobs),
439 input_tokens: readout.input_tokens,
440 prompt_version: Some(PROMPT_VERSION.to_string()),
441 },
442 option_logprobs: readout.logprobs,
443 prompt_sha256: prompt_sha256(prompt),
444 })
445 .collect())
446 }
447
448 async fn score_binary(
457 &self,
458 state: &Value,
459 questions: &[Question],
460 ) -> Result<Vec<DetailedScore>> {
461 let evidence = evidence_json(state);
462 let mut prompts = Vec::new();
463 let mut owner = Vec::new();
464 for (question_index, question) in questions.iter().enumerate() {
465 for (option_index, option) in question.options.iter().enumerate() {
466 let messages = binary_messages(&evidence, &question.question, option);
467 let prompt = self
468 .template
469 .render(&messages)
470 .await
471 .with_context(|| format!("question {:?} option {:?}", question.id, option.id))?;
472 prompts.push(prompt);
473 owner.push((question_index, option_index));
474 }
475 }
476
477 let (yes, input_tokens) = self.read_binary(&prompts).await?;
478 let mut raw: Vec<Vec<f64>> = questions
479 .iter()
480 .map(|question| vec![0.0; question.options.len()])
481 .collect();
482 for ((question_index, option_index), probability) in owner.into_iter().zip(&yes) {
483 raw[question_index][option_index] = *probability;
484 }
485 let prompt_sha256 = prompt_sha256(&prompts.join("\n"));
486
487 Ok(questions
488 .iter()
489 .zip(raw)
490 .map(|(question, raw)| {
491 let total: f64 = raw.iter().sum();
492 let probabilities: Vec<f64> = if total > 0.0 {
493 raw.iter().map(|value| value / total).collect()
494 } else {
495 vec![1.0 / raw.len() as f64; raw.len()]
496 };
497 DetailedScore {
498 answer: ScoredAnswer {
499 id: question.id.clone(),
500 option_ids: question
501 .options
502 .iter()
503 .map(|option| option.id.clone())
504 .collect(),
505 probabilities,
506 input_tokens,
507 prompt_version: Some(BINARY_PROMPT_VERSION.to_string()),
508 },
509 option_logprobs: raw,
510 prompt_sha256: prompt_sha256.clone(),
511 }
512 })
513 .collect())
514 }
515
516 async fn read_binary(&self, prompts: &[String]) -> Result<(Vec<f64>, u64)> {
523 let slots = self
524 .binary_slots
525 .as_ref()
526 .context("binary slots were not resolved for this bridge")?;
527 let mut yes = Vec::with_capacity(prompts.len());
528 let mut input_tokens = 0;
529 for prompt in prompts {
530 let (probability, tokens) = self.read_binary_one(prompt, slots).await?;
531 yes.push(probability);
532 input_tokens += tokens;
533 }
534 Ok((yes, input_tokens))
535 }
536
537 async fn read_binary_one(&self, prompt: &str, slots: &BinarySlots) -> Result<(f64, u64)> {
539 let all: Vec<u32> = slots.yes.iter().chain(slots.no.iter()).copied().collect();
540 let url = request_url(self.probe, &self.openai_base, &self.native_base);
541 let prompts = [prompt.to_string()];
542 let mut deepest = 0;
543 for candidates_requested in candidate_ladder(self.probe, all.len()) {
544 deepest = candidates_requested;
545 let body = build_body_with_candidates(
546 self.probe,
547 &self.upstream_model,
548 &prompts,
549 &all,
550 candidates_requested,
551 );
552 let response: CompletionResponse =
553 post_json(&self.client, &url, &body, self.upstream_key.as_deref()).await?;
554 if truncated(self.probe, &response) {
555 bail!(
556 "the upstream runtime truncated a binary prompt; shorten the state or raise \
557 its context size"
558 );
559 }
560 let tokens = input_tokens(self.probe, &response);
561 if let Some(limit) = self.max_input_tokens
562 && tokens > limit as u64
563 {
564 bail!("{tokens} input tokens exceed limit {limit}; no truncation allowed");
565 }
566 let candidates = parse_candidates(self.probe, &response)?;
567 if let Some(probability) = yes_probability(&candidates, slots) {
568 return Ok((probability, tokens));
569 }
570 if self.probe.restricts_distribution() {
571 bail!("the restricted distribution carries neither a yes nor a no token");
572 }
573 }
574 bail!("no request in the ladder returned a yes or no token (deepest: {deepest} candidates)")
575 }
576
577 async fn read_batch(&self, questions: &[Question], prompts: &[String]) -> Result<Vec<Readout>> {
585 let max_options = questions
589 .iter()
590 .map(|question| question.options.len())
591 .max()
592 .unwrap_or(0);
593 let slots = &self.slots[..max_options];
594 let url = request_url(self.probe, &self.openai_base, &self.native_base);
595
596 let mut last_error = None;
597 for candidates_requested in candidate_ladder(self.probe, max_options) {
598 let body = build_body_with_candidates(
599 self.probe,
600 &self.upstream_model,
601 prompts,
602 slots,
603 candidates_requested,
604 );
605 let response: CompletionResponse =
606 post_json(&self.client, &url, &body, self.upstream_key.as_deref()).await?;
607
608 let batch_tokens = input_tokens(self.probe, &response);
613
614 if let Some(limit) = self.max_input_tokens
619 && batch_tokens > limit as u64 * questions.len() as u64
620 {
621 bail!(
622 "the batched readout reports {batch_tokens} input tokens, over {} x {limit}; \
623 no truncation allowed",
624 questions.len()
625 );
626 }
627
628 let candidates = parse_batch_candidates(self.probe, &response, questions.len())?;
629 let mut readouts = Vec::with_capacity(questions.len());
630 let mut missing = None;
631 for (index, (question, candidates)) in questions.iter().zip(&candidates).enumerate() {
632 let count = question.options.len();
633 let slots = &self.slots[..count];
634 let letters: Vec<char> = LETTERS.chars().take(count).collect();
635 match slot_logprobs_from_candidates(candidates, slots, &letters) {
636 Ok(logprobs) => readouts.push(Readout {
637 logprobs,
638 input_tokens: if index == 0 { batch_tokens } else { 0 },
639 }),
640 Err(error) => {
641 missing = Some(error.context(format!("question {:?}", question.id)));
642 break;
643 }
644 }
645 }
646 match missing {
647 None => return Ok(readouts),
648 Some(error) if !self.probe.restricts_distribution() => {
651 eprintln!(
652 "{error:#}; deepening the batched readout past {candidates_requested} \
653 candidates"
654 );
655 last_error = Some(error);
656 }
657 Some(error) => return Err(error),
658 }
659 }
660 Err(last_error.expect("a failed ladder records its last error"))
661 }
662
663 async fn read_one_by_one(
668 &self,
669 questions: &[Question],
670 prompts: &[String],
671 ) -> Result<Vec<Readout>> {
672 let mut readouts = Vec::with_capacity(questions.len());
673 for (question, prompt) in questions.iter().zip(prompts) {
674 let count = question.options.len();
675 let slots = &self.slots[..count];
676 let letters: Vec<char> = LETTERS.chars().take(count).collect();
677 let url = request_url(self.probe, &self.openai_base, &self.native_base);
678
679 let mut last_error = None;
680 let mut readout = None;
681 for candidates_requested in candidate_ladder(self.probe, count) {
682 let body = build_body_with_candidates(
683 self.probe,
684 &self.upstream_model,
685 std::slice::from_ref(prompt),
686 slots,
687 candidates_requested,
688 );
689 let response: CompletionResponse =
690 post_json(&self.client, &url, &body, self.upstream_key.as_deref()).await?;
691
692 if truncated(self.probe, &response) {
693 bail!(
694 "question {:?}: the upstream runtime truncated the prompt; shorten the state \
695 or raise its context size",
696 question.id
697 );
698 }
699 let tokens = input_tokens(self.probe, &response);
700 if let Some(limit) = self.max_input_tokens
701 && tokens > limit as u64
702 {
703 bail!(
704 "question {:?}: {tokens} input tokens exceed limit {limit}; no truncation \
705 allowed",
706 question.id
707 );
708 }
709
710 match parse_logprobs(self.probe, &response, slots, &letters) {
711 Ok(logprobs) => {
712 readout = Some(Readout {
713 logprobs,
714 input_tokens: tokens,
715 });
716 break;
717 }
718 Err(error) if !self.probe.restricts_distribution() => {
719 eprintln!(
720 "question {:?}: {error:#}; deepening the readout past \
721 {candidates_requested} candidates",
722 question.id
723 );
724 last_error = Some(error.context(format!("question {:?}", question.id)));
725 }
726 Err(error) => {
727 return Err(error).with_context(|| format!("question {:?}", question.id));
728 }
729 }
730 }
731 readouts.push(
732 readout.ok_or_else(|| last_error.expect("a failed ladder records its last error"))?,
733 );
734 }
735 Ok(readouts)
736 }
737}
738
739fn yes_probability(candidates: &[Candidate], slots: &BinarySlots) -> Option<f64> {
745 let logprob = |ids: &[u32]| {
746 ids.iter().find_map(|id| {
747 candidates
748 .iter()
749 .find(|candidate| candidate.id == Some(*id))
750 .map(|candidate| candidate.logprob)
751 })
752 };
753 let yes = logprob(&slots.yes)?;
754 let no = logprob(&slots.no)?;
755 let maximum = yes.max(no);
756 let yes_mass = (yes - maximum).exp();
757 let no_mass = (no - maximum).exp();
758 Some(yes_mass / (yes_mass + no_mass))
759}
760
761async fn try_probe(
762 client: &reqwest::Client,
763 probe: Probe,
764 config: &BridgeConfig,
765 prompt: &str,
766 slots: &[u32],
767 letters: &[char],
768) -> Result<()> {
769 let url = request_url(probe, &config.openai_base, &config.native_base);
770 let mut last_error = None;
774 for candidates_requested in candidate_ladder(probe, slots.len()) {
775 let body = build_body_with_candidates(
776 probe,
777 &config.upstream_model,
778 &[prompt.to_string()],
779 slots,
780 candidates_requested,
781 );
782 let response: CompletionResponse =
783 post_json(client, &url, &body, config.upstream_key.as_deref()).await?;
784 match parse_logprobs(probe, &response, slots, letters) {
785 Ok(logprobs) => {
786 crate::strategy::validate_probe(&logprobs)?;
787 if probe.restricts_distribution() {
788 let candidates = parse_candidates(probe, &response)?;
789 if !candidates_restricted_to(&candidates, slots, letters) {
790 bail!(
791 "the server ignored the distribution restriction and returned foreign tokens, so \
792 this transport would not be what it claims"
793 );
794 }
795 }
796 return Ok(());
797 }
798 Err(error) if !probe.restricts_distribution() => {
801 last_error = Some(error);
802 }
803 Err(error) => return Err(error),
804 }
805 }
806 Err(last_error.expect("a failed ladder records its last error"))
807}
808
809pub async fn post_json<T: serde::de::DeserializeOwned>(
815 client: &reqwest::Client,
816 url: &str,
817 body: &(impl serde::Serialize + ?Sized),
818 api_key: Option<&str>,
819) -> Result<T> {
820 let mut request = client.post(url).json(body);
821 if let Some(key) = api_key {
822 request = request.bearer_auth(key);
823 }
824 let response = request
825 .send()
826 .await
827 .with_context(|| format!("POST {url} failed"))?;
828 let status = response.status();
829 let text = response
830 .text()
831 .await
832 .with_context(|| format!("reading the {url} response failed"))?;
833 if !status.is_success() {
834 return Err(anyhow::Error::new(UpstreamStatus {
835 url: url.to_string(),
836 status: status.as_u16(),
837 body: text.chars().take(400).collect(),
838 }));
839 }
840 serde_json::from_str(&text).with_context(|| {
841 format!(
842 "POST {url} returned an unexpected body: {}",
843 text.chars().take(400).collect::<String>()
844 )
845 })
846}
847
848pub struct AppState {
850 pub bridge: Bridge,
852 pub api_key: Option<String>,
854 pub readout_check: Option<crate::readout::ReadoutCheck>,
856}
857
858pub fn router(state: Arc<AppState>) -> Router {
860 Router::new()
861 .route("/v1/systemone", post(systemone))
862 .route("/v1/models", get(models))
863 .route("/health", get(health))
864 .with_state(state)
865}
866
867async fn systemone(State(state): State<Arc<AppState>>, headers: HeaderMap, body: Bytes) -> Response {
868 if let Err(response) = authorize(&state, &headers) {
869 return response;
870 }
871 let payload: Value = match serde_json::from_slice(&body) {
872 Ok(value) => value,
873 Err(error) => {
874 return error_response(
875 StatusCode::BAD_REQUEST,
876 format!("body: must be a JSON object ({error})"),
877 )
878 }
879 };
880 let batch = match request_batch(
881 &payload,
882 &state.bridge.served_model,
883 state.bridge.accepts_any_model(),
884 ) {
885 Ok(parsed) => parsed,
886 Err(error) => return error_response(StatusCode::BAD_REQUEST, error.0),
887 };
888 let results = match state
889 .bridge
890 .score_questions(&batch.state, &batch.questions)
891 .await
892 {
893 Ok(results) => results,
894 Err(error) => {
895 return error_response(StatusCode::BAD_GATEWAY, format!("{error:#}"));
896 }
897 };
898 let answers: Vec<ScoredAnswer> = results.into_iter().map(|scored| scored.answer).collect();
899 match response_from_results(
900 &state.bridge.served_model,
901 &batch.specs,
902 &answers,
903 state.bridge.readout(),
904 state.bridge.confidence_method(),
905 ) {
906 Ok(response) => Json(response).into_response(),
907 Err(error) => error_response(StatusCode::INTERNAL_SERVER_ERROR, error.0),
908 }
909}
910
911#[derive(Debug, Serialize)]
913struct ModelEntry {
914 name: String,
915 description: String,
916 release_date: String,
917}
918
919#[derive(Debug, Serialize)]
920struct ModelsResponse {
921 models: Vec<ModelEntry>,
922}
923
924#[derive(Debug, Serialize)]
927struct HealthResponse {
928 status: &'static str,
929 served_model: String,
930 upstream_model: String,
931 probe: &'static str,
932 probe_note: &'static str,
933 endpoint: &'static str,
934 prompt_version: &'static str,
935 answer_slots: OrderedMap<u32>,
937 chat_template_kwargs: Value,
938 renders_locally: bool,
939 tokenizes_locally: bool,
940 probability_status: &'static str,
941 readout: ReadoutStatus,
943 batch_fallbacks: u64,
946 #[serde(skip_serializing_if = "Option::is_none")]
948 readout_check: Option<crate::readout::ReadoutCheck>,
949}
950
951async fn models(State(state): State<Arc<AppState>>, headers: HeaderMap) -> Response {
952 if let Err(response) = authorize(&state, &headers) {
953 return response;
954 }
955 Json(ModelsResponse {
956 models: vec![ModelEntry {
957 name: state.bridge.served_model.clone(),
958 description: state.bridge.description.clone(),
959 release_date: state.bridge.release_date.clone(),
960 }],
961 })
962 .into_response()
963}
964
965async fn health(State(state): State<Arc<AppState>>, headers: HeaderMap) -> Response {
966 if let Err(response) = authorize(&state, &headers) {
967 return response;
968 }
969 let mut answer_slots = OrderedMap::new();
970 for (letter, id) in LETTERS.chars().zip(state.bridge.slots()) {
971 answer_slots.insert(letter.to_string(), *id);
972 }
973 Json(HealthResponse {
974 status: "ok",
975 served_model: state.bridge.served_model.clone(),
976 upstream_model: state.bridge.upstream_model().to_string(),
977 probe: state.bridge.probe().name(),
978 probe_note: state.bridge.probe().note(),
979 endpoint: state.bridge.probe().endpoint(),
980 prompt_version: PROMPT_VERSION,
981 answer_slots,
982 chat_template_kwargs: state.bridge.chat_template_kwargs(),
983 renders_locally: state.bridge.renders_locally(),
984 tokenizes_locally: state.bridge.tokenizes_locally(),
985 probability_status: PROBABILITY_STATUS,
986 readout: state.bridge.readout().clone(),
987 batch_fallbacks: state.bridge.batch_fallbacks(),
988 readout_check: state.readout_check.clone(),
989 })
990 .into_response()
991}
992
993fn error_response(status: StatusCode, message: String) -> Response {
994 (status, Json(json!({"error": message}))).into_response()
995}
996
997#[allow(clippy::result_large_err)]
1000fn authorize(state: &AppState, headers: &HeaderMap) -> Result<(), Response> {
1001 let Some(expected) = &state.api_key else {
1002 return Ok(());
1003 };
1004 let presented = headers
1005 .get(AUTHORIZATION)
1006 .and_then(|value| value.to_str().ok())
1007 .and_then(|value| value.strip_prefix("Bearer "))
1008 .unwrap_or_default();
1009 if constant_time_eq(presented.as_bytes(), expected.as_bytes()) {
1010 Ok(())
1011 } else {
1012 Err(error_response(
1013 StatusCode::UNAUTHORIZED,
1014 "missing or invalid bearer token".to_string(),
1015 ))
1016 }
1017}
1018
1019fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
1020 if left.len() != right.len() {
1021 return false;
1022 }
1023 left.iter()
1024 .zip(right)
1025 .fold(0u8, |accumulator, (a, b)| accumulator | (a ^ b))
1026 == 0
1027}