diff --git a/api-reference/openapi.json b/api-reference/openapi.json index 1361579..42c9519 100644 --- a/api-reference/openapi.json +++ b/api-reference/openapi.json @@ -211,28 +211,18 @@ ] } }, - "/v1/admin/feature-requests": { + "/v1/admin/aml/allowlist": { "get": { "tags": [ - "Feature Requests" + "Admin" ], - "summary": "List aggregated feature requests for admins.", - "operationId": "list_admin_feature_requests", + "summary": "List AML allowlist entries (Admin only)", + "operationId": "list_aml_allowlist", "parameters": [ - { - "name": "kind", - "in": "query", - "required": false, - "schema": { - "type": [ - "string", - "null" - ] - } - }, { "name": "limit", "in": "query", + "description": "Maximum number of allowlist entries to return (default: 100)", "required": false, "schema": { "type": "integer", @@ -242,6 +232,7 @@ { "name": "offset", "in": "query", + "description": "Number of allowlist entries to skip (default: 0)", "required": false, "schema": { "type": "integer", @@ -251,17 +242,17 @@ ], "responses": { "200": { - "description": "Aggregated feature requests", + "description": "AML allowlist entries retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminFeatureRequestListResponse" + "$ref": "#/components/schemas/ListAdminAmlAllowlistResponse" } } } }, "400": { - "description": "Invalid parameters", + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -280,6 +271,16 @@ } } }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -296,102 +297,30 @@ "session_token": [] } ] - } - }, - "/v1/admin/invitation-email-deliveries": { - "get": { + }, + "post": { "tags": [ "Admin" ], - "summary": "List organization invitation email deliveries (Admin only)", - "description": "Returns delivery metadata for organization invitation emails without exposing invitation tokens.", - "operationId": "list_invitation_email_deliveries", - "parameters": [ - { - "name": "limit", - "in": "query", - "description": "Maximum number of deliveries to return (default: 100)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Number of deliveries to skip (default: 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "organization_id", - "in": "query", - "description": "Filter by organization ID", - "required": false, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "recipient_email", - "in": "query", - "description": "Case-insensitive recipient email substring filter", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "email_status", - "in": "query", - "description": "Filter by email delivery status", - "required": false, - "schema": { - "$ref": "#/components/schemas/InvitationEmailStatus" - } - }, - { - "name": "invitation_status", - "in": "query", - "description": "Filter by invitation status", - "required": false, - "schema": { - "$ref": "#/components/schemas/InvitationStatus" - } - }, - { - "name": "created_after", - "in": "query", - "description": "Only invitations created at or after this timestamp", - "required": false, - "schema": { - "type": "string", - "format": "date-time" + "summary": "Add or update an AML allowlist entry (Admin only)", + "operationId": "upsert_aml_allowlist_entry", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpsertAmlAllowlistEntryRequest" + } } }, - { - "name": "created_before", - "in": "query", - "description": "Only invitations created at or before this timestamp", - "required": false, - "schema": { - "type": "string", - "format": "date-time" - } - } - ], + "required": true + }, "responses": { "200": { - "description": "Invitation email deliveries retrieved successfully", + "description": "AML allowlist entry upserted successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListAdminInvitationEmailDeliveriesResponse" + "$ref": "#/components/schemas/AdminAmlAllowlistEntryResponse" } } } @@ -416,6 +345,16 @@ } } }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -434,38 +373,40 @@ ] } }, - "/v1/admin/invitation-email-deliveries/{invitation_id}/resend": { - "post": { + "/v1/admin/aml/allowlist/{account_id}": { + "delete": { "tags": [ "Admin" ], - "summary": "Resend a single organization invitation email (Admin only)", - "operationId": "resend_invitation_email", + "summary": "Remove an AML allowlist entry (Admin only)", + "operationId": "delete_aml_allowlist_entry", "parameters": [ { - "name": "invitation_id", + "name": "account_id", "in": "path", - "description": "Invitation ID", + "description": "NEAR account ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], "responses": { - "200": { - "description": "Invitation email resend attempted", + "204": { + "description": "AML allowlist entry removed successfully" + }, + "400": { + "description": "Invalid request", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminInvitationEmailResendResultResponse" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "400": { - "description": "Invitation is not pending or has expired", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -474,8 +415,8 @@ } } }, - "401": { - "description": "Unauthorized", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -485,7 +426,7 @@ } }, "404": { - "description": "Invitation not found", + "description": "AML allowlist entry not found", "content": { "application/json": { "schema": { @@ -512,19 +453,18 @@ ] } }, - "/v1/admin/models": { + "/v1/admin/aml/reports": { "get": { "tags": [ "Admin" ], - "summary": "List all models (Admin only)", - "description": "Returns a paginated list of all models in the system. By default, only active models are returned.\nUse `include_inactive=true` to also include disabled models.", - "operationId": "list_models", + "summary": "List AML reports (Admin only)", + "operationId": "list_aml_reports", "parameters": [ { "name": "limit", "in": "query", - "description": "Maximum number of models to return (default: 100)", + "description": "Maximum number of reports to return (default: 100)", "required": false, "schema": { "type": "integer", @@ -534,30 +474,31 @@ { "name": "offset", "in": "query", - "description": "Number of models to skip (default: 0)", + "description": "Number of reports to skip (default: 0)", "required": false, "schema": { "type": "integer", "format": "int64" } - }, - { - "name": "include_inactive", - "in": "query", - "description": "Whether to include inactive (disabled) models (default: false)", - "required": false, - "schema": { - "type": "boolean" - } } ], "responses": { "200": { - "description": "Models retrieved successfully", + "description": "AML reports retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminModelListResponse" + "$ref": "#/components/schemas/ListAdminAmlReportsResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -572,6 +513,16 @@ } } }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -588,19 +539,31 @@ "session_token": [] } ] - }, + } + }, + "/v1/admin/aml/reports/{report_id}/status": { "patch": { "tags": [ "Admin" ], - "summary": "Batch upsert models metadata (Admin only)", - "description": "Upserts (inserts or updates) pricing and metadata for one or more models. Only authenticated admins can perform this operation.\nThe body should be an array of objects where each key is a model name and the value is the model data.", - "operationId": "batch_upsert_models", + "summary": "Update AML report active status (Admin only)", + "operationId": "update_aml_report_status", + "parameters": [ + { + "name": "report_id", + "in": "path", + "description": "AML report UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HashMap" + "$ref": "#/components/schemas/UpdateAmlReportStatusRequest" } } }, @@ -608,14 +571,11 @@ }, "responses": { "200": { - "description": "Models upserted successfully", + "description": "AML report status updated successfully", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ModelWithPricing" - } + "$ref": "#/components/schemas/AdminAmlReportResponse" } } } @@ -640,6 +600,26 @@ } } }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "AML report not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -658,37 +638,57 @@ ] } }, - "/v1/admin/models/deprecate": { - "post": { + "/v1/admin/feature-requests": { + "get": { "tags": [ - "Admin" + "Feature Requests" + ], + "summary": "List aggregated feature requests for admins.", + "operationId": "list_admin_feature_requests", + "parameters": [ + { + "name": "kind", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + } ], - "summary": "Deprecate a model in favor of another (Admin only)", - "description": "Atomically marks `modelId` as deprecated and routes its traffic to\n`successorModelId`:\n1. Adds `modelId` as an alias of `successorModelId`, so existing clients\n sending `model: \"\"` keep working — the alias resolver rewrites\n the request-side `model` field to the successor before backend dispatch,\n and the response's `model` field reflects the canonical (successor) name.\n2. Re-points any pre-existing inbound aliases of `modelId` at the\n successor, so historical aliases keep resolving.\n3. Sets `modelId.isActive = false` so it is hidden from public\n `GET /v1/models` and from `GET /v1/admin/models` unless\n `include_inactive=true`.\n4. Records a `model_history` entry for audit purposes.\n\nAll steps run in a single DB transaction. If the successor is inactive or\neither model is missing, returns 404 without modifying state.", - "operationId": "deprecate_model", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeprecateModelRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Model deprecated successfully", + "description": "Aggregated feature requests", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeprecateModelResponse" + "$ref": "#/components/schemas/AdminFeatureRequestListResponse" } } } }, "400": { - "description": "Invalid request (e.g. self-deprecation, empty model id)", + "description": "Invalid parameters", "content": { "application/json": { "schema": { @@ -707,16 +707,6 @@ } } }, - "404": { - "description": "Either model not found, or successor is not active", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -735,27 +725,29 @@ ] } }, - "/v1/admin/models/pricing-changes": { + "/v1/admin/invitation-email-deliveries": { "get": { "tags": [ "Admin" ], - "summary": "List scheduled pricing changes (Admin only).", - "operationId": "list_model_pricing_changes", + "summary": "List organization invitation email deliveries (Admin only)", + "description": "Returns delivery metadata for organization invitation emails without exposing invitation tokens.", + "operationId": "list_invitation_email_deliveries", "parameters": [ { - "name": "status", + "name": "limit", "in": "query", - "description": "Filter by status: pending, applying, applied, cancelled, failed. Omit for all.", + "description": "Maximum number of deliveries to return (default: 100)", "required": false, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } }, { - "name": "limit", + "name": "offset", "in": "query", - "description": "Maximum number of changes to return (default: 100)", + "description": "Number of deliveries to skip (default: 0)", "required": false, "schema": { "type": "integer", @@ -763,23 +755,70 @@ } }, { - "name": "offset", + "name": "organization_id", "in": "query", - "description": "Number of changes to skip (default: 0)", + "description": "Filter by organization ID", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": "string", + "format": "uuid" + } + }, + { + "name": "recipient_email", + "in": "query", + "description": "Case-insensitive recipient email substring filter", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "email_status", + "in": "query", + "description": "Filter by email delivery status", + "required": false, + "schema": { + "$ref": "#/components/schemas/InvitationEmailStatus" + } + }, + { + "name": "invitation_status", + "in": "query", + "description": "Filter by invitation status", + "required": false, + "schema": { + "$ref": "#/components/schemas/InvitationStatus" + } + }, + { + "name": "created_after", + "in": "query", + "description": "Only invitations created at or after this timestamp", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "created_before", + "in": "query", + "description": "Only invitations created at or before this timestamp", + "required": false, + "schema": { + "type": "string", + "format": "date-time" } } ], "responses": { "200": { - "description": "Scheduled pricing changes", + "description": "Invitation email deliveries retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListPricingChangesResponse" + "$ref": "#/components/schemas/ListAdminInvitationEmailDeliveriesResponse" } } } @@ -822,37 +861,38 @@ ] } }, - "/v1/admin/models/pricing-changes/confirm": { + "/v1/admin/invitation-email-deliveries/{invitation_id}/resend": { "post": { "tags": [ "Admin" ], - "summary": "Confirm a batch of scheduled pricing changes and notify affected admins (Admin only).", - "description": "Persists the schedule (the background scheduler applies each change at its\neffective date) and sends one consolidated email per affected recipient.", - "operationId": "confirm_model_pricing_changes", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PricingChangeBatchRequest" - } + "summary": "Resend a single organization invitation email (Admin only)", + "operationId": "resend_invitation_email", + "parameters": [ + { + "name": "invitation_id", + "in": "path", + "description": "Invitation ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } - }, - "required": true - }, + } + ], "responses": { "200": { - "description": "Pricing changes scheduled and notifications attempted", + "description": "Invitation email resend attempted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PricingChangeConfirmResponse" + "$ref": "#/components/schemas/AdminInvitationEmailResendResultResponse" } } } }, "400": { - "description": "Invalid request", + "description": "Invitation is not pending or has expired", "content": { "application/json": { "schema": { @@ -872,7 +912,7 @@ } }, "404": { - "description": "Model not found or inactive", + "description": "Invitation not found", "content": { "application/json": { "schema": { @@ -881,8 +921,76 @@ } } }, - "409": { - "description": "A pending pricing change already exists for a model", + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/admin/models": { + "get": { + "tags": [ + "Admin" + ], + "summary": "List all models (Admin only)", + "description": "Returns a paginated list of all models in the system. By default, only active models are returned.\nUse `include_inactive=true` to also include disabled models.", + "operationId": "list_models", + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Maximum number of models to return (default: 100)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Number of models to skip (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "include_inactive", + "in": "query", + "description": "Whether to include inactive (disabled) models (default: false)", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Models retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminModelListResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -907,20 +1015,19 @@ "session_token": [] } ] - } - }, - "/v1/admin/models/pricing-changes/preview": { - "post": { + }, + "patch": { "tags": [ "Admin" ], - "summary": "Preview a batch of scheduled pricing changes without mutating state (Admin only).", - "operationId": "preview_model_pricing_changes", + "summary": "Batch upsert models metadata (Admin only)", + "description": "Upserts (inserts or updates) pricing and metadata for one or more models. Only authenticated admins can perform this operation.\nThe body should be an array of objects where each key is a model name and the value is the model data.", + "operationId": "batch_upsert_models", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PricingChangeBatchRequest" + "$ref": "#/components/schemas/HashMap" } } }, @@ -928,11 +1035,14 @@ }, "responses": { "200": { - "description": "Pricing change notification preview", + "description": "Models upserted successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PricingChangePreviewResponse" + "type": "array", + "items": { + "$ref": "#/components/schemas/ModelWithPricing" + } } } } @@ -957,16 +1067,6 @@ } } }, - "404": { - "description": "Model not found or inactive", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -985,32 +1085,41 @@ ] } }, - "/v1/admin/models/pricing-changes/{id}": { - "delete": { + "/v1/admin/models/deprecate": { + "post": { "tags": [ "Admin" ], - "summary": "Cancel a pending scheduled pricing change (Admin only).", - "operationId": "cancel_model_pricing_change", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Scheduled pricing change ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "summary": "Deprecate a model in favor of another (Admin only)", + "description": "Atomically marks `modelId` as deprecated and routes its traffic to\n`successorModelId`:\n1. Adds `modelId` as an alias of `successorModelId`, so existing clients\n sending `model: \"\"` keep working — the alias resolver rewrites\n the request-side `model` field to the successor before backend dispatch,\n and the response's `model` field reflects the canonical (successor) name.\n2. Re-points any pre-existing inbound aliases of `modelId` at the\n successor, so historical aliases keep resolving.\n3. Sets `modelId.isActive = false` so it is hidden from public\n `GET /v1/models` and from `GET /v1/admin/models` unless\n `include_inactive=true`.\n4. Records a `model_history` entry for audit purposes.\n\nAll steps run in a single DB transaction. If the successor is inactive or\neither model is missing, returns 404 without modifying state.", + "operationId": "deprecate_model", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeprecateModelRequest" + } } - } - ], + }, + "required": true + }, "responses": { "200": { - "description": "Pricing change cancelled", + "description": "Model deprecated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ScheduledPricingChangeDto" + "$ref": "#/components/schemas/DeprecateModelResponse" + } + } + } + }, + "400": { + "description": "Invalid request (e.g. self-deprecation, empty model id)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -1026,7 +1135,7 @@ } }, "404": { - "description": "Pricing change not found or no longer pending", + "description": "Either model not found, or successor is not active", "content": { "application/json": { "schema": { @@ -1053,41 +1162,57 @@ ] } }, - "/v1/admin/models/{model_name}": { - "delete": { + "/v1/admin/models/pricing-changes": { + "get": { "tags": [ "Admin" ], - "summary": "Delete a model (Admin only)", - "description": "Soft deletes a model by setting is_active to false. This preserves historical usage records\nthat reference the model name while preventing it from being used in new requests.\n\n**Note:** Model names containing forward slashes (e.g., \"Qwen/Qwen3-30B-A3B-Instruct-2507\") must be URL-encoded.\nFor example, use \"Qwen%2FQwen3-30B-A3B-Instruct-2507\" in the URL path.", - "operationId": "delete_model", + "summary": "List scheduled pricing changes (Admin only).", + "operationId": "list_model_pricing_changes", "parameters": [ { - "name": "model_name", - "in": "path", - "description": "Model name to delete (URL-encode if it contains slashes)", - "required": true, + "name": "status", + "in": "query", + "description": "Filter by status: pending, applying, applied, cancelled, failed. Omit for all.", + "required": false, "schema": { "type": "string" } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of changes to return (default: 100)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Number of changes to skip (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeleteModelRequest" + "responses": { + "200": { + "description": "Scheduled pricing changes", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListPricingChangesResponse" + } } } }, - "required": true - }, - "responses": { - "204": { - "description": "Model deleted successfully" - }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -1096,8 +1221,8 @@ } } }, - "404": { - "description": "Model not found", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -1124,28 +1249,19 @@ ] } }, - "/v1/admin/models/{model_name}/deprecation/confirm": { + "/v1/admin/models/pricing-changes/confirm": { "post": { "tags": [ "Admin" ], - "summary": "Confirm a planned model deprecation, update the catalog, and notify affected admins.", - "operationId": "confirm_model_deprecation", - "parameters": [ - { - "name": "model_name", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], + "summary": "Confirm a batch of scheduled pricing changes and notify affected admins (Admin only).", + "description": "Persists the schedule (the background scheduler applies each change at its\neffective date) and sends one consolidated email per affected recipient.", + "operationId": "confirm_model_pricing_changes", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelDeprecationRequest" + "$ref": "#/components/schemas/PricingChangeBatchRequest" } } }, @@ -1153,11 +1269,11 @@ }, "responses": { "200": { - "description": "Deprecation confirmed and notifications attempted", + "description": "Pricing changes scheduled and notifications attempted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelDeprecationConfirmResponse" + "$ref": "#/components/schemas/PricingChangeConfirmResponse" } } } @@ -1183,7 +1299,17 @@ } }, "404": { - "description": "Model or successor not found", + "description": "Model not found or inactive", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "A pending pricing change already exists for a model", "content": { "application/json": { "schema": { @@ -1210,28 +1336,18 @@ ] } }, - "/v1/admin/models/{model_name}/deprecation/preview": { + "/v1/admin/models/pricing-changes/preview": { "post": { "tags": [ "Admin" ], - "summary": "Preview affected admins for a planned model deprecation (Admin only).", - "operationId": "preview_model_deprecation", - "parameters": [ - { - "name": "model_name", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], + "summary": "Preview a batch of scheduled pricing changes without mutating state (Admin only).", + "operationId": "preview_model_pricing_changes", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelDeprecationRequest" + "$ref": "#/components/schemas/PricingChangeBatchRequest" } } }, @@ -1239,11 +1355,11 @@ }, "responses": { "200": { - "description": "Deprecation notification preview", + "description": "Pricing change notification preview", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelDeprecationPreviewResponse" + "$ref": "#/components/schemas/PricingChangePreviewResponse" } } } @@ -1269,7 +1385,7 @@ } }, "404": { - "description": "Model or successor not found", + "description": "Model not found or inactive", "content": { "application/json": { "schema": { @@ -1296,52 +1412,32 @@ ] } }, - "/v1/admin/models/{model_name}/history": { - "get": { + "/v1/admin/models/pricing-changes/{id}": { + "delete": { "tags": [ "Admin" ], - "summary": "Get complete history for a model (Admin only)", - "description": "Returns the complete history for a specific model, showing all changes over time including pricing,\ncontext length, display name, and description.\n\n**Note:** Model names containing forward slashes (e.g., \"Qwen/Qwen3-30B-A3B-Instruct-2507\") must be URL-encoded.\nFor example, use \"Qwen%2FQwen3-30B-A3B-Instruct-2507\" in the URL path.", - "operationId": "get_model_history", + "summary": "Cancel a pending scheduled pricing change (Admin only).", + "operationId": "cancel_model_pricing_change", "parameters": [ { - "name": "model_name", + "name": "id", "in": "path", - "description": "Model name to get complete history for (URL-encode if it contains slashes)", + "description": "Scheduled pricing change ID", "required": true, "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "description": "Maximum number of history entries to return (default: 50)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Number of history entries to skip (default: 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" + "type": "string", + "format": "uuid" } } ], "responses": { "200": { - "description": "Model history retrieved successfully", + "description": "Pricing change cancelled", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelHistoryResponse" + "$ref": "#/components/schemas/ScheduledPricingChangeDto" } } } @@ -1357,7 +1453,7 @@ } }, "404": { - "description": "Model not found", + "description": "Pricing change not found or no longer pending", "content": { "application/json": { "schema": { @@ -1384,37 +1480,39 @@ ] } }, - "/v1/admin/organizations/{org_id}": { - "get": { + "/v1/admin/models/{model_name}": { + "delete": { "tags": [ "Admin" ], - "summary": "Get a single organization by id (Admin only)", - "description": "Returns one organization with its spend limit and usage. Only authenticated\nadmins can perform this operation. Returns 404 if the organization does not\nexist or is inactive (consistent with the admin organizations list, which\nhides inactive orgs).", - "operationId": "get_organization", + "summary": "Delete a model (Admin only)", + "description": "Soft deletes a model by setting is_active to false. This preserves historical usage records\nthat reference the model name while preventing it from being used in new requests.\n\n**Note:** Model names containing forward slashes (e.g., \"Qwen/Qwen3-30B-A3B-Instruct-2507\") must be URL-encoded.\nFor example, use \"Qwen%2FQwen3-30B-A3B-Instruct-2507\" in the URL path.", + "operationId": "delete_model", "parameters": [ { - "name": "org_id", + "name": "model_name", "in": "path", - "description": "Organization ID", + "description": "Model name to delete (URL-encode if it contains slashes)", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], - "responses": { - "200": { - "description": "Organization retrieved successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AdminOrganizationResponse" - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteModelRequest" } } }, + "required": true + }, + "responses": { + "204": { + "description": "Model deleted successfully" + }, "401": { "description": "Unauthorized", "content": { @@ -1426,7 +1524,7 @@ } }, "404": { - "description": "Organization not found", + "description": "Model not found", "content": { "application/json": { "schema": { @@ -1453,32 +1551,50 @@ ] } }, - "/v1/admin/organizations/{org_id}/concurrent-limit": { - "get": { + "/v1/admin/models/{model_name}/deprecation/confirm": { + "post": { "tags": [ "Admin" ], - "summary": "Get organization concurrent request limit (Admin only)", - "description": "Returns the current concurrent request limit for an organization.\nIf no custom limit is set, returns null for concurrent_limit and the default (64) for effective_limit.", - "operationId": "get_organization_concurrent_limit", + "summary": "Confirm a planned model deprecation, update the catalog, and notify affected admins.", + "operationId": "confirm_model_deprecation", "parameters": [ { - "name": "org_id", + "name": "model_name", "in": "path", - "description": "The organization's ID (as a UUID)", "required": true, "schema": { "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelDeprecationRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Concurrent limit retrieved successfully", + "description": "Deprecation confirmed and notifications attempted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetOrganizationConcurrentLimitResponse" + "$ref": "#/components/schemas/ModelDeprecationConfirmResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -1494,7 +1610,7 @@ } }, "404": { - "description": "Organization not found", + "description": "Model or successor not found", "content": { "application/json": { "schema": { @@ -1519,19 +1635,19 @@ "session_token": [] } ] - }, - "patch": { + } + }, + "/v1/admin/models/{model_name}/deprecation/preview": { + "post": { "tags": [ "Admin" ], - "summary": "Update organization concurrent request limit (Admin only)", - "description": "Updates the maximum concurrent requests allowed per model for an organization.\nSet to null to use the default limit (64).\nChanges take effect within 5 minutes due to caching.", - "operationId": "update_organization_concurrent_limit", + "summary": "Preview affected admins for a planned model deprecation (Admin only).", + "operationId": "preview_model_deprecation", "parameters": [ { - "name": "org_id", + "name": "model_name", "in": "path", - "description": "The organization's ID (as a UUID)", "required": true, "schema": { "type": "string" @@ -1542,7 +1658,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateOrganizationConcurrentLimitRequest" + "$ref": "#/components/schemas/ModelDeprecationRequest" } } }, @@ -1550,11 +1666,11 @@ }, "responses": { "200": { - "description": "Concurrent limit updated successfully", + "description": "Deprecation notification preview", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateOrganizationConcurrentLimitResponse" + "$ref": "#/components/schemas/ModelDeprecationPreviewResponse" } } } @@ -1580,7 +1696,7 @@ } }, "404": { - "description": "Organization not found", + "description": "Model or successor not found", "content": { "application/json": { "schema": { @@ -1607,52 +1723,52 @@ ] } }, - "/v1/admin/organizations/{org_id}/limits": { - "patch": { + "/v1/admin/models/{model_name}/history": { + "get": { "tags": [ "Admin" ], - "summary": "Update organization limits (Admin only)", - "description": "Updates spending limits for a specific organization. This endpoint is typically called by\na billing service with an admin API key when a customer makes a purchase.", - "operationId": "update_organization_limits", + "summary": "Get complete history for a model (Admin only)", + "description": "Returns the complete history for a specific model, showing all changes over time including pricing,\ncontext length, display name, and description.\n\n**Note:** Model names containing forward slashes (e.g., \"Qwen/Qwen3-30B-A3B-Instruct-2507\") must be URL-encoded.\nFor example, use \"Qwen%2FQwen3-30B-A3B-Instruct-2507\" in the URL path.", + "operationId": "get_model_history", "parameters": [ { - "name": "org_id", + "name": "model_name", "in": "path", - "description": "Organization ID to update limits for", + "description": "Model name to get complete history for (URL-encode if it contains slashes)", "required": true, "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrganizationLimitsRequest" - } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of history entries to return (default: 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" } }, - "required": true - }, + { + "name": "offset", + "in": "query", + "description": "Number of history entries to skip (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], "responses": { "200": { - "description": "Organization limits updated successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrganizationLimitsResponse" - } - } - } - }, - "400": { - "description": "Invalid request", + "description": "Model history retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ModelHistoryResponse" } } } @@ -1668,7 +1784,7 @@ } }, "404": { - "description": "Organization not found", + "description": "Model not found", "content": { "application/json": { "schema": { @@ -1695,52 +1811,33 @@ ] } }, - "/v1/admin/organizations/{org_id}/limits/history": { + "/v1/admin/organizations/{org_id}": { "get": { "tags": [ "Admin" ], - "summary": "Get limits history for an organization (Admin only)", - "description": "Returns the complete limits history for a specific organization, showing all limits changes over time.\nGet limits history for an organization (Admin only)\n\nReturns the complete limits history for a specific organization, showing all limits changes over time.", - "operationId": "get_organization_limits_history", + "summary": "Get a single organization by id (Admin only)", + "description": "Returns one organization with its spend limit and usage. Only authenticated\nadmins can perform this operation. Returns 404 if the organization does not\nexist or is inactive (consistent with the admin organizations list, which\nhides inactive orgs).", + "operationId": "get_organization", "parameters": [ { "name": "org_id", "in": "path", - "description": "The organization's ID (as a UUID)", + "description": "Organization ID", "required": true, "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "description": "Maximum number of history records to return (default: 50)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Number of records to skip (default: 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" + "type": "string", + "format": "uuid" } } ], "responses": { "200": { - "description": "Limits history retrieved successfully", + "description": "Organization retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrgLimitsHistoryResponse" + "$ref": "#/components/schemas/AdminOrganizationResponse" } } } @@ -1783,63 +1880,32 @@ ] } }, - "/v1/admin/organizations/{org_id}/members": { + "/v1/admin/organizations/{org_id}/concurrent-limit": { "get": { "tags": [ "Admin" ], - "summary": "List members of a specific organization (Admin only)", - "description": "Returns the members of the given organization with full user details\n(email, last login, active status), consistent with `/v1/admin/users`.\nOnly authenticated admins can perform this operation.", - "operationId": "list_organization_members", + "summary": "Get organization concurrent request limit (Admin only)", + "description": "Returns the current concurrent request limit for an organization.\nIf no custom limit is set, returns null for concurrent_limit and the default (64) for effective_limit.", + "operationId": "get_organization_concurrent_limit", "parameters": [ { "name": "org_id", "in": "path", - "description": "Organization ID", + "description": "The organization's ID (as a UUID)", "required": true, "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "limit", - "in": "query", - "description": "Maximum number of members to return (default: 100)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Number of members to skip (default: 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], "responses": { "200": { - "description": "Organization members retrieved successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ListAdminOrganizationMembersResponse" - } - } - } - }, - "400": { - "description": "Invalid request", + "description": "Concurrent limit retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/GetOrganizationConcurrentLimitResponse" } } } @@ -1880,48 +1946,45 @@ "session_token": [] } ] - } - }, - "/v1/admin/organizations/{org_id}/metrics": { - "get": { + }, + "patch": { "tags": [ "Admin" ], - "summary": "Get organization metrics (Admin only)", - "description": "Returns usage metrics for an organization including summary totals,\nand breakdowns by workspace, API key, and model.", - "operationId": "get_organization_metrics", + "summary": "Update organization concurrent request limit (Admin only)", + "description": "Updates the maximum concurrent requests allowed per model for an organization.\nSet to null to use the default limit (64).\nChanges take effect within 5 minutes due to caching.", + "operationId": "update_organization_concurrent_limit", "parameters": [ { "name": "org_id", "in": "path", - "description": "Organization ID to get metrics for", + "description": "The organization's ID (as a UUID)", "required": true, "schema": { "type": "string" } - }, - { - "name": "start", - "in": "query", - "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end", - "in": "query", - "description": "End of time range (ISO 8601). Defaults to now.", - "required": false, - "schema": { - "type": "string" - } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationConcurrentLimitRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Organization metrics retrieved successfully" + "description": "Concurrent limit updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationConcurrentLimitResponse" + } + } + } }, "400": { "description": "Invalid request", @@ -1971,62 +2034,32 @@ ] } }, - "/v1/admin/organizations/{org_id}/metrics/timeseries": { + "/v1/admin/organizations/{org_id}/fallback": { "get": { "tags": [ "Admin" ], - "summary": "Get time series metrics for an organization (Admin only)", - "description": "Returns daily/weekly/hourly aggregations for charting:\nrequests, tokens, and cost per time period.", - "operationId": "get_organization_timeseries", + "summary": "Get an organization's effective fallback policy (Admin only).", + "operationId": "get_organization_fallback", "parameters": [ { "name": "org_id", "in": "path", - "description": "Organization ID to get metrics for", + "description": "Organization ID", "required": true, "schema": { - "type": "string" - } - }, - { - "name": "start", - "in": "query", - "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end", - "in": "query", - "description": "End of time range (ISO 8601). Defaults to now.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "granularity", - "in": "query", - "description": "Time granularity: hour, day (default), or week", - "required": false, - "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], "responses": { "200": { - "description": "Time series metrics retrieved successfully" - }, - "400": { - "description": "Invalid request", + "description": "Fallback policy retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/OrganizationFallbackResponse" } } } @@ -2050,16 +2083,6 @@ } } } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } } }, "security": [ @@ -2067,16 +2090,13 @@ "session_token": [] } ] - } - }, - "/v1/admin/organizations/{org_id}/staking/farm": { - "get": { + }, + "patch": { "tags": [ "Admin" ], - "summary": "Get admin organization staking farm state", - "description": "Returns the staking farm source and last synced farm-credit state for any\norganization. Requires platform admin access.", - "operationId": "get_admin_organization_staking_farm", + "summary": "Update an organization's fallback policy (Admin only).", + "operationId": "update_organization_fallback", "parameters": [ { "name": "org_id", @@ -2084,27 +2104,28 @@ "description": "Organization ID", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], - "responses": { - "200": { - "description": "Organization staking farm state", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StakingFarmStateResponse" - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationFallbackRequest" } } }, - "400": { - "description": "Invalid organization ID", + "required": true + }, + "responses": { + "200": { + "description": "Fallback policy updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/OrganizationFallbackResponse" } } } @@ -2119,28 +2140,8 @@ } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "404": { - "description": "No staking farm source found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal server error", + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -2157,38 +2158,48 @@ ] } }, - "/v1/admin/organizations/{org_id}/staking/farm/sync": { - "post": { + "/v1/admin/organizations/{org_id}/limits": { + "patch": { "tags": [ "Admin" ], - "summary": "Sync admin organization staking farm credits", - "description": "Refreshes staking farm reward units and derived credits for any linked\norganization. Requires platform admin access.", - "operationId": "sync_admin_organization_staking_farm", + "summary": "Update organization limits (Admin only)", + "description": "Updates spending limits for a specific organization. This endpoint is typically called by\na billing service with an admin API key when a customer makes a purchase.", + "operationId": "update_organization_limits", "parameters": [ { "name": "org_id", "in": "path", - "description": "Organization ID", + "description": "Organization ID to update limits for", "required": true, "schema": { "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationLimitsRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Synced organization staking farm state", + "description": "Organization limits updated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StakingFarmStateResponse" + "$ref": "#/components/schemas/UpdateOrganizationLimitsResponse" } } } }, "400": { - "description": "Invalid organization ID", + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -2207,18 +2218,8 @@ } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "404": { - "description": "No staking farm source found", + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -2245,42 +2246,52 @@ ] } }, - "/v1/admin/organizations/{org_id}/usage/balance": { + "/v1/admin/organizations/{org_id}/limits/history": { "get": { "tags": [ "Admin" ], - "summary": "Get organization balance (Admin only)", - "description": "Returns the current spending balance for an organization without requiring\nthe caller to be a member of that organization. Intended for trusted\nautomated billing services.", - "operationId": "get_admin_organization_balance", + "summary": "Get limits history for an organization (Admin only)", + "description": "Returns the complete limits history for a specific organization, showing all limits changes over time.\nGet limits history for an organization (Admin only)\n\nReturns the complete limits history for a specific organization, showing all limits changes over time.", + "operationId": "get_organization_limits_history", "parameters": [ { "name": "org_id", "in": "path", - "description": "Organization ID", + "description": "The organization's ID (as a UUID)", "required": true, "schema": { "type": "string" } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of history records to return (default: 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Number of records to skip (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } } ], "responses": { "200": { - "description": "Organization balance", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OrganizationBalanceResponse" - } - } - } - }, - "400": { - "description": "Invalid request", + "description": "Limits history retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/OrgLimitsHistoryResponse" } } } @@ -2296,7 +2307,7 @@ } }, "404": { - "description": "Not found", + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -2323,21 +2334,63 @@ ] } }, - "/v1/admin/platform/billing-summary": { + "/v1/admin/organizations/{org_id}/members": { "get": { "tags": [ "Admin" ], - "summary": "Get the platform billing summary (Admin only)", - "description": "Credit LIMITS (caps) and consumption — NOT payments/cash. Returns active paid/grant\ncredit limits, total consumed, paying/granted org counts, and a breakdown by funding\nsource. Real money-in lives in the billing service, not cloud-api.", - "operationId": "get_billing_summary", + "summary": "List members of a specific organization (Admin only)", + "description": "Returns the members of the given organization with full user details\n(email, last login, active status), consistent with `/v1/admin/users`.\nOnly authenticated admins can perform this operation.", + "operationId": "list_organization_members", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of members to return (default: 100)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Number of members to skip (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], "responses": { "200": { - "description": "Billing summary retrieved successfully", + "description": "Organization members retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BillingSummary" + "$ref": "#/components/schemas/ListAdminOrganizationMembersResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -2352,6 +2405,16 @@ } } }, + "404": { + "description": "Organization not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2370,21 +2433,53 @@ ] } }, - "/v1/admin/platform/infra-summary": { + "/v1/admin/organizations/{org_id}/metrics": { "get": { "tags": [ "Admin" ], - "summary": "Get the platform infrastructure / fleet burn summary (Admin only)", - "description": "Fetches the live host list, counts active/idle hosts, and computes the monthly/daily\nGPU burn rate from the configured cost-per-host. Degrades gracefully (stale=true) if\nthe host inventory is unreachable.", - "operationId": "get_infra_summary", + "summary": "Get organization metrics (Admin only)", + "description": "Returns usage metrics for an organization including summary totals,\nand breakdowns by workspace, API key, and model.", + "operationId": "get_organization_metrics", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID to get metrics for", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "start", + "in": "query", + "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "end", + "in": "query", + "description": "End of time range (ISO 8601). Defaults to now.", + "required": false, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { - "description": "Infra summary retrieved successfully", + "description": "Organization metrics retrieved successfully" + }, + "400": { + "description": "Invalid request", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InfraSummary" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -2399,6 +2494,16 @@ } } }, + "404": { + "description": "Organization not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2417,15 +2522,24 @@ ] } }, - "/v1/admin/platform/metrics": { + "/v1/admin/organizations/{org_id}/metrics/timeseries": { "get": { "tags": [ "Admin" ], - "summary": "Get platform-wide metrics for admin dashboards (Admin only)", - "description": "Returns aggregated metrics across all organizations including:\n- Total users and organizations\n- Total requests and revenue\n- Top models by usage\n- Top organizations by spend", - "operationId": "get_platform_metrics", + "summary": "Get time series metrics for an organization (Admin only)", + "description": "Returns daily/weekly/hourly aggregations for charting:\nrequests, tokens, and cost per time period.", + "operationId": "get_organization_timeseries", "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID to get metrics for", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "start", "in": "query", @@ -2443,15 +2557,27 @@ "schema": { "type": "string" } + }, + { + "name": "granularity", + "in": "query", + "description": "Time granularity: hour, day (default), or week", + "required": false, + "schema": { + "type": "string" + } } ], "responses": { "200": { - "description": "Platform metrics retrieved successfully", + "description": "Time series metrics retrieved successfully" + }, + "400": { + "description": "Invalid request", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PlatformMetrics" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -2466,6 +2592,16 @@ } } }, + "404": { + "description": "Organization not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2484,38 +2620,20 @@ ] } }, - "/v1/admin/platform/metrics/timeseries": { + "/v1/admin/organizations/{org_id}/staking/farm": { "get": { "tags": [ "Admin" ], - "summary": "Get platform-wide time series for admin dashboards (Admin only)", - "description": "Returns per-bucket requests, tokens, cost (paid/granted + verifiable/external splits),\nactive organizations, and new signups for growth/mix trend charts.", - "operationId": "get_platform_timeseries", + "summary": "Get admin organization staking farm state", + "description": "Returns the staking farm source and last synced farm-credit state for any\norganization. Requires platform admin access.", + "operationId": "get_admin_organization_staking_farm", "parameters": [ { - "name": "start", - "in": "query", - "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end", - "in": "query", - "description": "End of time range (ISO 8601). Defaults to now.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "granularity", - "in": "query", - "description": "Time granularity: hour, day (default), week, or month", - "required": false, + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, "schema": { "type": "string" } @@ -2523,17 +2641,17 @@ ], "responses": { "200": { - "description": "Platform time series retrieved successfully", + "description": "Organization staking farm state", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PlatformTimeSeriesMetrics" + "$ref": "#/components/schemas/StakingFarmStateResponse" } } } }, "400": { - "description": "Invalid request", + "description": "Invalid organization ID", "content": { "application/json": { "schema": { @@ -2552,6 +2670,26 @@ } } }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "No staking farm source found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2570,76 +2708,20 @@ ] } }, - "/v1/admin/platform/model-revenue": { - "get": { + "/v1/admin/organizations/{org_id}/staking/farm/sync": { + "post": { "tags": [ "Admin" ], - "summary": "Get the per-model consumption ranking (Admin only)", - "description": "Models for the selected period ranked by consumed cost, with requests, tokens,\nunique orgs, verifiable flag, provider type, and latency. Paginated and filterable.", - "operationId": "get_model_revenue", + "summary": "Sync admin organization staking farm credits", + "description": "Refreshes staking farm reward units and derived credits for any linked\norganization. Requires platform admin access.", + "operationId": "sync_admin_organization_staking_farm", "parameters": [ { - "name": "start", - "in": "query", - "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end", - "in": "query", - "description": "End of time range (ISO 8601). Defaults to now.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "description": "Page size (1-1000, default 100)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Page offset (default 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "verifiable", - "in": "query", - "description": "Filter to verifiable (true) or non-verifiable (false) models", - "required": false, - "schema": { - "type": "boolean" - } - }, - { - "name": "provider_type", - "in": "query", - "description": "Filter by provider type (e.g. vllm, external, chutes)", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "sort", - "in": "query", - "description": "Sort: revenue (default), requests, tokens", - "required": false, + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, "schema": { "type": "string" } @@ -2647,17 +2729,17 @@ ], "responses": { "200": { - "description": "Model revenue retrieved successfully", + "description": "Synced organization staking farm state", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelRevenueReport" + "$ref": "#/components/schemas/StakingFarmStateResponse" } } } }, "400": { - "description": "Invalid request", + "description": "Invalid organization ID", "content": { "application/json": { "schema": { @@ -2676,6 +2758,26 @@ } } }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "No staking farm source found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2694,67 +2796,20 @@ ] } }, - "/v1/admin/platform/org-revenue": { + "/v1/admin/organizations/{org_id}/usage/balance": { "get": { "tags": [ "Admin" ], - "summary": "Get the per-organization consumption ranking (Admin only)", - "description": "Organizations with usage in the selected period ranked by consumed cost, with the\nverifiable/external split, requests, tokens, models used, a current paying flag, and\nlast-usage timestamp. Paginated and filterable — full attribution of usage/spend per org.", - "operationId": "get_org_revenue", + "summary": "Get organization balance (Admin only)", + "description": "Returns the current spending balance for an organization without requiring\nthe caller to be a member of that organization. Intended for trusted\nautomated billing services.", + "operationId": "get_admin_organization_balance", "parameters": [ { - "name": "start", - "in": "query", - "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end", - "in": "query", - "description": "End of time range (ISO 8601). Defaults to now.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "description": "Page size (1-1000, default 100)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Page offset (default 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "paying", - "in": "query", - "description": "Filter to current paying (true) / non-paying (false) orgs", - "required": false, - "schema": { - "type": "boolean" - } - }, - { - "name": "sort", - "in": "query", - "description": "Sort: revenue (default), requests, tokens", - "required": false, + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, "schema": { "type": "string" } @@ -2762,11 +2817,11 @@ ], "responses": { "200": { - "description": "Org revenue retrieved successfully", + "description": "Organization balance", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrgRevenueReport" + "$ref": "#/components/schemas/OrganizationBalanceResponse" } } } @@ -2791,6 +2846,16 @@ } } }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2809,40 +2874,21 @@ ] } }, - "/v1/admin/services": { - "post": { + "/v1/admin/platform/billing-summary": { + "get": { "tags": [ "Admin" ], - "summary": "Create platform service (Admin only)", - "operationId": "create_service", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateServiceRequest" - } - } - }, - "required": true - }, + "summary": "Get the platform billing summary (Admin only)", + "description": "Credit LIMITS (caps) and consumption — NOT payments/cash. Returns active paid/grant\ncredit limits, total consumed, paying/granted org counts, and a breakdown by funding\nsource. Real money-in lives in the billing service, not cloud-api.", + "operationId": "get_billing_summary", "responses": { "200": { - "description": "Service created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AdminServiceResponse" - } - } - } - }, - "400": { - "description": "Invalid request", + "description": "Billing summary retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/BillingSummary" } } } @@ -2875,52 +2921,21 @@ ] } }, - "/v1/admin/services/{id}": { - "patch": { + "/v1/admin/platform/infra-summary": { + "get": { "tags": [ "Admin" ], - "summary": "Update platform service (Admin only; display_name, description, cost_per_unit, is_active)", - "operationId": "update_service", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Service ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateServiceRequest" - } - } - }, - "required": true - }, + "summary": "Get the platform infrastructure / fleet burn summary (Admin only)", + "description": "Fetches the live host list, counts active/idle hosts, and computes the monthly/daily\nGPU burn rate from the configured cost-per-host. Degrades gracefully (stale=true) if\nthe host inventory is unreachable.", + "operationId": "get_infra_summary", "responses": { "200": { - "description": "Service updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AdminServiceResponse" - } - } - } - }, - "400": { - "description": "Invalid request", + "description": "Infra summary retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/InfraSummary" } } } @@ -2935,16 +2950,6 @@ } } }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -2963,66 +2968,28 @@ ] } }, - "/v1/admin/users": { + "/v1/admin/platform/metrics": { "get": { "tags": [ "Admin" ], - "summary": "List all registered users with pagination (Admin only)", - "description": "Returns a paginated list of all users in the system. Only authenticated admins can perform this operation.", - "operationId": "list_users", + "summary": "Get platform-wide metrics for admin dashboards (Admin only)", + "description": "Returns aggregated metrics across all organizations including:\n- Total users and organizations\n- Total requests and revenue\n- Top models by usage\n- Top organizations by spend", + "operationId": "get_platform_metrics", "parameters": [ { - "name": "limit", - "in": "query", - "description": "Maximum number of users to return (default: 100)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Number of users to skip (default: 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "include_organizations", - "in": "query", - "description": "Whether to include organization information and spend limits for the first organization owned by each user (default: false)", - "required": false, - "schema": { - "type": "boolean" - } - }, - { - "name": "search", + "name": "start", "in": "query", - "description": "Filter users by email, username, display name, user id, auth provider, or provider user id (case-insensitive partial match).", + "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", "required": false, "schema": { "type": "string" } }, { - "name": "is_active", - "in": "query", - "description": "Filter users by active status. Omit to include active and inactive users.", - "required": false, - "schema": { - "type": "boolean" - } - }, - { - "name": "search_by_name", + "name": "end", "in": "query", - "description": "Filter users by organization name (case-insensitive match). Only effective when include_organizations=true; separate from user search.", + "description": "End of time range (ISO 8601). Defaults to now.", "required": false, "schema": { "type": "string" @@ -3031,11 +2998,11 @@ ], "responses": { "200": { - "description": "Users retrieved successfully", + "description": "Platform metrics retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListUsersResponse" + "$ref": "#/components/schemas/PlatformMetrics" } } } @@ -3068,137 +3035,180 @@ ] } }, - "/v1/attestation/ita-token": { + "/v1/admin/platform/metrics/timeseries": { "get": { "tags": [ - "Attestation" + "Admin" ], - "summary": "Get Intel Trust Authority attestation token", - "description": "Get Intel Trust Authority signed attestation JWTs for the gateway and, when\nrequested, compatible model provider evidence. Public endpoint — this is an\nexplicit, documented decision (nearai/infra#193): the response carries only\nITA-signed TEE evidence (no customer or key-scoped data), public access lets\nthird parties verify the platform without an account, and upstream ITA rate\nlimits bound abuse (propagated as 429). See\n`build_public_attestation_routes` in `routes/attestation.rs`.", - "operationId": "get_ita_token", + "summary": "Get platform-wide time series for admin dashboards (Admin only)", + "description": "Returns per-bucket requests, tokens, cost (paid/granted + verifiable/external splits),\nactive organizations, and new signups for growth/mix trend charts.", + "operationId": "get_platform_timeseries", "parameters": [ { - "name": "model", + "name": "start", "in": "query", + "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" } }, { - "name": "nonce", + "name": "end", "in": "query", + "description": "End of time range (ISO 8601). Defaults to now.", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" } }, { - "name": "signing_algo", + "name": "granularity", "in": "query", + "description": "Time granularity: hour, day (default), week, or month", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Platform time series retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformTimeSeriesMetrics" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ { - "name": "signing_address", + "session_token": [] + } + ] + } + }, + "/v1/admin/platform/model-revenue": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Get the per-model consumption ranking (Admin only)", + "description": "Models for the selected period ranked by consumed cost, with requests, tokens,\nunique orgs, verifiable flag, provider type, and latency. Paginated and filterable.", + "operationId": "get_model_revenue", + "parameters": [ + { + "name": "start", "in": "query", + "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" } }, { - "name": "include_tls_fingerprint", + "name": "end", "in": "query", + "description": "End of time range (ISO 8601). Defaults to now.", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" } }, { - "name": "policy_ids", + "name": "limit", "in": "query", + "description": "Page size (1-1000, default 100)", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "integer", + "format": "int64" } }, { - "name": "policy_must_match", + "name": "offset", "in": "query", + "description": "Page offset (default 0)", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "integer", + "format": "int64" } }, { - "name": "token_signing_alg", + "name": "verifiable", + "in": "query", + "description": "Filter to verifiable (true) or non-verifiable (false) models", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "provider_type", "in": "query", + "description": "Filter by provider type (e.g. vllm, external, chutes)", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" + } + }, + { + "name": "sort", + "in": "query", + "description": "Sort: revenue (default), requests, tokens", + "required": false, + "schema": { + "type": "string" } } ], "responses": { "200": { - "description": "ITA attestation token retrieved", + "description": "Model revenue retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItaTokenResponse" + "$ref": "#/components/schemas/ModelRevenueReport" } } } }, "400": { - "description": "Invalid parameters", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "429": { - "description": "ITA rate limit exceeded", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "502": { - "description": "Bad ITA upstream response", + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -3207,8 +3217,8 @@ } } }, - "503": { - "description": "ITA attestation unavailable", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -3217,8 +3227,8 @@ } } }, - "504": { - "description": "ITA request timed out", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -3229,101 +3239,91 @@ } }, "security": [ - {} + { + "session_token": [] + } ] } }, - "/v1/attestation/report": { + "/v1/admin/platform/org-revenue": { "get": { "tags": [ - "Attestation" + "Admin" ], - "summary": "Get attestation report", - "description": "Get hardware attestation report for TEE verification. Requires an API key\n(nearai/infra#193); report retrieval is non-billable — no usage or billing\nrecords are created.", - "operationId": "get_attestation_report", + "summary": "Get the per-organization consumption ranking (Admin only)", + "description": "Organizations with usage in the selected period ranked by consumed cost, with the\nverifiable/external split, requests, tokens, models used, a current paying flag, and\nlast-usage timestamp. Paginated and filterable — full attribution of usage/spend per org.", + "operationId": "get_org_revenue", "parameters": [ { - "name": "model", + "name": "start", "in": "query", + "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" } }, { - "name": "signing_algo", + "name": "end", "in": "query", + "description": "End of time range (ISO 8601). Defaults to now.", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" } }, { - "name": "nonce", + "name": "limit", "in": "query", + "description": "Page size (1-1000, default 100)", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "integer", + "format": "int64" } }, { - "name": "signing_address", + "name": "offset", "in": "query", + "description": "Page offset (default 0)", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "integer", + "format": "int64" } }, { - "name": "include_tls_fingerprint", + "name": "paying", "in": "query", - "description": "Include TLS certificate fingerprint in the report data.\nDefaults to false; when true, report_data[..32] = SHA256(signing_address || cert_fingerprint).", + "description": "Filter to current paying (true) / non-paying (false) orgs", "required": false, "schema": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, { - "name": "provider", + "name": "sort", "in": "query", - "description": "Restrict the report to a specific serving tier.\nAccepted values: `near` (NEAR AI's own TEE fleet) or `chutes` (attested Chutes fallback).\nWhen omitted, the first successfully responding provider is used.", + "description": "Sort: revenue (default), requests, tokens", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" } } ], "responses": { "200": { - "description": "Attestation report retrieved", + "description": "Org revenue retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AttestationResponse" + "$ref": "#/components/schemas/OrgRevenueReport" } } } }, "400": { - "description": "Invalid nonce format", + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -3333,7 +3333,7 @@ } }, "401": { - "description": "Missing or invalid API key", + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -3342,8 +3342,8 @@ } } }, - "503": { - "description": "Service unavailable", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -3355,24 +3355,23 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/audio/transcriptions": { + "/v1/admin/services": { "post": { "tags": [ - "Audio" + "Admin" ], - "summary": "Audio transcription endpoint", - "description": "Transcribe audio files using Whisper models. Accepts audio file uploads via multipart/form-data.\nSupports MP3, WAV, WEBM, FLAC, OGG, and M4A formats. Maximum file size: 25 MB.\n\n**Request Body (multipart/form-data):**\nAll fields should be provided as text values or files as indicated in the schema.", - "operationId": "audio_transcriptions", + "summary": "Create platform service (Admin only)", + "operationId": "create_service", "requestBody": { "content": { - "multipart/form-data": { + "application/json": { "schema": { - "$ref": "#/components/schemas/AudioTranscriptionRequestSchema" + "$ref": "#/components/schemas/CreateServiceRequest" } } }, @@ -3380,22 +3379,17 @@ }, "responses": { "200": { - "description": "Successful transcription", + "description": "Service created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AudioTranscriptionResponse" - } - }, - "text/plain": { - "schema": { - "type": "string" + "$ref": "#/components/schemas/AdminServiceResponse" } } } }, "400": { - "description": "Invalid request (empty file, unsupported format, file too large)", + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -3405,27 +3399,7 @@ } }, "401": { - "description": "Unauthorized (missing or invalid API key)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Model not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "429": { - "description": "Rate limited or overloaded — retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -3435,7 +3409,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -3447,24 +3421,35 @@ }, "security": [ { - "ApiKeyAuth": [] + "session_token": [] } ] } }, - "/v1/billing/costs": { - "post": { + "/v1/admin/services/{id}": { + "patch": { "tags": [ - "Billing" + "Admin" + ], + "summary": "Update platform service (Admin only; display_name, description, cost_per_unit, is_active)", + "operationId": "update_service", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Service ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } ], - "summary": "Get costs by request IDs", - "description": "Returns the cost in nano-USD for each request ID provided.\nThis endpoint is designed for HuggingFace billing integration.\n\nRequest IDs that are not found will be returned with costNanoUsd: 0", - "operationId": "get_billing_costs", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BillingCostsRequest" + "$ref": "#/components/schemas/UpdateServiceRequest" } } }, @@ -3472,11 +3457,11 @@ }, "responses": { "200": { - "description": "Costs retrieved successfully", + "description": "Service updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BillingCostsResponse" + "$ref": "#/components/schemas/AdminServiceResponse" } } } @@ -3501,6 +3486,16 @@ } } }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -3514,139 +3509,90 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/chat/completions": { - "post": { + "/v1/admin/users": { + "get": { "tags": [ - "Chat" + "Admin" ], - "summary": "Create chat completion", - "description": "Generate AI model responses for chat conversations. Supports both streaming and non-streaming modes.\nOpenAI-compatible endpoint.", - "operationId": "chat_completions", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChatCompletionRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Completion generated successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChatCompletionResponse" - } - } + "summary": "List all registered users with pagination (Admin only)", + "description": "Returns a paginated list of all users in the system. Only authenticated admins can perform this operation.", + "operationId": "list_users", + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Maximum number of users to return (default: 100)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" } }, - "400": { - "description": "Invalid request parameters", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "offset", + "in": "query", + "description": "Number of users to skip (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" } }, - "401": { - "description": "Invalid or missing API key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "include_organizations", + "in": "query", + "description": "Whether to include organization information and spend limits for the first organization owned by each user (default: false)", + "required": false, + "schema": { + "type": "boolean" } }, - "402": { - "description": "Insufficient credits", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "search", + "in": "query", + "description": "Filter users by email, username, display name, user id, auth provider, or provider user id (case-insensitive partial match).", + "required": false, + "schema": { + "type": "string" } }, - "429": { - "description": "Rate limited or overloaded — retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "is_active", + "in": "query", + "description": "Filter users by active status. Omit to include active and inactive users.", + "required": false, + "schema": { + "type": "boolean" } }, - "500": { - "description": "Server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ { - "api_key": [] + "name": "search_by_name", + "in": "query", + "description": "Filter users by organization name (case-insensitive match). Only effective when include_organizations=true; separate from user search.", + "required": false, + "schema": { + "type": "string" + } } - ] - } - }, - "/v1/check_api_key": { - "post": { - "tags": [ - "Gateway" ], - "summary": "Check API key validity", - "description": "Validates the provided API key (via Bearer token), checks rate limits,\nand verifies the organization has sufficient credits.\n\nThis endpoint is designed for external model gateways to authenticate\nuser requests before forwarding to inference engines.", - "operationId": "check_api_key", "responses": { "200": { - "description": "API key is valid and has sufficient credits", + "description": "Users retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CheckApiKeyResponse" + "$ref": "#/components/schemas/ListUsersResponse" } } } }, "401": { - "description": "Invalid or missing API key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "402": { - "description": "Insufficient credits or spend limit exceeded", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "429": { - "description": "Rate limit exceeded", + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -3668,42 +3614,122 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/completions": { - "post": { + "/v1/attestation/ita-token": { + "get": { "tags": [ - "Chat" + "Attestation" ], - "summary": "Create text completion", - "description": "Generate AI model responses for text prompts. OpenAI-compatible endpoint.", - "operationId": "completions", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CompletionRequest" - } + "summary": "Get Intel Trust Authority attestation token", + "description": "Get Intel Trust Authority signed attestation JWTs for the gateway and, when\nrequested, compatible model provider evidence. Public endpoint — this is an\nexplicit, documented decision (nearai/infra#193): the response carries only\nITA-signed TEE evidence (no customer or key-scoped data), public access lets\nthird parties verify the platform without an account, and upstream ITA rate\nlimits bound abuse (propagated as 429). See\n`build_public_attestation_routes` in `routes/attestation.rs`.", + "operationId": "get_ita_token", + "parameters": [ + { + "name": "model", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] } }, - "required": true - }, - "responses": { + { + "name": "nonce", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "signing_algo", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "signing_address", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "include_tls_fingerprint", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "policy_ids", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "policy_must_match", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "token_signing_alg", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + } + ], + "responses": { "200": { - "description": "Completion generated successfully", + "description": "ITA attestation token retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CompletionResponse" + "$ref": "#/components/schemas/ItaTokenResponse" } } } }, "400": { - "description": "Invalid request parameters", + "description": "Invalid parameters", "content": { "application/json": { "schema": { @@ -3712,8 +3738,8 @@ } } }, - "401": { - "description": "Invalid or missing API key", + "429": { + "description": "ITA rate limit exceeded", "content": { "application/json": { "schema": { @@ -3722,8 +3748,8 @@ } } }, - "429": { - "description": "Rate limited or overloaded — retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "502": { + "description": "Bad ITA upstream response", "content": { "application/json": { "schema": { @@ -3732,8 +3758,18 @@ } } }, - "500": { - "description": "Server error", + "503": { + "description": "ITA attestation unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "504": { + "description": "ITA request timed out", "content": { "application/json": { "schema": { @@ -3744,42 +3780,101 @@ } }, "security": [ - { - "api_key": [] - } + {} ] } }, - "/v1/feature-requests": { - "post": { + "/v1/attestation/report": { + "get": { "tags": [ - "Feature Requests" + "Attestation" ], - "summary": "Submit or update the current user's interest in a feature request target.", - "operationId": "submit_feature_request", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubmitFeatureRequest" - } + "summary": "Get attestation report", + "description": "Get hardware attestation report for TEE verification. Requires an API key\n(nearai/infra#193); report retrieval is non-billable — no usage or billing\nrecords are created.", + "operationId": "get_attestation_report", + "parameters": [ + { + "name": "model", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] } }, - "required": true - }, + { + "name": "signing_algo", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "nonce", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "signing_address", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "include_tls_fingerprint", + "in": "query", + "description": "Include the TLS certificate SPKI fingerprint in the report-data binding.\nDefaults to false; when true, report_data[..32] = SHA256(signing_address || tls_cert_fingerprint).", + "required": false, + "schema": { + "type": [ + "boolean", + "null" + ] + } + }, + { + "name": "provider", + "in": "query", + "description": "Restrict the report to a specific serving tier.\nAccepted values: `near` (NEAR AI's own TEE fleet) or `chutes` (attested Chutes fallback).\nWhen omitted, the first successfully responding provider is used.", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + } + ], "responses": { "200": { - "description": "Feature request recorded", + "description": "Attestation report retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SubmitFeatureRequestResponse" + "$ref": "#/components/schemas/AttestationResponse" } } } }, "400": { - "description": "Invalid request", + "description": "Invalid nonce format", "content": { "application/json": { "schema": { @@ -3789,17 +3884,7 @@ } }, "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "403": { - "description": "Organization membership required", + "description": "Missing or invalid API key", "content": { "application/json": { "schema": { @@ -3808,8 +3893,8 @@ } } }, - "500": { - "description": "Internal server error", + "503": { + "description": "Service unavailable", "content": { "application/json": { "schema": { @@ -3821,46 +3906,24 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/health": { - "get": { - "tags": [ - "Health" - ], - "summary": "Health check", - "description": "Check service health status. No authentication required.", - "operationId": "health_check", - "responses": { - "200": { - "description": "Service is healthy", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HealthResponse" - } - } - } - } - } - } - }, - "/v1/images/edits": { + "/v1/audio/transcriptions": { "post": { "tags": [ - "Images" + "Audio" ], - "summary": "Edit images from a text prompt and image", - "description": "Edit images using an AI model from an image and text description. OpenAI-compatible endpoint.\n\n**Request Body (multipart/form-data):**\nAll fields should be provided as text values or files as indicated in the schema.", - "operationId": "image_edits", + "summary": "Audio transcription endpoint", + "description": "Transcribe audio files using Whisper models. Accepts audio file uploads via multipart/form-data.\nSupports MP3, WAV, WEBM, FLAC, OGG, and M4A formats. Maximum file size: 25 MB.\n\n**Request Body (multipart/form-data):**\nAll fields should be provided as text values or files as indicated in the schema.", + "operationId": "audio_transcriptions", "requestBody": { "content": { "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/ImageEditRequestSchema" + "$ref": "#/components/schemas/AudioTranscriptionRequestSchema" } } }, @@ -3868,17 +3931,22 @@ }, "responses": { "200": { - "description": "Image edited successfully", + "description": "Successful transcription", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ImageGenerationResponse" + "$ref": "#/components/schemas/AudioTranscriptionResponse" + } + }, + "text/plain": { + "schema": { + "type": "string" } } } }, "400": { - "description": "Invalid request parameters", + "description": "Invalid request (empty file, unsupported format, file too large)", "content": { "application/json": { "schema": { @@ -3888,7 +3956,7 @@ } }, "401": { - "description": "Invalid or missing API key", + "description": "Unauthorized (missing or invalid API key)", "content": { "application/json": { "schema": { @@ -3907,8 +3975,8 @@ } } }, - "413": { - "description": "Payload too large", + "429": { + "description": "Rate limited or overloaded — retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", "content": { "application/json": { "schema": { @@ -3917,8 +3985,65 @@ } } }, - "429": { - "description": "Rate limited or overloaded — retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v1/billing/costs": { + "post": { + "tags": [ + "Billing" + ], + "summary": "Get costs by request IDs", + "description": "Returns the cost in nano-USD for each request ID provided. A request ID is\nthe UUID from the `inference-id` response header of /v1/chat/completions\nand /v1/messages responses (equivalently: UUIDv5 of the response body `id`\nunder the DNS namespace). The `x-request-id` response header is a transport\ncorrelation ID and cannot be used here.\n\nRequest IDs that are not found are returned with costNanoUsd: 0\n(HuggingFace-compatible); the response then carries a `warning` field\npointing at the correct ID source. Usage for a just-finished request can\ntake a few seconds to become visible.", + "operationId": "get_billing_costs", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BillingCostsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Costs retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BillingCostsResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -3928,7 +4053,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -3945,19 +4070,19 @@ ] } }, - "/v1/images/generations": { + "/v1/chat/completions": { "post": { "tags": [ - "Images" + "Chat" ], - "summary": "Generate images from text prompt", - "description": "Generate images using an AI model from a text description. OpenAI-compatible endpoint.", - "operationId": "image_generations", + "summary": "Create chat completion", + "description": "Generate AI model responses for chat conversations. Supports both streaming and non-streaming modes.\nOpenAI-compatible endpoint.", + "operationId": "chat_completions", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ImageGenerationRequest" + "$ref": "#/components/schemas/ChatCompletionRequest" } } }, @@ -3965,11 +4090,11 @@ }, "responses": { "200": { - "description": "Image generated successfully", + "description": "Completion generated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ImageGenerationResponse" + "$ref": "#/components/schemas/ChatCompletionResponse" } } } @@ -3994,6 +4119,16 @@ } } }, + "402": { + "description": "Insufficient credits", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "429": { "description": "Rate limited or overloaded — retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", "content": { @@ -4022,38 +4157,27 @@ ] } }, - "/v1/invitations/{token}": { - "get": { + "/v1/check_api_key": { + "post": { "tags": [ - "Invitations" - ], - "summary": "Get invitation details by token (public endpoint)", - "description": "Returns invitation details for a specific token. This is a public endpoint\nthat allows users to view invitation details before logging in.", - "operationId": "get_invitation_by_token", - "parameters": [ - { - "name": "token", - "in": "path", - "description": "Invitation token", - "required": true, - "schema": { - "type": "string" - } - } + "Gateway" ], + "summary": "Check API key validity", + "description": "Validates the provided API key (via Bearer token), checks rate limits,\nand verifies the organization has sufficient credits.\n\nThis endpoint is designed for external model gateways to authenticate\nuser requests before forwarding to inference engines.", + "operationId": "check_api_key", "responses": { "200": { - "description": "Invitation details", + "description": "API key is valid and has sufficient credits", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationInvitationResponse" + "$ref": "#/components/schemas/CheckApiKeyResponse" } } } }, - "404": { - "description": "Invitation not found", + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -4062,8 +4186,18 @@ } } }, - "410": { - "description": "Invitation expired or no longer pending", + "402": { + "description": "Insufficient credits or spend limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Rate limit exceeded", "content": { "application/json": { "schema": { @@ -4082,41 +4216,45 @@ } } } - } + }, + "security": [ + { + "api_key": [] + } + ] } }, - "/v1/invitations/{token}/accept": { + "/v1/completions": { "post": { "tags": [ - "Invitations" + "Chat" ], - "summary": "Accept invitation by token (requires authentication)", - "description": "Accepts an invitation using its token. The authenticated user's email\nmust match the invitation email.", - "operationId": "accept_invitation_by_token", - "parameters": [ - { - "name": "token", - "in": "path", - "description": "Invitation token", - "required": true, - "schema": { - "type": "string" + "summary": "Create text completion", + "description": "Generate AI model responses for text prompts. OpenAI-compatible endpoint.", + "operationId": "completions", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompletionRequest" + } } - } - ], + }, + "required": true + }, "responses": { "200": { - "description": "Invitation accepted successfully", + "description": "Completion generated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AcceptInvitationResponse" + "$ref": "#/components/schemas/CompletionResponse" } } } }, "400": { - "description": "Bad request - invitation expired or invalid", + "description": "Invalid request parameters", "content": { "application/json": { "schema": { @@ -4126,7 +4264,7 @@ } }, "401": { - "description": "Unauthorized", + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -4135,8 +4273,8 @@ } } }, - "403": { - "description": "Forbidden - invitation belongs to another user", + "429": { + "description": "Rate limited or overloaded — retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", "content": { "application/json": { "schema": { @@ -4145,8 +4283,8 @@ } } }, - "404": { - "description": "Invitation not found", + "500": { + "description": "Server error", "content": { "application/json": { "schema": { @@ -4154,9 +4292,56 @@ } } } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v1/conversations": { + "post": { + "tags": [ + "Conversations" + ], + "summary": "Create conversation", + "description": "Create a new conversation to organize chat messages.", + "operationId": "create_conversation", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateConversationRequest" + } + } }, - "409": { - "description": "User is already a member", + "required": true + }, + "responses": { + "201": { + "description": "Conversation created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationObject" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -4166,7 +4351,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -4178,60 +4363,43 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/model/list": { + "/v1/conversations/{conversation_id}": { "get": { "tags": [ - "Models" + "Conversations" ], - "summary": "List models with pricing", - "description": "Get all available models with pricing information. Public endpoint.\n\nThe full model catalog (a few dozen entries) is loaded once and cached\nin-process for a short TTL. `limit` / `offset` slice the cached list\nin memory, so pagination is consistent across pages within a single\ncache window and adds essentially no DB load.", - "operationId": "list_models", + "summary": "Get conversation", + "description": "Retrieve conversation details by ID.", + "operationId": "get_conversation", "parameters": [ { - "name": "limit", - "in": "query", - "description": "Maximum number of models to return. Defaults to 100. Must be\nnon-negative; values are capped only by the catalog size.", - "required": false, - "schema": { - "type": [ - "integer", - "null" - ], - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Number of models to skip from the start of the catalog.\nDefaults to 0. Must be non-negative.", - "required": false, + "name": "conversation_id", + "in": "path", + "description": "Conversation ID", + "required": true, "schema": { - "type": [ - "integer", - "null" - ], - "format": "int64" + "type": "string" } } ], "responses": { "200": { - "description": "List of models with pricing", + "description": "Conversation details", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelListResponse" + "$ref": "#/components/schemas/ConversationObject" } } } }, "400": { - "description": "Invalid pagination parameters", + "description": "Bad request", "content": { "application/json": { "schema": { @@ -4240,8 +4408,8 @@ } } }, - "500": { - "description": "Server error", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -4249,42 +4417,9 @@ } } } - } - } - } - }, - "/v1/model/{model_name}": { - "get": { - "tags": [ - "Models" - ], - "summary": "Get model details", - "description": "Get pricing and metadata for a specific model. URL-encode model names containing slashes. Public endpoint.", - "operationId": "get_model_by_name", - "parameters": [ - { - "name": "model_name", - "in": "path", - "description": "Model name (URL-encode if it contains slashes)", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Model details with pricing", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ModelWithPricing" - } - } - } }, "404": { - "description": "Model not found", + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -4294,7 +4429,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -4303,30 +4438,54 @@ } } } - } - } - }, - "/v1/models": { - "get": { + }, + "security": [ + { + "api_key": [] + } + ] + }, + "post": { "tags": [ - "Chat" + "Conversations" ], - "summary": "List available models", - "description": "Returns all AI models available for completions. OpenAI-compatible endpoint.", - "operationId": "models", + "summary": "Update conversation", + "description": "Update conversation metadata.", + "operationId": "update_conversation", + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "description": "Conversation ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateConversationRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "List of available models", + "description": "Conversation updated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelsResponse" + "$ref": "#/components/schemas/ConversationObject" } } } }, - "500": { - "description": "Server error", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -4334,74 +4493,19 @@ } } } - } - }, - "security": [ - {} - ] - } - }, - "/v1/organizations": { - "get": { - "tags": [ - "Organizations" - ], - "summary": "List organizations", - "description": "Get all organizations you belong to.", - "operationId": "list_organizations", - "parameters": [ - { - "name": "limit", - "in": "query", - "description": "Maximum number to return", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Number to skip", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "order_by", - "in": "query", - "description": "Sort by field", - "required": false, - "schema": { - "$ref": "#/components/schemas/OrganizationOrderBy" - } }, - { - "name": "order_direction", - "in": "query", - "description": "Sort direction", - "required": false, - "schema": { - "$ref": "#/components/schemas/OrganizationOrderDirection" - } - } - ], - "responses": { - "200": { - "description": "List of organizations", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListOrganizationsResponse" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "401": { - "description": "Invalid or missing session token", + "404": { + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -4411,7 +4515,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -4423,34 +4527,35 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] }, - "post": { + "delete": { "tags": [ - "Organizations" + "Conversations" ], - "summary": "Create a new organization", - "description": "Creates a new organization with the authenticated user as owner.", - "operationId": "create_organization", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateOrganizationRequest" - } + "summary": "Delete conversation", + "description": "Delete a conversation and all its messages.", + "operationId": "delete_conversation", + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "description": "Conversation ID", + "required": true, + "schema": { + "type": "string" } - }, - "required": true - }, + } + ], "responses": { "200": { - "description": "Organization created successfully", + "description": "Conversation deleted successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationResponse" + "$ref": "#/components/schemas/ConversationDeleteResult" } } } @@ -4475,8 +4580,8 @@ } } }, - "409": { - "description": "Organization already exists", + "404": { + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -4498,44 +4603,43 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/organizations/{org_id}": { - "get": { + "/v1/conversations/{conversation_id}/archive": { + "post": { "tags": [ - "Organizations" + "Conversations" ], - "summary": "Get organization by ID", - "description": "Returns organization details for a specific organization ID.", - "operationId": "get_organization", + "summary": "Archive a conversation", + "description": "Archives a conversation to hide it from the main list.", + "operationId": "archive_conversation", "parameters": [ { - "name": "org_id", + "name": "conversation_id", "in": "path", - "description": "Organization ID", + "description": "Conversation ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], "responses": { "200": { - "description": "Organization details", + "description": "Conversation archived successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationResponse" + "$ref": "#/components/schemas/ConversationObject" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -4544,8 +4648,8 @@ } } }, - "403": { - "description": "Forbidden", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -4555,7 +4659,7 @@ } }, "404": { - "description": "Organization not found", + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -4577,46 +4681,35 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] }, - "put": { + "delete": { "tags": [ - "Organizations" + "Conversations" ], - "summary": "Update organization", - "description": "Updates organization details for a specific organization ID.", - "operationId": "update_organization", + "summary": "Unarchive a conversation", + "description": "Unarchives a conversation to show it in the main list again.", + "operationId": "unarchive_conversation", "parameters": [ { - "name": "org_id", + "name": "conversation_id", "in": "path", - "description": "Organization ID", + "description": "Conversation ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrganizationRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Updated organization", + "description": "Conversation unarchived successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationResponse" + "$ref": "#/components/schemas/ConversationObject" } } } @@ -4641,18 +4734,8 @@ } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Organization not found", + "404": { + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -4674,45 +4757,43 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] - }, - "delete": { + } + }, + "/v1/conversations/{conversation_id}/clone": { + "post": { "tags": [ - "Organizations" + "Conversations" ], - "summary": "Delete organization (owner only)", - "description": "Deletes an organization. Only the organization owner can perform this action.", - "operationId": "delete_organization", + "summary": "Clone a conversation", + "description": "Creates a copy of an existing conversation with a new ID.", + "operationId": "clone_conversation", "parameters": [ { - "name": "org_id", + "name": "conversation_id", "in": "path", - "description": "Organization ID", + "description": "Conversation ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], "responses": { - "200": { - "description": "Organization deleted successfully" - }, - "401": { - "description": "Unauthorized", + "201": { + "description": "Conversation cloned successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ConversationObject" } } } }, - "403": { - "description": "Forbidden", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -4721,8 +4802,8 @@ } } }, - "404": { - "description": "Organization not found", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -4731,8 +4812,8 @@ } } }, - "409": { - "description": "Organization is bound to a NEAR staking wallet", + "404": { + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -4754,57 +4835,63 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/organizations/{org_id}/invitations/{invitation_id}": { - "delete": { + "/v1/conversations/{conversation_id}/items": { + "get": { "tags": [ - "Organization Members" + "Conversations" ], - "summary": "Cancel an organization invitation (short path)", - "description": "Cancels a pending invitation for the organization. Only owners and admins can cancel invitations.\nThis endpoint is an alias for `DELETE /v1/organizations/{org_id}/members/invitations/{invitation_id}`.", - "operationId": "cancel_invitation", + "summary": "List conversation messages", + "description": "Get all messages and responses in a conversation, sorted by creation time.", + "operationId": "list_conversation_items", "parameters": [ { - "name": "org_id", + "name": "conversation_id", "in": "path", - "description": "Organization ID", + "description": "Conversation ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } }, { - "name": "invitation_id", - "in": "path", - "description": "Invitation ID", - "required": true, + "name": "limit", + "in": "query", + "description": "Maximum number of items to return", + "required": false, "schema": { - "type": "string", - "format": "uuid" + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Number of items to skip", + "required": false, + "schema": { + "type": "integer", + "format": "int64" } } ], "responses": { - "204": { - "description": "Invitation cancelled successfully" - }, - "400": { - "description": "Bad request - invitation is not pending", + "200": { + "description": "List of conversation items", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ConversationItemList" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -4813,8 +4900,8 @@ } } }, - "403": { - "description": "Forbidden - not an admin or owner", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -4824,7 +4911,7 @@ } }, "404": { - "description": "Organization or invitation not found", + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -4846,58 +4933,67 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] - } - }, - "/v1/organizations/{org_id}/members": { - "get": { + }, + "post": { "tags": [ - "Organization Members" + "Conversations" ], - "summary": "List organization members with limited user information", - "description": "Returns limited user information for privacy and security:\n- All members: See only public user info (username, display name, avatar)\n- Sensitive data (email, last login, etc.) is not exposed to any organization members", - "operationId": "list_organization_members", + "summary": "Create items in a conversation (for backfilling)", + "description": "Adds items to a conversation, allowing API callers to backfill conversations.", + "operationId": "create_conversation_items", "parameters": [ { - "name": "org_id", + "name": "conversation_id", "in": "path", - "description": "Organization ID", + "description": "Conversation ID", "required": true, "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "limit", - "in": "query", - "description": "Number of records to return (default: 100, max: 1000)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" + "type": "string" } }, { - "name": "offset", + "name": "include", "in": "query", - "description": "Offset for pagination (default: 0)", + "description": "Additional fields to include in the response", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": "array", + "items": { + "type": "string" + } } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateConversationItemsRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "List of organization members with public user information", + "description": "Items created successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListOrganizationMembersResponse" + "$ref": "#/components/schemas/ConversationItemList" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -4912,8 +5008,8 @@ } } }, - "403": { - "description": "Forbidden - not a member of the organization", + "404": { + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -4935,52 +5031,43 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] - }, + } + }, + "/v1/conversations/{conversation_id}/pin": { "post": { "tags": [ - "Organization Members" + "Conversations" ], - "summary": "Add a member to an organization", - "description": "Adds a new member to the organization. The authenticated user must be an owner or admin.", - "operationId": "add_organization_member", + "summary": "Pin a conversation", + "description": "Pins a conversation to keep it at the top of the list.", + "operationId": "pin_conversation", "parameters": [ { - "name": "org_id", + "name": "conversation_id", "in": "path", - "description": "Organization ID", + "description": "Conversation ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AddOrganizationMemberRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Member added successfully", + "description": "Conversation pinned successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationMemberResponse" + "$ref": "#/components/schemas/ConversationObject" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -4989,8 +5076,8 @@ } } }, - "403": { - "description": "Forbidden - not an admin or owner", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5000,17 +5087,7 @@ } }, "404": { - "description": "User not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "409": { - "description": "User is already a member", + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -5032,56 +5109,41 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] - } - }, - "/v1/organizations/{org_id}/members/invitations": { - "get": { + }, + "delete": { "tags": [ - "Organization Members" + "Conversations" ], - "summary": "List organization invitations", - "description": "Returns invitations for the organization. Only accessible to owners and admins.", - "operationId": "list_organization_invitations", + "summary": "Unpin a conversation", + "description": "Unpins a conversation.", + "operationId": "unpin_conversation", "parameters": [ { - "name": "org_id", + "name": "conversation_id", "in": "path", - "description": "Organization ID", + "description": "Conversation ID", "required": true, "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "status", - "in": "query", - "description": "Filter by status", - "required": false, - "schema": { - "$ref": "#/components/schemas/InvitationStatus" + "type": "string" } } ], "responses": { "200": { - "description": "List of organization invitations", + "description": "Conversation unpinned successfully", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrganizationInvitationResponse" - } + "$ref": "#/components/schemas/ConversationObject" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -5090,8 +5152,8 @@ } } }, - "403": { - "description": "Forbidden - not an admin or owner", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5101,7 +5163,7 @@ } }, "404": { - "description": "Organization not found", + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -5123,57 +5185,41 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/organizations/{org_id}/members/invitations/{invitation_id}": { - "delete": { + "/v1/feature-requests": { + "post": { "tags": [ - "Organization Members" + "Feature Requests" ], - "summary": "Cancel an organization invitation", - "description": "Cancels a pending invitation for the organization. Only owners and admins can cancel invitations.", - "operationId": "cancel_organization_invitation", - "parameters": [ - { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "summary": "Submit or update the current user's interest in a feature request target.", + "operationId": "submit_feature_request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmitFeatureRequest" + } } }, - { - "name": "invitation_id", - "in": "path", - "description": "Invitation ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], + "required": true + }, "responses": { - "204": { - "description": "Invitation cancelled successfully" - }, - "400": { - "description": "Bad request - invitation is not pending", + "200": { + "description": "Feature request recorded", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/SubmitFeatureRequestResponse" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -5182,8 +5228,8 @@ } } }, - "403": { - "description": "Forbidden - not an admin or owner", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5192,8 +5238,8 @@ } } }, - "404": { - "description": "Organization or invitation not found", + "403": { + "description": "Organization membership required", "content": { "application/json": { "schema": { @@ -5220,49 +5266,64 @@ ] } }, - "/v1/organizations/{org_id}/members/invite-by-email": { - "post": { + "/v1/files": { + "get": { "tags": [ - "Organization Members" + "Files" ], - "summary": "Invite users to an organization by email", - "description": "Invites multiple users to the organization by their email addresses. The authenticated user must be an owner or admin.\nReturns results for each invitation attempt, including successes and failures.", - "operationId": "invite_organization_member_by_email", + "operationId": "list_files", "parameters": [ { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, + "name": "after", + "in": "query", + "description": "A cursor for pagination. Pass the file ID to fetch files after this one.", + "required": false, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InviteOrganizationMemberByEmailRequest" - } + }, + { + "name": "limit", + "in": "query", + "description": "Number of files to return (1-10000, default 10000)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" } }, - "required": true - }, + { + "name": "order", + "in": "query", + "description": "Sort order by created_at timestamp: 'asc' or 'desc' (default 'desc')", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "purpose", + "in": "query", + "description": "Filter files by purpose", + "required": false, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { - "description": "Invitation results (may include partial failures)", + "description": "List of files retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InviteOrganizationMemberByEmailResponse" + "$ref": "#/components/schemas/FileListResponse" } } } }, "400": { - "description": "Bad request - empty invitation list", + "description": "Bad request", "content": { "application/json": { "schema": { @@ -5280,9 +5341,37 @@ } } } + } + }, + "security": [ + { + "api_key": [] + } + ] + }, + "post": { + "tags": [ + "Files" + ], + "operationId": "upload_file", + "requestBody": { + "content": { + "multipart/form-data": {} + } + }, + "responses": { + "201": { + "description": "File uploaded successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileUploadResponse" + } + } + } }, - "403": { - "description": "Forbidden - not an admin or owner", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -5291,8 +5380,18 @@ } } }, - "500": { - "description": "Internal server error", + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "File too large", "content": { "application/json": { "schema": { @@ -5304,64 +5403,41 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/organizations/{org_id}/members/{user_id}": { - "put": { + "/v1/files/{file_id}": { + "get": { "tags": [ - "Organization Members" + "Files" ], - "summary": "Update an organization member's role", - "description": "Updates a member's role in the organization. The authenticated user must be an owner or admin.\nOnly owners can promote members to owner role.", - "operationId": "update_organization_member", + "operationId": "get_file", "parameters": [ { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "user_id", + "name": "file_id", "in": "path", - "description": "User ID of the member to update", + "description": "The ID of the file to retrieve", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrganizationMemberRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Member updated successfully", + "description": "File information retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationMemberResponse" + "$ref": "#/components/schemas/FileUploadResponse" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -5370,8 +5446,8 @@ } } }, - "403": { - "description": "Forbidden - not an admin or owner", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5380,8 +5456,8 @@ } } }, - "500": { - "description": "Internal server error", + "404": { + "description": "File not found", "content": { "application/json": { "schema": { @@ -5393,55 +5469,39 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] }, "delete": { "tags": [ - "Organization Members" + "Files" ], - "summary": "Remove a member from an organization", - "description": "Removes a member from the organization. The authenticated user must be an owner or admin,\nor the member can remove themselves. The last owner cannot be removed.", - "operationId": "remove_organization_member", + "operationId": "delete_file", "parameters": [ { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "user_id", + "name": "file_id", "in": "path", - "description": "User ID of the member to remove", + "description": "The ID of the file to delete", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], "responses": { - "204": { - "description": "Member removed successfully" - }, - "400": { - "description": "Bad request - cannot remove last owner", + "200": { + "description": "File deleted successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/FileDeleteResponse" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -5450,8 +5510,8 @@ } } }, - "403": { - "description": "Forbidden - not an admin or owner", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5461,17 +5521,7 @@ } }, "404": { - "description": "Member not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal server error", + "description": "File not found", "content": { "application/json": { "schema": { @@ -5483,38 +5533,41 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/organizations/{org_id}/reporting-tokens": { + "/v1/files/{file_id}/content": { "get": { "tags": [ - "Reporting" + "Files" ], - "summary": "List active reporting tokens for an organization.", - "description": "The response includes non-secret prefixes and audit timestamps. It never\nincludes raw reporting tokens or stored hashes.", - "operationId": "list_reporting_tokens", + "operationId": "get_file_content", "parameters": [ { - "name": "org_id", + "name": "file_id", "in": "path", - "description": "Organization ID", + "description": "The ID of the file to retrieve content from", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], "responses": { "200": { - "description": "Active reporting tokens", + "description": "File content retrieved successfully", + "content": { + "application/octet-stream": {} + } + }, + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListReportingTokensResponse" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -5529,18 +5582,8 @@ } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal server error", + "404": { + "description": "File not found", "content": { "application/json": { "schema": { @@ -5552,52 +5595,64 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] - }, - "post": { + } + }, + "/v1/health": { + "get": { "tags": [ - "Reporting" + "Health" ], - "summary": "Create a read-only reporting token.", - "description": "Organization owners and admins can create reporting tokens for usage\nexport and summary endpoints. The raw `rpt-` token is returned once.", - "operationId": "create_reporting_token", - "parameters": [ - { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "summary": "Health check", + "description": "Check service health status. No authentication required.", + "operationId": "health_check", + "responses": { + "200": { + "description": "Service is healthy", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } + } } } + } + } + }, + "/v1/images/edits": { + "post": { + "tags": [ + "Images" ], + "summary": "Edit images from a text prompt and image", + "description": "Edit images using an AI model from an image and text description. OpenAI-compatible endpoint.\n\n**Request Body (multipart/form-data):**\nAll fields should be provided as text values or files as indicated in the schema.", + "operationId": "image_edits", "requestBody": { "content": { - "application/json": { + "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/CreateReportingTokenRequest" + "$ref": "#/components/schemas/ImageEditRequestSchema" } } }, "required": true }, "responses": { - "201": { - "description": "Reporting token created", + "200": { + "description": "Image edited successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateReportingTokenResponse" + "$ref": "#/components/schemas/ImageGenerationResponse" } } } }, "400": { - "description": "Invalid request", + "description": "Invalid request parameters", "content": { "application/json": { "schema": { @@ -5607,7 +5662,7 @@ } }, "401": { - "description": "Unauthorized", + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -5616,8 +5671,28 @@ } } }, - "403": { - "description": "Forbidden", + "404": { + "description": "Model not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "Payload too large", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Rate limited or overloaded — retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", "content": { "application/json": { "schema": { @@ -5627,7 +5702,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -5639,47 +5714,42 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/organizations/{org_id}/reporting-tokens/{token_id}": { - "delete": { + "/v1/images/generations": { + "post": { "tags": [ - "Reporting" + "Images" ], - "summary": "Revoke a reporting token.", - "description": "Revoked reporting tokens can no longer authenticate usage reporting\nrequests.", - "operationId": "revoke_reporting_token", - "parameters": [ - { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "summary": "Generate images from text prompt", + "description": "Generate images using an AI model from a text description. OpenAI-compatible endpoint.", + "operationId": "image_generations", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageGenerationRequest" + } } }, - { - "name": "token_id", - "in": "path", - "description": "Reporting token ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], + "required": true + }, "responses": { - "204": { - "description": "Reporting token revoked" + "200": { + "description": "Image generated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageGenerationResponse" + } + } + } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Invalid request parameters", "content": { "application/json": { "schema": { @@ -5688,8 +5758,8 @@ } } }, - "403": { - "description": "Forbidden", + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -5698,8 +5768,8 @@ } } }, - "404": { - "description": "Reporting token not found", + "429": { + "description": "Rate limited or overloaded — retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", "content": { "application/json": { "schema": { @@ -5709,7 +5779,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -5721,24 +5791,24 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/organizations/{org_id}/staking/farm": { + "/v1/invitations/{token}": { "get": { "tags": [ - "Staking Farm" + "Invitations" ], - "summary": "Get organization staking farm state", - "description": "Returns the staking farm source and last synced farm-credit state for an\norganization. The organization must be the NEAR-authenticated user's default\norganization.", - "operationId": "get_organization_staking_farm", + "summary": "Get invitation details by token (public endpoint)", + "description": "Returns invitation details for a specific token. This is a public endpoint\nthat allows users to view invitation details before logging in.", + "operationId": "get_invitation_by_token", "parameters": [ { - "name": "org_id", + "name": "token", "in": "path", - "description": "Organization ID", + "description": "Invitation token", "required": true, "schema": { "type": "string" @@ -5747,37 +5817,17 @@ ], "responses": { "200": { - "description": "Organization staking farm state", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StakingFarmStateResponse" - } - } - } - }, - "400": { - "description": "Invalid organization ID", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "401": { - "description": "Unauthorized", + "description": "Invitation details", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/OrganizationInvitationResponse" } } } }, - "403": { - "description": "Forbidden", + "404": { + "description": "Invitation not found", "content": { "application/json": { "schema": { @@ -5786,8 +5836,8 @@ } } }, - "404": { - "description": "No staking farm source found", + "410": { + "description": "Invitation expired or no longer pending", "content": { "application/json": { "schema": { @@ -5806,27 +5856,22 @@ } } } - }, - "security": [ - { - "session_token": [] - } - ] + } } }, - "/v1/organizations/{org_id}/staking/farm/sync": { + "/v1/invitations/{token}/accept": { "post": { "tags": [ - "Staking Farm" + "Invitations" ], - "summary": "Sync organization staking farm credits", - "description": "Links or refreshes the NEAR-authenticated user's staking farm source for the\norganization, then syncs reward units from the configured staking contract.", - "operationId": "sync_organization_staking_farm", + "summary": "Accept invitation by token (requires authentication)", + "description": "Accepts an invitation using its token. The authenticated user's email\nmust match the invitation email.", + "operationId": "accept_invitation_by_token", "parameters": [ { - "name": "org_id", + "name": "token", "in": "path", - "description": "Organization ID", + "description": "Invitation token", "required": true, "schema": { "type": "string" @@ -5835,17 +5880,17 @@ ], "responses": { "200": { - "description": "Synced organization staking farm state", + "description": "Invitation accepted successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StakingFarmStateResponse" + "$ref": "#/components/schemas/AcceptInvitationResponse" } } } }, "400": { - "description": "Invalid organization ID", + "description": "Bad request - invitation expired or invalid", "content": { "application/json": { "schema": { @@ -5865,7 +5910,17 @@ } }, "403": { - "description": "Forbidden", + "description": "Forbidden - invitation belongs to another user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Invitation not found", "content": { "application/json": { "schema": { @@ -5875,7 +5930,7 @@ } }, "409": { - "description": "NEAR account conflict or organization inactive", + "description": "User is already a member", "content": { "application/json": { "schema": { @@ -5902,58 +5957,55 @@ ] } }, - "/v1/organizations/{org_id}/usage/balance": { + "/v1/model/list": { "get": { "tags": [ - "Usage" + "Models" ], - "summary": "Get organization balance", - "description": "Returns the current spending balance for an organization", - "operationId": "get_organization_balance", - "parameters": [ + "summary": "List models with pricing", + "description": "Get all available models with pricing information. Public endpoint.\n\nThe full model catalog (a few dozen entries) is loaded once and cached\nin-process for a short TTL. `limit` / `offset` slice the cached list\nin memory, so pagination is consistent across pages within a single\ncache window and adds essentially no DB load.", + "operationId": "list_models", + "parameters": [ { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, + "name": "limit", + "in": "query", + "description": "Maximum number of models to return. Defaults to 100. Must be\nnon-negative; values are capped only by the catalog size.", + "required": false, "schema": { - "type": "string" + "type": [ + "integer", + "null" + ], + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Number of models to skip from the start of the catalog.\nDefaults to 0. Must be non-negative.", + "required": false, + "schema": { + "type": [ + "integer", + "null" + ], + "format": "int64" } } ], "responses": { "200": { - "description": "Organization balance", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OrganizationBalanceResponse" - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "403": { - "description": "Forbidden", + "description": "List of models with pricing", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ModelListResponse" } } } }, - "404": { - "description": "Not found", + "400": { + "description": "Invalid pagination parameters", "content": { "application/json": { "schema": { @@ -5963,7 +6015,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -5972,55 +6024,41 @@ } } } - }, - "security": [ - { - "session_token": [] - } - ] + } } }, - "/v1/organizations/{org_id}/usage/by-model": { + "/v1/model/{model_name}": { "get": { "tags": [ - "Usage" + "Models" ], - "summary": "Get organization usage broken down by model.", - "description": "Returns one row per model, summed over a rolling window ending now:\n`day` = last 24h, `week` = last 7 days, `month` = last 30 days (NOT calendar\nday/week/month-to-date). Used by the dashboard pie chart to show which models\ndrive spend.", - "operationId": "get_organization_usage_by_model", + "summary": "Get model details", + "description": "Get pricing and metadata for a specific model. URL-encode model names containing slashes. Public endpoint.", + "operationId": "get_model_by_name", "parameters": [ { - "name": "org_id", + "name": "model_name", "in": "path", - "description": "Organization ID", + "description": "Model name (URL-encode if it contains slashes)", "required": true, "schema": { "type": "string" } - }, - { - "name": "period", - "in": "query", - "description": "Rolling window: `day` (last 24h), `week` (last 7d), or `month` (last 30d). Default: `month`", - "required": false, - "schema": { - "type": "string" - } } ], "responses": { "200": { - "description": "Per-model usage breakdown", + "description": "Model details with pricing", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UsageByModelResponse" + "$ref": "#/components/schemas/ModelWithPricing" } } } }, - "401": { - "description": "Unauthorized", + "404": { + "description": "Model not found", "content": { "application/json": { "schema": { @@ -6029,8 +6067,8 @@ } } }, - "403": { - "description": "Forbidden", + "500": { + "description": "Server error", "content": { "application/json": { "schema": { @@ -6038,9 +6076,31 @@ } } } + } + } + } + }, + "/v1/models": { + "get": { + "tags": [ + "Chat" + ], + "summary": "List available models", + "description": "Returns all AI models available for completions. OpenAI-compatible endpoint.", + "operationId": "models", + "responses": { + "200": { + "description": "List of available models", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelsResponse" + } + } + } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -6051,149 +6111,71 @@ } }, "security": [ - { - "session_token": [] - } + {} ] } }, - "/v1/organizations/{org_id}/usage/export": { + "/v1/organizations": { "get": { "tags": [ - "Reporting" + "Organizations" ], - "summary": "Export organization usage and cost rows.", - "description": "Requires a reporting token scoped to the organization in the path. Results\nare returned in descending `(created_at, source, id)` order with opaque\ncursor pagination.", - "operationId": "export_usage", + "summary": "List organizations", + "description": "Get all organizations you belong to.", + "operationId": "list_organizations", "parameters": [ { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "start_time", - "in": "query", - "description": "Inclusive RFC3339 start timestamp. Defaults to 366 days before the effective end_time.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end_time", - "in": "query", - "description": "Inclusive RFC3339 end timestamp. Defaults to the request time. The effective range must not exceed 366 days.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "source", - "in": "query", - "description": "Usage source to export. Defaults to all.", - "required": false, - "schema": { - "$ref": "#/components/schemas/ReportingUsageSource" - } - }, - { - "name": "workspace_id", - "in": "query", - "description": "Filter by workspace ID.", - "required": false, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "api_key_id", - "in": "query", - "description": "Filter by API key ID.", - "required": false, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "model", - "in": "query", - "description": "Filter inference rows by model name.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "inference_type", + "name": "limit", "in": "query", - "description": "Filter inference rows by inference type.", + "description": "Maximum number to return", "required": false, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } }, { - "name": "service_name", + "name": "offset", "in": "query", - "description": "Filter service rows by platform service name.", + "description": "Number to skip", "required": false, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } }, { - "name": "limit", + "name": "order_by", "in": "query", - "description": "Maximum rows to return. Defaults to 100 and must not exceed 1000.", + "description": "Sort by field", "required": false, "schema": { - "type": "integer", - "format": "int32", - "minimum": 0 + "$ref": "#/components/schemas/OrganizationOrderBy" } }, { - "name": "cursor", + "name": "order_direction", "in": "query", - "description": "Opaque cursor returned by the previous export page.", + "description": "Sort direction", "required": false, "schema": { - "type": "string" + "$ref": "#/components/schemas/OrganizationOrderDirection" } } ], "responses": { "200": { - "description": "Usage export page", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReportingUsageExportResponse" - } - } - } - }, - "400": { - "description": "Invalid filters or cursor", + "description": "List of organizations", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ListOrganizationsResponse" } } } }, "401": { - "description": "Unauthorized", + "description": "Invalid or missing session token", "content": { "application/json": { "schema": { @@ -6202,8 +6184,8 @@ } } }, - "403": { - "description": "Reporting token is not scoped to this organization", + "500": { + "description": "Server error", "content": { "application/json": { "schema": { @@ -6211,9 +6193,44 @@ } } } - }, - "429": { - "description": "Reporting rate or concurrency limit exceeded", + } + }, + "security": [ + { + "session_token": [] + } + ] + }, + "post": { + "tags": [ + "Organizations" + ], + "summary": "Create a new organization", + "description": "Creates a new organization with the authenticated user as owner.", + "operationId": "create_organization", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Organization created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationResponse" + } + } + } + }, + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -6222,8 +6239,8 @@ } } }, - "500": { - "description": "Internal server error", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -6232,8 +6249,18 @@ } } }, - "504": { - "description": "Reporting request timed out", + "409": { + "description": "Organization already exists", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -6245,100 +6272,25 @@ }, "security": [ { - "reporting_token": [] + "session_token": [] } ] } }, - "/v1/organizations/{org_id}/usage/history": { + "/v1/organizations/{org_id}": { "get": { "tags": [ - "Usage" + "Organizations" ], - "summary": "Get organization usage history", - "description": "Returns paginated usage history for an organization", - "operationId": "get_organization_usage_history", + "summary": "Get organization by ID", + "description": "Returns organization details for a specific organization ID.", + "operationId": "get_organization", "parameters": [ { "name": "org_id", "in": "path", "description": "Organization ID", "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "description": "Number of records to return (default: 100)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Offset for pagination (default: 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "start_date", - "in": "query", - "description": "Inclusive UTC start date in YYYY-MM-DD or RFC3339 format.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end_date", - "in": "query", - "description": "Inclusive UTC end date in YYYY-MM-DD or RFC3339 format.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "start_time", - "in": "query", - "description": "Inclusive RFC3339 start timestamp. Takes precedence over start_date.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end_time", - "in": "query", - "description": "Inclusive RFC3339 end timestamp. Takes precedence over end_date.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "workspace_id", - "in": "query", - "description": "Filter by workspace ID.", - "required": false, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "api_key_id", - "in": "query", - "description": "Filter by API key ID.", - "required": false, "schema": { "type": "string", "format": "uuid" @@ -6347,11 +6299,11 @@ ], "responses": { "200": { - "description": "Usage history", + "description": "Organization details", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UsageHistoryResponse" + "$ref": "#/components/schemas/OrganizationResponse" } } } @@ -6376,6 +6328,16 @@ } } }, + "404": { + "description": "Organization not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -6392,14 +6354,14 @@ "session_token": [] } ] - } - }, - "/v1/organizations/{org_id}/usage/metrics": { - "get": { + }, + "put": { "tags": [ - "Usage" + "Organizations" ], - "operationId": "get_user_organization_metrics", + "summary": "Update organization", + "description": "Updates organization details for a specific organization ID.", + "operationId": "update_organization", "parameters": [ { "name": "org_id", @@ -6407,35 +6369,38 @@ "description": "Organization ID", "required": true, "schema": { - "type": "string" - } - }, - { - "name": "start", - "in": "query", - "description": "Start date (ISO 8601, default: 30 days ago)", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end", - "in": "query", - "description": "End date (ISO 8601, default: now)", - "required": false, - "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Organization usage metrics", + "description": "Updated organization", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserOrganizationMetrics" + "$ref": "#/components/schemas/OrganizationResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -6460,6 +6425,16 @@ } } }, + "404": { + "description": "Organization not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -6476,16 +6451,14 @@ "session_token": [] } ] - } - }, - "/v1/organizations/{org_id}/usage/summary": { - "get": { + }, + "delete": { "tags": [ - "Reporting" + "Organizations" ], - "summary": "Summarize organization usage and costs.", - "description": "Requires a reporting token scoped to the organization in the path. Totals\ninclude inference and platform service usage by default.", - "operationId": "summary_usage", + "summary": "Delete organization (owner only)", + "description": "Deletes an organization. Only the organization owner can perform this action.", + "operationId": "delete_organization", "parameters": [ { "name": "org_id", @@ -6496,102 +6469,11 @@ "type": "string", "format": "uuid" } - }, - { - "name": "start_time", - "in": "query", - "description": "Inclusive RFC3339 start timestamp. Defaults to 366 days before the effective end_time.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end_time", - "in": "query", - "description": "Inclusive RFC3339 end timestamp. Defaults to the request time. The effective range must not exceed 366 days.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "source", - "in": "query", - "description": "Usage source to summarize. Defaults to all.", - "required": false, - "schema": { - "$ref": "#/components/schemas/ReportingUsageSource" - } - }, - { - "name": "workspace_id", - "in": "query", - "description": "Filter by workspace ID.", - "required": false, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "api_key_id", - "in": "query", - "description": "Filter by API key ID.", - "required": false, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "model", - "in": "query", - "description": "Filter inference usage by model name.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "inference_type", - "in": "query", - "description": "Filter inference usage by inference type.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "service_name", - "in": "query", - "description": "Filter service usage by platform service name.", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Usage summary", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReportingUsageSummaryResponse" - } - } - } - }, - "400": { - "description": "Invalid filters", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + } + ], + "responses": { + "200": { + "description": "Organization deleted successfully" }, "401": { "description": "Unauthorized", @@ -6604,7 +6486,7 @@ } }, "403": { - "description": "Reporting token is not scoped to this organization", + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -6613,8 +6495,8 @@ } } }, - "429": { - "description": "Reporting rate or concurrency limit exceeded", + "404": { + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -6623,8 +6505,8 @@ } } }, - "500": { - "description": "Internal server error", + "409": { + "description": "Organization is bound to a NEAR staking wallet", "content": { "application/json": { "schema": { @@ -6633,8 +6515,8 @@ } } }, - "504": { - "description": "Reporting request timed out", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -6646,17 +6528,19 @@ }, "security": [ { - "reporting_token": [] + "session_token": [] } ] } }, - "/v1/organizations/{org_id}/usage/timeseries": { - "get": { + "/v1/organizations/{org_id}/invitations/{invitation_id}": { + "delete": { "tags": [ - "Usage" + "Organization Members" ], - "operationId": "get_user_organization_timeseries", + "summary": "Cancel an organization invitation (short path)", + "description": "Cancels a pending invitation for the organization. Only owners and admins can cancel invitations.\nThis endpoint is an alias for `DELETE /v1/organizations/{org_id}/members/invitations/{invitation_id}`.", + "operationId": "cancel_invitation", "parameters": [ { "name": "org_id", @@ -6664,44 +6548,31 @@ "description": "Organization ID", "required": true, "schema": { - "type": "string" - } - }, - { - "name": "start", - "in": "query", - "description": "Start date (ISO 8601, default: 30 days ago)", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end", - "in": "query", - "description": "End date (ISO 8601, default: now)", - "required": false, - "schema": { - "type": "string" + "type": "string", + "format": "uuid" } }, { - "name": "granularity", - "in": "query", - "description": "Time bucket size: hour, day, week (default: day)", - "required": false, + "name": "invitation_id", + "in": "path", + "description": "Invitation ID", + "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], "responses": { - "200": { - "description": "Organization usage timeseries", + "204": { + "description": "Invitation cancelled successfully" + }, + "400": { + "description": "Bad request - invitation is not pending", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserTimeSeriesMetrics" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -6717,7 +6588,17 @@ } }, "403": { - "description": "Forbidden", + "description": "Forbidden - not an admin or owner", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Organization or invitation not found", "content": { "application/json": { "schema": { @@ -6744,14 +6625,14 @@ ] } }, - "/v1/organizations/{org_id}/workspaces": { + "/v1/organizations/{org_id}/members": { "get": { "tags": [ - "Workspaces" + "Organization Members" ], - "summary": "List workspaces in an organization", - "description": "Lists all workspaces within the specified organization. The authenticated user must\nbe a member of the organization to list workspaces.", - "operationId": "list_organization_workspaces", + "summary": "List organization members with limited user information", + "description": "Returns limited user information for privacy and security:\n- All members: See only public user info (username, display name, avatar)\n- Sensitive data (email, last login, etc.) is not exposed to any organization members", + "operationId": "list_organization_members", "parameters": [ { "name": "org_id", @@ -6766,7 +6647,7 @@ { "name": "limit", "in": "query", - "description": "Maximum number of results", + "description": "Number of records to return (default: 100, max: 1000)", "required": false, "schema": { "type": "integer", @@ -6776,39 +6657,21 @@ { "name": "offset", "in": "query", - "description": "Number of results to skip", + "description": "Offset for pagination (default: 0)", "required": false, "schema": { "type": "integer", "format": "int64" } - }, - { - "name": "order_by", - "in": "query", - "description": "Order by", - "required": false, - "schema": { - "$ref": "#/components/schemas/WorkspaceOrderBy" - } - }, - { - "name": "order_direction", - "in": "query", - "description": "Order direction", - "required": false, - "schema": { - "$ref": "#/components/schemas/WorkspaceOrderDirection" - } } ], "responses": { "200": { - "description": "Paginated list of workspaces", + "description": "List of organization members with public user information", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListWorkspacesResponse" + "$ref": "#/components/schemas/ListOrganizationMembersResponse" } } } @@ -6824,7 +6687,7 @@ } }, "403": { - "description": "Forbidden - not a member of organization", + "description": "Forbidden - not a member of the organization", "content": { "application/json": { "schema": { @@ -6852,11 +6715,11 @@ }, "post": { "tags": [ - "Workspaces" + "Organization Members" ], - "summary": "Create a new workspace in an organization", - "description": "Creates a new workspace within the specified organization. The authenticated user must\nbe a member of the organization to create workspaces.", - "operationId": "create_workspace", + "summary": "Add a member to an organization", + "description": "Adds a new member to the organization. The authenticated user must be an owner or admin.", + "operationId": "add_organization_member", "parameters": [ { "name": "org_id", @@ -6873,25 +6736,25 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateWorkspaceRequest" + "$ref": "#/components/schemas/AddOrganizationMemberRequest" } } }, "required": true }, "responses": { - "201": { - "description": "Workspace created successfully", + "200": { + "description": "Member added successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkspaceResponse" + "$ref": "#/components/schemas/OrganizationMemberResponse" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -6900,8 +6763,8 @@ } } }, - "401": { - "description": "Unauthorized", + "403": { + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -6910,8 +6773,8 @@ } } }, - "403": { - "description": "Forbidden - not a member of organization", + "404": { + "description": "User not found", "content": { "application/json": { "schema": { @@ -6921,7 +6784,7 @@ } }, "409": { - "description": "Workspace name already exists in organization", + "description": "User is already a member", "content": { "application/json": { "schema": { @@ -6948,39 +6811,45 @@ ] } }, - "/v1/privacy/classify": { - "post": { + "/v1/organizations/{org_id}/members/invitations": { + "get": { "tags": [ - "Privacy" + "Organization Members" ], - "operationId": "privacy_classify", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PrivacyClassifyRequestDoc" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Privacy classification completed successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PrivacyClassifyResponseDoc" - } - } + "summary": "List organization invitations", + "description": "Returns invitations for the organization. Only accessible to owners and admins.", + "operationId": "list_organization_invitations", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } }, - "400": { - "description": "Invalid request", + { + "name": "status", + "in": "query", + "description": "Filter by status", + "required": false, + "schema": { + "$ref": "#/components/schemas/InvitationStatus" + } + } + ], + "responses": { + "200": { + "description": "List of organization invitations", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "type": "array", + "items": { + "$ref": "#/components/schemas/OrganizationInvitationResponse" + } } } } @@ -6995,8 +6864,8 @@ } } }, - "404": { - "description": "Model not found", + "403": { + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -7005,8 +6874,8 @@ } } }, - "429": { - "description": "Rate limited or overloaded — retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "404": { + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -7016,7 +6885,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -7028,40 +6897,47 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/privacy/redact": { - "post": { + "/v1/organizations/{org_id}/members/invitations/{invitation_id}": { + "delete": { "tags": [ - "Privacy" + "Organization Members" ], - "operationId": "privacy_redact", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PrivacyRedactRequestDoc" - } + "summary": "Cancel an organization invitation", + "description": "Cancels a pending invitation for the organization. Only owners and admins can cancel invitations.", + "operationId": "cancel_organization_invitation", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } }, - "required": true - }, - "responses": { - "200": { - "description": "Privacy redaction completed successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PrivacyRedactResponseDoc" - } - } + { + "name": "invitation_id", + "in": "path", + "description": "Invitation ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } + } + ], + "responses": { + "204": { + "description": "Invitation cancelled successfully" }, "400": { - "description": "Invalid request", + "description": "Bad request - invitation is not pending", "content": { "application/json": { "schema": { @@ -7080,8 +6956,8 @@ } } }, - "404": { - "description": "Model not found", + "403": { + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -7090,8 +6966,8 @@ } } }, - "429": { - "description": "Rate limited or overloaded — retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "404": { + "description": "Organization or invitation not found", "content": { "application/json": { "schema": { @@ -7101,7 +6977,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -7113,24 +6989,36 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/rerank": { + "/v1/organizations/{org_id}/members/invite-by-email": { "post": { "tags": [ - "Rerank" + "Organization Members" + ], + "summary": "Invite users to an organization by email", + "description": "Invites multiple users to the organization by their email addresses. The authenticated user must be an owner or admin.\nReturns results for each invitation attempt, including successes and failures.", + "operationId": "invite_organization_member_by_email", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } ], - "summary": "Document reranking endpoint", - "description": "Ranks documents by relevance to a query using a reranker model.\n\n**Concurrent Request Limits:** Each organization has a per-model concurrent request limit (default: 64).\nWhen the limit is reached, new requests will fail with 429 status code. Wait for in-flight requests to complete before retrying.", - "operationId": "rerank", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RerankRequest" + "$ref": "#/components/schemas/InviteOrganizationMemberByEmailRequest" } } }, @@ -7138,17 +7026,17 @@ }, "responses": { "200": { - "description": "Successful rerank", + "description": "Invitation results (may include partial failures)", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RerankResponse" + "$ref": "#/components/schemas/InviteOrganizationMemberByEmailResponse" } } } }, "400": { - "description": "Invalid request (empty documents, invalid model, etc.)", + "description": "Bad request - empty invitation list", "content": { "application/json": { "schema": { @@ -7158,17 +7046,7 @@ } }, "401": { - "description": "Unauthorized (missing or invalid API key)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Model not found", + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -7177,8 +7055,8 @@ } } }, - "429": { - "description": "Rate limited or overloaded — retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "403": { + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -7188,7 +7066,7 @@ } }, "500": { - "description": "Server error (billing failure, provider error)", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -7200,24 +7078,46 @@ }, "security": [ { - "ApiKeyAuth": [] + "session_token": [] } ] } }, - "/v1/responses": { - "post": { + "/v1/organizations/{org_id}/members/{user_id}": { + "put": { "tags": [ - "Responses" + "Organization Members" + ], + "summary": "Update an organization member's role", + "description": "Updates a member's role in the organization. The authenticated user must be an owner or admin.\nOnly owners can promote members to owner role.", + "operationId": "update_organization_member", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "user_id", + "in": "path", + "description": "User ID of the member to update", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } ], - "summary": "Create response", - "description": "Generate a single-turn, stateless AI response with optional streaming.", - "operationId": "create_response", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StatelessCreateResponseRequestSchema" + "$ref": "#/components/schemas/UpdateOrganizationMemberRequest" } } }, @@ -7225,27 +7125,17 @@ }, "responses": { "200": { - "description": "Response created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResponseObject" - } - } - } - }, - "400": { - "description": "Invalid request", + "description": "Member updated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/OrganizationMemberResponse" } } } }, "401": { - "description": "Invalid or missing API key", + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -7254,8 +7144,8 @@ } } }, - "402": { - "description": "Insufficient credits", + "403": { + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -7265,7 +7155,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -7277,42 +7167,45 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] - } - }, - "/v1/score": { - "post": { + }, + "delete": { "tags": [ - "Score" + "Organization Members" ], - "summary": "Text similarity scoring endpoint", - "description": "Scores the similarity between two texts using a scoring/ranking model.\n\n**Concurrent Request Limits:** Each organization has a per-model concurrent request limit (default: 64).\nWhen the limit is reached, new requests will fail with 429 status code. Wait for in-flight requests to complete before retrying.", - "operationId": "score", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ScoreRequest" - } + "summary": "Remove a member from an organization", + "description": "Removes a member from the organization. The authenticated user must be an owner or admin,\nor the member can remove themselves. The last owner cannot be removed.", + "operationId": "remove_organization_member", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } }, - "required": true - }, - "responses": { - "200": { - "description": "Successful score", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ScoreResponse" - } - } + { + "name": "user_id", + "in": "path", + "description": "User ID of the member to remove", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } + } + ], + "responses": { + "204": { + "description": "Member removed successfully" }, "400": { - "description": "Invalid request (empty texts, invalid model, etc.)", + "description": "Bad request - cannot remove last owner", "content": { "application/json": { "schema": { @@ -7322,7 +7215,7 @@ } }, "401": { - "description": "Unauthorized (missing or invalid API key)", + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -7331,8 +7224,8 @@ } } }, - "404": { - "description": "Model not found", + "403": { + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -7341,8 +7234,8 @@ } } }, - "429": { - "description": "Rate limited or overloaded — retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "404": { + "description": "Member not found", "content": { "application/json": { "schema": { @@ -7352,7 +7245,7 @@ } }, "500": { - "description": "Server error (billing failure, provider error)", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -7364,51 +7257,54 @@ }, "security": [ { - "ApiKeyAuth": [] + "session_token": [] } ] } }, - "/v1/services": { + "/v1/organizations/{org_id}/reporting-tokens": { "get": { "tags": [ - "Services" + "Reporting" ], - "summary": "List platform services (public, no auth)", - "operationId": "list_services", + "summary": "List active reporting tokens for an organization.", + "description": "The response includes non-secret prefixes and audit timestamps. It never\nincludes raw reporting tokens or stored hashes.", + "operationId": "list_reporting_tokens", "parameters": [ { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "required": false, + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string", + "format": "uuid" } } ], "responses": { "200": { - "description": "Services list", + "description": "Active reporting tokens", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServiceListResponse" + "$ref": "#/components/schemas/ListReportingTokensResponse" } } } }, - "400": { - "description": "Invalid parameters", + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -7427,40 +7323,75 @@ } } } - } - } - }, - "/v1/services/{service_name}": { - "get": { + }, + "security": [ + { + "session_token": [] + } + ] + }, + "post": { "tags": [ - "Services" + "Reporting" ], - "summary": "Get platform service by name (public, no auth)", - "operationId": "get_service_by_name", + "summary": "Create a read-only reporting token.", + "description": "Organization owners and admins can create reporting tokens for usage\nexport and summary endpoints. The raw `rpt-` token is returned once.", + "operationId": "create_reporting_token", "parameters": [ { - "name": "service_name", + "name": "org_id", "in": "path", - "description": "Service name (e.g. web_search)", + "description": "Organization ID", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateReportingTokenRequest" + } + } + }, + "required": true + }, "responses": { - "200": { - "description": "Service details", + "201": { + "description": "Reporting token created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServiceResponse" + "$ref": "#/components/schemas/CreateReportingTokenResponse" } } } }, - "404": { - "description": "Service not found", + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -7479,63 +7410,60 @@ } } } - } + }, + "security": [ + { + "session_token": [] + } + ] } }, - "/v1/signature/{chat_id}": { - "get": { + "/v1/organizations/{org_id}/reporting-tokens/{token_id}": { + "delete": { "tags": [ - "Attestation" + "Reporting" ], - "summary": "Get completion or Responses API signature", - "description": "Get a cryptographic signature for a chat completion or completed stateless\n`resp_*` response. Returns signature data on success, or an unavailable\nresponse if a chat-completion stream was disconnected.", - "operationId": "get_signature", + "summary": "Revoke a reporting token.", + "description": "Revoked reporting tokens can no longer authenticate usage reporting\nrequests.", + "operationId": "revoke_reporting_token", "parameters": [ { - "name": "chat_id", + "name": "org_id", "in": "path", - "description": "Chat completion ID or resp_* Responses API ID", + "description": "Organization ID", "required": true, "schema": { - "type": "string" - } - }, - { - "name": "model", - "in": "query", - "required": false, - "schema": { - "type": [ - "string", - "null" - ] + "type": "string", + "format": "uuid" } }, { - "name": "signing_algo", - "in": "query", - "required": false, + "name": "token_id", + "in": "path", + "description": "Reporting token ID", + "required": true, "schema": { - "type": [ - "string", - "null" - ] + "type": "string", + "format": "uuid" } } ], "responses": { - "200": { - "description": "Signature retrieved or unavailable due to disconnect", + "204": { + "description": "Reporting token revoked" + }, + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignatureResponse" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "400": { - "description": "Invalid parameters", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -7545,7 +7473,17 @@ } }, "404": { - "description": "Signature not found", + "description": "Reporting token not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -7557,26 +7495,47 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/staking/farm/config": { + "/v1/organizations/{org_id}/staking/farm": { "get": { "tags": [ "Staking Farm" ], - "summary": "Get staking farm configuration", - "description": "Returns the active House of Stake farm configuration used to convert\non-chain staking reward units into NEAR AI Cloud credits.", - "operationId": "get_staking_farm_config", + "summary": "Get organization staking farm state", + "description": "Returns the staking farm source and last synced farm-credit state for an\norganization. The organization must be the NEAR-authenticated user's default\norganization.", + "operationId": "get_organization_staking_farm", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { - "description": "Staking farm configuration", + "description": "Organization staking farm state", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StakingFarmConfigResponse" + "$ref": "#/components/schemas/StakingFarmStateResponse" + } + } + } + }, + "400": { + "description": "Invalid organization ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -7591,45 +7550,8 @@ } } }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "session_token": [] - } - ] - } - }, - "/v1/users/me": { - "get": { - "tags": [ - "Users" - ], - "summary": "Get current user", - "description": "Returns the profile of the currently authenticated user, including their organizations and workspaces.", - "operationId": "get_current_user", - "responses": { - "200": { - "description": "Current user profile", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserResponse" - } - } - } - }, - "401": { - "description": "Unauthorized", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -7639,7 +7561,7 @@ } }, "404": { - "description": "User not found", + "description": "No staking farm source found", "content": { "application/json": { "schema": { @@ -7664,47 +7586,40 @@ "session_token": [] } ] - }, - "patch": { + } + }, + "/v1/organizations/{org_id}/staking/farm/sync": { + "post": { "tags": [ - "Users" + "Staking Farm" ], - "summary": "Update current user's profile", - "description": "Updates the profile information for the currently authenticated user.", - "operationId": "update_current_user_profile", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateUserProfileRequest" - } + "summary": "Sync organization staking farm credits", + "description": "Links or refreshes the NEAR-authenticated user's staking farm source for the\norganization, then syncs reward units from the configured staking contract.", + "operationId": "sync_organization_staking_farm", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string" } - }, - "required": true - }, + } + ], "responses": { "200": { - "description": "Updated user profile", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserResponse" - } - } - } - }, - "401": { - "description": "Unauthorized", + "description": "Synced organization staking farm state", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/StakingFarmStateResponse" } } } }, - "500": { - "description": "Internal server error", + "400": { + "description": "Invalid organization ID", "content": { "application/json": { "schema": { @@ -7712,33 +7627,6 @@ } } } - } - }, - "security": [ - { - "session_token": [] - } - ] - } - }, - "/v1/users/me/access-tokens": { - "post": { - "tags": [ - "Users" - ], - "summary": "Create a new access token", - "description": "Creates a new short-lived access token using the current refresh token.", - "operationId": "create_access_token", - "responses": { - "200": { - "description": "Access token created successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AccessAndRefreshTokenResponse" - } - } - } }, "401": { "description": "Unauthorized", @@ -7750,8 +7638,8 @@ } } }, - "500": { - "description": "Internal server error", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -7759,39 +7647,9 @@ } } } - } - }, - "security": [ - { - "refresh_token": [] - } - ] - } - }, - "/v1/users/me/invitations": { - "get": { - "tags": [ - "Users" - ], - "summary": "List pending invitations for the current user", - "description": "Returns all pending organization invitations for the authenticated user's email.", - "operationId": "list_user_invitations", - "responses": { - "200": { - "description": "List of pending invitations", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrganizationInvitationWithOrgResponse" - } - } - } - } }, - "401": { - "description": "Unauthorized", + "409": { + "description": "NEAR account conflict or organization inactive", "content": { "application/json": { "schema": { @@ -7818,43 +7676,32 @@ ] } }, - "/v1/users/me/invitations/{invitation_id}/accept": { - "post": { + "/v1/organizations/{org_id}/usage/balance": { + "get": { "tags": [ - "Users" + "Usage" ], - "summary": "Accept an organization invitation", - "description": "Accepts a pending invitation and adds the user as a member of the organization.", - "operationId": "accept_invitation", + "summary": "Get organization balance", + "description": "Returns the current spending balance for an organization", + "operationId": "get_organization_balance", "parameters": [ { - "name": "invitation_id", + "name": "org_id", "in": "path", - "description": "Invitation ID", + "description": "Organization ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], "responses": { "200": { - "description": "Invitation accepted successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AcceptInvitationResponse" - } - } - } - }, - "400": { - "description": "Bad request - invitation expired or invalid", + "description": "Organization balance", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/OrganizationBalanceResponse" } } } @@ -7870,7 +7717,7 @@ } }, "403": { - "description": "Forbidden - invitation belongs to another user", + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -7880,17 +7727,7 @@ } }, "404": { - "description": "Invitation not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "409": { - "description": "User is already a member", + "description": "Not found", "content": { "application/json": { "schema": { @@ -7917,36 +7754,41 @@ ] } }, - "/v1/users/me/invitations/{invitation_id}/decline": { - "post": { + "/v1/organizations/{org_id}/usage/by-model": { + "get": { "tags": [ - "Users" + "Usage" ], - "summary": "Decline an organization invitation", - "description": "Declines a pending invitation to join an organization.", - "operationId": "decline_invitation", + "summary": "Get organization usage broken down by model.", + "description": "Returns one row per model, summed over a rolling window ending now:\n`day` = last 24h, `week` = last 7 days, `month` = last 30 days (NOT calendar\nday/week/month-to-date). Used by the dashboard pie chart to show which models\ndrive spend.", + "operationId": "get_organization_usage_by_model", "parameters": [ { - "name": "invitation_id", + "name": "org_id", "in": "path", - "description": "Invitation ID", + "description": "Organization ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" + } + }, + { + "name": "period", + "in": "query", + "description": "Rolling window: `day` (last 24h), `week` (last 7d), or `month` (last 30d). Default: `month`", + "required": false, + "schema": { + "type": "string" } } ], "responses": { "200": { - "description": "Invitation declined successfully" - }, - "400": { - "description": "Bad request - invitation not pending", + "description": "Per-model usage breakdown", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/UsageByModelResponse" } } } @@ -7962,17 +7804,7 @@ } }, "403": { - "description": "Forbidden - invitation belongs to another user", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Invitation not found", + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -7999,139 +7831,133 @@ ] } }, - "/v1/users/me/refresh-tokens": { + "/v1/organizations/{org_id}/usage/export": { "get": { "tags": [ - "Users" + "Reporting" ], - "summary": "Get user's refresh tokens", - "description": "Returns all active refresh tokens for the currently authenticated user.", - "operationId": "get_user_refresh_tokens", - "responses": { - "200": { - "description": "List of user refresh tokens", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RefreshTokenResponse" - } - } - } + "summary": "Export organization usage and cost rows.", + "description": "Requires a reporting token scoped to the organization in the path. Results\nare returned in descending `(created_at, source, id)` order with opaque\ncursor pagination.", + "operationId": "export_usage", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "start_time", + "in": "query", + "description": "Inclusive RFC3339 start timestamp. Defaults to 366 days before the effective end_time.", + "required": false, + "schema": { + "type": "string" } }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "end_time", + "in": "query", + "description": "Inclusive RFC3339 end timestamp. Defaults to the request time. The effective range must not exceed 366 days.", + "required": false, + "schema": { + "type": "string" } - } - }, - "security": [ + }, { - "session_token": [] - } - ] - } - }, - "/v1/users/me/refresh-tokens/{refresh_token_id}": { - "delete": { - "tags": [ - "Users" - ], - "summary": "Revoke a user refresh token", - "description": "Revokes a specific refresh token for the currently authenticated user.", - "operationId": "revoke_user_refresh_token", - "parameters": [ + "name": "source", + "in": "query", + "description": "Usage source to export. Defaults to all.", + "required": false, + "schema": { + "$ref": "#/components/schemas/ReportingUsageSource" + } + }, { - "name": "refresh_token_id", - "in": "path", - "description": "Refresh token ID to revoke", - "required": true, + "name": "workspace_id", + "in": "query", + "description": "Filter by workspace ID.", + "required": false, "schema": { "type": "string", "format": "uuid" } - } - ], - "responses": { - "204": { - "description": "Refresh token revoked successfully" }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "api_key_id", + "in": "query", + "description": "Filter by API key ID.", + "required": false, + "schema": { + "type": "string", + "format": "uuid" } }, - "404": { - "description": "Refresh token not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "model", + "in": "query", + "description": "Filter inference rows by model name.", + "required": false, + "schema": { + "type": "string" } }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "inference_type", + "in": "query", + "description": "Filter inference rows by inference type.", + "required": false, + "schema": { + "type": "string" } - } - }, - "security": [ + }, { - "session_token": [] + "name": "service_name", + "in": "query", + "description": "Filter service rows by platform service name.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum rows to return. Defaults to 100 and must not exceed 1000.", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + }, + { + "name": "cursor", + "in": "query", + "description": "Opaque cursor returned by the previous export page.", + "required": false, + "schema": { + "type": "string" + } } - ] - } - }, - "/v1/users/me/status": { - "get": { - "tags": [ - "Users" ], - "summary": "Get current user's account eligibility status", - "description": "Returns a minimal eligibility response for frontend preflight checks. AML/KYT\nprovider details are never exposed in this public response.", - "operationId": "get_user_status", "responses": { "200": { - "description": "Current account is eligible", + "description": "Usage export page", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserStatusResponse" + "$ref": "#/components/schemas/ReportingUsageExportResponse" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Invalid filters or cursor", "content": { "application/json": { "schema": { @@ -8140,8 +7966,8 @@ } } }, - "403": { - "description": "Current account is not eligible", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -8150,8 +7976,8 @@ } } }, - "500": { - "description": "Internal server error", + "403": { + "description": "Reporting token is not scoped to this organization", "content": { "application/json": { "schema": { @@ -8159,34 +7985,19 @@ } } } - } - }, - "security": [ - { - "session_token": [] - } - ] - } - }, - "/v1/users/me/tokens": { - "delete": { - "tags": [ - "Users" - ], - "summary": "Revoke all tokens for a user", - "description": "Revokes all tokens (both refresh tokens and access tokens) for the currently authenticated user.\nThis deletes all refresh tokens from the database and invalidates all JWT access tokens\nby updating the user's tokens_revoked_at timestamp.", - "operationId": "revoke_all_user_tokens", - "responses": { - "200": { - "description": "All tokens revoked successfully", + }, + "429": { + "description": "Reporting rate or concurrency limit exceeded", "content": { "application/json": { - "schema": {} + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "401": { - "description": "Unauthorized", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -8195,8 +8006,8 @@ } } }, - "500": { - "description": "Internal server error", + "504": { + "description": "Reporting request timed out", "content": { "application/json": { "schema": { @@ -8208,135 +8019,113 @@ }, "security": [ { - "session_token": [] + "reporting_token": [] } ] } }, - "/v1/workspaces/{workspace_id}": { + "/v1/organizations/{org_id}/usage/history": { "get": { "tags": [ - "Workspaces" + "Usage" ], - "summary": "Get workspace by ID", - "description": "Returns workspace details for a specific workspace ID.", - "operationId": "get_workspace", + "summary": "Get organization usage history", + "description": "Returns paginated usage history for an organization", + "operationId": "get_organization_usage_history", "parameters": [ { - "name": "workspace_id", + "name": "org_id", "in": "path", - "description": "Workspace ID", + "description": "Organization ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } - } - ], - "responses": { - "200": { - "description": "Workspace details", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorkspaceResponse" - } - } + }, + { + "name": "limit", + "in": "query", + "description": "Number of records to return (default: 100)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" } }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "offset", + "in": "query", + "description": "Offset for pagination (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "start_date", + "in": "query", + "description": "Inclusive UTC start date in YYYY-MM-DD or RFC3339 format.", + "required": false, + "schema": { + "type": "string" } }, - "404": { - "description": "Workspace not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "end_date", + "in": "query", + "description": "Inclusive UTC end date in YYYY-MM-DD or RFC3339 format.", + "required": false, + "schema": { + "type": "string" } }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "start_time", + "in": "query", + "description": "Inclusive RFC3339 start timestamp. Takes precedence over start_date.", + "required": false, + "schema": { + "type": "string" } - } - }, - "security": [ + }, { - "session_token": [] - } - ] - }, - "put": { - "tags": [ - "Workspaces" - ], - "summary": "Update workspace", - "description": "Updates workspace details for a specific workspace ID.", - "operationId": "update_workspace", - "parameters": [ + "name": "end_time", + "in": "query", + "description": "Inclusive RFC3339 end timestamp. Takes precedence over end_date.", + "required": false, + "schema": { + "type": "string" + } + }, { "name": "workspace_id", - "in": "path", - "description": "Workspace ID", - "required": true, + "in": "query", + "description": "Filter by workspace ID.", + "required": false, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "api_key_id", + "in": "query", + "description": "Filter by API key ID.", + "required": false, "schema": { "type": "string", "format": "uuid" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateWorkspaceRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Updated workspace", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorkspaceResponse" - } - } - } - }, - "400": { - "description": "Bad request", + "description": "Usage history", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/UsageHistoryResponse" } } } @@ -8361,16 +8150,6 @@ } } }, - "404": { - "description": "Workspace not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -8387,42 +8166,56 @@ "session_token": [] } ] - }, - "delete": { + } + }, + "/v1/organizations/{org_id}/usage/metrics": { + "get": { "tags": [ - "Workspaces" + "Usage" ], - "summary": "Delete workspace", - "description": "Deletes (deactivates) a workspace. Only the workspace creator or organization admin/owner can delete.", - "operationId": "delete_workspace", + "operationId": "get_user_organization_metrics", "parameters": [ { - "name": "workspace_id", + "name": "org_id", "in": "path", - "description": "Workspace ID", + "description": "Organization ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" + } + }, + { + "name": "start", + "in": "query", + "description": "Start date (ISO 8601, default: 30 days ago)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "end", + "in": "query", + "description": "End date (ISO 8601, default: now)", + "required": false, + "schema": { + "type": "string" } } ], "responses": { "200": { - "description": "Workspace deleted successfully" - }, - "401": { - "description": "Unauthorized", + "description": "Organization usage metrics", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/UserOrganizationMetrics" } } } }, - "403": { - "description": "Forbidden", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -8431,8 +8224,8 @@ } } }, - "404": { - "description": "Workspace not found", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -8459,19 +8252,19 @@ ] } }, - "/v1/workspaces/{workspace_id}/api-keys": { + "/v1/organizations/{org_id}/usage/summary": { "get": { "tags": [ - "Workspaces" + "Reporting" ], - "summary": "List API keys for workspace", - "description": "Returns a paginated list of all API keys for a workspace with usage information.", - "operationId": "list_workspace_api_keys", + "summary": "Summarize organization usage and costs.", + "description": "Requires a reporting token scoped to the organization in the path. Totals\ninclude inference and platform service usage by default.", + "operationId": "summary_usage", "parameters": [ { - "name": "workspace_id", + "name": "org_id", "in": "path", - "description": "Workspace ID", + "description": "Organization ID", "required": true, "schema": { "type": "string", @@ -8479,144 +8272,93 @@ } }, { - "name": "limit", + "name": "start_time", "in": "query", - "description": "Maximum number of results (default: 20)", + "description": "Inclusive RFC3339 start timestamp. Defaults to 366 days before the effective end_time.", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } }, { - "name": "offset", + "name": "end_time", "in": "query", - "description": "Number of results to skip (default: 0)", + "description": "Inclusive RFC3339 end timestamp. Defaults to the request time. The effective range must not exceed 366 days.", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } }, { - "name": "order_by", + "name": "source", "in": "query", - "description": "Field to order by: created_at or usage", + "description": "Usage source to summarize. Defaults to all.", "required": false, "schema": { - "$ref": "#/components/schemas/ApiKeyOrderBy" + "$ref": "#/components/schemas/ReportingUsageSource" } }, { - "name": "order_direction", + "name": "workspace_id", "in": "query", - "description": "Sort direction: asc or desc", + "description": "Filter by workspace ID.", "required": false, "schema": { - "$ref": "#/components/schemas/ApiKeyOrderDirection" - } - } - ], - "responses": { - "200": { - "description": "Paginated list of workspace API keys", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ListApiKeysResponse" - } - } + "type": "string", + "format": "uuid" } }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "api_key_id", + "in": "query", + "description": "Filter by API key ID.", + "required": false, + "schema": { + "type": "string", + "format": "uuid" } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "model", + "in": "query", + "description": "Filter inference usage by model name.", + "required": false, + "schema": { + "type": "string" } }, - "404": { - "description": "Workspace not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "inference_type", + "in": "query", + "description": "Filter inference usage by inference type.", + "required": false, + "schema": { + "type": "string" } }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "session_token": [] - } - ] - }, - "post": { - "tags": [ - "Workspaces" - ], - "summary": "Create API key for workspace", - "description": "Creates a new API key for a workspace. Note: The key may take up to 10 seconds\nto become active for authentication due to internal caching.", - "operationId": "create_workspace_api_key", - "parameters": [ { - "name": "workspace_id", - "in": "path", - "description": "Workspace ID", - "required": true, + "name": "service_name", + "in": "query", + "description": "Filter service usage by platform service name.", + "required": false, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateApiKeyRequest" - } - } - }, - "required": true - }, "responses": { - "201": { - "description": "API key created successfully", + "200": { + "description": "Usage summary", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ApiKeyResponse" + "$ref": "#/components/schemas/ReportingUsageSummaryResponse" } } } }, "400": { - "description": "Bad request", + "description": "Invalid filters", "content": { "application/json": { "schema": { @@ -8636,7 +8378,7 @@ } }, "403": { - "description": "Forbidden", + "description": "Reporting token is not scoped to this organization", "content": { "application/json": { "schema": { @@ -8645,8 +8387,8 @@ } } }, - "404": { - "description": "Workspace not found", + "429": { + "description": "Reporting rate or concurrency limit exceeded", "content": { "application/json": { "schema": { @@ -8664,70 +8406,76 @@ } } } + }, + "504": { + "description": "Reporting request timed out", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } }, "security": [ { - "session_token": [] + "reporting_token": [] } ] } }, - "/v1/workspaces/{workspace_id}/api-keys/{api_key_id}/usage/history": { + "/v1/organizations/{org_id}/usage/timeseries": { "get": { "tags": [ "Usage" ], - "summary": "Get API key usage history", - "description": "Returns paginated usage history for a specific API key", - "operationId": "get_api_key_usage_history", + "operationId": "get_user_organization_timeseries", "parameters": [ { - "name": "workspace_id", + "name": "org_id", "in": "path", - "description": "Workspace ID", + "description": "Organization ID", "required": true, "schema": { "type": "string" } }, { - "name": "api_key_id", - "in": "path", - "description": "API Key ID", - "required": true, + "name": "start", + "in": "query", + "description": "Start date (ISO 8601, default: 30 days ago)", + "required": false, "schema": { "type": "string" } }, { - "name": "limit", + "name": "end", "in": "query", - "description": "Number of records to return (default: 100)", + "description": "End date (ISO 8601, default: now)", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } }, { - "name": "offset", + "name": "granularity", "in": "query", - "description": "Offset for pagination (default: 0)", + "description": "Time bucket size: hour, day, week (default: day)", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], "responses": { "200": { - "description": "Usage history", + "description": "Organization usage timeseries", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UsageHistoryResponse" + "$ref": "#/components/schemas/UserTimeSeriesMetrics" } } } @@ -8752,16 +8500,6 @@ } } }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -8780,19 +8518,19 @@ ] } }, - "/v1/workspaces/{workspace_id}/api-keys/{key_id}": { - "delete": { + "/v1/organizations/{org_id}/workspaces": { + "get": { "tags": [ "Workspaces" ], - "summary": "Revoke an API key", - "description": "Revokes a specific API key from a workspace.", - "operationId": "revoke_workspace_api_key", + "summary": "List workspaces in an organization", + "description": "Lists all workspaces within the specified organization. The authenticated user must\nbe a member of the organization to list workspaces.", + "operationId": "list_organization_workspaces", "parameters": [ { - "name": "workspace_id", + "name": "org_id", "in": "path", - "description": "Workspace ID", + "description": "Organization ID", "required": true, "schema": { "type": "string", @@ -8800,32 +8538,57 @@ } }, { - "name": "key_id", - "in": "path", - "description": "API Key ID", - "required": true, + "name": "limit", + "in": "query", + "description": "Maximum number of results", + "required": false, "schema": { - "type": "string", - "format": "uuid" + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Number of results to skip", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "order_by", + "in": "query", + "description": "Order by", + "required": false, + "schema": { + "$ref": "#/components/schemas/WorkspaceOrderBy" + } + }, + { + "name": "order_direction", + "in": "query", + "description": "Order direction", + "required": false, + "schema": { + "$ref": "#/components/schemas/WorkspaceOrderDirection" } } ], "responses": { - "204": { - "description": "API key revoked successfully" - }, - "401": { - "description": "Unauthorized", + "200": { + "description": "Paginated list of workspaces", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ListWorkspacesResponse" } } } }, - "403": { - "description": "Forbidden", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -8834,8 +8597,8 @@ } } }, - "404": { - "description": "API key not found", + "403": { + "description": "Forbidden - not a member of organization", "content": { "application/json": { "schema": { @@ -8861,28 +8624,18 @@ } ] }, - "patch": { + "post": { "tags": [ "Workspaces" ], - "summary": "Update API key", - "description": "Updates an API key's name, expiration date, and/or spending limit. The user must be a member of the\norganization that owns the workspace. All fields are optional - only provided fields will be updated.", - "operationId": "update_workspace_api_key", + "summary": "Create a new workspace in an organization", + "description": "Creates a new workspace within the specified organization. The authenticated user must\nbe a member of the organization to create workspaces.", + "operationId": "create_workspace", "parameters": [ { - "name": "workspace_id", - "in": "path", - "description": "Workspace ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "key_id", + "name": "org_id", "in": "path", - "description": "API Key ID", + "description": "Organization ID", "required": true, "schema": { "type": "string", @@ -8894,19 +8647,19 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateApiKeyRequest" + "$ref": "#/components/schemas/CreateWorkspaceRequest" } } }, "required": true }, "responses": { - "200": { - "description": "API key updated successfully", + "201": { + "description": "Workspace created successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ApiKeyResponse" + "$ref": "#/components/schemas/WorkspaceResponse" } } } @@ -8932,7 +8685,82 @@ } }, "403": { - "description": "Forbidden - not authorized to update this key", + "description": "Forbidden - not a member of organization", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Workspace name already exists in organization", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/privacy/classify": { + "post": { + "tags": [ + "Privacy" + ], + "operationId": "privacy_classify", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrivacyClassifyRequestDoc" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Privacy classification completed successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrivacyClassifyResponseDoc" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -8942,7 +8770,7 @@ } }, "404": { - "description": "API key or workspace not found", + "description": "Model not found", "content": { "application/json": { "schema": { @@ -8951,8 +8779,8 @@ } } }, - "409": { - "description": "Conflict - API key with this name already exists", + "429": { + "description": "Rate limited or overloaded — retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", "content": { "application/json": { "schema": { @@ -8962,7 +8790,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -8974,46 +8802,109 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/workspaces/{workspace_id}/api-keys/{key_id}/spend-limit": { - "patch": { + "/v1/privacy/redact": { + "post": { "tags": [ - "Workspaces" + "Privacy" ], - "summary": "Update API key spend limit", - "description": "Updates the spending limit for a specific API key. The user must be a member of the\norganization that owns the workspace. Set spend_limit to null to remove the limit.", - "operationId": "update_api_key_spend_limit", - "parameters": [ - { - "name": "workspace_id", - "in": "path", - "description": "Workspace ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "operationId": "privacy_redact", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrivacyRedactRequestDoc" + } } }, - { - "name": "key_id", - "in": "path", - "description": "API Key ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "required": true + }, + "responses": { + "200": { + "description": "Privacy redaction completed successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrivacyRedactResponseDoc" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Model not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Rate limited or overloaded — retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v1/rerank": { + "post": { + "tags": [ + "Rerank" ], + "summary": "Document reranking endpoint", + "description": "Ranks documents by relevance to a query using a reranker model.\n\n**Concurrent Request Limits:** Each organization has a per-model concurrent request limit (default: 64).\nWhen the limit is reached, new requests will fail with 429 status code. Wait for in-flight requests to complete before retrying.", + "operationId": "rerank", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateApiKeySpendLimitRequest" + "$ref": "#/components/schemas/RerankRequest" } } }, @@ -9021,17 +8912,17 @@ }, "responses": { "200": { - "description": "Spend limit updated successfully", + "description": "Successful rerank", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ApiKeyResponse" + "$ref": "#/components/schemas/RerankResponse" } } } }, "400": { - "description": "Bad request", + "description": "Invalid request (empty documents, invalid model, etc.)", "content": { "application/json": { "schema": { @@ -9041,7 +8932,7 @@ } }, "401": { - "description": "Unauthorized", + "description": "Unauthorized (missing or invalid API key)", "content": { "application/json": { "schema": { @@ -9050,8 +8941,8 @@ } } }, - "403": { - "description": "Forbidden - not authorized to update this key", + "404": { + "description": "Model not found", "content": { "application/json": { "schema": { @@ -9060,8 +8951,8 @@ } } }, - "404": { - "description": "API key or workspace not found", + "429": { + "description": "Rate limited or overloaded — retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", "content": { "application/json": { "schema": { @@ -9071,7 +8962,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error (billing failure, provider error)", "content": { "application/json": { "schema": { @@ -9083,1392 +8974,4456 @@ }, "security": [ { - "session_token": [] + "ApiKeyAuth": [] } ] } - } - }, - "components": { - "schemas": { - "AcceptInvitationResponse": { - "type": "object", - "description": "Accept invitation response", - "required": [ - "organization_member", - "message" + }, + "/v1/responses": { + "post": { + "tags": [ + "Responses" ], - "properties": { - "message": { - "type": "string" + "summary": "Create response", + "description": "Generate an AI response for a conversation with tool calling and streaming support.", + "operationId": "create_response", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateResponseRequest" + } + } }, - "organization_member": { - "$ref": "#/components/schemas/OrganizationMemberResponse" - } - } - }, - "AccessAndRefreshTokenResponse": { - "type": "object", - "description": "Access token + refresh token response model", - "required": [ - "access_token", - "refresh_token", - "refresh_token_expiration" - ], - "properties": { - "access_token": { - "type": "string" + "required": true + }, + "responses": { + "200": { + "description": "Response created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseObject" + } + } + } }, - "refresh_token": { - "type": "string" + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "refresh_token_expiration": { - "type": "string", - "format": "date-time" + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "402": { + "description": "Insufficient credits", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "AddOrganizationMemberRequest": { - "type": "object", - "description": "Request to add an organization member", - "required": [ - "user_id", - "role" + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v1/responses/{response_id}": { + "get": { + "tags": [ + "Responses" ], - "properties": { - "role": { - "$ref": "#/components/schemas/MemberRole" - }, - "user_id": { - "type": "string" + "summary": "Get a response by ID", + "description": "Retrieve details of a specific response.", + "operationId": "get_response", + "parameters": [ + { + "name": "response_id", + "in": "path", + "description": "Response ID", + "required": true, + "schema": { + "type": "string" + } } - } - }, - "AdminAccessTokenResponse": { - "type": "object", - "description": "Admin access token response model", - "required": [ - "id", - "access_token", - "created_by_user_id", - "created_at", - "expires_at", - "name", - "reason" ], - "properties": { - "access_token": { - "type": "string" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "created_by_user_id": { - "type": "string" - }, - "expires_at": { - "type": "string", - "format": "date-time" + "responses": { + "200": { + "description": "Response details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseObject" + } + } + } }, - "id": { - "type": "string" + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "name": { - "type": "string" + "404": { + "description": "Response not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "reason": { - "type": "string" + "501": { + "description": "Not implemented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } + }, + "security": [ + { + "api_key": [] + } + ] }, - "AdminFeatureRequestListResponse": { - "type": "object", - "required": [ - "requests", - "limit", - "offset", - "total" + "delete": { + "tags": [ + "Responses" ], - "properties": { - "limit": { - "type": "integer", - "format": "int64" - }, - "offset": { - "type": "integer", - "format": "int64" - }, - "requests": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AdminFeatureRequestSummaryResponse" + "summary": "Delete a response", + "description": "Delete a specific response.", + "operationId": "delete_response", + "parameters": [ + { + "name": "response_id", + "in": "path", + "description": "Response ID", + "required": true, + "schema": { + "type": "string" } - }, - "total": { - "type": "integer", - "format": "int64" } - } - }, - "AdminFeatureRequestSummaryResponse": { - "type": "object", - "required": [ - "target", - "uniqueUserCount", - "uniqueOrganizationCount", - "latestRequestedAt", - "recentVotes" ], - "properties": { - "latestRequestedAt": { - "type": "string", - "format": "date-time" + "responses": { + "200": { + "description": "Response deleted successfully" }, - "recentVotes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FeatureRequestVoteResponse" + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - "target": { - "$ref": "#/components/schemas/FeatureRequestTargetResponse" - }, - "uniqueOrganizationCount": { - "type": "integer", - "format": "int64" + "404": { + "description": "Response not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "uniqueUserCount": { - "type": "integer", - "format": "int64" + "501": { + "description": "Not implemented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "AdminInvitationEmailDeliveryResponse": { - "type": "object", - "description": "Admin view of invitation email delivery metadata.", - "required": [ - "organization_id", - "organization_name", - "invitation_id", - "recipient_email", - "role", - "invitation_status", - "email_status", - "invited_by_user_id", - "created_at", - "expires_at" + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v1/responses/{response_id}/cancel": { + "post": { + "tags": [ + "Responses" ], - "properties": { - "created_at": { - "type": "string", - "format": "date-time" - }, - "email_last_error": { - "type": [ - "string", - "null" - ] + "summary": "Cancel a response (for background responses)", + "description": "Cancel an in-progress background response.", + "operationId": "cancel_response", + "parameters": [ + { + "name": "response_id", + "in": "path", + "description": "Response ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Response cancelled successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseObject" + } + } + } }, - "email_message_id": { - "type": [ - "string", - "null" - ] + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "email_sent_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" + "404": { + "description": "Response not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "email_status": { - "$ref": "#/components/schemas/InvitationEmailStatus" + "501": { + "description": "Not implemented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v1/responses/{response_id}/input_items": { + "get": { + "tags": [ + "Responses" + ], + "summary": "List input items for a response", + "description": "Retrieve all input items (user messages and files) for a specific response.", + "operationId": "list_input_items", + "parameters": [ + { + "name": "response_id", + "in": "path", + "description": "Response ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of input items", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseInputItemList" + } + } + } }, - "expires_at": { - "type": "string", - "format": "date-time" + "400": { + "description": "Invalid response ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "invitation_id": { - "type": "string" + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "invitation_status": { - "$ref": "#/components/schemas/InvitationStatus" + "404": { + "description": "Response not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "invited_by_display_name": { - "type": [ - "string", - "null" - ] + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v1/score": { + "post": { + "tags": [ + "Score" + ], + "summary": "Text similarity scoring endpoint", + "description": "Scores the similarity between two texts using a scoring/ranking model.\n\n**Concurrent Request Limits:** Each organization has a per-model concurrent request limit (default: 64).\nWhen the limit is reached, new requests will fail with 429 status code. Wait for in-flight requests to complete before retrying.", + "operationId": "score", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScoreRequest" + } + } }, - "invited_by_email": { - "type": [ - "string", - "null" - ] + "required": true + }, + "responses": { + "200": { + "description": "Successful score", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScoreResponse" + } + } + } }, - "invited_by_user_id": { - "type": "string" + "400": { + "description": "Invalid request (empty texts, invalid model, etc.)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "organization_id": { - "type": "string" + "401": { + "description": "Unauthorized (missing or invalid API key)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "organization_name": { - "type": "string" + "404": { + "description": "Model not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "recipient_email": { - "type": "string" + "429": { + "description": "Rate limited or overloaded — retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "responded_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" - }, - "role": { - "$ref": "#/components/schemas/MemberRole" + "500": { + "description": "Server error (billing failure, provider error)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "AdminInvitationEmailResendResultResponse": { - "type": "object", - "description": "Admin invitation email resend result.", - "required": [ - "invitation_id", - "recipient_email", - "success", - "email_sent", - "email_status" + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v1/services": { + "get": { + "tags": [ + "Services" ], - "properties": { - "email_last_error": { - "type": [ - "string", - "null" - ] - }, - "email_message_id": { - "type": [ - "string", - "null" - ] - }, - "email_sent": { - "type": "boolean" - }, - "email_sent_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" - }, - "email_status": { - "$ref": "#/components/schemas/InvitationEmailStatus" - }, - "error": { - "type": [ - "string", - "null" - ] - }, - "invitation_id": { - "type": "string" - }, - "recipient_email": { - "type": "string" + "summary": "List platform services (public, no auth)", + "operationId": "list_services", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } }, - "success": { - "type": "boolean" + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } } - } - }, - "AdminModelListResponse": { - "type": "object", - "description": "Response for admin model list endpoint", - "required": [ - "models", - "limit", - "offset", - "total" ], - "properties": { - "limit": { - "type": "integer", - "format": "int64" - }, - "models": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AdminModelWithPricing" + "responses": { + "200": { + "description": "Services list", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceListResponse" + } + } } }, - "offset": { - "type": "integer", - "format": "int64" + "400": { + "description": "Invalid parameters", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "total": { - "type": "integer", - "format": "int64" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } - }, - "AdminModelWithPricing": { - "type": "object", - "description": "Model with pricing information for admin listing", - "required": [ - "modelId", - "inputCostPerToken", - "outputCostPerToken", - "costPerImage", - "metadata", - "isActive", - "createdAt", - "updatedAt" + } + }, + "/v1/services/{service_name}": { + "get": { + "tags": [ + "Services" ], - "properties": { - "cacheReadCostPerToken": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/DecimalPrice", - "description": "Cost per cached input token. Omitted when cache pricing is disabled;\nan amount of 0 means cached tokens are genuinely free." + "summary": "Get platform service by name (public, no auth)", + "operationId": "get_service_by_name", + "parameters": [ + { + "name": "service_name", + "in": "path", + "description": "Service name (e.g. web_search)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Service details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceResponse" + } } - ] - }, - "costPerImage": { - "$ref": "#/components/schemas/DecimalPrice" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "inputCostPerToken": { - "$ref": "#/components/schemas/DecimalPrice" - }, - "isActive": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/ModelMetadata" - }, - "modelId": { - "type": "string" + } }, - "outputCostPerToken": { - "$ref": "#/components/schemas/DecimalPrice" + "404": { + "description": "Service not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "updatedAt": { - "type": "string", - "format": "date-time" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } - }, - "AdminOrganizationMemberResponse": { - "type": "object", - "description": "Admin organization member response (for owners/admins)\nContains member info with full user details including sensitive data", - "required": [ - "id", - "organization_id", - "role", - "joined_at", - "user" - ], - "properties": { - "id": { - "type": "string" - }, - "invited_by": { - "type": [ - "string", - "null" - ] - }, - "joined_at": { - "type": "string", - "format": "date-time" - }, - "organization_id": { - "type": "string" + } + }, + "/v1/signature/{chat_id}": { + "get": { + "tags": [ + "Attestation" + ], + "summary": "Get completion signature", + "description": "Get cryptographic signature for a chat completion for verification.\nReturns signature data on success, or an unavailable response if the stream was disconnected.", + "operationId": "get_signature", + "parameters": [ + { + "name": "chat_id", + "in": "path", + "description": "Chat completion ID", + "required": true, + "schema": { + "type": "string" + } }, - "role": { - "$ref": "#/components/schemas/MemberRole" + { + "name": "model", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } }, - "user": { - "$ref": "#/components/schemas/AdminUserResponse" + { + "name": "signing_algo", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } } - } - }, - "AdminOrganizationResponse": { - "type": "object", - "description": "Organization details for admin organization listing", - "required": [ - "id", - "name", - "created_at" ], - "properties": { - "created_at": { - "type": "string", - "format": "date-time" - }, - "currentUsage": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/OrganizationUsage" + "responses": { + "200": { + "description": "Signature retrieved or unavailable due to disconnect", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignatureResponse" + } } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "id": { - "type": "string" + } }, - "name": { - "type": "string" + "400": { + "description": "Invalid parameters", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "spendLimit": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/SpendLimit" + "404": { + "description": "Signature not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } - ] + } } - } - }, - "AdminServiceListResponse": { - "type": "object", - "description": "Response for admin service list endpoint", - "required": [ - "services", - "limit", - "offset", - "total" + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v1/staking/farm/config": { + "get": { + "tags": [ + "Staking Farm" ], - "properties": { - "limit": { - "type": "integer", - "format": "int64" - }, - "offset": { - "type": "integer", - "format": "int64" + "summary": "Get staking farm configuration", + "description": "Returns the active House of Stake farm configuration used to convert\non-chain staking reward units into NEAR AI Cloud credits.", + "operationId": "get_staking_farm_config", + "responses": { + "200": { + "description": "Staking farm configuration", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StakingFarmConfigResponse" + } + } + } }, - "services": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AdminServiceResponse" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - "total": { - "type": "integer", - "format": "int64" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "AdminServiceResponse": { - "type": "object", - "description": "Platform service (admin CRUD) — single item", - "required": [ - "id", - "serviceName", - "displayName", - "unit", - "costPerUnit", - "isActive", - "createdAt", - "updatedAt" + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/users/me": { + "get": { + "tags": [ + "Users" ], - "properties": { - "costPerUnit": { - "type": "integer", - "format": "int64", - "description": "Price per unit in nano-USD (scale 9)." + "summary": "Get current user", + "description": "Returns the profile of the currently authenticated user, including their organizations and workspaces.", + "operationId": "get_current_user", + "responses": { + "200": { + "description": "Current user profile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponse" + } + } + } }, - "createdAt": { - "type": "string", - "format": "date-time" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "description": { - "type": [ - "string", - "null" - ] + "404": { + "description": "User not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "displayName": { - "type": "string" - }, - "id": { - "type": "string", - "format": "uuid" - }, - "isActive": { - "type": "boolean" - }, - "serviceName": { - "type": "string" - }, - "unit": { - "$ref": "#/components/schemas/ServiceUnit" - }, - "updatedAt": { - "type": "string", - "format": "date-time" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } + }, + "security": [ + { + "session_token": [] + } + ] }, - "AdminUserOrganizationDetails": { - "type": "object", - "description": "Organization details with spend limit and usage (for admin user listing)", - "required": [ - "id", - "name" + "patch": { + "tags": [ + "Users" ], - "properties": { - "currentUsage": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/OrganizationUsage" + "summary": "Update current user's profile", + "description": "Updates the profile information for the currently authenticated user.", + "operationId": "update_current_user_profile", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserProfileRequest" } - ] - }, - "description": { - "type": [ - "string", - "null" - ] + } }, - "id": { - "type": "string" + "required": true + }, + "responses": { + "200": { + "description": "Updated user profile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponse" + } + } + } }, - "name": { - "type": "string" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "spendLimit": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/SpendLimit" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } - ] + } } - } - }, - "AdminUserResponse": { - "type": "object", - "description": "Admin user response model (for owners/admins)\nContains sensitive information only visible to organization owners/admins", - "required": [ - "id", - "email", - "created_at", - "is_active", - "auth_provider", - "provider_user_id" + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/users/me/access-tokens": { + "post": { + "tags": [ + "Users" ], - "properties": { - "auth_provider": { - "type": "string" - }, - "avatar_url": { - "type": [ - "string", - "null" - ] - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "display_name": { - "type": [ - "string", - "null" - ] - }, - "email": { - "type": "string" - }, - "id": { - "type": "string" - }, - "is_active": { - "type": "boolean" - }, - "last_login_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" - }, - "organizations": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/components/schemas/AdminUserOrganizationDetails" + "summary": "Create a new access token", + "description": "Creates a new short-lived access token using the current refresh token.", + "operationId": "create_access_token", + "responses": { + "200": { + "description": "Access token created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessAndRefreshTokenResponse" + } + } } }, - "provider_user_id": { - "type": "string" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "username": { - "type": [ - "string", - "null" - ] + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "ApiKeyResponse": { - "type": "object", - "description": "API Key response model", - "required": [ - "id", - "key_prefix", - "workspace_id", - "created_by_user_id", - "created_at", - "is_active" + }, + "security": [ + { + "refresh_token": [] + } + ] + } + }, + "/v1/users/me/invitations": { + "get": { + "tags": [ + "Users" ], - "properties": { - "created_at": { - "type": "string", - "format": "date-time" - }, - "created_by_user_id": { - "type": "string" - }, - "deleted_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" - }, - "expires_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" - }, - "id": { - "type": "string" + "summary": "List pending invitations for the current user", + "description": "Returns all pending organization invitations for the authenticated user's email.", + "operationId": "list_user_invitations", + "responses": { + "200": { + "description": "List of pending invitations", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrganizationInvitationWithOrgResponse" + } + } + } + } }, - "is_active": { - "type": "boolean" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "key": { - "type": [ - "string", - "null" - ] + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/users/me/invitations/{invitation_id}/accept": { + "post": { + "tags": [ + "Users" + ], + "summary": "Accept an organization invitation", + "description": "Accepts a pending invitation and adds the user as a member of the organization.", + "operationId": "accept_invitation", + "parameters": [ + { + "name": "invitation_id", + "in": "path", + "description": "Invitation ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Invitation accepted successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AcceptInvitationResponse" + } + } + } }, - "key_prefix": { - "type": "string" + "400": { + "description": "Bad request - invitation expired or invalid", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "last_used_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "name": { - "type": [ - "string", - "null" - ] + "403": { + "description": "Forbidden - invitation belongs to another user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "spend_limit": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/DecimalPrice" + "404": { + "description": "Invitation not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } - ] + } }, - "usage": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/DecimalPrice" + "409": { + "description": "User is already a member", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } - ] + } }, - "workspace_id": { - "type": "string" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "AttestationResponse": { - "type": "object", - "required": [ - "gateway_attestation" + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/users/me/invitations/{invitation_id}/decline": { + "post": { + "tags": [ + "Users" ], - "properties": { - "gateway_attestation": { - "$ref": "#/components/schemas/DstackCpuQuote" + "summary": "Decline an organization invitation", + "description": "Declines a pending invitation to join an organization.", + "operationId": "decline_invitation", + "parameters": [ + { + "name": "invitation_id", + "in": "path", + "description": "Invitation ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Invitation declined successfully" }, - "model_attestations": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": {}, - "propertyNames": { - "type": "string" + "400": { + "description": "Bad request - invitation not pending", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "ohttp_attestation": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/OhttpAttestation", - "description": "OHTTP key attestation payload. Includes an Ed25519 signature over the decoded\n`key_config` bytes so clients can verify the HPKE key is bound to the TEE." + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } - ] + } }, - "ohttp_key_config": { - "type": [ - "string", - "null" - ], - "description": "Hex-encoded OHTTP key configuration (RFC 9458). Present only when OHTTP_ENABLED=true.\nLegacy flat field; mirrors `ohttp_attestation.key_config` when present." + "403": { + "description": "Forbidden - invitation belongs to another user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "tls_certificate": { - "type": [ - "string", - "null" - ], - "description": "TLS certificate file (PEM) from TLS_CERT_PATH; report_data binds via SHA256 of these exact bytes" - } - } - }, - "AudioTranscriptionRequestSchema": { - "type": "object", - "description": "Audio transcription request schema for OpenAPI documentation\nThis represents the multipart/form-data fields", - "required": [ - "file", - "model" - ], - "properties": { - "file": { - "type": "string", - "description": "Audio file (required) - binary audio data" - }, - "language": { - "type": [ - "string", - "null" - ], - "description": "Language code (optional) - e.g. \"en\", \"es\"" - }, - "model": { - "type": "string", - "description": "Model identifier (required) - e.g. \"openai/whisper-large-v3\"" - }, - "response_format": { - "type": [ - "string", - "null" - ], - "description": "Response format (optional) - one of: \"json\", \"text\", \"verbose_json\"" + "404": { + "description": "Invitation not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "timestamp_granularities": { - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - }, - "description": "Timestamp granularities (optional) - supports \"segment\" and \"word\" with verbose_json.\nMultipart requests may send repeated timestamp_granularities[] fields,\nrepeated timestamp_granularities fields, or comma-separated values." + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "AudioTranscriptionResponse": { - "type": "object", - "description": "Audio transcription response (with OpenAPI schema)", - "required": [ - "text" + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/users/me/refresh-tokens": { + "get": { + "tags": [ + "Users" ], - "properties": { - "duration": { - "type": [ - "number", - "null" - ], - "format": "double", - "description": "Total audio duration in seconds" - }, - "language": { - "type": [ - "string", - "null" - ], - "description": "Detected language code" - }, - "segments": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/components/schemas/TranscriptionSegment" - }, - "description": "Transcription segments with timing" + "summary": "Get user's refresh tokens", + "description": "Returns all active refresh tokens for the currently authenticated user.", + "operationId": "get_user_refresh_tokens", + "responses": { + "200": { + "description": "List of user refresh tokens", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RefreshTokenResponse" + } + } + } + } }, - "text": { - "type": "string", - "description": "Transcribed text" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "words": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/components/schemas/TranscriptionWord" - }, - "description": "Word-level timing information" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "BillingCostsRequest": { - "type": "object", - "description": "Billing costs request (HuggingFace compatible)", - "required": [ - "requestIds" + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/users/me/refresh-tokens/{refresh_token_id}": { + "delete": { + "tags": [ + "Users" ], - "properties": { - "requestIds": { - "type": "array", - "items": { + "summary": "Revoke a user refresh token", + "description": "Revokes a specific refresh token for the currently authenticated user.", + "operationId": "revoke_user_refresh_token", + "parameters": [ + { + "name": "refresh_token_id", + "in": "path", + "description": "Refresh token ID to revoke", + "required": true, + "schema": { "type": "string", "format": "uuid" - }, - "description": "Array of request IDs (inference IDs) to get costs for" - } - } - }, - "BillingCostsResponse": { - "type": "object", - "description": "Billing costs response (HuggingFace compatible)", - "required": [ - "requests" - ], - "properties": { - "requests": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RequestCost" - }, - "description": "Array of request costs" + } } - } - }, - "BillingSourceBreakdown": { - "type": "object", - "description": "Active credit limit by funding source (caps, not payments).", - "required": [ - "source", - "paid_credit_limit_usd", - "org_count" ], - "properties": { - "org_count": { - "type": "integer", - "format": "int64" + "responses": { + "204": { + "description": "Refresh token revoked successfully" }, - "paid_credit_limit_usd": { - "type": "number", - "format": "double", - "description": "Sum of active payment-type spend limits (caps) for this source, USD" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "source": { - "type": "string" + "404": { + "description": "Refresh token not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "BillingSummary": { - "type": "object", - "description": "Platform billing summary — credit LIMITS (caps) and consumption.\n\nThese are spend-limit ceilings from `organization_limits_history`, NOT payments\nor cash received. Real money-in (Stripe top-ups) lives in the billing service\n(nearai-cloud-ui), not in cloud-api.", - "required": [ - "generated_at", - "active_paid_credit_limit_usd", - "active_grant_credit_limit_usd", - "total_consumed_usd", - "inference_consumed_usd", - "service_consumed_usd", - "paying_org_count", - "granted_org_count", - "by_source" + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/users/me/status": { + "get": { + "tags": [ + "Users" ], - "properties": { - "active_grant_credit_limit_usd": { - "type": "number", - "format": "double", - "description": "Sum of active grant-type spend limits (caps), USD" - }, - "active_paid_credit_limit_usd": { - "type": "number", - "format": "double", - "description": "Sum of active payment-type spend limits (caps), USD" - }, - "by_source": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BillingSourceBreakdown" + "summary": "Get current user's account eligibility status", + "description": "Returns a minimal eligibility response for frontend preflight checks. AML/KYT\nprovider details are never exposed in this public response.", + "operationId": "get_user_status", + "responses": { + "200": { + "description": "Current account is eligible", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserStatusResponse" + } + } } }, - "generated_at": { - "type": "string", - "format": "date-time" - }, - "granted_org_count": { - "type": "integer", - "format": "int64" - }, - "inference_consumed_usd": { - "type": "number", - "format": "double", - "description": "All-time inference consumed cost, USD (organization_usage_log)" - }, - "paying_org_count": { - "type": "integer", - "format": "int64" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "service_consumed_usd": { - "type": "number", - "format": "double", - "description": "All-time service consumed cost, USD (organization_service_usage_log, e.g. web_search)" + "403": { + "description": "Current account is not eligible", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "total_consumed_usd": { - "type": "number", - "format": "double", - "description": "All-time consumed cost across all orgs, USD — **all usage** (from\norganization_balance: inference + services). `inference_consumed_usd +\nservice_consumed_usd` reconcile to this." + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "ChatChoice": { - "type": "object", - "required": [ - "index", - "message" + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/users/me/tokens": { + "delete": { + "tags": [ + "Users" ], - "properties": { - "finish_reason": { - "type": [ - "string", - "null" - ] + "summary": "Revoke all tokens for a user", + "description": "Revokes all tokens (both refresh tokens and access tokens) for the currently authenticated user.\nThis deletes all refresh tokens from the database and invalidates all JWT access tokens\nby updating the user's tokens_revoked_at timestamp.", + "operationId": "revoke_all_user_tokens", + "responses": { + "200": { + "description": "All tokens revoked successfully", + "content": { + "application/json": { + "schema": {} + } + } }, - "index": { - "type": "integer", - "format": "int64" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "message": { - "$ref": "#/components/schemas/Message" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "ChatCompletionRequest": { - "type": "object", - "required": [ - "model", - "messages" + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/workspaces/{workspace_id}": { + "get": { + "tags": [ + "Workspaces" ], - "properties": { - "frequency_penalty": { - "type": [ - "number", - "null" - ], - "format": "float" - }, - "max_tokens": { - "type": [ - "integer", - "null" - ], - "format": "int64" + "summary": "Get workspace by ID", + "description": "Returns workspace details for a specific workspace ID.", + "operationId": "get_workspace", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Workspace details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkspaceResponse" + } + } + } }, - "messages": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - "model": { - "type": "string" + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "n": { - "type": [ - "integer", - "null" - ], - "format": "int64" + "404": { + "description": "Workspace not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + }, + "put": { + "tags": [ + "Workspaces" + ], + "summary": "Update workspace", + "description": "Updates workspace details for a specific workspace ID.", + "operationId": "update_workspace", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWorkspaceRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Updated workspace", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkspaceResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Workspace not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + }, + "delete": { + "tags": [ + "Workspaces" + ], + "summary": "Delete workspace", + "description": "Deletes (deactivates) a workspace. Only the workspace creator or organization admin/owner can delete.", + "operationId": "delete_workspace", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Workspace deleted successfully" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Workspace not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/workspaces/{workspace_id}/api-keys": { + "get": { + "tags": [ + "Workspaces" + ], + "summary": "List API keys for workspace", + "description": "Returns a paginated list of all API keys for a workspace with usage information.", + "operationId": "list_workspace_api_keys", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of results (default: 20)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Number of results to skip (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "order_by", + "in": "query", + "description": "Field to order by: created_at or usage", + "required": false, + "schema": { + "$ref": "#/components/schemas/ApiKeyOrderBy" + } + }, + { + "name": "order_direction", + "in": "query", + "description": "Sort direction: asc or desc", + "required": false, + "schema": { + "$ref": "#/components/schemas/ApiKeyOrderDirection" + } + } + ], + "responses": { + "200": { + "description": "Paginated list of workspace API keys", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListApiKeysResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Workspace not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + }, + "post": { + "tags": [ + "Workspaces" + ], + "summary": "Create API key for workspace", + "description": "Creates a new API key for a workspace. Note: The key may take up to 10 seconds\nto become active for authentication due to internal caching.", + "operationId": "create_workspace_api_key", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "API key created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Workspace not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/workspaces/{workspace_id}/api-keys/{api_key_id}/usage/history": { + "get": { + "tags": [ + "Usage" + ], + "summary": "Get API key usage history", + "description": "Returns paginated usage history for a specific API key", + "operationId": "get_api_key_usage_history", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "api_key_id", + "in": "path", + "description": "API Key ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of records to return (default: 100)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Offset for pagination (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "Usage history", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageHistoryResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/workspaces/{workspace_id}/api-keys/{key_id}": { + "delete": { + "tags": [ + "Workspaces" + ], + "summary": "Revoke an API key", + "description": "Revokes a specific API key from a workspace.", + "operationId": "revoke_workspace_api_key", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "key_id", + "in": "path", + "description": "API Key ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "API key revoked successfully" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "API key not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + }, + "patch": { + "tags": [ + "Workspaces" + ], + "summary": "Update API key", + "description": "Updates an API key's name, expiration date, and/or spending limit. The user must be a member of the\norganization that owns the workspace. All fields are optional - only provided fields will be updated.", + "operationId": "update_workspace_api_key", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "key_id", + "in": "path", + "description": "API Key ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateApiKeyRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "API key updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden - not authorized to update this key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "API key or workspace not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict - API key with this name already exists", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/workspaces/{workspace_id}/api-keys/{key_id}/spend-limit": { + "patch": { + "tags": [ + "Workspaces" + ], + "summary": "Update API key spend limit", + "description": "Updates the spending limit for a specific API key. The user must be a member of the\norganization that owns the workspace. Set spend_limit to null to remove the limit.", + "operationId": "update_api_key_spend_limit", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "key_id", + "in": "path", + "description": "API Key ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateApiKeySpendLimitRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Spend limit updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden - not authorized to update this key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "API key or workspace not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + } + } + }, + "components": { + "schemas": { + "AcceptInvitationResponse": { + "type": "object", + "description": "Accept invitation response", + "required": [ + "organization_member", + "message" + ], + "properties": { + "message": { + "type": "string" + }, + "organization_member": { + "$ref": "#/components/schemas/OrganizationMemberResponse" + } + } + }, + "AccessAndRefreshTokenResponse": { + "type": "object", + "description": "Access token + refresh token response model", + "required": [ + "access_token", + "refresh_token", + "refresh_token_expiration" + ], + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + }, + "refresh_token_expiration": { + "type": "string", + "format": "date-time" + } + } + }, + "AddOrganizationMemberRequest": { + "type": "object", + "description": "Request to add an organization member", + "required": [ + "user_id", + "role" + ], + "properties": { + "role": { + "$ref": "#/components/schemas/MemberRole" + }, + "user_id": { + "type": "string" + } + } + }, + "AdminAccessTokenResponse": { + "type": "object", + "description": "Admin access token response model", + "required": [ + "id", + "access_token", + "created_by_user_id", + "created_at", + "expires_at", + "name", + "reason" + ], + "properties": { + "access_token": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "created_by_user_id": { + "type": "string" + }, + "expires_at": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "reason": { + "type": "string" + } + } + }, + "AdminAmlAllowlistEntryResponse": { + "type": "object", + "description": "Admin AML allowlist entry response.", + "required": [ + "account_id", + "address_type", + "created_at" + ], + "properties": { + "account_id": { + "type": "string" + }, + "address_type": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "created_by_user_id": { + "type": [ + "string", + "null" + ] + }, + "reason": { + "type": [ + "string", + "null" + ] + } + } + }, + "AdminAmlReportResponse": { + "type": "object", + "description": "Admin AML report audit entry.", + "required": [ + "id", + "flow", + "provider", + "account_id", + "address_type", + "risk_level", + "active", + "created_at", + "updated_at" + ], + "properties": { + "account_id": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "address_type": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "flow": { + "type": "string" + }, + "id": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "provider_report_time": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "reason": { + "type": [ + "string", + "null" + ] + }, + "report_id": { + "type": [ + "string", + "null" + ] + }, + "risk_level": { + "type": "string" + }, + "score": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "user_id": { + "type": [ + "string", + "null" + ] + } + } + }, + "AdminFeatureRequestListResponse": { + "type": "object", + "required": [ + "requests", + "limit", + "offset", + "total" + ], + "properties": { + "limit": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "requests": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminFeatureRequestSummaryResponse" + } + }, + "total": { + "type": "integer", + "format": "int64" + } + } + }, + "AdminFeatureRequestSummaryResponse": { + "type": "object", + "required": [ + "target", + "uniqueUserCount", + "uniqueOrganizationCount", + "latestRequestedAt", + "recentVotes" + ], + "properties": { + "latestRequestedAt": { + "type": "string", + "format": "date-time" + }, + "recentVotes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FeatureRequestVoteResponse" + } + }, + "target": { + "$ref": "#/components/schemas/FeatureRequestTargetResponse" + }, + "uniqueOrganizationCount": { + "type": "integer", + "format": "int64" + }, + "uniqueUserCount": { + "type": "integer", + "format": "int64" + } + } + }, + "AdminInvitationEmailDeliveryResponse": { + "type": "object", + "description": "Admin view of invitation email delivery metadata.", + "required": [ + "organization_id", + "organization_name", + "invitation_id", + "recipient_email", + "role", + "invitation_status", + "email_status", + "invited_by_user_id", + "created_at", + "expires_at" + ], + "properties": { + "created_at": { + "type": "string", + "format": "date-time" + }, + "email_last_error": { + "type": [ + "string", + "null" + ] + }, + "email_message_id": { + "type": [ + "string", + "null" + ] + }, + "email_sent_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "email_status": { + "$ref": "#/components/schemas/InvitationEmailStatus" + }, + "expires_at": { + "type": "string", + "format": "date-time" + }, + "invitation_id": { + "type": "string" + }, + "invitation_status": { + "$ref": "#/components/schemas/InvitationStatus" + }, + "invited_by_display_name": { + "type": [ + "string", + "null" + ] + }, + "invited_by_email": { + "type": [ + "string", + "null" + ] + }, + "invited_by_user_id": { + "type": "string" + }, + "organization_id": { + "type": "string" + }, + "organization_name": { + "type": "string" + }, + "recipient_email": { + "type": "string" + }, + "responded_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "role": { + "$ref": "#/components/schemas/MemberRole" + } + } + }, + "AdminInvitationEmailResendResultResponse": { + "type": "object", + "description": "Admin invitation email resend result.", + "required": [ + "invitation_id", + "recipient_email", + "success", + "email_sent", + "email_status" + ], + "properties": { + "email_last_error": { + "type": [ + "string", + "null" + ] + }, + "email_message_id": { + "type": [ + "string", + "null" + ] + }, + "email_sent": { + "type": "boolean" + }, + "email_sent_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "email_status": { + "$ref": "#/components/schemas/InvitationEmailStatus" + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "invitation_id": { + "type": "string" + }, + "recipient_email": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "AdminModelListResponse": { + "type": "object", + "description": "Response for admin model list endpoint", + "required": [ + "models", + "limit", + "offset", + "total" + ], + "properties": { + "limit": { + "type": "integer", + "format": "int64" + }, + "models": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminModelWithPricing" + } + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "total": { + "type": "integer", + "format": "int64" + } + } + }, + "AdminModelWithPricing": { + "type": "object", + "description": "Model with pricing information for admin listing", + "required": [ + "modelId", + "inputCostPerToken", + "outputCostPerToken", + "costPerImage", + "metadata", + "isActive", + "createdAt", + "updatedAt" + ], + "properties": { + "cacheReadCostPerToken": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/DecimalPrice", + "description": "Cost per cached input token. Omitted when cache pricing is disabled;\nan amount of 0 means cached tokens are genuinely free." + } + ] + }, + "costPerImage": { + "$ref": "#/components/schemas/DecimalPrice" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "inputCostPerToken": { + "$ref": "#/components/schemas/DecimalPrice" + }, + "isActive": { + "type": "boolean" + }, + "metadata": { + "$ref": "#/components/schemas/ModelMetadata" + }, + "modelId": { + "type": "string" + }, + "outputCostPerToken": { + "$ref": "#/components/schemas/DecimalPrice" + }, + "textPricing": {}, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "AdminOrganizationMemberResponse": { + "type": "object", + "description": "Admin organization member response (for owners/admins)\nContains member info with full user details including sensitive data", + "required": [ + "id", + "organization_id", + "role", + "joined_at", + "user" + ], + "properties": { + "id": { + "type": "string" + }, + "invited_by": { + "type": [ + "string", + "null" + ] + }, + "joined_at": { + "type": "string", + "format": "date-time" + }, + "organization_id": { + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/MemberRole" + }, + "user": { + "$ref": "#/components/schemas/AdminUserResponse" + } + } + }, + "AdminOrganizationResponse": { + "type": "object", + "description": "Organization details for admin organization listing", + "required": [ + "id", + "name", + "created_at" + ], + "properties": { + "created_at": { + "type": "string", + "format": "date-time" + }, + "currentUsage": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/OrganizationUsage" + } + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "spendLimit": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SpendLimit" + } + ] + } + } + }, + "AdminServiceListResponse": { + "type": "object", + "description": "Response for admin service list endpoint", + "required": [ + "services", + "limit", + "offset", + "total" + ], + "properties": { + "limit": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminServiceResponse" + } + }, + "total": { + "type": "integer", + "format": "int64" + } + } + }, + "AdminServiceResponse": { + "type": "object", + "description": "Platform service (admin CRUD) — single item", + "required": [ + "id", + "serviceName", + "displayName", + "unit", + "costPerUnit", + "isActive", + "createdAt", + "updatedAt" + ], + "properties": { + "costPerUnit": { + "type": "integer", + "format": "int64", + "description": "Price per unit in nano-USD (scale 9)." + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "displayName": { + "type": "string" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "isActive": { + "type": "boolean" + }, + "serviceName": { + "type": "string" + }, + "unit": { + "$ref": "#/components/schemas/ServiceUnit" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "AdminUserOrganizationDetails": { + "type": "object", + "description": "Organization details with spend limit and usage (for admin user listing)", + "required": [ + "id", + "name" + ], + "properties": { + "currentUsage": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/OrganizationUsage" + } + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "spendLimit": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SpendLimit" + } + ] + } + } + }, + "AdminUserResponse": { + "type": "object", + "description": "Admin user response model (for owners/admins)\nContains sensitive information only visible to organization owners/admins", + "required": [ + "id", + "email", + "created_at", + "is_active", + "auth_provider", + "provider_user_id" + ], + "properties": { + "auth_provider": { + "type": "string" + }, + "avatar_url": { + "type": [ + "string", + "null" + ] + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "display_name": { + "type": [ + "string", + "null" + ] + }, + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "last_login_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "organizations": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/AdminUserOrganizationDetails" + } + }, + "provider_user_id": { + "type": "string" + }, + "username": { + "type": [ + "string", + "null" + ] + } + } + }, + "ApiKeyResponse": { + "type": "object", + "description": "API Key response model", + "required": [ + "id", + "key_prefix", + "workspace_id", + "created_by_user_id", + "created_at", + "is_active" + ], + "properties": { + "created_at": { + "type": "string", + "format": "date-time" + }, + "created_by_user_id": { + "type": "string" + }, + "deleted_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "expires_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "id": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "key": { + "type": [ + "string", + "null" + ] + }, + "key_prefix": { + "type": "string" + }, + "last_used_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "spend_limit": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/DecimalPrice" + } + ] + }, + "usage": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/DecimalPrice" + } + ] + }, + "workspace_id": { + "type": "string" + } + } + }, + "AttestationResponse": { + "type": "object", + "required": [ + "gateway_attestation" + ], + "properties": { + "gateway_attestation": { + "$ref": "#/components/schemas/DstackCpuQuote" + }, + "model_attestations": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": {}, + "propertyNames": { + "type": "string" + } + } + }, + "ohttp_attestation": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/OhttpAttestation", + "description": "OHTTP key attestation payload. Includes an Ed25519 signature over the decoded\n`key_config` bytes so clients can verify the HPKE key is bound to the TEE." + } + ] + }, + "ohttp_key_config": { + "type": [ + "string", + "null" + ], + "description": "Hex-encoded OHTTP key configuration (RFC 9458). Present only when OHTTP_ENABLED=true.\nLegacy flat field; mirrors `ohttp_attestation.key_config` when present." + }, + "tls_certificate": { + "type": [ + "string", + "null" + ], + "description": "PEM-encoded TLS certificate read from `TLS_CERT_PATH` when `include_tls_fingerprint=true`.\n`report_data` binds `tls_cert_fingerprint` (the SHA-256 hash of the TLS SPKI) together with\n`signing_address`, not the PEM bytes." + } + } + }, + "AudioTranscriptionRequestSchema": { + "type": "object", + "description": "Audio transcription request schema for OpenAPI documentation\nThis represents the multipart/form-data fields", + "required": [ + "file", + "model" + ], + "properties": { + "file": { + "type": "string", + "description": "Audio file (required) - binary audio data" + }, + "language": { + "type": [ + "string", + "null" + ], + "description": "Language code (optional) - e.g. \"en\", \"es\"" + }, + "model": { + "type": "string", + "description": "Model identifier (required) - e.g. \"openai/whisper-large-v3\"" + }, + "response_format": { + "type": [ + "string", + "null" + ], + "description": "Response format (optional) - one of: \"json\", \"text\", \"verbose_json\"" + }, + "timestamp_granularities": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + }, + "description": "Timestamp granularities (optional) - supports \"segment\" and \"word\" with verbose_json.\nMultipart requests may send repeated timestamp_granularities[] fields,\nrepeated timestamp_granularities fields, or comma-separated values." + } + } + }, + "AudioTranscriptionResponse": { + "type": "object", + "description": "Audio transcription response (with OpenAPI schema)", + "required": [ + "text" + ], + "properties": { + "duration": { + "type": [ + "number", + "null" + ], + "format": "double", + "description": "Total audio duration in seconds" + }, + "language": { + "type": [ + "string", + "null" + ], + "description": "Detected language code" + }, + "segments": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/TranscriptionSegment" + }, + "description": "Transcription segments with timing" + }, + "text": { + "type": "string", + "description": "Transcribed text" + }, + "words": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/TranscriptionWord" + }, + "description": "Word-level timing information" + } + } + }, + "BillingCostsRequest": { + "type": "object", + "description": "Billing costs request (HuggingFace compatible)", + "required": [ + "requestIds" + ], + "properties": { + "requestIds": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "Array of request IDs to get costs for: the UUID values of the\n`inference-id` response header returned by /v1/chat/completions and\n/v1/messages (not the `x-request-id` header)." + } + } + }, + "BillingCostsResponse": { + "type": "object", + "description": "Billing costs response (HuggingFace compatible)", + "required": [ + "requests" + ], + "properties": { + "requests": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RequestCost" + }, + "description": "Array of request costs" + }, + "warning": { + "type": [ + "string", + "null" + ], + "description": "Present when some request IDs matched no usage record for this\norganization (those entries report costNanoUsd 0). Explains where the\ncorrect request IDs come from." + } + } + }, + "BillingSourceBreakdown": { + "type": "object", + "description": "Active credit limit by funding source (caps, not payments).", + "required": [ + "source", + "paid_credit_limit_usd", + "org_count" + ], + "properties": { + "org_count": { + "type": "integer", + "format": "int64" + }, + "paid_credit_limit_usd": { + "type": "number", + "format": "double", + "description": "Sum of active payment-type spend limits (caps) for this source, USD" + }, + "source": { + "type": "string" + } + } + }, + "BillingSummary": { + "type": "object", + "description": "Platform billing summary — credit LIMITS (caps) and consumption.\n\nThese are spend-limit ceilings from `organization_limits_history`, NOT payments\nor cash received. Real money-in (Stripe top-ups) lives in the billing service\n(nearai-cloud-ui), not in cloud-api.", + "required": [ + "generated_at", + "active_paid_credit_limit_usd", + "active_grant_credit_limit_usd", + "total_consumed_usd", + "inference_consumed_usd", + "service_consumed_usd", + "paying_org_count", + "granted_org_count", + "by_source" + ], + "properties": { + "active_grant_credit_limit_usd": { + "type": "number", + "format": "double", + "description": "Sum of active grant-type spend limits (caps), USD" + }, + "active_paid_credit_limit_usd": { + "type": "number", + "format": "double", + "description": "Sum of active payment-type spend limits (caps), USD" + }, + "by_source": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BillingSourceBreakdown" + } + }, + "generated_at": { + "type": "string", + "format": "date-time" + }, + "granted_org_count": { + "type": "integer", + "format": "int64" + }, + "inference_consumed_usd": { + "type": "number", + "format": "double", + "description": "All-time inference consumed cost, USD (organization_usage_log)" + }, + "paying_org_count": { + "type": "integer", + "format": "int64" + }, + "service_consumed_usd": { + "type": "number", + "format": "double", + "description": "All-time service consumed cost, USD (organization_service_usage_log, e.g. web_search)" + }, + "total_consumed_usd": { + "type": "number", + "format": "double", + "description": "All-time consumed cost across all orgs, USD — **all usage** (from\norganization_balance: inference + services). `inference_consumed_usd +\nservice_consumed_usd` reconcile to this." + } + } + }, + "ChatChoice": { + "type": "object", + "required": [ + "index", + "message" + ], + "properties": { + "finish_reason": { + "type": [ + "string", + "null" + ] + }, + "index": { + "type": "integer", + "format": "int64" + }, + "message": { + "$ref": "#/components/schemas/Message" + } + } + }, + "ChatCompletionRequest": { + "type": "object", + "required": [ + "model", + "messages" + ], + "properties": { + "frequency_penalty": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "max_tokens": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + } + }, + "model": { + "type": "string" + }, + "n": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "presence_penalty": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "service_tier": { + "type": [ + "string", + "null" + ] + }, + "stop": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/StopSequences", + "description": "OpenAI `stop` accepts either a single string or an array of strings." + } + ] + }, + "stream": { + "type": [ + "boolean", + "null" + ] + }, + "temperature": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "top_p": { + "type": [ + "number", + "null" + ], + "format": "float" + } + }, + "additionalProperties": {} + }, + "ChatCompletionResponse": { + "type": "object", + "required": [ + "id", + "object", + "created", + "model", + "choices", + "usage" + ], + "properties": { + "choices": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatChoice" + } + }, + "created": { + "type": "integer", + "format": "int64" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "object": { + "type": "string" + }, + "usage": { + "$ref": "#/components/schemas/CompletionUsage" + } + }, + "additionalProperties": { + "description": "Unknown top-level fields from the provider (e.g. system_fingerprint, prompt_logprobs).\nRe-emitted when serializing so we do not drop them." + } + }, + "CheckApiKeyResponse": { + "type": "object", + "description": "Response from the check_api_key endpoint.\n\n`organization_id`, `workspace_id`, and `api_key_id` are returned so\ndownstream gateways (e.g. inference-proxy) have a server-side\nauthoritative subject identity and don't have to trust caller-supplied\nheaders when populating logs / usage records / billing reports. In\nparticular, this lets a trusted gateway report usage to cloud-api with\na shared service token instead of forwarding the user's `sk-…`.", + "required": [ + "valid", + "organization_id", + "workspace_id", + "api_key_id" + ], + "properties": { + "api_key_id": { + "type": "string", + "description": "UUID of the API key itself. Used by trusted gateways to attribute\nper-key usage rows without holding the raw `sk-…` value." + }, + "organization_id": { + "type": "string", + "description": "Organization the API key belongs to" + }, + "valid": { + "type": "boolean", + "description": "Whether the API key is valid and authorized" + }, + "workspace_id": { + "type": "string", + "description": "Workspace the API key belongs to" + } + } + }, + "CompletionChoice": { + "type": "object", + "required": [ + "index", + "text" + ], + "properties": { + "finish_reason": { + "type": [ + "string", + "null" + ] + }, + "index": { + "type": "integer", + "format": "int64" + }, + "logprobs": {}, + "text": { + "type": "string" + } + } + }, + "CompletionPrompt": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + ], + "description": "OpenAI `/v1/completions` `prompt`: a single string, a batch of strings, or\ntoken-ID array(s). This endpoint serves only the single-string form; the\nother shapes still deserialize (so the handler can return a clean 400 rather\nthan a framework deserialization error) and are rejected there." + }, + "CompletionRequest": { + "type": "object", + "required": [ + "model", + "prompt" + ], + "properties": { + "best_of": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "echo": { + "type": [ + "boolean", + "null" + ] + }, + "frequency_penalty": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "logprobs": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "max_tokens": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "model": { + "type": "string" + }, + "n": { + "type": [ + "integer", + "null" + ], + "format": "int64" }, "presence_penalty": { "type": [ "number", "null" ], - "format": "float" + "format": "float" + }, + "prompt": { + "$ref": "#/components/schemas/CompletionPrompt" + }, + "stop": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/StopSequences" + } + ] + }, + "stream": { + "type": [ + "boolean", + "null" + ] + }, + "temperature": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "top_p": { + "type": [ + "number", + "null" + ], + "format": "float" + } + }, + "additionalProperties": {} + }, + "CompletionResponse": { + "type": "object", + "required": [ + "id", + "object", + "created", + "model", + "choices", + "usage" + ], + "properties": { + "choices": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CompletionChoice" + } + }, + "created": { + "type": "integer", + "format": "int64" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "object": { + "type": "string" + }, + "usage": { + "$ref": "#/components/schemas/CompletionUsage" + } + } + }, + "CompletionUsage": { + "type": "object", + "description": "Usage for chat/completions endpoints.\nSerializes as prompt_tokens, completion_tokens, prompt_tokens_details, completion_tokens_details, total_tokens.", + "required": [ + "prompt_tokens", + "completion_tokens", + "total_tokens" + ], + "properties": { + "completion_tokens": { + "type": "integer", + "format": "int32" + }, + "completion_tokens_details": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/OutputTokensDetails" + } + ] + }, + "prompt_tokens": { + "type": "integer", + "format": "int32" + }, + "prompt_tokens_details": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/InputTokensDetails" + } + ] + }, + "total_tokens": { + "type": "integer", + "format": "int32" + } + } + }, + "ConversationContent": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConversationContentPart" + } + } + ], + "description": "Content for conversation items" + }, + "ConversationContentPart": { + "oneOf": [ + { + "type": "object", + "required": [ + "text", + "type" + ], + "properties": { + "text": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "input_text" + ] + } + } + }, + { + "type": "object", + "required": [ + "image_url", + "type" + ], + "properties": { + "detail": { + "type": [ + "string", + "null" + ] + }, + "image_url": { + "$ref": "#/components/schemas/ResponseImageUrl" + }, + "type": { + "type": "string", + "enum": [ + "input_image" + ] + } + } + }, + { + "type": "object", + "required": [ + "file_id", + "type" + ], + "properties": { + "detail": { + "type": [ + "string", + "null" + ] + }, + "file_id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "input_file" + ] + } + } + }, + { + "type": "object", + "required": [ + "text", + "type" + ], + "properties": { + "annotations": { + "type": [ + "array", + "null" + ], + "items": {} + }, + "text": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "output_text" + ] + } + } + } + ], + "description": "Content part for conversations" + }, + "ConversationDeleteResult": { + "type": "object", + "description": "Deleted conversation result", + "required": [ + "id", + "object", + "deleted" + ], + "properties": { + "deleted": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "object": { + "type": "string" + } + } + }, + "ConversationInputItem": { + "oneOf": [ + { + "type": "object", + "required": [ + "role", + "content", + "type" + ], + "properties": { + "content": { + "$ref": "#/components/schemas/ConversationContent" + }, + "metadata": {}, + "model": { + "type": [ + "string", + "null" + ] + }, + "role": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "message" + ] + } + } + } + ], + "description": "Input item for conversations" + }, + "ConversationItem": { + "oneOf": [ + { + "type": "object", + "required": [ + "id", + "response_id", + "created_at", + "status", + "role", + "content", + "model", + "type" + ], + "properties": { + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConversationContentPart" + } + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "id": { + "type": "string" + }, + "metadata": {}, + "model": { + "type": "string" + }, + "next_response_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "response_id": { + "type": "string" + }, + "role": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/ResponseItemStatus" + }, + "type": { + "type": "string", + "enum": [ + "message" + ] + } + } + }, + { + "type": "object", + "required": [ + "id", + "response_id", + "created_at", + "status", + "tool_type", + "function", + "model", + "type" + ], + "properties": { + "created_at": { + "type": "integer", + "format": "int64" + }, + "function": { + "$ref": "#/components/schemas/ConversationItemFunction" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "next_response_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "response_id": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/ResponseItemStatus" + }, + "tool_type": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "tool_call" + ] + } + } + }, + { + "type": "object", + "required": [ + "id", + "response_id", + "created_at", + "status", + "action", + "model", + "type" + ], + "properties": { + "action": { + "$ref": "#/components/schemas/ConversationItemWebSearchAction" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "next_response_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "response_id": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/ResponseItemStatus" + }, + "type": { + "type": "string", + "enum": [ + "web_search_call" + ] + } + } + }, + { + "type": "object", + "required": [ + "id", + "response_id", + "created_at", + "status", + "summary", + "content", + "model", + "type" + ], + "properties": { + "content": { + "type": "string" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "next_response_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "response_id": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/ResponseItemStatus" + }, + "summary": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "reasoning" + ] + } + } + }, + { + "type": "object", + "required": [ + "id", + "server_label", + "tools", + "type" + ], + "properties": { + "error": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "server_label": { + "type": "string" + }, + "tools": { + "type": "array", + "items": { + "$ref": "#/components/schemas/McpDiscoveredTool" + } + }, + "type": { + "type": "string", + "enum": [ + "mcp_list_tools" + ] + } + } + }, + { + "type": "object", + "required": [ + "id", + "response_id", + "created_at", + "server_label", + "name", + "arguments", + "model", + "type" + ], + "properties": { + "approval_request_id": { + "type": [ + "string", + "null" + ] + }, + "arguments": { + "type": "string" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "name": { + "type": "string" + }, + "next_response_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "output": { + "type": [ + "string", + "null" + ] + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "response_id": { + "type": "string" + }, + "server_label": { + "type": "string" + }, + "status": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "mcp_call" + ] + } + } }, - "stop": { - "oneOf": [ - { - "type": "null" + { + "type": "object", + "required": [ + "id", + "response_id", + "created_at", + "server_label", + "name", + "arguments", + "model", + "type" + ], + "properties": { + "arguments": { + "type": "string" }, - { - "$ref": "#/components/schemas/StopSequences", - "description": "OpenAI `stop` accepts either a single string or an array of strings." + "created_at": { + "type": "integer", + "format": "int64" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "name": { + "type": "string" + }, + "next_response_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "response_id": { + "type": "string" + }, + "server_label": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "mcp_approval_request" + ] + } + } + }, + { + "type": "object", + "description": "Function call requiring client execution", + "required": [ + "id", + "response_id", + "created_at", + "call_id", + "name", + "arguments", + "status", + "model", + "type" + ], + "properties": { + "arguments": { + "type": "string", + "description": "JSON-encoded arguments" + }, + "call_id": { + "type": "string", + "description": "The LLM's tool_call_id for correlation" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "name": { + "type": "string", + "description": "Function name" + }, + "next_response_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "response_id": { + "type": "string" + }, + "status": { + "type": "string", + "description": "Status: \"in_progress\" when pending client execution" + }, + "type": { + "type": "string", + "enum": [ + "function_call" + ] + } + } + }, + { + "type": "object", + "description": "Result of a client-executed function call", + "required": [ + "id", + "response_id", + "created_at", + "call_id", + "output", + "type" + ], + "properties": { + "call_id": { + "type": "string", + "description": "The call_id from the FunctionCall this is a response to" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "id": { + "type": "string" + }, + "next_response_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "output": { + "type": "string", + "description": "Result of the function execution" + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "response_id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "function_call_output" + ] } - ] - }, - "stream": { - "type": [ - "boolean", - "null" - ] - }, - "temperature": { - "type": [ - "number", - "null" - ], - "format": "float" - }, - "top_p": { - "type": [ - "number", - "null" - ], - "format": "float" + } } - }, - "additionalProperties": {} + ], + "description": "Conversation item (for responses)" }, - "ChatCompletionResponse": { + "ConversationItemFunction": { "type": "object", "required": [ - "id", - "object", - "created", - "model", - "choices", - "usage" + "name", + "arguments" ], "properties": { - "choices": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ChatChoice" - } - }, - "created": { - "type": "integer", - "format": "int64" - }, - "id": { - "type": "string" - }, - "model": { + "arguments": { "type": "string" }, - "object": { + "name": { "type": "string" - }, - "usage": { - "$ref": "#/components/schemas/CompletionUsage" } - }, - "additionalProperties": { - "description": "Unknown top-level fields from the provider (e.g. system_fingerprint, prompt_logprobs).\nRe-emitted when serializing so we do not drop them." } }, - "CheckApiKeyResponse": { + "ConversationItemList": { "type": "object", - "description": "Response from the check_api_key endpoint.\n\n`organization_id`, `workspace_id`, and `api_key_id` are returned so\ndownstream gateways (e.g. inference-proxy) have a server-side\nauthoritative subject identity and don't have to trust caller-supplied\nheaders when populating logs / usage records / billing reports. In\nparticular, this lets a trusted gateway report usage to cloud-api with\na shared service token instead of forwarding the user's `sk-…`.", + "description": "List of conversation items", "required": [ - "valid", - "organization_id", - "workspace_id", - "api_key_id" + "object", + "data", + "first_id", + "last_id", + "has_more" ], "properties": { - "api_key_id": { - "type": "string", - "description": "UUID of the API key itself. Used by trusted gateways to attribute\nper-key usage rows without holding the raw `sk-…` value." - }, - "organization_id": { - "type": "string", - "description": "Organization the API key belongs to" + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConversationItem" + } }, - "valid": { - "type": "boolean", - "description": "Whether the API key is valid and authorized" + "first_id": { + "type": "string" }, - "workspace_id": { - "type": "string", - "description": "Workspace the API key belongs to" - } - } - }, - "CompletionChoice": { - "type": "object", - "required": [ - "index", - "text" - ], - "properties": { - "finish_reason": { - "type": [ - "string", - "null" - ] + "has_more": { + "type": "boolean" }, - "index": { - "type": "integer", - "format": "int64" + "last_id": { + "type": "string" }, - "logprobs": {}, - "text": { + "object": { "type": "string" } } }, - "CompletionPrompt": { + "ConversationItemWebSearchAction": { "oneOf": [ { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "array", - "items": { - "type": "integer", - "format": "int64" - } - }, - { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "integer", - "format": "int64" - } - } - } - ], - "description": "OpenAI `/v1/completions` `prompt`: a single string, a batch of strings, or\ntoken-ID array(s). This endpoint serves only the single-string form; the\nother shapes still deserialize (so the handler can return a clean 400 rather\nthan a framework deserialization error) and are rejected there." - }, - "CompletionRequest": { - "type": "object", - "required": [ - "model", - "prompt" - ], - "properties": { - "best_of": { - "type": [ - "integer", - "null" - ], - "format": "int64" - }, - "echo": { - "type": [ - "boolean", - "null" - ] - }, - "frequency_penalty": { - "type": [ - "number", - "null" - ], - "format": "float" - }, - "logprobs": { - "type": [ - "integer", - "null" - ], - "format": "int64" - }, - "max_tokens": { - "type": [ - "integer", - "null" - ], - "format": "int64" - }, - "model": { - "type": "string" - }, - "n": { - "type": [ - "integer", - "null" - ], - "format": "int64" - }, - "presence_penalty": { - "type": [ - "number", - "null" + "type": "object", + "required": [ + "query", + "type" ], - "format": "float" - }, - "prompt": { - "$ref": "#/components/schemas/CompletionPrompt" - }, - "stop": { - "oneOf": [ - { - "type": "null" + "properties": { + "query": { + "type": "string" }, - { - "$ref": "#/components/schemas/StopSequences" + "type": { + "type": "string", + "enum": [ + "search" + ] } - ] - }, - "stream": { - "type": [ - "boolean", - "null" - ] - }, - "temperature": { - "type": [ - "number", - "null" - ], - "format": "float" - }, - "top_p": { - "type": [ - "number", - "null" - ], - "format": "float" + } } - }, - "additionalProperties": {} + ] }, - "CompletionResponse": { + "ConversationObject": { "type": "object", + "description": "Conversation object (follows OpenAI spec)", "required": [ "id", "object", - "created", - "model", - "choices", - "usage" + "created_at", + "metadata" ], - "properties": { - "choices": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CompletionChoice" - } - }, - "created": { + "properties": { + "created_at": { "type": "integer", "format": "int64" }, "id": { "type": "string" }, - "model": { - "type": "string" - }, + "metadata": {}, "object": { "type": "string" - }, - "usage": { - "$ref": "#/components/schemas/CompletionUsage" } } }, - "CompletionUsage": { - "type": "object", - "description": "Usage for chat/completions endpoints.\nSerializes as prompt_tokens, completion_tokens, prompt_tokens_details, completion_tokens_details, total_tokens.", - "required": [ - "prompt_tokens", - "completion_tokens", - "total_tokens" - ], - "properties": { - "completion_tokens": { - "type": "integer", - "format": "int32" - }, - "completion_tokens_details": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/OutputTokensDetails" - } - ] - }, - "prompt_tokens": { - "type": "integer", - "format": "int32" + "ConversationReference": { + "oneOf": [ + { + "type": "string" }, - "prompt_tokens_details": { - "oneOf": [ - { - "type": "null" + { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" }, - { - "$ref": "#/components/schemas/InputTokensDetails" - } - ] - }, - "total_tokens": { - "type": "integer", - "format": "int32" + "metadata": {} + } } - } + ], + "description": "Conversation reference" }, "ConversationResponseReference": { "type": "object", @@ -10534,6 +13489,28 @@ } } }, + "CreateConversationItemsRequest": { + "type": "object", + "description": "Request to create items in a conversation", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConversationInputItem" + } + } + } + }, + "CreateConversationRequest": { + "type": "object", + "description": "Request to create a conversation", + "properties": { + "metadata": {} + } + }, "CreateOrganizationRequest": { "type": "object", "description": "Request to create a new organization", @@ -10547,90 +13524,244 @@ "null" ] }, - "name": { - "type": "string" - } - }, - "additionalProperties": false - }, - "CreateReportingTokenRequest": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "expires_at": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "CreateReportingTokenRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "expires_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Optional expiration time. Must be in the future." + }, + "name": { + "type": "string", + "description": "Human-readable name for the reporting token." + } + }, + "additionalProperties": false + }, + "CreateReportingTokenResponse": { + "type": "object", + "required": [ + "id", + "organization_id", + "name", + "token", + "token_prefix", + "created_by_user_id", + "created_at", + "scope" + ], + "properties": { + "created_at": { + "type": "string", + "format": "date-time" + }, + "created_by_user_id": { + "type": "string", + "format": "uuid" + }, + "expires_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "last_used_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Approximate last-authentication timestamp, refreshed at most once every 15 minutes." + }, + "name": { + "type": "string" + }, + "organization_id": { + "type": "string", + "format": "uuid" + }, + "scope": { + "$ref": "#/components/schemas/ReportingTokenScope" + }, + "token": { + "type": "string", + "description": "Raw reporting token. Returned only once when the token is created." + }, + "token_prefix": { + "type": "string", + "description": "Non-secret token prefix for display and audit correlation." + } + } + }, + "CreateResponseRequest": { + "type": "object", + "description": "Request to create a response", + "required": [ + "model" + ], + "properties": { + "background": { + "type": [ + "boolean", + "null" + ] + }, + "conversation": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ConversationReference" + } + ] + }, + "include": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "input": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ResponseInput" + } + ] + }, + "instructions": { + "type": [ + "string", + "null" + ] + }, + "max_output_tokens": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "max_tool_calls": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "metadata": {}, + "model": { + "type": "string" + }, + "parallel_tool_calls": { + "type": [ + "boolean", + "null" + ] + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "prompt_cache_key": { + "type": [ + "string", + "null" + ] + }, + "reasoning": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ResponseReasoningConfig" + } + ] + }, + "safety_identifier": { + "type": [ + "string", + "null" + ] + }, + "service_tier": { "type": [ "string", "null" ], - "format": "date-time", - "description": "Optional expiration time. Must be in the future." + "description": "Responses is Standard-only for now. `auto` is accepted but normalized\nto Standard before the internal Chat Completions call." }, - "name": { - "type": "string", - "description": "Human-readable name for the reporting token." - } - }, - "additionalProperties": false - }, - "CreateReportingTokenResponse": { - "type": "object", - "required": [ - "id", - "organization_id", - "name", - "token", - "token_prefix", - "created_by_user_id", - "created_at", - "scope" - ], - "properties": { - "created_at": { - "type": "string", - "format": "date-time" + "store": { + "type": [ + "boolean", + "null" + ] }, - "created_by_user_id": { - "type": "string", - "format": "uuid" + "stream": { + "type": [ + "boolean", + "null" + ] }, - "expires_at": { + "temperature": { "type": [ - "string", + "number", "null" ], - "format": "date-time" + "format": "float" }, - "id": { - "type": "string", - "format": "uuid" + "tool_choice": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ResponseToolChoice" + } + ] }, - "last_used_at": { + "tools": { "type": [ - "string", + "array", "null" ], - "format": "date-time", - "description": "Approximate last-authentication timestamp, refreshed at most once every 15 minutes." - }, - "name": { - "type": "string" - }, - "organization_id": { - "type": "string", - "format": "uuid" - }, - "scope": { - "$ref": "#/components/schemas/ReportingTokenScope" - }, - "token": { - "type": "string", - "description": "Raw reporting token. Returned only once when the token is created." + "items": { + "$ref": "#/components/schemas/ResponseTool" + } }, - "token_prefix": { - "type": "string", - "description": "Non-secret token prefix for display and audit correlation." + "top_p": { + "type": [ + "number", + "null" + ], + "format": "float" } } }, @@ -10873,7 +14004,7 @@ }, "report_data": { "type": "string", - "description": "The report data that contains signing address and nonce" + "description": "Value bound into the quote. Its final 32 bytes contain the request nonce.\nWith `include_tls_fingerprint=true`, its first 32 bytes are\nSHA-256(signing_address || tls_cert_fingerprint)." }, "request_nonce": { "type": "string", @@ -10957,6 +14088,23 @@ } } }, + "ExpiresAfter": { + "type": "object", + "description": "Expires after configuration", + "required": [ + "anchor", + "seconds" + ], + "properties": { + "anchor": { + "type": "string" + }, + "seconds": { + "type": "integer", + "format": "int64" + } + } + }, "FeatureRequestKind": { "type": "string", "enum": [ @@ -11054,6 +14202,102 @@ } } }, + "FileDeleteResponse": { + "type": "object", + "description": "File delete response", + "required": [ + "id", + "object", + "deleted" + ], + "properties": { + "deleted": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "object": { + "type": "string" + } + } + }, + "FileListResponse": { + "type": "object", + "description": "File list response", + "required": [ + "object", + "data", + "has_more" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileUploadResponse" + } + }, + "first_id": { + "type": [ + "string", + "null" + ] + }, + "has_more": { + "type": "boolean" + }, + "last_id": { + "type": [ + "string", + "null" + ] + }, + "object": { + "type": "string" + } + } + }, + "FileUploadResponse": { + "type": "object", + "description": "File upload response", + "required": [ + "id", + "object", + "bytes", + "created_at", + "filename", + "purpose" + ], + "properties": { + "bytes": { + "type": "integer", + "format": "int64" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "expires_at": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "filename": { + "type": "string" + }, + "id": { + "type": "string" + }, + "object": { + "type": "string" + }, + "purpose": { + "type": "string" + } + } + }, "FunctionCall": { "type": "object", "required": [ @@ -11327,6 +14571,13 @@ }, "description": "Sampling parameters accepted by the model (OpenRouter vocabulary)." }, + "textPricing": { + "type": [ + "object", + "null" + ], + "description": "Exact, versioned USD-per-million text pricing profile.\nOmitted leaves it unchanged; null removes it." + }, "verifiable": { "type": [ "boolean", @@ -11533,6 +14784,11 @@ "cost_per_host_usd_month", "monthly_burn_usd", "daily_burn_usd", + "cost_per_gpu_hour_usd", + "total_allocated_gpus", + "hourly_gpu_burn_usd", + "model_gpu_allocations", + "gpu_data_stale", "fetched_at", "stale" ], @@ -11542,6 +14798,11 @@ "format": "int64", "description": "Hosts serving ≥1 model." }, + "cost_per_gpu_hour_usd": { + "type": "number", + "format": "double", + "description": "Planning rate for one allocated physical GPU-hour." + }, "cost_per_host_usd_month": { "type": "number", "format": "double" @@ -11554,11 +14815,27 @@ "type": "string", "format": "date-time" }, + "gpu_data_stale": { + "type": "boolean", + "description": "True when current GPU allocation is unavailable or served from cache." + }, + "hourly_gpu_burn_usd": { + "type": "number", + "format": "double", + "description": "Current projected fleet GPU burn per hour." + }, "idle_hosts": { "type": "integer", "format": "int64", "description": "Hosts serving no models (idle capacity we still pay for)." }, + "model_gpu_allocations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ModelGpuAllocation" + }, + "description": "Current allocation grouped by the model label emitted by DCGM. A GPU\ncarrying multiple labels can appear in multiple groups; the fleet total\nremains separately deduplicated by physical UUID." + }, "monthly_burn_usd": { "type": "number", "format": "double" @@ -11567,6 +14844,11 @@ "type": "boolean", "description": "True when this is last-known / fallback data because the live fetch failed." }, + "total_allocated_gpus": { + "type": "integer", + "format": "int64", + "description": "Current distinct physical GPUs allocated to a model workload." + }, "total_hosts": { "type": "integer", "format": "int64" @@ -11817,17 +15099,77 @@ "$ref": "#/components/schemas/ItaModelTokenItem" } }, - "nonce": { - "type": "string" + "nonce": { + "type": "string" + }, + "policy_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "policy_must_match": { + "type": "boolean" + } + } + }, + "ListAdminAmlAllowlistResponse": { + "type": "object", + "description": "Admin AML allowlist response.", + "required": [ + "accounts", + "total", + "limit", + "offset" + ], + "properties": { + "accounts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminAmlAllowlistEntryResponse" + } + }, + "limit": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "total": { + "type": "integer", + "format": "int64" + } + } + }, + "ListAdminAmlReportsResponse": { + "type": "object", + "description": "Paginated admin AML reports response.", + "required": [ + "reports", + "total", + "limit", + "offset" + ], + "properties": { + "limit": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" }, - "policy_ids": { + "reports": { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/AdminAmlReportResponse" } }, - "policy_must_match": { - "type": "boolean" + "total": { + "type": "integer", + "format": "int64" } } }, @@ -12084,8 +15426,7 @@ "enum": [ "always", "never" - ], - "description": "Simple MCP approval mode" + ] }, "McpApprovalRequirement": { "oneOf": [ @@ -12103,8 +15444,14 @@ } } } - ], - "description": "Approval requirement for MCP tool calls" + ] + }, + "McpApprovalResponseType": { + "type": "string", + "description": "Type marker for MCP approval response input", + "enum": [ + "mcp_approval_response" + ] }, "McpDiscoveredTool": { "type": "object", @@ -12125,6 +15472,13 @@ } } }, + "McpListToolsType": { + "type": "string", + "description": "Type marker for MCP list tools input", + "enum": [ + "mcp_list_tools" + ] + }, "McpToolNameFilter": { "type": "object", "required": [ @@ -12138,8 +15492,7 @@ }, "uniqueItems": true } - }, - "description": "Filter for tool names that don't require approval" + } }, "MemberRole": { "type": "string", @@ -12520,6 +15873,22 @@ } } }, + "ModelGpuAllocation": { + "type": "object", + "required": [ + "model_name", + "allocated_gpus" + ], + "properties": { + "allocated_gpus": { + "type": "integer", + "format": "int64" + }, + "model_name": { + "type": "string" + } + } + }, "ModelHistoryEntry": { "type": "object", "description": "Model history entry - includes pricing, context length, and other model attributes", @@ -12713,6 +16082,7 @@ "type": "string" } }, + "textPricing": {}, "verifiable": { "type": "boolean" } @@ -12911,6 +16281,9 @@ }, "description": "Sampling parameters accepted by the model (OpenRouter `supported_sampling_parameters`)." }, + "textPricing": { + "description": "Exact, versioned USD-per-million text pricing. The legacy `pricing`\nprojection remains Standard-short for compatibility." + }, "top_provider": { "oneOf": [ { @@ -13330,7 +16703,8 @@ }, "outputCostPerToken": { "$ref": "#/components/schemas/DecimalPrice" - } + }, + "textPricing": {} } }, "ModelsResponse": { @@ -13682,6 +17056,23 @@ } } }, + "OrganizationFallbackResponse": { + "type": "object", + "description": "Effective fallback policy returned by admin organization endpoints.", + "required": [ + "organizationId", + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "organizationId": { + "type": "string", + "format": "uuid" + } + } + }, "OrganizationInvitationResponse": { "type": "object", "description": "Organization invitation response", @@ -14277,6 +17668,12 @@ "$ref": "#/components/schemas/DecimalPriceRequest" } ] + }, + "textPricing": { + "type": [ + "object", + "null" + ] } } }, @@ -14390,7 +17787,8 @@ "$ref": "#/components/schemas/DecimalPrice" } ] - } + }, + "textPricing": {} } }, "PricingFields": { @@ -14420,7 +17818,8 @@ }, "outputCostPerToken": { "$ref": "#/components/schemas/DecimalPrice" - } + }, + "textPricing": {} } }, "PrivacyClassifyRequestDoc": { @@ -14740,6 +18139,7 @@ "output_tokens", "total_tokens", "cache_read_tokens", + "cache_write_tokens", "input_cost", "output_cost", "total_cost", @@ -14753,6 +18153,11 @@ "format": "int32", "description": "Number of prompt tokens that were cache hits (subset of input_tokens)" }, + "cache_write_tokens": { + "type": "integer", + "format": "int32", + "description": "Number of prompt tokens written to provider cache (subset of input_tokens)" + }, "created_at": { "type": "string", "description": "Timestamp of the recorded entry (RFC3339)" @@ -15620,6 +19025,20 @@ } } }, + "ResponseContent": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResponseContentPart" + } + } + ], + "description": "Content can be text or array of content parts" + }, "ResponseContentItem": { "oneOf": [ { @@ -15770,6 +19189,77 @@ ], "description": "Unified content item that can represent both user inputs and assistant outputs\nThis replaces ResponseOutputContent and correctly represents semantic types" }, + "ResponseContentPart": { + "oneOf": [ + { + "type": "object", + "required": [ + "text", + "type" + ], + "properties": { + "text": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "input_text" + ] + } + } + }, + { + "type": "object", + "required": [ + "image_url", + "type" + ], + "properties": { + "detail": { + "type": [ + "string", + "null" + ] + }, + "image_url": { + "$ref": "#/components/schemas/ResponseImageUrl" + }, + "type": { + "type": "string", + "enum": [ + "input_image" + ] + } + } + }, + { + "type": "object", + "required": [ + "file_id", + "type" + ], + "properties": { + "detail": { + "type": [ + "string", + "null" + ] + }, + "file_id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "input_file" + ] + } + } + } + ], + "description": "Content part from user inputs (input-only variants).\n\nThis type is used for type-safe operations on user inputs only.\nIt cannot contain output variants, providing compile-time safety.\n\nUsed in:\n- ResponseContent::Parts (for input listing)\n- list_input_items endpoint\n- Input validation operations" + }, "ResponseError": { "type": "object", "required": [ @@ -15826,6 +19316,68 @@ } } }, + "ResponseInput": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResponseInputItem" + } + } + ], + "description": "Input for a response - can be text, array of items, or single item" + }, + "ResponseInputItem": { + "type": "object", + "description": "Single input item", + "required": [ + "role", + "content" + ], + "properties": { + "content": { + "$ref": "#/components/schemas/ResponseContent" + }, + "metadata": {}, + "role": { + "type": "string" + } + } + }, + "ResponseInputItemList": { + "type": "object", + "description": "Input item list for responses", + "required": [ + "object", + "data", + "first_id", + "last_id", + "has_more" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResponseInputItem" + } + }, + "first_id": { + "type": "string" + }, + "has_more": { + "type": "boolean" + }, + "last_id": { + "type": "string" + }, + "object": { + "type": "string" + } + } + }, "ResponseItemStatus": { "type": "string", "enum": [ @@ -15989,7 +19541,7 @@ "tools": { "type": "array", "items": { - "$ref": "#/components/schemas/ClientManagedResponseTool" + "$ref": "#/components/schemas/ResponseTool" } }, "top_logprobs": { @@ -16516,13 +20068,6 @@ "type": "string", "description": "Status: \"in_progress\" when pending client execution" }, - "thought_signature": { - "type": [ - "string", - "null" - ], - "description": "Provider-specific metadata that clients must echo unchanged when\nreplaying the function call (for example Gemini's thought signature)." - }, "type": { "type": "string", "enum": [ @@ -16734,11 +20279,28 @@ "type" ], "properties": { + "filters": {}, + "search_context_size": { + "type": [ + "string", + "null" + ] + }, "type": { "type": "string", "enum": [ "web_search" ] + }, + "user_location": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserLocation" + } + ] } } }, @@ -16800,6 +20362,7 @@ }, { "type": "object", + "description": "Remote MCP server tool", "required": [ "server_label", "server_url", @@ -16815,14 +20378,9 @@ "type": "string" } }, - "authorization": { - "type": [ - "string", - "null" - ] - }, "require_approval": { - "$ref": "#/components/schemas/McpApprovalRequirement" + "$ref": "#/components/schemas/McpApprovalRequirement", + "description": "Tool approval requirement (default: \"always\")" }, "server_description": { "type": [ @@ -16834,7 +20392,8 @@ "type": "string" }, "server_url": { - "type": "string" + "type": "string", + "description": "HTTPS endpoint for the remote MCP server" }, "type": { "type": "string", @@ -17777,6 +21336,18 @@ } } }, + "UpdateAmlReportStatusRequest": { + "type": "object", + "description": "Admin AML report status update request.", + "required": [ + "active" + ], + "properties": { + "active": { + "type": "boolean" + } + } + }, "UpdateApiKeyRequest": { "type": "object", "description": "Request to update API key (general update for name, expires_at, and/or spend_limit)", @@ -17828,6 +21399,13 @@ } } }, + "UpdateConversationRequest": { + "type": "object", + "description": "Request to update a conversation", + "properties": { + "metadata": {} + } + }, "UpdateModelApiRequest": { "type": "object", "description": "Request to update model pricing (admin endpoint)", @@ -18049,6 +21627,13 @@ }, "description": "Sampling parameters accepted by the model (OpenRouter vocabulary)." }, + "textPricing": { + "type": [ + "object", + "null" + ], + "description": "Exact, versioned USD-per-million text pricing profile.\nOmitted leaves it unchanged; null removes it." + }, "verifiable": { "type": [ "boolean", @@ -18097,6 +21682,18 @@ } } }, + "UpdateOrganizationFallbackRequest": { + "type": "object", + "description": "Admin request to update an organization's fallback policy.", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + } + } + }, "UpdateOrganizationLimitsRequest": { "type": "object", "description": "Request to update organization limits (Admin only)", @@ -18269,6 +21866,24 @@ "settings": {} } }, + "UpsertAmlAllowlistEntryRequest": { + "type": "object", + "description": "Admin AML allowlist upsert request.", + "required": [ + "account_id" + ], + "properties": { + "account_id": { + "type": "string" + }, + "reason": { + "type": [ + "string", + "null" + ] + } + } + }, "Usage": { "type": "object", "required": [ @@ -18386,6 +22001,7 @@ "input_tokens", "output_tokens", "cache_read_tokens", + "cache_write_tokens", "total_tokens", "total_cost", "total_cost_display", @@ -18396,11 +22012,23 @@ "api_key_id": { "type": "string" }, + "billingDetails": {}, "cache_read_tokens": { "type": "integer", "format": "int32", "description": "Number of prompt tokens that were cache hits" }, + "cache_write_tokens": { + "type": "integer", + "format": "int32", + "description": "Number of prompt tokens written to provider cache" + }, + "contextBand": { + "type": [ + "string", + "null" + ] + }, "created_at": { "type": "string" }, @@ -18450,6 +22078,12 @@ ], "description": "Response ID when called from Responses API" }, + "serviceTier": { + "type": [ + "string", + "null" + ] + }, "stop_reason": { "type": [ "string", @@ -18503,6 +22137,42 @@ } } }, + "UserLocation": { + "type": "object", + "description": "User location for web search", + "required": [ + "type" + ], + "properties": { + "city": { + "type": [ + "string", + "null" + ] + }, + "country": { + "type": [ + "string", + "null" + ] + }, + "region": { + "type": [ + "string", + "null" + ] + }, + "timezone": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string" + } + } + }, "UserMetricsSummary": { "type": "object", "required": [ @@ -18853,469 +22523,113 @@ "id": { "type": "string" }, - "is_active": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "organization_id": { - "type": "string" - } - } - }, - "VerifyRequest": { - "type": "object", - "properties": { - "request_hash": { - "type": [ - "string", - "null" - ] - } - } - }, - "VpcInfo": { - "type": "object", - "description": "VPC information in attestation", - "properties": { - "vpc_hostname": { - "type": [ - "string", - "null" - ], - "description": "VPC hostname of this node" - }, - "vpc_server_app_id": { - "type": [ - "string", - "null" - ], - "description": "VPC server app ID" - } - } - }, - "WebSearchAction": { - "oneOf": [ - { - "type": "object", - "required": [ - "query", - "type" - ], - "properties": { - "query": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "search" - ] - } - } - } - ], - "description": "Web search action details" - }, - "WorkspaceResponse": { - "type": "object", - "description": "Workspace response model", - "required": [ - "id", - "name", - "organization_id", - "created_by_user_id", - "created_at", - "updated_at", - "is_active" - ], - "properties": { - "created_at": { - "type": "string", - "format": "date-time" - }, - "created_by_user_id": { - "type": "string" - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "id": { - "type": "string" - }, - "is_active": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "organization_id": { - "type": "string" - }, - "settings": {}, - "updated_at": { - "type": "string", - "format": "date-time" - } - } - }, - "ClientManagedResponseTool": { - "oneOf": [ - { - "type": "object", - "required": [ - "name", - "type" - ], - "properties": { - "description": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "parameters": {}, - "type": { - "type": "string", - "enum": [ - "function" - ] - } - } - } - ], - "description": "The only tool shape accepted from a public Responses request.\n\n`ResponseTool` retains legacy variants for dormant/internal code paths, but\nthe `CreateResponseRequest::tools` deserializer first parses this enum. That\nmakes unsupported builtin types ordinary request-deserialization failures,\nbefore the route or a provider can do any work." - }, - "StatelessResponseInputItemSchema": { - "oneOf": [ - { - "type": "object", - "required": [ - "type", - "call_id", - "name", - "arguments" - ], - "properties": { - "arguments": { - "type": "string" - }, - "call_id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "thought_signature": { - "type": [ - "string", - "null" - ] - }, - "type": { - "$ref": "#/components/schemas/FunctionCallType" - } - } - }, - { - "type": "object", - "required": [ - "type", - "call_id", - "output" - ], - "properties": { - "call_id": { - "type": "string" - }, - "output": { - "type": "string" - }, - "type": { - "$ref": "#/components/schemas/FunctionCallOutputType" - } - } - }, - { - "type": "object", - "required": [ - "role", - "content" - ], - "properties": { - "content": { - "$ref": "#/components/schemas/StatelessResponseContentSchema" - }, - "metadata": {}, - "role": { - "type": "string" - } - } - } - ] - }, - "StatelessResponseContentSchema": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "$ref": "#/components/schemas/StatelessResponseContentPartSchema" - } - } - ] - }, - "StatelessResponseInputSchema": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "$ref": "#/components/schemas/StatelessResponseInputItemSchema" - } + "is_active": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "organization_id": { + "type": "string" } - ] + } }, - "FunctionCallType": { - "type": "string", - "description": "Type marker for a client-replayed function call input item.", - "enum": [ - "function_call" - ] + "VerifyRequest": { + "type": "object", + "properties": { + "request_hash": { + "type": [ + "string", + "null" + ] + } + } }, - "StatelessResponseToolSchema": { - "oneOf": [ - { - "type": "object", - "required": [ - "name", - "type" + "VpcInfo": { + "type": "object", + "description": "VPC information in attestation", + "properties": { + "vpc_hostname": { + "type": [ + "string", + "null" ], - "properties": { - "description": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "parameters": {}, - "type": { - "type": "string", - "enum": [ - "function" - ] - } - } + "description": "VPC hostname of this node" + }, + "vpc_server_app_id": { + "type": [ + "string", + "null" + ], + "description": "VPC server app ID" } - ] + } }, - "StatelessResponseContentPartSchema": { + "WebSearchAction": { "oneOf": [ { "type": "object", "required": [ - "text", - "type" - ], - "properties": { - "text": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "input_text" - ] - } - } - }, - { - "type": "object", - "required": [ - "text", + "query", "type" ], "properties": { - "text": { + "query": { "type": "string" }, "type": { "type": "string", "enum": [ - "output_text" - ] - } - } - }, - { - "type": "object", - "required": [ - "image_url", - "type" - ], - "properties": { - "detail": { - "type": [ - "string", - "null" - ] - }, - "image_url": { - "$ref": "#/components/schemas/ResponseImageUrl" - }, - "type": { - "type": "string", - "enum": [ - "input_image" + "search" ] } } } - ] + ], + "description": "Web search action details" }, - "StatelessCreateResponseRequestSchema": { + "WorkspaceResponse": { "type": "object", - "description": "OpenAPI-only view of the stateless Responses request contract.\n\nThe runtime request type keeps legacy variants so it can return a precise\n`invalid_request_error` for them. The public endpoint schema should instead\nshow only the items accepted by the stateless implementation.", + "description": "Workspace response model", "required": [ - "model" + "id", + "name", + "organization_id", + "created_by_user_id", + "created_at", + "updated_at", + "is_active" ], "properties": { - "include": { - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "input": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/StatelessResponseInputSchema" - } - ] - }, - "instructions": { - "type": [ - "string", - "null" - ] - }, - "max_output_tokens": { - "type": [ - "integer", - "null" - ], - "format": "int64" - }, - "max_tool_calls": { - "type": [ - "integer", - "null" - ], - "format": "int64" + "created_at": { + "type": "string", + "format": "date-time" }, - "metadata": {}, - "model": { + "created_by_user_id": { "type": "string" }, - "parallel_tool_calls": { - "type": [ - "boolean", - "null" - ] - }, - "prompt_cache_key": { - "type": [ - "string", - "null" - ] - }, - "reasoning": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/ResponseReasoningConfig" - } - ] - }, - "safety_identifier": { - "type": [ - "string", - "null" - ] - }, - "service_tier": { + "description": { "type": [ "string", "null" ] }, - "store": { - "type": "boolean", - "description": "Must be `false` when supplied; omitted is normalized to `false`.", - "default": false, - "example": false, - "const": false - }, - "stream": { - "type": [ - "boolean", - "null" - ] + "id": { + "type": "string" }, - "temperature": { - "type": [ - "number", - "null" - ], - "format": "float" + "is_active": { + "type": "boolean" }, - "tool_choice": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/ResponseToolChoice" - } - ] + "name": { + "type": "string" }, - "tools": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/components/schemas/StatelessResponseToolSchema" - } + "organization_id": { + "type": "string" }, - "top_p": { - "type": [ - "number", - "null" - ], - "format": "float" + "settings": {}, + "updated_at": { + "type": "string", + "format": "date-time" } } } @@ -19384,9 +22698,13 @@ "name": "Models", "description": "Public model catalog and information" }, + { + "name": "Conversations", + "description": "Conversation management" + }, { "name": "Responses", - "description": "Stateless response inference (`store: false` only). Raw request/response content, response items, and history are not persisted. Clients must include any prior context in each request. Every successful Responses inference makes exactly one Chat Completions call. Only custom `function` tools are supported. They are client-managed: Cloud returns `function_call` items but never executes them; a later `store: false` request replays the individual call (the raw item from output is accepted) with its matching `function_call_output`, alongside caller-managed message history and the same function tool definitions. The minimal replay path also accepts assistant `message` text parts of type `output_text`, but not reasoning or arbitrary full `response.output` items. Server-executed tools (`web_search`, `web_context_search`, `file_search`, `code_interpreter`, `computer`, and remote `mcp`) and image-generation/editing models are rejected. The separate `POST /mcp` endpoint continues to expose its `web_search` tool independently of Responses; use `/v1/images/*` for image generation/editing. Existing completed-response gateway attestation is preserved best-effort: when the signature write succeeds, `GET /v1/signature/resp_*` retrieves signatures over SHA-256 request/response digests, never raw content. Interrupted streams create no `resp_*` attestation record or legacy disconnect fallback. Conversations, response history, and file input are rejected." + "description": "Response handling and streaming" }, { "name": "Organizations", @@ -19400,6 +22718,10 @@ "name": "Workspaces", "description": "Workspace and API key management" }, + { + "name": "Files", + "description": "File upload and management" + }, { "name": "Users", "description": "User profile and token management" diff --git a/cloud/guides/e2ee-chat-completions.mdx b/cloud/guides/e2ee-chat-completions.mdx index bb668c5..d1ad8dc 100644 --- a/cloud/guides/e2ee-chat-completions.mdx +++ b/cloud/guides/e2ee-chat-completions.mdx @@ -8,12 +8,14 @@ icon: "message-square-lock" NEAR AI Cloud runs both the gateway and model inference inside Trusted Execution Environments (TEEs), providing strong hardware-level isolation. End-to-end encryption (E2EE) adds an additional layer of protection by encrypting your messages with the model's public key before they leave your machine. +This guide uses the NEAR model E2EE protocol. The gateway sends client-E2EE requests only to a compatible NEAR-serving path; if none is available, the request is unsupported rather than routed through a different E2EE protocol. + ## Why Use E2EE? With E2EE enabled, your messages are encrypted client-side using the model's public key, which is cryptographically bound to its TEE attestation. This provides: - **Defense in depth** — Multiple independent encryption layers on top of TLS -- **Model-specific encryption** — Only the specific attested model instance can decrypt your messages +- **Model-specific encryption** — Only model environments holding the attested model key can decrypt your messages - **Cryptographic binding** — Messages are tied to a verified TEE through attestation - **Forward secrecy** — Each request uses ephemeral keys @@ -28,11 +30,12 @@ sequenceDiagram Client->>GatewayTEE: GET /attestation/report?model=X GatewayTEE->>ModelTEE: Forward attestation request Note over ModelTEE: Generate attestation report
with model's public key - ModelTEE->>GatewayTEE: Attestation report - GatewayTEE->>Client: Model public key (from Model TEE) + ModelTEE->>GatewayTEE: Attestation reports + GatewayTEE->>Client: Model attestation candidates + Note over Client: Verify candidates and select a key
under the application's acceptance policy Note over Client: Generate ephemeral key pair - Note over Client: Encrypt messages with model's public key + Note over Client: Encrypt messages with the selected model key Client->>GatewayTEE: POST /chat/completions
with encryption headers GatewayTEE->>ModelTEE: Forward encrypted request @@ -82,7 +85,7 @@ Wire format for all encrypted fields: `[ephemeral_pubkey (32 bytes)][nonce (24 b ## Quick Start -A self-contained Python script that performs the full E2EE flow: fetch the model's key, generate a client key pair, encrypt, send, and decrypt. +A Python script that demonstrates the E2EE encryption flow after you have obtained a verified model public key: generate a client key pair, encrypt, send, and decrypt. For a gateway report, verify every returned model-attestation candidate before applying your acceptance policy to select a key; the endpoint-specific attestation guides describe those checks. **Requirements:** `pip install requests PyNaCl cryptography` @@ -90,7 +93,7 @@ A self-contained Python script that performs the full E2EE flow: fetch the model - **cryptography** — X25519 ECDH key exchange and HKDF-SHA256 key derivation ```python -import secrets, requests +import os, secrets, requests from nacl.signing import SigningKey from nacl.bindings import ( crypto_sign_ed25519_pk_to_curve25519, @@ -104,8 +107,11 @@ from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives import hashes ENDPOINT = "https://cloud-api.near.ai" -API_KEY = "sk-..." -MODEL = "zai-org/GLM-5.1-FP8" +API_KEY = os.environ["NEAR_AI_CLOUD_API_KEY"] +MODEL = "zai-org/GLM-5.1-FP8" +# Obtain this from `signing_public_key` only after verifying the attestation +# candidates and selecting one report under your application's policy. +MODEL_ED25519_PUBLIC_KEY_HEX = os.environ["MODEL_ED25519_PUBLIC_KEY_HEX"] # ── Helpers ───────────────────────────────────────────────────────── @@ -124,14 +130,11 @@ def v2_decrypt(data: bytes, secret_x25519: bytes) -> bytes: key = HKDF(hashes.SHA256(), 32, None, b"ed25519_encryption").derive(shared) return crypto_aead_xchacha20poly1305_ietf_decrypt(ct, None, nonce, key) -# ── 1. Fetch model's Ed25519 public key ───────────────────────────── +# ── 1. Use the model's verified Ed25519 public key ────────────────── -attestation = requests.get( - f"{ENDPOINT}/v1/attestation/report", - headers={"Authorization": f"Bearer {API_KEY}"}, - params={"model": MODEL, "signing_algo": "ed25519"}, -).json() -model_ed25519_pub = bytes.fromhex(attestation["model_attestations"][0]["signing_public_key"]) +model_ed25519_pub = bytes.fromhex(MODEL_ED25519_PUBLIC_KEY_HEX) +if len(model_ed25519_pub) != 32: + raise ValueError("MODEL_ED25519_PUBLIC_KEY_HEX must encode 32 bytes") model_x25519_pub = crypto_sign_ed25519_pk_to_curve25519(model_ed25519_pub) # ── 2. Generate client Ed25519 key pair ────────────────────────────── @@ -153,7 +156,9 @@ resp = requests.post( "Content-Type": "application/json", "X-Signing-Algo": "ed25519", "X-Client-Pub-Key": client_pub_hex, + "X-Model-Pub-Key": model_ed25519_pub.hex(), "X-Encryption-Version": "2", + "x-no-aliasing": "true", }, json={ "model": MODEL, @@ -169,6 +174,9 @@ msg = resp.json()["choices"][0]["message"] if msg.get("reasoning_content"): print("Reasoning:", v2_decrypt(bytes.fromhex(msg["reasoning_content"]), client_x25519_secret).decode()) +if msg.get("reasoning"): + print("Reasoning:", v2_decrypt(bytes.fromhex(msg["reasoning"]), client_x25519_secret).decode()) + if msg.get("content"): print("Content:", v2_decrypt(bytes.fromhex(msg["content"]), client_x25519_secret).decode()) ``` @@ -179,7 +187,7 @@ if msg.get("content"): ### Step 1: Get the Model's Public Key -Fetch the model's Ed25519 public key from its TEE attestation report. The public key is generated inside the Model TEE and cryptographically bound to its hardware attestation. +Fetch the model's Ed25519 public key from its TEE attestation report. The public key is generated inside the Model TEE and cryptographically bound to its hardware attestation. For a gateway request, reject an empty `model_attestations[]` array, verify every returned candidate and its fresh nonce, then choose one verified report under your application's acceptance policy. Use that report's `signing_public_key` as the `X-Model-Pub-Key` routing pin. For a direct request, verify the root model report before using its key. See [NEAR model attestations](/cloud/verification/cloud-api/model-attestations) or [direct model attestation](/cloud/verification/direct/model-attestation). The gateway endpoint requires an API key (`Authorization: Bearer `); report retrieval is free and never counts against your usage. The direct completions endpoint is served by the model TEE itself and needs no API key. @@ -187,7 +195,15 @@ The gateway endpoint requires an API key (`Authorization: Bearer `); re ```bash -curl "https://cloud-api.near.ai/v1/attestation/report?model=zai-org/GLM-5.1-FP8&signing_algo=ed25519" \ +NONCE="$(openssl rand -hex 32)" + +curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ + --data-urlencode 'model=zai-org/GLM-5.1-FP8' \ + --data-urlencode 'provider=near' \ + --data-urlencode 'signing_algo=ed25519' \ + --data-urlencode 'include_tls_fingerprint=false' \ + --data-urlencode "nonce=${NONCE}" \ + -H 'x-no-aliasing: true' \ -H 'Authorization: Bearer ' ``` @@ -196,7 +212,11 @@ curl "https://cloud-api.near.ai/v1/attestation/report?model=zai-org/GLM-5.1-FP8& ```bash # Direct completions — no model parameter needed, the endpoint serves one model -curl "https://qwen35-122b.completions.near.ai/v1/attestation/report?signing_algo=ed25519" +NONCE="$(openssl rand -hex 32)" + +curl -G 'https://qwen35-122b.completions.near.ai/v1/attestation/report' \ + --data-urlencode 'signing_algo=ed25519' \ + --data-urlencode "nonce=${NONCE}" ``` @@ -204,54 +224,84 @@ curl "https://qwen35-122b.completions.near.ai/v1/attestation/report?signing_algo ```python import os +import secrets import requests +nonce = secrets.token_hex(32) response = requests.get( "https://cloud-api.near.ai/v1/attestation/report", - headers={"Authorization": f"Bearer {os.environ['NEAR_AI_CLOUD_API_KEY']}"}, + headers={ + "Authorization": f"Bearer {os.environ['NEAR_AI_CLOUD_API_KEY']}", + "x-no-aliasing": "true", + }, params={ "model": "zai-org/GLM-5.1-FP8", - "signing_algo": "ed25519" + "provider": "near", + "signing_algo": "ed25519", + "include_tls_fingerprint": "false", + "nonce": nonce, } ) +response.raise_for_status() # For direct completions: # response = requests.get( # "https://qwen35-122b.completions.near.ai/v1/attestation/report", -# params={"signing_algo": "ed25519"} +# params={"signing_algo": "ed25519", "nonce": nonce} # ) attestation = response.json() -model_public_key_hex = attestation["model_attestations"][0]["signing_public_key"] -print(f"Model Ed25519 public key: {model_public_key_hex}") +# NEAR AI Cloud gateway: +model_reports = attestation.get("model_attestations", []) +if not model_reports: + raise ValueError("No NEAR model attestations returned") +# Do not read a key from a raw candidate. Verify every candidate and `nonce` +# using the model-attestation flow, then select a verified report under your +# E2EE acceptance policy and read its `signing_public_key`. +print(f"Retrieved {len(model_reports)} NEAR model-attestation candidate(s)") +# Direct completions use the root model report instead: +# model_public_key_hex = attestation["signing_public_key"] ``` ```javascript +const nonce = Array.from(crypto.getRandomValues(new Uint8Array(32)), (byte) => + byte.toString(16).padStart(2, '0') +).join(''); + const response = await fetch( 'https://cloud-api.near.ai/v1/attestation/report?' + - new URLSearchParams({ - model: 'zai-org/GLM-5.1-FP8', - signing_algo: 'ed25519' - }), + new URLSearchParams({ + model: 'zai-org/GLM-5.1-FP8', + provider: 'near', + signing_algo: 'ed25519', + include_tls_fingerprint: 'false', + nonce, + }), { headers: { 'Authorization': `Bearer ${process.env.NEAR_AI_CLOUD_API_KEY}`, + 'x-no-aliasing': 'true', }, } ); +if (!response.ok) throw new Error(`Attestation request failed: ${response.status}`); const attestation = await response.json(); -const modelPublicKeyHex = attestation.model_attestations[0].signing_public_key; -console.log('Model Ed25519 public key:', modelPublicKeyHex); +const modelReports = attestation.model_attestations ?? []; +if (modelReports.length === 0) throw new Error('No NEAR model attestations returned'); +// Do not read a key from a raw candidate. Verify every candidate and `nonce` +// using the model-attestation flow, then select a verified report under your +// E2EE acceptance policy and read its `signing_public_key`. +console.log(`Retrieved ${modelReports.length} NEAR model-attestation candidate(s)`); ``` -The response contains `model_attestations` with the model's `signing_public_key` in hex format. +For the NEAR AI Cloud gateway, request `provider=near` with a canonical model ID and `x-no-aliasing: true`. Reject an empty response, verify every returned model-report candidate with the nonce you generated, and retain only reports your application accepts. Select a key only from that verified set, then send its `signing_public_key` in `X-Model-Pub-Key`. For a direct completions endpoint, the key is in the root report's `signing_public_key`. --- @@ -421,6 +471,7 @@ curl https://cloud-api.near.ai/v1/chat/completions \ -H "X-Client-Pub-Key: YOUR_ED25519_PUBLIC_KEY_HEX" \ -H "X-Model-Pub-Key: MODEL_ED25519_PUBLIC_KEY_HEX" \ -H "X-Encryption-Version: 2" \ + -H "x-no-aliasing: true" \ -d '{ "model": "zai-org/GLM-5.1-FP8", "messages": [{ @@ -465,7 +516,9 @@ response = requests.post( "Content-Type": "application/json", "X-Signing-Algo": "ed25519", "X-Client-Pub-Key": client_public_key_hex, + "X-Model-Pub-Key": model_public_key_hex, "X-Encryption-Version": "2", + "x-no-aliasing": "true", }, json={ "model": "zai-org/GLM-5.1-FP8", @@ -491,7 +544,9 @@ const response = await fetch('https://cloud-api.near.ai/v1/chat/completions', { 'Content-Type': 'application/json', 'X-Signing-Algo': 'ed25519', 'X-Client-Pub-Key': clientPublicKeyHex, + 'X-Model-Pub-Key': modelPublicKeyHex, 'X-Encryption-Version': '2', + 'x-no-aliasing': 'true', }, body: JSON.stringify({ model: 'zai-org/GLM-5.1-FP8', @@ -516,14 +571,15 @@ const data = await response.json(); | `X-Signing-Algo` | Set to `ed25519` | | `X-Client-Pub-Key` | Your Ed25519 public key in hex format (32 bytes / 64 hex chars) | | `X-Encryption-Version` | Set to `2` | -| `X-Model-Pub-Key` | Model's Ed25519 public key from attestation (required when using the gateway for routing) | +| `X-Model-Pub-Key` | Ed25519 public key from the selected, verified model report; required as the gateway routing pin | +| `x-no-aliasing` | Set to `true` for gateway requests so the model ID and attested model key cannot be silently redirected. | | `X-Encrypt-All-Fields` | Optional. Set to `true` to extend encryption beyond message content to tool definitions, tool calls, and other sensitive fields — see [Encrypting All Fields](#encrypting-all-fields-tool-calling) | --- ### Step 5: Decrypt the Response -The response `content` and `reasoning_content` fields will contain hex-encoded encrypted data. Decrypt using your private key. +The response `content`, `reasoning_content`, and `reasoning` fields can contain hex-encoded encrypted data. Decrypt each field that is present using your private key. @@ -572,6 +628,9 @@ if msg.get("content"): if msg.get("reasoning_content"): print("Reasoning:", decrypt_response(msg["reasoning_content"], client_x25519_secret)) + +if msg.get("reasoning"): + print("Reasoning:", decrypt_response(msg["reasoning"], client_x25519_secret)) ``` @@ -613,6 +672,9 @@ if (msg.content) { if (msg.reasoning_content) { console.log('Reasoning:', decryptResponse(msg.reasoning_content, clientX25519Secret)); } +if (msg.reasoning) { + console.log('Reasoning:', decryptResponse(msg.reasoning, clientX25519Secret)); +} ``` @@ -652,7 +714,9 @@ resp = requests.post( "Content-Type": "application/json", "X-Signing-Algo": "ed25519", "X-Client-Pub-Key": client_pub_hex, + "X-Model-Pub-Key": model_ed25519_pub.hex(), "X-Encryption-Version": "2", + "x-no-aliasing": "true", "X-Encrypt-All-Fields": "true", }, json={ @@ -686,12 +750,12 @@ E2EE is supported on the **Chat Completions API** (`/v1/chat/completions`), **Co ### Message Format - Encrypted message content must be hex-encoded -- The `content` and `reasoning_content` fields in responses will be hex-encoded encrypted data +- The `content`, `reasoning_content`, and `reasoning` fields in responses can be hex-encoded encrypted data - Each streaming chunk's content is independently encrypted ### Verification -You can verify the model's public key is authentic by checking the attestation report against hardware attestation. See [Model Verification](/cloud/verification/model) for details. +Before using a model public key for encryption, verify the attestation report and its fresh client nonce against hardware attestation. For a gateway response, verify every returned model-attestation candidate and choose a key only from reports accepted by your policy. Choose the matching endpoint flow in [Verification](/cloud/verification). --- @@ -700,8 +764,18 @@ You can verify the model's public key is authentic by checking the attestation r ### Get the Model's ECDSA Public Key + Verify every returned model-attestation candidate and its fresh nonce before applying your acceptance policy and using a selected public key, as described in [NEAR model attestations](/cloud/verification/cloud-api/model-attestations). + ```bash - curl "https://cloud-api.near.ai/v1/attestation/report?model=zai-org/GLM-5.1-FP8&signing_algo=ecdsa" \ + NONCE="$(openssl rand -hex 32)" + + curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ + --data-urlencode 'model=zai-org/GLM-5.1-FP8' \ + --data-urlencode 'provider=near' \ + --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode 'include_tls_fingerprint=false' \ + --data-urlencode "nonce=${NONCE}" \ + -H 'x-no-aliasing: true' \ -H 'Authorization: Bearer ' ``` @@ -770,6 +844,7 @@ You can verify the model's public key is authentic by checking the attestation r -H "X-Signing-Algo: ecdsa" \ -H "X-Client-Pub-Key: YOUR_ECDSA_PUBLIC_KEY_HEX" \ -H "X-Model-Pub-Key: MODEL_ECDSA_PUBLIC_KEY_HEX" \ + -H "x-no-aliasing: true" \ -d '{ "model": "zai-org/GLM-5.1-FP8", "messages": [{"role": "user", "content": "ENCRYPTED_CONTENT_HEX"}], @@ -810,5 +885,5 @@ You can verify the model's public key is authentic by checking the attestation r ## See Also - [Private Inference](/cloud/private-inference) — How TEE isolation protects your data -- [Model Verification](/cloud/verification/model) — Verify the model's TEE attestation -- [Chat Verification](/cloud/verification/chat) — Verify chat signatures +- [Verification](/cloud/verification) — Select NEAR AI Cloud gateway or direct model attestation verification +- [Verification Policy](/cloud/verification/reference/verification-policy) — Define the evidence your application requires diff --git a/cloud/private-inference.mdx b/cloud/private-inference.mdx index 05e01b1..467714e 100644 --- a/cloud/private-inference.mdx +++ b/cloud/private-inference.mdx @@ -26,7 +26,7 @@ NEAR AI Cloud's private inference provides three core guarantees: Your prompts, model weights, and outputs are encrypted and isolated in hardware-secured environments. Infrastructure providers, model providers, and NEAR cannot access your data at any point in the process. - Every computation generates cryptographic proof that it occurred inside a genuine, secure TEE. You can independently verify that your AI requests were processed in a protected environment without trusting any third party. + Attestation reports provide cryptographic evidence for a specific runtime and request path. When a response signature is available, you can also verify response integrity. Hardware-accelerated TEEs with [NVIDIA Confidential Computing](https://www.nvidia.com/en-us/data-center/solutions/confidential-computing/) deliver high-throughput inference with minimal latency overhead, making private inference practical for real-world applications. @@ -54,13 +54,13 @@ NEAR AI Cloud combines Intel TDX and NVIDIA TEE technologies to create isolated, If you're using a standard OpenAI SDK or curl, your prompts are automatically protected by TLS encryption—no additional setup required. -NEAR AI Cloud supports two connection modes — both provide TLS termination inside a TEE: +NEAR AI Cloud supports two connection modes. Each has its own TLS-attestation flow when your application requires evidence for the TLS endpoint of a particular connection: **Gateway mode** routes through `cloud-api.near.ai`, which runs in its own TEE before forwarding to the model TEE: ```mermaid flowchart LR - A["Your Machine"] -->|"🔒 Encrypted"| B["Gateway TEE"] -->|"🔒 Encrypted"| C["Model TEE: TLS Termination → LLM"] + A["Your Machine"] -->|"🔒 HTTPS"| B["NEAR AI Cloud gateway"] -->|"🔒 HTTPS"| C["Model endpoint → LLM"] style A fill:#1a1a2e,stroke:#4fc3f7,stroke-width:2px,color:#fff style B fill:#1b5e20,stroke:#66bb6a,stroke-width:2px,color:#fff @@ -71,23 +71,23 @@ flowchart LR ```mermaid flowchart LR - A["Your Machine"] -->|"🔒 Encrypted"| C["Model TEE: TLS Termination → LLM"] + A["Your Machine"] -->|"🔒 HTTPS"| C["Model endpoint → LLM"] style A fill:#1a1a2e,stroke:#4fc3f7,stroke-width:2px,color:#fff style C fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff ``` -**Key insight:** In both modes, TLS termination happens inside the TEE (green boxes), not at an external load balancer. Your prompts remain encrypted until they reach the secure enclave. +**Key insight:** TLS protects traffic in transit. When the route-specific TLS attestation flow passes for a particular connection, it binds that connection's endpoint to verified attestation evidence. Here's why this works: 1. **Standard HTTPS = TLS encryption**: When you make API calls using the OpenAI SDK or curl, you're connecting via HTTPS. This means TLS encryption is applied automatically by your client before any data leaves your machine. -2. **TLS terminates inside the TEE**: Unlike traditional cloud services where TLS terminates at an external load balancer, NEAR AI Cloud terminates TLS connections **inside the Trusted Execution Environment**. Your encrypted data travels from your laptop directly into the secure enclave before being decrypted. +2. **TLS endpoint verification**: Use TLS attestation when your application needs to verify the endpoint for a particular connection. The same TLS connection must supply both the peer certificate and attestation report. -3. **No plaintext exposure**: Because TLS termination happens within the TEE, your prompts are never exposed in plaintext outside the hardware-secured environment—not to network infrastructure, not to cloud providers, not to anyone. +3. **Evidence-driven trust**: Use attestation and the workload policy appropriate for your application to decide which environment properties are required. -This means you get the same seamless developer experience as any OpenAI-compatible API, while your data remains encrypted from your machine all the way into the secure TEE. +This gives you the same OpenAI-compatible development flow while allowing your application to verify the properties it requires. ### Direct Completions @@ -138,8 +138,8 @@ Gateway and direct completions responses include an `X-Request-Id` response head **Benefits of direct completions:** - **Fewer hops** — Your request reaches the model TEE directly, reducing latency -- **Simpler trust model** — Only the model TEE needs to be verified (no [gateway verification](/cloud/verification/gateway) required) -- **TLS binds to attestation** — The `include_tls_fingerprint=true` parameter on the attestation endpoint binds the TLS certificate to the attestation report +- **Simpler trust model** — Only the model TEE needs to be verified (no [NEAR AI Cloud gateway attestation](/cloud/verification/cloud-api/gateway-attestation) required) +- **TLS binds to attestation** — The `include_tls_fingerprint=true` parameter can bind the direct endpoint TLS certificate to its attestation report when the [direct TLS flow](/cloud/verification/direct/tls) is completed ### The Inference Process @@ -148,7 +148,7 @@ When you make a request to NEAR AI Cloud, your data flows through a secure pipel **Via Gateway (`cloud-api.near.ai`):** 1. **Request Initiation:** - You send chat completion requests via HTTPS to the LLM Gateway. TLS encryption protects your data in transit, and the TLS connection terminates inside the TEE, ensuring your prompts are decrypted only within the secure environment. + You send chat completion requests via HTTPS to the LLM Gateway. TLS protects data in transit; use [NEAR AI Cloud gateway TLS connection binding](/cloud/verification/cloud-api/tls) when your application needs to verify the endpoint for a particular connection. 2. **Secure Request Routing:** The LLM Gateway routes your request to the appropriate Private LLM Node based on the requested model, availability, and load balancing requirements. @@ -157,27 +157,27 @@ When you make a request to NEAR AI Cloud, your data flows through a secure pipel AI inference computations execute inside the Private LLM Node's TEE, where all data and model weights are protected by hardware-enforced isolation. 4. **Attestation Generation** - The TEE generates CPU and GPU attestation reports that provide cryptographic proof of the environment's integrity and configuration. + Attestation reports provide evidence about the environment and its measured configuration. - 5. **Cryptographic Signing:** - The TEE cryptographically signs both your original request and the inference results to ensure authenticity and prevent tampering. + 5. **Response Signing:** + Supported flows can make a response signature available for integrity verification. 6. **Verifiable Response:** - You receive the AI response along with cryptographic signatures and attestation data for independent verification. + Use [verification](/cloud/verification) to check the evidence required by your application. A response without an available signature is not response-verified. **Via Direct Completions (`{slug}.completions.near.ai`):** 1. **Direct Request:** - You send chat completion requests via HTTPS directly to the model's subdomain. TLS terminates inside the model's TEE — there is no intermediate gateway. + You send chat completion requests via HTTPS directly to the model's subdomain, with no intermediate NEAR AI Cloud gateway in the request path. Use [direct TLS connection binding](/cloud/verification/direct/tls) when endpoint verification is required. 2. **Secure Inference:** The model processes your request entirely within its TEE, where all data and model weights are protected by hardware-enforced isolation. 3. **Attestation & Signing:** - The TEE generates attestation reports and cryptographically signs both your request and the inference results. + Attestation reports provide environment evidence; supported flows can make response signatures available for integrity verification. 4. **Verifiable Response:** - You receive the response with cryptographic signatures. Only [model verification](/cloud/verification/model) is needed — no gateway attestation to check. + Use [direct model attestation](/cloud/verification/direct/model-attestation) for the direct endpoint. If response verification is required, also check that a signature is available and valid. --- @@ -215,13 +215,13 @@ NEAR AI Cloud's private inference implements multiple layers of security to prot TEEs create isolated execution environments enforced at the hardware level, preventing unauthorized access to memory and computation even from privileged system administrators or cloud providers. - **Secure Communication** : -All communication between your applications and the LLM infrastructure uses TLS encryption. Critically, TLS termination occurs inside the TEE—not at an external load balancer—so your prompts remain encrypted until they reach the secure enclave. +All communication between your applications and the LLM infrastructure uses TLS encryption. Use the route-specific [verification guide](/cloud/verification) to verify endpoint binding for a particular connection when that property is required. - **Cryptographic Attestation** : Every TEE environment generates cryptographic proofs that verify the integrity of the execution environment, allowing you to independently confirm your computations occurred in a genuine, unmodified TEE. - **Result Authentication** : -All AI outputs are cryptographically signed inside the TEE before leaving the secure environment, ensuring the authenticity and integrity of responses. +When a response signature is available, it can be verified against the attested signer to check response integrity. ### Threat Protection @@ -247,8 +247,8 @@ Cryptographic signatures generated inside the TEE ensure that responses cannot b Understand how to verify and validate secure interactions with AI models - - Verify that your HTTPS connection terminates inside the TEE using hardware-backed TLS attestation + + Verify gateway, model, TLS, and response evidence for requests sent through the NEAR AI Cloud gateway Add client-side encryption for defense-in-depth protection of your messages diff --git a/cloud/verification/chat.mdx b/cloud/verification/chat.mdx deleted file mode 100644 index 9e92f47..0000000 --- a/cloud/verification/chat.mdx +++ /dev/null @@ -1,334 +0,0 @@ ---- -title: "Chat Message Verification" -sidebarTitle: "Chat Verification" ---- - - -You can verify each chat message with NEAR AI Cloud. For this you will need: - -1. [Chat Message **REQUEST** Hash](#chat-message-request-hash) -2. [Chat Message **RESPONSE** Hash](#chat-message-response-hash) -3. [Chat Message Signature](#chat-message-signature) - - - See an example implementation in the [NEAR AI Cloud Verification Example](https://github.com/near-examples/nearai-cloud-verification-example) repo. - - - **Two signature kinds** - - There are two kinds of chat signatures, distinguished by the `signature_kind` field on the gateway's signature response — see [Signature Kinds](#signature-kinds): - - - **`provider_tee`** — generated **inside the model TEE** over the exact bytes the TEE received and sent. This is always what you get from a model's [direct completions endpoint](/cloud/private-inference#direct-completions) (`{slug}.completions.near.ai`) — the examples below use `qwen35-122b.completions.near.ai`. - - **`gateway`** — generated **inside the gateway TEE** (`cloud-api.near.ai`) over the exact bytes the gateway returned to you. Used when the gateway rewrites streamed response bytes (OpenAI-spec usage accounting), so the model TEE's byte-exact signature could no longer match what you received. - ---- - -## Chat Message Request Hash - -The value is calculated from the **exact JSON request body string** as it was sent over the wire. - -**_Example:_** - -```json -{"messages":[{"content":"Respond with only two words.","role":"user"}],"stream":true,"model":"Qwen/Qwen3.5-122B-A10B","chat_template_kwargs":{"enable_thinking":false}} -``` - -Which hashes to: - -```bash -2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad -``` - -Here is an example of how to get the sha256 hash of your message request body: - - - - -```js -import crypto from 'crypto'; - -const requestBody = JSON.stringify({ - "messages": [ - { - "content": "Respond with only two words.", - "role": "user" - } - ], - "stream": true, - "model": "Qwen/Qwen3.5-122B-A10B", - "chat_template_kwargs": {"enable_thinking": false} -}); - -const hash = crypto.createHash('sha256').update(requestBody).digest('hex'); -console.log(hash); //2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad -``` - - - - -```python -import hashlib -import json - -request_body = { - "messages": [ - { - "content": "Respond with only two words.", - "role": "user" - } - ], - "stream": True, - "model": "Qwen/Qwen3.5-122B-A10B", - "chat_template_kwargs": {"enable_thinking": False} -} - -# Convert to JSON string with same formatting as JavaScript -request_body_str = json.dumps(request_body, separators=(',', ':')) - -# Calculate SHA-256 hash -hash_obj = hashlib.sha256(request_body_str.encode()) -hash_hex = hash_obj.hexdigest() -print(hash_hex) #2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad -``` - - - - ---- - -## Chat Message Response Hash - -This value is calculated from the **exact response payload string**. - - - Please note that the streaming response contains two new lines at the end and should not be omitted when copying the response as the hash value will change. - -**_Example Response Body:_** - -```bash -data: {"choices":[{"delta":{"content":"","reasoning_content":null,"role":"assistant"},"finish_reason":null,"index":0,"logprobs":null,"matched_stop":null}],"created":1780404899,"id":"afa7975eaf844b1888776cf41548e230","model":"Qwen/Qwen3.5-122B-A10B","object":"chat.completion.chunk"} - -data: {"choices":[{"delta":{"content":"Under","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null,"matched_stop":null}],"created":1780404899,"id":"afa7975eaf844b1888776cf41548e230","model":"Qwen/Qwen3.5-122B-A10B","object":"chat.completion.chunk"} - -data: {"choices":[{"delta":{"content":"stood.","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null,"matched_stop":null}],"created":1780404899,"id":"afa7975eaf844b1888776cf41548e230","model":"Qwen/Qwen3.5-122B-A10B","object":"chat.completion.chunk"} - -data: {"choices":[{"delta":{"reasoning_content":null},"finish_reason":"stop","index":0,"logprobs":null,"matched_stop":248046}],"created":1780404899,"id":"afa7975eaf844b1888776cf41548e230","model":"Qwen/Qwen3.5-122B-A10B","object":"chat.completion.chunk"} - -data: {"choices":[],"created":1780404899,"id":"afa7975eaf844b1888776cf41548e230","model":"Qwen/Qwen3.5-122B-A10B","object":"chat.completion.chunk","usage":{"completion_tokens":4,"prompt_tokens":18,"prompt_tokens_details":null,"reasoning_tokens":0,"total_tokens":22}} - -data: [DONE] - -``` - -Which hashes to: - -```bash -8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c -``` - -Here is an example of how to get the sha256 hash of your message response payload: - - - - - -```js -const response = await fetch('https://qwen35-122b.completions.near.ai/v1/chat/completions', { - method: 'POST', - headers: { - 'accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${process.env.NEARAI_CLOUD_API_KEY}` - }, - body: requestBody -}); - -const responseBody = await response.text(); -const hash = crypto.createHash('sha256').update(responseBody).digest('hex'); -console.log(hash); // 8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c -``` - - - - -```python -import hashlib -import requests -import os - -response = requests.post( - 'https://qwen35-122b.completions.near.ai/v1/chat/completions', - headers={ - 'accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': f'Bearer {os.environ["NEARAI_CLOUD_API_KEY"]}' - }, - data=request_body_str # Uses the exact string from the previous example -) - -response_body = response.text -hash_obj = hashlib.sha256(response_body.encode()) -hash_hex = hash_obj.hexdigest() -print(hash_hex) # 8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c -``` - - - - ---- - -## Chat Message Signature - -From the Chat Message Response you will get a unique chat `id` that is used to fetch the Chat Message Signature from NEAR AI Cloud. - -You can query the signature API with the value of `id` from the response after chat completion. - -Use one of the following endpoints to get the signature: - -**Via Direct Completions:** -```bash -GET https://{slug}.completions.near.ai/v1/signature/{chat_id}?signing_algo=ecdsa -``` - -**Via Gateway:** -```bash -GET https://cloud-api.near.ai/v1/signature/{chat_id}?model={model_id}&signing_algo=ecdsa -``` - -For example, the `id` from the response in the previous section is: - - `afa7975eaf844b1888776cf41548e230` - -```bash -# Via direct completions: -curl -X GET 'https://qwen35-122b.completions.near.ai/v1/signature/afa7975eaf844b1888776cf41548e230?signing_algo=ecdsa' \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer " -``` - - - A model can be served by multiple TEE nodes behind the same domain. The signature is cached on the node that served your chat completion, so a lookup may transiently return `Chat id not found or expired` if it lands on a different node — simply retry until you hit the right one. - -***Example Response:*** - -```json -{ - "text":"Qwen/Qwen3.5-122B-A10B:2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad:8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c", - "signature":"0xed381e84d059198d1826e44dbbbac9501caaa8f79f913f27578acafa5be852e6103fe34fabd6446d7fde3f5250c0a16e109fe088a562bae08ac13d081a66d0761b", - "signing_address":"0x6525e128afcffebf7eed05d485d7be983cdae934", - "signing_algo":"ecdsa" -} -``` - -The above response gives us all of the crucial information we need to verify that the message was executed in our trusted environment: - -- `text` - The signed payload. Its format depends on the [signature kind](#signature-kinds): `{model_id}:{request_hash}:{response_hash}` for a model-TEE signature (as in this example), `{request_hash}:{response_hash}` for a gateway signature -- `signature` - This is the cryptographic signature of the `text` field, generated using the TEE's private key -- `signing_address` - Public key of the TEE that signed (the model TEE for `provider_tee`, the gateway TEE for `gateway`) -- `signing_algo` - Cryptography curve used to sign -- `signature_kind` - Which key produced the signature: `"provider_tee"` or `"gateway"` (gateway endpoint only — see [Signature Kinds](#signature-kinds)) - -You can see that `text` is: - -`Qwen/Qwen3.5-122B-A10B:2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad:8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c` - -This exactly matches the model we requested and the values we calculated in the previous sections: - -- Model: `Qwen/Qwen3.5-122B-A10B` -- Request hash: `2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad` -- Response hash: `8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c` - ---- - -## Signature Kinds - -Signatures fetched from the gateway (`GET https://cloud-api.near.ai/v1/signature/{chat_id}`) carry a `signature_kind` field that tells you which key signed and what the `text` payload contains: - -### `provider_tee` - -- **Who signs:** the model TEE that served your request, with the signing key from its [model attestation](/cloud/verification/model). Verify `signing_address` against that attestation. -- **Payload:** `text = "{model_id}:{request_hash}:{response_hash}"`. -- **What it covers:** the exact request bytes the model TEE received and the exact response bytes it sent. When you use a [direct completions endpoint](/cloud/private-inference#direct-completions), the bytes you sent and received are those bytes, so both hashes verify byte-for-byte against your own traffic. (Direct completions signature responses come straight from the model TEE and do not carry a `signature_kind` field — they are always model-TEE signatures.) - -### `gateway` - -- **Who signs:** the gateway TEE (`cloud-api.near.ai`), with the same signing key whose address is returned by the [gateway attestation report](/cloud/verification/gateway) (`GET /v1/attestation/report`). Verify `signing_address` against that report. -- **Payload:** `text = "{request_hash}:{response_hash}"` — no model-ID prefix. -- **What it covers:** the exact request body you sent to the gateway and the exact response bytes the gateway returned to you, so both hashes verify byte-for-byte against your own traffic. - -The gateway signs a streamed response itself when it has rewritten the stream bytes for OpenAI compatibility, which makes the model TEE's byte-exact signature unable to match what you received: - -- **Usage accounting** — with `stream_options: {"include_usage": true}`, the gateway rewrites intermediate chunks to carry `usage: null` and appends a single final usage chunk. -- **Usage stripping** — on attested models, default streaming (no `stream_options`) and `"include_usage": false` streams have per-chunk usage nulled out and the upstream's trailing usage-only chunk dropped. - -In both cases the gateway stores its signature **before** emitting the final `data: [DONE]` line, so the signature is retrievable the moment the stream ends. The gateway signature also covers attested third-party fallback serving (`x-serving-provider: chutes` response header): those backends protect response integrity with an end-to-end encrypted channel rather than a per-response model signature, so a rewritten stream served by them is signed by the gateway alone. - - - **Legacy signatures** - - Signatures stored before `signature_kind` was recorded omit the field — their provenance is unknown rather than guessed. You can still distinguish them structurally: a `text` with three `:`-separated fields (`{model_id}:{request_hash}:{response_hash}`) is a model-TEE payload; one with two (`{request_hash}:{response_hash}`) is a gateway payload. - ---- - -## Verify Signature - -Signature verification can be easily done with any standard ECDSA verification library such as [ethers](https://www.npmjs.com/package/ethers) or even an online tool such as [etherscan's VerifySignatures](https://etherscan.io/verifiedSignatures). - -These tools will require: - -- `Address`: What the expected address is for the signature. In our case it will be the one retrieved from your [attestation API query](/cloud/verification/model) (see Model Verification page). -- `Message`: The original message before signing. In our case it will be the `text` field from [Chat Message Signature](#chat-message-signature) (`{model_id}:{request_hash}:{response_hash}`) -- `Signature`: The signed message from above - -Here is an example of how to verify the Chat Message signature using `ethers`: - - - - -```js -import { ethers } from 'ethers'; - -const text = "Qwen/Qwen3.5-122B-A10B:2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad:8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c"; -const signature = "0xed381e84d059198d1826e44dbbbac9501caaa8f79f913f27578acafa5be852e6103fe34fabd6446d7fde3f5250c0a16e109fe088a562bae08ac13d081a66d0761b"; -const expectedAddress = "0x6525e128afcffebf7eed05d485d7be983cdae934"; - -// Recover the address from the signature -const recoveredAddress = ethers.verifyMessage(text, signature); - -// Compare with expected address (case-insensitive) -const isValid = recoveredAddress.toLowerCase() === expectedAddress.toLowerCase(); - -console.log("Text:", text); -console.log("Expected address:", expectedAddress); -console.log("Recovered address:", recoveredAddress); -console.log("Signature valid:", isValid); -``` - - - - -```python -from eth_account import Account -from eth_account.messages import encode_defunct - -text = "Qwen/Qwen3.5-122B-A10B:2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad:8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c" -signature = "0xed381e84d059198d1826e44dbbbac9501caaa8f79f913f27578acafa5be852e6103fe34fabd6446d7fde3f5250c0a16e109fe088a562bae08ac13d081a66d0761b" -expected_address = "0x6525e128afcffebf7eed05d485d7be983cdae934" - -# Create a message object that can be signed -message = encode_defunct(text=text) - -# Recover the address from the signature -recovered_address = Account.recover_message(message, signature=signature) - -# Compare with expected address (case-insensitive) -is_valid = recovered_address.lower() == expected_address.lower() - -print("Text:", text) -print("Expected address:", expected_address) -print("Recovered address:", recovered_address) -print("Signature valid:", is_valid) -``` - - - diff --git a/cloud/verification/cloud-api/gateway-attestation.mdx b/cloud/verification/cloud-api/gateway-attestation.mdx new file mode 100644 index 0000000..061cda9 --- /dev/null +++ b/cloud/verification/cloud-api/gateway-attestation.mdx @@ -0,0 +1,54 @@ +--- +title: "Gateway Attestation" +description: "Verify the NEAR AI Cloud gateway attestation for requests sent to cloud-api.near.ai." +--- + +`gateway_attestation` is evidence for the NEAR AI Cloud gateway. It is not evidence for a model environment. Use [model attestations](/cloud/verification/cloud-api/model-attestations) when your policy also requires model evidence. + +## Request gateway evidence + +Generate the nonce in your client. It must be 32 random bytes encoded as 64 hexadecimal characters. + +```bash +NONCE="$(openssl rand -hex 32)" + +curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ + --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode "nonce=${NONCE}" \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' +``` + +The response contains the gateway report at `gateway_attestation`. For a deployment-first completion, verify and retain it before sending the completion. Add `model=` and `provider=near` when you also need [NEAR model evidence](/cloud/verification/cloud-api/model-attestations). That request can return multiple candidates in `model_attestations[]`; verify every candidate before sending the completion. Add `include_tls_fingerprint=true` only when you are following the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). + +## Verify the gateway report + +For `gateway_attestation`: + +1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. +2. Read `report_data` and measurements from the **verified quote**, not only from fields echoed in the HTTP response. Reject the report if its echoed `request_nonce` or `report_data` is missing or differs from the nonce and report data you verified. +3. Check that the verified quote binds the nonce generated by your client and the reported `signing_address`. Use `signing_algo` to interpret that identity and verify response signatures. +4. Replay `event_log` and require the result to equal the RTMR3 in the verified quote. +5. Obtain the raw `info.tcb_info.app_compose` string. If `tcb_info` is JSON text, decode it first. Hash `app_compose` without parsing or reserializing it, and require the quote's MRCONFIGID to be `01` followed by that SHA-256 hash. +6. When a TLS fingerprint was requested, verify its binding as part of the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). +7. Apply your accepted measurement and image-provenance policy. + +The verified quote is the cryptographic source of truth. The HTTP fields are consistency checks, not a substitute for quote verification. + +## Field scope + +| Field path | What it describes | +| --- | --- | +| `gateway_attestation.intel_quote` | The gateway's Intel TDX quote. | +| `gateway_attestation.event_log` | Runtime measurement log. Replay it against the quote's RTMR3. | +| `gateway_attestation.request_nonce` and `.report_data` | HTTP copies to compare with the client nonce and verified quote. | +| `gateway_attestation.signing_address` and `.signing_algo` | The gateway signing identity and algorithm. | +| `gateway_attestation.tls_cert_fingerprint` | The gateway TLS binding when requested. | +| `gateway_attestation.info.tcb_info.app_compose` | Raw measured configuration string. Hash it and match it to the quote's MRCONFIGID. | +| `model_attestations[]` | Separate NEAR model-evidence candidates when requested with `model` and `provider=near`. Verify every candidate separately; none is a substitute for the gateway quote. | + +## Freshness and failure handling + +The nonce establishes freshness only when the client generated and retained it, and the verified quote contains that nonce. If the report is unavailable, the quote fails, the nonce does not match, or the identity does not match, treat the gateway property as unverified. + +For a deployment-first response-signature flow, retain this verified report and require a later `gateway` signature to match its signer and algorithm. A report fetched after the completion cannot replace that preflight evidence, and a report from another signer is not a substitute. diff --git a/cloud/verification/cloud-api/image-provenance.mdx b/cloud/verification/cloud-api/image-provenance.mdx new file mode 100644 index 0000000..5e0eacc --- /dev/null +++ b/cloud/verification/cloud-api/image-provenance.mdx @@ -0,0 +1,36 @@ +--- +title: "Image Provenance" +description: "Apply an image-provenance policy to configurations measured in NEAR AI Cloud gateway and model attestations." +--- + +Image provenance is separate from hardware attestation. A verified quote can bind a configuration measurement to a runtime. Provenance determines whether the image digests in that configuration were built by a source and build process your policy accepts. + +For model-level provenance through the gateway, this flow applies to every NEAR model report returned by a request with `provider=near`. + +For a NEAR AI Cloud gateway flow, decide separately whether your policy requires provenance for: + +- the configuration measured by `gateway_attestation` +- each NEAR model report in `model_attestations[]`, requested with `provider=near` +- both + +## Verification flow + +1. Verify the relevant Intel TDX quote first. For `model_attestations[]`, verify each candidate report separately before treating it as eligible evidence. +2. For each eligible NEAR model report, use the raw `info.tcb_info.app_compose` string. Calculate its SHA-256 hash without parsing or reserializing it, and compare it with the configuration measurement in that report's verified quote. +3. Extract every immutable image digest (`@sha256:…`) from the matched configuration. +4. Cryptographically verify provenance for each required digest and confirm that the statement's subject equals the image digest exactly. +5. Apply your policy for repository, build identity, workflow, ref, and source revision. + + + A tag, search result, or HTTP `200` only locates a possible record. It does not cryptographically verify the provenance statement, its signer, or its subject digest. + + +## Missing evidence + +An unavailable provenance record, including an HTTP `404`, means the reported digest could not be verified under that policy. It does not by itself identify a security incident. + +Fetch a fresh report after a deployment change. If the current evidence still lacks provenance required by your policy, treat that image as unverified. + + + When requesting help, share only non-sensitive verification metadata: the endpoint, UTC time, requested model, signing address, `info.instance_id` when present, image name, and full digest. Never include API keys, prompts, or response content. + diff --git a/cloud/verification/cloud-api/index.mdx b/cloud/verification/cloud-api/index.mdx new file mode 100644 index 0000000..028e1fb --- /dev/null +++ b/cloud/verification/cloud-api/index.mdx @@ -0,0 +1,36 @@ +--- +title: "Verify the NEAR AI Cloud Gateway" +description: "Verify the gateway, model evidence, connection, response, and software evidence for requests sent through cloud-api.near.ai." +--- + +Use this guide when your application sends inference requests to `https://cloud-api.near.ai`. If your application sends requests directly to a model's `completions.near.ai` endpoint, use the [direct model endpoint guide](/cloud/verification/direct) instead. + +## What you can verify + + + + Verify the NEAR AI Cloud gateway deployment before an inference request. + + + Verify every returned NEAR model-attestation candidate before an inference request. + + + Verify that your live HTTPS connection terminates at the attested gateway. + + + Verify exact response bytes against the matching preflight signer. + + + Check that the software images in the attested environment meet your source and build policy. + + + +## Verify a completion + +1. Before the completion, request and verify fresh gateway evidence. If your policy requires NEAR model evidence, request it with the canonical model ID and `provider=near`, verify every returned candidate, and retain the verified results. For Gateway TLS binding, follow the same-connection [TLS flow](/cloud/verification/cloud-api/tls). +2. Send the completion with the same canonical model ID and `x-no-aliasing: true`. Retain the exact request and response bytes. +3. Fetch the completion signature. Its `signature_kind` selects the verified gateway report or the uniquely matching verified model report; it does not decide which deployment checks your policy required. + +Use one explicit `signing_algo` for the deployment reports and completion signature. Do not rely on endpoint defaults to choose a matching signer. + +Start with [Gateway attestation](/cloud/verification/cloud-api/gateway-attestation). The other guides provide the individual checks. diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx new file mode 100644 index 0000000..422cc54 --- /dev/null +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -0,0 +1,76 @@ +--- +title: "NEAR Model Attestations" +description: "Verify the NEAR model-attestation report returned through the NEAR AI Cloud gateway." +--- + +Use this page when a request to `cloud-api.near.ai` needs evidence for a NEAR-operated model environment in addition to the gateway. Request that report explicitly with `provider=near`. + +## Request model evidence + +Include the canonical model ID when requesting the report. Do not use an alias: send `x-no-aliasing: true` so the request fails instead of being rewritten to another model ID. + +```bash +NONCE="$(openssl rand -hex 32)" + +curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ + --data-urlencode 'model=' \ + --data-urlencode 'provider=near' \ + --data-urlencode 'include_tls_fingerprint=false' \ + --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode "nonce=${NONCE}" \ + -H 'Accept: application/json' \ + -H 'x-no-aliasing: true' \ + -H 'Authorization: Bearer ' +``` + +The response includes gateway evidence and zero or more NEAR model-report candidates in `model_attestations[]`. If your policy requires NEAR model evidence, reject an empty array. Before sending a completion, verify every candidate and retain the verified results; do not choose a raw entry yourself. + +For a later `provider_tee` response signature, select exactly one retained verified report whose `signing_address` and `signing_algo` match the signature. If no report or more than one report matches, the response is unverified. A report fetched after the completion cannot replace this preflight evidence. + + + `provider=near` requests NEAR model reports from this endpoint. It does not select the provider for a later completion. A `provider_tee` response is verified only when its signer and algorithm uniquely match a retained verified report. + + +## NEAR model report fields + +The NEAR report uses these fields: + +| Field | Purpose | +| --- | --- | +| `intel_quote` | Intel TDX evidence. | +| `event_log` | Runtime measurement log. Replay it and match the result to the quote's RTMR3. | +| `request_nonce` | Echoed request nonce. Require it to match the client nonce, then confirm the nonce binding from the verified quote. | +| `report_data` | Optional HTTP copy of quote report data. The verified quote is the source of truth; when this field is present, compare it with the verified quote. | +| `signing_address` and `signing_algo` | Model signing identity and its algorithm. | +| `signing_public_key` | Model public key needed by the E2EE flow. Other model-attestation flows do not require it. Request `signing_algo=ed25519` for E2EE. | +| `nvidia_payload` | NVIDIA GPU evidence, when present. | +| `info.tcb_info.app_compose` | Raw measured configuration string. Hash it and match it to the quote's MRCONFIGID. `tcb_info` can itself be JSON text. | + +## Verify NEAR model reports + +Perform these checks on every returned candidate before sending a completion: + +1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. +2. Read report data from the verified quote. Require it to bind the client-generated nonce and `signing_address`; require `request_nonce` to match the client nonce. When `report_data` is present, require it to match the verified quote. Interpret the address using `signing_algo`. +3. Replay `event_log` and require the resulting RTMR3 to equal the RTMR3 in the verified quote. +4. If `tcb_info` is JSON text, decode it to obtain `app_compose`. Hash the raw `app_compose` string without parsing or reserializing it. Require the quote's MRCONFIGID to begin with `01` followed by that SHA-256 hash. +5. When GPU evidence is present, follow [GPU evidence verification](/cloud/verification/reference/quote-nonce-signer#verify-gpu-evidence-when-present). +6. Apply your own allowlist and provenance policy to the verified measurements. + +After receiving a `provider_tee` response signature, select the one retained report whose signer identity and algorithm match the signature. If the match is absent or ambiguous, reject the response as unverified. + + + `model_attestations[]` is not an inventory of every model instance behind a URL. Candidates are not interchangeable: a response signature must be matched by signer identity and algorithm. + + +## Relationship to NEAR AI Cloud gateway TLS + +The browser or client connection is to `cloud-api.near.ai`. Its TLS endpoint binding is verified against `gateway_attestation`, not against a TLS fingerprint carried by an upstream model report. See [NEAR AI Cloud gateway TLS connection binding](/cloud/verification/cloud-api/tls). + +## Verify a signed response + +For a NEAR AI Cloud gateway response with `signature_kind: "provider_tee"`, verify the signature over the endpoint-defined payload and match its signer to exactly one previously verified model report. For a `gateway` signature, match the signer to the previously verified `gateway_attestation` instead. See [NEAR AI Cloud gateway response signatures](/cloud/verification/cloud-api/response-signatures). + +## Other provider documentation + +Chutes documents its own evidence format in [TEE Evidence Verification](https://github.com/chutesai/chutes-api/blob/main/docs/tee-verification.md). diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx new file mode 100644 index 0000000..b85496e --- /dev/null +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -0,0 +1,74 @@ +--- +title: "Response Signatures" +description: "Verify available response signatures from the NEAR AI Cloud gateway against exact bytes and matching preflight evidence." +--- + +Response signatures returned through the NEAR AI Cloud gateway can be produced by either a model-serving TEE or the gateway itself. Before sending a completion, verify the gateway and, when your policy requires model evidence, every returned NEAR model-attestation candidate. When present, `signature_kind` determines which verified preflight result must match the signer. + + + A response without an available signature is not a successfully verified response. If response verification is required by your policy, treat it as unverified after a short, bounded retry interval. + + +## Preserve exact request and response bytes + +Build the request body once and retain the exact bytes sent by the client. Retain the raw response bytes before parsing JSON or Server-Sent Events. + +To verify a `provider_tee` signature, send the original completion with the canonical model ID and `x-no-aliasing: true`. Do not send an alias and replace it after receiving the response: the model ID in the original request is part of the signed text. + +```text +request_hash = SHA-256(exact request-body bytes) +response_hash = SHA-256(exact response-body bytes) +``` + +Do not parse and re-serialize JSON, normalize whitespace or line endings, omit stream delimiters, or reconstruct a streamed response from parsed events. Configure the HTTP client to retain the entity bytes it received; requesting `Accept-Encoding: identity` is the simplest way to avoid transparent decompression changing those bytes. + +## Fetch and verify the signature + +Set `` to the `id` in the completion response whose exact bytes you retained. For the Responses API, use its returned `resp_…` ID. + +```bash +curl --fail-with-body -G 'https://cloud-api.near.ai/v1/signature/' \ + --data-urlencode 'signing_algo=' \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' +``` + +A successful (`2xx`) response containing `error_code` and `message` is an unavailable result, not a signature. A `404` means no matching signature was found; it can be retried for a short, bounded interval when the signature may still be recorded. Treat either result as unverified if a signature is required. + +Use the same explicit `signing_algo` as both preflight attestation requests. These endpoints have different defaults. + +For `ecdsa`, recover the Ethereum EIP-191 signer from `text` and `signature`, then compare it with `signing_address`. For `ed25519`, verify the UTF-8 bytes of `text` using `signing_address` as the public key. Reject unsupported algorithms. + +Then compare `text` with the expected payload: + +| `signature_kind` | Expected `text` | Required preflight match | +| --- | --- | --- | +| `provider_tee` | `::` | Exactly one verified NEAR model report with the same `signing_address` and `signing_algo`. | +| `gateway` | `:` | A verified `gateway_attestation` with the same `signing_address` and `signing_algo`. | + +Only `provider_tee` binds a response to a model-serving TEE. A `gateway` signature binds the response to the gateway, not to a model report. If a model signature is unavailable, model-level response binding is unavailable. + +The Gateway can rewrite client-visible streaming bytes, so a `gateway` signature covers the exact bytes received from the Gateway, including stream framing and terminal events. + +If `signature_kind` is absent or unfamiliar, do not infer it from the shape of `text`. Treat the signer classification as unavailable unless your endpoint contract gives a rule for that response. + +For `provider_tee`, `` is the canonical model identifier in the signed payload. + +Use the [manual response-signature helpers](/cloud/verification/reference/response-signature-verification) with the exact request and response bytes. For `provider_tee`, pass the canonical model ID from the original request to build the expected text and use the signer from the uniquely matching verified model report. For `gateway`, omit the model ID and use the verified Gateway report's signer. + +## Match preflight evidence + +Before sending the completion, request and verify gateway evidence with a client-generated nonce. To support `provider_tee`, also request NEAR model evidence with `model=` and `provider=near`. Reject an empty `model_attestations[]` array; verify every returned candidate and retain the verified results. + +After fetching the signature, use `signature_kind` to select the matching preflight result: + +- For `provider_tee`, require the signature signer and algorithm to match exactly one verified NEAR model report. If the match is absent or ambiguous, the response is unverified. +- For `gateway`, require them to match the verified gateway report. + +Do not use a report obtained after the completion as a substitute for preflight evidence. If the signature's signer or algorithm differs from the required preflight evidence, the response is unverified under this flow. + +`ecdsa` identities are Ethereum-style signing addresses. `ed25519` identities are Ed25519 public-key bytes encoded as hexadecimal. Always compare the identity together with `signing_algo`. + + + Preflight Gateway and model evidence plus one response signature do not yet form a complete model-to-Gateway-to-final-response chain. A `gateway` signature proves that the verified Gateway signed the final client-visible bytes, not that the verified model produced an upstream response. A `provider_tee` signature does not bind its model-signed bytes to the preflight Gateway or TLS evidence. + diff --git a/cloud/verification/cloud-api/tls.mdx b/cloud/verification/cloud-api/tls.mdx new file mode 100644 index 0000000..d81bf4e --- /dev/null +++ b/cloud/verification/cloud-api/tls.mdx @@ -0,0 +1,44 @@ +--- +title: "TLS Connection Binding" +description: "Bind a live NEAR AI Cloud gateway TLS connection to the gateway attestation." +--- + +Use this flow to establish that the HTTPS connection to `cloud-api.near.ai` terminates at the endpoint bound to `gateway_attestation`. + + + The peer certificate and attestation report must be obtained on the **same TLS connection**. Two independent connections can reach different instances or observe different certificates, so they cannot establish this connection-specific conclusion. + + + + Browser Fetch APIs do not expose the peer certificate or its SPKI. A browser client can verify a gateway quote without TLS binding, but cannot establish this TLS connection-binding property. + + +## What this checks + +When you request `include_tls_fingerprint=true`, `gateway_attestation.tls_cert_fingerprint` is the SHA-256 hash of the gateway certificate's SubjectPublicKeyInfo (SPKI). The verified quote binds that fingerprint, the gateway signing identity, and the nonce. + +This is a gateway check. It does not bind the client connection to an upstream model endpoint. + +## Verification flow + +1. Generate a fresh 32-byte nonce in the client. +2. Open a TLS connection to `cloud-api.near.ai` using the trust configuration required by your application. +3. Read the peer certificate from that connection and calculate `SHA-256(SPKI_DER)`. +4. Reuse the same open connection to request `/v1/attestation/report` with `include_tls_fingerprint=true`, the nonce, and the signing algorithm you require. Include the normal `Authorization: Bearer ` header. NEAR AI Cloud gateway report retrieval requires an API key. +5. Verify `gateway_attestation.intel_quote`, then check the nonce, gateway signer, and TLS-fingerprint binding in the verified quote. +6. Compare the calculated peer SPKI hash with `gateway_attestation.tls_cert_fingerprint`. + +The optional `tls_certificate` response field is not a substitute for the peer certificate observed on the connection. A command-line report request alone also cannot establish the same-connection property. + +This result applies to the connection used for the evidence request. When it must also cover an inference request, send that request over the same open TLS connection. If the client reconnects, repeat the flow. + +## Required handling + +| Result | Action when TLS endpoint binding is required | +| --- | --- | +| `gateway_attestation.tls_cert_fingerprint` is absent | Treat TLS binding as unavailable. Request a new report with `include_tls_fingerprint=true`. | +| Peer SPKI hash differs | Reject the connection and repeat the flow with one TLS connection. | +| Quote, nonce, or signer binding fails | Reject the report and fetch fresh evidence. | +| Certificate and report came from different connections | Treat the result as inconclusive. | + +Do not use a cached fingerprint as evidence for a new connection. Fetch new evidence after a certificate, signer, measurement, or accepted-attestation-age change. diff --git a/cloud/verification/direct/image-provenance.mdx b/cloud/verification/direct/image-provenance.mdx new file mode 100644 index 0000000..4acf9f4 --- /dev/null +++ b/cloud/verification/direct/image-provenance.mdx @@ -0,0 +1,32 @@ +--- +title: "Image Provenance" +description: "Apply an image-provenance policy to the configuration measured by a direct model attestation." +--- + +Image provenance is separate from hardware attestation. A verified direct model quote can bind a configuration measurement to a runtime. Provenance determines whether the image digests in that configuration were built by a source and build process your policy accepts. + +## Verification flow + +1. Verify the direct model's Intel TDX quote first. +2. Use the raw `info.tcb_info.app_compose` string from the direct report. Calculate its SHA-256 hash without parsing or reserializing it, and compare it with the configuration measurement in the verified quote. +3. Extract every immutable image digest (`@sha256:…`) from the matched configuration. +4. Cryptographically verify provenance for each required digest and confirm that the statement's subject equals the image digest exactly. +5. Apply your policy for repository, build identity, workflow, ref, and source revision. + + + An image tag, search result, or HTTP `200` only locates a possible record. It does not cryptographically verify the provenance statement, its signer, or its subject digest. + + +## Relationship to direct attestation + +The quote and configuration measurement establish that the configuration text you matched is bound to the verified model report. Provenance establishes that a specific digest satisfies your source and build policy. Use both checks when your application requires a source-to-runtime trust chain. + +## Missing evidence + +An unavailable provenance record, including an HTTP `404`, means the reported digest could not be verified under that policy. It does not by itself identify a security incident. + +Fetch a fresh report after a deployment change. If the current evidence still lacks provenance required by your policy, treat that image as unverified. + + + When requesting help, share only non-sensitive verification metadata: the endpoint, UTC time, model name, signing address, `info.instance_id` when present, image name, and full digest. Never include API keys, prompts, or response content. + diff --git a/cloud/verification/direct/index.mdx b/cloud/verification/direct/index.mdx new file mode 100644 index 0000000..7f753bd --- /dev/null +++ b/cloud/verification/direct/index.mdx @@ -0,0 +1,32 @@ +--- +title: "Verify Direct Model Endpoints" +description: "Verify the model, connection, response, and software evidence for direct completions endpoints." +--- + +Use this guide when your application sends inference requests directly to `https://.completions.near.ai`. The NEAR AI Cloud gateway is not in this request path. If your application sends requests to `cloud-api.near.ai`, use the [NEAR AI Cloud gateway guide](/cloud/verification/cloud-api) instead. + +## What you can verify + + + + Verify the trusted model environment reported by the direct endpoint. + + + Verify that your live HTTPS connection terminates at the attested model endpoint. + + + Verify that a signed response came from the attested model environment. + + + Check that the software images in the attested environment meet your source and build policy. + + + +## Start with model attestation + +1. Generate a fresh nonce for this verification attempt. +2. Request a fresh report from the same direct endpoint your application uses for inference. +3. Verify the model's hardware attestation and apply your acceptance policy. +4. Add TLS, response-signature, or image-provenance checks when your application requires them. + +Start with [Model attestation report](/cloud/verification/direct/model-attestation). That guide shows how to request fresh evidence. The other guides explain the additional checks. diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx new file mode 100644 index 0000000..51bc3e9 --- /dev/null +++ b/cloud/verification/direct/model-attestation.mdx @@ -0,0 +1,54 @@ +--- +title: "Model Attestation Report" +description: "Verify the root model report returned by a direct completions endpoint." +--- + +Use this page to verify the model environment serving a direct `https://.completions.near.ai` endpoint. + +## Request a fresh report + +Generate the nonce in your client as 32 random bytes encoded as 64 hexadecimal characters. + +```bash +NONCE="$(openssl rand -hex 32)" + +curl --fail-with-body -G 'https://.completions.near.ai/v1/attestation/report' \ + --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode "nonce=${NONCE}" \ + -H 'Accept: application/json' +``` + +To request evidence for a response signer, add `signing_address=` and use the response's `signing_algo`. Add `include_tls_fingerprint=true` only for [direct TLS connection binding](/cloud/verification/direct/tls). + +## Direct response shape + +The root response is the model report. Relevant fields include: + +| Field | Meaning | +| --- | --- | +| `model_name` | The model name stated in this report. | +| `signing_address`, `signing_algo` | The model signing identity and algorithm. | +| `request_nonce` | The nonce echoed for diagnostics. Verify it inside the quote. | +| `report_data` | Optional HTTP copy of quote report data. When present, compare it with the verified quote. | +| `intel_quote` | The model's Intel TDX quote. | +| `event_log` | Runtime measurement log. Replay it and match the result to the quote's RTMR3. | +| `nvidia_payload` | GPU evidence, when present. | +| `info.tcb_info.app_compose` | Raw measured configuration string. Hash it and match it to the quote's MRCONFIGID. `tcb_info` can itself be JSON text. | +| `tls_cert_fingerprint` | The direct endpoint TLS binding, when requested. | +| `all_attestations[]` | Model-report entries in this direct response envelope. It can include the root report and is not a fleet inventory. | + +## Verify the report + +1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. +2. Check the client-generated nonce and signer identity against the verified quote. Read report data from the verified quote; require `request_nonce` to match the client nonce and, when `report_data` is present, require it to match the verified quote. Use `signing_algo` to interpret that identity and verify response signatures. +3. Replay `event_log` and require the resulting RTMR3 to equal the RTMR3 in the verified quote. +4. If `tcb_info` is JSON text, decode it to obtain `app_compose`. Hash the raw `app_compose` string without parsing or reserializing it. Require the quote's MRCONFIGID to begin with `01` followed by that SHA-256 hash. +5. When GPU evidence is present, follow [GPU evidence verification](/cloud/verification/reference/quote-nonce-signer#verify-gpu-evidence-when-present). +6. Apply your accepted measurement policy to the verified configuration. +7. If source and build provenance are required, continue with [direct image provenance](/cloud/verification/direct/image-provenance). + +When a response signature is being verified, match both `signing_address` and `signing_algo`. A direct report for another signer is not a substitute, even when the hostname and model name are the same. + +## Load balancing and freshness + +The same direct model hostname can serve more than one instance. Do not assume two independent HTTP requests reached the same instance. Fetch fresh evidence when the signer, certificate, measured configuration, or accepted-attestation-age policy changes. diff --git a/cloud/verification/direct/response-signatures.mdx b/cloud/verification/direct/response-signatures.mdx new file mode 100644 index 0000000..b22dee3 --- /dev/null +++ b/cloud/verification/direct/response-signatures.mdx @@ -0,0 +1,54 @@ +--- +title: "Response Signatures" +description: "Verify an available direct-model response signature against exact bytes and the matching model report." +--- + +Use this flow for a completion sent directly to a model endpoint. When a signature is available, it lets you verify that the exact response bytes came from the attested model environment. + + + A response without an available signature is not a successfully verified response. If response verification is required by your policy, treat it as unverified after a short, bounded retry interval. + + +## Preserve exact request and response bytes + +Build the request body once and retain the exact bytes sent by the client. Retain the raw response bytes before parsing JSON or Server-Sent Events. + +```text +request_hash = SHA-256(exact request-body bytes) +response_hash = SHA-256(exact response-body bytes) +``` + +Do not parse and re-serialize JSON, normalize whitespace or line endings, omit stream delimiters, or reconstruct a streamed response from parsed events. Configure the HTTP client to retain the entity bytes it received; requesting `Accept-Encoding: identity` is the simplest way to avoid transparent decompression changing those bytes. + +## Fetch and verify the signature + +Set `` to the `id` in the completion response whose exact bytes you retained. + +```bash +curl --fail-with-body -G 'https://.completions.near.ai/v1/signature/' \ + --data-urlencode 'signing_algo=' \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' +``` + +Set `` to the explicit algorithm used for the matching model attestation. + +For `ecdsa`, recover the Ethereum EIP-191 signer from `text` and `signature`, then compare it with `signing_address`. For `ed25519`, verify the UTF-8 bytes of `text` using `signing_address` as the public key. Reject unsupported algorithms. + +Then require that `text` is exactly: + +```text +:: +``` + +Use the `model_name` reported by the direct model attestation. A different string, even if it resembles the hostname or a NEAR AI Cloud gateway model ID, does not satisfy this check. If the report has no non-empty `model_name`, this direct response-signature format cannot be verified. + +Use the [manual response-signature helpers](/cloud/verification/reference/response-signature-verification) with the exact request and response bytes, this `model_name`, and the signer from the verified direct model attestation. + +## Match the model signer + +Request or select a direct model attestation whose `signing_address` and `signing_algo` exactly match the signature. Verify its quote, client-generated nonce, signer binding, and required workload policy as described in [direct model attestation](/cloud/verification/direct/model-attestation). + +Do not infer a gateway or model classification from the format of the signed text, and do not substitute a report for a different signer. + +`ecdsa` identities are Ethereum-style signing addresses. `ed25519` identities are Ed25519 public-key bytes encoded as hexadecimal. Always compare the identity together with `signing_algo`. diff --git a/cloud/verification/direct/tls.mdx b/cloud/verification/direct/tls.mdx new file mode 100644 index 0000000..0af2ab6 --- /dev/null +++ b/cloud/verification/direct/tls.mdx @@ -0,0 +1,40 @@ +--- +title: "TLS Connection Binding" +description: "Bind a live direct-model TLS connection to the direct model attestation report." +--- + +Use this flow to establish that the HTTPS connection to a direct model endpoint terminates at the endpoint bound to its model attestation. + + + Obtain the peer certificate and direct attestation report over the **same TLS connection**. Separate connections can reach different instances or observe a rotated certificate, so they cannot establish this connection-specific conclusion. + + +## What this checks + +With `include_tls_fingerprint=true`, the direct report includes `tls_cert_fingerprint`: the SHA-256 hash of the direct endpoint certificate's SubjectPublicKeyInfo (SPKI). The verified model quote binds that fingerprint, the model signing identity, and the nonce. + +This check applies to the direct model connection. It does not verify a connection to `cloud-api.near.ai`. + +## Verification flow + +1. Generate a fresh 32-byte nonce in the client. +2. Open a TLS connection to `https://.completions.near.ai` using the trust configuration required by your application. +3. Read the peer certificate from that connection and calculate `SHA-256(SPKI_DER)`. +4. Reuse the same open connection to request `/v1/attestation/report` with `include_tls_fingerprint=true`, the nonce, and the signing algorithm you require. +5. Verify the direct report's Intel TDX quote, then check the nonce, model signer, and TLS-fingerprint binding in the verified quote. +6. Compare the calculated peer SPKI hash with `tls_cert_fingerprint`. + +A command-line request can retrieve a report, but cannot by itself demonstrate that the report and peer certificate came from one TLS connection. + +This result applies only to that open TLS connection. Send the inference request on it. If the client reconnects, repeat this flow before treating the new connection as bound. + +## Required handling + +| Result | Action when TLS endpoint binding is required | +| --- | --- | +| `tls_cert_fingerprint` is absent | Treat the TLS binding as unavailable. Request a new report with `include_tls_fingerprint=true`. | +| Peer SPKI hash differs | Reject the connection and repeat the flow with one TLS connection. | +| Quote, nonce, or signer binding fails | Reject the report and fetch fresh evidence. | +| Certificate and report came from different connections | Treat the result as inconclusive. | + +Do not use a cached fingerprint as evidence for a new connection. Fetch new evidence after a certificate, signer, measurement, or accepted-attestation-age change. diff --git a/cloud/verification/gateway.mdx b/cloud/verification/gateway.mdx deleted file mode 100644 index 25404c6..0000000 --- a/cloud/verification/gateway.mdx +++ /dev/null @@ -1,166 +0,0 @@ ---- -title: "Gateway Verification" ---- - - - - **Direct Completions — No Gateway Verification Needed** - - If you are using [direct completions endpoints](/cloud/private-inference#direct-completions) (e.g., `https://qwen35-122b.completions.near.ai`), your requests go straight to the model's TEE with no gateway in the path. In that case, only [Model Verification](/cloud/verification/model) is needed — you can skip gateway verification entirely. - -To verify the NEAR AI Cloud private inference gateway is operating in a secure trusted environment, you need to verify the gateway attestation. The gateway attestation proves that the API gateway itself runs in a Trusted Execution Environment (TEE). - -- [Request Gateway Attestation](#request-gateway-attestation) report from NEAR AI Cloud -- [Verify Gateway Attestation](#verifying-gateway-attestation) report using Intel attestation authenticators - - - See an example implementation in the [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) repo. - ---- - -## Request Gateway Attestation - -The gateway attestation can be requested standalone or is included in the response when you request a model attestation. To request gateway attestation standalone, use the following `GET` API endpoint: - -```bash -https://cloud-api.near.ai/v1/attestation/report?signing_algo=ecdsa&nonce={nonce} -``` - -The `signing_algo` parameter specifies the signing algorithm used (`ecdsa` or `ed25519`). The `nonce` parameter is optional but recommended. It should be a randomly generated 64 character hexadecimal string (32 bytes) that ensures attestation freshness and prevents replay attacks. If not provided, the server will generate one for you. - -The endpoint requires an API key (`Authorization: Bearer `); report retrieval is free and never counts against your usage. - -If you want to verify that the HTTPS connection to `cloud-api.near.ai` terminates inside the gateway TEE, add `include_tls_fingerprint=true` and follow [TLS Attestation Verification](/cloud/verification/tls). That opt-in flag binds the gateway's TLS certificate fingerprint into `report_data`; it is disabled by default for compatibility with existing clients. - - - - -```bash -# Generate a random 64-character hex nonce (optional but recommended) -NONCE=$(openssl rand -hex 32) - -curl "https://cloud-api.near.ai/v1/attestation/report?signing_algo=ecdsa&nonce=${NONCE}" \ - -H 'accept: application/json' \ - -H 'Authorization: Bearer ' -``` - - - - -```js -import crypto from 'crypto'; - -// Generate a random 64-character hex nonce (optional but recommended) -const nonce = crypto.randomBytes(32).toString('hex'); - -const response = await fetch( - `https://cloud-api.near.ai/v1/attestation/report?signing_algo=ecdsa&nonce=${nonce}`, - { - headers: { - 'accept': 'application/json', - 'Authorization': `Bearer ${process.env.NEAR_AI_CLOUD_API_KEY}`, - }, - } -); -``` - - - - -```python -import os -import requests -import secrets - -# Generate a random 64-character hex nonce (optional but recommended) -nonce = secrets.token_hex(32) - -response = requests.get( - f'https://cloud-api.near.ai/v1/attestation/report?signing_algo=ecdsa&nonce={nonce}', - headers={ - 'accept': 'application/json', - 'Authorization': f'Bearer {os.environ["NEAR_AI_CLOUD_API_KEY"]}', - } -) -``` - - - - - - ```json - { - "gateway_attestation": { - "request_nonce": "...", \\ The nonce you provided in the request - "intel_quote": "...", \\ Attestation report used to verify w/ Intel TDX - "event_log": [...], \\ TDX event log - "info": { \\ TCB information including Docker compose manifest - "compose": "...", - ... - } - } - } - ``` - - - `gateway_attestation`: Attestation report for the private inference gateway (API gateway) - - `request_nonce`: The nonce you provided in the request - - `intel_quote`: Intel TDX quote for the gateway TEE - - `event_log`: TDX event log - - `info`: TCB information including Docker compose manifest - - - - **Implementation**: This endpoint is defined in the [NEAR AI Cloud API](https://github.com/nearai/cloud-api). - ---- - -## Verifying Gateway Attestation - -Once you have [requested a gateway attestation](#request-gateway-attestation) from NEAR AI Cloud, you can use the returned payload to verify its authenticity. You can verify: - -- **Intel TDX quote**: Verifies TDX quote with [`dcap-qvl`](https://github.com/Phala-Network/dcap-qvl) library -- **TDX report data**: Validates that report data includes the nonce in request -- **Compose manifest**: Displays Docker compose manifest and verifies it matches the mr_config measurement -- **Source Code provenance**: Verifies container image provenance - -### Verify TDX Quote - -You can verify the Intel TDX quote with the value of `intel_quote` from `gateway_attestation` using the [`dcap-qvl`](https://github.com/Phala-Network/dcap-qvl) library. This verifies: - -- The CPU TEE measurements are valid -- The quote is authentic and signed by Intel -- The TEE environment is genuine - -Alternatively, you can verify the Intel TDX quote at [TEE Attestation Explorer](https://proof.t16z.com/). - -### Verify TDX Report Data - -The TDX report data validates that: -- The report data binds the signing address (ECDSA or Ed25519) -- The report data embeds the request nonce - -This ensures cryptographic binding between the signing address and the hardware, and prevents replay attacks through nonce freshness. - -### Verify Compose Manifest - -The attestation response includes Docker compose manifest information in the `gateway_attestation.info` field. To verify: - -1. Extract the Docker compose manifest from the attestation -2. Calculate the SHA-256 hash of the compose manifest -3. Compare it with the `mr_config` measurement from the verified TDX quote -4. Verify they match, proving the exact container configuration - -This ensures the exact Docker compose file is deployed to the TEE environment. - -### Verify Source Code Provenance - -Extract the `nearaidev/cloud-api` container image digests from the Docker compose manifest (matching `@sha256:xxx` patterns) and fetch the source code provenance from Sigstore for each image. This allows you to: - -1. Verify the container images were built from the expected source repository with exact release tag -2. Review the GitHub Actions workflow that built the images -3. Audit the build provenance and supply chain metadata -4. Audit the source code of a given release - -The source code of the [cloud-api gateway](https://github.com/nearai/cloud-api) is reproducible, which means you can build from the source code of a given release and verify that the resulting Docker image digest matches the digest from the attestation (`@sha256:xxx`). - -This ensures the exact version of source code is built into the `nearaidev/cloud-api` Docker image and makes it easy to audit and validate the source code. You can find one [Sigstore link example](https://search.sigstore.dev/?hash=sha256:f75c2a8f1a3d8a36ed6cd7479e848edf0a0e814381d7b83993a703201594bc14) in the [v0.1.7 release of cloud-api](https://github.com/nearai/cloud-api/releases/tag/v0.1.7). diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 9d3ce6d..1a1f179 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -1,57 +1,36 @@ --- title: "Verification" +description: "Choose the verification guide that matches the inference endpoint your application calls." --- -[NEAR AI Cloud](https://cloud.near.ai) operates in Trusted Execution Environments (TEEs) which use cryptographic proofs to verify that your private AI conversations actually happened in secure, isolated environments that are not on compromised systems or with unauthorized access. +Use these guides to verify the evidence returned for your AI requests. Start with the inference endpoint your application calls. -This section will show you step-by-step processes for checking these proofs, validating digital signatures, and confirming that your AI interactions haven't been tampered with. +## Choose your request path - - See an easy-to-follow example implementation in the [NEAR AI Cloud Verification Example](https://github.com/near-examples/nearai-cloud-verification-example) repo. - - A more complete verifier implementation is available in the [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) repo. - - - **Simplified Verification with Direct Completions** - - When using [direct completions endpoints](/cloud/private-inference#direct-completions) (e.g., `https://qwen35-122b.completions.near.ai`), your requests go straight to the model's TEE. This means only **model verification** is needed — there is no gateway in the path, so [gateway verification](/cloud/verification/gateway) can be skipped entirely. - - - **TLS Termination Inside the TEE** - - If you also need to prove that your HTTPS connection terminates inside the same TEE, request attestation with `include_tls_fingerprint=true`. That opt-in flag changes the attested `report_data` from `signing_address || nonce` to `SHA256(signing_address || tls_cert_fingerprint) || nonce`. See [TLS Attestation Verification](/cloud/verification/tls). - -## How NEAR AI Cloud Verification Works - -1. **Secure Key Generation:** When NEAR AI Cloud initializes, it generates a unique cryptographic signing key pair inside the Trusted Execution Environment (TEE). The private key never leaves the secure hardware. - -2. **Hardware Attestation:** The system generates attestation reports that cryptographically prove it's running on genuine NVIDIA H100/H200 hardware in TEE mode within a confidential VM. - -3. **Key Binding:** These attestation reports include the public key from step 1, creating a verifiable link between the secure hardware and the signing capability. - -4. **Message Signing:** Every AI inference request and response is digitally signed using the private key that remains secured within the TEE. + + + Use this when your application sends requests through `cloud-api.near.ai`. Verify the gateway and, when your policy requires it, every returned NEAR model-attestation candidate. + + + Use this when your application connects directly to a model's `completions.near.ai` endpoint. Verify the model environment reported by that endpoint. + + -5. **End-to-End Verification:** You can verify that your AI interactions were genuinely processed in the secure environment by: + + Choose the guide from the inference endpoint in your application configuration. If your application uses both request paths, verify each request with the guide for the endpoint it used. + - - Checking the hardware attestation reports - - Validating the digital signatures on your messages - - Confirming the signing key matches the attested hardware +## What happens next ---- +After you choose a path, its guide walks you through verifying fresh deployment evidence before a completion and, when present, a response signature against its reported signer. Depending on your application's policy, you can also verify the TLS connection and software-image provenance. -## What You Can Verify +## Common verification concepts - - Verify that AI models are running in secure TEE environments using NVIDIA and Intel attestation - - - Verify that the private inference gateway is running in secure TEE environments using Intel attestation - - - Verify that your HTTPS connection terminates inside the attested TEE by binding the TLS certificate into report_data + + Learn how fresh attestation evidence is tied to your request and to a signing identity. - - Verify individual chat messages and responses through cryptographic signatures + + Decide which checks, measurements, and source-provenance requirements your application accepts. diff --git a/cloud/verification/model.mdx b/cloud/verification/model.mdx deleted file mode 100644 index 9dfd65c..0000000 --- a/cloud/verification/model.mdx +++ /dev/null @@ -1,297 +0,0 @@ ---- -title: "Model Verification" ---- - - -To verify a NEAR AI model is operating in a secure trusted environment, there are two main steps: - -- [Request Model Attestation](#request-model-attestation) report from NEAR AI Cloud -- [Verify Model Attestation](#verifying-model-attestation) report using NVIDIA & Intel attestation authenticators - - - See an easy-to-follow example implementation in the [NEAR AI Cloud Verification Example](https://github.com/near-examples/nearai-cloud-verification-example) repo. - - A more complete verifier implementation is available in the [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) repo. - ---- - -## Request Model Attestation - -To request a model attestation from NEAR AI cloud, use one of the following `GET` API endpoints: - -**Via Gateway:** -```bash -https://cloud-api.near.ai/v1/attestation/report?model={model_name}&signing_algo=ecdsa&nonce={nonce} -``` - -**Via Direct Completions:** -```bash -https://{slug}.completions.near.ai/v1/attestation/report?signing_algo=ecdsa&nonce={nonce} -``` - -The `signing_algo` parameter specifies the signing algorithm used (`ecdsa` or `ed25519`). The `nonce` parameter is optional but recommended. It should be a randomly generated 64 character hexadecimal string (32 bytes) that ensures attestation freshness and prevents replay attacks. If not provided, the server will generate one for you. - -The gateway endpoint requires an API key (`Authorization: Bearer `); report retrieval is free and never counts against your usage. The direct completions endpoint is served by the model TEE itself and needs no API key. - -With direct completions, you can also pass `include_tls_fingerprint=true` to bind the TLS certificate fingerprint to the attestation report. This is disabled by default for compatibility with existing clients. Without the flag, `report_data` binds only the signing key and nonce. With the flag, `report_data[0:32]` becomes `SHA256(signing_address || tls_cert_fingerprint)`, which lets you verify that the HTTPS connection terminates inside the model TEE. See [TLS Attestation Verification](/cloud/verification/tls). - - - - -```bash -# Generate a random 64-character hex nonce (optional but recommended) -NONCE=$(openssl rand -hex 32) - -curl "https://cloud-api.near.ai/v1/attestation/report?model=zai-org/GLM-5.1-FP8&signing_algo=ecdsa&nonce=${NONCE}" \ - -H 'accept: application/json' \ - -H 'Authorization: Bearer ' -``` - - - - -```bash -# Generate a random 64-character hex nonce (optional but recommended) -NONCE=$(openssl rand -hex 32) - -# Direct completions — no model parameter needed -curl "https://qwen35-122b.completions.near.ai/v1/attestation/report?signing_algo=ecdsa&nonce=${NONCE}&include_tls_fingerprint=true" \ - -H 'accept: application/json' -``` - - - - -```js -import crypto from 'crypto'; - -const MODEL_NAME = 'zai-org/GLM-5.1-FP8' -// Generate a random 64-character hex nonce (optional but recommended) -const nonce = crypto.randomBytes(32).toString('hex'); - -// Via gateway (requires an API key): -const response = await fetch( - `https://cloud-api.near.ai/v1/attestation/report?model=${MODEL_NAME}&signing_algo=ecdsa&nonce=${nonce}`, - { - headers: { - 'accept': 'application/json', - 'Authorization': `Bearer ${process.env.NEAR_AI_CLOUD_API_KEY}`, - }, - } -); - -// Or via direct completions: -// const response = await fetch( -// `https://qwen35-122b.completions.near.ai/v1/attestation/report?signing_algo=ecdsa&nonce=${nonce}&include_tls_fingerprint=true`, -// { headers: { 'accept': 'application/json' } } -// ); -``` - - - - -```python -import os -import requests -import secrets - -MODEL_NAME = 'zai-org/GLM-5.1-FP8' -# Generate a random 64-character hex nonce (optional but recommended) -nonce = secrets.token_hex(32) - -# Via gateway (requires an API key): -response = requests.get( - f'https://cloud-api.near.ai/v1/attestation/report?model={MODEL_NAME}&signing_algo=ecdsa&nonce={nonce}', - headers={ - 'accept': 'application/json', - 'Authorization': f'Bearer {os.environ["NEAR_AI_CLOUD_API_KEY"]}', - } -) - -# Or via direct completions: -# response = requests.get( -# f'https://qwen35-122b.completions.near.ai/v1/attestation/report?signing_algo=ecdsa&nonce={nonce}&include_tls_fingerprint=true', -# headers={'accept': 'application/json'} -# ) -``` - - - - - - ```json - { - "model_attestations": [ \\ List of all GPU nodes in the network - { - "signing_address": "...", \\ TEE Public Key - "nvidia_payload": "...", \\ Attestation report used to verify w/ NVIDIA - "intel_quote": "..." \\ Attestation report used to verify w/ Intel - } - ] - } - ``` - - - `model_attestations`: List attestations from all GPU nodes as multiple TEE nodes may be used to serve inference requests. - - - `signing_address`: Account address generated inside TEE that will be used to sign the chat response. You can utilize the `signing_address` from `model_attestations` to select the appropriate TEE node for verifying its integrity. - - - `nvidia_payload` and `intel_quote`: Attestation report formatted for NVIDIA TEE and Intel TEE respectively. You can use them to verify the integrity of the TEE. See [Verifying Model Attestation](#verifying-model-attestation) for more details. - - - - **Implementation**: This endpoint is defined in the [NEAR AI Private ML SDK](https://github.com/nearai/private-ml-sdk/blob/a23fa797dfd7e676fba08cba68471b51ac9a13d9/vllm-proxy/src/app/api/v1/openai.py#L170). - -## Verifying Model Attestation - -Once you have [requested a model attestation](#request-model-attestation) from NEAR AI Cloud, you can use the returned payload to verify its authenticity. You can verify: - -- **GPU attestation**: Submit GPU evidence payload to NVIDIA NRAS and verify the nonce matches -- **Intel TDX quote**: Verifies TDX quote with [`dcap-qvl`](https://github.com/Phala-Network/dcap-qvl) library -- **TDX report data**: Validates that report data binds the signing key (ECDSA or Ed25519) and nonce -- **Compose manifest**: Displays Docker compose manifest and verifies it matches the mr_config measurement - -### Verify GPU Attestation - -NVIDIA offers a [Remote Attestation Service](https://docs.nvidia.com/attestation/cloud-services/latest/nras/nras_introduction.html) that allows you to verify that you are using a trusted environment with one of their GPUs. To verify: - -1. Submit the GPU evidence payload (`nvidia_payload`) to NVIDIA NRAS -2. Verify the nonce in the GPU payload matches the request nonce -3. Validate the NVIDIA attestation verdict is PASS - -The `nvidia_payload` contains: -- `nonce` - The nonce from your attestation request -- `arch` - Architecture of the GPU _(HOPPER or BLACKWELL)_ -- `evidence_list` - A list of GPU evidence items, each containing an evidence and a corresponding certificate - -The `evidence_list` contains Base64 encoded data that lists the GPU's: - -- Hardware Identity -- Firmware & Software measurements -- Security configuration state -- Endorsement certificates (Signed measurements from the GPU's unique key) - -The private key of this GPU is how we can securely verify the authenticity. NVIDIA burns this unique private key into each GPU during the manufacturing process and only retains the corresponding public key, which is used to verify the signature of attestations provided to them. - -All of this data is provided to you from the [Model Attestation response](#request-model-attestation) as `nvidia_payload`. - -Simply use this JSON Object with your API call: - -```bash -curl -X POST https://nras.attestation.nvidia.com/v3/attest/gpu \ - -H "accept: application/json" \ - -H "content-type: application/json" \ - -d "" -``` - -See official documentation: https://docs.api.nvidia.com/attestation/reference/attestmultigpu_1 - - - ```json - [ - [ - "JWT", - "eyJraWQiOiJudi1lYXQta2lkLXByb2QtMjAyNTA4MjQxNzI2MzczMzEtMGM4YzM2MzQtY2ZkMC00YmViLWFmNWYtMTE2MzliOWUxOTIyIiwiYWxnIjoiRVMzODQifQ.eyJzdWIiOiJOVklESUEtUExBVEZPUk0tQVRURVNUQVRJT04iLCJ4LW52aWRpYS12ZXIiOiIyLjAiLCJuYmYiOjE3NTYxNjg5MjYsImlzcyI6Imh0dHBzOi8vbnJhcy5hdHRlc3RhdGlvbi5udmlkaWEuY29tIiwieC1udmlkaWEtb3ZlcmFsbC1hdHQtcmVzdWx0Ijp0cnVlLCJzdWJtb2RzIjp7IkdQVS0wIjpbIkRJR0VTVCIsWyJTSEEtMjU2IiwiMDJmYzJmMTg3M2JkZjg5Y2VlNGYzZTQzYzU3ZTE3YzI0ODUxODcwMmQ4ZGZjMzcwNmE3YjdmZTgwMzZlOTNkMCJdXX0sImVhdF9ub25jZSI6IjRkNmUwYzQ5MzIxZDIyZGFhOWJkN2ZjMjIwNWUzODFmOTUwNmMyMGU3N2RkNTA4MmVjZjVlMTI0ZWMwZjQ2MTgiLCJleHAiOjE3NTYxNzI1MjYsImlhdCI6MTc1NjE2ODkyNiwianRpIjoiYzFhM2NkYzktZWUyMi00MmFkLTljZDEtNDRhMTE5OWYyZGVlIn0.199S4bah6SVZpy4lpBvRBc975tmf25gkf_mLDwR9-fwrc_kWYePNxGygTRQUzGbRdbrZOQHXWP0eALUPkJvmwGIV_MVfHRIKaBIRdr1e2_7jEP1-mqkbCmbefimiZN8t" - ], - { - "GPU-0": "eyJraWQiOiJudi1lYXQta2lkLXByb2QtMjAyNTA4MjQxNzI2MzczMzEtMGM4YzM2MzQtY2ZkMC00YmViLWFmNWYtMTE2MzliOWUxOTIyIiwiYWxnIjoiRVMzODQifQ.eyJ4LW52aWRpYS1ncHUtZHJpdmVyLXJpbS1zY2hlbWEtdmFsaWRhdGVkIjp0cnVlLCJpc3MiOiJodHRwczovL25yYXMuYXR0ZXN0YXRpb24ubnZpZGlhLmNvbSIsIngtbnZpZGlhLWdwdS1hdHRlc3RhdGlvbi1yZXBvcnQtY2VydC1jaGFpbi12YWxpZGF0ZWQiOnRydWUsImVhdF9ub25jZSI6IjRkNmUwYzQ5MzIxZDIyZGFhOWJkN2ZjMjIwNWUzODFmOTUwNmMyMGU3N2RkNTA4MmVjZjVlMTI0ZWMwZjQ2MTgiLCJ4LW52aWRpYS1ncHUtdmJpb3MtcmltLXNpZ25hdHVyZS12ZXJpZmllZCI6dHJ1ZSwieC1udmlkaWEtZ3B1LXZiaW9zLXJpbS1mZXRjaGVkIjp0cnVlLCJleHAiOjE3NTYxNzI1MjYsImlhdCI6MTc1NjE2ODkyNiwidWVpZCI6IjY0Mjk2MDE4OTI5ODAwNzUxMTI1MDk1ODAzMDUwMDc0OTE1MjczMDIyMTE0MjQ2OCIsImp0aSI6IjFhMzhjMTAzLWMyODAtNDQyMi1hZDc1LTRkMTA3OTkyMGI2MyIsIngtbnZpZGlhLWdwdS1hdHRlc3RhdGlvbi1yZXBvcnQtbm9uY2UtbWF0Y2giOnRydWUsIngtbnZpZGlhLWdwdS12Ymlvcy1pbmRleC1uby1jb25mbGljdCI6dHJ1ZSwieC1udmlkaWEtZ3B1LXZiaW9zLXJpbS1jZXJ0LXZhbGlkYXRlZCI6dHJ1ZSwic2VjYm9vdCI6dHJ1ZSwieC1udmlkaWEtZ3B1LWF0dGVzdGF0aW9uLXJlcG9ydC1wYXJzZWQiOnRydWUsIngtbnZpZGlhLWdwdS1kcml2ZXItcmltLXNpZ25hdHVyZS12ZXJpZmllZCI6dHJ1ZSwieC1udmlkaWEtZ3B1LWFyY2gtY2hlY2siOnRydWUsIngtbnZpZGlhLWF0dGVzdGF0aW9uLXdhcm5pbmciOm51bGwsIm5iZiI6MTc1NjE2ODkyNiwieC1udmlkaWEtZ3B1LWRyaXZlci12ZXJzaW9uIjoiNTcwLjEzMy4yMCIsIngtbnZpZGlhLWdwdS1kcml2ZXItcmltLW1lYXN1cmVtZW50cy1hdmFpbGFibGUiOnRydWUsIngtbnZpZGlhLWdwdS1hdHRlc3RhdGlvbi1yZXBvcnQtc2lnbmF0dXJlLXZlcmlmaWVkIjp0cnVlLCJod21vZGVsIjoiR0gxMDAgQTAxIEdTUCBCUk9NIiwiZGJnc3RhdCI6ImRpc2FibGVkIiwieC1udmlkaWEtZ3B1LWRyaXZlci1yaW0tZmV0Y2hlZCI6dHJ1ZSwib2VtaWQiOiI1NzAzIiwieC1udmlkaWEtZ3B1LXZiaW9zLXJpbS1zY2hlbWEtdmFsaWRhdGVkIjp0cnVlLCJtZWFzcmVzIjoic3VjY2VzcyIsIngtbnZpZGlhLWdwdS1kcml2ZXItcmltLWNlcnQtdmFsaWRhdGVkIjp0cnVlLCJ4LW52aWRpYS1ncHUtdmJpb3MtdmVyc2lvbiI6Ijk2LjAwLkNGLjAwLjAyIiwieC1udmlkaWEtZ3B1LXZiaW9zLXJpbS1tZWFzdXJlbWVudHMtYXZhaWxhYmxlIjp0cnVlfQ.Zjac1Al0OsYbrXu7lOKDAH7lLNnRU_G2R1UJBnUpvZKL1EE8mjPyy-4sqRvE_d8uZJ4GuhXoy_EonyuUIXESd3sxjY0Eohe9Rtlzatj14iLOdcVrF_eOq12ZHNIYs4Go" - } - ] - ``` - - - - NVIDIA's attestation verification response returns a "Entity Attestation Token" (EAT) encoded as a JSON Web Token (JWT) - - To decode these values, you can use an online tool such as [jwt.io](https://www.jwt.io) or a library such as [Jose](https://www.npmjs.com/package/jose). - - - ```json - - "JWT": - { - "sub": "NVIDIA-PLATFORM-ATTESTATION", - "x-nvidia-ver": "2.0", - "nbf": 1756168926, - "iss": "https://nras.attestation.nvidia.com", - "x-nvidia-overall-att-result": true, - "submods": { - "GPU-0": [ - "DIGEST", - [ - "SHA-256", - "02fc2f1873bdf89cee4f3e43c57e17c248518702d8dfc3706a7b7fe8036e93d0" - ] - ] - }, - "eat_nonce": "4d6e0c49321d22daa9bd7fc2205e381f9506c20e77dd5082ecf5e124ec0f4618", - "exp": 1756172526, - "iat": 1756168926, - "jti": "c1a3cdc9-ee22-42ad-9cd1-44a1199f2dee" - } - - "GPU-0": - { - "x-nvidia-gpu-driver-rim-schema-validated": true, - "iss": "https://nras.attestation.nvidia.com", - "x-nvidia-gpu-attestation-report-cert-chain-validated": true, - "eat_nonce": "4d6e0c49321d22daa9bd7fc2205e381f9506c20e77dd5082ecf5e124ec0f4618", - "x-nvidia-gpu-vbios-rim-signature-verified": true, - "x-nvidia-gpu-vbios-rim-fetched": true, - "exp": 1756172526, - "iat": 1756168926, - "ueid": "642960189298007511250958030500749152730221142468", - "jti": "1a38c103-c280-4422-ad75-4d1079920b63", - "x-nvidia-gpu-attestation-report-nonce-match": true, - "x-nvidia-gpu-vbios-index-no-conflict": true, - "x-nvidia-gpu-vbios-rim-cert-validated": true, - "secboot": true, - "x-nvidia-gpu-attestation-report-parsed": true, - "x-nvidia-gpu-driver-rim-signature-verified": true, - "x-nvidia-gpu-arch-check": true, - "x-nvidia-attestation-warning": null, - "nbf": 1756168926, - "x-nvidia-gpu-driver-version": "570.133.20", - "x-nvidia-gpu-driver-rim-measurements-available": true, - "x-nvidia-gpu-attestation-report-signature-verified": true, - "hwmodel": "GH100 A01 GSP BROM", - "dbgstat": "disabled", - "x-nvidia-gpu-driver-rim-fetched": true, - "oemid": "5703", - "x-nvidia-gpu-vbios-rim-schema-validated": true, - "measres": "success", - "x-nvidia-gpu-driver-rim-cert-validated": true, - "x-nvidia-gpu-vbios-version": "96.00.CF.00.02", - "x-nvidia-gpu-vbios-rim-measurements-available": true - } - - ``` - - -### Verify TDX Quote - -You can verify the Intel TDX quote with the value of `intel_quote` using the [`dcap-qvl`](https://github.com/Phala-Network/dcap-qvl) library. This verifies: - -- The CPU TEE measurements are valid -- The quote is authentic and signed by Intel -- The TEE environment is genuine - -Alternatively, you can verify the Intel TDX quote at [TEE Attestation Explorer](https://proof.t16z.com/). - -### Verify TDX Report Data - -The TDX report data validates that: -- The report data binds the signing address (ECDSA or Ed25519) -- The report data embeds the request nonce - -This ensures cryptographic binding between the signing address and the hardware, and prevents replay attacks through nonce freshness. - -### Verify Compose Manifest - -The attestation response includes Docker compose manifest information in the `info` field. To verify: - -1. Extract the Docker compose manifest from the attestation -2. Calculate the SHA-256 hash of the compose manifest -3. Compare it with the `mr_config` measurement from the verified TDX quote -4. Verify they match, proving the exact container configuration - -This ensures the exact Docker compose file is deployed to the TEE environment. diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx new file mode 100644 index 0000000..01ccdfc --- /dev/null +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -0,0 +1,69 @@ +--- +title: "Quote, Nonce, and Signer Bindings" +description: "Shared rules for reading verified quotes, checking freshness, and matching signing identities." +--- + +Every NEAR verification flow starts with a client-generated nonce and a verified quote. HTTP fields are inputs to compare with the quote. They are not a replacement for quote verification. + +## Use a client-generated nonce + +Generate 32 random bytes in the client and encode them as 64 hexadecimal characters. Retain the value until verification completes. + +```bash +NONCE="$(openssl rand -hex 32)" +``` + +Send this value with the attestation request. Reject a report unless the verified quote contains the same nonce in its request-freshness binding. + +## Find the quote for your path + +| Request path | Quote location | +| --- | --- | +| NEAR AI Cloud gateway | `gateway_attestation.intel_quote` | +| NEAR AI Cloud gateway model evidence (`provider=near`) | Each candidate report in `model_attestations[]` | +| Direct model endpoint | Root `intel_quote` (and any applicable direct report entry) | + +## Verify the signer binding + +For a NEAR-operated report requested with `include_tls_fingerprint=true`, the verified quote binds the signer identity, TLS certificate fingerprint, and nonce. + +`signing_address` is algorithm-specific: + +| `signing_algo` | `signing_address` | +| --- | --- | +| `ecdsa` | An Ethereum-style address derived from the signing key. | +| `ed25519` | Ed25519 public-key bytes encoded as hexadecimal. | + +Always compare both `signing_address` and `signing_algo`. Do not compare identities across algorithms. + +## Read NEAR report data + +For NEAR-operated reports, quote report data is 64 bytes. The final 32 bytes are the client nonce. The first 32 bytes bind the signing identity: + +- Without TLS binding, they are the decoded signing identity, zero-padded on the right to 32 bytes. An ECDSA address occupies 20 bytes; an Ed25519 public key occupies all 32 bytes. +- With TLS binding, they are `SHA-256(decoded signing identity || TLS SPKI SHA-256)`. Compare the quote-bound fingerprint with the peer certificate observed on the same TLS connection. + +This layout applies only to NEAR-operated dstack reports; other providers have their own report-data contracts. + +## Verify GPU evidence when present + +`nvidia_payload`, when present, is a JSON string. Decode it and send the resulting JSON object unchanged to NVIDIA NRAS; do not wrap it in another JSON property. For a NEAR AI Cloud response, save each `model_attestations[]` candidate as a separate report file and run this check for every candidate before accepting it. For a direct response, save its root model report. + +```bash +GPU_PAYLOAD="$(jq -er '.nvidia_payload | fromjson' report.json)" + +printf '%s\n' "$GPU_PAYLOAD" | jq -e --arg nonce "$NONCE" \ + '(.nonce | type == "string") and ((.nonce | ascii_downcase) == ($nonce | ascii_downcase))' + +curl --fail-with-body -sS -X POST \ + 'https://nras.attestation.nvidia.com/v3/attest/gpu' \ + -H 'Accept: application/json' \ + -H 'Content-Type: application/json' \ + --data-binary "$GPU_PAYLOAD" +``` + +NRAS returns a JSON array whose first entry is `["JWT", ""]`. Decode that JWT's payload and require `x-nvidia-overall-att-result` to be boolean `true`. Reject missing or malformed GPU evidence, a nonce mismatch, or any other verdict. This minimal flow relies on the TLS-authenticated NRAS verdict; apply local JWT/EAT validation separately if your policy requires it. Define in advance whether missing GPU evidence is acceptable for your workload policy. + +## Verification SDK + +For a language-specific library, see the [Verification SDK](/cloud/verification/reference/sdk). diff --git a/cloud/verification/reference/response-signature-verification.mdx b/cloud/verification/reference/response-signature-verification.mdx new file mode 100644 index 0000000..ff11b48 --- /dev/null +++ b/cloud/verification/reference/response-signature-verification.mdx @@ -0,0 +1,213 @@ +--- +title: "Response Signature Verification" +description: "Verify a response signature over exact request and response bytes." +--- + +Use these helpers after you have fetched a response signature and verified the applicable preflight attestation. They verify only the signed response: exact-byte hashes, the signature, and the signer match. They do not verify a quote or choose which preflight evidence applies. + +Pass the signature JSON, the exact request and response bytes, the expected signed text for the endpoint, and the signer from the already verified preflight report: + +```text +preflight_signer = { + signing_algo: verified report signing_algo, + signing_address: verified report signing_address +} +``` + +The endpoint pages define the expected text and which preflight report to use: + +- [NEAR AI Cloud Gateway response signatures](/cloud/verification/cloud-api/response-signatures) +- [Direct model response signatures](/cloud/verification/direct/response-signatures) + + + + +Install [`ethers`](https://www.npmjs.com/package/ethers) and [`tweetnacl`](https://www.npmjs.com/package/tweetnacl): + +```bash +npm install ethers tweetnacl +``` + +```javascript +import { Buffer } from 'node:buffer'; +import { createHash, timingSafeEqual } from 'node:crypto'; +import { ethers } from 'ethers'; +import nacl from 'tweetnacl'; + +function sha256(bytes) { + return createHash('sha256').update(bytes).digest('hex'); +} + +// Omit modelName for a Gateway signature. +export function responseSignatureText(requestBytes, responseBytes, modelName) { + const hashes = `${sha256(requestBytes)}:${sha256(responseBytes)}`; + if (modelName === undefined) return hashes; + if (typeof modelName !== 'string' || !modelName) { + throw new Error('modelName must be a non-empty string'); + } + return `${modelName}:${hashes}`; +} + +function requireHex(value, label, expectedBytes) { + if (typeof value !== 'string') throw new Error(`${label} must be hex`); + const hex = value.replace(/^0x/i, ''); + if (!hex || !/^[0-9a-f]*$/i.test(hex) || hex.length % 2) { + throw new Error(`${label} must be hexadecimal`); + } + const bytes = Buffer.from(hex, 'hex'); + if (bytes.length !== expectedBytes) { + throw new Error(`${label} must be ${expectedBytes} bytes`); + } + return bytes; +} + +function signerIdentity(value, label) { + const algorithm = value?.signing_algo; + if (algorithm !== 'ecdsa' && algorithm !== 'ed25519') { + throw new Error(`${label}.signing_algo is unsupported`); + } + return { + algorithm, + address: requireHex( + value.signing_address, + `${label}.signing_address`, + algorithm === 'ecdsa' ? 20 : 32, + ), + }; +} + +function sameBytes(left, right) { + return left.length === right.length && timingSafeEqual(left, right); +} + +// signature is the JSON returned by GET /v1/signature/. +// preflightSigner comes from the already verified report selected by the endpoint. +export function verifyResponseSignature({ signature, expectedText, preflightSigner }) { + if (typeof signature?.text !== 'string' || signature.text !== expectedText) { + throw new Error('signature text does not match the exact request/response bytes'); + } + + const signer = signerIdentity(signature, 'signature'); + const signatureBytes = requireHex( + signature.signature, + 'signature.signature', + signer.algorithm === 'ecdsa' ? 65 : 64, + ); + + if (signer.algorithm === 'ecdsa') { + // ethers.verifyMessage applies the Ethereum EIP-191 personal-sign prefix. + const recovered = requireHex( + ethers.verifyMessage(Buffer.from(signature.text, 'utf8'), ethers.hexlify(signatureBytes)), + 'recovered signer', + 20, + ); + if (!sameBytes(recovered, signer.address)) { + throw new Error('ECDSA signature does not match signing_address'); + } + } else if (!nacl.sign.detached.verify( + Buffer.from(signature.text, 'utf8'), signatureBytes, signer.address, + )) { + throw new Error('Ed25519 signature does not match signing_address'); + } + + const preflight = signerIdentity(preflightSigner, 'preflightSigner'); + if (preflight.algorithm !== signer.algorithm || !sameBytes(preflight.address, signer.address)) { + throw new Error('signature signer does not match verified preflight evidence'); + } +} +``` + + + + +Install [`eth-account`](https://pypi.org/project/eth-account/) and [`PyNaCl`](https://pypi.org/project/PyNaCl/): + +```bash +pip install eth-account pynacl +``` + +```python +import re +from hashlib import sha256 +from hmac import compare_digest +from typing import Optional + +from eth_account import Account +from eth_account.messages import encode_defunct +from nacl.exceptions import BadSignatureError +from nacl.signing import VerifyKey + + +def response_signature_text( + request_bytes: bytes, response_bytes: bytes, model_name: Optional[str] = None +) -> str: + hashes = f'{sha256(request_bytes).hexdigest()}:{sha256(response_bytes).hexdigest()}' + if model_name is None: + return hashes + if not isinstance(model_name, str) or not model_name: + raise ValueError('model_name must be a non-empty string') + return f'{model_name}:{hashes}' + + +def _require_hex(value: object, label: str, expected_bytes: int) -> bytes: + if not isinstance(value, str): + raise ValueError(f'{label} must be hex') + value = value.removeprefix('0x').removeprefix('0X') + if not value or len(value) % 2 or not re.fullmatch(r'[0-9a-fA-F]*', value): + raise ValueError(f'{label} must be hexadecimal') + decoded = bytes.fromhex(value) + if len(decoded) != expected_bytes: + raise ValueError(f'{label} must be {expected_bytes} bytes') + return decoded + + +def _signer_identity(value: dict, label: str) -> tuple[str, bytes]: + algorithm = value.get('signing_algo') + if algorithm not in ('ecdsa', 'ed25519'): + raise ValueError(f'{label}.signing_algo is unsupported') + address = _require_hex( + value.get('signing_address'), + f'{label}.signing_address', + 20 if algorithm == 'ecdsa' else 32, + ) + return algorithm, address + + +# signature is the JSON returned by GET /v1/signature/. +# preflight_signer comes from the already verified report selected by the endpoint. +def verify_response_signature( + signature: dict, expected_text: str, preflight_signer: dict +) -> None: + if signature.get('text') != expected_text: + raise ValueError('signature text does not match the exact request/response bytes') + + algorithm, address = _signer_identity(signature, 'signature') + signature_bytes = _require_hex( + signature.get('signature'), + 'signature.signature', + 65 if algorithm == 'ecdsa' else 64, + ) + + if algorithm == 'ecdsa': + # encode_defunct applies the Ethereum EIP-191 personal-sign prefix. + recovered = Account.recover_message( + encode_defunct(text=signature['text']), + signature=f'0x{signature_bytes.hex()}', + ) + if not compare_digest(_require_hex(recovered, 'recovered signer', 20), address): + raise ValueError('ECDSA signature does not match signing_address') + else: + try: + VerifyKey(address).verify(signature['text'].encode('utf-8'), signature_bytes) + except (BadSignatureError, ValueError) as error: + raise ValueError('Ed25519 signature does not match signing_address') from error + + preflight_algorithm, preflight_address = _signer_identity( + preflight_signer, 'preflight_signer' + ) + if algorithm != preflight_algorithm or not compare_digest(address, preflight_address): + raise ValueError('signature signer does not match verified preflight evidence') +``` + + + diff --git a/cloud/verification/reference/sdk.mdx b/cloud/verification/reference/sdk.mdx new file mode 100644 index 0000000..eb1d43c --- /dev/null +++ b/cloud/verification/reference/sdk.mdx @@ -0,0 +1,64 @@ +--- +title: "Verification SDK" +description: "Use the NEAR AI verification SDK to verify attestation evidence in your application." +--- + +The NEAR AI verification SDK provides language-specific implementations for NEAR AI Cloud gateway attestation, NEAR model evidence returned through the gateway, and response signatures. + +The SDK supports TypeScript, Python, and Rust. + +## Installation + + + + +Requires Node.js 24 or later. + +```bash +npm install verifiable-ai-sdk +``` + + + + +Requires Python 3.12 or later. + +```bash +pip install verifiable-ai-sdk +``` + +Import it as `verifiable_ai_sdk`. + + + + +```bash +cargo add verifiable-ai-sdk +``` + +Import it as `verifiable_ai_sdk`. + + + + +## Verification flow + +For each completion, first use the SDK to verify the NEAR AI Cloud gateway and, when your policy requires model evidence, every returned NEAR model-attestation candidate. Then send the completion with your HTTP client, retaining the exact request and response bytes. Finally, fetch the completion signature and verify it with the matching preflight result. For a `provider_tee` signature, select exactly one verified model result whose signer identity and algorithm match the signature. + +The SDK creates a fresh nonce for every evidence request. It retrieves and verifies evidence, but does not send the completion request or retain its bytes for you. + +Use one explicit `signing_algo` for the Gateway attestation request, model-attestation request, and completion-signature request in the same verification flow. The Gateway attestation and signature endpoints have different defaults. + + + Learn the protocol requirements and trust decisions for gateway and NEAR model evidence. + + +## Examples + +Runnable TypeScript, Python, and Rust examples follow this flow for non-streaming and streaming completions. + + + In TypeScript, import from `verifiable-ai-sdk/node` to bind Gateway evidence to the TLS peer observed for the Gateway attestation request. This does not automatically cover a later inference request. The generic `verifiable-ai-sdk` entry point verifies Gateway evidence without a TLS peer binding. + + +The SDK currently covers the NEAR AI Cloud Gateway flow. For direct model endpoints, image provenance, and E2EE, use the corresponding endpoint guides. diff --git a/cloud/verification/reference/verification-policy.mdx b/cloud/verification/reference/verification-policy.mdx new file mode 100644 index 0000000..0e61dc8 --- /dev/null +++ b/cloud/verification/reference/verification-policy.mdx @@ -0,0 +1,44 @@ +--- +title: "Verification Policy" +description: "Define required evidence, acceptable measurements, provenance requirements, and handling for unavailable verification results." +--- + +Attestation verifies particular cryptographic statements. Your application must decide which statements it requires and what software, hardware, and source provenance it accepts. + +## Define acceptance before verification + +At minimum, specify: + +- acceptable Intel TDX verification status and advisory policy +- rejection of quotes with TDX debug mode enabled +- required signer algorithm and identity matching rules +- whether GPU evidence is required and the accepted verdict +- accepted workload measurements and configuration rules +- trusted image repositories, build identities, workflows, refs, and source revisions +- maximum acceptable evidence age +- whether TLS endpoint binding and response signatures are required + +## Treat required checks as fail-closed + +When a required check is missing, unavailable, malformed, mismatched, or fails verification, the result is unverified. Do not replace it with a report for a different signer, certificate, request, or model instance. + +| Condition | Recommended result | +| --- | --- | +| Quote or nonce mismatch | Reject the evidence and request a fresh report. | +| Quote has TDX debug mode enabled | Reject the evidence. | +| TLS report or peer SPKI unavailable | Treat endpoint binding as unverified. | +| Response signature unavailable after a bounded retry | Treat the response as unverified when signatures are required. | +| Image-provenance record is missing or returns `404` | Treat that digest as unverified when provenance is required. | +| A required NEAR model candidate is unavailable, malformed, or fails verification | Treat the model property as unverified. | + +## Load balancing and rotations + +The same hostname can be served by more than one attested instance. A report is not evidence for a later request merely because it came from the same hostname. + +- For TLS, obtain the peer certificate and report on the same connection. +- For a `provider_tee` response signature, verify and retain every returned model-attestation candidate before the completion. Accept the signature only when its signer identity and algorithm match exactly one verified candidate. A report fetched after the completion cannot replace that preflight check. +- Refresh preflight evidence before a later request after a signer, certificate, configuration, deployment, or policy-age change. + +## What attestation does not establish by itself + +Attestation does not validate the quality of a model response, secure a compromised client device, or establish image provenance without a separate cryptographic provenance check. It also does not turn a reachable build record into a verified provenance statement. diff --git a/cloud/verification/tls.mdx b/cloud/verification/tls.mdx deleted file mode 100644 index 4bb7aea..0000000 --- a/cloud/verification/tls.mdx +++ /dev/null @@ -1,463 +0,0 @@ ---- -title: "TLS Attestation Verification" -description: "How to verify that your HTTPS connection to NEAR AI terminates inside a Trusted Execution Environment using TLS attestation." ---- - - -NEAR AI runs inference models inside **Confidential VMs (CVMs)** — virtual machines backed by Intel TDX hardware that provide a Trusted Execution Environment (TEE). The key property: even the cloud operator cannot see or tamper with what runs inside the TEE. - -TLS attestation proves that the TLS connection used for your HTTPS requests terminates directly **inside the TEE**, ensuring your messages remain encrypted end-to-end. - - - Check our [example](https://github.com/nearai/nearai-cloud-verifier/tree/main) on how to perform TLS attestation verification. - -## What TLS Attestation Proves - -TLS attestation binds the server's **TLS certificate** to the **TEE hardware attestation**: - -1. You request `/v1/attestation/report?include_tls_fingerprint=true`. -2. The TEE computes the certificate's **SPKI hash** (SHA-256 of the SubjectPublicKeyInfo) and binds it into the attested `report_data`. -3. Intel TDX hardware signs this quote — it cannot be forged. -4. You verify the quote, then confirm the live TLS certificate matches the attested hash. - -This proves the TLS private key is held by the TEE. Your HTTPS traffic is end-to-end encrypted all the way to the hardware enclave. - - - **Opt-In Behavior** - - `include_tls_fingerprint` is disabled by default so existing attestation clients keep the original `report_data` layout. Without the flag, `report_data[0:32]` contains the signing address bytes (padded or truncated to 32 bytes). With the flag, `report_data[0:32]` becomes `SHA256(signing_address || tls_cert_fingerprint)`. - -## Trust Model - -You trust: -- **Intel TDX** hardware (CPU attestation) -- **NVIDIA GPU** hardware (GPU attestation) -- **The code** running inside the TEE (verified via compose file hash) - -You do **not** need to trust the cloud operator, network infrastructure, or certificate authorities. - -```mermaid -flowchart TD - A["Intel TDX Hardware"] --> Q["TDX Quote
(signed by CPU)"] - B["NVIDIA GPU Hardware"] --> G["GPU Attestation
(NVIDIA NRAS)"] - Q --> C["mr_config_id

Docker Compose file

Container images"] - Q --> D["report_data[0:32]

SHA256(signing_address || tls_cert_fingerprint)"] - Q --> E["report_data[32:64]

nonce (freshness)"] - D --> F["Live TLS cert match

Your HTTPS connection"] -``` - ---- - -## Prerequisites - -Install the required Python packages: - -```bash -pip install dcap-qvl cryptography requests pyyaml -``` - ---- -## Verifications - -### Discover Available Endpoints - -The `completions.near.ai/endpoints` API lists all available model endpoints and their domains. - -```python -import http.client -import json - -conn = http.client.HTTPSConnection("completions.near.ai") -conn.request("GET", "/endpoints") -resp = conn.getresponse() -endpoints_data = json.loads(resp.read()) -conn.close() - -# Build a lookup: domain -> models -domain_models = {} -for entry in endpoints_data.get("endpoints", []): - domain = entry["domain"] - domain_models[domain] = entry.get("models", []) - print(f"{domain:<45} {', '.join(domain_models[domain])}") - -# Pick an endpoint to verify -TARGET_DOMAIN = "glm-5-1.completions.near.ai" -print(f"\nVerifying: {TARGET_DOMAIN}") -``` - ---- - -### Fetch Attestation + Live SPKI Hash - -Multiple backend CVMs may serve the same domain via load balancing. Making two separate connections — one for attestation, one to check the certificate — could hit different backends and produce a false SPKI mismatch. - -The solution: extract the live certificate **and** make the attestation request over the **same TLS connection**. - - - CA verification is intentionally skipped. The TEE generates its own TLS key pair — it is not CA-signed. Trust comes from the TEE hardware attestation, not from Certificate Authorities. - -```python -import ssl -import secrets -from hashlib import sha256 -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization - -def compute_spki_hash(cert_der: bytes) -> str: - """Compute SHA-256 of a certificate's SubjectPublicKeyInfo DER encoding. - - Hashing only the public key info (not the full certificate) keeps the hash - stable across certificate renewals that reuse the same key. - """ - cert = x509.load_der_x509_certificate(cert_der, default_backend()) - spki_der = cert.public_key().public_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ) - return sha256(spki_der).hexdigest() - -def fetch_attestation_and_spki(hostname, port=443, signing_algo="ecdsa"): - """Fetch attestation report and live TLS cert SPKI hash over one connection.""" - nonce = secrets.token_hex(32) - - # Skip CA verification — trust comes from the TEE hardware attestation - context = ssl.create_default_context() - context.check_hostname = False - context.verify_mode = ssl.CERT_NONE - - conn = http.client.HTTPSConnection(hostname, port, context=context, timeout=60) - conn.connect() - - # Extract the live certificate from this TLS session - cert_der = conn.sock.getpeercert(binary_form=True) - if not cert_der: - conn.close() - raise Exception("Failed to get certificate from server") - live_spki_hash = compute_spki_hash(cert_der) - - # Make the attestation request over the same connection - path = ( - f"/v1/attestation/report" - f"?include_tls_fingerprint=true&nonce={nonce}&signing_algo={signing_algo}" - ) - conn.request("GET", path, headers={"Host": hostname}) - resp = conn.getresponse() - body = resp.read() - conn.close() - - if resp.status != 200: - raise Exception(f"HTTP {resp.status}: {body.decode()}") - - attestation = json.loads(body) - return attestation, live_spki_hash, nonce - -attestation, live_spki_hash, request_nonce = fetch_attestation_and_spki(TARGET_DOMAIN) - -print(f"Request nonce: {request_nonce}") -print(f"Live SPKI hash: {live_spki_hash}") -print(f"TLS fingerprint: {attestation.get('tls_cert_fingerprint')}") -print(f"Model name: {attestation.get('model_name')}") -print(f"Signing address: {attestation['signing_address']}") -print(f"Intel quote length: {len(attestation.get('intel_quote', ''))} hex chars") -print(f"NVIDIA payload: {bool(attestation.get('nvidia_payload'))}") -``` - -Query parameters used in the attestation request: - -| Parameter | Description | -|-----------|-------------| -| `include_tls_fingerprint=true` | Tells the TEE to include its TLS certificate's SPKI hash in the report data | -| `nonce` | A random 32-byte hex string you generate; included in the report to prove freshness | -| `signing_algo` | `ecdsa` (secp256k1) or `ed25519` — which signing key to bind | - ---- - -### Verify the Intel TDX Quote - -The attestation report contains an Intel TDX quote — a hardware-signed data structure that proves what code is running and what data the TEE attests to. Verify it using `dcap-qvl`, which validates the quote against Intel's DCAP (Data Center Attestation Primitives) collateral. - -```python -import dcap_qvl - -intel_quote_bytes = bytes.fromhex(attestation["intel_quote"]) -print(f"Quote size: {len(intel_quote_bytes)} bytes") -print("Verifying against Intel DCAP collateral...") - -result = await dcap_qvl.get_collateral_and_verify(intel_quote_bytes) -result_json = json.loads(result.to_json()) - -print(f"Verification status: {result.status}") -print(f"Advisory IDs: {result.advisory_ids}") - -# Extract key fields from the verified quote -td10 = result_json["report"]["TD10"] -report_data_hex = td10["report_data"] -mr_config_id = td10["mr_config_id"] -``` - -| Status | Meaning | -|--------|---------| -| `UpToDate` | Quote is valid, TCB firmware is current | -| `SWHardeningNeeded` | Quote is valid but has advisories — check `advisory_ids` | -| Any other value | Verification failed | - -Key fields extracted from the verified quote: - -| Field | What it proves | -|-------|----------------| -| `report_data[0:32]` | `SHA256(signing_address ∥ tls_cert_fingerprint)` — binds signing key + TLS cert to this TEE | -| `report_data[32:64]` | The nonce — proves the attestation is fresh, not a replay | -| `mr_config_id` | `"01" + SHA256(app_compose)` — binds the Docker Compose file to this TEE | - ---- - -### Verify Report Data Binds Signing Key + TLS Cert - -Check that the 64-byte `report_data` in the TDX quote contains the expected values. This cryptographically binds the signing key, TLS certificate, and nonce all to the same hardware attestation. - -```python -report_data = bytes.fromhex(report_data_hex.removeprefix("0x")) -signing_algo = attestation.get("signing_algo", "ecdsa").lower() - -if signing_algo == "ecdsa": - signing_address_bytes = bytes.fromhex(attestation["signing_address"].removeprefix("0x")) -else: - signing_address_bytes = bytes.fromhex(attestation["signing_address"]) - -# report_data[0:32] = SHA256(signing_address || tls_cert_fingerprint) -cert_fp_bytes = bytes.fromhex(attestation["tls_cert_fingerprint"]) -expected_hash = sha256(signing_address_bytes + cert_fp_bytes).digest() - -binds_key_and_tls = report_data[:32] == expected_hash -print(f"Signing key + TLS cert bound to TEE: {binds_key_and_tls}") - -# report_data[32:64] = nonce -embeds_nonce = report_data[32:].hex() == request_nonce -print(f"Nonce matches (fresh attestation): {embeds_nonce}") -``` - ---- - -### Verify Live TLS Certificate Matches Attested Fingerprint - -Compare the SPKI hash extracted from the live TLS session (Step 2) against the `tls_cert_fingerprint` from the attestation report. A match proves the TLS connection terminates inside the TEE. - -```python -attested_fingerprint = attestation["tls_cert_fingerprint"] - -print(f"Live SPKI hash: {live_spki_hash}") -print(f"Attested fingerprint: {attested_fingerprint}") - -tls_match = live_spki_hash == attested_fingerprint -if tls_match: - print("\nThe TLS connection terminates inside the TEE.") - print("Your HTTPS traffic is end-to-end encrypted to the hardware enclave.") -else: - print("\nWARNING: TLS certificate mismatch! The connection may not terminate in the TEE.") -``` - ---- - -### Verify Model Name - -The attestation report's `model_name` field is self-reported by the inference proxy running inside the TEE. Since it originates from within the attested enclave, it tells you which model is actually serving your requests. Compare it against the model listed in `/endpoints` for this domain. - -```python -attested_model = attestation.get("model_name") -expected_models = domain_models.get(TARGET_DOMAIN, []) - -print(f"Attested model (from TEE): {attested_model}") -print(f"Expected models (from /endpoints): {expected_models}") - -if attested_model: - model_match = attested_model in expected_models - print(f"\nModel verified: {model_match}") -else: - print("\nModel name not present — older proxy version.") -``` - ---- - -### Verify GPU Attestation - -In addition to CPU-level attestation, the inference GPUs provide hardware attestation via NVIDIA's Remote Attestation Service (NRAS). The GPU evidence is collected inside the TEE and included as `nvidia_payload` in the report. It's bound to the same nonce as the TDX quote, ensuring both attestations are fresh and from the same request. - -```python -import base64 -import requests - -GPU_VERIFIER_API = "https://nras.attestation.nvidia.com/v3/attest/gpu" - -def decode_jwt_payload(jwt_token): - payload_b64 = jwt_token.split(".")[1] - padded = payload_b64 + "=" * ((4 - len(payload_b64) % 4) % 4) - return json.loads(base64.urlsafe_b64decode(padded).decode()) - -nvidia_payload_str = attestation.get("nvidia_payload") -if nvidia_payload_str: - payload = json.loads(nvidia_payload_str) - - gpu_nonce_matches = payload["nonce"].lower() == request_nonce.lower() - print(f"GPU nonce matches request nonce: {gpu_nonce_matches}") - - print("Submitting GPU evidence to NVIDIA NRAS...") - nras_resp = requests.post(GPU_VERIFIER_API, json=payload, timeout=30) - jwt_token = nras_resp.json()[0][1] - verdict = decode_jwt_payload(jwt_token) - overall_result = verdict["x-nvidia-overall-att-result"] - - print(f"NVIDIA attestation verdict: {overall_result}") - if overall_result == "success": - print("GPU hardware attestation verified.") - else: - print(f"WARNING: GPU attestation result is '{overall_result}'") -else: - print("No NVIDIA payload — GPU attestation not available for this endpoint.") -``` - ---- - -### Verify Docker Compose File - -The TDX quote's `mr_config_id` encodes `"01" + SHA256(app_compose_json)`, binding the exact Docker Compose configuration — including pinned container image digests — to the hardware attestation. Verifying this confirms the TEE is running exactly the code defined in that compose file. - -```python -import re -import yaml # pip install pyyaml - -tcb_info = attestation["info"]["tcb_info"] -if isinstance(tcb_info, str): - tcb_info = json.loads(tcb_info) - -app_compose_str = tcb_info.get("app_compose") -if app_compose_str: - app_compose = json.loads(app_compose_str) - docker_compose_yaml = app_compose["docker_compose_file"] - - # List services and their pinned image digests - compose_parsed = yaml.safe_load(docker_compose_yaml) - print("Services in this TEE:") - for svc_name, svc_config in compose_parsed.get("services", {}).items(): - print(f" {svc_name}: {svc_config.get('image', '(no image)')[:80]}") - - # Verify hash matches mr_config_id - compose_hash = sha256(app_compose_str.encode()).hexdigest() - expected_mr_config = "01" + compose_hash - compose_matches = mr_config_id.lower().startswith(expected_mr_config.lower()) - print(f"\nCompose hash matches mr_config_id: {compose_matches}") - if compose_matches: - print("The TEE is running exactly the code defined in this compose file.") - - # Extract pinned image digests for Sigstore checks - digest_pattern = r'@sha256:([0-9a-f]{64})' - unique_digests = list(dict.fromkeys(re.findall(digest_pattern, docker_compose_yaml))) - print(f"\nPinned image digests ({len(unique_digests)}):") - for digest in unique_digests: - print(f" sha256:{digest[:24]}...") -else: - print("No app_compose in tcb_info — cannot verify compose file.") -``` - - - `mr_config_id` may be all zeros on some endpoints during TEE configuration transitions. In that case, skip this check — the TDX quote verification, report data binding, and TLS fingerprint checks above still provide strong guarantees. - -**Cross-reference with GitHub:** The compose files deployed to CVMs come from the public repository [nearai/cvm-compose-files](https://github.com/nearai/cvm-compose-files). You can fetch the file by its tag and compare it byte-for-byte with what the TEE attests to. - -**Sigstore provenance:** Container images are pinned by `@sha256:` digest. Check `https://search.sigstore.dev/?hash=sha256:` for each image to confirm it was built by a traceable GitHub Actions pipeline, not pushed manually. - ---- - -## Caching Verified Certificates for Production - -Running the full attestation flow is thorough but takes several seconds. For production use, cache the verified SPKI hash and check it on every connection as a lightweight trust anchor. - -### The key insight: SPKI hash as trust anchor - -Once you've verified a particular SPKI hash is bound to a TEE via hardware attestation, you can reuse it: -1. Connect to the endpoint via TLS -2. Extract the live certificate's SPKI hash -3. Compare against your cached, previously-verified hash -4. If it matches → you're talking to the same TEE — no full attestation needed - -### Build the cache - -```python -trusted_certs = {} # domain -> {"spki_hash": ..., "model_name": ..., "signing_address": ...} - -for domain in domain_models: - try: - att, spki, nonce = fetch_attestation_and_spki(domain) - tls_fp = att.get("tls_cert_fingerprint") - if tls_fp and spki == tls_fp: - trusted_certs[domain] = { - "spki_hash": spki, - "model_name": att.get("model_name"), - "signing_address": att["signing_address"], - } - print(f"PASS {domain}") - else: - print(f"FAIL {domain}") - except Exception as e: - print(f"ERROR {domain}: {e}") -``` - -### Verify on every connection - -```python -def verified_request(domain, method, path, body=None, headers=None, port=443): - """Make an HTTPS request verified against the cached SPKI hash.""" - if domain not in trusted_certs: - raise Exception(f"No cached SPKI for {domain}. Run full attestation first.") - - cached_spki = trusted_certs[domain]["spki_hash"] - - context = ssl.create_default_context() - context.check_hostname = False - context.verify_mode = ssl.CERT_NONE - - conn = http.client.HTTPSConnection(domain, port, context=context, timeout=30) - conn.connect() - - live_spki = compute_spki_hash(conn.sock.getpeercert(binary_form=True)) - if live_spki != cached_spki: - conn.close() - raise Exception( - f"SPKI mismatch for {domain}! " - f"Certificate may have rotated — re-run full attestation." - ) - - # SPKI matches — this connection terminates in the verified TEE - all_headers = {"Host": domain, **(headers or {})} - conn.request(method, path, body=body, headers=all_headers) - resp = conn.getresponse() - data = resp.read() - conn.close() - return resp.status, json.loads(data) -``` - -**When to re-verify:** Re-run full attestation when the SPKI hash changes (certificate rotated with a new key), when the TEE is redeployed, or when your cached entry expires (a 24-hour TTL is a reasonable default). - ---- - -## Verification Summary - -| Check | What it proves | -|-------|----------------| -| **Intel TDX quote** | Attestation comes from genuine Intel TDX hardware | -| **Report data binding** | Signing key + TLS cert are bound to this specific TEE | -| **Live TLS match** | Your HTTPS connection terminates inside the TEE | -| **Model name** | The TEE is running the model you expect | -| **GPU attestation** | NVIDIA GPUs are in verified confidential computing mode | -| **Compose file hash** | `mr_config_id` matches `SHA256(app_compose)` — exact code verified | -| **GitHub cross-reference** | Compose file matches the public source at `nearai/cvm-compose-files` | -| **Sigstore provenance** | Container images were built by a traceable CI pipeline | - ---- - -## Resources - -- [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) — Reference client that performs TLS certificate and attestation verification -- [Private Inference](/cloud/private-inference) — How TEE isolation protects your data -- [E2EE Chat Completions](/cloud/guides/e2ee-chat-completions) — Defense-in-depth encryption on top of TLS -- [Verification](/cloud/verification) — Verify chat signatures and model attestation diff --git a/docs.json b/docs.json index 3efe7e9..a5bbca3 100644 --- a/docs.json +++ b/docs.json @@ -59,10 +59,36 @@ "icon": "file-check", "root": "cloud/verification/index", "pages": [ - "cloud/verification/model", - "cloud/verification/gateway", - "cloud/verification/tls", - "cloud/verification/chat" + { + "group": "NEAR AI Cloud Gateway", + "root": "cloud/verification/cloud-api/index", + "pages": [ + "cloud/verification/cloud-api/gateway-attestation", + "cloud/verification/cloud-api/model-attestations", + "cloud/verification/cloud-api/tls", + "cloud/verification/cloud-api/response-signatures", + "cloud/verification/cloud-api/image-provenance" + ] + }, + { + "group": "Direct Model Endpoints", + "root": "cloud/verification/direct/index", + "pages": [ + "cloud/verification/direct/model-attestation", + "cloud/verification/direct/tls", + "cloud/verification/direct/response-signatures", + "cloud/verification/direct/image-provenance" + ] + }, + { + "group": "Reference", + "pages": [ + "cloud/verification/reference/sdk", + "cloud/verification/reference/quote-nonce-signer", + "cloud/verification/reference/response-signature-verification", + "cloud/verification/reference/verification-policy" + ] + } ] } ] @@ -131,7 +157,11 @@ { "group": "Responses", "pages": [ - "POST /v1/responses" + "POST /v1/responses", + "GET /v1/responses/{response_id}", + "DELETE /v1/responses/{response_id}", + "POST /v1/responses/{response_id}/cancel", + "GET /v1/responses/{response_id}/input_items" ] }, { @@ -308,6 +338,11 @@ "GET /v1/admin/access-tokens", "POST /v1/admin/access-tokens", "DELETE /v1/admin/access-tokens/{token_id}", + "GET /v1/admin/aml/allowlist", + "POST /v1/admin/aml/allowlist", + "DELETE /v1/admin/aml/allowlist/{account_id}", + "GET /v1/admin/aml/reports", + "PATCH /v1/admin/aml/reports/{report_id}/status", "GET /v1/admin/invitation-email-deliveries", "POST /v1/admin/invitation-email-deliveries/{invitation_id}/resend", "GET /v1/admin/models", @@ -324,6 +359,8 @@ "GET /v1/admin/organizations/{org_id}", "GET /v1/admin/organizations/{org_id}/concurrent-limit", "PATCH /v1/admin/organizations/{org_id}/concurrent-limit", + "GET /v1/admin/organizations/{org_id}/fallback", + "PATCH /v1/admin/organizations/{org_id}/fallback", "PATCH /v1/admin/organizations/{org_id}/limits", "GET /v1/admin/organizations/{org_id}/limits/history", "GET /v1/admin/organizations/{org_id}/members", @@ -342,6 +379,32 @@ "PATCH /v1/admin/services/{id}", "GET /v1/admin/users" ] + }, + { + "group": "Conversations", + "pages": [ + "POST /v1/conversations", + "GET /v1/conversations/{conversation_id}", + "POST /v1/conversations/{conversation_id}", + "DELETE /v1/conversations/{conversation_id}", + "POST /v1/conversations/{conversation_id}/archive", + "DELETE /v1/conversations/{conversation_id}/archive", + "POST /v1/conversations/{conversation_id}/clone", + "GET /v1/conversations/{conversation_id}/items", + "POST /v1/conversations/{conversation_id}/items", + "POST /v1/conversations/{conversation_id}/pin", + "DELETE /v1/conversations/{conversation_id}/pin" + ] + }, + { + "group": "Files", + "pages": [ + "GET /v1/files", + "POST /v1/files", + "GET /v1/files/{file_id}", + "DELETE /v1/files/{file_id}", + "GET /v1/files/{file_id}/content" + ] } ] } @@ -439,6 +502,26 @@ { "source": "/api", "destination": "/api-reference/introduction" + }, + { + "source": "/cloud/verification/chat", + "destination": "/cloud/verification" + }, + { + "source": "/cloud/verification/gateway", + "destination": "/cloud/verification/cloud-api/gateway-attestation" + }, + { + "source": "/cloud/verification/model", + "destination": "/cloud/verification" + }, + { + "source": "/cloud/verification/tls", + "destination": "/cloud/verification" + }, + { + "source": "/cloud/verification/provenance", + "destination": "/cloud/verification" } ] }