From 38329ddc82bc054ed7115edda38c29d22bb6478d Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 20 Aug 2026 14:42:02 +0300 Subject: [PATCH] feat(api): add spend caps and API key spend-cap routes The OpenAPI manifest and generated route tables are updated to include a new spend caps resource with GET and PUT endpoints, plus a PUT endpoint on API keys for setting spend caps. The sync script now deduplicates supplemental routes that already appear in the spec, preventing duplicate entries in the generated route list. Several admin and webhook routes are also added to the unexposed routes list, and the corresponding assertion counts are updated to match the regenerated manifest. Auto-committed-on: macbook Co-authored-by: Medulla --- api/tinyhumans.backend.json | 29 +++++++++++++++++++++-------- scripts/sync-openapi.mjs | 14 +++++++++++++- src/generated_public_routes.rs | 8 ++++++++ src/lib.rs | 2 +- tests/openapi_sync.rs | 6 +++--- 5 files changed, 46 insertions(+), 13 deletions(-) diff --git a/api/tinyhumans.backend.json b/api/tinyhumans.backend.json index eef8ed1..d498a17 100644 --- a/api/tinyhumans.backend.json +++ b/api/tinyhumans.backend.json @@ -8,11 +8,11 @@ "url": "https://api.tinyhumans.ai/swagger.json", "title": "TinyHumans API", "version": "1.0.0", - "pathCount": 177, - "totalOperationCount": 198, - "operationCount": 212, + "pathCount": 228, + "totalOperationCount": 259, + "operationCount": 215, "supplementalOperationCount": 14, - "excludedAdminOperationCount": 32, + "excludedAdminOperationCount": 37, "excludedWebhookOperationCount": 12, "servers": [ "https://api.tinyhumans.ai/", @@ -119,14 +119,15 @@ "name": "apiKeys", "basePath": "/api-keys", "auth": "bearer", - "operationCount": 3, + "operationCount": 4, "tags": [ "API Keys" ], "routes": [ "DELETE /api-keys/{keyId}", "GET /api-keys", - "POST /api-keys" + "POST /api-keys", + "PUT /api-keys/{keyId}/spend-caps" ] }, { @@ -422,13 +423,25 @@ "POST /rewards/claim" ] }, + { + "name": "spendCaps", + "basePath": "/spend-caps", + "auth": "bearer", + "operationCount": 2, + "tags": [ + "Spend Caps" + ], + "routes": [ + "GET /spend-caps", + "PUT /spend-caps" + ] + }, { "name": "teams", "basePath": "/teams", "auth": "bearer", "operationCount": 17, "tags": [ - "OpenHuman parity", "Teams" ], "routes": [ @@ -488,7 +501,7 @@ "auth": "bearer", "operationCount": 6, "tags": [ - "OpenHuman parity" + "Webhooks" ], "routes": [ "DELETE /webhooks/core/{id}", diff --git a/scripts/sync-openapi.mjs b/scripts/sync-openapi.mjs index ae538bd..27faf8f 100644 --- a/scripts/sync-openapi.mjs +++ b/scripts/sync-openapi.mjs @@ -238,8 +238,11 @@ function buildManifest(spec) { excludedOperations.push({ method: method.toUpperCase(), path }); continue; } + // Secured by a per-integration shared secret the caller supplies, not by + // a user token, so it is not part of the public client surface. Counted + // with the admin exclusions below, which are derived from + // `excludedOperations` rather than tallied here. if (isCustomLlmSecretOperation(operation)) { - excludedAdminOperationCount += 1; excludedOperations.push({ method: method.toUpperCase(), path }); continue; } @@ -266,7 +269,16 @@ function buildManifest(spec) { const excludedAdminOperationCount = excludedOperations.length - excludedWebhookOperationCount; + // Skip any supplemental entry the spec already describes. The supplemental + // list exists for operations the DEPLOYED document omits; when this script is + // fed a local spec (`--input`) that does describe them, appending + // unconditionally emits the route twice and PUBLIC_ROUTES ends up with + // duplicates that `generated_rust_routes_match_the_public_manifest` rejects. + const describedOperations = new Set( + publicOperations.map(({ method, path }) => `${method} ${path}`), + ); for (const [method, path] of SUPPLEMENTAL_PUBLIC_OPERATIONS) { + if (describedOperations.has(`${method} ${path}`)) continue; publicOperations.push({ method, namespace: namespaceFor(path), diff --git a/src/generated_public_routes.rs b/src/generated_public_routes.rs index a1f8233..e838e63 100644 --- a/src/generated_public_routes.rs +++ b/src/generated_public_routes.rs @@ -68,6 +68,7 @@ pub const PUBLIC_ROUTES: &[(&str, &str)] = &[ ("GET", "/api-keys"), ("POST", "/api-keys"), ("DELETE", "/api-keys/{keyId}"), + ("PUT", "/api-keys/{keyId}/spend-caps"), ("GET", "/auth/{provider}/callback"), ("GET", "/auth/{provider}/connect"), ("GET", "/auth/{provider}/login"), @@ -182,6 +183,8 @@ pub const PUBLIC_ROUTES: &[(&str, &str)] = &[ ("POST", "/rewards/claim"), ("DELETE", "/rewards/discord"), ("GET", "/rewards/me"), + ("GET", "/spend-caps"), + ("PUT", "/spend-caps"), ("GET", "/teams"), ("GET", "/teams/{teamId}"), ("PUT", "/teams/{teamId}"), @@ -231,8 +234,11 @@ pub(crate) const UNEXPOSED_ROUTES: &[(&str, &str)] = &[ ("POST", "/admin/feedback/triage/{feedbackId}/merge"), ("POST", "/admin/feedback/triage/{feedbackId}/reject"), ("POST", "/admin/feedback/triage/{feedbackId}/reprocess"), + ("GET", "/admin/settings"), + ("PATCH", "/admin/settings/{key}"), ("POST", "/admin/users/{userId}/credits"), ("PATCH", "/admin/users/{userId}/medulla-access"), + ("PATCH", "/admin/users/{userId}/spend-caps"), ("DELETE", "/admin/users/{userId}/subscription"), ("POST", "/admin/users/{userId}/subscription"), ("POST", "/admin/users/credits/bulk"), @@ -247,12 +253,14 @@ pub(crate) const UNEXPOSED_ROUTES: &[(&str, &str)] = &[ ("GET", "/feedback/admin/triage/{id}"), ("POST", "/feedback/admin/triage/{id}/approve"), ("PATCH", "/feedback/admin/triage/{id}/draft"), + ("POST", "/feedback/admin/triage/{id}/link"), ("POST", "/feedback/admin/triage/{id}/merge"), ("POST", "/feedback/admin/triage/{id}/reject"), ("POST", "/feedback/admin/triage/{id}/reprocess"), ("GET", "/invite/campaign"), ("POST", "/invite/campaign"), ("DELETE", "/invite/campaign/{codeId}"), + ("POST", "/voice-agent/chat/completions"), ("POST", "/webhooks/composio"), ("POST", "/webhooks/discord"), ("POST", "/webhooks/github"), diff --git a/src/lib.rs b/src/lib.rs index cc807b4..6e3c860 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -494,7 +494,7 @@ mod exclusion_tests { // Pinned so the list can only ever be reviewed upward. A regenerated // spec that stopped describing admin or webhook routes would otherwise // shrink this list and silently unblock them at the raw transport. - assert_eq!(UNEXPOSED_ROUTES.len(), 44); + assert_eq!(UNEXPOSED_ROUTES.len(), 49); for (method, template) in UNEXPOSED_ROUTES { let concrete_path = template .split('/') diff --git a/tests/openapi_sync.rs b/tests/openapi_sync.rs index e8732e9..81adb49 100644 --- a/tests/openapi_sync.rs +++ b/tests/openapi_sync.rs @@ -134,11 +134,11 @@ fn generated_rust_routes_match_the_public_manifest() { .collect::>(); let rust_routes = PUBLIC_ROUTES.iter().copied().collect::>(); - assert_eq!(manifest["source"]["operationCount"], 212); + assert_eq!(manifest["source"]["operationCount"], 215); assert_eq!(manifest["source"]["supplementalOperationCount"], 14); - assert_eq!(manifest["source"]["excludedAdminOperationCount"], 32); + assert_eq!(manifest["source"]["excludedAdminOperationCount"], 37); assert_eq!(manifest["source"]["excludedWebhookOperationCount"], 12); - assert_eq!(rust_routes.len(), 212); + assert_eq!(rust_routes.len(), 215); assert_eq!(rust_routes, manifest_routes); assert!(rust_routes .iter()