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::{direct_messages, prompt_sha256, softmax, ChatTemplate, LocalComponents, RuntimeClient, LETTERS, PROMPT_VERSION};
19use crate::strategy::{
20 build_body, candidates_restricted_to, input_tokens, parse_candidates, parse_logprobs,
21 request_url, truncated, CompletionResponse, Probe,
22};
23use crate::wire::{
24 request_rows, response_from_results, OrderedMap, ReadoutStatus, Row, ScoredAnswer,
25 PROBABILITY_STATUS,
26};
27
28pub struct Bridge {
30 client: reqwest::Client,
31 template: ChatTemplate,
32 probe: Probe,
33 upstream_model: String,
34 upstream_key: Option<String>,
35 openai_base: String,
36 native_base: String,
37 slots: Vec<u32>,
38 served_model: String,
39 description: String,
40 release_date: String,
41 max_input_tokens: Option<usize>,
42 readout: ReadoutStatus,
44 lock: Mutex<()>,
46}
47
48pub struct BridgeConfig {
50 pub openai_base: String,
52 pub native_base: String,
54 pub upstream_model: String,
56 pub upstream_key: Option<String>,
58 pub served_model: String,
60 pub description: String,
62 pub release_date: String,
64 pub chat_template_kwargs: Value,
66 pub max_input_tokens: Option<usize>,
68 pub local: LocalComponents,
71 pub readout: ReadoutStatus,
77}
78
79#[derive(Debug, Clone)]
82pub struct DetailedScore {
83 pub answer: ScoredAnswer,
85 pub option_logprobs: Vec<f64>,
87 pub prompt_sha256: String,
89}
90
91impl Bridge {
92 pub async fn connect(client: reqwest::Client, mut config: BridgeConfig) -> Result<Self> {
94 let runtime = RuntimeClient::new(
95 client.clone(),
96 config.native_base.clone(),
97 config.upstream_model.clone(),
98 config.upstream_key.clone(),
99 );
100 let template = ChatTemplate::new(
101 runtime,
102 std::mem::take(&mut config.local),
103 config.chat_template_kwargs.clone(),
104 );
105
106 let slots = template
107 .resolve_slots()
108 .await
109 .context("resolving the single-token answer slots failed")?;
110
111 let probe_options: Vec<crate::wire::RowOption> = LETTERS
114 .chars()
115 .map(|letter| crate::wire::RowOption {
116 id: letter.to_string(),
117 description: format!("Probe option {letter}"),
118 })
119 .collect();
120 let messages = direct_messages(&json!("probe"), "probe", &probe_options);
121 let prompt = template
122 .render(&messages)
123 .await
124 .context("rendering the probe prompt failed")?;
125 template
126 .verify_boundary(&prompt, &slots)
127 .await
128 .context("verifying the answer-token boundary failed")?;
129
130 let letters: Vec<char> = LETTERS.chars().collect();
131 let mut chosen = None;
132 let mut failures = Vec::new();
133 for probe in crate::strategy::CANDIDATES {
134 match try_probe(
135 &client,
136 probe,
137 &config,
138 &prompt,
139 &slots,
140 &letters,
141 )
142 .await
143 {
144 Ok(()) => {
145 chosen = Some(probe);
146 break;
147 }
148 Err(error) => failures.push(format!("{}: {error:#}", probe.name())),
149 }
150 }
151 let probe = chosen.ok_or_else(|| {
152 anyhow::anyhow!(
153 "no supported transport answered the probe:\n {}",
154 failures.join("\n ")
155 )
156 })?;
157
158 Ok(Self {
159 client,
160 template,
161 probe,
162 upstream_model: config.upstream_model,
163 upstream_key: config.upstream_key,
164 openai_base: config.openai_base,
165 native_base: config.native_base,
166 slots,
167 served_model: config.served_model,
168 description: config.description,
169 release_date: config.release_date,
170 max_input_tokens: config.max_input_tokens,
171 readout: config.readout,
172 lock: Mutex::new(()),
173 })
174 }
175
176 pub fn probe(&self) -> Probe {
178 self.probe
179 }
180
181 pub fn readout(&self) -> &ReadoutStatus {
183 &self.readout
184 }
185
186 pub fn slots(&self) -> &[u32] {
188 &self.slots
189 }
190
191 pub fn upstream_model(&self) -> &str {
193 &self.upstream_model
194 }
195
196 pub fn chat_template_kwargs(&self) -> Value {
198 self.template.chat_template_kwargs()
199 }
200
201 pub fn renders_locally(&self) -> bool {
203 self.template.renders_locally()
204 }
205
206 pub fn tokenizes_locally(&self) -> bool {
208 self.template.tokenizes_locally()
209 }
210
211 pub async fn score_rows(&self, rows: &[Row]) -> Result<Vec<DetailedScore>> {
213 let _guard = self.lock.lock().await;
214 let mut results = Vec::with_capacity(rows.len());
215 for row in rows {
216 results.push(self.score_row(row).await?);
217 }
218 Ok(results)
219 }
220
221 async fn score_row(&self, row: &Row) -> Result<DetailedScore> {
222 let count = row.options.len();
223 let messages = direct_messages(&row.state, &row.question, &row.options);
224 let prompt = self
225 .template
226 .render(&messages)
227 .await
228 .with_context(|| format!("row {:?}", row.id))?;
229
230 let slots = &self.slots[..count];
231 let letters: Vec<char> = LETTERS.chars().take(count).collect();
232 let body = build_body(self.probe, &self.upstream_model, &prompt, slots);
233 let url = request_url(self.probe, &self.openai_base, &self.native_base);
234 let response: CompletionResponse =
235 post_json(&self.client, &url, &body, self.upstream_key.as_deref()).await?;
236
237 if truncated(self.probe, &response) {
238 bail!(
239 "row {:?}: the upstream runtime truncated the prompt; shorten the state or raise \
240 its context size",
241 row.id
242 );
243 }
244 let input_tokens = input_tokens(self.probe, &response);
245 if let Some(limit) = self.max_input_tokens
246 && input_tokens > limit as u64
247 {
248 bail!(
249 "row {:?}: {input_tokens} input tokens exceed limit {limit}; no truncation allowed",
250 row.id
251 );
252 }
253
254 let logprobs = parse_logprobs(self.probe, &response, slots, &letters)
255 .with_context(|| format!("row {:?}", row.id))?;
256 let probabilities = softmax(&logprobs);
257
258 Ok(DetailedScore {
259 answer: ScoredAnswer {
260 id: row.id.clone(),
261 option_ids: row.options.iter().map(|option| option.id.clone()).collect(),
262 probabilities,
263 input_tokens,
264 prompt_version: Some(PROMPT_VERSION.to_string()),
265 },
266 option_logprobs: logprobs,
267 prompt_sha256: prompt_sha256(&prompt),
268 })
269 }
270}
271
272async fn try_probe(
273 client: &reqwest::Client,
274 probe: Probe,
275 config: &BridgeConfig,
276 prompt: &str,
277 slots: &[u32],
278 letters: &[char],
279) -> Result<()> {
280 let body = build_body(probe, &config.upstream_model, prompt, slots);
281 let url = request_url(probe, &config.openai_base, &config.native_base);
282 let response: CompletionResponse = post_json(client, &url, &body, config.upstream_key.as_deref()).await?;
283 let logprobs = parse_logprobs(probe, &response, slots, letters)?;
284 crate::strategy::validate_probe(&logprobs)?;
285 if probe.restricts_distribution() {
286 let candidates = parse_candidates(probe, &response)?;
287 if !candidates_restricted_to(&candidates, slots, letters) {
288 bail!(
289 "the server ignored the distribution restriction and returned foreign tokens, so \
290 this transport would not be what it claims"
291 );
292 }
293 }
294 Ok(())
295}
296
297pub async fn post_json<T: serde::de::DeserializeOwned>(
303 client: &reqwest::Client,
304 url: &str,
305 body: &(impl serde::Serialize + ?Sized),
306 api_key: Option<&str>,
307) -> Result<T> {
308 let mut request = client.post(url).json(body);
309 if let Some(key) = api_key {
310 request = request.bearer_auth(key);
311 }
312 let response = request
313 .send()
314 .await
315 .with_context(|| format!("POST {url} failed"))?;
316 let status = response.status();
317 let text = response
318 .text()
319 .await
320 .with_context(|| format!("reading the {url} response failed"))?;
321 if !status.is_success() {
322 bail!(
323 "POST {url} returned HTTP {status}: {}",
324 text.chars().take(400).collect::<String>()
325 );
326 }
327 serde_json::from_str(&text).with_context(|| {
328 format!(
329 "POST {url} returned an unexpected body: {}",
330 text.chars().take(400).collect::<String>()
331 )
332 })
333}
334
335pub struct AppState {
337 pub bridge: Bridge,
339 pub api_key: Option<String>,
341}
342
343pub fn router(state: Arc<AppState>) -> Router {
345 Router::new()
346 .route("/v1/systemone", post(systemone))
347 .route("/v1/models", get(models))
348 .route("/health", get(health))
349 .with_state(state)
350}
351
352async fn systemone(State(state): State<Arc<AppState>>, headers: HeaderMap, body: Bytes) -> Response {
353 if let Err(response) = authorize(&state, &headers) {
354 return response;
355 }
356 let payload: Value = match serde_json::from_slice(&body) {
357 Ok(value) => value,
358 Err(error) => {
359 return error_response(
360 StatusCode::BAD_REQUEST,
361 format!("body: must be a JSON object ({error})"),
362 )
363 }
364 };
365 let (specs, rows) = match request_rows(&payload, &state.bridge.served_model) {
366 Ok(parsed) => parsed,
367 Err(error) => return error_response(StatusCode::BAD_REQUEST, error.0),
368 };
369 let results = match state.bridge.score_rows(&rows).await {
370 Ok(results) => results,
371 Err(error) => {
372 return error_response(StatusCode::BAD_GATEWAY, format!("{error:#}"));
373 }
374 };
375 let answers: Vec<ScoredAnswer> = results.into_iter().map(|scored| scored.answer).collect();
376 match response_from_results(
377 &state.bridge.served_model,
378 &specs,
379 &answers,
380 state.bridge.readout(),
381 ) {
382 Ok(response) => Json(response).into_response(),
383 Err(error) => error_response(StatusCode::INTERNAL_SERVER_ERROR, error.0),
384 }
385}
386
387#[derive(Debug, Serialize)]
389struct ModelEntry {
390 name: String,
391 description: String,
392 release_date: String,
393}
394
395#[derive(Debug, Serialize)]
396struct ModelsResponse {
397 models: Vec<ModelEntry>,
398}
399
400#[derive(Debug, Serialize)]
403struct HealthResponse {
404 status: &'static str,
405 served_model: String,
406 upstream_model: String,
407 probe: &'static str,
408 probe_note: &'static str,
409 endpoint: &'static str,
410 prompt_version: &'static str,
411 answer_slots: OrderedMap<u32>,
413 chat_template_kwargs: Value,
414 renders_locally: bool,
415 tokenizes_locally: bool,
416 probability_status: &'static str,
417 readout: ReadoutStatus,
419}
420
421async fn models(State(state): State<Arc<AppState>>, headers: HeaderMap) -> Response {
422 if let Err(response) = authorize(&state, &headers) {
423 return response;
424 }
425 Json(ModelsResponse {
426 models: vec![ModelEntry {
427 name: state.bridge.served_model.clone(),
428 description: state.bridge.description.clone(),
429 release_date: state.bridge.release_date.clone(),
430 }],
431 })
432 .into_response()
433}
434
435async fn health(State(state): State<Arc<AppState>>, headers: HeaderMap) -> Response {
436 if let Err(response) = authorize(&state, &headers) {
437 return response;
438 }
439 let mut answer_slots = OrderedMap::new();
440 for (letter, id) in LETTERS.chars().zip(state.bridge.slots()) {
441 answer_slots.insert(letter.to_string(), *id);
442 }
443 Json(HealthResponse {
444 status: "ok",
445 served_model: state.bridge.served_model.clone(),
446 upstream_model: state.bridge.upstream_model().to_string(),
447 probe: state.bridge.probe().name(),
448 probe_note: state.bridge.probe().note(),
449 endpoint: state.bridge.probe().endpoint(),
450 prompt_version: PROMPT_VERSION,
451 answer_slots,
452 chat_template_kwargs: state.bridge.chat_template_kwargs(),
453 renders_locally: state.bridge.renders_locally(),
454 tokenizes_locally: state.bridge.tokenizes_locally(),
455 probability_status: PROBABILITY_STATUS,
456 readout: state.bridge.readout().clone(),
457 })
458 .into_response()
459}
460
461fn error_response(status: StatusCode, message: String) -> Response {
462 (status, Json(json!({"error": message}))).into_response()
463}
464
465#[allow(clippy::result_large_err)]
468fn authorize(state: &AppState, headers: &HeaderMap) -> Result<(), Response> {
469 let Some(expected) = &state.api_key else {
470 return Ok(());
471 };
472 let presented = headers
473 .get(AUTHORIZATION)
474 .and_then(|value| value.to_str().ok())
475 .and_then(|value| value.strip_prefix("Bearer "))
476 .unwrap_or_default();
477 if constant_time_eq(presented.as_bytes(), expected.as_bytes()) {
478 Ok(())
479 } else {
480 Err(error_response(
481 StatusCode::UNAUTHORIZED,
482 "missing or invalid bearer token".to_string(),
483 ))
484 }
485}
486
487fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
488 if left.len() != right.len() {
489 return false;
490 }
491 left.iter()
492 .zip(right)
493 .fold(0u8, |accumulator, (a, b)| accumulator | (a ^ b))
494 == 0
495}