Skip to main content

openkind_api/
proxy.rs

1//! Optional proxy-cache hook for the evaluation handlers.
2//!
3//! When the daemon runs with `--proxy-cache-upstream`, it builds a
4//! [`SystemProxy`] implementation and stores it on [`AppState`]. The HTTP
5//! handler consults the hook before dispatching to the local registry, so
6//! proxied model aliases can be answered from the distilling cache or
7//! forwarded to the upstream Jev API with the caller's own credentials.
8//! The trait lives here so `openkind-api` never depends on the server or
9//! backends crates; the daemon supplies the implementation.
10
11use async_trait::async_trait;
12use openkind_core::{SystemRequest, SystemResponse};
13use serde::{Deserialize, Serialize};
14
15use crate::error::ApiError;
16use crate::models::ModelsResponse;
17
18/// Where a proxied answer came from.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "lowercase")]
21pub enum ProxySource {
22    /// Served locally by the distilling cache.
23    Local,
24    /// Forwarded to the upstream Jev-compatible API.
25    Upstream,
26}
27
28impl ProxySource {
29    /// Stable wire token for the `x-openkind-cache` response header.
30    pub fn as_str(&self) -> &'static str {
31        match self {
32            ProxySource::Local => "local",
33            ProxySource::Upstream => "upstream",
34        }
35    }
36}
37
38/// The outcome of a proxied evaluation.
39#[derive(Debug)]
40pub struct ProxyOutcome {
41    /// The wire response (locally built or relayed from upstream).
42    pub response: SystemResponse,
43    /// Where the answer came from.
44    pub source: ProxySource,
45    /// Optional per-question routing detail (JSON object: question id →
46    /// student version or forward reason).
47    pub detail: Option<serde_json::Value>,
48}
49
50/// The proxy-cache hook the daemon installs.
51#[async_trait]
52pub trait SystemProxy: Send + Sync {
53    /// Whether this hook wants to handle a request (e.g. its `model` is a
54    /// proxied alias). Cheap and synchronous.
55    fn wants(&self, request: &SystemRequest) -> bool;
56
57    /// Whether the proxy forwards caller credentials to the upstream Jev API.
58    ///
59    /// When `false` (for example, when a fixed upstream key is configured on
60    /// the daemon), the HTTP handler does not extract the inbound `Authorization`
61    /// bearer token or supply it to [`Self::evaluate`], preventing local daemon
62    /// bearer credentials from crossing the proxy boundary.
63    fn forwards_caller_credentials(&self) -> bool {
64        true
65    }
66
67    /// Handle the request: answer locally when the cache is confident,
68    /// otherwise forward upstream with the caller's credentials.
69    async fn evaluate(
70        &self,
71        request: SystemRequest,
72        caller_key: Option<String>,
73    ) -> Result<ProxyOutcome, ApiError>;
74
75    /// Upstream `/v1/models` listing when the proxy should answer model
76    /// discovery transparently; `None` falls back to the local registry.
77    async fn models(&self) -> Option<ModelsResponse>;
78}