Skip to main content

jev_bridge/
server.rs

1//! The bridge: one resident upstream model behind a Jev-style scoring API.
2
3use 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    direct_messages, direct_messages_reusing, evidence_json, prompt_sha256, softmax, ChatTemplate,
20    LocalComponents, RuntimeClient, LETTERS, PROMPT_VERSION,
21};
22use crate::strategy::{
23    build_body, candidates_restricted_to, input_tokens, parse_batch_candidates, parse_candidates,
24    parse_logprobs, request_url, slot_logprobs_from_candidates, truncated, CompletionResponse,
25    Probe,
26};
27use crate::wire::{
28    request_batch, response_from_results, OrderedMap, Question, ReadoutStatus, Row, ScoredAnswer,
29    PROBABILITY_STATUS,
30};
31use std::sync::atomic::{AtomicU64, Ordering};
32
33/// One configured bridge over a single upstream model.
34pub struct Bridge {
35    client: reqwest::Client,
36    template: ChatTemplate,
37    probe: Probe,
38    upstream_model: String,
39    upstream_key: Option<String>,
40    openai_base: String,
41    native_base: String,
42    slots: Vec<u32>,
43    served_model: String,
44    description: String,
45    release_date: String,
46    max_input_tokens: Option<usize>,
47    /// What the deployment declared about the readout channel on this model.
48    readout: ReadoutStatus,
49    /// How many batched readouts were served by the sequential fallback
50    /// because the endpoint rejected the array-`prompt` shape. Non-zero means
51    /// the shared prefill did not happen; `GET /health` reports it.
52    batch_fallbacks: AtomicU64,
53    /// Serialize upstream work so one resident model is not driven concurrently.
54    lock: Mutex<()>,
55}
56
57/// One question's readout from one upstream call.
58struct Readout {
59    /// Raw option log probabilities, before the softmax.
60    logprobs: Vec<f64>,
61    /// Prompt tokens attributed to this question. For the tail of a batched
62    /// readout this is 0: the endpoint reports one number for the whole batch
63    /// and it is attached to the first readout, so summing the vector
64    /// reproduces the endpoint's own count instead of a faked per-prompt split.
65    input_tokens: u64,
66}
67
68/// A non-success HTTP response from the upstream service.
69///
70/// The status code is kept because callers have to tell a *request-shape*
71/// rejection — the endpoint refuses this shape at all, where a fallback can
72/// help — from a call-level failure. A 429, 401/403 or 5xx must propagate:
73/// falling back there would turn a throttled or broken endpoint into a
74/// "successful" run.
75#[derive(Debug)]
76pub struct UpstreamStatus {
77    /// The URL that was posted to.
78    pub url: String,
79    /// The HTTP status the upstream returned.
80    pub status: u16,
81    /// The first 400 characters of the response body.
82    pub body: String,
83}
84
85impl UpstreamStatus {
86    /// Whether the endpoint rejected the request *shape* rather than failing
87    /// on this particular call.
88    pub fn is_shape_rejection(&self) -> bool {
89        matches!(self.status, 400 | 404 | 405 | 415 | 422)
90    }
91}
92
93impl std::fmt::Display for UpstreamStatus {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        write!(f, "POST {} returned HTTP {}: {}", self.url, self.status, self.body)
96    }
97}
98
99impl std::error::Error for UpstreamStatus {}
100
101/// Everything [`Bridge::connect`] needs to reach one upstream model.
102pub struct BridgeConfig {
103    /// OpenAI-compatible root, for example `http://127.0.0.1:8080/v1`.
104    pub openai_base: String,
105    /// Native root for llama.cpp's `/completion`.
106    pub native_base: String,
107    /// Model name sent upstream.
108    pub upstream_model: String,
109    /// Bearer token for the upstream service, when it needs one.
110    pub upstream_key: Option<String>,
111    /// Model id this bridge accepts from clients.
112    pub served_model: String,
113    /// Description returned by `GET /v1/models`.
114    pub description: String,
115    /// ISO release date returned by `GET /v1/models`.
116    pub release_date: String,
117    /// Template variables, for example `{"enable_thinking": false}`.
118    pub chat_template_kwargs: Value,
119    /// Reject rows above this token count instead of truncating them.
120    pub max_input_tokens: Option<usize>,
121    /// Render the chat template and/or tokenize in-process instead of asking
122    /// the runtime.
123    pub local: LocalComponents,
124    /// What the deployment knows about the readout channel on this model.
125    ///
126    /// Defaults to `unvalidated`: the bridge cannot prove readout quality at
127    /// runtime, so a deployment that checked it (for example with
128    /// [`crate::evaluate`] over an alignment run) states so explicitly.
129    pub readout: ReadoutStatus,
130}
131
132/// A scored row plus the evidence needed to compare a bridged run against a
133/// reference run row by row.
134#[derive(Debug, Clone)]
135pub struct DetailedScore {
136    /// The normalized answer handed to the wire layer.
137    pub answer: ScoredAnswer,
138    /// Raw option log probabilities, before the softmax.
139    pub option_logprobs: Vec<f64>,
140    /// SHA-256 of the rendered prompt, for comparing runs row by row.
141    pub prompt_sha256: String,
142}
143
144impl Bridge {
145    /// Resolve the prompt contract and pick a transport, or fail with reasons.
146    pub async fn connect(client: reqwest::Client, mut config: BridgeConfig) -> Result<Self> {
147        let runtime = RuntimeClient::new(
148            client.clone(),
149            config.native_base.clone(),
150            config.upstream_model.clone(),
151            config.upstream_key.clone(),
152        );
153        let template = ChatTemplate::new(
154            runtime,
155            std::mem::take(&mut config.local),
156            config.chat_template_kwargs.clone(),
157        );
158
159        let slots = template
160            .resolve_slots()
161            .await
162            .context("resolving the single-token answer slots failed")?;
163
164        // A sixteen-option probe exercises the widest contract this bridge
165        // serves, so a transport that passes it will hold for real requests.
166        let probe_options: Vec<crate::wire::RowOption> = LETTERS
167            .chars()
168            .map(|letter| crate::wire::RowOption {
169                id: letter.to_string(),
170                description: format!("Probe option {letter}"),
171            })
172            .collect();
173        let messages = direct_messages(&json!("probe"), "probe", &probe_options);
174        let prompt = template
175            .render(&messages)
176            .await
177            .context("rendering the probe prompt failed")?;
178        template
179            .verify_boundary(&prompt, &slots)
180            .await
181            .context("verifying the answer-token boundary failed")?;
182
183        let letters: Vec<char> = LETTERS.chars().collect();
184        let mut chosen = None;
185        let mut failures = Vec::new();
186        for probe in crate::strategy::CANDIDATES {
187            match try_probe(
188                &client,
189                probe,
190                &config,
191                &prompt,
192                &slots,
193                &letters,
194            )
195            .await
196            {
197                Ok(()) => {
198                    chosen = Some(probe);
199                    break;
200                }
201                Err(error) => failures.push(format!("{}: {error:#}", probe.name())),
202            }
203        }
204        let probe = chosen.ok_or_else(|| {
205            anyhow::anyhow!(
206                "no supported transport answered the probe:\n  {}",
207                failures.join("\n  ")
208            )
209        })?;
210
211        Ok(Self {
212            client,
213            template,
214            probe,
215            upstream_model: config.upstream_model,
216            upstream_key: config.upstream_key,
217            openai_base: config.openai_base,
218            native_base: config.native_base,
219            slots,
220            served_model: config.served_model,
221            description: config.description,
222            release_date: config.release_date,
223            max_input_tokens: config.max_input_tokens,
224            readout: config.readout,
225            batch_fallbacks: AtomicU64::new(0),
226            lock: Mutex::new(()),
227        })
228    }
229
230    /// The transport negotiated at startup.
231    pub fn probe(&self) -> Probe {
232        self.probe
233    }
234
235    /// What the deployment declared about the readout channel on this model.
236    pub fn readout(&self) -> &ReadoutStatus {
237        &self.readout
238    }
239
240    /// How many batched readouts fell back to sequential requests because the
241    /// endpoint rejected the array-`prompt` shape.
242    pub fn batch_fallbacks(&self) -> u64 {
243        self.batch_fallbacks.load(Ordering::Relaxed)
244    }
245
246    /// Run the readout self-check: a few questions whose answers are obvious,
247    /// read through the exact slot channel every real request uses.
248    ///
249    /// It cannot prove the model is good at decisions — that needs a
250    /// gold-labelled workload, which is [`crate::evaluate`]'s job — but it
251    /// catches the model that is not answering the question at all, the
252    /// failure mode that would otherwise be silently confident.
253    pub async fn run_readout_check(&self) -> Result<crate::readout::ReadoutCheck> {
254        let questions = crate::readout::questions();
255        let scored = self
256            .score_questions(&json!(crate::readout::PROBE_STATE), &questions)
257            .await
258            .context("the readout self-check could not be scored")?;
259        let results = crate::readout::PROBES
260            .iter()
261            .zip(&scored)
262            .map(|(probe, score)| {
263                crate::readout::outcome(probe, &score.answer.option_ids, &score.answer.probabilities)
264            })
265            .collect();
266        Ok(crate::readout::ReadoutCheck::from_results(results))
267    }
268
269    /// Resolved token id for each of the sixteen answer letters.
270    pub fn slots(&self) -> &[u32] {
271        &self.slots
272    }
273
274    /// The model name this bridge sends upstream.
275    pub fn upstream_model(&self) -> &str {
276        &self.upstream_model
277    }
278
279    /// Template variables forwarded on every render.
280    pub fn chat_template_kwargs(&self) -> Value {
281        self.template.chat_template_kwargs()
282    }
283
284    /// Whether the prompt is rendered in-process rather than by the runtime.
285    pub fn renders_locally(&self) -> bool {
286        self.template.renders_locally()
287    }
288
289    /// Whether tokenization happens in-process rather than by the runtime.
290    pub fn tokenizes_locally(&self) -> bool {
291        self.template.tokenizes_locally()
292    }
293
294    /// Score one decision that carries its own state (the `--score` shape).
295    pub async fn score_row(&self, row: &Row) -> Result<DetailedScore> {
296        let question = Question {
297            id: row.id.clone(),
298            question: row.question.clone(),
299            options: row.options.clone(),
300        };
301        let mut scored = self.score_questions(&row.state, &[question]).await?;
302        Ok(scored.remove(0))
303    }
304
305    /// Score several questions that share one state (the System One shape).
306    ///
307    /// The state is serialised once and reused by every question. When the
308    /// negotiated transport accepts an array `prompt` and more than one
309    /// question is asked, the whole batch is read in **one** request, so a
310    /// prefix-caching server prefills the shared state once. A transport that
311    /// rejects the batched shape falls back to sequential single-prompt calls;
312    /// the fallback is counted (`GET /health` reports it) because a run served
313    /// that way did not get the shared prefill.
314    pub async fn score_questions(
315        &self,
316        state: &Value,
317        questions: &[Question],
318    ) -> Result<Vec<DetailedScore>> {
319        let _guard = self.lock.lock().await;
320
321        // Render up front: the evidence text is serialised once for the whole
322        // batch, and every question reuses it.
323        let evidence = evidence_json(state);
324        let mut prompts = Vec::with_capacity(questions.len());
325        for question in questions {
326            let messages =
327                direct_messages_reusing(&evidence, &question.question, &question.options);
328            let prompt = self
329                .template
330                .render(&messages)
331                .await
332                .with_context(|| format!("question {:?}", question.id))?;
333            prompts.push(prompt);
334        }
335
336        let readouts = if questions.len() > 1 && self.probe.supports_batch() {
337            match self.read_batch(questions, &prompts).await {
338                Ok(readouts) => readouts,
339                Err(error) => {
340                    // Only a *request-shape* rejection means "this endpoint
341                    // cannot batch". Anything else is about the call itself
342                    // and must propagate.
343                    if error
344                        .downcast_ref::<UpstreamStatus>()
345                        .is_some_and(UpstreamStatus::is_shape_rejection)
346                    {
347                        self.batch_fallbacks.fetch_add(1, Ordering::Relaxed);
348                        eprintln!(
349                            "the upstream rejected the batched readout shape ({error}); falling \
350                             back to {} sequential requests (no shared prefill on this endpoint)",
351                            questions.len()
352                        );
353                        self.read_one_by_one(questions, &prompts).await?
354                    } else {
355                        return Err(error);
356                    }
357                }
358            }
359        } else {
360            self.read_one_by_one(questions, &prompts).await?
361        };
362
363        // Assemble: every answer is derived from its own readout only.
364        Ok(readouts
365            .into_iter()
366            .zip(questions)
367            .zip(&prompts)
368            .map(|((readout, question), prompt)| DetailedScore {
369                answer: ScoredAnswer {
370                    id: question.id.clone(),
371                    option_ids: question
372                        .options
373                        .iter()
374                        .map(|option| option.id.clone())
375                        .collect(),
376                    probabilities: softmax(&readout.logprobs),
377                    input_tokens: readout.input_tokens,
378                    prompt_version: Some(PROMPT_VERSION.to_string()),
379                },
380                option_logprobs: readout.logprobs,
381                prompt_sha256: prompt_sha256(prompt),
382            })
383            .collect())
384    }
385
386    /// One batched readout for every question, in request order.
387    async fn read_batch(&self, questions: &[Question], prompts: &[String]) -> Result<Vec<Readout>> {
388        // One `logprobs` value covers every prompt of the request, so it is
389        // sized for the largest declared option set; each question then reads
390        // only its own slots from its own candidate list.
391        let max_options = questions
392            .iter()
393            .map(|question| question.options.len())
394            .max()
395            .unwrap_or(0);
396        let slots = &self.slots[..max_options];
397        let body = build_body(self.probe, &self.upstream_model, prompts, slots);
398        let url = request_url(self.probe, &self.openai_base, &self.native_base);
399        let response: CompletionResponse =
400            post_json(&self.client, &url, &body, self.upstream_key.as_deref()).await?;
401
402        // The endpoint reports one usage block for the whole batch, and
403        // endpoints disagree on what it counts (llama.cpp b11096 counts the
404        // shared prefix once). It is attached to the first readout so summing
405        // the vector reproduces the endpoint's own number.
406        let batch_tokens = input_tokens(self.probe, &response);
407
408        // A batched prompt cannot be checked against `max_input_tokens` per
409        // prompt: the only number the endpoint gives is the batch total. The
410        // loose upper bound below still catches a request that is over budget
411        // even if every prompt shared one prefix.
412        if let Some(limit) = self.max_input_tokens
413            && batch_tokens > limit as u64 * questions.len() as u64
414        {
415            bail!(
416                "the batched readout reports {batch_tokens} input tokens, over {} x {limit}; \
417                 no truncation allowed",
418                questions.len()
419            );
420        }
421
422        let candidates = parse_batch_candidates(self.probe, &response, questions.len())?;
423        let mut readouts = Vec::with_capacity(questions.len());
424        for (index, (question, candidates)) in questions.iter().zip(&candidates).enumerate() {
425            let count = question.options.len();
426            let slots = &self.slots[..count];
427            let letters: Vec<char> = LETTERS.chars().take(count).collect();
428            let logprobs = slot_logprobs_from_candidates(candidates, slots, &letters)
429                .with_context(|| format!("question {:?}", question.id))?;
430            readouts.push(Readout {
431                logprobs,
432                input_tokens: if index == 0 { batch_tokens } else { 0 },
433            });
434        }
435        Ok(readouts)
436    }
437
438    /// One readout per question, one upstream request each.
439    async fn read_one_by_one(
440        &self,
441        questions: &[Question],
442        prompts: &[String],
443    ) -> Result<Vec<Readout>> {
444        let mut readouts = Vec::with_capacity(questions.len());
445        for (question, prompt) in questions.iter().zip(prompts) {
446            let count = question.options.len();
447            let slots = &self.slots[..count];
448            let letters: Vec<char> = LETTERS.chars().take(count).collect();
449            let body = build_body(
450                self.probe,
451                &self.upstream_model,
452                std::slice::from_ref(prompt),
453                slots,
454            );
455            let url = request_url(self.probe, &self.openai_base, &self.native_base);
456            let response: CompletionResponse =
457                post_json(&self.client, &url, &body, self.upstream_key.as_deref()).await?;
458
459            if truncated(self.probe, &response) {
460                bail!(
461                    "question {:?}: the upstream runtime truncated the prompt; shorten the state \
462                     or raise its context size",
463                    question.id
464                );
465            }
466            let tokens = input_tokens(self.probe, &response);
467            if let Some(limit) = self.max_input_tokens
468                && tokens > limit as u64
469            {
470                bail!(
471                    "question {:?}: {tokens} input tokens exceed limit {limit}; no truncation \
472                     allowed",
473                    question.id
474                );
475            }
476
477            let logprobs = parse_logprobs(self.probe, &response, slots, &letters)
478                .with_context(|| format!("question {:?}", question.id))?;
479            readouts.push(Readout {
480                logprobs,
481                input_tokens: tokens,
482            });
483        }
484        Ok(readouts)
485    }
486}
487
488async fn try_probe(
489    client: &reqwest::Client,
490    probe: Probe,
491    config: &BridgeConfig,
492    prompt: &str,
493    slots: &[u32],
494    letters: &[char],
495) -> Result<()> {
496    let body = build_body(probe, &config.upstream_model, &[prompt.to_string()], slots);
497    let url = request_url(probe, &config.openai_base, &config.native_base);
498    let response: CompletionResponse = post_json(client, &url, &body, config.upstream_key.as_deref()).await?;
499    let logprobs = parse_logprobs(probe, &response, slots, letters)?;
500    crate::strategy::validate_probe(&logprobs)?;
501    if probe.restricts_distribution() {
502        let candidates = parse_candidates(probe, &response)?;
503        if !candidates_restricted_to(&candidates, slots, letters) {
504            bail!(
505                "the server ignored the distribution restriction and returned foreign tokens, so \
506                 this transport would not be what it claims"
507            );
508        }
509    }
510    Ok(())
511}
512
513/// POST a typed request body and decode a typed response.
514///
515/// The bridge always knows which shape it expects, so the response is decoded
516/// into a struct instead of being poked at as a generic document; a server that
517/// answers with something else fails here rather than later.
518pub async fn post_json<T: serde::de::DeserializeOwned>(
519    client: &reqwest::Client,
520    url: &str,
521    body: &(impl serde::Serialize + ?Sized),
522    api_key: Option<&str>,
523) -> Result<T> {
524    let mut request = client.post(url).json(body);
525    if let Some(key) = api_key {
526        request = request.bearer_auth(key);
527    }
528    let response = request
529        .send()
530        .await
531        .with_context(|| format!("POST {url} failed"))?;
532    let status = response.status();
533    let text = response
534        .text()
535        .await
536        .with_context(|| format!("reading the {url} response failed"))?;
537    if !status.is_success() {
538        return Err(anyhow::Error::new(UpstreamStatus {
539            url: url.to_string(),
540            status: status.as_u16(),
541            body: text.chars().take(400).collect(),
542        }));
543    }
544    serde_json::from_str(&text).with_context(|| {
545        format!(
546            "POST {url} returned an unexpected body: {}",
547            text.chars().take(400).collect::<String>()
548        )
549    })
550}
551
552/// Shared HTTP state.
553pub struct AppState {
554    /// The configured bridge, shared by every handler.
555    pub bridge: Bridge,
556    /// When set, clients must present this bearer token.
557    pub api_key: Option<String>,
558    /// The startup readout self-check, when one ran.
559    pub readout_check: Option<crate::readout::ReadoutCheck>,
560}
561
562/// The HTTP surface: `/v1/systemone`, `/v1/models` and `/health`.
563pub fn router(state: Arc<AppState>) -> Router {
564    Router::new()
565        .route("/v1/systemone", post(systemone))
566        .route("/v1/models", get(models))
567        .route("/health", get(health))
568        .with_state(state)
569}
570
571async fn systemone(State(state): State<Arc<AppState>>, headers: HeaderMap, body: Bytes) -> Response {
572    if let Err(response) = authorize(&state, &headers) {
573        return response;
574    }
575    let payload: Value = match serde_json::from_slice(&body) {
576        Ok(value) => value,
577        Err(error) => {
578            return error_response(
579                StatusCode::BAD_REQUEST,
580                format!("body: must be a JSON object ({error})"),
581            )
582        }
583    };
584    let batch = match request_batch(&payload, &state.bridge.served_model) {
585        Ok(parsed) => parsed,
586        Err(error) => return error_response(StatusCode::BAD_REQUEST, error.0),
587    };
588    let results = match state
589        .bridge
590        .score_questions(&batch.state, &batch.questions)
591        .await
592    {
593        Ok(results) => results,
594        Err(error) => {
595            return error_response(StatusCode::BAD_GATEWAY, format!("{error:#}"));
596        }
597    };
598    let answers: Vec<ScoredAnswer> = results.into_iter().map(|scored| scored.answer).collect();
599    match response_from_results(
600        &state.bridge.served_model,
601        &batch.specs,
602        &answers,
603        state.bridge.readout(),
604    ) {
605        Ok(response) => Json(response).into_response(),
606        Err(error) => error_response(StatusCode::INTERNAL_SERVER_ERROR, error.0),
607    }
608}
609
610/// One entry in `GET /v1/models`.
611#[derive(Debug, Serialize)]
612struct ModelEntry {
613    name: String,
614    description: String,
615    release_date: String,
616}
617
618#[derive(Debug, Serialize)]
619struct ModelsResponse {
620    models: Vec<ModelEntry>,
621}
622
623/// What the bridge negotiated, exposed so an operator can see it rather than
624/// guess which transport is in use.
625#[derive(Debug, Serialize)]
626struct HealthResponse {
627    status: &'static str,
628    served_model: String,
629    upstream_model: String,
630    probe: &'static str,
631    probe_note: &'static str,
632    endpoint: &'static str,
633    prompt_version: &'static str,
634    /// The resolved token id of every answer letter.
635    answer_slots: OrderedMap<u32>,
636    chat_template_kwargs: Value,
637    renders_locally: bool,
638    tokenizes_locally: bool,
639    probability_status: &'static str,
640    /// Whether the readout channel was validated on the served model.
641    readout: ReadoutStatus,
642    /// Batched readouts served by the sequential fallback (0 = every batch got
643    /// the shared prefill).
644    batch_fallbacks: u64,
645    /// The startup readout self-check, when one ran.
646    #[serde(skip_serializing_if = "Option::is_none")]
647    readout_check: Option<crate::readout::ReadoutCheck>,
648}
649
650async fn models(State(state): State<Arc<AppState>>, headers: HeaderMap) -> Response {
651    if let Err(response) = authorize(&state, &headers) {
652        return response;
653    }
654    Json(ModelsResponse {
655        models: vec![ModelEntry {
656            name: state.bridge.served_model.clone(),
657            description: state.bridge.description.clone(),
658            release_date: state.bridge.release_date.clone(),
659        }],
660    })
661    .into_response()
662}
663
664async fn health(State(state): State<Arc<AppState>>, headers: HeaderMap) -> Response {
665    if let Err(response) = authorize(&state, &headers) {
666        return response;
667    }
668    let mut answer_slots = OrderedMap::new();
669    for (letter, id) in LETTERS.chars().zip(state.bridge.slots()) {
670        answer_slots.insert(letter.to_string(), *id);
671    }
672    Json(HealthResponse {
673        status: "ok",
674        served_model: state.bridge.served_model.clone(),
675        upstream_model: state.bridge.upstream_model().to_string(),
676        probe: state.bridge.probe().name(),
677        probe_note: state.bridge.probe().note(),
678        endpoint: state.bridge.probe().endpoint(),
679        prompt_version: PROMPT_VERSION,
680        answer_slots,
681        chat_template_kwargs: state.bridge.chat_template_kwargs(),
682        renders_locally: state.bridge.renders_locally(),
683        tokenizes_locally: state.bridge.tokenizes_locally(),
684        probability_status: PROBABILITY_STATUS,
685        readout: state.bridge.readout().clone(),
686        batch_fallbacks: state.bridge.batch_fallbacks(),
687        readout_check: state.readout_check.clone(),
688    })
689    .into_response()
690}
691
692fn error_response(status: StatusCode, message: String) -> Response {
693    (status, Json(json!({"error": message}))).into_response()
694}
695
696/// Rejecting a request returns a full HTTP response, which is larger than the
697/// success value; boxing it would only add an allocation on the error path.
698#[allow(clippy::result_large_err)]
699fn authorize(state: &AppState, headers: &HeaderMap) -> Result<(), Response> {
700    let Some(expected) = &state.api_key else {
701        return Ok(());
702    };
703    let presented = headers
704        .get(AUTHORIZATION)
705        .and_then(|value| value.to_str().ok())
706        .and_then(|value| value.strip_prefix("Bearer "))
707        .unwrap_or_default();
708    if constant_time_eq(presented.as_bytes(), expected.as_bytes()) {
709        Ok(())
710    } else {
711        Err(error_response(
712            StatusCode::UNAUTHORIZED,
713            "missing or invalid bearer token".to_string(),
714        ))
715    }
716}
717
718fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
719    if left.len() != right.len() {
720        return false;
721    }
722    left.iter()
723        .zip(right)
724        .fold(0u8, |accumulator, (a, b)| accumulator | (a ^ b))
725        == 0
726}