Skip to main content

synapse/
jev_native.rs

1//! Native TypeSafe System One (Jev) lane. Jev evaluates typed questions
2//! (choice / score / noul) against a state and returns structured decisions —
3//! it has no chat surface, so the gateway exposes it as a verbatim passthrough
4//! (`POST /typesafe/v1/systemone`, see `server::jev_passthrough`).
5
6use std::collections::HashMap;
7use std::time::Duration;
8
9use serde_json::Value;
10
11use crate::error::GatewayError;
12
13/// TypeSafe's hosted API. Override with `TYPESAFE_BASE_URL` (self-hosted
14/// deployments, tests).
15pub const DEFAULT_BASE_URL: &str = "https://api.typesafe.ai";
16/// Model id resolving to the current stable Jev build, used when the client
17/// omits `model` from the request body.
18pub const DEFAULT_MODEL: &str = "jev-latest";
19
20#[derive(Debug, Clone)]
21pub struct JevNativeProvider {
22    http: reqwest::Client,
23    api_key: String,
24    base_url: String,
25}
26
27impl JevNativeProvider {
28    pub fn new(api_key: String, base_url: Option<String>, request_timeout: Duration) -> Self {
29        Self {
30            http: reqwest::Client::builder()
31                .timeout(request_timeout)
32                .build()
33                .unwrap(),
34            api_key,
35            base_url: base_url.unwrap_or_else(|| DEFAULT_BASE_URL.to_string()),
36        }
37    }
38
39    /// The lane is available when `TYPESAFE_API_KEY` is set and non-empty;
40    /// `TYPESAFE_BASE_URL` overrides the hosted endpoint.
41    pub fn from_env(env: &HashMap<String, String>, request_timeout: Duration) -> Option<Self> {
42        let api_key = env
43            .get("TYPESAFE_API_KEY")
44            .map(|s| s.trim())
45            .filter(|s| !s.is_empty())?;
46        let base_url = env
47            .get("TYPESAFE_BASE_URL")
48            .map(|s| s.trim())
49            .filter(|s| !s.is_empty())
50            .map(str::to_string);
51        Some(Self::new(api_key.to_string(), base_url, request_timeout))
52    }
53
54    /// Forward a System One request body verbatim. The caller renders the
55    /// upstream status/body — successes and errors both pass through
56    /// untranslated, like `VertexNativeProvider::passthrough_request`.
57    pub async fn evaluate(&self, body: Value) -> Result<reqwest::Response, GatewayError> {
58        self.http
59            .post(format!("{}/v1/systemone", self.base_url))
60            .bearer_auth(&self.api_key)
61            .json(&body)
62            .send()
63            .await
64            .map_err(|e| GatewayError::Upstream {
65                status: 502,
66                body: e.to_string(),
67            })
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn from_env_is_none_without_key() {
77        assert!(JevNativeProvider::from_env(&HashMap::new(), Duration::from_secs(1)).is_none());
78        let blank = HashMap::from([("TYPESAFE_API_KEY".to_string(), "  ".to_string())]);
79        assert!(JevNativeProvider::from_env(&blank, Duration::from_secs(1)).is_none());
80    }
81
82    #[test]
83    fn from_env_picks_up_key_and_base_url_override() {
84        let env = HashMap::from([
85            ("TYPESAFE_API_KEY".to_string(), "key".to_string()),
86            (
87                "TYPESAFE_BASE_URL".to_string(),
88                "http://localhost:9".to_string(),
89            ),
90        ]);
91        let provider = JevNativeProvider::from_env(&env, Duration::from_secs(1)).unwrap();
92        assert_eq!(provider.base_url, "http://localhost:9");
93    }
94
95    #[test]
96    fn defaults_to_the_hosted_api() {
97        let provider = JevNativeProvider::new("key".into(), None, Duration::from_secs(1));
98        assert_eq!(provider.base_url, DEFAULT_BASE_URL);
99    }
100}