From ba7b4a65d1b28e3e6ca8ce207c73ad975deb1938 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 20 Sep 2026 20:38:46 +0300 Subject: [PATCH] feat: add OpenRouter System One client support Co-authored-by: Medulla --- api/tinyhumans.backend.json | 10 +++--- src/api/agent_integrations/openrouter.rs | 16 +++++++++- src/generated_public_routes.rs | 2 ++ tests/openapi_sync.rs | 5 +-- tests/openrouter.rs | 39 ++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 7 deletions(-) diff --git a/api/tinyhumans.backend.json b/api/tinyhumans.backend.json index 62d1d16..d6b7069 100644 --- a/api/tinyhumans.backend.json +++ b/api/tinyhumans.backend.json @@ -8,9 +8,9 @@ "url": "https://api.tinyhumans.ai/swagger.json", "title": "TinyHumans API", "version": "1.0.0", - "pathCount": 255, - "totalOperationCount": 288, - "operationCount": 237, + "pathCount": 257, + "totalOperationCount": 290, + "operationCount": 239, "supplementalOperationCount": 14, "excludedAdminOperationCount": 44, "excludedWebhookOperationCount": 12, @@ -34,7 +34,7 @@ "name": "agentIntegrations", "basePath": "/agent-integrations", "auth": "mixed", - "operationCount": 73, + "operationCount": 75, "tags": [ "Agent Integrations", "OpenHuman parity" @@ -99,6 +99,8 @@ "POST /agent-integrations/openrouter/embeddings", "POST /agent-integrations/openrouter/images", "POST /agent-integrations/openrouter/messages", + "POST /agent-integrations/openrouter/systemone", + "POST /agent-integrations/openrouter/v1/systemone", "POST /agent-integrations/openrouter/videos", "POST /agent-integrations/parallel/chat", "POST /agent-integrations/parallel/dataset", diff --git a/src/api/agent_integrations/openrouter.rs b/src/api/agent_integrations/openrouter.rs index 9eb6b25..970f668 100644 --- a/src/api/agent_integrations/openrouter.rs +++ b/src/api/agent_integrations/openrouter.rs @@ -9,10 +9,11 @@ //! [`DynamicResponse`], matching how [`crate::api::inference`] treats the //! OpenAI-compatible surface. //! -//! Four request shapes share the one provider: +//! Five request shapes share the one provider: //! //! - OpenAI chat and text completions. //! - Anthropic-format messages, which the Anthropic SDK can call directly. +//! - TypeSafe System One requests for Jev typed decisions. //! - Embeddings, which resolve against a **separate** upstream catalog — an //! embedding slug is not valid on the chat routes, or vice versa. //! - Image and video generation, billed per generation rather than per token @@ -182,6 +183,19 @@ impl AgentIntegrationsApi<'_> { .await } + /// Evaluate typed questions with a Jev System One model. + /// + /// The request and response use TypeSafe's wire shape. For a fully typed + /// client, point `tinyjevclient` at the companion `/v1/systemone` alias by + /// using `/agent-integrations/openrouter` as its base URL. + pub async fn openrouter_system_one( + &self, + request: &impl Serialize, + ) -> Result { + self.passthrough("/agent-integrations/openrouter/systemone", request) + .await + } + /// List embedding models. A separate catalog from the chat models. pub async fn list_openrouter_embedding_models( &self, diff --git a/src/generated_public_routes.rs b/src/generated_public_routes.rs index db706c1..0684042 100644 --- a/src/generated_public_routes.rs +++ b/src/generated_public_routes.rs @@ -51,6 +51,8 @@ pub const PUBLIC_ROUTES: &[(&str, &str)] = &[ ("GET", "/agent-integrations/openrouter/images/models"), ("POST", "/agent-integrations/openrouter/messages"), ("GET", "/agent-integrations/openrouter/models"), + ("POST", "/agent-integrations/openrouter/systemone"), + ("POST", "/agent-integrations/openrouter/v1/systemone"), ("POST", "/agent-integrations/openrouter/videos"), ("GET", "/agent-integrations/openrouter/videos/{jobId}"), ("GET", "/agent-integrations/openrouter/videos/{jobId}/content"), diff --git a/tests/openapi_sync.rs b/tests/openapi_sync.rs index 317214c..28357e4 100644 --- a/tests/openapi_sync.rs +++ b/tests/openapi_sync.rs @@ -216,7 +216,8 @@ fn generated_rust_routes_match_the_public_manifest() { // 234 -> 237: the dashboard's usage reads — // `GET /opencompany/instances/usage`, `GET /payments/credits/ledger` and // `GET /payments/credits/ledger/export`. - assert_eq!(manifest["source"]["operationCount"], 237); + // 237 -> 239: OpenRouter System One plus its TypeSafe-compatible alias. + assert_eq!(manifest["source"]["operationCount"], 239); assert_eq!(manifest["source"]["supplementalOperationCount"], 14); // 37 -> 39: the two service-token operations on // `/opencompany/instances/{slug}/inference-key`. They are counted with the @@ -238,7 +239,7 @@ fn generated_rust_routes_match_the_public_manifest() { // post's cover and body figures. Same token as the other blog writes. assert_eq!(manifest["source"]["excludedAdminOperationCount"], 44); assert_eq!(manifest["source"]["excludedWebhookOperationCount"], 12); - assert_eq!(rust_routes.len(), 237); + assert_eq!(rust_routes.len(), 239); assert_eq!(rust_routes, manifest_routes); assert!(rust_routes .iter() diff --git a/tests/openrouter.rs b/tests/openrouter.rs index 51cff7c..22a7655 100644 --- a/tests/openrouter.rs +++ b/tests/openrouter.rs @@ -217,6 +217,45 @@ async fn messages_speaks_the_anthropic_shape() { assert_eq!(response["usage"]["input_tokens"], 12); } +#[tokio::test] +async fn system_one_preserves_the_typesafe_wire_shape() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/agent-integrations/openrouter/systemone")) + .and(body_json(json!({ + "model": "jev-latest", + "state": {"ticket": "I was charged twice"}, + "questions": { + "refund": {"type": "noul", "instructions": "Is a refund requested?"} + } + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "id": "gen-dec-1", + "model": "typesafe/jev-1.13-20260917", + "provider": "TypeSafe", + "answers": {"refund": {"type": "noul", "noul": 0.98}}, + "usage": {"input_tokens": 275, "output_tokens": 20, "cost": 0.00003} + }))) + .mount(&server) + .await; + + let response = TinyHumansClient::new(server.uri()) + .agent_integrations() + .openrouter_system_one(&json!({ + "model": "jev-latest", + "state": {"ticket": "I was charged twice"}, + "questions": { + "refund": {"type": "noul", "instructions": "Is a refund requested?"} + } + })) + .await + .unwrap(); + + assert_eq!(response["answers"]["refund"]["type"], "noul"); + assert_eq!(response["answers"]["refund"]["noul"], 0.98); + assert_eq!(response["usage"]["cost"], 0.00003); +} + #[tokio::test] async fn embedding_models_come_from_their_own_catalog() { let server = MockServer::start().await;