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