diff --git a/api/tinyhumans.backend.json b/api/tinyhumans.backend.json index 4d733e4..ff28b2e 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": 231, - "totalOperationCount": 256, - "operationCount": 204, + "pathCount": 233, + "totalOperationCount": 258, + "operationCount": 206, "supplementalOperationCount": 13, "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 6ad257b..f9d5ed0 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 3a8922a..06d0b26 100644 --- a/tests/openapi_sync.rs +++ b/tests/openapi_sync.rs @@ -158,7 +158,8 @@ fn generated_rust_routes_match_the_public_manifest() { // last a year). // 203 -> 204: `POST /opencompany/instances/{slug}/update`, the // owner-triggered "update to latest" for a hosted company. - assert_eq!(manifest["source"]["operationCount"], 204); + // 204 -> 206: OpenRouter System One plus its TypeSafe-compatible alias. + assert_eq!(manifest["source"]["operationCount"], 206); // 14 -> 13: `GET /orchestration/v1/steering` left with that family. assert_eq!(manifest["source"]["supplementalOperationCount"], 13); // 37 -> 39: the two service-token operations on @@ -181,7 +182,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(), 204); + assert_eq!(rust_routes.len(), 206); 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;