From 831a91bd05b14170f853edaaf46e98a2f7470deb Mon Sep 17 00:00:00 2001 From: Coffee Date: Mon, 24 Aug 2026 14:07:42 +0800 Subject: [PATCH 01/36] docs: refresh Cloud verification guides --- cloud/private-inference.mdx | 36 +-- cloud/verification/chat.mdx | 359 ++++------------------ cloud/verification/gateway.mdx | 168 ++--------- cloud/verification/index.mdx | 99 +++--- cloud/verification/model.mdx | 309 ++++--------------- cloud/verification/provenance.mdx | 50 ++++ cloud/verification/tls.mdx | 479 +++--------------------------- docs.json | 3 +- 8 files changed, 310 insertions(+), 1193 deletions(-) create mode 100644 cloud/verification/provenance.mdx diff --git a/cloud/private-inference.mdx b/cloud/private-inference.mdx index 05e01b1..c215ee1 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. Use [TLS attestation](/cloud/verification/tls) 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["Cloud API 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 [TLS attestation flow](/cloud/verification/tls) 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 @@ -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 [TLS attestation](/cloud/verification/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 Cloud API gateway in the request path. Use [TLS attestation](/cloud/verification/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 [model verification](/cloud/verification/model) for the direct endpoint. If response verification is required, also check that a signature is available and valid. --- @@ -215,7 +215,7 @@ 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 [TLS attestation](/cloud/verification/tls) 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. @@ -248,7 +248,7 @@ 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 TLS endpoint binding for a particular HTTPS connection using hardware-backed attestation Add client-side encryption for defense-in-depth protection of your messages diff --git a/cloud/verification/chat.mdx b/cloud/verification/chat.mdx index 9e92f47..38fb96f 100644 --- a/cloud/verification/chat.mdx +++ b/cloud/verification/chat.mdx @@ -1,334 +1,103 @@ --- -title: "Chat Message Verification" -sidebarTitle: "Chat Verification" +title: "Chat Verification" +description: "Verify an available chat-completion signature against the exact request and response bytes." --- +Chat verification checks the integrity of a response for which a signature is available. It combines three pieces of evidence: -You can verify each chat message with NEAR AI Cloud. For this you will need: +1. SHA-256 hashes of the exact request and response bytes; +2. a valid signature over the expected payload; and +3. a matching [model](/cloud/verification/model) or [gateway](/cloud/verification/gateway) attestation for the signer. -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 -``` - - - + + Not every response has an available signature. An unavailable signature is not a successful verification result. If response verification is required by your policy, treat that response as unverified. + -```python -import hashlib -import json +## Preserve the exact bytes -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} -} +Build the request body once and retain the exact UTF-8 bytes that your HTTP client sends. Read and retain the raw response body before parsing it as JSON or Server-Sent Events. -# Convert to JSON string with same formatting as JavaScript -request_body_str = json.dumps(request_body, separators=(',', ':')) +Calculate: -# Calculate SHA-256 hash -hash_obj = hashlib.sha256(request_body_str.encode()) -hash_hex = hash_obj.hexdigest() -print(hash_hex) #2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad +```text +request_hash = SHA-256(exact request-body bytes) +response_hash = SHA-256(exact response-body bytes) ``` - - - ---- - -## Chat Message Response Hash +Do not parse and re-serialize JSON, normalize whitespace or line endings, omit stream delimiters, or reconstruct the response from parsed events. Any of those changes can produce a different 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"} +For a streaming response, capture all bytes delivered by the HTTP response body, including framing such as the final SSE delimiter when present. -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] - -``` +## Fetch the signature -Which hashes to: - -```bash -8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c -``` - -Here is an example of how to get the sha256 hash of your message response payload: +After completion, use the completion ID as `chat_id` and request a signature with the algorithm you want to verify. + - - -```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 +curl --fail-with-body -G 'https://cloud-api.near.ai/v1/signature/' \ + --data-urlencode 'model=' \ + --data-urlencode 'signing_algo=ecdsa' \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' ``` -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 " +curl --fail-with-body -G 'https://.completions.near.ai/v1/signature/' \ + --data-urlencode 'signing_algo=ecdsa' \ + -H 'Accept: 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`: +The returned object contains: - - +| Field | Meaning | +| --- | --- | +| `text` | The payload that was signed. | +| `signature` | The cryptographic signature over `text`. | +| `signing_address` | The algorithm-specific identity that made the signature. | +| `signing_algo` | The algorithm required to verify the signature. | +| `signature_kind` | On a gateway response, identifies whether the model-serving signer or gateway signer produced the signature. | -```js -import { ethers } from 'ethers'; +## Check the signed payload -const text = "Qwen/Qwen3.5-122B-A10B:2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad:8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c"; -const signature = "0xed381e84d059198d1826e44dbbbac9501caaa8f79f913f27578acafa5be852e6103fe34fabd6446d7fde3f5250c0a16e109fe088a562bae08ac13d081a66d0761b"; -const expectedAddress = "0x6525e128afcffebf7eed05d485d7be983cdae934"; +First calculate your `request_hash` and `response_hash`. Then construct the expected payload from `signature_kind`: -// Recover the address from the signature -const recoveredAddress = ethers.verifyMessage(text, signature); +| `signature_kind` | Expected `text` | Match signer to | +| --- | --- | --- | +| `provider_tee` | `::` | A model attestation with the same `signing_address` and `signing_algo`. | +| `gateway` | `:` | A gateway attestation with the same `signing_address` and `signing_algo`. | -// Compare with expected address (case-insensitive) -const isValid = recoveredAddress.toLowerCase() === expectedAddress.toLowerCase(); +For a direct completion signature, use the model-attestation path. For a gateway response whose `signature_kind` is absent or unfamiliar, do not infer its classification from the shape of `text`; treat the signer classification as unavailable unless your endpoint contract supplies it. -console.log("Text:", text); -console.log("Expected address:", expectedAddress); -console.log("Recovered address:", recoveredAddress); -console.log("Signature valid:", isValid); -``` +Reject the signature if `text` does not exactly equal the expected payload. Then verify `signature` locally with a cryptographic library that supports the reported `signing_algo`. - - + + `ecdsa` signatures use an Ethereum-style signing address. `ed25519` signatures use an Ed25519 public key. Use the verification method appropriate to the reported algorithm; do not send prompts, responses, or signatures to a third-party verification website. + -```python -from eth_account import Account -from eth_account.messages import encode_defunct +## Match the signature to attestation -text = "Qwen/Qwen3.5-122B-A10B:2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad:8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c" -signature = "0xed381e84d059198d1826e44dbbbac9501caaa8f79f913f27578acafa5be852e6103fe34fabd6446d7fde3f5250c0a16e109fe088a562bae08ac13d081a66d0761b" -expected_address = "0x6525e128afcffebf7eed05d485d7be983cdae934" +Fetch or select an attestation for the signer reported in the signature. Verify the quote, your client-generated nonce, the signer binding, and the workload policy as described in [model verification](/cloud/verification/model) or [gateway verification](/cloud/verification/gateway). -# Create a message object that can be signed -message = encode_defunct(text=text) +Record the response time and attestation time, and enforce the maximum attestation age your policy requires. A matching report establishes the signer binding for that attestation; your policy determines how close in time it must be to the response. -# Recover the address from the signature -recovered_address = Account.recover_message(message, signature=signature) +## Signature availability and retries -# Compare with expected address (case-insensitive) -is_valid = recovered_address.lower() == expected_address.lower() +The signature can be unavailable immediately after completion or when the response did not complete. Retry for a short, bounded interval, then make a decision according to your policy. -print("Text:", text) -print("Expected address:", expected_address) -print("Recovered address:", recovered_address) -print("Signature valid:", is_valid) -``` +| Result | Recommended handling | +| --- | --- | +| Signature returned | Complete the payload, cryptographic-signature, and attestation checks. | +| Temporary lookup failure | Retry briefly with the same completion ID and signing algorithm. | +| No signature after the retry window | Treat the response as unverified if a signature is required. | +| Signature or signer mismatch | Reject the verification result; do not substitute a different response or attestation. | - - +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for signature-related verification steps. diff --git a/cloud/verification/gateway.mdx b/cloud/verification/gateway.mdx index 25404c6..cfcd3de 100644 --- a/cloud/verification/gateway.mdx +++ b/cloud/verification/gateway.mdx @@ -1,166 +1,52 @@ --- title: "Gateway Verification" +description: "Verify the attestation evidence for requests sent through the NEAR AI Cloud API gateway." --- +Use gateway verification when your request is sent to `https://cloud-api.near.ai`. It verifies evidence for the gateway request path. It does not replace [model verification](/cloud/verification/model) when you also need to verify the environment that served the model. - **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. + A [direct completions endpoint](/cloud/private-inference#direct-completions) connects to the model endpoint without the Cloud API gateway in the request path. Start with [model verification](/cloud/verification/model) for that flow. ---- - -## 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. +## Request a fresh gateway report -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. - - - +Generate a new 32-byte nonce in your client for each verification attempt. Encode it as 64 hexadecimal characters. ```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 ' -``` - - - +NONCE="$(openssl rand -hex 32)" -```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"]}', - } -) +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 ' ``` - - - - - ```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 +Add `model=` when you also want the request to include model attestation evidence. Add `include_tls_fingerprint=true` when you need to verify the HTTPS connection; see [TLS attestation](/cloud/verification/tls). -The TDX report data validates that: -- The report data binds the signing address (ECDSA or Ed25519) -- The report data embeds the request nonce +## Verify the gateway evidence -This ensures cryptographic binding between the signing address and the hardware, and prevents replay attacks through nonce freshness. +Verify the Intel TDX quote before trusting report fields supplied over HTTP. Then check the following: -### Verify Compose Manifest +1. The verified quote satisfies your Intel DCAP and advisory policy. +2. The quote's report data contains the nonce generated by your client and binds the reported gateway signing identity. +3. The signing identity is paired with its reported `signing_algo`. +4. The measured configuration satisfies the policy your application accepts. +5. If you require source and build provenance, verify it for each image digest in the measured configuration. See [image provenance](/cloud/verification/provenance). -The attestation response includes Docker compose manifest information in the `gateway_attestation.info` field. To verify: +When the report includes `gateway_attestation`, use that entry for gateway verification. A request that includes a model can also include `model_attestations`; verify those separately when model evidence is required. -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 +## Verify responses signed by the gateway -This ensures the exact Docker compose file is deployed to the TEE environment. +When a response signature reports `signature_kind: "gateway"`, verify its signature against the gateway attestation whose `signing_address` and `signing_algo` match the signature. Then verify that the signed payload contains the SHA-256 hashes of the exact request and response bytes you observed. -### Verify Source Code Provenance +Follow [chat verification](/cloud/verification/chat) for the full response-signature flow. -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: +## Freshness and failure handling -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 nonce proves freshness only when your client created and retained it. Do not accept a report merely because an HTTP field echoes a nonce; verify the binding inside the validated quote. -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`). +An unavailable report, failed quote, nonce mismatch, identity mismatch, or policy mismatch is not an equivalent result. If your application requires that property, mark the verification as failed or unverified and fetch new evidence rather than reusing a report from a different instance. -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). +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) includes implementation examples for individual verification steps. diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 9d3ce6d..35ff5b5 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -1,57 +1,80 @@ --- title: "Verification" +description: "Verify NEAR AI Cloud attestation reports, TLS endpoints, signed chat responses, and deployed image provenance." --- -[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. +Verification lets you independently check evidence for a specific NEAR AI Cloud request path. Each check answers a different question: whether an attestation is fresh, whether a TLS connection reaches the attested endpoint, whether a response was signed by an attested signer, or whether deployed images have the provenance your policy requires. -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. +Start with the result you need to verify. - - See an easy-to-follow example implementation in the [NEAR AI Cloud Verification Example](https://github.com/near-examples/nearai-cloud-verification-example) repo. + + + Verify a model attestation, including its Intel TDX quote, nonce, signer, GPU evidence, and measured configuration. + + + Verify the Cloud API gateway when requests are sent through cloud-api.near.ai. + + + Verify that one HTTPS connection terminates at the endpoint named by its attestation. + + + Verify the integrity and signer of a chat completion. + + + Apply a source and build-provenance policy to every image pinned in an attested deployment. + + - 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** +## Choose the checks you need - 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** +| Goal | Required evidence | +| --- | --- | +| Verify a model environment | A fresh model attestation, a verified Intel TDX quote, the request nonce, signer binding, and GPU evidence when present. | +| Verify the gateway | A fresh gateway attestation and a verified Intel TDX quote. Verify the model separately when you also need evidence for model execution. | +| Verify an HTTPS connection | A TLS-bound attestation and the live certificate's SPKI hash from the **same TLS connection**. | +| Verify a chat response | The exact request and response bytes, a valid signature, and an attestation for the signature's signer. | +| Verify deployed software provenance | A verified compose measurement plus a provenance decision for every image digest in that compose file. | + +## Recommended verification flow - 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). +For a production integration, use the following order: + +1. Generate a fresh, random 32-byte nonce in your client. Send it as 64 hexadecimal characters with the attestation request. +2. Request the attestation for the endpoint and model you are using. When you need TLS endpoint verification, set `include_tls_fingerprint=true`. +3. Verify the Intel TDX quote with an Intel DCAP verifier. Read `report_data` and measurements from the **verified quote**, not from unverified response fields. +4. Check that the verified quote binds your nonce and the reported signing identity. Verify NVIDIA GPU evidence when the report contains it. +5. Apply your workload policy: inspect the measured compose configuration and, when required, verify provenance for every pinned image digest. +6. For a chat response, verify the signature over the exact bytes you sent and received, then match its signer to the appropriate attestation. + + + The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for individual checks. Treat any example as a starting point for your own policy: your application should fail verification when a check it requires is missing or does not pass. -## 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. +## Direct completions and the gateway -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. +Requests sent to a [direct completions endpoint](/cloud/private-inference#direct-completions) go directly to the model endpoint. In that path, start with [model verification](/cloud/verification/model); gateway verification is not required for the direct request path. -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. +Requests sent to `https://cloud-api.near.ai` pass through the Cloud API gateway. Verify the gateway when you need evidence for that request path, and verify the model when you need evidence for the model environment. -4. **Message Signing:** Every AI inference request and response is digitally signed using the private key that remains secured within the TEE. +## Important details for load-balanced endpoints -5. **End-to-End Verification:** You can verify that your AI interactions were genuinely processed in the secure environment by: +An endpoint can be served by more than one attested instance. Do not assume that two independent requests reached the same instance. - - Checking the hardware attestation reports - - Validating the digital signatures on your messages - - Confirming the signing key matches the attested hardware +- When verifying a chat response, use the response's `signing_address` and `signing_algo` to select the matching attestation. +- When verifying TLS, obtain the live certificate and the attestation over the same TLS connection. Fetching them over separate connections can produce a false mismatch after load balancing or certificate rotation. +- When an instance is redeployed or its certificate changes, fetch a new attestation before accepting the new evidence. ---- +## What attestation does and does not establish -## What You Can Verify +Attestation verifies specific cryptographic evidence about a runtime and its measured configuration. It does not, by itself, validate the quality of a model response, secure a compromised client device, or establish source provenance for an image whose build evidence you have not verified. - - - 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 - - - Verify individual chat messages and responses through cryptographic signatures - - +Use the individual guides in this section to decide which guarantees your application requires. + +## Common verification failures + +| Symptom | What to do | +| --- | --- | +| Nonce mismatch | Reject the report. Generate a new nonce in the client and request a new attestation. | +| TLS fingerprint mismatch | Reject the connection. Repeat the TLS flow using one connection for both the certificate and attestation request. | +| Signature is unavailable | Do not mark the response as verified. Retry briefly after completion; if it remains unavailable, retain the response ID and request ID for support. | +| Image provenance is unavailable | Treat the image as unverified under a provenance-required policy. Fetch a fresh attestation after a deployment change, then repeat the provenance check. | diff --git a/cloud/verification/model.mdx b/cloud/verification/model.mdx index 9dfd65c..44aaec1 100644 --- a/cloud/verification/model.mdx +++ b/cloud/verification/model.mdx @@ -1,297 +1,88 @@ --- title: "Model Verification" +description: "Verify the attestation evidence for a NEAR AI Cloud model endpoint." --- +Use model verification to check the environment serving a model. It is the required attestation check for [direct completions](/cloud/private-inference#direct-completions), and it complements [gateway verification](/cloud/verification/gateway) when you call `cloud-api.near.ai`. -To verify a NEAR AI model is operating in a secure trusted environment, there are two main steps: +Model verification does not prove a particular response by itself. To verify a response, also follow [chat verification](/cloud/verification/chat). -- [Request Model Attestation](#request-model-attestation) report from NEAR AI Cloud -- [Verify Model Attestation](#verifying-model-attestation) report using NVIDIA & Intel attestation authenticators +## Request a fresh report - - 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). +Generate the nonce in your client. Use 32 random bytes encoded as 64 hexadecimal characters, and retain it until verification has completed. - + ```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 ' +NONCE="$(openssl rand -hex 32)" + +curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ + --data-urlencode 'model=' \ + --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode "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' } } -// ); -``` - - - +NONCE="$(openssl rand -hex 32)" -```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'} -# ) +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' \ + -H 'Authorization: Bearer ' ``` - + - - ```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 - } - ] - } - ``` +Use the same `signing_algo` as the signature you plan to verify. Supported values are `ecdsa` and `ed25519`. If you need to bind the report to the HTTPS endpoint, add `include_tls_fingerprint=true` and follow the [TLS attestation guide](/cloud/verification/tls). - - `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. - +The Cloud API request accepts a `model` parameter. A direct endpoint already identifies the model through its hostname. If you are verifying a response signature and the endpoint supports it, include its `signing_address` and `signing_algo` to request evidence for that signer. - **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). + A report can contain evidence from more than one serving instance. Do not assume a fixed number of entries. Select the entry that matches the signer and algorithm of the response you are verifying. -## 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 +## Verify the report - "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" - } +Verify the Intel TDX quote before trusting fields copied into the HTTP response. Your verifier should read the report data and measurements from the verified quote. - "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 - } +For each applicable model-attestation entry, check the following: - ``` - - -### Verify TDX Quote +1. Verify the Intel TDX quote using an Intel DCAP verifier and apply your policy for its result and advisories. +2. Verify that the quote's report data contains the nonce your client generated and binds the reported signing identity. +3. Match `signing_address` and `signing_algo` to the identity that signed the response, when you are verifying a response. +4. When GPU evidence is present, verify it with the applicable NVIDIA verifier and ensure it is bound to the same nonce. +5. Check the measured configuration against the policy your application accepts. If you require source and build provenance, continue with [image provenance verification](/cloud/verification/provenance). -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 response's `request_nonce` and `report_data` fields are useful for diagnostics, but the verified quote is the cryptographic source of truth. -- The CPU TEE measurements are valid -- The quote is authentic and signed by Intel -- The TEE environment is genuine +## Signing identities -Alternatively, you can verify the Intel TDX quote at [TEE Attestation Explorer](https://proof.t16z.com/). +`signing_address` is an algorithm-specific identity, not always a public key in the same format: -### Verify TDX Report Data +| `signing_algo` | `signing_address` | +| --- | --- | +| `ecdsa` | An Ethereum-style address derived from the signing key. | +| `ed25519` | The Ed25519 public-key bytes, encoded as hexadecimal. | -The TDX report data validates that: -- The report data binds the signing address (ECDSA or Ed25519) -- The report data embeds the request nonce +Always compare both the identity and `signing_algo`. Do not compare identities across algorithms. -This ensures cryptographic binding between the signing address and the hardware, and prevents replay attacks through nonce freshness. +## Handling rotations and load balancing -### Verify Compose Manifest +An endpoint may be served by several instances. A report fetched before or after a completion can therefore describe a different signer. -The attestation response includes Docker compose manifest information in the `info` field. To verify: +- Fetch a new report when the signer, certificate, or measured configuration changes. +- For a signed response, request or select the attestation entry that matches its signer. +- Record the report time and enforce the maximum attestation age your policy permits. +- When a required check cannot be completed, treat that result as unverified rather than substituting evidence from a different instance. -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 +## Reference implementation -This ensures the exact Docker compose file is deployed to the TEE environment. +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) provides implementation examples for quote, GPU, TLS, signature, and image-digest inspection. Integrate the checks and acceptance policy appropriate for your application. diff --git a/cloud/verification/provenance.mdx b/cloud/verification/provenance.mdx new file mode 100644 index 0000000..e303e0a --- /dev/null +++ b/cloud/verification/provenance.mdx @@ -0,0 +1,50 @@ +--- +title: "Image Provenance" +description: "Verify source and build provenance for images named in an attested deployment." +--- + +An attestation can establish that a measured deployment configuration was running. Image provenance answers a separate question: whether each image in that configuration was built from the source and build process your policy accepts. + +Use both checks when your application requires a source-to-runtime trust chain. + +## Verification flow + +1. Verify the Intel TDX quote. Obtain the compose configuration text from the report, calculate its hash, and compare it with the configuration measurement in the verified quote. Only then treat that text as the attested configuration. +2. Extract every container image pinned by immutable digest (`@sha256:…`) from the matched configuration. +3. For each digest, retrieve provenance evidence from the relevant build-provenance system. +4. Cryptographically verify the provenance statement and confirm that its subject digest exactly matches the image digest in the attested configuration. +5. Apply your policy to the verified statement, such as the expected repository, build identity, workflow, ref, and source revision. +6. Accept the deployment only when every image required by your policy passes the checks. + + + An image tag is not sufficient evidence. A search result or an HTTP `200` response only locates a possible record; it does not cryptographically verify the statement, its signer, or its subject digest. + + +## Define a provenance policy + +Your policy should specify the identities and inputs you accept before you evaluate a report. At minimum, define: + +- the expected source repository or repositories; +- the accepted build identity and workflow; +- the allowed source branch, tag, or commit policy; and +- whether provenance is required for every image or only selected images. + +Pin every evaluated image by digest. If a configuration references an image without an immutable digest, it cannot satisfy a digest-matching provenance check. + +## When a provenance record is missing + +A missing record, including an HTTP `404`, means provenance for the reported image digest could not be verified. It does not by itself identify a security incident. + +Deployments can change. Fetch a fresh attestation report and repeat the verification flow. If the current report still references a digest without the provenance required by your policy, treat that image as unverified. + +When requesting help, include the endpoint, time, report identifier, image name, and full digest. Do not include API keys, prompts, or response content. + +## Relationship to attestation + +| Check | What it establishes | +| --- | --- | +| Quote and configuration measurement | The configuration text you matched is bound to a measurement in the verified quote. | +| Image provenance | A particular image digest satisfies your source and build policy. | +| Both together | The measured runtime configuration uses images whose provenance satisfies your policy. | + +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) includes reference code for inspecting image digests. Use a provenance verifier that validates the cryptographic evidence required by your policy. diff --git a/cloud/verification/tls.mdx b/cloud/verification/tls.mdx index 4bb7aea..b8be96a 100644 --- a/cloud/verification/tls.mdx +++ b/cloud/verification/tls.mdx @@ -1,463 +1,60 @@ --- -title: "TLS Attestation Verification" -description: "How to verify that your HTTPS connection to NEAR AI terminates inside a Trusted Execution Environment using TLS attestation." +title: "TLS Attestation" +description: "Verify that a specific HTTPS connection terminates at the endpoint bound to an attestation report." --- +TLS attestation links a live HTTPS connection to the signing identity in a verified attestation. It is the check to use when your policy requires evidence that the TLS endpoint for a particular connection is the attested endpoint. -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. + + Obtain the peer certificate and the attestation report over the **same TLS connection**. Fetching them over separate connections can reach different instances or observe a rotated certificate, especially for load-balanced endpoints. + -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. +## What the check establishes - - Check our [example](https://github.com/nearai/nearai-cloud-verifier/tree/main) on how to perform TLS attestation verification. - -## What TLS Attestation Proves +When you request an attestation with `include_tls_fingerprint=true`, the report includes `tls_cert_fingerprint`: the SHA-256 hash of the certificate's SubjectPublicKeyInfo (SPKI). The verified TDX quote binds that fingerprint, the signing identity, and your nonce. -TLS attestation binds the server's **TLS certificate** to the **TEE hardware attestation**: +After you compare the live peer certificate's SPKI hash with `tls_cert_fingerprint`, the result establishes that the TLS connection you observed is bound to that verified 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. +Your application must make an explicit certificate-validation decision. TLS attestation becomes a security conclusion only after the quote, binding, and same-connection checks pass; an unverified report is not a reason to relax TLS validation. -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. +## Verification flow - - **Opt-In Behavior** +1. Generate a fresh, random 32-byte nonce in your client and encode it as 64 hexadecimal characters. +2. Open a TLS connection using the trust configuration appropriate for your application. +3. From that connection, read the peer certificate and calculate `SHA-256(SPKI_DER)`. +4. Reuse the **same open connection** to request `/v1/attestation/report` with `include_tls_fingerprint=true`, your nonce, and the signing algorithm you require. +5. Verify the Intel TDX quote and verify that its report data binds the nonce, signing identity, and reported TLS fingerprint. +6. Compare the SPKI hash you calculated from the peer certificate with `tls_cert_fingerprint` from the report. +7. Apply the same measurement and provenance policy you use for [model](/cloud/verification/model) or [gateway](/cloud/verification/gateway) verification. - `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 +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) includes implementation examples for TLS-related verification steps. Independently ensure that your client keeps the certificate read and report request on one connection. -You trust: -- **Intel TDX** hardware (CPU attestation) -- **NVIDIA GPU** hardware (GPU attestation) -- **The code** running inside the TEE (verified via compose file hash) +## Request parameters -You do **not** need to trust the cloud operator, network infrastructure, or certificate authorities. +Use the normal attestation endpoint for your request path, adding `include_tls_fingerprint=true`: -```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"] +```text +GET /v1/attestation/report?signing_algo=ecdsa&nonce=&include_tls_fingerprint=true ``` ---- - -## 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 +For the Cloud API gateway, include your Bearer API key and, when needed, `model=`. For a direct endpoint, call that endpoint's `/v1/attestation/report` route. A command-line request can retrieve a report, but it does not by itself prove the same-connection property unless your client also reads the peer certificate from that very connection. -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. +## Load balancing and certificate rotation -```python -attested_fingerprint = attestation["tls_cert_fingerprint"] +Do not use a cached fingerprint as evidence for a new connection. It can be useful as an operational hint, but it is not a fresh attestation result. -print(f"Live SPKI hash: {live_spki_hash}") -print(f"Attested fingerprint: {attested_fingerprint}") +Request new evidence when any of the following changes: -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 | - ---- +- the live certificate's public key; +- the report's signing identity or signing algorithm; +- the measured configuration; or +- your policy's allowed attestation age. -## Resources +## Failure handling -- [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 +| Result | Required action when TLS endpoint verification is required | +| --- | --- | +| No `tls_cert_fingerprint` | Treat the TLS binding as unavailable; request a new report with `include_tls_fingerprint=true`. | +| Peer SPKI hash differs from the report | Reject the connection and repeat the flow with one TLS connection. | +| Quote, nonce, or signing-identity check fails | Reject the report and fetch fresh evidence. | +| Certificate read and report fetch used different connections | Treat the result as inconclusive, not verified. | diff --git a/docs.json b/docs.json index 257f4de..756abb9 100644 --- a/docs.json +++ b/docs.json @@ -70,7 +70,8 @@ "cloud/verification/model", "cloud/verification/gateway", "cloud/verification/tls", - "cloud/verification/chat" + "cloud/verification/chat", + "cloud/verification/provenance" ] } ] From eaa281a449b84cd0e435f143e9705d232bc80787 Mon Sep 17 00:00:00 2001 From: Coffee Date: Mon, 24 Aug 2026 15:10:17 +0800 Subject: [PATCH 02/36] Clarify Compose Manager provenance checks --- cloud/verification/provenance.mdx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cloud/verification/provenance.mdx b/cloud/verification/provenance.mdx index e303e0a..c52bf6e 100644 --- a/cloud/verification/provenance.mdx +++ b/cloud/verification/provenance.mdx @@ -20,6 +20,12 @@ Use both checks when your application requires a source-to-runtime trust chain. An image tag is not sufficient evidence. A search result or an HTTP `200` response only locates a possible record; it does not cryptographically verify the statement, its signer, or its subject digest. +## Compose Manager images + +When the attested configuration contains platform images such as `compose-manager` or `compose-manager-launcher`, evaluate their exact digests the same way as every other image in the configuration. First bind the configuration to the verified quote, then verify build provenance for each digest under your policy. + +An integrity check for a reported action list is a separate check. It does not establish the provenance of the image that ran the action, and it does not replace quote or image-provenance verification. + ## Define a provenance policy Your policy should specify the identities and inputs you accept before you evaluate a report. At minimum, define: @@ -47,4 +53,4 @@ When requesting help, include the endpoint, time, report identifier, image name, | Image provenance | A particular image digest satisfies your source and build policy. | | Both together | The measured runtime configuration uses images whose provenance satisfies your policy. | -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) includes reference code for inspecting image digests. Use a provenance verifier that validates the cryptographic evidence required by your policy. +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) includes reference code for locating selected image digests in a configuration. Use a provenance verifier that validates the cryptographic evidence required by your policy. From f113c405a7713ca1cc49b4d621e86f6e09cd8175 Mon Sep 17 00:00:00 2001 From: Coffee Date: Mon, 24 Aug 2026 17:49:00 +0800 Subject: [PATCH 03/36] docs: split Cloud API and direct verification flows --- cloud/guides/e2ee-chat-completions.mdx | 11 +- cloud/private-inference.mdx | 22 ++-- cloud/verification/chat.mdx | 110 ++---------------- .../cloud-api/gateway-attestation.mdx | 47 ++++++++ .../cloud-api/image-provenance.mdx | 34 ++++++ cloud/verification/cloud-api/index.mdx | 71 +++++++++++ .../cloud-api/model-attestations.mdx | 48 ++++++++ .../cloud-api/response-signatures.mdx | 47 ++++++++ cloud/verification/cloud-api/tls.mdx | 38 ++++++ .../direct/deployment-control-evidence.mdx | 29 +++++ .../verification/direct/image-provenance.mdx | 28 +++++ cloud/verification/direct/index.mdx | 52 +++++++++ .../verification/direct/model-attestation.mdx | 52 +++++++++ .../direct/response-signatures.mdx | 46 ++++++++ cloud/verification/direct/tls.mdx | 38 ++++++ cloud/verification/gateway.mdx | 54 +-------- cloud/verification/index.mdx | 91 +++++---------- cloud/verification/model.mdx | 95 ++------------- cloud/verification/provenance.mdx | 63 ++-------- .../reference/quote-nonce-signer.mdx | 47 ++++++++ .../reference/verification-policy.mdx | 42 +++++++ cloud/verification/tls.mdx | 67 ++--------- docs.json | 34 +++++- 23 files changed, 743 insertions(+), 423 deletions(-) create mode 100644 cloud/verification/cloud-api/gateway-attestation.mdx create mode 100644 cloud/verification/cloud-api/image-provenance.mdx create mode 100644 cloud/verification/cloud-api/index.mdx create mode 100644 cloud/verification/cloud-api/model-attestations.mdx create mode 100644 cloud/verification/cloud-api/response-signatures.mdx create mode 100644 cloud/verification/cloud-api/tls.mdx create mode 100644 cloud/verification/direct/deployment-control-evidence.mdx create mode 100644 cloud/verification/direct/image-provenance.mdx create mode 100644 cloud/verification/direct/index.mdx create mode 100644 cloud/verification/direct/model-attestation.mdx create mode 100644 cloud/verification/direct/response-signatures.mdx create mode 100644 cloud/verification/direct/tls.mdx create mode 100644 cloud/verification/reference/quote-nonce-signer.mdx create mode 100644 cloud/verification/reference/verification-policy.mdx diff --git a/cloud/guides/e2ee-chat-completions.mdx b/cloud/guides/e2ee-chat-completions.mdx index bb668c5..59f0d89 100644 --- a/cloud/guides/e2ee-chat-completions.mdx +++ b/cloud/guides/e2ee-chat-completions.mdx @@ -222,7 +222,10 @@ response = requests.get( # ) attestation = response.json() +# Cloud API: model_public_key_hex = attestation["model_attestations"][0]["signing_public_key"] +# Direct completions use the root model report instead: +# model_public_key_hex = attestation["signing_public_key"] print(f"Model Ed25519 public key: {model_public_key_hex}") ``` @@ -251,7 +254,7 @@ console.log('Model Ed25519 public key:', modelPublicKeyHex); -The response contains `model_attestations` with the model's `signing_public_key` in hex format. +For Cloud API, the model key is in `model_attestations[0].signing_public_key`. For a direct completions endpoint, it is in the root report's `signing_public_key`. --- @@ -691,7 +694,7 @@ E2EE is supported on the **Chat Completions API** (`/v1/chat/completions`), **Co ### 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. +You can verify the model's public key is authentic by checking the attestation report against hardware attestation. Choose the matching endpoint flow in [Verification](/cloud/verification). --- @@ -810,5 +813,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 Cloud API 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 c215ee1..b752a17 100644 --- a/cloud/private-inference.mdx +++ b/cloud/private-inference.mdx @@ -54,7 +54,7 @@ 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. Use [TLS attestation](/cloud/verification/tls) when your application requires evidence for the TLS endpoint of a particular connection: +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: @@ -77,7 +77,7 @@ flowchart LR style C fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff ``` -**Key insight:** TLS protects traffic in transit. When the [TLS attestation flow](/cloud/verification/tls) passes for a particular connection, it binds that connection's endpoint to verified attestation evidence. +**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: @@ -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 [Cloud API 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 protects data in transit; use [TLS attestation](/cloud/verification/tls) when your application needs to verify the endpoint for a particular connection. + You send chat completion requests via HTTPS to the LLM Gateway. TLS protects data in transit; use [Cloud API 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. @@ -168,7 +168,7 @@ When you make a request to NEAR AI Cloud, your data flows through a secure pipel **Via Direct Completions (`{slug}.completions.near.ai`):** 1. **Direct Request:** - You send chat completion requests via HTTPS directly to the model's subdomain, with no intermediate Cloud API gateway in the request path. Use [TLS attestation](/cloud/verification/tls) when endpoint verification is required. + You send chat completion requests via HTTPS directly to the model's subdomain, with no intermediate Cloud API 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. @@ -177,7 +177,7 @@ When you make a request to NEAR AI Cloud, your data flows through a secure pipel Attestation reports provide environment evidence; supported flows can make response signatures available for integrity verification. 4. **Verifiable Response:** - Use [model verification](/cloud/verification/model) for the direct endpoint. If response verification is required, also check that a signature is available and valid. + 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. Use [TLS attestation](/cloud/verification/tls) to verify endpoint binding for a particular connection when that property is required. +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 TLS endpoint binding for a particular HTTPS connection using hardware-backed attestation + + Verify gateway, model, TLS, and response evidence for Cloud API requests Add client-side encryption for defense-in-depth protection of your messages diff --git a/cloud/verification/chat.mdx b/cloud/verification/chat.mdx index 38fb96f..621a2e1 100644 --- a/cloud/verification/chat.mdx +++ b/cloud/verification/chat.mdx @@ -1,103 +1,15 @@ --- -title: "Chat Verification" -description: "Verify an available chat-completion signature against the exact request and response bytes." +title: "Choose a Response-Signature Flow" +description: "Select the response-signature guide that matches the endpoint your application calls." --- -Chat verification checks the integrity of a response for which a signature is available. It combines three pieces of evidence: +The signature payload and the attestation to match depend on the request path. -1. SHA-256 hashes of the exact request and response bytes; -2. a valid signature over the expected payload; and -3. a matching [model](/cloud/verification/model) or [gateway](/cloud/verification/gateway) attestation for the signer. - - - Not every response has an available signature. An unavailable signature is not a successful verification result. If response verification is required by your policy, treat that response as unverified. - - -## Preserve the exact bytes - -Build the request body once and retain the exact UTF-8 bytes that your HTTP client sends. Read and retain the raw response body before parsing it as JSON or Server-Sent Events. - -Calculate: - -```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 the response from parsed events. Any of those changes can produce a different hash. - -For a streaming response, capture all bytes delivered by the HTTP response body, including framing such as the final SSE delimiter when present. - -## Fetch the signature - -After completion, use the completion ID as `chat_id` and request a signature with the algorithm you want to verify. - - - - -```bash -curl --fail-with-body -G 'https://cloud-api.near.ai/v1/signature/' \ - --data-urlencode 'model=' \ - --data-urlencode 'signing_algo=ecdsa' \ - -H 'Accept: application/json' \ - -H 'Authorization: Bearer ' -``` - - - - -```bash -curl --fail-with-body -G 'https://.completions.near.ai/v1/signature/' \ - --data-urlencode 'signing_algo=ecdsa' \ - -H 'Accept: application/json' \ - -H 'Authorization: Bearer ' -``` - - - - -The returned object contains: - -| Field | Meaning | -| --- | --- | -| `text` | The payload that was signed. | -| `signature` | The cryptographic signature over `text`. | -| `signing_address` | The algorithm-specific identity that made the signature. | -| `signing_algo` | The algorithm required to verify the signature. | -| `signature_kind` | On a gateway response, identifies whether the model-serving signer or gateway signer produced the signature. | - -## Check the signed payload - -First calculate your `request_hash` and `response_hash`. Then construct the expected payload from `signature_kind`: - -| `signature_kind` | Expected `text` | Match signer to | -| --- | --- | --- | -| `provider_tee` | `::` | A model attestation with the same `signing_address` and `signing_algo`. | -| `gateway` | `:` | A gateway attestation with the same `signing_address` and `signing_algo`. | - -For a direct completion signature, use the model-attestation path. For a gateway response whose `signature_kind` is absent or unfamiliar, do not infer its classification from the shape of `text`; treat the signer classification as unavailable unless your endpoint contract supplies it. - -Reject the signature if `text` does not exactly equal the expected payload. Then verify `signature` locally with a cryptographic library that supports the reported `signing_algo`. - - - `ecdsa` signatures use an Ethereum-style signing address. `ed25519` signatures use an Ed25519 public key. Use the verification method appropriate to the reported algorithm; do not send prompts, responses, or signatures to a third-party verification website. - - -## Match the signature to attestation - -Fetch or select an attestation for the signer reported in the signature. Verify the quote, your client-generated nonce, the signer binding, and the workload policy as described in [model verification](/cloud/verification/model) or [gateway verification](/cloud/verification/gateway). - -Record the response time and attestation time, and enforce the maximum attestation age your policy requires. A matching report establishes the signer binding for that attestation; your policy determines how close in time it must be to the response. - -## Signature availability and retries - -The signature can be unavailable immediately after completion or when the response did not complete. Retry for a short, bounded interval, then make a decision according to your policy. - -| Result | Recommended handling | -| --- | --- | -| Signature returned | Complete the payload, cryptographic-signature, and attestation checks. | -| Temporary lookup failure | Retry briefly with the same completion ID and signing algorithm. | -| No signature after the retry window | Treat the response as unverified if a signature is required. | -| Signature or signer mismatch | Reject the verification result; do not substitute a different response or attestation. | - -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for signature-related verification steps. + + + Use `signature_kind` when present to select a gateway or model attestation. + + + Match the signature to the direct model report. + + diff --git a/cloud/verification/cloud-api/gateway-attestation.mdx b/cloud/verification/cloud-api/gateway-attestation.mdx new file mode 100644 index 0000000..13be109 --- /dev/null +++ b/cloud/verification/cloud-api/gateway-attestation.mdx @@ -0,0 +1,47 @@ +--- +title: "Gateway Attestation" +description: "Verify the Cloud API gateway attestation for requests sent to cloud-api.near.ai." +--- + +`gateway_attestation` is evidence for the Cloud API 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`. Add `model=` only when you also need model evidence, and add `include_tls_fingerprint=true` only when you are following the [Cloud API 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. +3. Check that the verified quote binds the nonce generated by your client, the reported `signing_address`, and its `signing_algo`. +4. When a TLS fingerprint was requested, verify its binding as part of the [Cloud API TLS flow](/cloud/verification/cloud-api/tls). +5. Apply your accepted measurement and image-provenance policy. + +`request_nonce` and `report_data` in the HTTP body are useful diagnostics. The verified quote is the cryptographic source of truth. + +## Field scope + +| Field path | What it describes | +| --- | --- | +| `gateway_attestation.intel_quote` | The gateway's Intel TDX 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. | +| `model_attestations[]` | Separate model evidence; do not use it as 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. Do not reuse evidence from a different request or instance as 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..deb2319 --- /dev/null +++ b/cloud/verification/cloud-api/image-provenance.mdx @@ -0,0 +1,34 @@ +--- +title: "Image Provenance" +description: "Apply an image-provenance policy to configurations measured by Cloud API 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 a Cloud API flow, decide separately whether your policy requires provenance for: + +- the configuration measured by `gateway_attestation`; +- each applicable model report in `model_attestations[]`; or +- both. + +## Verification flow + +1. Verify the relevant Intel TDX quote first. +2. Obtain the configuration text from that report, calculate its configuration hash, 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. + + + 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. + + +## Provider boundaries + +Do not assume every Cloud API model report exposes configuration evidence in the same format. Apply this flow only where the provider's report format and your verifier establish the configuration-to-quote binding. An unfamiliar or incomplete report is not equivalent to a verified configuration. + +## 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, include the endpoint, UTC time, requested model, signing address and `info.instance_id` when present, image name, and full digestβ€”never 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..8700478 --- /dev/null +++ b/cloud/verification/cloud-api/index.mdx @@ -0,0 +1,71 @@ +--- +title: "Verify Cloud API" +description: "Verify gateway, model, TLS, response-signature, and image-provenance evidence for requests sent through cloud-api.near.ai." +--- + +Use this flow when your application sends requests to `https://cloud-api.near.ai`. The request path includes the Cloud API gateway and can include evidence for a selected model. + +The relevant verification fields in a Cloud API response are: + +```text +gateway_attestation +model_attestations[] # returned when model evidence is available +tls_certificate # optional +``` + +`gateway_attestation` and `model_attestations[]` are separate pieces of evidence. Verify the gateway when you need evidence for the Cloud API request path; verify a model attestation when you also need evidence for the model environment. + + + `model_attestations[]` is not an inventory of every healthy model instance. Do not use its length, or the presence of one entry, as evidence that a later request will be served by that same instance. + + +## Recommended flow + +1. Generate a fresh 32-byte nonce in the client for each verification attempt. +2. Request a Cloud API attestation report, including `model=` when model evidence is needed. +3. Verify `gateway_attestation` and apply your gateway measurement policy. +4. Verify the applicable item in `model_attestations[]` using the verifier and policy for its provider. +5. If you need endpoint binding, verify the TLS connection to `cloud-api.near.ai` against the **gateway** attestation. +6. If you need response integrity, verify the response signature and match its signer to the appropriate gateway or model attestation. +7. Apply an image-provenance policy to each measured configuration your policy requires. + + + + Verify the Cloud API gateway quote and signer binding. + + + Verify provider model evidence returned in `model_attestations[]`. + + + Bind the live Cloud API TLS connection to `gateway_attestation`. + + + Match a response signature to its gateway or model signer. + + + Apply provenance policy to measured gateway and model configurations. + + + +## Get a fresh report + +```bash +NONCE="$(openssl rand -hex 32)" + +curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ + --data-urlencode 'model=' \ + --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode "nonce=${NONCE}" \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' +``` + +Use the same `signing_algo` as the response signature you intend to verify. Add `include_tls_fingerprint=true` only when following the [Cloud API TLS flow](/cloud/verification/cloud-api/tls). + +## Provider-specific model evidence + +Cloud API can return model evidence from different provider tiers. The outer `model_attestations[]` field does not make their internal report formats interchangeable. + +This guide describes the Cloud API envelope and the NEAR-operated model flow. When your integration selects a different provider, use that provider's report format and verification policy; do not apply a NEAR-specific signer or measurement rule to an unfamiliar report shape. + +The report request accepts `provider=near` or `provider=chutes` when your integration needs to select a provider tier. Those choices can return different model-evidence formats, so make the selection part of your verification policy. diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx new file mode 100644 index 0000000..11e754a --- /dev/null +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -0,0 +1,48 @@ +--- +title: "Model Attestations" +description: "Verify model evidence returned in the Cloud API model_attestations array." +--- + +Use this page when a request to `cloud-api.near.ai` needs evidence for a model environment in addition to the gateway. + +Cloud API returns model evidence in `model_attestations[]`; this is different from the direct endpoint's root model report and `all_attestations[]` field. Treat the Cloud API envelope and each provider report as separate contracts. + +## Request model evidence + +Include the model ID when requesting the report: + +```bash +NONCE="$(openssl rand -hex 32)" + +curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ + --data-urlencode 'model=' \ + --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode "nonce=${NONCE}" \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' +``` + +The response always has `gateway_attestation`; model evidence, when available, is carried in `model_attestations[]`. + +When matching a response signature, request fresh evidence for that signer by adding both `signing_address=` and the signature's `signing_algo`. Do not use a report for a different signer as a substitute. + +Cloud API also accepts `provider=near` or `provider=chutes`. These provider tiers can use different model-report formats and verification rules, so select one only when it is covered by your policy. + +## Verify the applicable entry + +1. Determine which provider's model evidence your request is using. +2. Verify the Intel TDX quote, nonce, signer binding, GPU evidence, and measurement according to that provider's verification rules. +3. When verifying a response signed by a model-serving TEE, request or select an entry whose `signing_address` **and** `signing_algo` match the response signature. +4. Apply your own allowlist and provenance policy to the verified measurements. + + + Do not assume every `model_attestations[]` entry has the same fields, or that the array lists every model CVM behind a model URL. A response signature must be matched by signer identity and algorithm; a report from a different signer is not an equivalent substitute. + + +## Relationship to Cloud API 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 [Cloud API TLS connection binding](/cloud/verification/cloud-api/tls). + +## Verify a signed response + +For a Cloud API response with `signature_kind: "provider_tee"`, verify the signature over the endpoint-defined payload and match its signer to the applicable model attestation. For a `gateway` signature, match the signer to `gateway_attestation` instead. See [Cloud API response signatures](/cloud/verification/cloud-api/response-signatures). diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx new file mode 100644 index 0000000..6540794 --- /dev/null +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -0,0 +1,47 @@ +--- +title: "Response Signatures" +description: "Verify available Cloud API response signatures against the exact bytes and the matching attestation." +--- + +Cloud API response signatures can be produced by either a model-serving TEE or the Cloud API gateway. When present, `signature_kind` determines which attestation 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. + +```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. + +## Fetch and verify the signature + +```bash +curl --fail-with-body -G 'https://cloud-api.near.ai/v1/signature/' \ + --data-urlencode 'signing_algo=ecdsa' \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' +``` + +Verify `signature` locally using the returned `signing_algo`, then compare `text` with the expected payload: + +| `signature_kind` | Expected `text` | Required attestation match | +| --- | --- | --- | +| `provider_tee` | `::` | An applicable `model_attestations[]` item with the same `signing_address` and `signing_algo`. | +| `gateway` | `:` | `gateway_attestation` with the same `signing_address` and `signing_algo`. | + +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. Do not assume it is an alias originally supplied by the client. + +## Match the attestation + +For the selected signer, request a fresh report with a client-generated nonce and the signature's `signing_address` and `signing_algo`. Include `model=` for model evidence. Then verify the quote and apply the required measurement and provenance policy. A signature made by a different signer is not evidence for the response, even if the model name or endpoint is the same. + +`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/cloud-api/tls.mdx b/cloud/verification/cloud-api/tls.mdx new file mode 100644 index 0000000..d80120d --- /dev/null +++ b/cloud/verification/cloud-api/tls.mdx @@ -0,0 +1,38 @@ +--- +title: "TLS Connection Binding" +description: "Bind a live Cloud API TLS connection to the Cloud API 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. + + +## 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; Cloud API 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. + +## 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/deployment-control-evidence.mdx b/cloud/verification/direct/deployment-control-evidence.mdx new file mode 100644 index 0000000..1c9f82f --- /dev/null +++ b/cloud/verification/direct/deployment-control-evidence.mdx @@ -0,0 +1,29 @@ +--- +title: "Deployment Control Evidence" +description: "Interpret optional Compose Manager evidence returned by a direct model endpoint." +--- + +A direct model attestation response can include an optional top-level `compose_manager_attestation`. This is separate deployment-control evidence; it is not a model-attestation entry and is not part of the Cloud API attestation contract. + +The current payload records deployment-control actions and related integrity data. It can be useful for deployment investigation, but it has a narrower purpose than a model quote or a response signature. + +## What it can and cannot establish + +Use it as supplementary evidence about deployment-control activity. Do not use it alone to conclude that: + +- a particular project, model, or backend is currently running; +- every recorded action completed successfully; +- a later inference request was served by this same CVM; or +- the images involved satisfy your source and build-provenance policy. + +The same model hostname can be load-balanced across instances. A direct report and a later inference request are therefore not automatically tied to the same CVM. + +## Use it with independent checks + +When your application requires evidence for an inference response, verify the direct [model attestation](/cloud/verification/direct/model-attestation), [TLS connection binding](/cloud/verification/direct/tls) when required, and [response signature](/cloud/verification/direct/response-signatures) when available. + +When your policy requires software provenance, verify the image digests from the quote-bound configuration using the [direct image-provenance flow](/cloud/verification/direct/image-provenance). An action log or an image reference does not by itself verify image provenance. + + + Treat unavailable or incomplete deployment-control evidence as unavailable evidence for that optional check. Do not substitute it for a model quote, a response signature, or a verified provenance statement. + diff --git a/cloud/verification/direct/image-provenance.mdx b/cloud/verification/direct/image-provenance.mdx new file mode 100644 index 0000000..7af46ef --- /dev/null +++ b/cloud/verification/direct/image-provenance.mdx @@ -0,0 +1,28 @@ +--- +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. Obtain the configuration text from the direct report, calculate its configuration hash, 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, include the endpoint, UTC time, model name, signing address and `info.instance_id` when present, image name, and full digestβ€”never 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..6261745 --- /dev/null +++ b/cloud/verification/direct/index.mdx @@ -0,0 +1,52 @@ +--- +title: "Verify Direct Model Endpoints" +description: "Verify model, TLS, response-signature, and image-provenance evidence for direct completions endpoints." +--- + +Use this flow when your application sends requests directly to `https://.completions.near.ai`. The Cloud API gateway is not in this request path. + +The direct attestation response is a model report at the response root. It includes fields such as `model_name`, `signing_address`, `signing_algo`, `request_nonce`, and `intel_quote`, and also includes `all_attestations[]`. + + + `all_attestations[]` is not a fleet inventory. Do not infer that it enumerates every healthy CVM, or that a later completion will be served by the same instance as an earlier report. + + +## Recommended flow + +1. Generate a fresh 32-byte nonce in the client for each verification attempt. +2. Request the report from the same direct model endpoint used for inference. +3. Verify the model's quote, nonce, signer binding, GPU evidence when present, and accepted measurements. +4. If you need endpoint binding, verify the TLS connection to the direct model endpoint against that model report. +5. If you need response integrity, verify the response signature and match it to the model report's signer. +6. Apply image-provenance policy to the measured model configuration. + + + + Verify the direct endpoint's root model report and `all_attestations[]` semantics. + + + Bind the live direct-model TLS connection to its model quote. + + + Verify a direct-model response signature against the matching model signer. + + + Apply provenance policy to the direct model's measured configuration. + + + Interpret optional Compose Manager evidence without treating it as request-to-CVM binding. + + + +## Get a fresh report + +```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' +``` + +The endpoint hostname identifies the model, so this request does not use a `model` parameter. Add `include_tls_fingerprint=true` only when following the [direct TLS flow](/cloud/verification/direct/tls). If you are matching a response signature, use its `signing_address` and `signing_algo` when requesting the applicable report. diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx new file mode 100644 index 0000000..98f40a1 --- /dev/null +++ b/cloud/verification/direct/model-attestation.mdx @@ -0,0 +1,52 @@ +--- +title: "Model Attestation Report" +description: "Verify the root model report returned by a direct completions endpoint." +--- + +Use this page for a direct `https://.completions.near.ai` endpoint. This response is not the Cloud API `gateway_attestation` / `model_attestations[]` envelope. + +## 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 configured for this direct endpoint. | +| `signing_address`, `signing_algo` | The model signing identity and algorithm. | +| `request_nonce` | The nonce echoed for diagnostics; verify it inside the quote. | +| `intel_quote` | The model's Intel TDX quote. | +| `nvidia_payload` | GPU evidence, when present. | +| `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. | +| `compose_manager_attestation` | Optional, separate deployment-control evidence. See [deployment control evidence](/cloud/verification/direct/deployment-control-evidence). | + +The direct response does not define a top-level `report_data` field. Read `report_data` and measurements from the verified TDX quote. + +## 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, signer identity, and signing algorithm against the verified quote. +3. When GPU evidence is present, verify it with the applicable NVIDIA verifier and ensure it is bound to the same nonce. +4. Apply your accepted measurement policy to the verified configuration. +5. 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..1cf88d3 --- /dev/null +++ b/cloud/verification/direct/response-signatures.mdx @@ -0,0 +1,46 @@ +--- +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. The direct signature response has `text`, `signature`, `signing_address`, and `signing_algo`; it does not use the Cloud API `signature_kind` routing field. + + + 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. + +## Fetch and verify the signature + +```bash +curl --fail-with-body -G 'https://.completions.near.ai/v1/signature/' \ + --data-urlencode 'signing_algo=ecdsa' \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' +``` + +Verify `signature` locally using the returned `signing_algo`. 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 Cloud API model ID, does not satisfy this check. + +## 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/model classification from the format of the signed text, and do not substitute a report for a different signer. The direct endpoint's `all_attestations[]` field is not proof that every instance behind the hostname has been attested. + +`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..311cbe0 --- /dev/null +++ b/cloud/verification/direct/tls.mdx @@ -0,0 +1,38 @@ +--- +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. + +## 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 index cfcd3de..791ee9c 100644 --- a/cloud/verification/gateway.mdx +++ b/cloud/verification/gateway.mdx @@ -1,52 +1,10 @@ --- -title: "Gateway Verification" -description: "Verify the attestation evidence for requests sent through the NEAR AI Cloud API gateway." +title: "Gateway Attestation" +description: "Verify gateway evidence for requests sent through the NEAR AI Cloud API." --- -Use gateway verification when your request is sent to `https://cloud-api.near.ai`. It verifies evidence for the gateway request path. It does not replace [model verification](/cloud/verification/model) when you also need to verify the environment that served the model. +This page has moved into the Cloud API verification flow. - - A [direct completions endpoint](/cloud/private-inference#direct-completions) connects to the model endpoint without the Cloud API gateway in the request path. Start with [model verification](/cloud/verification/model) for that flow. - - -## Request a fresh gateway report - -Generate a new 32-byte nonce in your client for each verification attempt. Encode it 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 ' -``` - -Add `model=` when you also want the request to include model attestation evidence. Add `include_tls_fingerprint=true` when you need to verify the HTTPS connection; see [TLS attestation](/cloud/verification/tls). - -## Verify the gateway evidence - -Verify the Intel TDX quote before trusting report fields supplied over HTTP. Then check the following: - -1. The verified quote satisfies your Intel DCAP and advisory policy. -2. The quote's report data contains the nonce generated by your client and binds the reported gateway signing identity. -3. The signing identity is paired with its reported `signing_algo`. -4. The measured configuration satisfies the policy your application accepts. -5. If you require source and build provenance, verify it for each image digest in the measured configuration. See [image provenance](/cloud/verification/provenance). - -When the report includes `gateway_attestation`, use that entry for gateway verification. A request that includes a model can also include `model_attestations`; verify those separately when model evidence is required. - -## Verify responses signed by the gateway - -When a response signature reports `signature_kind: "gateway"`, verify its signature against the gateway attestation whose `signing_address` and `signing_algo` match the signature. Then verify that the signed payload contains the SHA-256 hashes of the exact request and response bytes you observed. - -Follow [chat verification](/cloud/verification/chat) for the full response-signature flow. - -## Freshness and failure handling - -The nonce proves freshness only when your client created and retained it. Do not accept a report merely because an HTTP field echoes a nonce; verify the binding inside the validated quote. - -An unavailable report, failed quote, nonce mismatch, identity mismatch, or policy mismatch is not an equivalent result. If your application requires that property, mark the verification as failed or unverified and fetch new evidence rather than reusing a report from a different instance. - -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) includes implementation examples for individual verification steps. + + Open the route-specific guide for `gateway_attestation`. + diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 35ff5b5..4d84052 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -1,80 +1,43 @@ --- title: "Verification" -description: "Verify NEAR AI Cloud attestation reports, TLS endpoints, signed chat responses, and deployed image provenance." +description: "Choose the Cloud API or direct-model verification flow, then verify the evidence for that request path." --- -Verification lets you independently check evidence for a specific NEAR AI Cloud request path. Each check answers a different question: whether an attestation is fresh, whether a TLS connection reaches the attested endpoint, whether a response was signed by an attested signer, or whether deployed images have the provenance your policy requires. - -Start with the result you need to verify. +NEAR AI Cloud has two verification paths. They use different endpoints, response envelopes, and signer relationships. Select the path your application actually uses before interpreting an attestation report. - - Verify a model attestation, including its Intel TDX quote, nonce, signer, GPU evidence, and measured configuration. - - - Verify the Cloud API gateway when requests are sent through cloud-api.near.ai. - - - Verify that one HTTPS connection terminates at the endpoint named by its attestation. + + Verify requests sent to cloud-api.near.ai, including its gateway attestation and any returned model attestations. - - Verify the integrity and signer of a chat completion. - - - Apply a source and build-provenance policy to every image pinned in an attested deployment. + + Verify requests sent directly to a model's completions.near.ai endpoint, without the Cloud API gateway in the request path. -## Choose the checks you need - -| Goal | Required evidence | -| --- | --- | -| Verify a model environment | A fresh model attestation, a verified Intel TDX quote, the request nonce, signer binding, and GPU evidence when present. | -| Verify the gateway | A fresh gateway attestation and a verified Intel TDX quote. Verify the model separately when you also need evidence for model execution. | -| Verify an HTTPS connection | A TLS-bound attestation and the live certificate's SPKI hash from the **same TLS connection**. | -| Verify a chat response | The exact request and response bytes, a valid signature, and an attestation for the signature's signer. | -| Verify deployed software provenance | A verified compose measurement plus a provenance decision for every image digest in that compose file. | - -## Recommended verification flow - -For a production integration, use the following order: - -1. Generate a fresh, random 32-byte nonce in your client. Send it as 64 hexadecimal characters with the attestation request. -2. Request the attestation for the endpoint and model you are using. When you need TLS endpoint verification, set `include_tls_fingerprint=true`. -3. Verify the Intel TDX quote with an Intel DCAP verifier. Read `report_data` and measurements from the **verified quote**, not from unverified response fields. -4. Check that the verified quote binds your nonce and the reported signing identity. Verify NVIDIA GPU evidence when the report contains it. -5. Apply your workload policy: inspect the measured compose configuration and, when required, verify provenance for every pinned image digest. -6. For a chat response, verify the signature over the exact bytes you sent and received, then match its signer to the appropriate attestation. - - - The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for individual checks. Treat any example as a starting point for your own policy: your application should fail verification when a check it requires is missing or does not pass. - +## Why the paths are separate -## Direct completions and the gateway +| | Cloud API | Direct model endpoint | +| --- | --- | --- | +| Request host | `cloud-api.near.ai` | `.completions.near.ai` | +| Attestation envelope | `gateway_attestation` and optional `model_attestations[]` | A model report at the response root, plus `all_attestations[]` | +| Gateway evidence | Available as `gateway_attestation` | Not in the request path | +| Response signature routing | `signature_kind` identifies a gateway or model-serving signer when present | Match the signature to the direct model report's signer | -Requests sent to a [direct completions endpoint](/cloud/private-inference#direct-completions) go directly to the model endpoint. In that path, start with [model verification](/cloud/verification/model); gateway verification is not required for the direct request path. +Do not rename or map these fields as if they were aliases. In particular, `model_attestations[]` is a Cloud API response field; it is not the direct endpoint's `all_attestations[]` field. -Requests sent to `https://cloud-api.near.ai` pass through the Cloud API gateway. Verify the gateway when you need evidence for that request path, and verify the model when you need evidence for the model environment. +## Shared requirements -## Important details for load-balanced endpoints +Both paths require a client-generated nonce, a verified Intel TDX quote, a signer binding, and an explicit acceptance policy. The path guides show where to find those values; the reference pages explain how to evaluate them. -An endpoint can be served by more than one attested instance. Do not assume that two independent requests reached the same instance. - -- When verifying a chat response, use the response's `signing_address` and `signing_algo` to select the matching attestation. -- When verifying TLS, obtain the live certificate and the attestation over the same TLS connection. Fetching them over separate connections can produce a false mismatch after load balancing or certificate rotation. -- When an instance is redeployed or its certificate changes, fetch a new attestation before accepting the new evidence. - -## What attestation does and does not establish - -Attestation verifies specific cryptographic evidence about a runtime and its measured configuration. It does not, by itself, validate the quality of a model response, secure a compromised client device, or establish source provenance for an image whose build evidence you have not verified. - -Use the individual guides in this section to decide which guarantees your application requires. - -## Common verification failures + + + Verify fresh evidence from the quote rather than trusting echoed HTTP fields. + + + Define required checks, handling for unavailable evidence, and image-provenance policy. + + -| Symptom | What to do | -| --- | --- | -| Nonce mismatch | Reject the report. Generate a new nonce in the client and request a new attestation. | -| TLS fingerprint mismatch | Reject the connection. Repeat the TLS flow using one connection for both the certificate and attestation request. | -| Signature is unavailable | Do not mark the response as verified. Retry briefly after completion; if it remains unavailable, retain the response ID and request ID for support. | -| Image provenance is unavailable | Treat the image as unverified under a provenance-required policy. Fetch a fresh attestation after a deployment change, then repeat the provenance check. | + + A report for one connection or one instance is not automatically evidence for another. For a TLS conclusion, obtain the peer certificate and report on the same TLS connection. For a signed response, match its signer and algorithm to the applicable attestation. + diff --git a/cloud/verification/model.mdx b/cloud/verification/model.mdx index 44aaec1..1ed1dd7 100644 --- a/cloud/verification/model.mdx +++ b/cloud/verification/model.mdx @@ -1,88 +1,15 @@ --- -title: "Model Verification" -description: "Verify the attestation evidence for a NEAR AI Cloud model endpoint." +title: "Choose a Model Attestation Flow" +description: "Select the verification guide that matches the endpoint your application calls." --- -Use model verification to check the environment serving a model. It is the required attestation check for [direct completions](/cloud/private-inference#direct-completions), and it complements [gateway verification](/cloud/verification/gateway) when you call `cloud-api.near.ai`. +The report structure depends on the endpoint, so select the guide for the request path your application uses. -Model verification does not prove a particular response by itself. To verify a response, also follow [chat verification](/cloud/verification/chat). - -## Request a fresh report - -Generate the nonce in your client. Use 32 random bytes encoded as 64 hexadecimal characters, and retain it until verification has completed. - - - - -```bash -NONCE="$(openssl rand -hex 32)" - -curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ - --data-urlencode 'model=' \ - --data-urlencode 'signing_algo=ecdsa' \ - --data-urlencode "nonce=${NONCE}" \ - -H 'Accept: application/json' \ - -H 'Authorization: Bearer ' -``` - - - - -```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' \ - -H 'Authorization: Bearer ' -``` - - - - -Use the same `signing_algo` as the signature you plan to verify. Supported values are `ecdsa` and `ed25519`. If you need to bind the report to the HTTPS endpoint, add `include_tls_fingerprint=true` and follow the [TLS attestation guide](/cloud/verification/tls). - -The Cloud API request accepts a `model` parameter. A direct endpoint already identifies the model through its hostname. If you are verifying a response signature and the endpoint supports it, include its `signing_address` and `signing_algo` to request evidence for that signer. - - - A report can contain evidence from more than one serving instance. Do not assume a fixed number of entries. Select the entry that matches the signer and algorithm of the response you are verifying. - - -## Verify the report - -Verify the Intel TDX quote before trusting fields copied into the HTTP response. Your verifier should read the report data and measurements from the verified quote. - -For each applicable model-attestation entry, check the following: - -1. Verify the Intel TDX quote using an Intel DCAP verifier and apply your policy for its result and advisories. -2. Verify that the quote's report data contains the nonce your client generated and binds the reported signing identity. -3. Match `signing_address` and `signing_algo` to the identity that signed the response, when you are verifying a response. -4. When GPU evidence is present, verify it with the applicable NVIDIA verifier and ensure it is bound to the same nonce. -5. Check the measured configuration against the policy your application accepts. If you require source and build provenance, continue with [image provenance verification](/cloud/verification/provenance). - -The response's `request_nonce` and `report_data` fields are useful for diagnostics, but the verified quote is the cryptographic source of truth. - -## Signing identities - -`signing_address` is an algorithm-specific identity, not always a public key in the same format: - -| `signing_algo` | `signing_address` | -| --- | --- | -| `ecdsa` | An Ethereum-style address derived from the signing key. | -| `ed25519` | The Ed25519 public-key bytes, encoded as hexadecimal. | - -Always compare both the identity and `signing_algo`. Do not compare identities across algorithms. - -## Handling rotations and load balancing - -An endpoint may be served by several instances. A report fetched before or after a completion can therefore describe a different signer. - -- Fetch a new report when the signer, certificate, or measured configuration changes. -- For a signed response, request or select the attestation entry that matches its signer. -- Record the report time and enforce the maximum attestation age your policy permits. -- When a required check cannot be completed, treat that result as unverified rather than substituting evidence from a different instance. - -## Reference implementation - -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) provides implementation examples for quote, GPU, TLS, signature, and image-digest inspection. Integrate the checks and acceptance policy appropriate for your application. + + + Use `model_attestations[]` returned by cloud-api.near.ai. + + + Use the model report returned by a direct completions endpoint. + + diff --git a/cloud/verification/provenance.mdx b/cloud/verification/provenance.mdx index c52bf6e..81b91b2 100644 --- a/cloud/verification/provenance.mdx +++ b/cloud/verification/provenance.mdx @@ -1,56 +1,15 @@ --- -title: "Image Provenance" -description: "Verify source and build provenance for images named in an attested deployment." +title: "Choose an Image-Provenance Flow" +description: "Select the provenance guide for the attestation envelope your application uses." --- -An attestation can establish that a measured deployment configuration was running. Image provenance answers a separate question: whether each image in that configuration was built from the source and build process your policy accepts. +The configuration to inspect is carried by different attestation envelopes for Cloud API and direct model endpoints. Select the matching path before evaluating image digests. -Use both checks when your application requires a source-to-runtime trust chain. - -## Verification flow - -1. Verify the Intel TDX quote. Obtain the compose configuration text from the report, calculate its hash, and compare it with the configuration measurement in the verified quote. Only then treat that text as the attested configuration. -2. Extract every container image pinned by immutable digest (`@sha256:…`) from the matched configuration. -3. For each digest, retrieve provenance evidence from the relevant build-provenance system. -4. Cryptographically verify the provenance statement and confirm that its subject digest exactly matches the image digest in the attested configuration. -5. Apply your policy to the verified statement, such as the expected repository, build identity, workflow, ref, and source revision. -6. Accept the deployment only when every image required by your policy passes the checks. - - - An image tag is not sufficient evidence. A search result or an HTTP `200` response only locates a possible record; it does not cryptographically verify the statement, its signer, or its subject digest. - - -## Compose Manager images - -When the attested configuration contains platform images such as `compose-manager` or `compose-manager-launcher`, evaluate their exact digests the same way as every other image in the configuration. First bind the configuration to the verified quote, then verify build provenance for each digest under your policy. - -An integrity check for a reported action list is a separate check. It does not establish the provenance of the image that ran the action, and it does not replace quote or image-provenance verification. - -## Define a provenance policy - -Your policy should specify the identities and inputs you accept before you evaluate a report. At minimum, define: - -- the expected source repository or repositories; -- the accepted build identity and workflow; -- the allowed source branch, tag, or commit policy; and -- whether provenance is required for every image or only selected images. - -Pin every evaluated image by digest. If a configuration references an image without an immutable digest, it cannot satisfy a digest-matching provenance check. - -## When a provenance record is missing - -A missing record, including an HTTP `404`, means provenance for the reported image digest could not be verified. It does not by itself identify a security incident. - -Deployments can change. Fetch a fresh attestation report and repeat the verification flow. If the current report still references a digest without the provenance required by your policy, treat that image as unverified. - -When requesting help, include the endpoint, time, report identifier, image name, and full digest. Do not include API keys, prompts, or response content. - -## Relationship to attestation - -| Check | What it establishes | -| --- | --- | -| Quote and configuration measurement | The configuration text you matched is bound to a measurement in the verified quote. | -| Image provenance | A particular image digest satisfies your source and build policy. | -| Both together | The measured runtime configuration uses images whose provenance satisfies your policy. | - -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) includes reference code for locating selected image digests in a configuration. Use a provenance verifier that validates the cryptographic evidence required by your policy. + + + Evaluate configuration measurements from the gateway and any returned model attestations. + + + Evaluate the measured configuration from the direct model report. + + diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx new file mode 100644 index 0000000..70e6fae --- /dev/null +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -0,0 +1,47 @@ +--- +title: "Quote, Nonce, and Signer Bindings" +description: "Shared rules for reading verified quotes, checking freshness, and matching signing identities." +--- + +Every 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 | +| --- | --- | +| Cloud API gateway | `gateway_attestation.intel_quote` | +| Cloud API model evidence | The provider-specific item in `model_attestations[]` | +| Direct model endpoint | Root `intel_quote` (and any applicable direct report entry) | + +The direct report does not expose a top-level `report_data` field. Always obtain report data and measurements from the verified quote itself. + +## Verify the signer binding + +For a NEAR-operated TLS-bound report, the verified quote binds the signer identity, TLS certificate fingerprint, and nonce. A provider can use a different report format, so verify the exact binding required by the selected endpoint and provider flow before accepting the report. + +`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. + +## Verify GPU evidence when present + +When a report contains NVIDIA GPU evidence, verify it with the applicable NVIDIA verification service and require that it is bound to the same client nonce. Define in advance whether GPU evidence is mandatory for your workload policy. + +## Reference code + +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for individual quote, GPU, TLS, signature, and configuration checks. Use it as an implementation reference, then enforce the acceptance policy required by your application. diff --git a/cloud/verification/reference/verification-policy.mdx b/cloud/verification/reference/verification-policy.mdx new file mode 100644 index 0000000..c76f4b7 --- /dev/null +++ b/cloud/verification/reference/verification-policy.mdx @@ -0,0 +1,42 @@ +--- +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; +- 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; and +- 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, model instance, or provider. + +| Condition | Recommended result | +| --- | --- | +| Quote or nonce mismatch | Reject the evidence and request a fresh report. | +| 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. | +| Provider report shape is not covered by your verifier | Treat the provider-specific property as unverified. | + +## Load balancing and rotations + +The same hostname can be served by more than one attested instance. A report is not automatically evidence for a later request. + +- For TLS, obtain the peer certificate and report on the same connection. +- For a response signature, match the signer identity and algorithm to the applicable attestation. +- Fetch new evidence 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 index b8be96a..9d44252 100644 --- a/cloud/verification/tls.mdx +++ b/cloud/verification/tls.mdx @@ -1,60 +1,15 @@ --- -title: "TLS Attestation" -description: "Verify that a specific HTTPS connection terminates at the endpoint bound to an attestation report." +title: "Choose a TLS Attestation Flow" +description: "Select the TLS verification guide that matches the endpoint your application calls." --- -TLS attestation links a live HTTPS connection to the signing identity in a verified attestation. It is the check to use when your policy requires evidence that the TLS endpoint for a particular connection is the attested endpoint. +TLS evidence applies to a particular request path and connection. Select the endpoint your application calls. - - Obtain the peer certificate and the attestation report over the **same TLS connection**. Fetching them over separate connections can reach different instances or observe a rotated certificate, especially for load-balanced endpoints. - - -## What the check establishes - -When you request an attestation with `include_tls_fingerprint=true`, the report includes `tls_cert_fingerprint`: the SHA-256 hash of the certificate's SubjectPublicKeyInfo (SPKI). The verified TDX quote binds that fingerprint, the signing identity, and your nonce. - -After you compare the live peer certificate's SPKI hash with `tls_cert_fingerprint`, the result establishes that the TLS connection you observed is bound to that verified attestation. - -Your application must make an explicit certificate-validation decision. TLS attestation becomes a security conclusion only after the quote, binding, and same-connection checks pass; an unverified report is not a reason to relax TLS validation. - -## Verification flow - -1. Generate a fresh, random 32-byte nonce in your client and encode it as 64 hexadecimal characters. -2. Open a TLS connection using the trust configuration appropriate for your application. -3. From that connection, read the peer certificate and calculate `SHA-256(SPKI_DER)`. -4. Reuse the **same open connection** to request `/v1/attestation/report` with `include_tls_fingerprint=true`, your nonce, and the signing algorithm you require. -5. Verify the Intel TDX quote and verify that its report data binds the nonce, signing identity, and reported TLS fingerprint. -6. Compare the SPKI hash you calculated from the peer certificate with `tls_cert_fingerprint` from the report. -7. Apply the same measurement and provenance policy you use for [model](/cloud/verification/model) or [gateway](/cloud/verification/gateway) verification. - -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) includes implementation examples for TLS-related verification steps. Independently ensure that your client keeps the certificate read and report request on one connection. - -## Request parameters - -Use the normal attestation endpoint for your request path, adding `include_tls_fingerprint=true`: - -```text -GET /v1/attestation/report?signing_algo=ecdsa&nonce=&include_tls_fingerprint=true -``` - -For the Cloud API gateway, include your Bearer API key and, when needed, `model=`. For a direct endpoint, call that endpoint's `/v1/attestation/report` route. A command-line request can retrieve a report, but it does not by itself prove the same-connection property unless your client also reads the peer certificate from that very connection. - -## Load balancing and certificate rotation - -Do not use a cached fingerprint as evidence for a new connection. It can be useful as an operational hint, but it is not a fresh attestation result. - -Request new evidence when any of the following changes: - -- the live certificate's public key; -- the report's signing identity or signing algorithm; -- the measured configuration; or -- your policy's allowed attestation age. - -## Failure handling - -| Result | Required action when TLS endpoint verification is required | -| --- | --- | -| No `tls_cert_fingerprint` | Treat the TLS binding as unavailable; request a new report with `include_tls_fingerprint=true`. | -| Peer SPKI hash differs from the report | Reject the connection and repeat the flow with one TLS connection. | -| Quote, nonce, or signing-identity check fails | Reject the report and fetch fresh evidence. | -| Certificate read and report fetch used different connections | Treat the result as inconclusive, not verified. | + + + Verify the TLS connection to cloud-api.near.ai against `gateway_attestation`. + + + Verify the TLS connection to a direct model endpoint against its model report. + + diff --git a/docs.json b/docs.json index 756abb9..0776347 100644 --- a/docs.json +++ b/docs.json @@ -67,11 +67,35 @@ "icon": "file-check", "root": "cloud/verification/index", "pages": [ - "cloud/verification/model", - "cloud/verification/gateway", - "cloud/verification/tls", - "cloud/verification/chat", - "cloud/verification/provenance" + { + "group": "Cloud API", + "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", + "cloud/verification/direct/deployment-control-evidence" + ] + }, + { + "group": "Reference", + "pages": [ + "cloud/verification/reference/quote-nonce-signer", + "cloud/verification/reference/verification-policy" + ] + } ] } ] From d7f0b31b31120f744a3effb299cbd95cb228eccf Mon Sep 17 00:00:00 2001 From: Coffee Date: Mon, 24 Aug 2026 20:48:50 +0800 Subject: [PATCH 04/36] docs: name the NEAR AI Cloud gateway --- cloud/guides/e2ee-chat-completions.mdx | 6 +++--- cloud/private-inference.mdx | 12 +++++------ cloud/verification/chat.mdx | 2 +- .../cloud-api/gateway-attestation.mdx | 8 ++++---- .../cloud-api/image-provenance.mdx | 6 +++--- cloud/verification/cloud-api/index.mdx | 20 +++++++++---------- .../cloud-api/model-attestations.mdx | 12 +++++------ .../cloud-api/response-signatures.mdx | 4 ++-- cloud/verification/cloud-api/tls.mdx | 4 ++-- .../direct/deployment-control-evidence.mdx | 2 +- cloud/verification/direct/index.mdx | 2 +- .../verification/direct/model-attestation.mdx | 2 +- .../direct/response-signatures.mdx | 4 ++-- cloud/verification/gateway.mdx | 6 +++--- cloud/verification/index.mdx | 10 +++++----- cloud/verification/model.mdx | 2 +- cloud/verification/provenance.mdx | 4 ++-- .../reference/quote-nonce-signer.mdx | 4 ++-- cloud/verification/tls.mdx | 2 +- docs.json | 2 +- 20 files changed, 57 insertions(+), 57 deletions(-) diff --git a/cloud/guides/e2ee-chat-completions.mdx b/cloud/guides/e2ee-chat-completions.mdx index 59f0d89..44dbbb1 100644 --- a/cloud/guides/e2ee-chat-completions.mdx +++ b/cloud/guides/e2ee-chat-completions.mdx @@ -222,7 +222,7 @@ response = requests.get( # ) attestation = response.json() -# Cloud API: +# NEAR AI Cloud gateway: model_public_key_hex = attestation["model_attestations"][0]["signing_public_key"] # Direct completions use the root model report instead: # model_public_key_hex = attestation["signing_public_key"] @@ -254,7 +254,7 @@ console.log('Model Ed25519 public key:', modelPublicKeyHex); -For Cloud API, the model key is in `model_attestations[0].signing_public_key`. For a direct completions endpoint, it is in the root report's `signing_public_key`. +For the NEAR AI Cloud gateway, the model key is in `model_attestations[0].signing_public_key`. For a direct completions endpoint, it is in the root report's `signing_public_key`. --- @@ -813,5 +813,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 -- [Verification](/cloud/verification) β€” Select Cloud API or direct model attestation verification +- [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 b752a17..467714e 100644 --- a/cloud/private-inference.mdx +++ b/cloud/private-inference.mdx @@ -60,7 +60,7 @@ NEAR AI Cloud supports two connection modes. Each has its own TLS-attestation fl ```mermaid flowchart LR - A["Your Machine"] -->|"πŸ”’ HTTPS"| B["Cloud API gateway"] -->|"πŸ”’ HTTPS"| C["Model endpoint β†’ 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 @@ -138,7 +138,7 @@ 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 [Cloud API gateway attestation](/cloud/verification/cloud-api/gateway-attestation) required) +- **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 protects data in transit; use [Cloud API TLS connection binding](/cloud/verification/cloud-api/tls) when your application needs to verify the endpoint for a particular connection. + 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. @@ -168,7 +168,7 @@ When you make a request to NEAR AI Cloud, your data flows through a secure pipel **Via Direct Completions (`{slug}.completions.near.ai`):** 1. **Direct Request:** - You send chat completion requests via HTTPS directly to the model's subdomain, with no intermediate Cloud API gateway in the request path. Use [direct TLS connection binding](/cloud/verification/direct/tls) when endpoint verification is required. + 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. @@ -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 gateway, model, TLS, and response evidence for Cloud API requests + + 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 index 621a2e1..2f94940 100644 --- a/cloud/verification/chat.mdx +++ b/cloud/verification/chat.mdx @@ -6,7 +6,7 @@ description: "Select the response-signature guide that matches the endpoint your The signature payload and the attestation to match depend on the request path. - + Use `signature_kind` when present to select a gateway or model attestation. diff --git a/cloud/verification/cloud-api/gateway-attestation.mdx b/cloud/verification/cloud-api/gateway-attestation.mdx index 13be109..47a3750 100644 --- a/cloud/verification/cloud-api/gateway-attestation.mdx +++ b/cloud/verification/cloud-api/gateway-attestation.mdx @@ -1,9 +1,9 @@ --- title: "Gateway Attestation" -description: "Verify the Cloud API gateway attestation for requests sent to cloud-api.near.ai." +description: "Verify the NEAR AI Cloud gateway attestation for requests sent to cloud-api.near.ai." --- -`gateway_attestation` is evidence for the Cloud API 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. +`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 @@ -19,7 +19,7 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -H 'Authorization: Bearer ' ``` -The response contains the gateway report at `gateway_attestation`. Add `model=` only when you also need model evidence, and add `include_tls_fingerprint=true` only when you are following the [Cloud API TLS flow](/cloud/verification/cloud-api/tls). +The response contains the gateway report at `gateway_attestation`. Add `model=` only when you also need model evidence, and 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 @@ -28,7 +28,7 @@ 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. 3. Check that the verified quote binds the nonce generated by your client, the reported `signing_address`, and its `signing_algo`. -4. When a TLS fingerprint was requested, verify its binding as part of the [Cloud API TLS flow](/cloud/verification/cloud-api/tls). +4. When a TLS fingerprint was requested, verify its binding as part of the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). 5. Apply your accepted measurement and image-provenance policy. `request_nonce` and `report_data` in the HTTP body are useful diagnostics. The verified quote is the cryptographic source of truth. diff --git a/cloud/verification/cloud-api/image-provenance.mdx b/cloud/verification/cloud-api/image-provenance.mdx index deb2319..ed60b72 100644 --- a/cloud/verification/cloud-api/image-provenance.mdx +++ b/cloud/verification/cloud-api/image-provenance.mdx @@ -1,11 +1,11 @@ --- title: "Image Provenance" -description: "Apply an image-provenance policy to configurations measured by Cloud API gateway and model attestations." +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 a Cloud API flow, decide separately whether your policy requires provenance for: +For a NEAR AI Cloud gateway flow, decide separately whether your policy requires provenance for: - the configuration measured by `gateway_attestation`; - each applicable model report in `model_attestations[]`; or @@ -25,7 +25,7 @@ For a Cloud API flow, decide separately whether your policy requires provenance ## Provider boundaries -Do not assume every Cloud API model report exposes configuration evidence in the same format. Apply this flow only where the provider's report format and your verifier establish the configuration-to-quote binding. An unfamiliar or incomplete report is not equivalent to a verified configuration. +Do not assume every model report returned through the NEAR AI Cloud gateway exposes configuration evidence in the same format. Apply this flow only where the provider's report format and your verifier establish the configuration-to-quote binding. An unfamiliar or incomplete report is not equivalent to a verified configuration. ## Missing evidence diff --git a/cloud/verification/cloud-api/index.mdx b/cloud/verification/cloud-api/index.mdx index 8700478..09730f5 100644 --- a/cloud/verification/cloud-api/index.mdx +++ b/cloud/verification/cloud-api/index.mdx @@ -1,11 +1,11 @@ --- -title: "Verify Cloud API" +title: "Verify the NEAR AI Cloud Gateway" description: "Verify gateway, model, TLS, response-signature, and image-provenance evidence for requests sent through cloud-api.near.ai." --- -Use this flow when your application sends requests to `https://cloud-api.near.ai`. The request path includes the Cloud API gateway and can include evidence for a selected model. +Use this flow when your application sends requests to `https://cloud-api.near.ai`. The request path includes the NEAR AI Cloud gateway and can include evidence for a selected model. -The relevant verification fields in a Cloud API response are: +The relevant verification fields in a NEAR AI Cloud gateway response are: ```text gateway_attestation @@ -13,7 +13,7 @@ model_attestations[] # returned when model evidence is available tls_certificate # optional ``` -`gateway_attestation` and `model_attestations[]` are separate pieces of evidence. Verify the gateway when you need evidence for the Cloud API request path; verify a model attestation when you also need evidence for the model environment. +`gateway_attestation` and `model_attestations[]` are separate pieces of evidence. Verify the gateway when you need evidence for the NEAR AI Cloud gateway request path; verify a model attestation when you also need evidence for the model environment. `model_attestations[]` is not an inventory of every healthy model instance. Do not use its length, or the presence of one entry, as evidence that a later request will be served by that same instance. @@ -22,7 +22,7 @@ tls_certificate # optional ## Recommended flow 1. Generate a fresh 32-byte nonce in the client for each verification attempt. -2. Request a Cloud API attestation report, including `model=` when model evidence is needed. +2. Request a NEAR AI Cloud gateway attestation report, including `model=` when model evidence is needed. 3. Verify `gateway_attestation` and apply your gateway measurement policy. 4. Verify the applicable item in `model_attestations[]` using the verifier and policy for its provider. 5. If you need endpoint binding, verify the TLS connection to `cloud-api.near.ai` against the **gateway** attestation. @@ -31,13 +31,13 @@ tls_certificate # optional - Verify the Cloud API gateway quote and signer binding. + Verify the gateway quote and signer binding. Verify provider model evidence returned in `model_attestations[]`. - Bind the live Cloud API TLS connection to `gateway_attestation`. + Bind the live gateway TLS connection to `gateway_attestation`. Match a response signature to its gateway or model signer. @@ -60,12 +60,12 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -H 'Authorization: Bearer ' ``` -Use the same `signing_algo` as the response signature you intend to verify. Add `include_tls_fingerprint=true` only when following the [Cloud API TLS flow](/cloud/verification/cloud-api/tls). +Use the same `signing_algo` as the response signature you intend to verify. Add `include_tls_fingerprint=true` only when following the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). ## Provider-specific model evidence -Cloud API can return model evidence from different provider tiers. The outer `model_attestations[]` field does not make their internal report formats interchangeable. +The NEAR AI Cloud gateway can return model evidence from different provider tiers. The outer `model_attestations[]` field does not make their internal report formats interchangeable. -This guide describes the Cloud API envelope and the NEAR-operated model flow. When your integration selects a different provider, use that provider's report format and verification policy; do not apply a NEAR-specific signer or measurement rule to an unfamiliar report shape. +This guide describes the NEAR AI Cloud gateway envelope and the NEAR-operated model flow. When your integration selects a different provider, use that provider's report format and verification policy; do not apply a NEAR-specific signer or measurement rule to an unfamiliar report shape. The report request accepts `provider=near` or `provider=chutes` when your integration needs to select a provider tier. Those choices can return different model-evidence formats, so make the selection part of your verification policy. diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index 11e754a..17cb2d1 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -1,11 +1,11 @@ --- title: "Model Attestations" -description: "Verify model evidence returned in the Cloud API model_attestations array." +description: "Verify model evidence returned in the NEAR AI Cloud gateway's model_attestations array." --- Use this page when a request to `cloud-api.near.ai` needs evidence for a model environment in addition to the gateway. -Cloud API returns model evidence in `model_attestations[]`; this is different from the direct endpoint's root model report and `all_attestations[]` field. Treat the Cloud API envelope and each provider report as separate contracts. +The NEAR AI Cloud gateway returns model evidence in `model_attestations[]`; this is different from the direct endpoint's root model report and `all_attestations[]` field. Treat the gateway envelope and each provider report as separate contracts. ## Request model evidence @@ -26,7 +26,7 @@ The response always has `gateway_attestation`; model evidence, when available, i When matching a response signature, request fresh evidence for that signer by adding both `signing_address=` and the signature's `signing_algo`. Do not use a report for a different signer as a substitute. -Cloud API also accepts `provider=near` or `provider=chutes`. These provider tiers can use different model-report formats and verification rules, so select one only when it is covered by your policy. +The NEAR AI Cloud gateway also accepts `provider=near` or `provider=chutes`. These provider tiers can use different model-report formats and verification rules, so select one only when it is covered by your policy. ## Verify the applicable entry @@ -39,10 +39,10 @@ Cloud API also accepts `provider=near` or `provider=chutes`. These provider tier Do not assume every `model_attestations[]` entry has the same fields, or that the array lists every model CVM behind a model URL. A response signature must be matched by signer identity and algorithm; a report from a different signer is not an equivalent substitute. -## Relationship to Cloud API TLS +## 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 [Cloud API TLS connection binding](/cloud/verification/cloud-api/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 Cloud API response with `signature_kind: "provider_tee"`, verify the signature over the endpoint-defined payload and match its signer to the applicable model attestation. For a `gateway` signature, match the signer to `gateway_attestation` instead. See [Cloud API response signatures](/cloud/verification/cloud-api/response-signatures). +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 the applicable model attestation. For a `gateway` signature, match the signer to `gateway_attestation` instead. See [NEAR AI Cloud gateway response signatures](/cloud/verification/cloud-api/response-signatures). diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx index 6540794..d00118f 100644 --- a/cloud/verification/cloud-api/response-signatures.mdx +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -1,9 +1,9 @@ --- title: "Response Signatures" -description: "Verify available Cloud API response signatures against the exact bytes and the matching attestation." +description: "Verify available response signatures from the NEAR AI Cloud gateway against the exact bytes and the matching attestation." --- -Cloud API response signatures can be produced by either a model-serving TEE or the Cloud API gateway. When present, `signature_kind` determines which attestation must match the signer. +Response signatures returned through the NEAR AI Cloud gateway can be produced by either a model-serving TEE or the gateway itself. When present, `signature_kind` determines which attestation 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. diff --git a/cloud/verification/cloud-api/tls.mdx b/cloud/verification/cloud-api/tls.mdx index d80120d..708d6e5 100644 --- a/cloud/verification/cloud-api/tls.mdx +++ b/cloud/verification/cloud-api/tls.mdx @@ -1,6 +1,6 @@ --- title: "TLS Connection Binding" -description: "Bind a live Cloud API TLS connection to the Cloud API gateway attestation." +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`. @@ -20,7 +20,7 @@ This is a gateway check. It does not bind the client connection to an upstream m 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; Cloud API report retrieval requires an API key. +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`. diff --git a/cloud/verification/direct/deployment-control-evidence.mdx b/cloud/verification/direct/deployment-control-evidence.mdx index 1c9f82f..5013956 100644 --- a/cloud/verification/direct/deployment-control-evidence.mdx +++ b/cloud/verification/direct/deployment-control-evidence.mdx @@ -3,7 +3,7 @@ title: "Deployment Control Evidence" description: "Interpret optional Compose Manager evidence returned by a direct model endpoint." --- -A direct model attestation response can include an optional top-level `compose_manager_attestation`. This is separate deployment-control evidence; it is not a model-attestation entry and is not part of the Cloud API attestation contract. +A direct model attestation response can include an optional top-level `compose_manager_attestation`. This is separate deployment-control evidence; it is not a model-attestation entry and is not part of the NEAR AI Cloud gateway attestation contract. The current payload records deployment-control actions and related integrity data. It can be useful for deployment investigation, but it has a narrower purpose than a model quote or a response signature. diff --git a/cloud/verification/direct/index.mdx b/cloud/verification/direct/index.mdx index 6261745..7106588 100644 --- a/cloud/verification/direct/index.mdx +++ b/cloud/verification/direct/index.mdx @@ -3,7 +3,7 @@ title: "Verify Direct Model Endpoints" description: "Verify model, TLS, response-signature, and image-provenance evidence for direct completions endpoints." --- -Use this flow when your application sends requests directly to `https://.completions.near.ai`. The Cloud API gateway is not in this request path. +Use this flow when your application sends requests directly to `https://.completions.near.ai`. The NEAR AI Cloud gateway is not in this request path. The direct attestation response is a model report at the response root. It includes fields such as `model_name`, `signing_address`, `signing_algo`, `request_nonce`, and `intel_quote`, and also includes `all_attestations[]`. diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx index 98f40a1..f5fdf9f 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -3,7 +3,7 @@ title: "Model Attestation Report" description: "Verify the root model report returned by a direct completions endpoint." --- -Use this page for a direct `https://.completions.near.ai` endpoint. This response is not the Cloud API `gateway_attestation` / `model_attestations[]` envelope. +Use this page for a direct `https://.completions.near.ai` endpoint. This response is not the NEAR AI Cloud gateway's `gateway_attestation` / `model_attestations[]` envelope. ## Request a fresh report diff --git a/cloud/verification/direct/response-signatures.mdx b/cloud/verification/direct/response-signatures.mdx index 1cf88d3..0ecc389 100644 --- a/cloud/verification/direct/response-signatures.mdx +++ b/cloud/verification/direct/response-signatures.mdx @@ -3,7 +3,7 @@ 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. The direct signature response has `text`, `signature`, `signing_address`, and `signing_algo`; it does not use the Cloud API `signature_kind` routing field. +Use this flow for a completion sent directly to a model endpoint. The direct signature response has `text`, `signature`, `signing_address`, and `signing_algo`; it does not use the NEAR AI Cloud gateway's `signature_kind` routing field. 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. @@ -35,7 +35,7 @@ Verify `signature` locally using the returned `signing_algo`. Then require that :: ``` -Use the `model_name` reported by the direct model attestation. A different string, even if it resembles the hostname or Cloud API model ID, does not satisfy this check. +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. ## Match the model signer diff --git a/cloud/verification/gateway.mdx b/cloud/verification/gateway.mdx index 791ee9c..681dc18 100644 --- a/cloud/verification/gateway.mdx +++ b/cloud/verification/gateway.mdx @@ -1,10 +1,10 @@ --- title: "Gateway Attestation" -description: "Verify gateway evidence for requests sent through the NEAR AI Cloud API." +description: "Verify gateway evidence for requests sent through the NEAR AI Cloud gateway." --- -This page has moved into the Cloud API verification flow. +This page has moved into the NEAR AI Cloud gateway verification flow. - + Open the route-specific guide for `gateway_attestation`. diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 4d84052..a7abc7d 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -1,29 +1,29 @@ --- title: "Verification" -description: "Choose the Cloud API or direct-model verification flow, then verify the evidence for that request path." +description: "Choose the NEAR AI Cloud gateway or direct-model verification flow, then verify the evidence for that request path." --- NEAR AI Cloud has two verification paths. They use different endpoints, response envelopes, and signer relationships. Select the path your application actually uses before interpreting an attestation report. - + Verify requests sent to cloud-api.near.ai, including its gateway attestation and any returned model attestations. - Verify requests sent directly to a model's completions.near.ai endpoint, without the Cloud API gateway in the request path. + Verify requests sent directly to a model's completions.near.ai endpoint, without the NEAR AI Cloud gateway in the request path. ## Why the paths are separate -| | Cloud API | Direct model endpoint | +| | NEAR AI Cloud gateway | Direct model endpoint | | --- | --- | --- | | Request host | `cloud-api.near.ai` | `.completions.near.ai` | | Attestation envelope | `gateway_attestation` and optional `model_attestations[]` | A model report at the response root, plus `all_attestations[]` | | Gateway evidence | Available as `gateway_attestation` | Not in the request path | | Response signature routing | `signature_kind` identifies a gateway or model-serving signer when present | Match the signature to the direct model report's signer | -Do not rename or map these fields as if they were aliases. In particular, `model_attestations[]` is a Cloud API response field; it is not the direct endpoint's `all_attestations[]` field. +Do not rename or map these fields as if they were aliases. In particular, `model_attestations[]` is a NEAR AI Cloud gateway response field; it is not the direct endpoint's `all_attestations[]` field. ## Shared requirements diff --git a/cloud/verification/model.mdx b/cloud/verification/model.mdx index 1ed1dd7..5a1b5d4 100644 --- a/cloud/verification/model.mdx +++ b/cloud/verification/model.mdx @@ -6,7 +6,7 @@ description: "Select the verification guide that matches the endpoint your appli The report structure depends on the endpoint, so select the guide for the request path your application uses. - + Use `model_attestations[]` returned by cloud-api.near.ai. diff --git a/cloud/verification/provenance.mdx b/cloud/verification/provenance.mdx index 81b91b2..62768c0 100644 --- a/cloud/verification/provenance.mdx +++ b/cloud/verification/provenance.mdx @@ -3,10 +3,10 @@ title: "Choose an Image-Provenance Flow" description: "Select the provenance guide for the attestation envelope your application uses." --- -The configuration to inspect is carried by different attestation envelopes for Cloud API and direct model endpoints. Select the matching path before evaluating image digests. +The configuration to inspect is carried by different attestation envelopes for the NEAR AI Cloud gateway and direct model endpoints. Select the matching path before evaluating image digests. - + Evaluate configuration measurements from the gateway and any returned model attestations. diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index 70e6fae..f8a7bd8 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -19,8 +19,8 @@ Send this value with the attestation request. Reject a report unless the verifie | Request path | Quote location | | --- | --- | -| Cloud API gateway | `gateway_attestation.intel_quote` | -| Cloud API model evidence | The provider-specific item in `model_attestations[]` | +| NEAR AI Cloud gateway | `gateway_attestation.intel_quote` | +| NEAR AI Cloud gateway model evidence | The provider-specific item in `model_attestations[]` | | Direct model endpoint | Root `intel_quote` (and any applicable direct report entry) | The direct report does not expose a top-level `report_data` field. Always obtain report data and measurements from the verified quote itself. diff --git a/cloud/verification/tls.mdx b/cloud/verification/tls.mdx index 9d44252..3828f2a 100644 --- a/cloud/verification/tls.mdx +++ b/cloud/verification/tls.mdx @@ -6,7 +6,7 @@ description: "Select the TLS verification guide that matches the endpoint your a TLS evidence applies to a particular request path and connection. Select the endpoint your application calls. - + Verify the TLS connection to cloud-api.near.ai against `gateway_attestation`. diff --git a/docs.json b/docs.json index 0776347..75c3f2a 100644 --- a/docs.json +++ b/docs.json @@ -68,7 +68,7 @@ "root": "cloud/verification/index", "pages": [ { - "group": "Cloud API", + "group": "NEAR AI Cloud Gateway", "root": "cloud/verification/cloud-api/index", "pages": [ "cloud/verification/cloud-api/gateway-attestation", From 5780840f1791796984c035ea2cf9d767f9bd72eb Mon Sep 17 00:00:00 2001 From: Coffee Date: Mon, 24 Aug 2026 20:57:10 +0800 Subject: [PATCH 05/36] docs: make verification guides task-oriented --- cloud/verification/chat.mdx | 6 +- cloud/verification/cloud-api/index.mdx | 64 ++++--------------- .../cloud-api/model-attestations.mdx | 12 ++-- .../direct/deployment-control-evidence.mdx | 6 +- cloud/verification/direct/index.mdx | 45 ++++--------- .../verification/direct/model-attestation.mdx | 4 +- .../direct/response-signatures.mdx | 4 +- cloud/verification/gateway.mdx | 4 +- cloud/verification/index.mdx | 35 ++++------ cloud/verification/model.mdx | 6 +- cloud/verification/provenance.mdx | 8 +-- cloud/verification/tls.mdx | 4 +- 12 files changed, 67 insertions(+), 131 deletions(-) diff --git a/cloud/verification/chat.mdx b/cloud/verification/chat.mdx index 2f94940..fdfbe75 100644 --- a/cloud/verification/chat.mdx +++ b/cloud/verification/chat.mdx @@ -3,13 +3,13 @@ title: "Choose a Response-Signature Flow" description: "Select the response-signature guide that matches the endpoint your application calls." --- -The signature payload and the attestation to match depend on the request path. +Choose the guide from the URL your application used for the completion. - Use `signature_kind` when present to select a gateway or model attestation. + Use this when your application sent the completion through `cloud-api.near.ai`. - Match the signature to the direct model report. + Use this when your application sent the completion directly to a model endpoint. diff --git a/cloud/verification/cloud-api/index.mdx b/cloud/verification/cloud-api/index.mdx index 09730f5..be17aa8 100644 --- a/cloud/verification/cloud-api/index.mdx +++ b/cloud/verification/cloud-api/index.mdx @@ -1,71 +1,35 @@ --- title: "Verify the NEAR AI Cloud Gateway" -description: "Verify gateway, model, TLS, response-signature, and image-provenance evidence for requests sent through cloud-api.near.ai." +description: "Verify the gateway, selected model, connection, response, and software evidence for requests sent through cloud-api.near.ai." --- -Use this flow when your application sends requests to `https://cloud-api.near.ai`. The request path includes the NEAR AI Cloud gateway and can include evidence for a selected model. +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. -The relevant verification fields in a NEAR AI Cloud gateway response are: - -```text -gateway_attestation -model_attestations[] # returned when model evidence is available -tls_certificate # optional -``` - -`gateway_attestation` and `model_attestations[]` are separate pieces of evidence. Verify the gateway when you need evidence for the NEAR AI Cloud gateway request path; verify a model attestation when you also need evidence for the model environment. - - - `model_attestations[]` is not an inventory of every healthy model instance. Do not use its length, or the presence of one entry, as evidence that a later request will be served by that same instance. - - -## Recommended flow - -1. Generate a fresh 32-byte nonce in the client for each verification attempt. -2. Request a NEAR AI Cloud gateway attestation report, including `model=` when model evidence is needed. -3. Verify `gateway_attestation` and apply your gateway measurement policy. -4. Verify the applicable item in `model_attestations[]` using the verifier and policy for its provider. -5. If you need endpoint binding, verify the TLS connection to `cloud-api.near.ai` against the **gateway** attestation. -6. If you need response integrity, verify the response signature and match its signer to the appropriate gateway or model attestation. -7. Apply an image-provenance policy to each measured configuration your policy requires. +## What you can verify - Verify the gateway quote and signer binding. + Verify the trusted environment that receives your request at the NEAR AI Cloud gateway. - Verify provider model evidence returned in `model_attestations[]`. + When your policy requires it, verify model evidence returned with the gateway report. - Bind the live gateway TLS connection to `gateway_attestation`. + Verify that your live HTTPS connection terminates at the attested gateway. - Match a response signature to its gateway or model signer. + Verify that a signed response came from the attested gateway or model environment. - Apply provenance policy to measured gateway and model configurations. + Check that the software images in the attested environment meet your source and build policy. -## Get a fresh report - -```bash -NONCE="$(openssl rand -hex 32)" - -curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ - --data-urlencode 'model=' \ - --data-urlencode 'signing_algo=ecdsa' \ - --data-urlencode "nonce=${NONCE}" \ - -H 'Accept: application/json' \ - -H 'Authorization: Bearer ' -``` - -Use the same `signing_algo` as the response signature you intend to verify. Add `include_tls_fingerprint=true` only when following the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). - -## Provider-specific model evidence - -The NEAR AI Cloud gateway can return model evidence from different provider tiers. The outer `model_attestations[]` field does not make their internal report formats interchangeable. +## Start with gateway attestation -This guide describes the NEAR AI Cloud gateway envelope and the NEAR-operated model flow. When your integration selects a different provider, use that provider's report format and verification policy; do not apply a NEAR-specific signer or measurement rule to an unfamiliar report shape. +1. Generate a fresh nonce for this verification attempt. +2. Request a fresh gateway attestation report. +3. Verify the gateway's hardware attestation and apply your acceptance policy. +4. Add model, TLS, response-signature, or image-provenance checks when your application requires them. -The report request accepts `provider=near` or `provider=chutes` when your integration needs to select a provider tier. Those choices can return different model-evidence formats, so make the selection part of your verification policy. +Start with [Gateway attestation](/cloud/verification/cloud-api/gateway-attestation). That guide shows how to request fresh evidence; the other guides explain the additional checks. diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index 17cb2d1..bda34aa 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -1,11 +1,9 @@ --- title: "Model Attestations" -description: "Verify model evidence returned in the NEAR AI Cloud gateway's model_attestations array." +description: "Verify evidence for the model environment selected for a request through the NEAR AI Cloud gateway." --- -Use this page when a request to `cloud-api.near.ai` needs evidence for a model environment in addition to the gateway. - -The NEAR AI Cloud gateway returns model evidence in `model_attestations[]`; this is different from the direct endpoint's root model report and `all_attestations[]` field. Treat the gateway envelope and each provider report as separate contracts. +Use this page when a request to `cloud-api.near.ai` needs evidence for a model environment in addition to the gateway. Model evidence is returned with the gateway attestation report. Its format and verification requirements can vary by provider, so use the guidance for the provider your application selected. ## Request model evidence @@ -22,11 +20,11 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -H 'Authorization: Bearer ' ``` -The response always has `gateway_attestation`; model evidence, when available, is carried in `model_attestations[]`. +The response includes gateway evidence. When model evidence is available, it is carried in `model_attestations[]`. When matching a response signature, request fresh evidence for that signer by adding both `signing_address=` and the signature's `signing_algo`. Do not use a report for a different signer as a substitute. -The NEAR AI Cloud gateway also accepts `provider=near` or `provider=chutes`. These provider tiers can use different model-report formats and verification rules, so select one only when it is covered by your policy. +Model providers can use different report formats and verification rules. Apply only the requirements documented for the provider your application uses; treat an unfamiliar report format as unverified under your policy. ## Verify the applicable entry @@ -36,7 +34,7 @@ The NEAR AI Cloud gateway also accepts `provider=near` or `provider=chutes`. The 4. Apply your own allowlist and provenance policy to the verified measurements. - Do not assume every `model_attestations[]` entry has the same fields, or that the array lists every model CVM behind a model URL. A response signature must be matched by signer identity and algorithm; a report from a different signer is not an equivalent substitute. + An entry in `model_attestations[]` is not an inventory of every model instance behind a URL. A response signature must be matched by signer identity and algorithm; a report from a different signer is not an equivalent substitute. ## Relationship to NEAR AI Cloud gateway TLS diff --git a/cloud/verification/direct/deployment-control-evidence.mdx b/cloud/verification/direct/deployment-control-evidence.mdx index 5013956..c6f2404 100644 --- a/cloud/verification/direct/deployment-control-evidence.mdx +++ b/cloud/verification/direct/deployment-control-evidence.mdx @@ -3,13 +3,11 @@ title: "Deployment Control Evidence" description: "Interpret optional Compose Manager evidence returned by a direct model endpoint." --- -A direct model attestation response can include an optional top-level `compose_manager_attestation`. This is separate deployment-control evidence; it is not a model-attestation entry and is not part of the NEAR AI Cloud gateway attestation contract. - -The current payload records deployment-control actions and related integrity data. It can be useful for deployment investigation, but it has a narrower purpose than a model quote or a response signature. +Some direct model reports include optional deployment-control evidence in `compose_manager_attestation`. It can help investigate deployment operations, but it does not verify an individual inference request. ## What it can and cannot establish -Use it as supplementary evidence about deployment-control activity. Do not use it alone to conclude that: +Use it as supplementary evidence about deployment activity. Do not use it alone to conclude that: - a particular project, model, or backend is currently running; - every recorded action completed successfully; diff --git a/cloud/verification/direct/index.mdx b/cloud/verification/direct/index.mdx index 7106588..b7dee3f 100644 --- a/cloud/verification/direct/index.mdx +++ b/cloud/verification/direct/index.mdx @@ -1,52 +1,35 @@ --- title: "Verify Direct Model Endpoints" -description: "Verify model, TLS, response-signature, and image-provenance evidence for direct completions endpoints." +description: "Verify the model, connection, response, and software evidence for direct completions endpoints." --- -Use this flow when your application sends requests directly to `https://.completions.near.ai`. The NEAR AI Cloud gateway is not in this request path. +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. -The direct attestation response is a model report at the response root. It includes fields such as `model_name`, `signing_address`, `signing_algo`, `request_nonce`, and `intel_quote`, and also includes `all_attestations[]`. - - - `all_attestations[]` is not a fleet inventory. Do not infer that it enumerates every healthy CVM, or that a later completion will be served by the same instance as an earlier report. - - -## Recommended flow - -1. Generate a fresh 32-byte nonce in the client for each verification attempt. -2. Request the report from the same direct model endpoint used for inference. -3. Verify the model's quote, nonce, signer binding, GPU evidence when present, and accepted measurements. -4. If you need endpoint binding, verify the TLS connection to the direct model endpoint against that model report. -5. If you need response integrity, verify the response signature and match it to the model report's signer. -6. Apply image-provenance policy to the measured model configuration. +## What you can verify - Verify the direct endpoint's root model report and `all_attestations[]` semantics. + Verify the trusted model environment reported by the direct endpoint. - Bind the live direct-model TLS connection to its model quote. + Verify that your live HTTPS connection terminates at the attested model endpoint. - Verify a direct-model response signature against the matching model signer. + Verify that a signed response came from the attested model environment. - Apply provenance policy to the direct model's measured configuration. + Check that the software images in the attested environment meet your source and build policy. - Interpret optional Compose Manager evidence without treating it as request-to-CVM binding. + Review optional deployment-management evidence separately from verification of an individual request. -## Get a fresh report - -```bash -NONCE="$(openssl rand -hex 32)" +## Start with model attestation -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' -``` +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. -The endpoint hostname identifies the model, so this request does not use a `model` parameter. Add `include_tls_fingerprint=true` only when following the [direct TLS flow](/cloud/verification/direct/tls). If you are matching a response signature, use its `signing_address` and `signing_algo` when requesting the applicable report. +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 index f5fdf9f..6186a88 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -3,7 +3,7 @@ title: "Model Attestation Report" description: "Verify the root model report returned by a direct completions endpoint." --- -Use this page for a direct `https://.completions.near.ai` endpoint. This response is not the NEAR AI Cloud gateway's `gateway_attestation` / `model_attestations[]` envelope. +Use this page to verify the model environment serving a direct `https://.completions.near.ai` endpoint. ## Request a fresh report @@ -26,7 +26,7 @@ The root response is the model report. Relevant fields include: | Field | Meaning | | --- | --- | -| `model_name` | The model name configured for this direct endpoint. | +| `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. | | `intel_quote` | The model's Intel TDX quote. | diff --git a/cloud/verification/direct/response-signatures.mdx b/cloud/verification/direct/response-signatures.mdx index 0ecc389..76aa253 100644 --- a/cloud/verification/direct/response-signatures.mdx +++ b/cloud/verification/direct/response-signatures.mdx @@ -3,7 +3,7 @@ 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. The direct signature response has `text`, `signature`, `signing_address`, and `signing_algo`; it does not use the NEAR AI Cloud gateway's `signature_kind` routing field. +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. @@ -41,6 +41,6 @@ Use the `model_name` reported by the direct model attestation. A different strin 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/model classification from the format of the signed text, and do not substitute a report for a different signer. The direct endpoint's `all_attestations[]` field is not proof that every instance behind the hostname has been attested. +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/gateway.mdx b/cloud/verification/gateway.mdx index 681dc18..5e789d8 100644 --- a/cloud/verification/gateway.mdx +++ b/cloud/verification/gateway.mdx @@ -3,8 +3,8 @@ title: "Gateway Attestation" description: "Verify gateway evidence for requests sent through the NEAR AI Cloud gateway." --- -This page has moved into the NEAR AI Cloud gateway verification flow. +This guide is now part of the NEAR AI Cloud gateway verification flow. - Open the route-specific guide for `gateway_attestation`. + Verify the trusted environment that receives requests through `cloud-api.near.ai`. diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index a7abc7d..7f549bc 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -1,43 +1,36 @@ --- title: "Verification" -description: "Choose the NEAR AI Cloud gateway or direct-model verification flow, then verify the evidence for that request path." +description: "Choose the verification guide that matches the URL your application uses for inference." --- -NEAR AI Cloud has two verification paths. They use different endpoints, response envelopes, and signer relationships. Select the path your application actually uses before interpreting an attestation report. +Use these guides to verify the evidence returned for your AI requests. Start with the URL your application sends requests to. + +## Choose your request path - Verify requests sent to cloud-api.near.ai, including its gateway attestation and any returned model attestations. + Use this when your application sends requests through `cloud-api.near.ai`. Verify the gateway and, when available, model evidence returned with the report. - Verify requests sent directly to a model's completions.near.ai endpoint, without the NEAR AI Cloud gateway in the request path. + Use this when your application connects directly to a model's `completions.near.ai` endpoint. Verify the model environment reported by that endpoint. -## Why the paths are separate - -| | NEAR AI Cloud gateway | Direct model endpoint | -| --- | --- | --- | -| Request host | `cloud-api.near.ai` | `.completions.near.ai` | -| Attestation envelope | `gateway_attestation` and optional `model_attestations[]` | A model report at the response root, plus `all_attestations[]` | -| Gateway evidence | Available as `gateway_attestation` | Not in the request path | -| Response signature routing | `signature_kind` identifies a gateway or model-serving signer when present | Match the signature to the direct model report's signer | + + Choose the guide from the request URL in your application configuration, not from the model name. If your application uses both request paths, verify each request with the guide for the path it used. + -Do not rename or map these fields as if they were aliases. In particular, `model_attestations[]` is a NEAR AI Cloud gateway response field; it is not the direct endpoint's `all_attestations[]` field. +## What happens next -## Shared requirements +After you choose a path, its guide walks you through obtaining fresh attestation evidence and verifying the environment that handled the request. Depending on your application's policy, you can also verify the TLS connection, signed responses, and software-image provenance. -Both paths require a client-generated nonce, a verified Intel TDX quote, a signer binding, and an explicit acceptance policy. The path guides show where to find those values; the reference pages explain how to evaluate them. +## Common verification concepts - Verify fresh evidence from the quote rather than trusting echoed HTTP fields. + Learn how fresh attestation evidence is tied to your request and to a signing identity. - Define required checks, handling for unavailable evidence, and image-provenance policy. + Decide which checks, measurements, and source-provenance requirements your application accepts. - - - A report for one connection or one instance is not automatically evidence for another. For a TLS conclusion, obtain the peer certificate and report on the same TLS connection. For a signed response, match its signer and algorithm to the applicable attestation. - diff --git a/cloud/verification/model.mdx b/cloud/verification/model.mdx index 5a1b5d4..a126551 100644 --- a/cloud/verification/model.mdx +++ b/cloud/verification/model.mdx @@ -3,13 +3,13 @@ title: "Choose a Model Attestation Flow" description: "Select the verification guide that matches the endpoint your application calls." --- -The report structure depends on the endpoint, so select the guide for the request path your application uses. +Choose the guide from the URL your application uses for inference. - Use `model_attestations[]` returned by cloud-api.near.ai. + Use this when your request goes through `cloud-api.near.ai` and you need to verify the selected model environment. - Use the model report returned by a direct completions endpoint. + Use this when your application sends requests directly to a model's `completions.near.ai` endpoint. diff --git a/cloud/verification/provenance.mdx b/cloud/verification/provenance.mdx index 62768c0..700a62c 100644 --- a/cloud/verification/provenance.mdx +++ b/cloud/verification/provenance.mdx @@ -1,15 +1,15 @@ --- title: "Choose an Image-Provenance Flow" -description: "Select the provenance guide for the attestation envelope your application uses." +description: "Select the provenance guide that matches the endpoint your application uses." --- -The configuration to inspect is carried by different attestation envelopes for the NEAR AI Cloud gateway and direct model endpoints. Select the matching path before evaluating image digests. +Choose the guide from the URL your application uses for inference. - Evaluate configuration measurements from the gateway and any returned model attestations. + Use this when your application sends requests through `cloud-api.near.ai`. - Evaluate the measured configuration from the direct model report. + Use this when your application sends requests directly to a model's `completions.near.ai` endpoint. diff --git a/cloud/verification/tls.mdx b/cloud/verification/tls.mdx index 3828f2a..b89278d 100644 --- a/cloud/verification/tls.mdx +++ b/cloud/verification/tls.mdx @@ -7,9 +7,9 @@ TLS evidence applies to a particular request path and connection. Select the end - Verify the TLS connection to cloud-api.near.ai against `gateway_attestation`. + Use this when your application connects to `cloud-api.near.ai`. - Verify the TLS connection to a direct model endpoint against its model report. + Use this when your application connects directly to a model's `completions.near.ai` endpoint. From 147a9d5e6eff490f92d66975f290a46470dfab51 Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 10:35:06 +0800 Subject: [PATCH 06/36] docs: refer to inference endpoints --- cloud/verification/index.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 7f549bc..a24c85f 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -1,9 +1,9 @@ --- title: "Verification" -description: "Choose the verification guide that matches the URL your application uses for inference." +description: "Choose the verification guide that matches the inference endpoint your application calls." --- -Use these guides to verify the evidence returned for your AI requests. Start with the URL your application sends requests to. +Use these guides to verify the evidence returned for your AI requests. Start with the inference endpoint your application calls. ## Choose your request path @@ -17,7 +17,7 @@ Use these guides to verify the evidence returned for your AI requests. Start wit - Choose the guide from the request URL in your application configuration, not from the model name. If your application uses both request paths, verify each request with the guide for the path it used. + Choose the guide from the inference endpoint in your application configuration, not from the model name. If your application uses both request paths, verify each request with the guide for the endpoint it used. ## What happens next From b5b55160411bd0fadc0af2b280050b7400c12c37 Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 12:55:19 +0800 Subject: [PATCH 07/36] docs: remove redundant direct report caveat --- cloud/verification/direct/model-attestation.mdx | 2 -- cloud/verification/reference/quote-nonce-signer.mdx | 2 -- 2 files changed, 4 deletions(-) diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx index 6186a88..e66d141 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -35,8 +35,6 @@ The root response is the model report. Relevant fields include: | `all_attestations[]` | Model-report entries in this direct response envelope. It can include the root report and is not a fleet inventory. | | `compose_manager_attestation` | Optional, separate deployment-control evidence. See [deployment control evidence](/cloud/verification/direct/deployment-control-evidence). | -The direct response does not define a top-level `report_data` field. Read `report_data` and measurements from the verified TDX quote. - ## Verify the report 1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index f8a7bd8..33ee70f 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -23,8 +23,6 @@ Send this value with the attestation request. Reject a report unless the verifie | NEAR AI Cloud gateway model evidence | The provider-specific item in `model_attestations[]` | | Direct model endpoint | Root `intel_quote` (and any applicable direct report entry) | -The direct report does not expose a top-level `report_data` field. Always obtain report data and measurements from the verified quote itself. - ## Verify the signer binding For a NEAR-operated TLS-bound report, the verified quote binds the signer identity, TLS certificate fingerprint, and nonce. A provider can use a different report format, so verify the exact binding required by the selected endpoint and provider flow before accepting the report. From 205fa56536f13895d26761e72b031bff425ae6b1 Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 13:01:17 +0800 Subject: [PATCH 08/36] docs: clarify safe provenance support details --- cloud/verification/cloud-api/image-provenance.mdx | 6 +++++- cloud/verification/direct/image-provenance.mdx | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/cloud/verification/cloud-api/image-provenance.mdx b/cloud/verification/cloud-api/image-provenance.mdx index ed60b72..b982276 100644 --- a/cloud/verification/cloud-api/image-provenance.mdx +++ b/cloud/verification/cloud-api/image-provenance.mdx @@ -31,4 +31,8 @@ Do not assume every model report returned through the NEAR AI Cloud gateway expo 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, include the endpoint, UTC time, requested model, signing address and `info.instance_id` when present, image name, and full digestβ€”never API keys, prompts, or response content. +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/direct/image-provenance.mdx b/cloud/verification/direct/image-provenance.mdx index 7af46ef..26c8a6c 100644 --- a/cloud/verification/direct/image-provenance.mdx +++ b/cloud/verification/direct/image-provenance.mdx @@ -25,4 +25,8 @@ The quote and configuration measurement establish that the configuration text yo 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, include the endpoint, UTC time, model name, signing address and `info.instance_id` when present, image name, and full digestβ€”never API keys, prompts, or response content. +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. + From e68dd60311fa43faa91c28a61d8ddda9fa836453 Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 13:04:01 +0800 Subject: [PATCH 09/36] docs: remove unsupported provenance caveat --- cloud/verification/cloud-api/image-provenance.mdx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cloud/verification/cloud-api/image-provenance.mdx b/cloud/verification/cloud-api/image-provenance.mdx index b982276..52aeeaa 100644 --- a/cloud/verification/cloud-api/image-provenance.mdx +++ b/cloud/verification/cloud-api/image-provenance.mdx @@ -23,10 +23,6 @@ For a NEAR AI Cloud gateway flow, decide separately whether your policy requires 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. -## Provider boundaries - -Do not assume every model report returned through the NEAR AI Cloud gateway exposes configuration evidence in the same format. Apply this flow only where the provider's report format and your verifier establish the configuration-to-quote binding. An unfamiliar or incomplete report is not equivalent to a verified configuration. - ## 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. From e4aac765f9f62f4fa4e0666573591c32cad96fad Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 13:41:47 +0800 Subject: [PATCH 10/36] docs: scope gateway model verification to NEAR --- cloud/guides/e2ee-chat-completions.mdx | 16 +++---- .../cloud-api/gateway-attestation.mdx | 6 +-- .../cloud-api/image-provenance.mdx | 4 +- cloud/verification/cloud-api/index.mdx | 4 +- .../cloud-api/model-attestations.mdx | 42 ++++++++++++++----- .../cloud-api/response-signatures.mdx | 12 ++++-- .../verification/direct/image-provenance.mdx | 2 +- .../verification/direct/model-attestation.mdx | 2 +- .../direct/response-signatures.mdx | 6 ++- cloud/verification/direct/tls.mdx | 2 + cloud/verification/model.mdx | 2 +- .../reference/quote-nonce-signer.mdx | 8 ++-- .../reference/verification-policy.mdx | 4 +- 13 files changed, 72 insertions(+), 38 deletions(-) diff --git a/cloud/guides/e2ee-chat-completions.mdx b/cloud/guides/e2ee-chat-completions.mdx index 44dbbb1..e036c35 100644 --- a/cloud/guides/e2ee-chat-completions.mdx +++ b/cloud/guides/e2ee-chat-completions.mdx @@ -129,7 +129,7 @@ def v2_decrypt(data: bytes, secret_x25519: bytes) -> bytes: attestation = requests.get( f"{ENDPOINT}/v1/attestation/report", headers={"Authorization": f"Bearer {API_KEY}"}, - params={"model": MODEL, "signing_algo": "ed25519"}, + params={"model": MODEL, "provider": "near", "signing_algo": "ed25519"}, ).json() model_ed25519_pub = bytes.fromhex(attestation["model_attestations"][0]["signing_public_key"]) model_x25519_pub = crypto_sign_ed25519_pk_to_curve25519(model_ed25519_pub) @@ -187,7 +187,7 @@ 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" \ +curl "https://cloud-api.near.ai/v1/attestation/report?model=zai-org/GLM-5.1-FP8&provider=near&signing_algo=ed25519" \ -H 'Authorization: Bearer ' ``` @@ -211,6 +211,7 @@ response = requests.get( headers={"Authorization": f"Bearer {os.environ['NEAR_AI_CLOUD_API_KEY']}"}, params={ "model": "zai-org/GLM-5.1-FP8", + "provider": "near", "signing_algo": "ed25519" } ) @@ -235,10 +236,11 @@ print(f"Model Ed25519 public key: {model_public_key_hex}") ```javascript 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' + }), { headers: { 'Authorization': `Bearer ${process.env.NEAR_AI_CLOUD_API_KEY}`, @@ -254,7 +256,7 @@ console.log('Model Ed25519 public key:', modelPublicKeyHex); -For the NEAR AI Cloud gateway, the model key is in `model_attestations[0].signing_public_key`. For a direct completions endpoint, it is in the root report's `signing_public_key`. +For the NEAR AI Cloud gateway, request `provider=near` before reading `model_attestations[0].signing_public_key`. For a direct completions endpoint, the key is in the root report's `signing_public_key`. --- diff --git a/cloud/verification/cloud-api/gateway-attestation.mdx b/cloud/verification/cloud-api/gateway-attestation.mdx index 47a3750..584232c 100644 --- a/cloud/verification/cloud-api/gateway-attestation.mdx +++ b/cloud/verification/cloud-api/gateway-attestation.mdx @@ -19,7 +19,7 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -H 'Authorization: Bearer ' ``` -The response contains the gateway report at `gateway_attestation`. Add `model=` only when you also need model evidence, and add `include_tls_fingerprint=true` only when you are following the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). +The response contains the gateway report at `gateway_attestation`. Add `model=` and `provider=near` only when you also need the [NEAR model report](/cloud/verification/cloud-api/model-attestations), and 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 @@ -27,7 +27,7 @@ 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. -3. Check that the verified quote binds the nonce generated by your client, the reported `signing_address`, and its `signing_algo`. +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. When a TLS fingerprint was requested, verify its binding as part of the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). 5. Apply your accepted measurement and image-provenance policy. @@ -40,7 +40,7 @@ For `gateway_attestation`: | `gateway_attestation.intel_quote` | The gateway's Intel TDX 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. | -| `model_attestations[]` | Separate model evidence; do not use it as a substitute for the gateway quote. | +| `model_attestations[]` | Separate NEAR model evidence when requested with `model` and `provider=near`; do not use it as a substitute for the gateway quote. | ## Freshness and failure handling diff --git a/cloud/verification/cloud-api/image-provenance.mdx b/cloud/verification/cloud-api/image-provenance.mdx index 52aeeaa..5ce0048 100644 --- a/cloud/verification/cloud-api/image-provenance.mdx +++ b/cloud/verification/cloud-api/image-provenance.mdx @@ -8,13 +8,13 @@ Image provenance is separate from hardware attestation. A verified quote can bin For a NEAR AI Cloud gateway flow, decide separately whether your policy requires provenance for: - the configuration measured by `gateway_attestation`; -- each applicable model report in `model_attestations[]`; or +- the NEAR model report in `model_attestations[]`, requested with `provider=near`; or - both. ## Verification flow 1. Verify the relevant Intel TDX quote first. -2. Obtain the configuration text from that report, calculate its configuration hash, and compare it with the configuration measurement in the verified quote. +2. For a 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 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. diff --git a/cloud/verification/cloud-api/index.mdx b/cloud/verification/cloud-api/index.mdx index be17aa8..3b7ad58 100644 --- a/cloud/verification/cloud-api/index.mdx +++ b/cloud/verification/cloud-api/index.mdx @@ -11,8 +11,8 @@ Use this guide when your application sends inference requests to `https://cloud- Verify the trusted environment that receives your request at the NEAR AI Cloud gateway. - - When your policy requires it, verify model evidence returned with the gateway report. + + Request `provider=near` to verify the NEAR model report returned with the gateway report. Verify that your live HTTPS connection terminates at the attested gateway. diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index bda34aa..e82ca8e 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -1,9 +1,9 @@ --- -title: "Model Attestations" -description: "Verify evidence for the model environment selected for a request through the NEAR AI Cloud gateway." +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 model environment in addition to the gateway. Model evidence is returned with the gateway attestation report. Its format and verification requirements can vary by provider, so use the guidance for the provider your application selected. +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 @@ -14,27 +14,43 @@ 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 'signing_algo=ecdsa' \ --data-urlencode "nonce=${NONCE}" \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' ``` -The response includes gateway evidence. When model evidence is available, it is carried in `model_attestations[]`. +The response includes gateway evidence and one NEAR model report in `model_attestations[]`. When matching a response signature, request fresh evidence for that signer by adding both `signing_address=` and the signature's `signing_algo`. Do not use a report for a different signer as a substitute. -Model providers can use different report formats and verification rules. Apply only the requirements documented for the provider your application uses; treat an unfamiliar report format as unverified under your policy. +## NEAR model report fields -## Verify the applicable entry +The NEAR report uses these fields: -1. Determine which provider's model evidence your request is using. -2. Verify the Intel TDX quote, nonce, signer binding, GPU evidence, and measurement according to that provider's verification rules. -3. When verifying a response signed by a model-serving TEE, request or select an entry whose `signing_address` **and** `signing_algo` match the response signature. -4. Apply your own allowlist and provenance policy to the verified measurements. +| 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; confirm its binding from the verified quote. | +| `signing_address` and `signing_algo` | Model signing identity and its algorithm. | +| `signing_public_key` | Model public key for the E2EE flow; request `signing_algo=ed25519` for that flow. | +| `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 the NEAR model report + +1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. +2. Check the client-generated nonce and `signing_address` against the verified quote. Interpret the address using `signing_algo`, and compare both values when matching a response signature. +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, verify `nvidia_payload` and require it to be bound to the same nonce. +6. When verifying a model-serving response signature, request a report whose `signing_address` and `signing_algo` match that signature. +7. Apply your own allowlist and provenance policy to the verified measurements. - An entry in `model_attestations[]` is not an inventory of every model instance behind a URL. A response signature must be matched by signer identity and algorithm; a report from a different signer is not an equivalent substitute. + `model_attestations[]` is not an inventory of every model instance behind a URL. A response signature must be matched by signer identity and algorithm; a report from a different signer is not an equivalent substitute. ## Relationship to NEAR AI Cloud gateway TLS @@ -44,3 +60,7 @@ The browser or client connection is to `cloud-api.near.ai`. Its TLS endpoint bin ## 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 the applicable model attestation. For a `gateway` signature, match the signer to `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 index d00118f..302bceb 100644 --- a/cloud/verification/cloud-api/response-signatures.mdx +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -22,6 +22,8 @@ Do not parse and re-serialize JSON, normalize whitespace or line endings, omit s ## 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=ecdsa' \ @@ -29,11 +31,15 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/signature/' \ -H 'Authorization: Bearer ' ``` -Verify `signature` locally using the returned `signing_algo`, then compare `text` with the expected payload: +A `200` response containing `error_code` and `message` is an unavailable result, not a signature. + +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 attestation match | | --- | --- | --- | -| `provider_tee` | `::` | An applicable `model_attestations[]` item with the same `signing_address` and `signing_algo`. | +| `provider_tee` | `::` | A NEAR model report requested with `provider=near`, with the same `signing_address` and `signing_algo`. | | `gateway` | `:` | `gateway_attestation` with the same `signing_address` and `signing_algo`. | 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. @@ -42,6 +48,6 @@ For `provider_tee`, `` is the canonical model identifier in the signed ## Match the attestation -For the selected signer, request a fresh report with a client-generated nonce and the signature's `signing_address` and `signing_algo`. Include `model=` for model evidence. Then verify the quote and apply the required measurement and provenance policy. A signature made by a different signer is not evidence for the response, even if the model name or endpoint is the same. +For the selected signer, request a fresh report with a client-generated nonce and the signature's `signing_address` and `signing_algo`. For `provider_tee`, include `model=` and `provider=near` to obtain the NEAR model report. Then verify the quote and apply the required measurement and provenance policy. A signature made by a different signer is not evidence for the response, even if the model name or endpoint is the same. `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/image-provenance.mdx b/cloud/verification/direct/image-provenance.mdx index 26c8a6c..3236385 100644 --- a/cloud/verification/direct/image-provenance.mdx +++ b/cloud/verification/direct/image-provenance.mdx @@ -8,7 +8,7 @@ Image provenance is separate from hardware attestation. A verified direct model ## Verification flow 1. Verify the direct model's Intel TDX quote first. -2. Obtain the configuration text from the direct report, calculate its configuration hash, and compare it with the configuration measurement in the verified quote. +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. diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx index e66d141..902ecaa 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -38,7 +38,7 @@ The root response is the model report. Relevant fields include: ## 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, signer identity, and signing algorithm against the verified quote. +2. Check the client-generated nonce and signer identity against the verified quote. Use `signing_algo` to interpret that identity and verify response signatures. 3. When GPU evidence is present, verify it with the applicable NVIDIA verifier and ensure it is bound to the same nonce. 4. Apply your accepted measurement policy to the verified configuration. 5. If source and build provenance are required, continue with [direct image provenance](/cloud/verification/direct/image-provenance). diff --git a/cloud/verification/direct/response-signatures.mdx b/cloud/verification/direct/response-signatures.mdx index 76aa253..caa657c 100644 --- a/cloud/verification/direct/response-signatures.mdx +++ b/cloud/verification/direct/response-signatures.mdx @@ -22,6 +22,8 @@ Do not parse and re-serialize JSON, normalize whitespace or line endings, omit s ## 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=ecdsa' \ @@ -29,7 +31,9 @@ curl --fail-with-body -G 'https://.completions.near.ai/v1/signature/ -H 'Authorization: Bearer ' ``` -Verify `signature` locally using the returned `signing_algo`. Then require that `text` is exactly: +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 :: diff --git a/cloud/verification/direct/tls.mdx b/cloud/verification/direct/tls.mdx index 311cbe0..eaacd68 100644 --- a/cloud/verification/direct/tls.mdx +++ b/cloud/verification/direct/tls.mdx @@ -26,6 +26,8 @@ This check applies to the direct model connection. It does not verify a connecti 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 | diff --git a/cloud/verification/model.mdx b/cloud/verification/model.mdx index a126551..5d2e7c4 100644 --- a/cloud/verification/model.mdx +++ b/cloud/verification/model.mdx @@ -7,7 +7,7 @@ Choose the guide from the URL your application uses for inference. - Use this when your request goes through `cloud-api.near.ai` and you need to verify the selected model environment. + Use this when your request goes through `cloud-api.near.ai`, then request `provider=near` to verify the NEAR model environment. Use this when your application sends requests directly to a model's `completions.near.ai` endpoint. diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index 33ee70f..2ea0b6f 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -3,7 +3,7 @@ title: "Quote, Nonce, and Signer Bindings" description: "Shared rules for reading verified quotes, checking freshness, and matching signing identities." --- -Every 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. +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 @@ -20,12 +20,12 @@ Send this value with the attestation request. Reject a report unless the verifie | Request path | Quote location | | --- | --- | | NEAR AI Cloud gateway | `gateway_attestation.intel_quote` | -| NEAR AI Cloud gateway model evidence | The provider-specific item in `model_attestations[]` | +| NEAR AI Cloud gateway model evidence (`provider=near`) | The single model report in `model_attestations[]` | | Direct model endpoint | Root `intel_quote` (and any applicable direct report entry) | ## Verify the signer binding -For a NEAR-operated TLS-bound report, the verified quote binds the signer identity, TLS certificate fingerprint, and nonce. A provider can use a different report format, so verify the exact binding required by the selected endpoint and provider flow before accepting the report. +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: @@ -42,4 +42,4 @@ When a report contains NVIDIA GPU evidence, verify it with the applicable NVIDIA ## Reference code -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for individual quote, GPU, TLS, signature, and configuration checks. Use it as an implementation reference, then enforce the acceptance policy required by your application. +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for NEAR quote, GPU, and configuration processing. Use the report-request examples in these guides; the repository is not a complete Cloud API client. Follow the endpoint-specific guides for TLS binding, response signatures, and image provenance. diff --git a/cloud/verification/reference/verification-policy.mdx b/cloud/verification/reference/verification-policy.mdx index c76f4b7..59733b8 100644 --- a/cloud/verification/reference/verification-policy.mdx +++ b/cloud/verification/reference/verification-policy.mdx @@ -19,7 +19,7 @@ At minimum, specify: ## 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, model instance, or provider. +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 | | --- | --- | @@ -27,7 +27,7 @@ When a required check is missing, unavailable, malformed, mismatched, or fails v | 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. | -| Provider report shape is not covered by your verifier | Treat the provider-specific property as unverified. | +| Required NEAR model report is unavailable or malformed | Treat the model property as unverified. | ## Load balancing and rotations From 4337b969df5396d5b36001409bde4568a59504a4 Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 14:45:09 +0800 Subject: [PATCH 11/36] docs: clarify model-provider verification scope --- cloud/guides/e2ee-chat-completions.mdx | 2 ++ cloud/verification/cloud-api/image-provenance.mdx | 2 ++ cloud/verification/cloud-api/model-attestations.mdx | 4 ++++ cloud/verification/cloud-api/response-signatures.mdx | 2 ++ 4 files changed, 10 insertions(+) diff --git a/cloud/guides/e2ee-chat-completions.mdx b/cloud/guides/e2ee-chat-completions.mdx index e036c35..247f6ff 100644 --- a/cloud/guides/e2ee-chat-completions.mdx +++ b/cloud/guides/e2ee-chat-completions.mdx @@ -8,6 +8,8 @@ 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: diff --git a/cloud/verification/cloud-api/image-provenance.mdx b/cloud/verification/cloud-api/image-provenance.mdx index 5ce0048..80b9617 100644 --- a/cloud/verification/cloud-api/image-provenance.mdx +++ b/cloud/verification/cloud-api/image-provenance.mdx @@ -5,6 +5,8 @@ description: "Apply an image-provenance policy to configurations measured in NEA 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 the NEAR model report requested with `provider=near`. + For a NEAR AI Cloud gateway flow, decide separately whether your policy requires provenance for: - the configuration measured by `gateway_attestation`; diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index e82ca8e..5f01e53 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -25,6 +25,10 @@ The response includes gateway evidence and one NEAR model report in `model_attes When matching a response signature, request fresh evidence for that signer by adding both `signing_address=` and the signature's `signing_algo`. Do not use a report for a different signer as a substitute. + + `provider=near` selects the model report returned by this request. It does not select the provider for a later completion. Treat a report as evidence for a completion only when its signer and algorithm match a `provider_tee` response signature. + + ## NEAR model report fields The NEAR report uses these fields: diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx index 302bceb..82a0627 100644 --- a/cloud/verification/cloud-api/response-signatures.mdx +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -42,6 +42,8 @@ Then compare `text` with the expected payload: | `provider_tee` | `::` | A NEAR model report requested with `provider=near`, with the same `signing_address` and `signing_algo`. | | `gateway` | `:` | `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. + 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. Do not assume it is an alias originally supplied by the client. From fd5dcf29c4c40b887668031b4bc15d63ef9dd414 Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 15:05:14 +0800 Subject: [PATCH 12/36] docs: simplify verification entry guidance --- cloud/verification/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index a24c85f..eea7381 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -17,7 +17,7 @@ Use these guides to verify the evidence returned for your AI requests. Start wit - Choose the guide from the inference endpoint in your application configuration, not from the model name. If your application uses both request paths, verify each request with the guide for the endpoint it used. + 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. ## What happens next From 925101816d854270e02f849177a383b36b54e33a Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 15:15:13 +0800 Subject: [PATCH 13/36] docs: simplify model attestation navigation --- cloud/verification/cloud-api/index.mdx | 4 ++-- cloud/verification/model.mdx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cloud/verification/cloud-api/index.mdx b/cloud/verification/cloud-api/index.mdx index 3b7ad58..56de90b 100644 --- a/cloud/verification/cloud-api/index.mdx +++ b/cloud/verification/cloud-api/index.mdx @@ -11,8 +11,8 @@ Use this guide when your application sends inference requests to `https://cloud- Verify the trusted environment that receives your request at the NEAR AI Cloud gateway. - - Request `provider=near` to verify the NEAR model report returned with the gateway report. + + Verify the environment serving the selected model. Verify that your live HTTPS connection terminates at the attested gateway. diff --git a/cloud/verification/model.mdx b/cloud/verification/model.mdx index 5d2e7c4..701b66a 100644 --- a/cloud/verification/model.mdx +++ b/cloud/verification/model.mdx @@ -6,8 +6,8 @@ description: "Select the verification guide that matches the endpoint your appli Choose the guide from the URL your application uses for inference. - - Use this when your request goes through `cloud-api.near.ai`, then request `provider=near` to verify the NEAR model environment. + + Use this when your request goes through `cloud-api.near.ai`. Use this when your application sends requests directly to a model's `completions.near.ai` endpoint. From d68852cfde5237e3ff64c4dcbf64753ed053ffeb Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 16:24:53 +0800 Subject: [PATCH 14/36] docs: simplify verification prose --- .../verification/cloud-api/gateway-attestation.mdx | 4 ++-- cloud/verification/cloud-api/image-provenance.mdx | 8 ++++---- cloud/verification/cloud-api/index.mdx | 2 +- .../verification/cloud-api/model-attestations.mdx | 10 +++++----- cloud/verification/cloud-api/tls.mdx | 2 +- .../direct/deployment-control-evidence.mdx | 8 ++++---- cloud/verification/direct/image-provenance.mdx | 2 +- cloud/verification/direct/index.mdx | 2 +- cloud/verification/direct/model-attestation.mdx | 2 +- cloud/verification/direct/tls.mdx | 2 +- .../verification/reference/quote-nonce-signer.mdx | 4 ++-- .../verification/reference/verification-policy.mdx | 14 +++++++------- 12 files changed, 30 insertions(+), 30 deletions(-) diff --git a/cloud/verification/cloud-api/gateway-attestation.mdx b/cloud/verification/cloud-api/gateway-attestation.mdx index 584232c..1af9871 100644 --- a/cloud/verification/cloud-api/gateway-attestation.mdx +++ b/cloud/verification/cloud-api/gateway-attestation.mdx @@ -3,7 +3,7 @@ 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. +`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 @@ -40,7 +40,7 @@ For `gateway_attestation`: | `gateway_attestation.intel_quote` | The gateway's Intel TDX 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. | -| `model_attestations[]` | Separate NEAR model evidence when requested with `model` and `provider=near`; do not use it as a substitute for the gateway quote. | +| `model_attestations[]` | Separate NEAR model evidence when requested with `model` and `provider=near`. Do not use it as a substitute for the gateway quote. | ## Freshness and failure handling diff --git a/cloud/verification/cloud-api/image-provenance.mdx b/cloud/verification/cloud-api/image-provenance.mdx index 80b9617..19572ce 100644 --- a/cloud/verification/cloud-api/image-provenance.mdx +++ b/cloud/verification/cloud-api/image-provenance.mdx @@ -3,15 +3,15 @@ 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. +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 the NEAR model report requested with `provider=near`. For a NEAR AI Cloud gateway flow, decide separately whether your policy requires provenance for: -- the configuration measured by `gateway_attestation`; -- the NEAR model report in `model_attestations[]`, requested with `provider=near`; or -- both. +- the configuration measured by `gateway_attestation` +- the NEAR model report in `model_attestations[]`, requested with `provider=near` +- both ## Verification flow diff --git a/cloud/verification/cloud-api/index.mdx b/cloud/verification/cloud-api/index.mdx index 56de90b..b3dd6d3 100644 --- a/cloud/verification/cloud-api/index.mdx +++ b/cloud/verification/cloud-api/index.mdx @@ -32,4 +32,4 @@ Use this guide when your application sends inference requests to `https://cloud- 3. Verify the gateway's hardware attestation and apply your acceptance policy. 4. Add model, TLS, response-signature, or image-provenance checks when your application requires them. -Start with [Gateway attestation](/cloud/verification/cloud-api/gateway-attestation). That guide shows how to request fresh evidence; the other guides explain the additional checks. +Start with [Gateway attestation](/cloud/verification/cloud-api/gateway-attestation). That guide shows how to request fresh evidence. The other guides explain the additional checks. diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index 5f01e53..b123cf6 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -36,12 +36,12 @@ 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; confirm its binding from the verified quote. | +| `event_log` | Runtime measurement log. Replay it and match the result to the quote's RTMR3. | +| `request_nonce` | Echoed request nonce. Confirm its binding from the verified quote. | | `signing_address` and `signing_algo` | Model signing identity and its algorithm. | -| `signing_public_key` | Model public key for the E2EE flow; request `signing_algo=ed25519` for that flow. | +| `signing_public_key` | Model public key for the E2EE flow. Request `signing_algo=ed25519` for that flow. | | `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. | +| `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 the NEAR model report @@ -54,7 +54,7 @@ The NEAR report uses these fields: 7. Apply your own allowlist and provenance policy to the verified measurements. - `model_attestations[]` is not an inventory of every model instance behind a URL. A response signature must be matched by signer identity and algorithm; a report from a different signer is not an equivalent substitute. + `model_attestations[]` is not an inventory of every model instance behind a URL. A response signature must be matched by signer identity and algorithm. A report from a different signer is not an equivalent substitute. ## Relationship to NEAR AI Cloud gateway TLS diff --git a/cloud/verification/cloud-api/tls.mdx b/cloud/verification/cloud-api/tls.mdx index 708d6e5..30b345b 100644 --- a/cloud/verification/cloud-api/tls.mdx +++ b/cloud/verification/cloud-api/tls.mdx @@ -20,7 +20,7 @@ This is a gateway check. It does not bind the client connection to an upstream m 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. +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`. diff --git a/cloud/verification/direct/deployment-control-evidence.mdx b/cloud/verification/direct/deployment-control-evidence.mdx index c6f2404..8a14263 100644 --- a/cloud/verification/direct/deployment-control-evidence.mdx +++ b/cloud/verification/direct/deployment-control-evidence.mdx @@ -9,10 +9,10 @@ Some direct model reports include optional deployment-control evidence in `compo Use it as supplementary evidence about deployment activity. Do not use it alone to conclude that: -- a particular project, model, or backend is currently running; -- every recorded action completed successfully; -- a later inference request was served by this same CVM; or -- the images involved satisfy your source and build-provenance policy. +- a particular project, model, or backend is currently running +- every recorded action completed successfully +- a later inference request was served by this same CVM +- the images involved satisfy your source and build-provenance policy The same model hostname can be load-balanced across instances. A direct report and a later inference request are therefore not automatically tied to the same CVM. diff --git a/cloud/verification/direct/image-provenance.mdx b/cloud/verification/direct/image-provenance.mdx index 3236385..4acf9f4 100644 --- a/cloud/verification/direct/image-provenance.mdx +++ b/cloud/verification/direct/image-provenance.mdx @@ -3,7 +3,7 @@ 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. +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 diff --git a/cloud/verification/direct/index.mdx b/cloud/verification/direct/index.mdx index b7dee3f..87c0239 100644 --- a/cloud/verification/direct/index.mdx +++ b/cloud/verification/direct/index.mdx @@ -32,4 +32,4 @@ Use this guide when your application sends inference requests directly to `https 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. +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 index 902ecaa..5c89919 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -28,7 +28,7 @@ The root response is the model report. Relevant fields include: | --- | --- | | `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. | +| `request_nonce` | The nonce echoed for diagnostics. Verify it inside the quote. | | `intel_quote` | The model's Intel TDX quote. | | `nvidia_payload` | GPU evidence, when present. | | `tls_cert_fingerprint` | The direct endpoint TLS binding, when requested. | diff --git a/cloud/verification/direct/tls.mdx b/cloud/verification/direct/tls.mdx index eaacd68..0af2ab6 100644 --- a/cloud/verification/direct/tls.mdx +++ b/cloud/verification/direct/tls.mdx @@ -26,7 +26,7 @@ This check applies to the direct model connection. It does not verify a connecti 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. +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 diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index 2ea0b6f..eaf378d 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -3,7 +3,7 @@ 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. +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 @@ -42,4 +42,4 @@ When a report contains NVIDIA GPU evidence, verify it with the applicable NVIDIA ## Reference code -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for NEAR quote, GPU, and configuration processing. Use the report-request examples in these guides; the repository is not a complete Cloud API client. Follow the endpoint-specific guides for TLS binding, response signatures, and image provenance. +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for NEAR quote, GPU, and configuration processing. Use the report-request examples in these guides. The repository is not a complete Cloud API client. Follow the endpoint-specific guides for TLS binding, response signatures, and image provenance. diff --git a/cloud/verification/reference/verification-policy.mdx b/cloud/verification/reference/verification-policy.mdx index 59733b8..13ff4fd 100644 --- a/cloud/verification/reference/verification-policy.mdx +++ b/cloud/verification/reference/verification-policy.mdx @@ -9,13 +9,13 @@ Attestation verifies particular cryptographic statements. Your application must At minimum, specify: -- acceptable Intel TDX verification status and advisory policy; -- 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; and -- whether TLS endpoint binding and response signatures are required. +- acceptable Intel TDX verification status and advisory policy +- 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 From cbefc6d3b6e2b166323f03690e80a80105fd85fe Mon Sep 17 00:00:00 2001 From: Coffee Date: Wed, 2 Sep 2026 13:53:15 +0800 Subject: [PATCH 15/36] docs: align verification guides with latest flows --- cloud/guides/e2ee-chat-completions.mdx | 127 +++++++++++++++--- .../cloud-api/gateway-attestation.mdx | 13 +- .../cloud-api/model-attestations.mdx | 9 +- .../cloud-api/response-signatures.mdx | 16 ++- cloud/verification/cloud-api/tls.mdx | 6 + .../direct/deployment-control-evidence.mdx | 12 ++ .../verification/direct/model-attestation.mdx | 13 +- .../direct/response-signatures.mdx | 2 +- .../reference/quote-nonce-signer.mdx | 13 +- cloud/verification/reference/sdk.mdx | 26 ++++ .../reference/verification-policy.mdx | 2 + docs.json | 1 + 12 files changed, 200 insertions(+), 40 deletions(-) create mode 100644 cloud/verification/reference/sdk.mdx diff --git a/cloud/guides/e2ee-chat-completions.mdx b/cloud/guides/e2ee-chat-completions.mdx index 247f6ff..1afe8bb 100644 --- a/cloud/guides/e2ee-chat-completions.mdx +++ b/cloud/guides/e2ee-chat-completions.mdx @@ -15,7 +15,7 @@ This guide uses the NEAR model E2EE protocol. The gateway sends client-E2EE requ 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 @@ -84,7 +84,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: fetch the model's key, generate a client key pair, encrypt, send, and decrypt. Verify the model report before using its key; the endpoint-specific attestation guides describe that check. **Requirements:** `pip install requests PyNaCl cryptography` @@ -126,14 +126,32 @@ 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. Fetch the model's Ed25519 public key ───────────────────────── -attestation = requests.get( +nonce = secrets.token_hex(32) +attestation_response = requests.get( f"{ENDPOINT}/v1/attestation/report", - headers={"Authorization": f"Bearer {API_KEY}"}, - params={"model": MODEL, "provider": "near", "signing_algo": "ed25519"}, -).json() -model_ed25519_pub = bytes.fromhex(attestation["model_attestations"][0]["signing_public_key"]) + headers={ + "Authorization": f"Bearer {API_KEY}", + "x-no-aliasing": "true", + }, + params={ + "model": MODEL, + "provider": "near", + "signing_algo": "ed25519", + "include_tls_fingerprint": "false", + "nonce": nonce, + }, +) +attestation_response.raise_for_status() +attestation = attestation_response.json() +model_reports = attestation.get("model_attestations", []) +if len(model_reports) != 1: + raise ValueError("Expected exactly one NEAR model attestation") + +# Verify this report and `nonce` before using its signing_public_key: +# https://docs.near.ai/cloud/verification/cloud-api/model-attestations +model_ed25519_pub = bytes.fromhex(model_reports[0]["signing_public_key"]) model_x25519_pub = crypto_sign_ed25519_pk_to_curve25519(model_ed25519_pub) # ── 2. Generate client Ed25519 key pair ────────────────────────────── @@ -155,7 +173,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, @@ -171,6 +191,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()) ``` @@ -181,7 +204,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. Verify the quote and fresh nonce before using the key; see [NEAR model attestations](/cloud/verification/cloud-api/model-attestations) for gateway requests or [direct model attestation](/cloud/verification/direct/model-attestation) for direct requests. 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. @@ -189,7 +212,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&provider=near&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 ' ``` @@ -198,7 +229,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}" ``` @@ -206,27 +241,39 @@ 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", "provider": "near", - "signing_algo": "ed25519" + "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() # NEAR AI Cloud gateway: -model_public_key_hex = attestation["model_attestations"][0]["signing_public_key"] +model_reports = attestation.get("model_attestations", []) +if len(model_reports) != 1: + raise ValueError("Expected exactly one NEAR model attestation") +# Verify `model_reports[0]` and `nonce` before using this key. +model_public_key_hex = model_reports[0]["signing_public_key"] # Direct completions use the root model report instead: # model_public_key_hex = attestation["signing_public_key"] print(f"Model Ed25519 public key: {model_public_key_hex}") @@ -236,29 +283,40 @@ print(f"Model Ed25519 public key: {model_public_key_hex}") ```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', provider: 'near', - signing_algo: 'ed25519' + 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; +const modelReports = attestation.model_attestations ?? []; +if (modelReports.length !== 1) throw new Error('Expected exactly one NEAR model attestation'); +// Verify `modelReports[0]` and `nonce` before using this key. +const modelPublicKeyHex = modelReports[0].signing_public_key; console.log('Model Ed25519 public key:', modelPublicKeyHex); ``` -For the NEAR AI Cloud gateway, request `provider=near` before reading `model_attestations[0].signing_public_key`. For a direct completions endpoint, the key is in the root report's `signing_public_key`. +For the NEAR AI Cloud gateway, request `provider=near` with a canonical model ID and `x-no-aliasing: true`. Require exactly one returned model report, verify it with the nonce you generated, then read `signing_public_key`. For a direct completions endpoint, the key is in the root report's `signing_public_key`. --- @@ -428,6 +486,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": [{ @@ -472,7 +531,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", @@ -498,7 +559,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', @@ -524,13 +587,14 @@ const data = await response.json(); | `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-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. @@ -579,6 +643,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)) ``` @@ -620,6 +687,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)); +} ``` @@ -659,7 +729,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={ @@ -693,12 +765,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. Choose the matching endpoint flow in [Verification](/cloud/verification). +Before using a model public key for encryption, verify the attestation report and its fresh client nonce against hardware attestation. Choose the matching endpoint flow in [Verification](/cloud/verification). --- @@ -707,8 +779,18 @@ You can verify the model's public key is authentic by checking the attestation r ### Get the Model's ECDSA Public Key + Verify the returned report and its fresh nonce before using the 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 ' ``` @@ -777,6 +859,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"}], diff --git a/cloud/verification/cloud-api/gateway-attestation.mdx b/cloud/verification/cloud-api/gateway-attestation.mdx index 1af9871..a8c1738 100644 --- a/cloud/verification/cloud-api/gateway-attestation.mdx +++ b/cloud/verification/cloud-api/gateway-attestation.mdx @@ -26,20 +26,25 @@ The response contains the gateway report at `gateway_attestation`. Add `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 one NEAR model report in `model_attestations[]`. +The response includes gateway evidence and exactly one NEAR model report in `model_attestations[]`. Treat an empty or multi-item array as unavailable evidence for this flow; do not choose an entry yourself. When matching a response signature, request fresh evidence for that signer by adding both `signing_address=` and the signature's `signing_algo`. Do not use a report for a different signer as a substitute. @@ -38,6 +40,7 @@ The NEAR report uses these fields: | `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. Confirm its binding from the verified quote. | +| `report_data` | HTTP copy of quote report data. Compare it with the verified quote. | | `signing_address` and `signing_algo` | Model signing identity and its algorithm. | | `signing_public_key` | Model public key for the E2EE flow. Request `signing_algo=ed25519` for that flow. | | `nvidia_payload` | NVIDIA GPU evidence, when present. | @@ -46,7 +49,7 @@ The NEAR report uses these fields: ## Verify the NEAR model report 1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. -2. Check the client-generated nonce and `signing_address` against the verified quote. Interpret the address using `signing_algo`, and compare both values when matching a response signature. +2. Check the client-generated nonce and `signing_address` against the verified quote. Require `request_nonce` and `report_data` to match the client nonce and verified quote. Interpret the address using `signing_algo`, and compare both values when matching a response signature. 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, verify `nvidia_payload` and require it to be bound to the same nonce. diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx index 82a0627..ab80948 100644 --- a/cloud/verification/cloud-api/response-signatures.mdx +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -13,12 +13,14 @@ Response signatures returned through the NEAR AI Cloud gateway can be produced b 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. +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 @@ -26,12 +28,12 @@ Set `` to the `id` in the completion response whose exact bytes you ret ```bash curl --fail-with-body -G 'https://cloud-api.near.ai/v1/signature/' \ - --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode 'signing_algo=' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' ``` -A `200` response containing `error_code` and `message` is an unavailable result, not a signature. +A `200` 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. 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. @@ -46,10 +48,14 @@ Only `provider_tee` binds a response to a model-serving TEE. A `gateway` signatu 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. Do not assume it is an alias originally supplied by the client. +For `provider_tee`, `` is the canonical model identifier in the signed payload. ## Match the attestation -For the selected signer, request a fresh report with a client-generated nonce and the signature's `signing_address` and `signing_algo`. For `provider_tee`, include `model=` and `provider=near` to obtain the NEAR model report. Then verify the quote and apply the required measurement and provenance policy. A signature made by a different signer is not evidence for the response, even if the model name or endpoint is the same. +For `provider_tee`, request fresh model evidence with a client-generated nonce, `model=`, `provider=near`, `signing_address=`, and the signature's `signing_algo`. Then verify the quote and apply the required measurement and provenance policy. + +For `gateway`, request fresh gateway evidence with a client-generated nonce and the signature's `signing_algo`, then compare the signer returned by the report locally. Gateway evidence does not need a model or provider selector. + +A signature made by a different signer is not evidence for the response, even if the model name or endpoint is the same. `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/cloud-api/tls.mdx b/cloud/verification/cloud-api/tls.mdx index 30b345b..d81bf4e 100644 --- a/cloud/verification/cloud-api/tls.mdx +++ b/cloud/verification/cloud-api/tls.mdx @@ -9,6 +9,10 @@ Use this flow to establish that the HTTPS connection to `cloud-api.near.ai` term 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. @@ -26,6 +30,8 @@ This is a gateway check. It does not bind the client connection to an upstream m 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 | diff --git a/cloud/verification/direct/deployment-control-evidence.mdx b/cloud/verification/direct/deployment-control-evidence.mdx index 8a14263..49051e7 100644 --- a/cloud/verification/direct/deployment-control-evidence.mdx +++ b/cloud/verification/direct/deployment-control-evidence.mdx @@ -16,6 +16,18 @@ Use it as supplementary evidence about deployment activity. Do not use it alone The same model hostname can be load-balanced across instances. A direct report and a later inference request are therefore not automatically tied to the same CVM. +## Verify recorded actions + +When this optional evidence is required, verify it as a separate attestation flow: + +1. Generate a fresh client nonce and require it to equal `compose_manager_attestation.nonce`. +2. Serialize `actions` as compact UTF-8 JSON with every object key sorted, then require its SHA-256 hash to equal `actions_hash`. Preserve the array order. +3. Verify the nested `quote` with an Intel DCAP verifier and replay its `event_log` against the verified quote. +4. Require the reported `report_data` to equal the verified quote's report data, and require that value to be `actions_hash || client_nonce`. +5. For an action that names a source `commit`, `file`, and `file_sha256`, obtain that exact file revision from the expected source and require its SHA-256 hash to match. + +These checks establish that the verified Compose Manager quote bound the returned action record to your nonce. They do not add a project or model identity, a successful-operation outcome, or a binding to a later inference request. + ## Use it with independent checks When your application requires evidence for an inference response, verify the direct [model attestation](/cloud/verification/direct/model-attestation), [TLS connection binding](/cloud/verification/direct/tls) when required, and [response signature](/cloud/verification/direct/response-signatures) when available. diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx index 5c89919..38076db 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -29,8 +29,11 @@ The root response is the model report. Relevant fields include: | `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. | | `compose_manager_attestation` | Optional, separate deployment-control evidence. See [deployment control evidence](/cloud/verification/direct/deployment-control-evidence). | @@ -38,10 +41,12 @@ The root response is the model report. Relevant fields include: ## 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. Use `signing_algo` to interpret that identity and verify response signatures. -3. When GPU evidence is present, verify it with the applicable NVIDIA verifier and ensure it is bound to the same nonce. -4. Apply your accepted measurement policy to the verified configuration. -5. If source and build provenance are required, continue with [direct image provenance](/cloud/verification/direct/image-provenance). +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, verify it with the applicable NVIDIA verifier and ensure it is bound to the same nonce. +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. diff --git a/cloud/verification/direct/response-signatures.mdx b/cloud/verification/direct/response-signatures.mdx index caa657c..1587cec 100644 --- a/cloud/verification/direct/response-signatures.mdx +++ b/cloud/verification/direct/response-signatures.mdx @@ -18,7 +18,7 @@ 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. +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 diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index eaf378d..5c6d865 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -36,10 +36,21 @@ For a NEAR-operated report requested with `include_tls_fingerprint=true`, the ve 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 to NEAR-operated dstack reports only. Compose Manager evidence and other providers have their own report-data contracts. + ## Verify GPU evidence when present When a report contains NVIDIA GPU evidence, verify it with the applicable NVIDIA verification service and require that it is bound to the same client nonce. Define in advance whether GPU evidence is mandatory for your workload policy. ## Reference code -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for NEAR quote, GPU, and configuration processing. Use the report-request examples in these guides. The repository is not a complete Cloud API client. Follow the endpoint-specific guides for TLS binding, response signatures, and image provenance. +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains runnable TypeScript and Python reference flows for gateway and model evidence, response signatures, direct TLS, and Compose Manager evidence. It is reference code rather than a general-purpose client or SDK; use the endpoint guides here to choose the evidence and policy your application requires. + +For a language-specific library, see the [Verification SDK](/cloud/verification/reference/sdk). diff --git a/cloud/verification/reference/sdk.mdx b/cloud/verification/reference/sdk.mdx new file mode 100644 index 0000000..1226ebf --- /dev/null +++ b/cloud/verification/reference/sdk.mdx @@ -0,0 +1,26 @@ +--- +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 + +Installation instructions will be added when the SDK packages are published. + +## Using the SDK + +The SDK follows the same verification model as these guides: generate a fresh nonce, obtain the gateway report, verify its quote and bindings, then apply your application's policy. + +For the protocol requirements and trust decisions behind each check, start with the gateway guide: + + + Verify gateway and NEAR model evidence returned through `cloud-api.near.ai`. + + + + Direct model endpoints, Compose Manager evidence, image provenance, and E2EE are not covered by the SDK yet. SDK examples and language-specific API documentation will be added as they become available. + diff --git a/cloud/verification/reference/verification-policy.mdx b/cloud/verification/reference/verification-policy.mdx index 13ff4fd..84c79ca 100644 --- a/cloud/verification/reference/verification-policy.mdx +++ b/cloud/verification/reference/verification-policy.mdx @@ -10,6 +10,7 @@ Attestation verifies particular cryptographic statements. Your application must 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 @@ -24,6 +25,7 @@ When a required check is missing, unavailable, malformed, mismatched, or fails v | 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. | diff --git a/docs.json b/docs.json index 75c3f2a..c2f4e36 100644 --- a/docs.json +++ b/docs.json @@ -92,6 +92,7 @@ { "group": "Reference", "pages": [ + "cloud/verification/reference/sdk", "cloud/verification/reference/quote-nonce-signer", "cloud/verification/reference/verification-policy" ] From 9eb1b3a41dff8989b0c45f0f866bccf10d7e8fc8 Mon Sep 17 00:00:00 2001 From: Coffee Date: Fri, 4 Sep 2026 14:10:03 +0800 Subject: [PATCH 16/36] docs: sync OpenAPI from staging --- api-reference/openapi.json | 13441 ++++++++++++++++++++++------------- docs.json | 39 +- 2 files changed, 8416 insertions(+), 5064 deletions(-) diff --git a/api-reference/openapi.json b/api-reference/openapi.json index 794aac4..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. 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" + "$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 - }, + { + "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,1399 +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 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" + "responses": { + "204": { + "description": "Refresh token revoked successfully" }, - "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" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "paid_credit_limit_usd": { - "type": "number", - "format": "double", - "description": "Sum of active payment-type spend limits (caps) for this source, USD" + "404": { + "description": "Refresh token not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "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" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "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" - }, - "messages": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" + "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" + } + } } }, - "model": { - "type": "string" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "n": { - "type": [ - "integer", - "null" - ], - "format": "int64" + "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": [] + } + ] + }, + "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", @@ -10541,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", @@ -10554,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" } } }, @@ -10880,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", @@ -10964,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": [ @@ -11061,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": [ @@ -11334,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", @@ -11540,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" ], @@ -11549,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" @@ -11561,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" @@ -11574,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" @@ -11824,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" } } }, @@ -12091,8 +15426,7 @@ "enum": [ "always", "never" - ], - "description": "Simple MCP approval mode" + ] }, "McpApprovalRequirement": { "oneOf": [ @@ -12110,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", @@ -12132,6 +15472,13 @@ } } }, + "McpListToolsType": { + "type": "string", + "description": "Type marker for MCP list tools input", + "enum": [ + "mcp_list_tools" + ] + }, "McpToolNameFilter": { "type": "object", "required": [ @@ -12145,8 +15492,7 @@ }, "uniqueItems": true } - }, - "description": "Filter for tool names that don't require approval" + } }, "MemberRole": { "type": "string", @@ -12527,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", @@ -12720,6 +16082,7 @@ "type": "string" } }, + "textPricing": {}, "verifiable": { "type": "boolean" } @@ -12918,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": [ { @@ -13337,7 +16703,8 @@ }, "outputCostPerToken": { "$ref": "#/components/schemas/DecimalPrice" - } + }, + "textPricing": {} } }, "ModelsResponse": { @@ -13689,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", @@ -14284,6 +17668,12 @@ "$ref": "#/components/schemas/DecimalPriceRequest" } ] + }, + "textPricing": { + "type": [ + "object", + "null" + ] } } }, @@ -14397,7 +17787,8 @@ "$ref": "#/components/schemas/DecimalPrice" } ] - } + }, + "textPricing": {} } }, "PricingFields": { @@ -14427,7 +17818,8 @@ }, "outputCostPerToken": { "$ref": "#/components/schemas/DecimalPrice" - } + }, + "textPricing": {} } }, "PrivacyClassifyRequestDoc": { @@ -14747,6 +18139,7 @@ "output_tokens", "total_tokens", "cache_read_tokens", + "cache_write_tokens", "input_cost", "output_cost", "total_cost", @@ -14760,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)" @@ -15627,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": [ { @@ -15777,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": [ @@ -15833,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": [ @@ -15996,7 +19541,7 @@ "tools": { "type": "array", "items": { - "$ref": "#/components/schemas/ClientManagedResponseTool" + "$ref": "#/components/schemas/ResponseTool" } }, "top_logprobs": { @@ -16523,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": [ @@ -16741,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" + } + ] } } }, @@ -16807,6 +20362,7 @@ }, { "type": "object", + "description": "Remote MCP server tool", "required": [ "server_label", "server_url", @@ -16822,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": [ @@ -16841,7 +20392,8 @@ "type": "string" }, "server_url": { - "type": "string" + "type": "string", + "description": "HTTPS endpoint for the remote MCP server" }, "type": { "type": "string", @@ -17784,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)", @@ -17835,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)", @@ -18056,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", @@ -18104,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)", @@ -18276,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": [ @@ -18393,6 +22001,7 @@ "input_tokens", "output_tokens", "cache_read_tokens", + "cache_write_tokens", "total_tokens", "total_cost", "total_cost_display", @@ -18403,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" }, @@ -18457,6 +22078,12 @@ ], "description": "Response ID when called from Responses API" }, + "serviceTier": { + "type": [ + "string", + "null" + ] + }, "stop_reason": { "type": [ "string", @@ -18510,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": [ @@ -18860,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" } } } @@ -19391,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", @@ -19407,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/docs.json b/docs.json index c2f4e36..717e0d6 100644 --- a/docs.json +++ b/docs.json @@ -166,7 +166,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" ] }, { @@ -343,6 +347,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", @@ -359,6 +368,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", @@ -377,6 +388,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" + ] } ] } From 8060809bed21a03c71911746a46f719fc3d99440 Mon Sep 17 00:00:00 2001 From: Coffee Date: Fri, 4 Sep 2026 14:15:49 +0800 Subject: [PATCH 17/36] docs: add verification SDK installation --- cloud/verification/reference/sdk.mdx | 34 ++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/cloud/verification/reference/sdk.mdx b/cloud/verification/reference/sdk.mdx index 1226ebf..4159c4e 100644 --- a/cloud/verification/reference/sdk.mdx +++ b/cloud/verification/reference/sdk.mdx @@ -9,7 +9,37 @@ The SDK supports TypeScript, Python, and Rust. ## Installation -Installation instructions will be added when the SDK packages are published. + + + +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`. + + + ## Using the SDK @@ -22,5 +52,5 @@ For the protocol requirements and trust decisions behind each check, start with - Direct model endpoints, Compose Manager evidence, image provenance, and E2EE are not covered by the SDK yet. SDK examples and language-specific API documentation will be added as they become available. + Direct model endpoints, Compose Manager evidence, image provenance, and E2EE are not covered by the SDK yet. SDK examples and language-specific API documentation will be added next. From 1be96bb06425900154d7a500e096d3d1f26d6ae8 Mon Sep 17 00:00:00 2001 From: Coffee Date: Fri, 4 Sep 2026 19:59:13 +0800 Subject: [PATCH 18/36] docs: align SDK verification flow --- .../cloud-api/model-attestations.mdx | 10 ++++----- .../cloud-api/response-signatures.mdx | 21 +++++++++++-------- cloud/verification/reference/sdk.mdx | 16 +++++++++----- .../reference/verification-policy.mdx | 4 ++-- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index 38b6708..1e8efd2 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -23,12 +23,12 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -H 'Authorization: Bearer ' ``` -The response includes gateway evidence and exactly one NEAR model report in `model_attestations[]`. Treat an empty or multi-item array as unavailable evidence for this flow; do not choose an entry yourself. +The response includes gateway evidence and exactly one NEAR model report in `model_attestations[]`. Treat an empty or multi-item array as unavailable evidence for this flow; do not choose an entry yourself. Verify and retain the report before sending a completion. -When matching a response signature, request fresh evidence for that signer by adding both `signing_address=` and the signature's `signing_algo`. Do not use a report for a different signer as a substitute. +For a later `provider_tee` response signature, require its `signing_address` and `signing_algo` to match this verified report. Do not fetch another report after the completion merely to select a signer. - `provider=near` selects the model report returned by this request. It does not select the provider for a later completion. Treat a report as evidence for a completion only when its signer and algorithm match a `provider_tee` response signature. + `provider=near` selects the model report returned by this request. It does not select the provider for a later completion. A `provider_tee` response is verified only when its signer and algorithm match the retained verified report. ## NEAR model report fields @@ -53,7 +53,7 @@ The NEAR report uses these fields: 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, verify `nvidia_payload` and require it to be bound to the same nonce. -6. When verifying a model-serving response signature, request a report whose `signing_address` and `signing_algo` match that signature. +6. When verifying a model-serving response signature, require its `signing_address` and `signing_algo` to match this verified report. 7. Apply your own allowlist and provenance policy to the verified measurements. @@ -66,7 +66,7 @@ The browser or client connection is to `cloud-api.near.ai`. Its TLS endpoint bin ## 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 the applicable model attestation. For a `gateway` signature, match the signer to `gateway_attestation` instead. See [NEAR AI Cloud gateway response signatures](/cloud/verification/cloud-api/response-signatures). +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 the previously verified model attestation. 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 diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx index ab80948..5ce4b3c 100644 --- a/cloud/verification/cloud-api/response-signatures.mdx +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -1,9 +1,9 @@ --- title: "Response Signatures" -description: "Verify available response signatures from the NEAR AI Cloud gateway against the exact bytes and the matching attestation." +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. When present, `signature_kind` determines which attestation must match the signer. +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 selected NEAR model deployment your policy requires. 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. @@ -39,10 +39,10 @@ For `ecdsa`, recover the Ethereum EIP-191 signer from `text` and `signature`, th Then compare `text` with the expected payload: -| `signature_kind` | Expected `text` | Required attestation match | +| `signature_kind` | Expected `text` | Required preflight match | | --- | --- | --- | -| `provider_tee` | `::` | A NEAR model report requested with `provider=near`, with the same `signing_address` and `signing_algo`. | -| `gateway` | `:` | `gateway_attestation` with the same `signing_address` and `signing_algo`. | +| `provider_tee` | `::` | A 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. @@ -50,12 +50,15 @@ If `signature_kind` is absent or unfamiliar, do not infer it from the shape of ` For `provider_tee`, `` is the canonical model identifier in the signed payload. -## Match the attestation +## Match preflight evidence -For `provider_tee`, request fresh model evidence with a client-generated nonce, `model=`, `provider=near`, `signing_address=`, and the signature's `signing_algo`. Then verify the quote and apply the required measurement and provenance policy. +Before sending the completion, request and verify gateway evidence with a client-generated nonce. To support either signature kind, also request and verify the selected NEAR model report with `model=` and `provider=near`. Retain both verified results. -For `gateway`, request fresh gateway evidence with a client-generated nonce and the signature's `signing_algo`, then compare the signer returned by the report locally. Gateway evidence does not need a model or provider selector. +After fetching the signature, use `signature_kind` to select the matching preflight result: -A signature made by a different signer is not evidence for the response, even if the model name or endpoint is the same. +- For `provider_tee`, require the signature signer and algorithm to match the verified NEAR model report. +- For `gateway`, require them to match the verified gateway report. + +Do not request a new report after the completion merely to make a signer match. If the signature's signer or algorithm differs from the preflight evidence, the response is unverified. `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/reference/sdk.mdx b/cloud/verification/reference/sdk.mdx index 4159c4e..7f1bf52 100644 --- a/cloud/verification/reference/sdk.mdx +++ b/cloud/verification/reference/sdk.mdx @@ -41,16 +41,22 @@ Import it as `verifiable_ai_sdk`. -## Using the SDK +## Verification flow -The SDK follows the same verification model as these guides: generate a fresh nonce, obtain the gateway report, verify its quote and bindings, then apply your application's policy. +For each completion, first use the SDK to verify the NEAR AI Cloud gateway and selected model deployments. 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 the protocol requirements and trust decisions behind each check, start with the gateway guide: +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. - Verify gateway and NEAR model evidence returned through `cloud-api.near.ai`. + 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. + - Direct model endpoints, Compose Manager evidence, image provenance, and E2EE are not covered by the SDK yet. SDK examples and language-specific API documentation will be added next. + In TypeScript, import from `verifiable-ai-sdk/node` when a Node.js application requires Gateway TLS binding. 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, Compose Manager evidence, 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 index 84c79ca..b37d75e 100644 --- a/cloud/verification/reference/verification-policy.mdx +++ b/cloud/verification/reference/verification-policy.mdx @@ -36,8 +36,8 @@ When a required check is missing, unavailable, malformed, mismatched, or fails v The same hostname can be served by more than one attested instance. A report is not automatically evidence for a later request. - For TLS, obtain the peer certificate and report on the same connection. -- For a response signature, match the signer identity and algorithm to the applicable attestation. -- Fetch new evidence after a signer, certificate, configuration, deployment, or policy-age change. +- For a response signature, use only preflight evidence whose signer identity and algorithm match. +- Refresh preflight evidence before a later request after a signer, certificate, configuration, deployment, or policy-age change. ## What attestation does not establish by itself From c457fc72e333d3f51549b9e3b9d69dff928a27da Mon Sep 17 00:00:00 2001 From: Coffee Date: Fri, 4 Sep 2026 20:16:26 +0800 Subject: [PATCH 19/36] docs: align manual verification flow --- .../cloud-api/gateway-attestation.mdx | 6 ++++-- cloud/verification/cloud-api/index.mdx | 19 ++++++++++--------- .../cloud-api/model-attestations.mdx | 2 +- .../cloud-api/response-signatures.mdx | 6 +++++- .../reference/verification-policy.mdx | 4 ++-- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/cloud/verification/cloud-api/gateway-attestation.mdx b/cloud/verification/cloud-api/gateway-attestation.mdx index a8c1738..538ebce 100644 --- a/cloud/verification/cloud-api/gateway-attestation.mdx +++ b/cloud/verification/cloud-api/gateway-attestation.mdx @@ -19,7 +19,7 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -H 'Authorization: Bearer ' ``` -The response contains the gateway report at `gateway_attestation`. Add `model=` and `provider=near` only when you also need the [NEAR model report](/cloud/verification/cloud-api/model-attestations), and add `include_tls_fingerprint=true` only when you are following the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). +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 the [NEAR model report](/cloud/verification/cloud-api/model-attestations), and 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 @@ -49,4 +49,6 @@ The verified quote is the cryptographic source of truth. The HTTP fields are con ## 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. Do not reuse evidence from a different request or instance as a substitute. +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/index.mdx b/cloud/verification/cloud-api/index.mdx index b3dd6d3..e4b9408 100644 --- a/cloud/verification/cloud-api/index.mdx +++ b/cloud/verification/cloud-api/index.mdx @@ -9,27 +9,28 @@ Use this guide when your application sends inference requests to `https://cloud- - Verify the trusted environment that receives your request at the NEAR AI Cloud gateway. + Verify the NEAR AI Cloud gateway deployment before an inference request. - Verify the environment serving the selected model. + Verify a selected NEAR model deployment before an inference request. Verify that your live HTTPS connection terminates at the attested gateway. - Verify that a signed response came from the attested gateway or model environment. + Verify exact response bytes against the matching preflight signer. Check that the software images in the attested environment meet your source and build policy. -## Start with gateway attestation +## Verify a completion -1. Generate a fresh nonce for this verification attempt. -2. Request a fresh gateway attestation report. -3. Verify the gateway's hardware attestation and apply your acceptance policy. -4. Add model, TLS, response-signature, or image-provenance checks when your application requires them. +1. Before the completion, request and verify fresh gateway evidence. If your policy requires NEAR model evidence, request and verify the selected model report with its canonical ID and `provider=near` as well. 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 matching verified preflight result; it does not decide which deployment checks your policy required. -Start with [Gateway attestation](/cloud/verification/cloud-api/gateway-attestation). That guide shows how to request fresh evidence. The other guides explain the additional checks. +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 index 1e8efd2..f28e4dd 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -25,7 +25,7 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ The response includes gateway evidence and exactly one NEAR model report in `model_attestations[]`. Treat an empty or multi-item array as unavailable evidence for this flow; do not choose an entry yourself. Verify and retain the report before sending a completion. -For a later `provider_tee` response signature, require its `signing_address` and `signing_algo` to match this verified report. Do not fetch another report after the completion merely to select a signer. +For a later `provider_tee` response signature, require its `signing_address` and `signing_algo` to match this verified report. A report fetched after the completion cannot replace this preflight evidence. `provider=near` selects the model report returned by this request. It does not select the provider for a later completion. A `provider_tee` response is verified only when its signer and algorithm match the retained verified report. diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx index 5ce4b3c..663a3d2 100644 --- a/cloud/verification/cloud-api/response-signatures.mdx +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -59,6 +59,10 @@ After fetching the signature, use `signature_kind` to select the matching prefli - For `provider_tee`, require the signature signer and algorithm to match the verified NEAR model report. - For `gateway`, require them to match the verified gateway report. -Do not request a new report after the completion merely to make a signer match. If the signature's signer or algorithm differs from the preflight evidence, the response is unverified. +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 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/reference/verification-policy.mdx b/cloud/verification/reference/verification-policy.mdx index b37d75e..142ca69 100644 --- a/cloud/verification/reference/verification-policy.mdx +++ b/cloud/verification/reference/verification-policy.mdx @@ -33,10 +33,10 @@ When a required check is missing, unavailable, malformed, mismatched, or fails v ## Load balancing and rotations -The same hostname can be served by more than one attested instance. A report is not automatically evidence for a later request. +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 response signature, use only preflight evidence whose signer identity and algorithm match. +- For a response signature, retain preflight evidence and use it only when its signer identity and algorithm match. 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 From 204ee48266186cfc04b811668d2ef77d79b34f3a Mon Sep 17 00:00:00 2001 From: Coffee Date: Wed, 9 Sep 2026 11:59:38 +0800 Subject: [PATCH 20/36] docs: complete verification guidance --- cloud/verification/chat.mdx | 15 -- .../cloud-api/model-attestations.mdx | 10 +- .../cloud-api/response-signatures.mdx | 6 + .../verification/direct/model-attestation.mdx | 2 +- .../direct/response-signatures.mdx | 8 +- cloud/verification/gateway.mdx | 10 - cloud/verification/index.mdx | 2 +- cloud/verification/model.mdx | 15 -- cloud/verification/provenance.mdx | 15 -- .../reference/quote-nonce-signer.mdx | 17 +- .../response-signature-verification.mdx | 213 ++++++++++++++++++ cloud/verification/reference/sdk.mdx | 4 +- cloud/verification/tls.mdx | 15 -- docs.json | 21 ++ 14 files changed, 272 insertions(+), 81 deletions(-) delete mode 100644 cloud/verification/chat.mdx delete mode 100644 cloud/verification/gateway.mdx delete mode 100644 cloud/verification/model.mdx delete mode 100644 cloud/verification/provenance.mdx create mode 100644 cloud/verification/reference/response-signature-verification.mdx delete mode 100644 cloud/verification/tls.mdx diff --git a/cloud/verification/chat.mdx b/cloud/verification/chat.mdx deleted file mode 100644 index fdfbe75..0000000 --- a/cloud/verification/chat.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "Choose a Response-Signature Flow" -description: "Select the response-signature guide that matches the endpoint your application calls." ---- - -Choose the guide from the URL your application used for the completion. - - - - Use this when your application sent the completion through `cloud-api.near.ai`. - - - Use this when your application sent the completion directly to a model endpoint. - - diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index f28e4dd..cea0d43 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -39,20 +39,20 @@ The NEAR report uses these fields: | --- | --- | | `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. Confirm its binding from the verified quote. | -| `report_data` | HTTP copy of quote report data. Compare it with the verified quote. | +| `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 for the E2EE flow. Request `signing_algo=ed25519` for that flow. | +| `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 the NEAR model report 1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. -2. Check the client-generated nonce and `signing_address` against the verified quote. Require `request_nonce` and `report_data` to match the client nonce and verified quote. Interpret the address using `signing_algo`, and compare both values when matching a response signature. +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`, and compare both values when matching a response signature. 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, verify `nvidia_payload` and require it to be bound to the same nonce. +5. When GPU evidence is present, follow [GPU evidence verification](/cloud/verification/reference/quote-nonce-signer#verify-gpu-evidence-when-present). 6. When verifying a model-serving response signature, require its `signing_address` and `signing_algo` to match this verified report. 7. Apply your own allowlist and provenance policy to the verified measurements. diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx index 663a3d2..43493c9 100644 --- a/cloud/verification/cloud-api/response-signatures.mdx +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -35,6 +35,8 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/signature/' \ A `200` 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: @@ -46,10 +48,14 @@ Then compare `text` with the expected payload: 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 verified model report's signer. 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 either signature kind, also request and verify the selected NEAR model report with `model=` and `provider=near`. Retain both verified results. diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx index 38076db..54aea0c 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -44,7 +44,7 @@ The root response is the model report. Relevant fields include: 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, verify it with the applicable NVIDIA verifier and ensure it is bound to the same nonce. +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). diff --git a/cloud/verification/direct/response-signatures.mdx b/cloud/verification/direct/response-signatures.mdx index 1587cec..b22dee3 100644 --- a/cloud/verification/direct/response-signatures.mdx +++ b/cloud/verification/direct/response-signatures.mdx @@ -26,11 +26,13 @@ Set `` to the `id` in the completion response whose exact bytes you ret ```bash curl --fail-with-body -G 'https://.completions.near.ai/v1/signature/' \ - --data-urlencode 'signing_algo=ecdsa' \ + --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: @@ -39,7 +41,9 @@ Then require that `text` is exactly: :: ``` -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. +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 diff --git a/cloud/verification/gateway.mdx b/cloud/verification/gateway.mdx deleted file mode 100644 index 5e789d8..0000000 --- a/cloud/verification/gateway.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: "Gateway Attestation" -description: "Verify gateway evidence for requests sent through the NEAR AI Cloud gateway." ---- - -This guide is now part of the NEAR AI Cloud gateway verification flow. - - - Verify the trusted environment that receives requests through `cloud-api.near.ai`. - diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index eea7381..1858c3f 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -22,7 +22,7 @@ Use these guides to verify the evidence returned for your AI requests. Start wit ## What happens next -After you choose a path, its guide walks you through obtaining fresh attestation evidence and verifying the environment that handled the request. Depending on your application's policy, you can also verify the TLS connection, signed responses, and software-image provenance. +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. ## Common verification concepts diff --git a/cloud/verification/model.mdx b/cloud/verification/model.mdx deleted file mode 100644 index 701b66a..0000000 --- a/cloud/verification/model.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "Choose a Model Attestation Flow" -description: "Select the verification guide that matches the endpoint your application calls." ---- - -Choose the guide from the URL your application uses for inference. - - - - Use this when your request goes through `cloud-api.near.ai`. - - - Use this when your application sends requests directly to a model's `completions.near.ai` endpoint. - - diff --git a/cloud/verification/provenance.mdx b/cloud/verification/provenance.mdx deleted file mode 100644 index 700a62c..0000000 --- a/cloud/verification/provenance.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "Choose an Image-Provenance Flow" -description: "Select the provenance guide that matches the endpoint your application uses." ---- - -Choose the guide from the URL your application uses for inference. - - - - Use this when your application sends requests through `cloud-api.near.ai`. - - - Use this when your application sends requests directly to a model's `completions.near.ai` endpoint. - - diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index 5c6d865..e34015c 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -47,7 +47,22 @@ This layout applies to NEAR-operated dstack reports only. Compose Manager eviden ## Verify GPU evidence when present -When a report contains NVIDIA GPU evidence, verify it with the applicable NVIDIA verification service and require that it is bound to the same client nonce. Define in advance whether GPU evidence is mandatory for your workload policy. +`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, first save the selected `model_attestations[]` entry as `report.json`; 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. ## Reference code 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 index 7f1bf52..3d09a49 100644 --- a/cloud/verification/reference/sdk.mdx +++ b/cloud/verification/reference/sdk.mdx @@ -47,6 +47,8 @@ For each completion, first use the SDK to verify the NEAR AI Cloud gateway and s 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. Those Cloud API endpoints have different defaults. + Learn the protocol requirements and trust decisions for gateway and NEAR model evidence. @@ -56,7 +58,7 @@ The SDK creates a fresh nonce for every evidence request. It retrieves and verif Runnable TypeScript, Python, and Rust examples follow this flow for non-streaming and streaming completions. - In TypeScript, import from `verifiable-ai-sdk/node` when a Node.js application requires Gateway TLS binding. The generic `verifiable-ai-sdk` entry point verifies Gateway evidence without a TLS peer binding. + 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, Compose Manager evidence, image provenance, and E2EE, use the corresponding endpoint guides. diff --git a/cloud/verification/tls.mdx b/cloud/verification/tls.mdx deleted file mode 100644 index b89278d..0000000 --- a/cloud/verification/tls.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "Choose a TLS Attestation Flow" -description: "Select the TLS verification guide that matches the endpoint your application calls." ---- - -TLS evidence applies to a particular request path and connection. Select the endpoint your application calls. - - - - Use this when your application connects to `cloud-api.near.ai`. - - - Use this when your application connects directly to a model's `completions.near.ai` endpoint. - - diff --git a/docs.json b/docs.json index 717e0d6..d4dd40c 100644 --- a/docs.json +++ b/docs.json @@ -94,6 +94,7 @@ "pages": [ "cloud/verification/reference/sdk", "cloud/verification/reference/quote-nonce-signer", + "cloud/verification/reference/response-signature-verification", "cloud/verification/reference/verification-policy" ] } @@ -511,6 +512,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" } ] } From 93474c442c81558fdcf32891906095f9adc383a8 Mon Sep 17 00:00:00 2001 From: Coffee Date: Wed, 9 Sep 2026 15:13:14 +0800 Subject: [PATCH 21/36] docs: support multiple model attestations --- cloud/guides/e2ee-chat-completions.mdx | 77 ++++++++----------- .../cloud-api/gateway-attestation.mdx | 4 +- .../cloud-api/image-provenance.mdx | 8 +- cloud/verification/cloud-api/index.mdx | 8 +- .../cloud-api/model-attestations.mdx | 21 ++--- .../cloud-api/response-signatures.mdx | 14 ++-- cloud/verification/index.mdx | 2 +- .../reference/quote-nonce-signer.mdx | 4 +- cloud/verification/reference/sdk.mdx | 4 +- .../reference/verification-policy.mdx | 4 +- 10 files changed, 67 insertions(+), 79 deletions(-) diff --git a/cloud/guides/e2ee-chat-completions.mdx b/cloud/guides/e2ee-chat-completions.mdx index 1afe8bb..d1ad8dc 100644 --- a/cloud/guides/e2ee-chat-completions.mdx +++ b/cloud/guides/e2ee-chat-completions.mdx @@ -30,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 @@ -84,7 +85,7 @@ Wire format for all encrypted fields: `[ephemeral_pubkey (32 bytes)][nonce (24 b ## Quick Start -A Python script that demonstrates the E2EE encryption flow: fetch the model's key, generate a client key pair, encrypt, send, and decrypt. Verify the model report before using its key; the endpoint-specific attestation guides describe that check. +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` @@ -92,7 +93,7 @@ A Python script that demonstrates the E2EE encryption flow: fetch the model's ke - **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, @@ -106,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 ───────────────────────────────────────────────────────── @@ -126,32 +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 the model's Ed25519 public key ───────────────────────── - -nonce = secrets.token_hex(32) -attestation_response = requests.get( - f"{ENDPOINT}/v1/attestation/report", - headers={ - "Authorization": f"Bearer {API_KEY}", - "x-no-aliasing": "true", - }, - params={ - "model": MODEL, - "provider": "near", - "signing_algo": "ed25519", - "include_tls_fingerprint": "false", - "nonce": nonce, - }, -) -attestation_response.raise_for_status() -attestation = attestation_response.json() -model_reports = attestation.get("model_attestations", []) -if len(model_reports) != 1: - raise ValueError("Expected exactly one NEAR model attestation") +# ── 1. Use the model's verified Ed25519 public key ────────────────── -# Verify this report and `nonce` before using its signing_public_key: -# https://docs.near.ai/cloud/verification/cloud-api/model-attestations -model_ed25519_pub = bytes.fromhex(model_reports[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 ────────────────────────────── @@ -204,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. Verify the quote and fresh nonce before using the key; see [NEAR model attestations](/cloud/verification/cloud-api/model-attestations) for gateway requests or [direct model attestation](/cloud/verification/direct/model-attestation) for direct requests. +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. @@ -270,13 +253,14 @@ response.raise_for_status() attestation = response.json() # NEAR AI Cloud gateway: model_reports = attestation.get("model_attestations", []) -if len(model_reports) != 1: - raise ValueError("Expected exactly one NEAR model attestation") -# Verify `model_reports[0]` and `nonce` before using this key. -model_public_key_hex = model_reports[0]["signing_public_key"] +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"] -print(f"Model Ed25519 public key: {model_public_key_hex}") ``` @@ -307,16 +291,17 @@ const response = await fetch( if (!response.ok) throw new Error(`Attestation request failed: ${response.status}`); const attestation = await response.json(); const modelReports = attestation.model_attestations ?? []; -if (modelReports.length !== 1) throw new Error('Expected exactly one NEAR model attestation'); -// Verify `modelReports[0]` and `nonce` before using this key. -const modelPublicKeyHex = modelReports[0].signing_public_key; -console.log('Model Ed25519 public key:', modelPublicKeyHex); +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)`); ``` -For the NEAR AI Cloud gateway, request `provider=near` with a canonical model ID and `x-no-aliasing: true`. Require exactly one returned model report, verify it with the nonce you generated, then read `signing_public_key`. For a direct completions endpoint, the key is in the root report's `signing_public_key`. +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`. --- @@ -586,7 +571,7 @@ 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) | @@ -770,7 +755,7 @@ E2EE is supported on the **Chat Completions API** (`/v1/chat/completions`), **Co ### Verification -Before using a model public key for encryption, verify the attestation report and its fresh client nonce against hardware attestation. Choose the matching endpoint flow in [Verification](/cloud/verification). +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). --- @@ -779,7 +764,7 @@ Before using a model public key for encryption, verify the attestation report an ### Get the Model's ECDSA Public Key - Verify the returned report and its fresh nonce before using the public key, as described in [NEAR model attestations](/cloud/verification/cloud-api/model-attestations). + 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 NONCE="$(openssl rand -hex 32)" diff --git a/cloud/verification/cloud-api/gateway-attestation.mdx b/cloud/verification/cloud-api/gateway-attestation.mdx index 538ebce..061cda9 100644 --- a/cloud/verification/cloud-api/gateway-attestation.mdx +++ b/cloud/verification/cloud-api/gateway-attestation.mdx @@ -19,7 +19,7 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -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 the [NEAR model report](/cloud/verification/cloud-api/model-attestations), and add `include_tls_fingerprint=true` only when you are following the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). +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 @@ -45,7 +45,7 @@ The verified quote is the cryptographic source of truth. The HTTP fields are con | `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 when requested with `model` and `provider=near`. Do not use it as a substitute for the gateway quote. | +| `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 diff --git a/cloud/verification/cloud-api/image-provenance.mdx b/cloud/verification/cloud-api/image-provenance.mdx index 19572ce..5e0eacc 100644 --- a/cloud/verification/cloud-api/image-provenance.mdx +++ b/cloud/verification/cloud-api/image-provenance.mdx @@ -5,18 +5,18 @@ description: "Apply an image-provenance policy to configurations measured in NEA 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 the NEAR model report requested with `provider=near`. +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` -- the NEAR model report in `model_attestations[]`, requested with `provider=near` +- each NEAR model report in `model_attestations[]`, requested with `provider=near` - both ## Verification flow -1. Verify the relevant Intel TDX quote first. -2. For a 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 the verified quote. +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. diff --git a/cloud/verification/cloud-api/index.mdx b/cloud/verification/cloud-api/index.mdx index e4b9408..028e1fb 100644 --- a/cloud/verification/cloud-api/index.mdx +++ b/cloud/verification/cloud-api/index.mdx @@ -1,6 +1,6 @@ --- title: "Verify the NEAR AI Cloud Gateway" -description: "Verify the gateway, selected model, connection, response, and software evidence for requests sent through cloud-api.near.ai." +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. @@ -12,7 +12,7 @@ Use this guide when your application sends inference requests to `https://cloud- Verify the NEAR AI Cloud gateway deployment before an inference request.
- Verify a selected NEAR model 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. @@ -27,9 +27,9 @@ Use this guide when your application sends inference requests to `https://cloud- ## Verify a completion -1. Before the completion, request and verify fresh gateway evidence. If your policy requires NEAR model evidence, request and verify the selected model report with its canonical ID and `provider=near` as well. For Gateway TLS binding, follow the same-connection [TLS flow](/cloud/verification/cloud-api/tls). +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 matching verified preflight result; it does not decide which deployment checks your policy required. +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. diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index cea0d43..422cc54 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -23,12 +23,12 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -H 'Authorization: Bearer ' ``` -The response includes gateway evidence and exactly one NEAR model report in `model_attestations[]`. Treat an empty or multi-item array as unavailable evidence for this flow; do not choose an entry yourself. Verify and retain the report before sending a completion. +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, require its `signing_address` and `signing_algo` to match this verified report. A report fetched after the completion cannot replace this preflight evidence. +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` selects the model report returned by this request. It does not select the provider for a later completion. A `provider_tee` response is verified only when its signer and algorithm match the retained verified report. + `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 @@ -46,18 +46,21 @@ The NEAR report uses these fields: | `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 the NEAR model report +## 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`, and compare both values when matching a response signature. +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. When verifying a model-serving response signature, require its `signing_address` and `signing_algo` to match this verified report. -7. Apply your own allowlist and provenance policy to the verified measurements. +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. A response signature must be matched by signer identity and algorithm. A report from a different signer is not an equivalent substitute. + `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 @@ -66,7 +69,7 @@ The browser or client connection is to `cloud-api.near.ai`. Its TLS endpoint bin ## 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 the previously verified model attestation. 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). +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 diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx index 43493c9..b85496e 100644 --- a/cloud/verification/cloud-api/response-signatures.mdx +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -3,7 +3,7 @@ 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 selected NEAR model deployment your policy requires. When present, `signature_kind` determines which verified preflight result must match the signer. +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. @@ -33,7 +33,7 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/signature/' \ -H 'Authorization: Bearer ' ``` -A `200` 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. +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. @@ -43,7 +43,7 @@ Then compare `text` with the expected payload: | `signature_kind` | Expected `text` | Required preflight match | | --- | --- | --- | -| `provider_tee` | `::` | A verified NEAR model report with the same `signing_address` and `signing_algo`. | +| `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. @@ -54,18 +54,18 @@ If `signature_kind` is absent or unfamiliar, do not infer it from the shape of ` 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 verified model report's signer. For `gateway`, omit the model ID and use the verified Gateway report's signer. +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 either signature kind, also request and verify the selected NEAR model report with `model=` and `provider=near`. Retain both verified results. +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 the verified NEAR model report. +- 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 preflight evidence, the response is unverified under this flow. +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`. diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 1858c3f..1a1f179 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -9,7 +9,7 @@ Use these guides to verify the evidence returned for your AI requests. Start wit - Use this when your application sends requests through `cloud-api.near.ai`. Verify the gateway and, when available, model evidence returned with the report. + 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. diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index e34015c..572301e 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -20,7 +20,7 @@ Send this value with the attestation request. Reject a report unless the verifie | Request path | Quote location | | --- | --- | | NEAR AI Cloud gateway | `gateway_attestation.intel_quote` | -| NEAR AI Cloud gateway model evidence (`provider=near`) | The single model report in `model_attestations[]` | +| 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 @@ -47,7 +47,7 @@ This layout applies to NEAR-operated dstack reports only. Compose Manager eviden ## 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, first save the selected `model_attestations[]` entry as `report.json`; for a direct response, save its root model report. +`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)" diff --git a/cloud/verification/reference/sdk.mdx b/cloud/verification/reference/sdk.mdx index 3d09a49..0714b24 100644 --- a/cloud/verification/reference/sdk.mdx +++ b/cloud/verification/reference/sdk.mdx @@ -43,11 +43,11 @@ Import it as `verifiable_ai_sdk`. ## Verification flow -For each completion, first use the SDK to verify the NEAR AI Cloud gateway and selected model deployments. 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 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. Those Cloud API endpoints have different defaults. +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. diff --git a/cloud/verification/reference/verification-policy.mdx b/cloud/verification/reference/verification-policy.mdx index 142ca69..0e61dc8 100644 --- a/cloud/verification/reference/verification-policy.mdx +++ b/cloud/verification/reference/verification-policy.mdx @@ -29,14 +29,14 @@ When a required check is missing, unavailable, malformed, mismatched, or fails v | 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. | -| Required NEAR model report is unavailable or malformed | Treat the model property as unverified. | +| 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 response signature, retain preflight evidence and use it only when its signer identity and algorithm match. A report fetched after the completion cannot replace that preflight check. +- 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 From bb1d86bb8fb1b90f7fd9fc690db913f6da7b4d5c Mon Sep 17 00:00:00 2001 From: Coffee Date: Wed, 9 Sep 2026 16:04:49 +0800 Subject: [PATCH 22/36] docs: remove deployment control evidence --- .../direct/deployment-control-evidence.mdx | 39 ------------------- cloud/verification/direct/index.mdx | 3 -- .../verification/direct/model-attestation.mdx | 1 - .../reference/quote-nonce-signer.mdx | 4 +- cloud/verification/reference/sdk.mdx | 2 +- docs.json | 3 +- 6 files changed, 4 insertions(+), 48 deletions(-) delete mode 100644 cloud/verification/direct/deployment-control-evidence.mdx diff --git a/cloud/verification/direct/deployment-control-evidence.mdx b/cloud/verification/direct/deployment-control-evidence.mdx deleted file mode 100644 index 49051e7..0000000 --- a/cloud/verification/direct/deployment-control-evidence.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: "Deployment Control Evidence" -description: "Interpret optional Compose Manager evidence returned by a direct model endpoint." ---- - -Some direct model reports include optional deployment-control evidence in `compose_manager_attestation`. It can help investigate deployment operations, but it does not verify an individual inference request. - -## What it can and cannot establish - -Use it as supplementary evidence about deployment activity. Do not use it alone to conclude that: - -- a particular project, model, or backend is currently running -- every recorded action completed successfully -- a later inference request was served by this same CVM -- the images involved satisfy your source and build-provenance policy - -The same model hostname can be load-balanced across instances. A direct report and a later inference request are therefore not automatically tied to the same CVM. - -## Verify recorded actions - -When this optional evidence is required, verify it as a separate attestation flow: - -1. Generate a fresh client nonce and require it to equal `compose_manager_attestation.nonce`. -2. Serialize `actions` as compact UTF-8 JSON with every object key sorted, then require its SHA-256 hash to equal `actions_hash`. Preserve the array order. -3. Verify the nested `quote` with an Intel DCAP verifier and replay its `event_log` against the verified quote. -4. Require the reported `report_data` to equal the verified quote's report data, and require that value to be `actions_hash || client_nonce`. -5. For an action that names a source `commit`, `file`, and `file_sha256`, obtain that exact file revision from the expected source and require its SHA-256 hash to match. - -These checks establish that the verified Compose Manager quote bound the returned action record to your nonce. They do not add a project or model identity, a successful-operation outcome, or a binding to a later inference request. - -## Use it with independent checks - -When your application requires evidence for an inference response, verify the direct [model attestation](/cloud/verification/direct/model-attestation), [TLS connection binding](/cloud/verification/direct/tls) when required, and [response signature](/cloud/verification/direct/response-signatures) when available. - -When your policy requires software provenance, verify the image digests from the quote-bound configuration using the [direct image-provenance flow](/cloud/verification/direct/image-provenance). An action log or an image reference does not by itself verify image provenance. - - - Treat unavailable or incomplete deployment-control evidence as unavailable evidence for that optional check. Do not substitute it for a model quote, a response signature, or a verified provenance statement. - diff --git a/cloud/verification/direct/index.mdx b/cloud/verification/direct/index.mdx index 87c0239..7f753bd 100644 --- a/cloud/verification/direct/index.mdx +++ b/cloud/verification/direct/index.mdx @@ -20,9 +20,6 @@ Use this guide when your application sends inference requests directly to `https Check that the software images in the attested environment meet your source and build policy. - - Review optional deployment-management evidence separately from verification of an individual request. - ## Start with model attestation diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx index 54aea0c..51bc3e9 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -36,7 +36,6 @@ The root response is the model report. Relevant fields include: | `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. | -| `compose_manager_attestation` | Optional, separate deployment-control evidence. See [deployment control evidence](/cloud/verification/direct/deployment-control-evidence). | ## Verify the report diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index 572301e..945ef11 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -43,7 +43,7 @@ For NEAR-operated reports, quote report data is 64 bytes. The final 32 bytes are - 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 to NEAR-operated dstack reports only. Compose Manager evidence and other providers have their own report-data contracts. +This layout applies only to NEAR-operated dstack reports; other providers have their own report-data contracts. ## Verify GPU evidence when present @@ -66,6 +66,6 @@ NRAS returns a JSON array whose first entry is `["JWT", ""]`. Decode that ## Reference code -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains runnable TypeScript and Python reference flows for gateway and model evidence, response signatures, direct TLS, and Compose Manager evidence. It is reference code rather than a general-purpose client or SDK; use the endpoint guides here to choose the evidence and policy your application requires. +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains runnable TypeScript and Python reference flows for gateway and model evidence, response signatures, and direct TLS. It is reference code rather than a general-purpose client or SDK; use the endpoint guides here to choose the evidence and policy your application requires. For a language-specific library, see the [Verification SDK](/cloud/verification/reference/sdk). diff --git a/cloud/verification/reference/sdk.mdx b/cloud/verification/reference/sdk.mdx index 0714b24..eb1d43c 100644 --- a/cloud/verification/reference/sdk.mdx +++ b/cloud/verification/reference/sdk.mdx @@ -61,4 +61,4 @@ Runnable TypeScript, Python, and Rust examples follow this flow for non-streamin 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, Compose Manager evidence, image provenance, and E2EE, use the corresponding endpoint guides. +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/docs.json b/docs.json index d4dd40c..74ae4df 100644 --- a/docs.json +++ b/docs.json @@ -85,8 +85,7 @@ "cloud/verification/direct/model-attestation", "cloud/verification/direct/tls", "cloud/verification/direct/response-signatures", - "cloud/verification/direct/image-provenance", - "cloud/verification/direct/deployment-control-evidence" + "cloud/verification/direct/image-provenance" ] }, { From 4dc7ca38da21f39b5193e0640a1781757a1d4bf9 Mon Sep 17 00:00:00 2001 From: Coffee Date: Thu, 10 Sep 2026 10:45:36 +0800 Subject: [PATCH 23/36] docs: remove deprecated verifier reference --- cloud/verification/reference/quote-nonce-signer.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index 945ef11..01ccdfc 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -64,8 +64,6 @@ curl --fail-with-body -sS -X POST \ 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. -## Reference code - -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains runnable TypeScript and Python reference flows for gateway and model evidence, response signatures, and direct TLS. It is reference code rather than a general-purpose client or SDK; use the endpoint guides here to choose the evidence and policy your application requires. +## Verification SDK For a language-specific library, see the [Verification SDK](/cloud/verification/reference/sdk). From 3afac5a50108bee31c23623babc94054ffc3b234 Mon Sep 17 00:00:00 2001 From: Coffee Date: Thu, 17 Sep 2026 10:32:24 +0800 Subject: [PATCH 24/36] docs: rename inference SDK packages --- .../reference/quote-nonce-signer.mdx | 4 +-- cloud/verification/reference/sdk.mdx | 30 ++++++++----------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index 01ccdfc..2e0bcec 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -64,6 +64,6 @@ curl --fail-with-body -sS -X POST \ 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 +## NEAR AI Inference SDK -For a language-specific library, see the [Verification SDK](/cloud/verification/reference/sdk). +For a language-specific library, see the [NEAR AI Inference SDK](/cloud/verification/reference/sdk). diff --git a/cloud/verification/reference/sdk.mdx b/cloud/verification/reference/sdk.mdx index eb1d43c..81b5487 100644 --- a/cloud/verification/reference/sdk.mdx +++ b/cloud/verification/reference/sdk.mdx @@ -1,11 +1,11 @@ --- -title: "Verification SDK" -description: "Use the NEAR AI verification SDK to verify attestation evidence in your application." +title: "NEAR AI Inference SDK" +description: "Use the NEAR AI Inference 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 NEAR AI Inference 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. +The SDK supports TypeScript, Python, and Rust. The TypeScript package also provides `InferenceClient` for NEAR AI Cloud Gateway Chat Completions with E2EE. ## Installation @@ -15,7 +15,7 @@ The SDK supports TypeScript, Python, and Rust. Requires Node.js 24 or later. ```bash -npm install verifiable-ai-sdk +npm install @nearai/inference-sdk@next ``` @@ -23,20 +23,12 @@ npm install verifiable-ai-sdk Requires Python 3.12 or later. -```bash -pip install verifiable-ai-sdk -``` - -Import it as `verifiable_ai_sdk`. +The `nearai-inference-sdk` distribution is not published to PyPI yet. It will import as `nearai_inference_sdk` when released. -```bash -cargo add verifiable-ai-sdk -``` - -Import it as `verifiable_ai_sdk`. +The `nearai-inference-sdk` crate is not published to crates.io yet. It will import as `nearai_inference_sdk` when released. @@ -45,7 +37,9 @@ Import it as `verifiable_ai_sdk`. 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. +`AttestationClient` 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. + +In TypeScript, `InferenceClient` can instead verify Gateway and model evidence before sending a Gateway Chat Completion. It enables E2EE by default, retains the exact request and response bytes, and can verify the completion response afterward. 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. @@ -58,7 +52,7 @@ Use one explicit `signing_algo` for the Gateway attestation request, model-attes 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. + In TypeScript, import from `@nearai/inference-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 `@nearai/inference-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. +The SDK currently covers the NEAR AI Cloud Gateway flow. For direct model endpoints, use the corresponding endpoint guides. From f1a8bbcfe69f3db7d52ff0eb9e2f47ef5cb19a9a Mon Sep 17 00:00:00 2001 From: Coffee Date: Mon, 21 Sep 2026 13:11:52 +0800 Subject: [PATCH 25/36] docs: add inference SDK tutorial --- cloud/verification/index.mdx | 4 ++ .../reference/quote-nonce-signer.mdx | 2 +- cloud/verification/reference/sdk.mdx | 58 ---------------- cloud/verification/sdk.mdx | 68 +++++++++++++++++++ docs.json | 11 ++- 5 files changed, 83 insertions(+), 60 deletions(-) delete mode 100644 cloud/verification/reference/sdk.mdx create mode 100644 cloud/verification/sdk.mdx diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 1a1f179..2b079ba 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -20,6 +20,10 @@ Use these guides to verify the evidence returned for your AI requests. Start wit 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.
+ + Start with a verified Gateway Chat Completion in TypeScript, then follow the endpoint guides when you need manual control. + + ## 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. diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index 2e0bcec..aaf20ba 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -66,4 +66,4 @@ NRAS returns a JSON array whose first entry is `["JWT", ""]`. Decode that ## NEAR AI Inference SDK -For a language-specific library, see the [NEAR AI Inference SDK](/cloud/verification/reference/sdk). +For a language-specific library, see the [NEAR AI Inference SDK](/cloud/verification/sdk). diff --git a/cloud/verification/reference/sdk.mdx b/cloud/verification/reference/sdk.mdx deleted file mode 100644 index 81b5487..0000000 --- a/cloud/verification/reference/sdk.mdx +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: "NEAR AI Inference SDK" -description: "Use the NEAR AI Inference SDK to verify attestation evidence in your application." ---- - -The NEAR AI Inference 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. The TypeScript package also provides `InferenceClient` for NEAR AI Cloud Gateway Chat Completions with E2EE. - -## Installation - - - - -Requires Node.js 24 or later. - -```bash -npm install @nearai/inference-sdk@next -``` - - - - -Requires Python 3.12 or later. - -The `nearai-inference-sdk` distribution is not published to PyPI yet. It will import as `nearai_inference_sdk` when released. - - - - -The `nearai-inference-sdk` crate is not published to crates.io yet. It will import as `nearai_inference_sdk` when released. - - - - -## 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. - -`AttestationClient` 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. - -In TypeScript, `InferenceClient` can instead verify Gateway and model evidence before sending a Gateway Chat Completion. It enables E2EE by default, retains the exact request and response bytes, and can verify the completion response afterward. - -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 `@nearai/inference-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 `@nearai/inference-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, use the corresponding endpoint guides. diff --git a/cloud/verification/sdk.mdx b/cloud/verification/sdk.mdx new file mode 100644 index 0000000..f052691 --- /dev/null +++ b/cloud/verification/sdk.mdx @@ -0,0 +1,68 @@ +--- +title: "NEAR AI Inference SDK" +description: "Send a verified, end-to-end encrypted Chat Completion with the NEAR AI Inference SDK." +icon: "braces" +--- + +Use the TypeScript SDK to verify deployment evidence before a NEAR AI Cloud Chat Completion and verify its response signature afterward. This tutorial uses the NEAR AI Cloud gateway. + +## Install + +The package requires Node.js 24 or later. + +```bash +npm install @nearai/inference-sdk@next +``` + +## Send and verify a Chat Completion + +Import the Node entry point when your application connects directly to NEAR AI Cloud. + +```ts +import { InferenceClient } from '@nearai/inference-sdk/node'; + +const apiKey = process.env.NEARAI_API_KEY; +if (!apiKey) throw new Error('NEARAI_API_KEY is required'); + +const client = new InferenceClient({ apiKey }); + +const completion = await client.chat.completions.create({ + model: 'z-ai/glm-5.3-flash', + messages: [{ role: 'user', content: 'Reply with the word ok.' }], +}); + +console.log(completion.choices[0]?.message.content); + +const receipt = await client.verifyResponse(completion.id); +console.log(`Verified ${receipt.signatureKind} response.`); +``` + +The SDK verifies the Gateway and every returned NEAR model-attestation candidate before sending the request. E2EE is enabled by default. `verifyResponse()` verifies the response signature using the request and response bytes captured by the client. + + + `verifyResponse()` is explicit. For a streaming completion, consume the entire stream first, then call it with the completion ID so the SDK can verify the exact response bytes. + + +## Use the right entry point + +- Use `@nearai/inference-sdk/node` for a server-side integration that connects directly to `cloud-api.near.ai`. It verifies the Gateway TLS peer and pins later requests to the verified Gateway identity. +- Use `@nearai/inference-sdk` in a browser or behind an application proxy. Browser Fetch cannot read the Gateway TLS peer certificate, so this entry point does not perform Gateway TLS binding. + +For direct model endpoints or a manual verification flow, use the corresponding [direct endpoint](/cloud/verification/direct) or [gateway](/cloud/verification/cloud-api) guide. + +## Next steps + + + + Learn the evidence and response-signature checks behind the Gateway flow. + + + Learn the protocol when you need to manage encryption yourself. + + + Define the measurements, provenance, and freshness rules your application accepts. + + + Verify a request sent directly to a model endpoint. + + diff --git a/docs.json b/docs.json index 74ae4df..884bef3 100644 --- a/docs.json +++ b/docs.json @@ -88,10 +88,15 @@ "cloud/verification/direct/image-provenance" ] }, + { + "group": "NEAR AI Inference SDK", + "pages": [ + "cloud/verification/sdk" + ] + }, { "group": "Reference", "pages": [ - "cloud/verification/reference/sdk", "cloud/verification/reference/quote-nonce-signer", "cloud/verification/reference/response-signature-verification", "cloud/verification/reference/verification-policy" @@ -531,6 +536,10 @@ { "source": "/cloud/verification/provenance", "destination": "/cloud/verification" + }, + { + "source": "/cloud/verification/reference/sdk", + "destination": "/cloud/verification/sdk" } ] } From 7dc9917d69226577bcbd3905abc4351dbcf3181f Mon Sep 17 00:00:00 2001 From: Coffee Date: Mon, 21 Sep 2026 13:44:05 +0800 Subject: [PATCH 26/36] docs: separate Cloud and direct SDK usage --- cloud/verification/index.mdx | 2 +- cloud/verification/sdk.mdx | 50 ++++++++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 2b079ba..1e60a2c 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -21,7 +21,7 @@ Use these guides to verify the evidence returned for your AI requests. Start wit
- Start with a verified Gateway Chat Completion in TypeScript, then follow the endpoint guides when you need manual control. + Send a verified Chat Completion through NEAR AI Cloud or a direct model endpoint in TypeScript. ## What happens next diff --git a/cloud/verification/sdk.mdx b/cloud/verification/sdk.mdx index f052691..2ed62b3 100644 --- a/cloud/verification/sdk.mdx +++ b/cloud/verification/sdk.mdx @@ -1,10 +1,10 @@ --- title: "NEAR AI Inference SDK" -description: "Send a verified, end-to-end encrypted Chat Completion with the NEAR AI Inference SDK." +description: "Send a verified, end-to-end encrypted Chat Completion through NEAR AI Cloud or a direct model endpoint." icon: "braces" --- -Use the TypeScript SDK to verify deployment evidence before a NEAR AI Cloud Chat Completion and verify its response signature afterward. This tutorial uses the NEAR AI Cloud gateway. +Use the TypeScript SDK to verify deployment evidence before a Chat Completion and verify its response signature afterward. Choose the client that matches the endpoint your application calls. ## Install @@ -14,20 +14,22 @@ The package requires Node.js 24 or later. npm install @nearai/inference-sdk@next ``` -## Send and verify a Chat Completion +## NEAR AI Cloud -Import the Node entry point when your application connects directly to NEAR AI Cloud. +Use `InferenceClient` when your application sends requests to NEAR AI Cloud. Set `baseUrl` to the Gateway endpoint your application uses. ```ts import { InferenceClient } from '@nearai/inference-sdk/node'; +const baseUrl = 'https://cloud-api.near.ai/v1'; +const model = 'z-ai/glm-5.3-flash'; const apiKey = process.env.NEARAI_API_KEY; if (!apiKey) throw new Error('NEARAI_API_KEY is required'); -const client = new InferenceClient({ apiKey }); +const client = new InferenceClient({ baseUrl, apiKey }); const completion = await client.chat.completions.create({ - model: 'z-ai/glm-5.3-flash', + model, messages: [{ role: 'user', content: 'Reply with the word ok.' }], }); @@ -37,18 +39,44 @@ const receipt = await client.verifyResponse(completion.id); console.log(`Verified ${receipt.signatureKind} response.`); ``` -The SDK verifies the Gateway and every returned NEAR model-attestation candidate before sending the request. E2EE is enabled by default. `verifyResponse()` verifies the response signature using the request and response bytes captured by the client. +Before sending the completion, the SDK verifies the Gateway and every returned NEAR model-attestation candidate. E2EE is enabled by default. `verifyResponse()` verifies the response signature using the request and response bytes captured by the client. + +## Direct model endpoint + +Use `DirectInferenceClient` when your application sends requests directly to a model endpoint. Set `baseUrl` to that endpoint, including `/v1`. + +```ts +import { DirectInferenceClient } from '@nearai/inference-sdk/node'; + +const baseUrl = 'https://glm-5-3-flash.completions.near.ai/v1'; +const model = 'z-ai/glm-5.3-flash'; +const apiKey = process.env.NEARAI_API_KEY; +if (!apiKey) throw new Error('NEARAI_API_KEY is required'); + +const client = new DirectInferenceClient({ baseUrl, apiKey }); + +const completion = await client.chat.completions.create({ + model, + messages: [{ role: 'user', content: 'Reply with the word ok.' }], +}); + +console.log(completion.choices[0]?.message.content); + +const receipt = await client.verifyResponse(completion.id); +console.log(`Verified ${receipt.signatureKind} response.`); +``` + +Before sending the completion, the SDK verifies the complete model-attestation set reported by that endpoint. It does not request Gateway evidence. E2EE is enabled by default. A direct endpoint can require credentials different from your NEAR AI Cloud API key. `verifyResponse()` is explicit. For a streaming completion, consume the entire stream first, then call it with the completion ID so the SDK can verify the exact response bytes. -## Use the right entry point +## Node, browser, and proxy integrations -- Use `@nearai/inference-sdk/node` for a server-side integration that connects directly to `cloud-api.near.ai`. It verifies the Gateway TLS peer and pins later requests to the verified Gateway identity. -- Use `@nearai/inference-sdk` in a browser or behind an application proxy. Browser Fetch cannot read the Gateway TLS peer certificate, so this entry point does not perform Gateway TLS binding. +The examples use `@nearai/inference-sdk/node`. In Node, the SDK verifies the TLS peer for the configured Gateway or direct model endpoint and pins later requests to the verified identity. -For direct model endpoints or a manual verification flow, use the corresponding [direct endpoint](/cloud/verification/direct) or [gateway](/cloud/verification/cloud-api) guide. +For a browser or an application proxy, import from `@nearai/inference-sdk`. Browser Fetch cannot read the TLS peer certificate, so this entry point does not perform TLS binding. Configure the same explicit `baseUrl` for the endpoint your application uses. ## Next steps From 548d92ffb7be52df232307789a35f950adf2dc13 Mon Sep 17 00:00:00 2001 From: Coffee Date: Mon, 21 Sep 2026 13:49:39 +0800 Subject: [PATCH 27/36] docs: split SDK endpoint guides --- cloud/verification/index.mdx | 13 +++- .../reference/quote-nonce-signer.mdx | 2 +- .../{sdk.mdx => sdk/cloud-api.mdx} | 46 +++---------- cloud/verification/sdk/direct.mdx | 66 +++++++++++++++++++ docs.json | 9 ++- 5 files changed, 92 insertions(+), 44 deletions(-) rename cloud/verification/{sdk.mdx => sdk/cloud-api.mdx} (50%) create mode 100644 cloud/verification/sdk/direct.mdx diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 1e60a2c..ac0361f 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -20,9 +20,16 @@ Use these guides to verify the evidence returned for your AI requests. Start wit 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. - - Send a verified Chat Completion through NEAR AI Cloud or a direct model endpoint in TypeScript. - +## Use the SDK + + + + Send a verified Chat Completion through NEAR AI Cloud in TypeScript. + + + Send a verified Chat Completion directly to a model endpoint in TypeScript. + + ## What happens next diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index aaf20ba..4bb0ee3 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -66,4 +66,4 @@ NRAS returns a JSON array whose first entry is `["JWT", ""]`. Decode that ## NEAR AI Inference SDK -For a language-specific library, see the [NEAR AI Inference SDK](/cloud/verification/sdk). +For a TypeScript SDK integration, choose [NEAR AI Cloud](/cloud/verification/sdk/cloud-api) or a [direct model endpoint](/cloud/verification/sdk/direct). diff --git a/cloud/verification/sdk.mdx b/cloud/verification/sdk/cloud-api.mdx similarity index 50% rename from cloud/verification/sdk.mdx rename to cloud/verification/sdk/cloud-api.mdx index 2ed62b3..344335c 100644 --- a/cloud/verification/sdk.mdx +++ b/cloud/verification/sdk/cloud-api.mdx @@ -1,10 +1,10 @@ --- -title: "NEAR AI Inference SDK" -description: "Send a verified, end-to-end encrypted Chat Completion through NEAR AI Cloud or a direct model endpoint." -icon: "braces" +title: "NEAR AI Cloud" +description: "Send a verified, end-to-end encrypted Chat Completion through NEAR AI Cloud." +icon: "cloud" --- -Use the TypeScript SDK to verify deployment evidence before a Chat Completion and verify its response signature afterward. Choose the client that matches the endpoint your application calls. +Use `InferenceClient` when your application sends requests through NEAR AI Cloud. ## Install @@ -14,9 +14,9 @@ The package requires Node.js 24 or later. npm install @nearai/inference-sdk@next ``` -## NEAR AI Cloud +## Send and verify a Chat Completion -Use `InferenceClient` when your application sends requests to NEAR AI Cloud. Set `baseUrl` to the Gateway endpoint your application uses. +Set `baseUrl` to the NEAR AI Cloud endpoint your application uses. ```ts import { InferenceClient } from '@nearai/inference-sdk/node'; @@ -41,42 +41,15 @@ console.log(`Verified ${receipt.signatureKind} response.`); Before sending the completion, the SDK verifies the Gateway and every returned NEAR model-attestation candidate. E2EE is enabled by default. `verifyResponse()` verifies the response signature using the request and response bytes captured by the client. -## Direct model endpoint - -Use `DirectInferenceClient` when your application sends requests directly to a model endpoint. Set `baseUrl` to that endpoint, including `/v1`. - -```ts -import { DirectInferenceClient } from '@nearai/inference-sdk/node'; - -const baseUrl = 'https://glm-5-3-flash.completions.near.ai/v1'; -const model = 'z-ai/glm-5.3-flash'; -const apiKey = process.env.NEARAI_API_KEY; -if (!apiKey) throw new Error('NEARAI_API_KEY is required'); - -const client = new DirectInferenceClient({ baseUrl, apiKey }); - -const completion = await client.chat.completions.create({ - model, - messages: [{ role: 'user', content: 'Reply with the word ok.' }], -}); - -console.log(completion.choices[0]?.message.content); - -const receipt = await client.verifyResponse(completion.id); -console.log(`Verified ${receipt.signatureKind} response.`); -``` - -Before sending the completion, the SDK verifies the complete model-attestation set reported by that endpoint. It does not request Gateway evidence. E2EE is enabled by default. A direct endpoint can require credentials different from your NEAR AI Cloud API key. - `verifyResponse()` is explicit. For a streaming completion, consume the entire stream first, then call it with the completion ID so the SDK can verify the exact response bytes. ## Node, browser, and proxy integrations -The examples use `@nearai/inference-sdk/node`. In Node, the SDK verifies the TLS peer for the configured Gateway or direct model endpoint and pins later requests to the verified identity. +The example uses `@nearai/inference-sdk/node`. In Node, the SDK verifies the Gateway TLS peer and pins later requests to the verified Gateway identity. -For a browser or an application proxy, import from `@nearai/inference-sdk`. Browser Fetch cannot read the TLS peer certificate, so this entry point does not perform TLS binding. Configure the same explicit `baseUrl` for the endpoint your application uses. +For a browser or an application proxy, import from `@nearai/inference-sdk`. Browser Fetch cannot read the TLS peer certificate, so this entry point does not perform Gateway TLS binding. Configure the same explicit `baseUrl` for the endpoint your application uses. ## Next steps @@ -90,7 +63,4 @@ For a browser or an application proxy, import from `@nearai/inference-sdk`. Brow Define the measurements, provenance, and freshness rules your application accepts. - - Verify a request sent directly to a model endpoint. -
diff --git a/cloud/verification/sdk/direct.mdx b/cloud/verification/sdk/direct.mdx new file mode 100644 index 0000000..8023660 --- /dev/null +++ b/cloud/verification/sdk/direct.mdx @@ -0,0 +1,66 @@ +--- +title: "Direct Model Endpoints" +description: "Send a verified, end-to-end encrypted Chat Completion directly to a model endpoint." +icon: "server" +--- + +Use `DirectInferenceClient` when your application sends requests directly to a model endpoint. + +## Install + +The package requires Node.js 24 or later. + +```bash +npm install @nearai/inference-sdk@next +``` + +## Send and verify a Chat Completion + +Set `baseUrl` to the direct model endpoint, including `/v1`. + +```ts +import { DirectInferenceClient } from '@nearai/inference-sdk/node'; + +const baseUrl = 'https://glm-5-3-flash.completions.near.ai/v1'; +const model = 'z-ai/glm-5.3-flash'; +const apiKey = process.env.NEARAI_API_KEY; +if (!apiKey) throw new Error('NEARAI_API_KEY is required'); + +const client = new DirectInferenceClient({ baseUrl, apiKey }); + +const completion = await client.chat.completions.create({ + model, + messages: [{ role: 'user', content: 'Reply with the word ok.' }], +}); + +console.log(completion.choices[0]?.message.content); + +const receipt = await client.verifyResponse(completion.id); +console.log(`Verified ${receipt.signatureKind} response.`); +``` + +Before sending the completion, the SDK verifies the complete model-attestation set reported by that endpoint. It does not request Gateway evidence. E2EE is enabled by default. A direct endpoint can require credentials different from your NEAR AI Cloud API key. + + + `verifyResponse()` is explicit. For a streaming completion, consume the entire stream first, then call it with the completion ID so the SDK can verify the exact response bytes. + + +## Node, browser, and proxy integrations + +The example uses `@nearai/inference-sdk/node`. In Node, the SDK verifies the direct endpoint TLS peer and pins later requests to the verified model identity. + +For a browser or an application proxy, import from `@nearai/inference-sdk`. Browser Fetch cannot read the TLS peer certificate, so this entry point does not perform direct-endpoint TLS binding. Configure the same explicit `baseUrl` for the endpoint your application uses. + +## Next steps + + + + Learn the evidence and response-signature checks behind the direct flow. + + + Learn the protocol when you need to manage encryption yourself. + + + Define the measurements, provenance, and freshness rules your application accepts. + + diff --git a/docs.json b/docs.json index 884bef3..7df4ceb 100644 --- a/docs.json +++ b/docs.json @@ -91,7 +91,8 @@ { "group": "NEAR AI Inference SDK", "pages": [ - "cloud/verification/sdk" + "cloud/verification/sdk/cloud-api", + "cloud/verification/sdk/direct" ] }, { @@ -539,7 +540,11 @@ }, { "source": "/cloud/verification/reference/sdk", - "destination": "/cloud/verification/sdk" + "destination": "/cloud/verification/sdk/cloud-api" + }, + { + "source": "/cloud/verification/sdk", + "destination": "/cloud/verification/sdk/cloud-api" } ] } From 7d80f157e496739df9f72285c168dfa969c3a5b7 Mon Sep 17 00:00:00 2001 From: Coffee Date: Mon, 21 Sep 2026 13:51:29 +0800 Subject: [PATCH 28/36] docs: name Cloud SDK guide after gateway --- cloud/verification/index.mdx | 4 ++-- cloud/verification/reference/quote-nonce-signer.mdx | 2 +- cloud/verification/sdk/cloud-api.mdx | 9 ++++----- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index ac0361f..726411d 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -23,8 +23,8 @@ Use these guides to verify the evidence returned for your AI requests. Start wit ## Use the SDK - - Send a verified Chat Completion through NEAR AI Cloud in TypeScript. + + Send a verified Chat Completion through the NEAR AI Cloud Gateway in TypeScript. Send a verified Chat Completion directly to a model endpoint in TypeScript. diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index 4bb0ee3..cd7ca42 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -66,4 +66,4 @@ NRAS returns a JSON array whose first entry is `["JWT", ""]`. Decode that ## NEAR AI Inference SDK -For a TypeScript SDK integration, choose [NEAR AI Cloud](/cloud/verification/sdk/cloud-api) or a [direct model endpoint](/cloud/verification/sdk/direct). +For a TypeScript SDK integration, choose the [NEAR AI Cloud Gateway](/cloud/verification/sdk/cloud-api) or a [direct model endpoint](/cloud/verification/sdk/direct). diff --git a/cloud/verification/sdk/cloud-api.mdx b/cloud/verification/sdk/cloud-api.mdx index 344335c..6f3f635 100644 --- a/cloud/verification/sdk/cloud-api.mdx +++ b/cloud/verification/sdk/cloud-api.mdx @@ -1,10 +1,9 @@ --- -title: "NEAR AI Cloud" -description: "Send a verified, end-to-end encrypted Chat Completion through NEAR AI Cloud." -icon: "cloud" +title: "NEAR AI Cloud Gateway" +description: "Send a verified, end-to-end encrypted Chat Completion through the NEAR AI Cloud Gateway." --- -Use `InferenceClient` when your application sends requests through NEAR AI Cloud. +Use `InferenceClient` when your application sends requests through the NEAR AI Cloud Gateway. ## Install @@ -16,7 +15,7 @@ npm install @nearai/inference-sdk@next ## Send and verify a Chat Completion -Set `baseUrl` to the NEAR AI Cloud endpoint your application uses. +Set `baseUrl` to the NEAR AI Cloud Gateway endpoint your application uses. ```ts import { InferenceClient } from '@nearai/inference-sdk/node'; From 411450b5ccfc409d4960418a04f24db3b1eead7e Mon Sep 17 00:00:00 2001 From: Coffee Date: Mon, 21 Sep 2026 13:52:40 +0800 Subject: [PATCH 29/36] docs: remove direct SDK page icon --- cloud/verification/sdk/direct.mdx | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/verification/sdk/direct.mdx b/cloud/verification/sdk/direct.mdx index 8023660..baed203 100644 --- a/cloud/verification/sdk/direct.mdx +++ b/cloud/verification/sdk/direct.mdx @@ -1,7 +1,6 @@ --- title: "Direct Model Endpoints" description: "Send a verified, end-to-end encrypted Chat Completion directly to a model endpoint." -icon: "server" --- Use `DirectInferenceClient` when your application sends requests directly to a model endpoint. From b3220e96a787f5ed06273fefb3635bcdb2b3abd0 Mon Sep 17 00:00:00 2001 From: Coffee Date: Mon, 21 Sep 2026 18:25:54 +0800 Subject: [PATCH 30/36] docs: add SDK E2EE and OHTTP examples --- cloud/verification/sdk/cloud-api.mdx | 32 +++++++++++++++++++++---- cloud/verification/sdk/direct.mdx | 36 +++++++++++++++++++++++----- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/cloud/verification/sdk/cloud-api.mdx b/cloud/verification/sdk/cloud-api.mdx index 6f3f635..b71af6c 100644 --- a/cloud/verification/sdk/cloud-api.mdx +++ b/cloud/verification/sdk/cloud-api.mdx @@ -13,9 +13,9 @@ The package requires Node.js 24 or later. npm install @nearai/inference-sdk@next ``` -## Send and verify a Chat Completion +## Send and verify an E2EE Chat Completion -Set `baseUrl` to the NEAR AI Cloud Gateway endpoint your application uses. +Set `baseUrl` to the NEAR AI Cloud Gateway endpoint your application uses. E2EE is enabled by default; it is set explicitly below. ```ts import { InferenceClient } from '@nearai/inference-sdk/node'; @@ -25,7 +25,11 @@ const model = 'z-ai/glm-5.3-flash'; const apiKey = process.env.NEARAI_API_KEY; if (!apiKey) throw new Error('NEARAI_API_KEY is required'); -const client = new InferenceClient({ baseUrl, apiKey }); +const client = new InferenceClient({ + baseUrl, + apiKey, + e2ee: true, +}); const completion = await client.chat.completions.create({ model, @@ -38,12 +42,32 @@ const receipt = await client.verifyResponse(completion.id); console.log(`Verified ${receipt.signatureKind} response.`); ``` -Before sending the completion, the SDK verifies the Gateway and every returned NEAR model-attestation candidate. E2EE is enabled by default. `verifyResponse()` verifies the response signature using the request and response bytes captured by the client. +Before sending the completion, the SDK verifies the Gateway and every returned NEAR model-attestation candidate. It encrypts supported Chat fields to a key from the verified model evidence and decrypts the response automatically. `verifyResponse()` verifies the response signature using the request and response bytes captured by the client. `verifyResponse()` is explicit. For a streaming completion, consume the entire stream first, then call it with the completion ID so the SDK can verify the exact response bytes. +## Add OHTTP + +Set `ohttp: true` to encapsulate Chat requests and responses with OHTTP. OHTTP requires Ed25519, so set `signingAlgo` explicitly. + +```ts +const client = new InferenceClient({ + baseUrl, + apiKey, + signingAlgo: 'ed25519', + e2ee: true, + ohttp: true, +}); +``` + +The Chat and `verifyResponse()` calls stay the same. The SDK verifies the Gateway's signed OHTTP configuration before sending Chat; a missing or invalid configuration blocks the request. OHTTP encapsulates the exchange to the Gateway, while E2EE encrypts supported Chat fields to the verified model key. + + + OHTTP applies only to Chat. Attestation and signature requests remain regular HTTPS. The configured endpoint or proxy must expose `/ohttp` at the same origin. Authorization and custom headers sent on the outer request, along with the client's network address, are not hidden by OHTTP. + + ## Node, browser, and proxy integrations The example uses `@nearai/inference-sdk/node`. In Node, the SDK verifies the Gateway TLS peer and pins later requests to the verified Gateway identity. diff --git a/cloud/verification/sdk/direct.mdx b/cloud/verification/sdk/direct.mdx index baed203..08b317d 100644 --- a/cloud/verification/sdk/direct.mdx +++ b/cloud/verification/sdk/direct.mdx @@ -13,9 +13,9 @@ The package requires Node.js 24 or later. npm install @nearai/inference-sdk@next ``` -## Send and verify a Chat Completion +## Send and verify an E2EE Chat Completion -Set `baseUrl` to the direct model endpoint, including `/v1`. +Set `baseUrl` to the direct model endpoint, including `/v1`. E2EE is enabled by default; it is set explicitly below. ```ts import { DirectInferenceClient } from '@nearai/inference-sdk/node'; @@ -25,7 +25,11 @@ const model = 'z-ai/glm-5.3-flash'; const apiKey = process.env.NEARAI_API_KEY; if (!apiKey) throw new Error('NEARAI_API_KEY is required'); -const client = new DirectInferenceClient({ baseUrl, apiKey }); +const client = new DirectInferenceClient({ + baseUrl, + apiKey, + e2ee: true, +}); const completion = await client.chat.completions.create({ model, @@ -38,17 +42,37 @@ const receipt = await client.verifyResponse(completion.id); console.log(`Verified ${receipt.signatureKind} response.`); ``` -Before sending the completion, the SDK verifies the complete model-attestation set reported by that endpoint. It does not request Gateway evidence. E2EE is enabled by default. A direct endpoint can require credentials different from your NEAR AI Cloud API key. +Before sending the completion, the SDK verifies the complete model-attestation set reported by that endpoint. It does not request Gateway evidence. It encrypts supported Chat fields to a key from the verified model evidence and decrypts the response automatically. A direct endpoint can require credentials different from your NEAR AI Cloud API key. `verifyResponse()` is explicit. For a streaming completion, consume the entire stream first, then call it with the completion ID so the SDK can verify the exact response bytes. +## Add OHTTP + +Set `ohttp: true` to encapsulate Chat requests and responses with OHTTP. OHTTP requires Ed25519, so set `signingAlgo` explicitly. + +```ts +const client = new DirectInferenceClient({ + baseUrl, + apiKey, + signingAlgo: 'ed25519', + e2ee: true, + ohttp: true, +}); +``` + +The Chat and `verifyResponse()` calls stay the same. The SDK verifies the direct endpoint's signed OHTTP configuration before sending Chat; a missing or invalid configuration blocks the request. For a direct endpoint, both OHTTP and E2EE are scoped to the model service. + + + OHTTP applies only to Chat. Attestation and signature requests remain regular HTTPS. The configured endpoint or proxy must expose `/ohttp` at the same origin. Authorization and custom headers sent on the outer request, along with the client's network address, are not hidden by OHTTP. + + ## Node, browser, and proxy integrations -The example uses `@nearai/inference-sdk/node`. In Node, the SDK verifies the direct endpoint TLS peer and pins later requests to the verified model identity. +The example uses `@nearai/inference-sdk/node`. Direct endpoint TLS binding is not currently enabled; normal HTTPS certificate validation still applies. -For a browser or an application proxy, import from `@nearai/inference-sdk`. Browser Fetch cannot read the TLS peer certificate, so this entry point does not perform direct-endpoint TLS binding. Configure the same explicit `baseUrl` for the endpoint your application uses. +For a browser or an application proxy, import from `@nearai/inference-sdk`. Configure the same explicit `baseUrl` for the endpoint your application uses. ## Next steps From f9293376adef76ae3ff8d812e8422a8095b37a9a Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 22 Sep 2026 11:52:02 +0800 Subject: [PATCH 31/36] docs: separate SDK guides from verification --- cloud/verification/index.mdx | 11 --- .../reference/quote-nonce-signer.mdx | 4 - cloud/verification/sdk/cloud-api.mdx | 89 ------------------- cloud/verification/sdk/direct.mdx | 89 ------------------- docs.json | 15 ---- 5 files changed, 208 deletions(-) delete mode 100644 cloud/verification/sdk/cloud-api.mdx delete mode 100644 cloud/verification/sdk/direct.mdx diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 726411d..1a1f179 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -20,17 +20,6 @@ Use these guides to verify the evidence returned for your AI requests. Start wit 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. -## Use the SDK - - - - Send a verified Chat Completion through the NEAR AI Cloud Gateway in TypeScript. - - - Send a verified Chat Completion directly to a model endpoint in TypeScript. - - - ## 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. diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index cd7ca42..c0f03e7 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -63,7 +63,3 @@ curl --fail-with-body -sS -X POST \ ``` 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. - -## NEAR AI Inference SDK - -For a TypeScript SDK integration, choose the [NEAR AI Cloud Gateway](/cloud/verification/sdk/cloud-api) or a [direct model endpoint](/cloud/verification/sdk/direct). diff --git a/cloud/verification/sdk/cloud-api.mdx b/cloud/verification/sdk/cloud-api.mdx deleted file mode 100644 index b71af6c..0000000 --- a/cloud/verification/sdk/cloud-api.mdx +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: "NEAR AI Cloud Gateway" -description: "Send a verified, end-to-end encrypted Chat Completion through the NEAR AI Cloud Gateway." ---- - -Use `InferenceClient` when your application sends requests through the NEAR AI Cloud Gateway. - -## Install - -The package requires Node.js 24 or later. - -```bash -npm install @nearai/inference-sdk@next -``` - -## Send and verify an E2EE Chat Completion - -Set `baseUrl` to the NEAR AI Cloud Gateway endpoint your application uses. E2EE is enabled by default; it is set explicitly below. - -```ts -import { InferenceClient } from '@nearai/inference-sdk/node'; - -const baseUrl = 'https://cloud-api.near.ai/v1'; -const model = 'z-ai/glm-5.3-flash'; -const apiKey = process.env.NEARAI_API_KEY; -if (!apiKey) throw new Error('NEARAI_API_KEY is required'); - -const client = new InferenceClient({ - baseUrl, - apiKey, - e2ee: true, -}); - -const completion = await client.chat.completions.create({ - model, - messages: [{ role: 'user', content: 'Reply with the word ok.' }], -}); - -console.log(completion.choices[0]?.message.content); - -const receipt = await client.verifyResponse(completion.id); -console.log(`Verified ${receipt.signatureKind} response.`); -``` - -Before sending the completion, the SDK verifies the Gateway and every returned NEAR model-attestation candidate. It encrypts supported Chat fields to a key from the verified model evidence and decrypts the response automatically. `verifyResponse()` verifies the response signature using the request and response bytes captured by the client. - - - `verifyResponse()` is explicit. For a streaming completion, consume the entire stream first, then call it with the completion ID so the SDK can verify the exact response bytes. - - -## Add OHTTP - -Set `ohttp: true` to encapsulate Chat requests and responses with OHTTP. OHTTP requires Ed25519, so set `signingAlgo` explicitly. - -```ts -const client = new InferenceClient({ - baseUrl, - apiKey, - signingAlgo: 'ed25519', - e2ee: true, - ohttp: true, -}); -``` - -The Chat and `verifyResponse()` calls stay the same. The SDK verifies the Gateway's signed OHTTP configuration before sending Chat; a missing or invalid configuration blocks the request. OHTTP encapsulates the exchange to the Gateway, while E2EE encrypts supported Chat fields to the verified model key. - - - OHTTP applies only to Chat. Attestation and signature requests remain regular HTTPS. The configured endpoint or proxy must expose `/ohttp` at the same origin. Authorization and custom headers sent on the outer request, along with the client's network address, are not hidden by OHTTP. - - -## Node, browser, and proxy integrations - -The example uses `@nearai/inference-sdk/node`. In Node, the SDK verifies the Gateway TLS peer and pins later requests to the verified Gateway identity. - -For a browser or an application proxy, import from `@nearai/inference-sdk`. Browser Fetch cannot read the TLS peer certificate, so this entry point does not perform Gateway TLS binding. Configure the same explicit `baseUrl` for the endpoint your application uses. - -## Next steps - - - - Learn the evidence and response-signature checks behind the Gateway flow. - - - Learn the protocol when you need to manage encryption yourself. - - - Define the measurements, provenance, and freshness rules your application accepts. - - diff --git a/cloud/verification/sdk/direct.mdx b/cloud/verification/sdk/direct.mdx deleted file mode 100644 index 08b317d..0000000 --- a/cloud/verification/sdk/direct.mdx +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: "Direct Model Endpoints" -description: "Send a verified, end-to-end encrypted Chat Completion directly to a model endpoint." ---- - -Use `DirectInferenceClient` when your application sends requests directly to a model endpoint. - -## Install - -The package requires Node.js 24 or later. - -```bash -npm install @nearai/inference-sdk@next -``` - -## Send and verify an E2EE Chat Completion - -Set `baseUrl` to the direct model endpoint, including `/v1`. E2EE is enabled by default; it is set explicitly below. - -```ts -import { DirectInferenceClient } from '@nearai/inference-sdk/node'; - -const baseUrl = 'https://glm-5-3-flash.completions.near.ai/v1'; -const model = 'z-ai/glm-5.3-flash'; -const apiKey = process.env.NEARAI_API_KEY; -if (!apiKey) throw new Error('NEARAI_API_KEY is required'); - -const client = new DirectInferenceClient({ - baseUrl, - apiKey, - e2ee: true, -}); - -const completion = await client.chat.completions.create({ - model, - messages: [{ role: 'user', content: 'Reply with the word ok.' }], -}); - -console.log(completion.choices[0]?.message.content); - -const receipt = await client.verifyResponse(completion.id); -console.log(`Verified ${receipt.signatureKind} response.`); -``` - -Before sending the completion, the SDK verifies the complete model-attestation set reported by that endpoint. It does not request Gateway evidence. It encrypts supported Chat fields to a key from the verified model evidence and decrypts the response automatically. A direct endpoint can require credentials different from your NEAR AI Cloud API key. - - - `verifyResponse()` is explicit. For a streaming completion, consume the entire stream first, then call it with the completion ID so the SDK can verify the exact response bytes. - - -## Add OHTTP - -Set `ohttp: true` to encapsulate Chat requests and responses with OHTTP. OHTTP requires Ed25519, so set `signingAlgo` explicitly. - -```ts -const client = new DirectInferenceClient({ - baseUrl, - apiKey, - signingAlgo: 'ed25519', - e2ee: true, - ohttp: true, -}); -``` - -The Chat and `verifyResponse()` calls stay the same. The SDK verifies the direct endpoint's signed OHTTP configuration before sending Chat; a missing or invalid configuration blocks the request. For a direct endpoint, both OHTTP and E2EE are scoped to the model service. - - - OHTTP applies only to Chat. Attestation and signature requests remain regular HTTPS. The configured endpoint or proxy must expose `/ohttp` at the same origin. Authorization and custom headers sent on the outer request, along with the client's network address, are not hidden by OHTTP. - - -## Node, browser, and proxy integrations - -The example uses `@nearai/inference-sdk/node`. Direct endpoint TLS binding is not currently enabled; normal HTTPS certificate validation still applies. - -For a browser or an application proxy, import from `@nearai/inference-sdk`. Configure the same explicit `baseUrl` for the endpoint your application uses. - -## Next steps - - - - Learn the evidence and response-signature checks behind the direct flow. - - - Learn the protocol when you need to manage encryption yourself. - - - Define the measurements, provenance, and freshness rules your application accepts. - - diff --git a/docs.json b/docs.json index 7df4ceb..5e6bd91 100644 --- a/docs.json +++ b/docs.json @@ -88,13 +88,6 @@ "cloud/verification/direct/image-provenance" ] }, - { - "group": "NEAR AI Inference SDK", - "pages": [ - "cloud/verification/sdk/cloud-api", - "cloud/verification/sdk/direct" - ] - }, { "group": "Reference", "pages": [ @@ -537,14 +530,6 @@ { "source": "/cloud/verification/provenance", "destination": "/cloud/verification" - }, - { - "source": "/cloud/verification/reference/sdk", - "destination": "/cloud/verification/sdk/cloud-api" - }, - { - "source": "/cloud/verification/sdk", - "destination": "/cloud/verification/sdk/cloud-api" } ] } From b9f5eb7d1b376342fcbe58d03dce8d6282daf56f Mon Sep 17 00:00:00 2001 From: Coffee Date: Wed, 23 Sep 2026 13:18:21 +0800 Subject: [PATCH 32/36] Revert "docs: sync OpenAPI from staging" This reverts commit 965d0f5b081d8ccb9e9455443dcf116d358d4b68. --- api-reference/openapi.json | 13494 +++++++++++++---------------------- docs.json | 39 +- 2 files changed, 5087 insertions(+), 8446 deletions(-) diff --git a/api-reference/openapi.json b/api-reference/openapi.json index 42c9519..1361579 100644 --- a/api-reference/openapi.json +++ b/api-reference/openapi.json @@ -211,18 +211,28 @@ ] } }, - "/v1/admin/aml/allowlist": { + "/v1/admin/feature-requests": { "get": { "tags": [ - "Admin" + "Feature Requests" ], - "summary": "List AML allowlist entries (Admin only)", - "operationId": "list_aml_allowlist", + "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", - "description": "Maximum number of allowlist entries to return (default: 100)", "required": false, "schema": { "type": "integer", @@ -232,7 +242,6 @@ { "name": "offset", "in": "query", - "description": "Number of allowlist entries to skip (default: 0)", "required": false, "schema": { "type": "integer", @@ -242,17 +251,17 @@ ], "responses": { "200": { - "description": "AML allowlist entries retrieved successfully", + "description": "Aggregated feature requests", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListAdminAmlAllowlistResponse" + "$ref": "#/components/schemas/AdminFeatureRequestListResponse" } } } }, "400": { - "description": "Invalid request", + "description": "Invalid parameters", "content": { "application/json": { "schema": { @@ -271,16 +280,6 @@ } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -297,30 +296,102 @@ "session_token": [] } ] - }, - "post": { + } + }, + "/v1/admin/invitation-email-deliveries": { + "get": { "tags": [ "Admin" ], - "summary": "Add or update an AML allowlist entry (Admin only)", - "operationId": "upsert_aml_allowlist_entry", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpsertAmlAllowlistEntryRequest" - } + "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" } }, - "required": true - }, + { + "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" + } + }, + { + "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": "AML allowlist entry upserted successfully", + "description": "Invitation email deliveries retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminAmlAllowlistEntryResponse" + "$ref": "#/components/schemas/ListAdminInvitationEmailDeliveriesResponse" } } } @@ -345,16 +416,6 @@ } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -373,40 +434,38 @@ ] } }, - "/v1/admin/aml/allowlist/{account_id}": { - "delete": { + "/v1/admin/invitation-email-deliveries/{invitation_id}/resend": { + "post": { "tags": [ "Admin" ], - "summary": "Remove an AML allowlist entry (Admin only)", - "operationId": "delete_aml_allowlist_entry", + "summary": "Resend a single organization invitation email (Admin only)", + "operationId": "resend_invitation_email", "parameters": [ { - "name": "account_id", + "name": "invitation_id", "in": "path", - "description": "NEAR account ID", + "description": "Invitation ID", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], "responses": { - "204": { - "description": "AML allowlist entry removed successfully" - }, - "400": { - "description": "Invalid request", + "200": { + "description": "Invitation email resend attempted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/AdminInvitationEmailResendResultResponse" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Invitation is not pending or has expired", "content": { "application/json": { "schema": { @@ -415,8 +474,8 @@ } } }, - "403": { - "description": "Forbidden", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -426,7 +485,7 @@ } }, "404": { - "description": "AML allowlist entry not found", + "description": "Invitation not found", "content": { "application/json": { "schema": { @@ -453,18 +512,19 @@ ] } }, - "/v1/admin/aml/reports": { + "/v1/admin/models": { "get": { "tags": [ "Admin" ], - "summary": "List AML reports (Admin only)", - "operationId": "list_aml_reports", + "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 reports to return (default: 100)", + "description": "Maximum number of models to return (default: 100)", "required": false, "schema": { "type": "integer", @@ -474,31 +534,30 @@ { "name": "offset", "in": "query", - "description": "Number of reports to skip (default: 0)", + "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": "AML reports retrieved successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ListAdminAmlReportsResponse" - } - } - } - }, - "400": { - "description": "Invalid request", + "description": "Models retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/AdminModelListResponse" } } } @@ -513,16 +572,6 @@ } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -539,31 +588,19 @@ "session_token": [] } ] - } - }, - "/v1/admin/aml/reports/{report_id}/status": { + }, "patch": { "tags": [ "Admin" ], - "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" - } - } - ], + "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/UpdateAmlReportStatusRequest" + "$ref": "#/components/schemas/HashMap" } } }, @@ -571,11 +608,14 @@ }, "responses": { "200": { - "description": "AML report status updated successfully", + "description": "Models upserted successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminAmlReportResponse" + "type": "array", + "items": { + "$ref": "#/components/schemas/ModelWithPricing" + } } } } @@ -600,26 +640,6 @@ } } }, - "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": { @@ -638,57 +658,37 @@ ] } }, - "/v1/admin/feature-requests": { - "get": { + "/v1/admin/models/deprecate": { + "post": { "tags": [ - "Feature Requests" + "Admin" ], - "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" + "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" + } } }, - { - "name": "offset", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], + "required": true + }, "responses": { "200": { - "description": "Aggregated feature requests", + "description": "Model deprecated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminFeatureRequestListResponse" + "$ref": "#/components/schemas/DeprecateModelResponse" } } } }, "400": { - "description": "Invalid parameters", + "description": "Invalid request (e.g. self-deprecation, empty model id)", "content": { "application/json": { "schema": { @@ -707,6 +707,16 @@ } } }, + "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": { @@ -725,29 +735,27 @@ ] } }, - "/v1/admin/invitation-email-deliveries": { + "/v1/admin/models/pricing-changes": { "get": { "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", + "summary": "List scheduled pricing changes (Admin only).", + "operationId": "list_model_pricing_changes", "parameters": [ { - "name": "limit", + "name": "status", "in": "query", - "description": "Maximum number of deliveries to return (default: 100)", + "description": "Filter by status: pending, applying, applied, cancelled, failed. Omit for all.", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } }, { - "name": "offset", + "name": "limit", "in": "query", - "description": "Number of deliveries to skip (default: 0)", + "description": "Maximum number of changes to return (default: 100)", "required": false, "schema": { "type": "integer", @@ -755,70 +763,23 @@ } }, { - "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" - } - }, - { - "name": "created_before", + "name": "offset", "in": "query", - "description": "Only invitations created at or before this timestamp", + "description": "Number of changes to skip (default: 0)", "required": false, "schema": { - "type": "string", - "format": "date-time" + "type": "integer", + "format": "int64" } } ], "responses": { "200": { - "description": "Invitation email deliveries retrieved successfully", + "description": "Scheduled pricing changes", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListAdminInvitationEmailDeliveriesResponse" + "$ref": "#/components/schemas/ListPricingChangesResponse" } } } @@ -861,38 +822,37 @@ ] } }, - "/v1/admin/invitation-email-deliveries/{invitation_id}/resend": { + "/v1/admin/models/pricing-changes/confirm": { "post": { "tags": [ "Admin" ], - "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" + "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" + } } - } - ], + }, + "required": true + }, "responses": { "200": { - "description": "Invitation email resend attempted", + "description": "Pricing changes scheduled and notifications attempted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminInvitationEmailResendResultResponse" + "$ref": "#/components/schemas/PricingChangeConfirmResponse" } } } }, "400": { - "description": "Invitation is not pending or has expired", + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -912,17 +872,7 @@ } }, "404": { - "description": "Invitation not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal server error", + "description": "Model not found or inactive", "content": { "application/json": { "schema": { @@ -930,67 +880,9 @@ } } } - } - }, - "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", + "409": { + "description": "A pending pricing change already exists for a model", "content": { "application/json": { "schema": { @@ -1015,19 +907,20 @@ "session_token": [] } ] - }, - "patch": { + } + }, + "/v1/admin/models/pricing-changes/preview": { + "post": { "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": "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/HashMap" + "$ref": "#/components/schemas/PricingChangeBatchRequest" } } }, @@ -1035,14 +928,11 @@ }, "responses": { "200": { - "description": "Models upserted successfully", + "description": "Pricing change notification preview", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ModelWithPricing" - } + "$ref": "#/components/schemas/PricingChangePreviewResponse" } } } @@ -1067,8 +957,8 @@ } } }, - "500": { - "description": "Internal server error", + "404": { + "description": "Model not found or inactive", "content": { "application/json": { "schema": { @@ -1076,7 +966,17 @@ } } } - } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } }, "security": [ { @@ -1085,41 +985,32 @@ ] } }, - "/v1/admin/models/deprecate": { - "post": { + "/v1/admin/models/pricing-changes/{id}": { + "delete": { "tags": [ "Admin" ], - "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" - } + "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" } - }, - "required": true - }, + } + ], "responses": { "200": { - "description": "Model deprecated successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeprecateModelResponse" - } - } - } - }, - "400": { - "description": "Invalid request (e.g. self-deprecation, empty model id)", + "description": "Pricing change cancelled", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ScheduledPricingChangeDto" } } } @@ -1135,7 +1026,7 @@ } }, "404": { - "description": "Either model not found, or successor is not active", + "description": "Pricing change not found or no longer pending", "content": { "application/json": { "schema": { @@ -1162,57 +1053,41 @@ ] } }, - "/v1/admin/models/pricing-changes": { - "get": { + "/v1/admin/models/{model_name}": { + "delete": { "tags": [ "Admin" ], - "summary": "List scheduled pricing changes (Admin only).", - "operationId": "list_model_pricing_changes", + "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": "status", - "in": "query", - "description": "Filter by status: pending, applying, applied, cancelled, failed. Omit for all.", - "required": false, + "name": "model_name", + "in": "path", + "description": "Model name to delete (URL-encode if it contains slashes)", + "required": true, "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" - } } ], - "responses": { - "200": { - "description": "Scheduled pricing changes", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ListPricingChangesResponse" - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteModelRequest" } } }, - "400": { - "description": "Invalid request", + "required": true + }, + "responses": { + "204": { + "description": "Model deleted successfully" + }, + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -1221,8 +1096,8 @@ } } }, - "401": { - "description": "Unauthorized", + "404": { + "description": "Model not found", "content": { "application/json": { "schema": { @@ -1249,19 +1124,28 @@ ] } }, - "/v1/admin/models/pricing-changes/confirm": { + "/v1/admin/models/{model_name}/deprecation/confirm": { "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", + "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" + } + } + ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PricingChangeBatchRequest" + "$ref": "#/components/schemas/ModelDeprecationRequest" } } }, @@ -1269,11 +1153,11 @@ }, "responses": { "200": { - "description": "Pricing changes scheduled and notifications attempted", + "description": "Deprecation confirmed and notifications attempted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PricingChangeConfirmResponse" + "$ref": "#/components/schemas/ModelDeprecationConfirmResponse" } } } @@ -1299,17 +1183,7 @@ } }, "404": { - "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", + "description": "Model or successor not found", "content": { "application/json": { "schema": { @@ -1336,18 +1210,28 @@ ] } }, - "/v1/admin/models/pricing-changes/preview": { + "/v1/admin/models/{model_name}/deprecation/preview": { "post": { "tags": [ "Admin" ], - "summary": "Preview a batch of scheduled pricing changes without mutating state (Admin only).", - "operationId": "preview_model_pricing_changes", + "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" + } + } + ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PricingChangeBatchRequest" + "$ref": "#/components/schemas/ModelDeprecationRequest" } } }, @@ -1355,11 +1239,11 @@ }, "responses": { "200": { - "description": "Pricing change notification preview", + "description": "Deprecation notification preview", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PricingChangePreviewResponse" + "$ref": "#/components/schemas/ModelDeprecationPreviewResponse" } } } @@ -1385,7 +1269,7 @@ } }, "404": { - "description": "Model not found or inactive", + "description": "Model or successor not found", "content": { "application/json": { "schema": { @@ -1412,32 +1296,52 @@ ] } }, - "/v1/admin/models/pricing-changes/{id}": { - "delete": { + "/v1/admin/models/{model_name}/history": { + "get": { "tags": [ "Admin" ], - "summary": "Cancel a pending scheduled pricing change (Admin only).", - "operationId": "cancel_model_pricing_change", + "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": "id", + "name": "model_name", "in": "path", - "description": "Scheduled pricing change ID", + "description": "Model name to get complete history for (URL-encode if it contains slashes)", "required": true, "schema": { - "type": "string", - "format": "uuid" + "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" } } ], "responses": { "200": { - "description": "Pricing change cancelled", + "description": "Model history retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ScheduledPricingChangeDto" + "$ref": "#/components/schemas/ModelHistoryResponse" } } } @@ -1453,7 +1357,7 @@ } }, "404": { - "description": "Pricing change not found or no longer pending", + "description": "Model not found", "content": { "application/json": { "schema": { @@ -1480,51 +1384,49 @@ ] } }, - "/v1/admin/models/{model_name}": { - "delete": { + "/v1/admin/organizations/{org_id}": { + "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": "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": "model_name", + "name": "org_id", "in": "path", - "description": "Model name to delete (URL-encode if it contains slashes)", + "description": "Organization ID", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeleteModelRequest" - } - } - }, - "required": true - }, "responses": { - "204": { - "description": "Model deleted successfully" - }, - "401": { - "description": "Unauthorized", + "200": { + "description": "Organization retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/AdminOrganizationResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { - "description": "Model not found", + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -1551,17 +1453,85 @@ ] } }, - "/v1/admin/models/{model_name}/deprecation/confirm": { - "post": { + "/v1/admin/organizations/{org_id}/concurrent-limit": { + "get": { "tags": [ "Admin" ], - "summary": "Confirm a planned model deprecation, update the catalog, and notify affected admins.", - "operationId": "confirm_model_deprecation", + "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": "model_name", + "name": "org_id", + "in": "path", + "description": "The organization's ID (as a UUID)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Concurrent limit retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrganizationConcurrentLimitResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Organization 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": [ + "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", + "parameters": [ + { + "name": "org_id", "in": "path", + "description": "The organization's ID (as a UUID)", "required": true, "schema": { "type": "string" @@ -1572,7 +1542,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelDeprecationRequest" + "$ref": "#/components/schemas/UpdateOrganizationConcurrentLimitRequest" } } }, @@ -1580,11 +1550,11 @@ }, "responses": { "200": { - "description": "Deprecation confirmed and notifications attempted", + "description": "Concurrent limit updated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelDeprecationConfirmResponse" + "$ref": "#/components/schemas/UpdateOrganizationConcurrentLimitResponse" } } } @@ -1610,7 +1580,7 @@ } }, "404": { - "description": "Model or successor not found", + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -1637,17 +1607,19 @@ ] } }, - "/v1/admin/models/{model_name}/deprecation/preview": { - "post": { + "/v1/admin/organizations/{org_id}/limits": { + "patch": { "tags": [ "Admin" ], - "summary": "Preview affected admins for a planned model deprecation (Admin only).", - "operationId": "preview_model_deprecation", + "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": "model_name", + "name": "org_id", "in": "path", + "description": "Organization ID to update limits for", "required": true, "schema": { "type": "string" @@ -1658,7 +1630,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelDeprecationRequest" + "$ref": "#/components/schemas/UpdateOrganizationLimitsRequest" } } }, @@ -1666,11 +1638,11 @@ }, "responses": { "200": { - "description": "Deprecation notification preview", + "description": "Organization limits updated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelDeprecationPreviewResponse" + "$ref": "#/components/schemas/UpdateOrganizationLimitsResponse" } } } @@ -1696,7 +1668,7 @@ } }, "404": { - "description": "Model or successor not found", + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -1723,19 +1695,19 @@ ] } }, - "/v1/admin/models/{model_name}/history": { + "/v1/admin/organizations/{org_id}/limits/history": { "get": { "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": "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": "model_name", + "name": "org_id", "in": "path", - "description": "Model name to get complete history for (URL-encode if it contains slashes)", + "description": "The organization's ID (as a UUID)", "required": true, "schema": { "type": "string" @@ -1744,7 +1716,7 @@ { "name": "limit", "in": "query", - "description": "Maximum number of history entries to return (default: 50)", + "description": "Maximum number of history records to return (default: 50)", "required": false, "schema": { "type": "integer", @@ -1754,7 +1726,7 @@ { "name": "offset", "in": "query", - "description": "Number of history entries to skip (default: 0)", + "description": "Number of records to skip (default: 0)", "required": false, "schema": { "type": "integer", @@ -1764,11 +1736,11 @@ ], "responses": { "200": { - "description": "Model history retrieved successfully", + "description": "Limits history retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelHistoryResponse" + "$ref": "#/components/schemas/OrgLimitsHistoryResponse" } } } @@ -1784,7 +1756,7 @@ } }, "404": { - "description": "Model not found", + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -1811,14 +1783,14 @@ ] } }, - "/v1/admin/organizations/{org_id}": { + "/v1/admin/organizations/{org_id}/members": { "get": { "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": "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", @@ -1829,15 +1801,45 @@ "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": "Organization retrieved successfully", + "description": "Organization members retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminOrganizationResponse" + "$ref": "#/components/schemas/ListAdminOrganizationMembersResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -1880,32 +1882,53 @@ ] } }, - "/v1/admin/organizations/{org_id}/concurrent-limit": { + "/v1/admin/organizations/{org_id}/metrics": { "get": { "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": "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": "The organization's ID (as a UUID)", + "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": "Concurrent limit retrieved successfully", + "description": "Organization metrics retrieved successfully" + }, + "400": { + "description": "Invalid request", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetOrganizationConcurrentLimitResponse" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -1946,45 +1969,57 @@ "session_token": [] } ] - }, - "patch": { + } + }, + "/v1/admin/organizations/{org_id}/metrics/timeseries": { + "get": { "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": "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": "The organization's ID (as a UUID)", + "description": "Organization ID to get metrics for", "required": true, "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrganizationConcurrentLimitRequest" - } + }, + { + "name": "start", + "in": "query", + "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", + "required": false, + "schema": { + "type": "string" } }, - "required": true - }, - "responses": { - "200": { - "description": "Concurrent limit updated successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrganizationConcurrentLimitResponse" - } - } + { + "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" } + } + ], + "responses": { + "200": { + "description": "Time series metrics retrieved successfully" }, "400": { "description": "Invalid request", @@ -2034,13 +2069,14 @@ ] } }, - "/v1/admin/organizations/{org_id}/fallback": { + "/v1/admin/organizations/{org_id}/staking/farm": { "get": { "tags": [ "Admin" ], - "summary": "Get an organization's effective fallback policy (Admin only).", - "operationId": "get_organization_fallback", + "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": "org_id", @@ -2048,24 +2084,23 @@ "description": "Organization ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], "responses": { "200": { - "description": "Fallback policy retrieved", + "description": "Organization staking farm state", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationFallbackResponse" + "$ref": "#/components/schemas/StakingFarmStateResponse" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Invalid organization ID", "content": { "application/json": { "schema": { @@ -2074,8 +2109,8 @@ } } }, - "404": { - "description": "Organization not found", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -2083,55 +2118,19 @@ } } } - } - }, - "security": [ - { - "session_token": [] - } - ] - }, - "patch": { - "tags": [ - "Admin" - ], - "summary": "Update an organization's fallback policy (Admin only).", - "operationId": "update_organization_fallback", - "parameters": [ - { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrganizationFallbackRequest" - } - } }, - "required": true - }, - "responses": { - "200": { - "description": "Fallback policy updated", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationFallbackResponse" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "401": { - "description": "Unauthorized", + "404": { + "description": "No staking farm source found", "content": { "application/json": { "schema": { @@ -2140,8 +2139,8 @@ } } }, - "404": { - "description": "Organization not found", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -2158,48 +2157,38 @@ ] } }, - "/v1/admin/organizations/{org_id}/limits": { - "patch": { + "/v1/admin/organizations/{org_id}/staking/farm/sync": { + "post": { "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": "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": "org_id", "in": "path", - "description": "Organization ID to update limits for", + "description": "Organization ID", "required": true, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrganizationLimitsRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Organization limits updated successfully", + "description": "Synced organization staking farm state", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateOrganizationLimitsResponse" + "$ref": "#/components/schemas/StakingFarmStateResponse" } } } }, "400": { - "description": "Invalid request", + "description": "Invalid organization ID", "content": { "application/json": { "schema": { @@ -2218,8 +2207,18 @@ } } }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "404": { - "description": "Organization not found", + "description": "No staking farm source found", "content": { "application/json": { "schema": { @@ -2246,52 +2245,42 @@ ] } }, - "/v1/admin/organizations/{org_id}/limits/history": { + "/v1/admin/organizations/{org_id}/usage/balance": { "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 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": "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" - } } ], "responses": { "200": { - "description": "Limits history retrieved successfully", + "description": "Organization balance", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrgLimitsHistoryResponse" + "$ref": "#/components/schemas/OrganizationBalanceResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -2307,7 +2296,7 @@ } }, "404": { - "description": "Organization not found", + "description": "Not found", "content": { "application/json": { "schema": { @@ -2334,59 +2323,27 @@ ] } }, - "/v1/admin/organizations/{org_id}/members": { + "/v1/admin/platform/billing-summary": { "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", - "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" - } - } - ], + "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": "Organization members retrieved successfully", + "description": "Billing summary retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListAdminOrganizationMembersResponse" + "$ref": "#/components/schemas/BillingSummary" } } } }, - "400": { - "description": "Invalid request", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -2395,8 +2352,8 @@ } } }, - "401": { - "description": "Unauthorized", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -2404,9 +2361,36 @@ } } } + } + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/admin/platform/infra-summary": { + "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", + "responses": { + "200": { + "description": "Infra summary retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InfraSummary" + } + } + } }, - "404": { - "description": "Organization not found", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -2433,24 +2417,15 @@ ] } }, - "/v1/admin/organizations/{org_id}/metrics": { + "/v1/admin/platform/metrics": { "get": { "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": "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": "org_id", - "in": "path", - "description": "Organization ID to get metrics for", - "required": true, - "schema": { - "type": "string" - } - }, { "name": "start", "in": "query", @@ -2472,14 +2447,11 @@ ], "responses": { "200": { - "description": "Organization metrics retrieved successfully" - }, - "400": { - "description": "Invalid request", + "description": "Platform metrics retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/PlatformMetrics" } } } @@ -2494,16 +2466,6 @@ } } }, - "404": { - "description": "Organization not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -2522,24 +2484,15 @@ ] } }, - "/v1/admin/organizations/{org_id}/metrics/timeseries": { + "/v1/admin/platform/metrics/timeseries": { "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 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": "org_id", - "in": "path", - "description": "Organization ID to get metrics for", - "required": true, - "schema": { - "type": "string" - } - }, { "name": "start", "in": "query", @@ -2561,7 +2514,7 @@ { "name": "granularity", "in": "query", - "description": "Time granularity: hour, day (default), or week", + "description": "Time granularity: hour, day (default), week, or month", "required": false, "schema": { "type": "string" @@ -2570,20 +2523,17 @@ ], "responses": { "200": { - "description": "Time series metrics retrieved successfully" - }, - "400": { - "description": "Invalid request", + "description": "Platform time series retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/PlatformTimeSeriesMetrics" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -2592,8 +2542,8 @@ } } }, - "404": { - "description": "Organization not found", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -2620,20 +2570,76 @@ ] } }, - "/v1/admin/organizations/{org_id}/staking/farm": { + "/v1/admin/platform/model-revenue": { "get": { "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": "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": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, + "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, "schema": { "type": "string" } @@ -2641,17 +2647,17 @@ ], "responses": { "200": { - "description": "Organization staking farm state", + "description": "Model revenue retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StakingFarmStateResponse" + "$ref": "#/components/schemas/ModelRevenueReport" } } } }, "400": { - "description": "Invalid organization ID", + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -2670,26 +2676,6 @@ } } }, - "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": { @@ -2708,20 +2694,67 @@ ] } }, - "/v1/admin/organizations/{org_id}/staking/farm/sync": { - "post": { + "/v1/admin/platform/org-revenue": { + "get": { "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": "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": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, + "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, "schema": { "type": "string" } @@ -2729,17 +2762,17 @@ ], "responses": { "200": { - "description": "Synced organization staking farm state", + "description": "Org revenue retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StakingFarmStateResponse" + "$ref": "#/components/schemas/OrgRevenueReport" } } } }, "400": { - "description": "Invalid organization ID", + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -2758,26 +2791,6 @@ } } }, - "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": { @@ -2796,32 +2809,30 @@ ] } }, - "/v1/admin/organizations/{org_id}/usage/balance": { - "get": { + "/v1/admin/services": { + "post": { "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", - "parameters": [ - { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, - "schema": { - "type": "string" + "summary": "Create platform service (Admin only)", + "operationId": "create_service", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateServiceRequest" + } } - } - ], + }, + "required": true + }, "responses": { "200": { - "description": "Organization balance", + "description": "Service created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationBalanceResponse" + "$ref": "#/components/schemas/AdminServiceResponse" } } } @@ -2846,16 +2857,6 @@ } } }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -2874,27 +2875,48 @@ ] } }, - "/v1/admin/platform/billing-summary": { - "get": { + "/v1/admin/services/{id}": { + "patch": { "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": "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 + }, "responses": { "200": { - "description": "Billing summary retrieved successfully", + "description": "Service updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BillingSummary" + "$ref": "#/components/schemas/AdminServiceResponse" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -2903,8 +2925,8 @@ } } }, - "500": { - "description": "Internal server error", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -2912,36 +2934,9 @@ } } } - } - }, - "security": [ - { - "session_token": [] - } - ] - } - }, - "/v1/admin/platform/infra-summary": { - "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", - "responses": { - "200": { - "description": "Infra summary retrieved successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InfraSummary" - } - } - } }, - "401": { - "description": "Unauthorized", + "404": { + "description": "Not found", "content": { "application/json": { "schema": { @@ -2968,104 +2963,66 @@ ] } }, - "/v1/admin/platform/metrics": { + "/v1/admin/users": { "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": "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": "start", + "name": "limit", "in": "query", - "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", + "description": "Maximum number of users to return (default: 100)", "required": false, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } }, { - "name": "end", + "name": "offset", "in": "query", - "description": "End of time range (ISO 8601). Defaults to now.", + "description": "Number of users to skip (default: 0)", "required": false, "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Platform metrics retrieved successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PlatformMetrics" - } - } + "type": "integer", + "format": "int64" } }, - "401": { - "description": "Unauthorized", - "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" } }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "session_token": [] - } - ] - } - }, - "/v1/admin/platform/metrics/timeseries": { - "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", - "parameters": [ { - "name": "start", + "name": "search", "in": "query", - "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", + "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" } }, { - "name": "end", + "name": "is_active", "in": "query", - "description": "End of time range (ISO 8601). Defaults to now.", + "description": "Filter users by active status. Omit to include active and inactive users.", "required": false, "schema": { - "type": "string" + "type": "boolean" } }, { - "name": "granularity", + "name": "search_by_name", "in": "query", - "description": "Time granularity: hour, day (default), week, or month", + "description": "Filter users by organization name (case-insensitive match). Only effective when include_organizations=true; separate from user search.", "required": false, "schema": { "type": "string" @@ -3074,21 +3031,11 @@ ], "responses": { "200": { - "description": "Platform time series retrieved successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PlatformTimeSeriesMetrics" - } - } - } - }, - "400": { - "description": "Invalid request", + "description": "Users retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ListUsersResponse" } } } @@ -3121,94 +3068,117 @@ ] } }, - "/v1/admin/platform/model-revenue": { + "/v1/attestation/ita-token": { "get": { "tags": [ - "Admin" + "Attestation" ], - "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": "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": "start", + "name": "model", "in": "query", - "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", "required": false, "schema": { - "type": "string" + "type": [ + "string", + "null" + ] } }, { - "name": "end", + "name": "nonce", "in": "query", - "description": "End of time range (ISO 8601). Defaults to now.", "required": false, "schema": { - "type": "string" + "type": [ + "string", + "null" + ] } }, { - "name": "limit", + "name": "signing_algo", "in": "query", - "description": "Page size (1-1000, default 100)", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": [ + "string", + "null" + ] } }, { - "name": "offset", + "name": "signing_address", "in": "query", - "description": "Page offset (default 0)", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": [ + "string", + "null" + ] } }, { - "name": "verifiable", + "name": "include_tls_fingerprint", "in": "query", - "description": "Filter to verifiable (true) or non-verifiable (false) models", "required": false, "schema": { - "type": "boolean" + "type": [ + "string", + "null" + ] } }, { - "name": "provider_type", + "name": "policy_ids", "in": "query", - "description": "Filter by provider type (e.g. vllm, external, chutes)", "required": false, "schema": { - "type": "string" + "type": [ + "string", + "null" + ] } }, { - "name": "sort", + "name": "policy_must_match", "in": "query", - "description": "Sort: revenue (default), requests, tokens", "required": false, "schema": { - "type": "string" + "type": [ + "string", + "null" + ] + } + }, + { + "name": "token_signing_alg", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] } } ], "responses": { "200": { - "description": "Model revenue retrieved successfully", + "description": "ITA attestation token retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelRevenueReport" + "$ref": "#/components/schemas/ItaTokenResponse" } } } }, "400": { - "description": "Invalid request", + "description": "Invalid parameters", "content": { "application/json": { "schema": { @@ -3217,8 +3187,8 @@ } } }, - "401": { - "description": "Unauthorized", + "429": { + "description": "ITA rate limit exceeded", "content": { "application/json": { "schema": { @@ -3227,8 +3197,28 @@ } } }, - "500": { - "description": "Internal server error", + "502": { + "description": "Bad ITA upstream response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "ITA attestation unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "504": { + "description": "ITA request timed out", "content": { "application/json": { "schema": { @@ -3239,91 +3229,101 @@ } }, "security": [ - { - "session_token": [] - } + {} ] } }, - "/v1/admin/platform/org-revenue": { + "/v1/attestation/report": { "get": { "tags": [ - "Admin" + "Attestation" ], - "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 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": "start", + "name": "model", "in": "query", - "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", "required": false, "schema": { - "type": "string" + "type": [ + "string", + "null" + ] } }, { - "name": "end", + "name": "signing_algo", "in": "query", - "description": "End of time range (ISO 8601). Defaults to now.", "required": false, "schema": { - "type": "string" + "type": [ + "string", + "null" + ] } }, { - "name": "limit", + "name": "nonce", "in": "query", - "description": "Page size (1-1000, default 100)", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": [ + "string", + "null" + ] } }, { - "name": "offset", + "name": "signing_address", "in": "query", - "description": "Page offset (default 0)", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": [ + "string", + "null" + ] } }, { - "name": "paying", + "name": "include_tls_fingerprint", "in": "query", - "description": "Filter to current paying (true) / non-paying (false) orgs", + "description": "Include TLS certificate fingerprint in the report data.\nDefaults to false; when true, report_data[..32] = SHA256(signing_address || cert_fingerprint).", "required": false, "schema": { - "type": "boolean" + "type": [ + "boolean", + "null" + ] } }, { - "name": "sort", + "name": "provider", "in": "query", - "description": "Sort: revenue (default), requests, tokens", + "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" + "type": [ + "string", + "null" + ] } } ], "responses": { "200": { - "description": "Org revenue retrieved successfully", + "description": "Attestation report retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrgRevenueReport" + "$ref": "#/components/schemas/AttestationResponse" } } } }, "400": { - "description": "Invalid request", + "description": "Invalid nonce format", "content": { "application/json": { "schema": { @@ -3333,7 +3333,7 @@ } }, "401": { - "description": "Unauthorized", + "description": "Missing or invalid API key", "content": { "application/json": { "schema": { @@ -3342,8 +3342,8 @@ } } }, - "500": { - "description": "Internal server error", + "503": { + "description": "Service unavailable", "content": { "application/json": { "schema": { @@ -3355,23 +3355,24 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/admin/services": { + "/v1/audio/transcriptions": { "post": { "tags": [ - "Admin" + "Audio" ], - "summary": "Create platform service (Admin only)", - "operationId": "create_service", + "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": { - "application/json": { + "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/CreateServiceRequest" + "$ref": "#/components/schemas/AudioTranscriptionRequestSchema" } } }, @@ -3379,17 +3380,22 @@ }, "responses": { "200": { - "description": "Service created", + "description": "Successful transcription", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminServiceResponse" + "$ref": "#/components/schemas/AudioTranscriptionResponse" + } + }, + "text/plain": { + "schema": { + "type": "string" } } } }, "400": { - "description": "Invalid request", + "description": "Invalid request (empty file, unsupported format, file too large)", "content": { "application/json": { "schema": { @@ -3399,7 +3405,27 @@ } }, "401": { - "description": "Unauthorized", + "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.", "content": { "application/json": { "schema": { @@ -3409,7 +3435,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -3421,35 +3447,24 @@ }, "security": [ { - "session_token": [] + "ApiKeyAuth": [] } ] } }, - "/v1/admin/services/{id}": { - "patch": { + "/v1/billing/costs": { + "post": { "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" - } - } + "Billing" ], + "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/UpdateServiceRequest" + "$ref": "#/components/schemas/BillingCostsRequest" } } }, @@ -3457,11 +3472,11 @@ }, "responses": { "200": { - "description": "Service updated", + "description": "Costs retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminServiceResponse" + "$ref": "#/components/schemas/BillingCostsResponse" } } } @@ -3486,16 +3501,6 @@ } } }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -3509,90 +3514,72 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/admin/users": { - "get": { + "/v1/chat/completions": { + "post": { "tags": [ - "Admin" + "Chat" ], - "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" - } - }, - { - "name": "offset", - "in": "query", - "description": "Number of users to skip (default: 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" + "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" + } } }, - { - "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" + "required": true + }, + "responses": { + "200": { + "description": "Completion generated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChatCompletionResponse" + } + } } }, - { - "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" + "400": { + "description": "Invalid request parameters", + "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" + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "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" - } - } - ], - "responses": { - "200": { - "description": "Users retrieved successfully", + "402": { + "description": "Insufficient credits", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListUsersResponse" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "401": { - "description": "Unauthorized", + "429": { + "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", "content": { "application/json": { "schema": { @@ -3602,7 +3589,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -3614,132 +3601,32 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/attestation/ita-token": { - "get": { + "/v1/check_api_key": { + "post": { "tags": [ - "Attestation" - ], - "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" - ] - } - }, - { - "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" - ] - } - } + "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": "ITA attestation token retrieved", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ItaTokenResponse" - } - } - } - }, - "400": { - "description": "Invalid parameters", + "description": "API key is valid and has sufficient credits", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/CheckApiKeyResponse" } } } }, - "429": { - "description": "ITA rate limit exceeded", + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -3748,8 +3635,8 @@ } } }, - "502": { - "description": "Bad ITA upstream response", + "402": { + "description": "Insufficient credits or spend limit exceeded", "content": { "application/json": { "schema": { @@ -3758,8 +3645,8 @@ } } }, - "503": { - "description": "ITA attestation unavailable", + "429": { + "description": "Rate limit exceeded", "content": { "application/json": { "schema": { @@ -3768,8 +3655,8 @@ } } }, - "504": { - "description": "ITA request timed out", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -3780,101 +3667,43 @@ } }, "security": [ - {} + { + "api_key": [] + } ] } }, - "/v1/attestation/report": { - "get": { + "/v1/completions": { + "post": { "tags": [ - "Attestation" + "Chat" ], - "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" - ] - } - }, - { - "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" - ] + "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" + } } }, - { - "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" - ] - } - } - ], + "required": true + }, "responses": { "200": { - "description": "Attestation report retrieved", + "description": "Completion generated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AttestationResponse" + "$ref": "#/components/schemas/CompletionResponse" } } } }, "400": { - "description": "Invalid nonce format", + "description": "Invalid request parameters", "content": { "application/json": { "schema": { @@ -3884,7 +3713,7 @@ } }, "401": { - "description": "Missing or invalid API key", + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -3893,8 +3722,18 @@ } } }, - "503": { - "description": "Service unavailable", + "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": { @@ -3911,19 +3750,18 @@ ] } }, - "/v1/audio/transcriptions": { + "/v1/feature-requests": { "post": { "tags": [ - "Audio" + "Feature Requests" ], - "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": "Submit or update the current user's interest in a feature request target.", + "operationId": "submit_feature_request", "requestBody": { "content": { - "multipart/form-data": { + "application/json": { "schema": { - "$ref": "#/components/schemas/AudioTranscriptionRequestSchema" + "$ref": "#/components/schemas/SubmitFeatureRequest" } } }, @@ -3931,22 +3769,17 @@ }, "responses": { "200": { - "description": "Successful transcription", + "description": "Feature request recorded", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AudioTranscriptionResponse" - } - }, - "text/plain": { - "schema": { - "type": "string" + "$ref": "#/components/schemas/SubmitFeatureRequestResponse" } } } }, "400": { - "description": "Invalid request (empty file, unsupported format, file too large)", + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -3956,17 +3789,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": { @@ -3975,8 +3798,8 @@ } } }, - "429": { - "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "403": { + "description": "Organization membership required", "content": { "application/json": { "schema": { @@ -3986,7 +3809,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -3998,24 +3821,46 @@ }, "security": [ { - "ApiKeyAuth": [] + "session_token": [] } ] } }, - "/v1/billing/costs": { + "/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": { "post": { "tags": [ - "Billing" + "Images" ], - "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", + "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/BillingCostsRequest" + "$ref": "#/components/schemas/ImageEditRequestSchema" } } }, @@ -4023,17 +3868,17 @@ }, "responses": { "200": { - "description": "Costs retrieved successfully", + "description": "Image edited successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BillingCostsResponse" + "$ref": "#/components/schemas/ImageGenerationResponse" } } } }, "400": { - "description": "Invalid request", + "description": "Invalid request parameters", "content": { "application/json": { "schema": { @@ -4043,7 +3888,37 @@ } }, "401": { - "description": "Unauthorized", + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "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": { @@ -4053,7 +3928,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -4070,19 +3945,19 @@ ] } }, - "/v1/chat/completions": { + "/v1/images/generations": { "post": { "tags": [ - "Chat" + "Images" ], - "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", + "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/ChatCompletionRequest" + "$ref": "#/components/schemas/ImageGenerationRequest" } } }, @@ -4090,11 +3965,11 @@ }, "responses": { "200": { - "description": "Completion generated successfully", + "description": "Image generated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ChatCompletionResponse" + "$ref": "#/components/schemas/ImageGenerationResponse" } } } @@ -4119,16 +3994,6 @@ } } }, - "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": { @@ -4157,37 +4022,38 @@ ] } }, - "/v1/check_api_key": { - "post": { + "/v1/invitations/{token}": { + "get": { "tags": [ - "Gateway" + "Invitations" ], - "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", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CheckApiKeyResponse" - } - } - } - }, - "401": { - "description": "Invalid or missing API key", + "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" + } + } + ], + "responses": { + "200": { + "description": "Invitation details", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/OrganizationInvitationResponse" } } } }, - "402": { - "description": "Insufficient credits or spend limit exceeded", + "404": { + "description": "Invitation not found", "content": { "application/json": { "schema": { @@ -4196,8 +4062,8 @@ } } }, - "429": { - "description": "Rate limit exceeded", + "410": { + "description": "Invitation expired or no longer pending", "content": { "application/json": { "schema": { @@ -4216,45 +4082,41 @@ } } } - }, - "security": [ - { - "api_key": [] - } - ] + } } }, - "/v1/completions": { + "/v1/invitations/{token}/accept": { "post": { "tags": [ - "Chat" + "Invitations" ], - "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": "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" } - }, - "required": true - }, + } + ], "responses": { "200": { - "description": "Completion generated successfully", + "description": "Invitation accepted successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CompletionResponse" + "$ref": "#/components/schemas/AcceptInvitationResponse" } } } }, "400": { - "description": "Invalid request parameters", + "description": "Bad request - invitation expired or invalid", "content": { "application/json": { "schema": { @@ -4264,17 +4126,7 @@ } }, "401": { - "description": "Invalid or missing API key", - "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": { @@ -4283,8 +4135,8 @@ } } }, - "500": { - "description": "Server error", + "403": { + "description": "Forbidden - invitation belongs to another user", "content": { "application/json": { "schema": { @@ -4292,46 +4144,9 @@ } } } - } - }, - "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" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Conversation created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConversationObject" - } - } - } }, - "400": { - "description": "Invalid request", + "404": { + "description": "Invitation not found", "content": { "application/json": { "schema": { @@ -4340,8 +4155,8 @@ } } }, - "401": { - "description": "Invalid or missing API key", + "409": { + "description": "User is already a member", "content": { "application/json": { "schema": { @@ -4351,7 +4166,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -4363,63 +4178,60 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/conversations/{conversation_id}": { + "/v1/model/list": { "get": { "tags": [ - "Conversations" + "Models" ], - "summary": "Get conversation", - "description": "Retrieve conversation details by ID.", - "operationId": "get_conversation", + "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": "conversation_id", - "in": "path", - "description": "Conversation 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": "Conversation details", + "description": "List of models with pricing", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ConversationObject" + "$ref": "#/components/schemas/ModelListResponse" } } } }, "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Conversation not found", + "description": "Invalid pagination parameters", "content": { "application/json": { "schema": { @@ -4429,7 +4241,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -4438,54 +4250,41 @@ } } } - }, - "security": [ - { - "api_key": [] - } - ] - }, - "post": { + } + } + }, + "/v1/model/{model_name}": { + "get": { "tags": [ - "Conversations" + "Models" ], - "summary": "Update conversation", - "description": "Update conversation metadata.", - "operationId": "update_conversation", + "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": "conversation_id", + "name": "model_name", "in": "path", - "description": "Conversation ID", + "description": "Model name (URL-encode if it contains slashes)", "required": true, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateConversationRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Conversation updated successfully", + "description": "Model details with pricing", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ConversationObject" + "$ref": "#/components/schemas/ModelWithPricing" } } } }, - "400": { - "description": "Bad request", + "404": { + "description": "Model not found", "content": { "application/json": { "schema": { @@ -4494,8 +4293,8 @@ } } }, - "401": { - "description": "Unauthorized", + "500": { + "description": "Server error", "content": { "application/json": { "schema": { @@ -4503,19 +4302,31 @@ } } } - }, - "404": { - "description": "Conversation not found", + } + } + } + }, + "/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/ErrorResponse" + "$ref": "#/components/schemas/ModelsResponse" } } } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -4526,36 +4337,120 @@ } }, "security": [ - { - "api_key": [] - } + {} ] - }, - "delete": { + } + }, + "/v1/organizations": { + "get": { "tags": [ - "Conversations" + "Organizations" ], - "summary": "Delete conversation", - "description": "Delete a conversation and all its messages.", - "operationId": "delete_conversation", + "summary": "List organizations", + "description": "Get all organizations you belong to.", + "operationId": "list_organizations", "parameters": [ { - "name": "conversation_id", - "in": "path", - "description": "Conversation ID", - "required": true, + "name": "limit", + "in": "query", + "description": "Maximum number to return", + "required": false, "schema": { - "type": "string" + "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", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListOrganizationsResponse" + } + } + } + }, + "401": { + "description": "Invalid or missing session token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } } + }, + "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": "Conversation deleted successfully", + "description": "Organization created successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ConversationDeleteResult" + "$ref": "#/components/schemas/OrganizationResponse" } } } @@ -4580,8 +4475,8 @@ } } }, - "404": { - "description": "Conversation not found", + "409": { + "description": "Organization already exists", "content": { "application/json": { "schema": { @@ -4603,43 +4498,44 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/conversations/{conversation_id}/archive": { - "post": { + "/v1/organizations/{org_id}": { + "get": { "tags": [ - "Conversations" + "Organizations" ], - "summary": "Archive a conversation", - "description": "Archives a conversation to hide it from the main list.", - "operationId": "archive_conversation", + "summary": "Get organization by ID", + "description": "Returns organization details for a specific organization ID.", + "operationId": "get_organization", "parameters": [ { - "name": "conversation_id", + "name": "org_id", "in": "path", - "description": "Conversation ID", + "description": "Organization ID", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], "responses": { "200": { - "description": "Conversation archived successfully", + "description": "Organization details", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ConversationObject" + "$ref": "#/components/schemas/OrganizationResponse" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -4648,8 +4544,8 @@ } } }, - "401": { - "description": "Unauthorized", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -4659,7 +4555,7 @@ } }, "404": { - "description": "Conversation not found", + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -4681,35 +4577,46 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] }, - "delete": { + "put": { "tags": [ - "Conversations" + "Organizations" ], - "summary": "Unarchive a conversation", - "description": "Unarchives a conversation to show it in the main list again.", - "operationId": "unarchive_conversation", + "summary": "Update organization", + "description": "Updates organization details for a specific organization ID.", + "operationId": "update_organization", "parameters": [ { - "name": "conversation_id", + "name": "org_id", "in": "path", - "description": "Conversation ID", + "description": "Organization ID", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Conversation unarchived successfully", + "description": "Updated organization", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ConversationObject" + "$ref": "#/components/schemas/OrganizationResponse" } } } @@ -4734,8 +4641,18 @@ } } }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "404": { - "description": "Conversation not found", + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -4757,43 +4674,45 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] - } - }, - "/v1/conversations/{conversation_id}/clone": { - "post": { + }, + "delete": { "tags": [ - "Conversations" + "Organizations" ], - "summary": "Clone a conversation", - "description": "Creates a copy of an existing conversation with a new ID.", - "operationId": "clone_conversation", + "summary": "Delete organization (owner only)", + "description": "Deletes an organization. Only the organization owner can perform this action.", + "operationId": "delete_organization", "parameters": [ { - "name": "conversation_id", + "name": "org_id", "in": "path", - "description": "Conversation ID", + "description": "Organization ID", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], "responses": { - "201": { - "description": "Conversation cloned successfully", + "200": { + "description": "Organization deleted successfully" + }, + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ConversationObject" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "400": { - "description": "Bad request", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -4802,8 +4721,8 @@ } } }, - "401": { - "description": "Unauthorized", + "404": { + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -4812,8 +4731,8 @@ } } }, - "404": { - "description": "Conversation not found", + "409": { + "description": "Organization is bound to a NEAR staking wallet", "content": { "application/json": { "schema": { @@ -4835,63 +4754,57 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/conversations/{conversation_id}/items": { - "get": { + "/v1/organizations/{org_id}/invitations/{invitation_id}": { + "delete": { "tags": [ - "Conversations" + "Organization Members" ], - "summary": "List conversation messages", - "description": "Get all messages and responses in a conversation, sorted by creation time.", - "operationId": "list_conversation_items", + "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": "conversation_id", + "name": "org_id", "in": "path", - "description": "Conversation ID", + "description": "Organization ID", "required": true, "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "description": "Maximum number of items to return", - "required": false, - "schema": { - "type": "integer", - "format": "int64" + "type": "string", + "format": "uuid" } }, { - "name": "offset", - "in": "query", - "description": "Number of items to skip", - "required": false, + "name": "invitation_id", + "in": "path", + "description": "Invitation ID", + "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string", + "format": "uuid" } } ], "responses": { - "200": { - "description": "List of conversation items", + "204": { + "description": "Invitation cancelled successfully" + }, + "400": { + "description": "Bad request - invitation is not pending", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ConversationItemList" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -4900,8 +4813,8 @@ } } }, - "401": { - "description": "Unauthorized", + "403": { + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -4911,7 +4824,7 @@ } }, "404": { - "description": "Conversation not found", + "description": "Organization or invitation not found", "content": { "application/json": { "schema": { @@ -4933,45 +4846,123 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] - }, - "post": { + } + }, + "/v1/organizations/{org_id}/members": { + "get": { "tags": [ - "Conversations" + "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", + "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": "conversation_id", + "name": "org_id", "in": "path", - "description": "Conversation ID", + "description": "Organization ID", "required": true, "schema": { - "type": "string" - } + "type": "string", + "format": "uuid" + } }, { - "name": "include", + "name": "limit", "in": "query", - "description": "Additional fields to include in the response", + "description": "Number of records to return (default: 100, max: 1000)", "required": false, "schema": { - "type": "array", - "items": { - "type": "string" + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Offset for pagination (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "List of organization members with public user information", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListOrganizationMembersResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden - not a member of the 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": [] + } + ] + }, + "post": { + "tags": [ + "Organization Members" + ], + "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", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateConversationItemsRequest" + "$ref": "#/components/schemas/AddOrganizationMemberRequest" } } }, @@ -4979,17 +4970,17 @@ }, "responses": { "200": { - "description": "Items created successfully", + "description": "Member added successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ConversationItemList" + "$ref": "#/components/schemas/OrganizationMemberResponse" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -4998,8 +4989,8 @@ } } }, - "401": { - "description": "Unauthorized", + "403": { + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -5009,7 +5000,17 @@ } }, "404": { - "description": "Conversation not found", + "description": "User not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "User is already a member", "content": { "application/json": { "schema": { @@ -5031,43 +5032,56 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/conversations/{conversation_id}/pin": { - "post": { + "/v1/organizations/{org_id}/members/invitations": { + "get": { "tags": [ - "Conversations" + "Organization Members" ], - "summary": "Pin a conversation", - "description": "Pins a conversation to keep it at the top of the list.", - "operationId": "pin_conversation", + "summary": "List organization invitations", + "description": "Returns invitations for the organization. Only accessible to owners and admins.", + "operationId": "list_organization_invitations", "parameters": [ { - "name": "conversation_id", + "name": "org_id", "in": "path", - "description": "Conversation ID", + "description": "Organization ID", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" + } + }, + { + "name": "status", + "in": "query", + "description": "Filter by status", + "required": false, + "schema": { + "$ref": "#/components/schemas/InvitationStatus" } } ], "responses": { "200": { - "description": "Conversation pinned successfully", + "description": "List of organization invitations", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ConversationObject" + "type": "array", + "items": { + "$ref": "#/components/schemas/OrganizationInvitationResponse" + } } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5076,8 +5090,8 @@ } } }, - "401": { - "description": "Unauthorized", + "403": { + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -5087,7 +5101,7 @@ } }, "404": { - "description": "Conversation not found", + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -5109,41 +5123,57 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] - }, + } + }, + "/v1/organizations/{org_id}/members/invitations/{invitation_id}": { "delete": { "tags": [ - "Conversations" + "Organization Members" ], - "summary": "Unpin a conversation", - "description": "Unpins a conversation.", - "operationId": "unpin_conversation", + "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": "conversation_id", + "name": "org_id", "in": "path", - "description": "Conversation ID", + "description": "Organization ID", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" + } + }, + { + "name": "invitation_id", + "in": "path", + "description": "Invitation ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } } ], "responses": { - "200": { - "description": "Conversation unpinned successfully", + "204": { + "description": "Invitation cancelled successfully" + }, + "400": { + "description": "Bad request - invitation is not pending", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ConversationObject" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5152,8 +5182,8 @@ } } }, - "401": { - "description": "Unauthorized", + "403": { + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -5163,7 +5193,7 @@ } }, "404": { - "description": "Conversation not found", + "description": "Organization or invitation not found", "content": { "application/json": { "schema": { @@ -5185,23 +5215,36 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/feature-requests": { + "/v1/organizations/{org_id}/members/invite-by-email": { "post": { "tags": [ - "Feature Requests" + "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": "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" + "$ref": "#/components/schemas/InviteOrganizationMemberByEmailRequest" } } }, @@ -5209,17 +5252,17 @@ }, "responses": { "200": { - "description": "Feature request recorded", + "description": "Invitation results (may include partial failures)", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SubmitFeatureRequestResponse" + "$ref": "#/components/schemas/InviteOrganizationMemberByEmailResponse" } } } }, "400": { - "description": "Invalid request", + "description": "Bad request - empty invitation list", "content": { "application/json": { "schema": { @@ -5239,7 +5282,7 @@ } }, "403": { - "description": "Organization membership required", + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -5266,64 +5309,59 @@ ] } }, - "/v1/files": { - "get": { + "/v1/organizations/{org_id}/members/{user_id}": { + "put": { "tags": [ - "Files" + "Organization Members" ], - "operationId": "list_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", "parameters": [ { - "name": "after", - "in": "query", - "description": "A cursor for pagination. Pass the file ID to fetch files after this one.", - "required": false, + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } }, { - "name": "limit", - "in": "query", - "description": "Number of files to return (1-10000, default 10000)", - "required": false, + "name": "user_id", + "in": "path", + "description": "User ID of the member to update", + "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationMemberRequest" + } } }, - { - "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" - } - } - ], + "required": true + }, "responses": { "200": { - "description": "List of files retrieved successfully", + "description": "Member updated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FileListResponse" + "$ref": "#/components/schemas/OrganizationMemberResponse" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5332,8 +5370,18 @@ } } }, - "401": { - "description": "Unauthorized", + "403": { + "description": "Forbidden - not an admin or owner", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -5345,33 +5393,55 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] }, - "post": { + "delete": { "tags": [ - "Files" + "Organization Members" ], - "operationId": "upload_file", - "requestBody": { - "content": { - "multipart/form-data": {} + "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" + } + }, + { + "name": "user_id", + "in": "path", + "description": "User ID of the member to remove", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } } - }, + ], "responses": { - "201": { - "description": "File uploaded successfully", + "204": { + "description": "Member removed successfully" + }, + "400": { + "description": "Bad request - cannot remove last owner", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FileUploadResponse" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5380,8 +5450,8 @@ } } }, - "401": { - "description": "Unauthorized", + "403": { + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -5390,8 +5460,18 @@ } } }, - "413": { - "description": "File too large", + "404": { + "description": "Member not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -5403,41 +5483,44 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/files/{file_id}": { + "/v1/organizations/{org_id}/reporting-tokens": { "get": { "tags": [ - "Files" + "Reporting" ], - "operationId": "get_file", + "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": "file_id", + "name": "org_id", "in": "path", - "description": "The ID of the file to retrieve", + "description": "Organization ID", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], "responses": { "200": { - "description": "File information retrieved successfully", + "description": "Active reporting tokens", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FileUploadResponse" + "$ref": "#/components/schemas/ListReportingTokensResponse" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5446,8 +5529,8 @@ } } }, - "401": { - "description": "Unauthorized", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -5456,8 +5539,8 @@ } } }, - "404": { - "description": "File not found", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -5469,39 +5552,52 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] }, - "delete": { + "post": { "tags": [ - "Files" + "Reporting" ], - "operationId": "delete_file", + "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": "file_id", + "name": "org_id", "in": "path", - "description": "The ID of the file to delete", + "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": "File deleted successfully", + "201": { + "description": "Reporting token created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FileDeleteResponse" + "$ref": "#/components/schemas/CreateReportingTokenResponse" } } } }, "400": { - "description": "Bad request", + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -5520,8 +5616,18 @@ } } }, - "404": { - "description": "File not found", + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -5533,37 +5639,57 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/files/{file_id}/content": { - "get": { + "/v1/organizations/{org_id}/reporting-tokens/{token_id}": { + "delete": { "tags": [ - "Files" + "Reporting" ], - "operationId": "get_file_content", + "summary": "Revoke a reporting token.", + "description": "Revoked reporting tokens can no longer authenticate usage reporting\nrequests.", + "operationId": "revoke_reporting_token", "parameters": [ { - "name": "file_id", + "name": "org_id", "in": "path", - "description": "The ID of the file to retrieve content from", + "description": "Organization ID", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" + } + }, + { + "name": "token_id", + "in": "path", + "description": "Reporting token ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } } ], "responses": { - "200": { - "description": "File content retrieved successfully", + "204": { + "description": "Reporting token revoked" + }, + "401": { + "description": "Unauthorized", "content": { - "application/octet-stream": {} + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - "400": { - "description": "Bad request", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -5572,8 +5698,8 @@ } } }, - "401": { - "description": "Unauthorized", + "404": { + "description": "Reporting token not found", "content": { "application/json": { "schema": { @@ -5582,8 +5708,8 @@ } } }, - "404": { - "description": "File not found", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -5595,64 +5721,43 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/health": { + "/v1/organizations/{org_id}/staking/farm": { "get": { "tags": [ - "Health" + "Staking Farm" + ], + "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" + } + } ], - "summary": "Health check", - "description": "Check service health status. No authentication required.", - "operationId": "health_check", "responses": { "200": { - "description": "Service is healthy", + "description": "Organization staking farm state", "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": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/ImageEditRequestSchema" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Image edited successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ImageGenerationResponse" + "$ref": "#/components/schemas/StakingFarmStateResponse" } } } }, "400": { - "description": "Invalid request parameters", + "description": "Invalid organization ID", "content": { "application/json": { "schema": { @@ -5662,17 +5767,7 @@ } }, "401": { - "description": "Invalid or missing API key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Model not found", + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5681,8 +5776,8 @@ } } }, - "413": { - "description": "Payload too large", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -5691,8 +5786,8 @@ } } }, - "429": { - "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "404": { + "description": "No staking farm source found", "content": { "application/json": { "schema": { @@ -5702,7 +5797,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -5714,42 +5809,43 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/images/generations": { + "/v1/organizations/{org_id}/staking/farm/sync": { "post": { "tags": [ - "Images" + "Staking Farm" ], - "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" - } + "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": "Image generated successfully", + "description": "Synced organization staking farm state", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ImageGenerationResponse" + "$ref": "#/components/schemas/StakingFarmStateResponse" } } } }, "400": { - "description": "Invalid request parameters", + "description": "Invalid organization ID", "content": { "application/json": { "schema": { @@ -5759,7 +5855,7 @@ } }, "401": { - "description": "Invalid or missing API key", + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5768,8 +5864,18 @@ } } }, - "429": { - "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "NEAR account conflict or organization inactive", "content": { "application/json": { "schema": { @@ -5779,7 +5885,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -5791,24 +5897,24 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/invitations/{token}": { + "/v1/organizations/{org_id}/usage/balance": { "get": { "tags": [ - "Invitations" + "Usage" ], - "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", + "summary": "Get organization balance", + "description": "Returns the current spending balance for an organization", + "operationId": "get_organization_balance", "parameters": [ { - "name": "token", + "name": "org_id", "in": "path", - "description": "Invitation token", + "description": "Organization ID", "required": true, "schema": { "type": "string" @@ -5817,17 +5923,17 @@ ], "responses": { "200": { - "description": "Invitation details", + "description": "Organization balance", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationInvitationResponse" + "$ref": "#/components/schemas/OrganizationBalanceResponse" } } } }, - "404": { - "description": "Invitation not found", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5836,8 +5942,18 @@ } } }, - "410": { - "description": "Invitation expired or no longer pending", + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", "content": { "application/json": { "schema": { @@ -5856,45 +5972,49 @@ } } } - } + }, + "security": [ + { + "session_token": [] + } + ] } }, - "/v1/invitations/{token}/accept": { - "post": { + "/v1/organizations/{org_id}/usage/by-model": { + "get": { "tags": [ - "Invitations" + "Usage" ], - "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", + "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": "token", + "name": "org_id", "in": "path", - "description": "Invitation token", + "description": "Organization ID", "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": "Invitation accepted successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AcceptInvitationResponse" - } - } - } - }, - "400": { - "description": "Bad request - invitation expired or invalid", + "description": "Per-model usage breakdown", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/UsageByModelResponse" } } } @@ -5910,27 +6030,7 @@ } }, "403": { - "description": "Forbidden - invitation belongs to another user", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Invitation not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "409": { - "description": "User is already a member", + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -5957,65 +6057,133 @@ ] } }, - "/v1/model/list": { + "/v1/organizations/{org_id}/usage/export": { "get": { "tags": [ - "Models" + "Reporting" ], - "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": "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": "limit", + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "start_time", "in": "query", - "description": "Maximum number of models to return. Defaults to 100. Must be\nnon-negative; values are capped only by the catalog size.", + "description": "Inclusive RFC3339 start timestamp. Defaults to 366 days before the effective end_time.", "required": false, "schema": { - "type": [ - "integer", - "null" - ], - "format": "int64" + "type": "string" } }, { - "name": "offset", + "name": "end_time", "in": "query", - "description": "Number of models to skip from the start of the catalog.\nDefaults to 0. Must be non-negative.", + "description": "Inclusive RFC3339 end timestamp. Defaults to the request time. The effective range must not exceed 366 days.", "required": false, "schema": { - "type": [ - "integer", - "null" - ], - "format": "int64" + "type": "string" } - } - ], - "responses": { - "200": { - "description": "List of models with pricing", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ModelListResponse" - } - } + }, + { + "name": "source", + "in": "query", + "description": "Usage source to export. Defaults to all.", + "required": false, + "schema": { + "$ref": "#/components/schemas/ReportingUsageSource" } }, - "400": { - "description": "Invalid pagination parameters", + { + "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", + "in": "query", + "description": "Filter inference rows by inference type.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "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" + } + } + ], + "responses": { + "200": { + "description": "Usage export page", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ReportingUsageExportResponse" } } } }, - "500": { - "description": "Server error", + "400": { + "description": "Invalid filters or cursor", "content": { "application/json": { "schema": { @@ -6023,42 +6191,19 @@ } } } - } - } - } - }, - "/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", + }, + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelWithPricing" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "404": { - "description": "Model not found", + "403": { + "description": "Reporting token is not scoped to this organization", "content": { "application/json": { "schema": { @@ -6067,8 +6212,8 @@ } } }, - "500": { - "description": "Server error", + "429": { + "description": "Reporting rate or concurrency limit exceeded", "content": { "application/json": { "schema": { @@ -6076,31 +6221,19 @@ } } } - } - } - } - }, - "/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", + }, + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelsResponse" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "500": { - "description": "Server error", + "504": { + "description": "Reporting request timed out", "content": { "application/json": { "schema": { @@ -6111,23 +6244,34 @@ } }, "security": [ - {} + { + "reporting_token": [] + } ] } }, - "/v1/organizations": { + "/v1/organizations/{org_id}/usage/history": { "get": { "tags": [ - "Organizations" + "Usage" ], - "summary": "List organizations", - "description": "Get all organizations you belong to.", - "operationId": "list_organizations", + "summary": "Get organization usage history", + "description": "Returns paginated usage history for an organization", + "operationId": "get_organization_usage_history", "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "limit", "in": "query", - "description": "Maximum number to return", + "description": "Number of records to return (default: 100)", "required": false, "schema": { "type": "integer", @@ -6137,7 +6281,7 @@ { "name": "offset", "in": "query", - "description": "Number to skip", + "description": "Offset for pagination (default: 0)", "required": false, "schema": { "type": "integer", @@ -6145,37 +6289,85 @@ } }, { - "name": "order_by", + "name": "start_date", "in": "query", - "description": "Sort by field", + "description": "Inclusive UTC start date in YYYY-MM-DD or RFC3339 format.", "required": false, "schema": { - "$ref": "#/components/schemas/OrganizationOrderBy" + "type": "string" } }, { - "name": "order_direction", + "name": "end_date", "in": "query", - "description": "Sort direction", + "description": "Inclusive UTC end date in YYYY-MM-DD or RFC3339 format.", "required": false, "schema": { - "$ref": "#/components/schemas/OrganizationOrderDirection" + "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" } } ], "responses": { "200": { - "description": "List of organizations", + "description": "Usage history", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListOrganizationsResponse" + "$ref": "#/components/schemas/UsageHistoryResponse" } } } }, "401": { - "description": "Invalid or missing session token", + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -6185,7 +6377,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -6200,41 +6392,50 @@ "session_token": [] } ] - }, - "post": { + } + }, + "/v1/organizations/{org_id}/usage/metrics": { + "get": { "tags": [ - "Organizations" + "Usage" ], - "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" - } - } + "operationId": "get_user_organization_metrics", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string" } }, - "400": { - "description": "Bad request", + { + "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": "Organization usage metrics", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/UserOrganizationMetrics" } } } @@ -6249,8 +6450,8 @@ } } }, - "409": { - "description": "Organization already exists", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -6277,14 +6478,14 @@ ] } }, - "/v1/organizations/{org_id}": { + "/v1/organizations/{org_id}/usage/summary": { "get": { "tags": [ - "Organizations" + "Reporting" ], - "summary": "Get organization by ID", - "description": "Returns organization details for a specific organization ID.", - "operationId": "get_organization", + "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": "org_id", @@ -6295,15 +6496,99 @@ "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": "Organization details", + "description": "Usage summary", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationResponse" + "$ref": "#/components/schemas/ReportingUsageSummaryResponse" + } + } + } + }, + "400": { + "description": "Invalid filters", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -6319,7 +6604,7 @@ } }, "403": { - "description": "Forbidden", + "description": "Reporting token is not scoped to this organization", "content": { "application/json": { "schema": { @@ -6328,8 +6613,8 @@ } } }, - "404": { - "description": "Organization not found", + "429": { + "description": "Reporting rate or concurrency limit exceeded", "content": { "application/json": { "schema": { @@ -6347,21 +6632,31 @@ } } } + }, + "504": { + "description": "Reporting request timed out", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } }, "security": [ { - "session_token": [] + "reporting_token": [] } ] - }, - "put": { + } + }, + "/v1/organizations/{org_id}/usage/timeseries": { + "get": { "tags": [ - "Organizations" + "Usage" ], - "summary": "Update organization", - "description": "Updates organization details for a specific organization ID.", - "operationId": "update_organization", + "operationId": "get_user_organization_timeseries", "parameters": [ { "name": "org_id", @@ -6369,38 +6664,44 @@ "description": "Organization ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrganizationRequest" - } + }, + { + "name": "start", + "in": "query", + "description": "Start date (ISO 8601, default: 30 days ago)", + "required": false, + "schema": { + "type": "string" } }, - "required": true - }, - "responses": { - "200": { - "description": "Updated organization", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OrganizationResponse" - } - } + { + "name": "end", + "in": "query", + "description": "End date (ISO 8601, default: now)", + "required": false, + "schema": { + "type": "string" } }, - "400": { - "description": "Bad request", + { + "name": "granularity", + "in": "query", + "description": "Time bucket size: hour, day, week (default: day)", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Organization usage timeseries", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/UserTimeSeriesMetrics" } } } @@ -6425,16 +6726,6 @@ } } }, - "404": { - "description": "Organization not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -6451,14 +6742,16 @@ "session_token": [] } ] - }, - "delete": { + } + }, + "/v1/organizations/{org_id}/workspaces": { + "get": { "tags": [ - "Organizations" + "Workspaces" ], - "summary": "Delete organization (owner only)", - "description": "Deletes an organization. Only the organization owner can perform this action.", - "operationId": "delete_organization", + "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": "org_id", @@ -6469,34 +6762,59 @@ "type": "string", "format": "uuid" } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of results", + "required": false, + "schema": { + "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": { "200": { - "description": "Organization deleted successfully" - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "403": { - "description": "Forbidden", + "description": "Paginated list of workspaces", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ListWorkspacesResponse" } } } }, - "404": { - "description": "Organization not found", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -6505,8 +6823,8 @@ } } }, - "409": { - "description": "Organization is bound to a NEAR staking wallet", + "403": { + "description": "Forbidden - not a member of organization", "content": { "application/json": { "schema": { @@ -6531,16 +6849,14 @@ "session_token": [] } ] - } - }, - "/v1/organizations/{org_id}/invitations/{invitation_id}": { - "delete": { + }, + "post": { "tags": [ - "Organization Members" + "Workspaces" ], - "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": "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": "org_id", @@ -6551,24 +6867,31 @@ "type": "string", "format": "uuid" } - }, - { - "name": "invitation_id", - "in": "path", - "description": "Invitation ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWorkspaceRequest" + } + } + }, + "required": true + }, "responses": { - "204": { - "description": "Invitation cancelled successfully" + "201": { + "description": "Workspace created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkspaceResponse" + } + } + } }, "400": { - "description": "Bad request - invitation is not pending", + "description": "Bad request", "content": { "application/json": { "schema": { @@ -6588,7 +6911,7 @@ } }, "403": { - "description": "Forbidden - not an admin or owner", + "description": "Forbidden - not a member of organization", "content": { "application/json": { "schema": { @@ -6597,8 +6920,8 @@ } } }, - "404": { - "description": "Organization or invitation not found", + "409": { + "description": "Workspace name already exists in organization", "content": { "application/json": { "schema": { @@ -6625,53 +6948,39 @@ ] } }, - "/v1/organizations/{org_id}/members": { - "get": { + "/v1/privacy/classify": { + "post": { "tags": [ - "Organization Members" + "Privacy" ], - "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", - "in": "path", - "description": "Organization 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" + "operationId": "privacy_classify", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrivacyClassifyRequestDoc" + } } }, - { - "name": "offset", - "in": "query", - "description": "Offset for pagination (default: 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], + "required": true + }, "responses": { "200": { - "description": "List of organization members with public user information", + "description": "Privacy classification completed successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListOrganizationMembersResponse" + "$ref": "#/components/schemas/PrivacyClassifyResponseDoc" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -6686,8 +6995,18 @@ } } }, - "403": { - "description": "Forbidden - not a member of the organization", + "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": { @@ -6697,7 +7016,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -6709,34 +7028,22 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] - }, + } + }, + "/v1/privacy/redact": { "post": { "tags": [ - "Organization Members" - ], - "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", - "in": "path", - "description": "Organization ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } + "Privacy" ], + "operationId": "privacy_redact", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AddOrganizationMemberRequest" + "$ref": "#/components/schemas/PrivacyRedactRequestDoc" } } }, @@ -6744,17 +7051,17 @@ }, "responses": { "200": { - "description": "Member added successfully", + "description": "Privacy redaction completed successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationMemberResponse" + "$ref": "#/components/schemas/PrivacyRedactResponseDoc" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -6763,8 +7070,8 @@ } } }, - "403": { - "description": "Forbidden - not an admin or owner", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -6774,7 +7081,7 @@ } }, "404": { - "description": "User not found", + "description": "Model not found", "content": { "application/json": { "schema": { @@ -6783,8 +7090,8 @@ } } }, - "409": { - "description": "User is already a member", + "429": { + "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", "content": { "application/json": { "schema": { @@ -6794,7 +7101,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -6806,56 +7113,42 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/organizations/{org_id}/members/invitations": { - "get": { + "/v1/rerank": { + "post": { "tags": [ - "Organization Members" + "Rerank" ], - "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" + "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" + } } }, - { - "name": "status", - "in": "query", - "description": "Filter by status", - "required": false, - "schema": { - "$ref": "#/components/schemas/InvitationStatus" - } - } - ], + "required": true + }, "responses": { "200": { - "description": "List of organization invitations", + "description": "Successful rerank", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrganizationInvitationResponse" - } + "$ref": "#/components/schemas/RerankResponse" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Invalid request (empty documents, invalid model, etc.)", "content": { "application/json": { "schema": { @@ -6864,8 +7157,8 @@ } } }, - "403": { - "description": "Forbidden - not an admin or owner", + "401": { + "description": "Unauthorized (missing or invalid API key)", "content": { "application/json": { "schema": { @@ -6875,7 +7168,17 @@ } }, "404": { - "description": "Organization not found", + "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": { @@ -6885,7 +7188,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error (billing failure, provider error)", "content": { "application/json": { "schema": { @@ -6897,57 +7200,42 @@ }, "security": [ { - "session_token": [] + "ApiKeyAuth": [] } ] } }, - "/v1/organizations/{org_id}/members/invitations/{invitation_id}": { - "delete": { + "/v1/responses": { + "post": { "tags": [ - "Organization Members" + "Responses" ], - "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": "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" + } } }, - { - "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": "Response created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ResponseObject" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -6956,8 +7244,8 @@ } } }, - "403": { - "description": "Forbidden - not an admin or owner", + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -6966,8 +7254,8 @@ } } }, - "404": { - "description": "Organization or invitation not found", + "402": { + "description": "Insufficient credits", "content": { "application/json": { "schema": { @@ -6977,7 +7265,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -6989,36 +7277,24 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/organizations/{org_id}/members/invite-by-email": { + "/v1/score": { "post": { "tags": [ - "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" - } - } + "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/InviteOrganizationMemberByEmailRequest" + "$ref": "#/components/schemas/ScoreRequest" } } }, @@ -7026,17 +7302,17 @@ }, "responses": { "200": { - "description": "Invitation results (may include partial failures)", + "description": "Successful score", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InviteOrganizationMemberByEmailResponse" + "$ref": "#/components/schemas/ScoreResponse" } } } }, "400": { - "description": "Bad request - empty invitation list", + "description": "Invalid request (empty texts, invalid model, etc.)", "content": { "application/json": { "schema": { @@ -7046,7 +7322,7 @@ } }, "401": { - "description": "Unauthorized", + "description": "Unauthorized (missing or invalid API key)", "content": { "application/json": { "schema": { @@ -7055,8 +7331,18 @@ } } }, - "403": { - "description": "Forbidden - not an admin or owner", + "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": { @@ -7066,7 +7352,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error (billing failure, provider error)", "content": { "application/json": { "schema": { @@ -7078,74 +7364,51 @@ }, "security": [ { - "session_token": [] + "ApiKeyAuth": [] } ] } }, - "/v1/organizations/{org_id}/members/{user_id}": { - "put": { + "/v1/services": { + "get": { "tags": [ - "Organization Members" + "Services" ], - "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", + "summary": "List platform services (public, no auth)", + "operationId": "list_services", "parameters": [ { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, + "name": "limit", + "in": "query", + "required": false, "schema": { - "type": "string", - "format": "uuid" + "type": "integer", + "format": "int64" } }, { - "name": "user_id", - "in": "path", - "description": "User ID of the member to update", - "required": true, + "name": "offset", + "in": "query", + "required": false, "schema": { - "type": "string", - "format": "uuid" + "type": "integer", + "format": "int64" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrganizationMemberRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Member updated successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OrganizationMemberResponse" - } - } - } - }, - "401": { - "description": "Unauthorized", + "description": "Services list", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ServiceListResponse" } } } }, - "403": { - "description": "Forbidden - not an admin or owner", + "400": { + "description": "Invalid parameters", "content": { "application/json": { "schema": { @@ -7164,48 +7427,115 @@ } } } - }, - "security": [ - { - "session_token": [] - } - ] - }, - "delete": { + } + } + }, + "/v1/services/{service_name}": { + "get": { "tags": [ - "Organization Members" + "Services" ], - "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", + "summary": "Get platform service by name (public, no auth)", + "operationId": "get_service_by_name", "parameters": [ { - "name": "org_id", + "name": "service_name", "in": "path", - "description": "Organization ID", + "description": "Service name (e.g. web_search)", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Service details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceResponse" + } + } + } + }, + "404": { + "description": "Service not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/v1/signature/{chat_id}": { + "get": { + "tags": [ + "Attestation" + ], + "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", + "parameters": [ { - "name": "user_id", + "name": "chat_id", "in": "path", - "description": "User ID of the member to remove", + "description": "Chat completion ID or resp_* Responses API ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" + } + }, + { + "name": "model", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "signing_algo", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] } } ], "responses": { - "204": { - "description": "Member removed successfully" + "200": { + "description": "Signature retrieved or unavailable due to disconnect", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignatureResponse" + } + } + } }, "400": { - "description": "Bad request - cannot remove last owner", + "description": "Invalid parameters", "content": { "application/json": { "schema": { @@ -7214,8 +7544,8 @@ } } }, - "401": { - "description": "Unauthorized", + "404": { + "description": "Signature not found", "content": { "application/json": { "schema": { @@ -7223,19 +7553,36 @@ } } } - }, - "403": { - "description": "Forbidden - not an admin or owner", + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v1/staking/farm/config": { + "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", + "responses": { + "200": { + "description": "Staking farm configuration", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/StakingFarmConfigResponse" } } } }, - "404": { - "description": "Member not found", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -7262,33 +7609,21 @@ ] } }, - "/v1/organizations/{org_id}/reporting-tokens": { + "/v1/users/me": { "get": { "tags": [ - "Reporting" - ], - "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": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } + "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": "Active reporting tokens", + "description": "Current user profile", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListReportingTokensResponse" + "$ref": "#/components/schemas/UserResponse" } } } @@ -7303,8 +7638,8 @@ } } }, - "403": { - "description": "Forbidden", + "404": { + "description": "User not found", "content": { "application/json": { "schema": { @@ -7330,52 +7665,30 @@ } ] }, - "post": { + "patch": { "tags": [ - "Reporting" - ], - "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" - } - } + "Users" ], + "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/CreateReportingTokenRequest" + "$ref": "#/components/schemas/UpdateUserProfileRequest" } } }, "required": true }, "responses": { - "201": { - "description": "Reporting token created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateReportingTokenResponse" - } - } - } - }, - "400": { - "description": "Invalid request", + "200": { + "description": "Updated user profile", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/UserResponse" } } } @@ -7390,16 +7703,6 @@ } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -7418,62 +7721,27 @@ ] } }, - "/v1/organizations/{org_id}/reporting-tokens/{token_id}": { - "delete": { + "/v1/users/me/access-tokens": { + "post": { "tags": [ - "Reporting" - ], - "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" - } - }, - { - "name": "token_id", - "in": "path", - "description": "Reporting token ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } + "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": { - "204": { - "description": "Reporting token revoked" - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "403": { - "description": "Forbidden", + "200": { + "description": "Access token created successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/AccessAndRefreshTokenResponse" } } } }, - "404": { - "description": "Reporting token not found", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -7495,43 +7763,94 @@ }, "security": [ { - "session_token": [] + "refresh_token": [] } ] } }, - "/v1/organizations/{org_id}/staking/farm": { + "/v1/users/me/invitations": { "get": { "tags": [ - "Staking Farm" + "Users" ], - "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": "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", + "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/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": "org_id", + "name": "invitation_id", "in": "path", - "description": "Organization ID", + "description": "Invitation ID", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], "responses": { "200": { - "description": "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": { @@ -7551,7 +7870,7 @@ } }, "403": { - "description": "Forbidden", + "description": "Forbidden - invitation belongs to another user", "content": { "application/json": { "schema": { @@ -7561,7 +7880,17 @@ } }, "404": { - "description": "No staking farm source found", + "description": "Invitation not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "User is already a member", "content": { "application/json": { "schema": { @@ -7588,38 +7917,32 @@ ] } }, - "/v1/organizations/{org_id}/staking/farm/sync": { + "/v1/users/me/invitations/{invitation_id}/decline": { "post": { "tags": [ - "Staking Farm" + "Users" ], - "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": "Decline an organization invitation", + "description": "Declines a pending invitation to join an organization.", + "operationId": "decline_invitation", "parameters": [ { - "name": "org_id", + "name": "invitation_id", "in": "path", - "description": "Organization ID", + "description": "Invitation ID", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], "responses": { "200": { - "description": "Synced organization staking farm state", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StakingFarmStateResponse" - } - } - } + "description": "Invitation declined successfully" }, "400": { - "description": "Invalid organization ID", + "description": "Bad request - invitation not pending", "content": { "application/json": { "schema": { @@ -7639,7 +7962,7 @@ } }, "403": { - "description": "Forbidden", + "description": "Forbidden - invitation belongs to another user", "content": { "application/json": { "schema": { @@ -7648,8 +7971,8 @@ } } }, - "409": { - "description": "NEAR account conflict or organization inactive", + "404": { + "description": "Invitation not found", "content": { "application/json": { "schema": { @@ -7676,32 +7999,24 @@ ] } }, - "/v1/organizations/{org_id}/usage/balance": { + "/v1/users/me/refresh-tokens": { "get": { "tags": [ - "Usage" - ], - "summary": "Get organization balance", - "description": "Returns the current spending balance for an organization", - "operationId": "get_organization_balance", - "parameters": [ - { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, - "schema": { - "type": "string" - } - } + "Users" ], + "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": "Organization balance", + "description": "List of user refresh tokens", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationBalanceResponse" + "type": "array", + "items": { + "$ref": "#/components/schemas/RefreshTokenResponse" + } } } } @@ -7716,8 +8031,50 @@ } } }, - "403": { - "description": "Forbidden", + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "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": "refresh_token_id", + "in": "path", + "description": "Refresh token ID to revoke", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Refresh token revoked successfully" + }, + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -7727,7 +8084,7 @@ } }, "404": { - "description": "Not found", + "description": "Refresh token not found", "content": { "application/json": { "schema": { @@ -7754,41 +8111,21 @@ ] } }, - "/v1/organizations/{org_id}/usage/by-model": { + "/v1/users/me/status": { "get": { "tags": [ - "Usage" - ], - "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": "org_id", - "in": "path", - "description": "Organization ID", - "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" - } - } + "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": "Per-model usage breakdown", + "description": "Current account is eligible", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UsageByModelResponse" + "$ref": "#/components/schemas/UserStatusResponse" } } } @@ -7804,7 +8141,7 @@ } }, "403": { - "description": "Forbidden", + "description": "Current account is not eligible", "content": { "application/json": { "schema": { @@ -7831,133 +8168,171 @@ ] } }, - "/v1/organizations/{org_id}/usage/export": { - "get": { + "/v1/users/me/tokens": { + "delete": { "tags": [ - "Reporting" + "Users" ], - "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" - } - }, - { - "name": "start_time", - "in": "query", - "description": "Inclusive RFC3339 start timestamp. Defaults to 366 days before the effective end_time.", - "required": false, - "schema": { - "type": "string" + "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": {} + } } }, - { - "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" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "source", - "in": "query", - "description": "Usage source to export. Defaults to all.", - "required": false, - "schema": { - "$ref": "#/components/schemas/ReportingUsageSource" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - }, + } + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/workspaces/{workspace_id}": { + "get": { + "tags": [ + "Workspaces" + ], + "summary": "Get workspace by ID", + "description": "Returns workspace details for a specific workspace ID.", + "operationId": "get_workspace", + "parameters": [ { "name": "workspace_id", - "in": "query", - "description": "Filter by workspace ID.", - "required": false, + "in": "path", + "description": "Workspace ID", + "required": true, "schema": { "type": "string", "format": "uuid" } - }, - { - "name": "api_key_id", - "in": "query", - "description": "Filter by API key ID.", - "required": false, - "schema": { - "type": "string", - "format": "uuid" + } + ], + "responses": { + "200": { + "description": "Workspace details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkspaceResponse" + } + } } }, - { - "name": "model", - "in": "query", - "description": "Filter inference rows by model name.", - "required": false, - "schema": { - "type": "string" + "401": { + "description": "Unauthorized", + "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" + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "service_name", - "in": "query", - "description": "Filter service rows by platform service name.", - "required": false, - "schema": { - "type": "string" + "404": { + "description": "Workspace not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "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 + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - }, + } + }, + "security": [ { - "name": "cursor", - "in": "query", - "description": "Opaque cursor returned by the previous export page.", - "required": false, + "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" + "type": "string", + "format": "uuid" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWorkspaceRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Usage export page", + "description": "Updated workspace", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReportingUsageExportResponse" + "$ref": "#/components/schemas/WorkspaceResponse" } } } }, "400": { - "description": "Invalid filters or cursor", + "description": "Bad request", "content": { "application/json": { "schema": { @@ -7977,7 +8352,7 @@ } }, "403": { - "description": "Reporting token is not scoped to this organization", + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -7986,8 +8361,8 @@ } } }, - "429": { - "description": "Reporting rate or concurrency limit exceeded", + "404": { + "description": "Workspace not found", "content": { "application/json": { "schema": { @@ -8005,127 +8380,150 @@ } } } - }, - "504": { - "description": "Reporting request timed out", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } } }, "security": [ { - "reporting_token": [] + "session_token": [] } ] - } - }, - "/v1/organizations/{org_id}/usage/history": { - "get": { + }, + "delete": { "tags": [ - "Usage" + "Workspaces" ], - "summary": "Get organization usage history", - "description": "Returns paginated usage history for an organization", - "operationId": "get_organization_usage_history", + "summary": "Delete workspace", + "description": "Deletes (deactivates) a workspace. Only the workspace creator or organization admin/owner can delete.", + "operationId": "delete_workspace", "parameters": [ { - "name": "org_id", + "name": "workspace_id", "in": "path", - "description": "Organization ID", + "description": "Workspace ID", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } + } + ], + "responses": { + "200": { + "description": "Workspace deleted successfully" }, - { - "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" + } + } } }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ { - "name": "end_date", - "in": "query", - "description": "Inclusive UTC end date in YYYY-MM-DD or RFC3339 format.", - "required": false, + "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" + "type": "string", + "format": "uuid" } }, { - "name": "start_time", + "name": "limit", "in": "query", - "description": "Inclusive RFC3339 start timestamp. Takes precedence over start_date.", + "description": "Maximum number of results (default: 20)", "required": false, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } }, { - "name": "end_time", + "name": "offset", "in": "query", - "description": "Inclusive RFC3339 end timestamp. Takes precedence over end_date.", + "description": "Number of results to skip (default: 0)", "required": false, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } }, { - "name": "workspace_id", + "name": "order_by", "in": "query", - "description": "Filter by workspace ID.", + "description": "Field to order by: created_at or usage", "required": false, "schema": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/ApiKeyOrderBy" } }, { - "name": "api_key_id", + "name": "order_direction", "in": "query", - "description": "Filter by API key ID.", + "description": "Sort direction: asc or desc", "required": false, "schema": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/ApiKeyOrderDirection" } } ], "responses": { "200": { - "description": "Usage history", + "description": "Paginated list of workspace API keys", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UsageHistoryResponse" + "$ref": "#/components/schemas/ListApiKeysResponse" } } } @@ -8150,6 +8548,16 @@ } } }, + "404": { + "description": "Workspace not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -8166,50 +8574,53 @@ "session_token": [] } ] - } - }, - "/v1/organizations/{org_id}/usage/metrics": { - "get": { + }, + "post": { "tags": [ - "Usage" + "Workspaces" ], - "operationId": "get_user_organization_metrics", + "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": "org_id", + "name": "workspace_id", "in": "path", - "description": "Organization ID", + "description": "Workspace 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/CreateApiKeyRequest" + } + } + }, + "required": true + }, "responses": { - "200": { - "description": "Organization usage metrics", + "201": { + "description": "API key created successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserOrganizationMetrics" + "$ref": "#/components/schemas/ApiKeyResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -8234,6 +8645,16 @@ } } }, + "404": { + "description": "Workspace not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -8252,119 +8673,63 @@ ] } }, - "/v1/organizations/{org_id}/usage/summary": { + "/v1/workspaces/{workspace_id}/api-keys/{api_key_id}/usage/history": { "get": { "tags": [ - "Reporting" + "Usage" ], - "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": "Get API key usage history", + "description": "Returns paginated usage history for a specific API key", + "operationId": "get_api_key_usage_history", "parameters": [ { - "name": "org_id", + "name": "workspace_id", "in": "path", - "description": "Organization ID", + "description": "Workspace 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, + "name": "api_key_id", + "in": "path", + "description": "API Key ID", + "required": true, "schema": { "type": "string" } }, { - "name": "source", + "name": "limit", "in": "query", - "description": "Usage source to summarize. Defaults to all.", + "description": "Number of records to return (default: 100)", "required": false, "schema": { - "$ref": "#/components/schemas/ReportingUsageSource" + "type": "integer", + "format": "int64" } }, { - "name": "workspace_id", + "name": "offset", "in": "query", - "description": "Filter by workspace ID.", + "description": "Offset for pagination (default: 0)", "required": false, "schema": { - "type": "string", - "format": "uuid" + "type": "integer", + "format": "int64" } - }, - { - "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": "Usage history", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageHistoryResponse" + } + } } }, "401": { @@ -8378,7 +8743,7 @@ } }, "403": { - "description": "Reporting token is not scoped to this organization", + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -8387,8 +8752,8 @@ } } }, - "429": { - "description": "Reporting rate or concurrency limit exceeded", + "404": { + "description": "Not found", "content": { "application/json": { "schema": { @@ -8406,82 +8771,61 @@ } } } - }, - "504": { - "description": "Reporting request timed out", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } } }, "security": [ { - "reporting_token": [] + "session_token": [] } ] } }, - "/v1/organizations/{org_id}/usage/timeseries": { - "get": { + "/v1/workspaces/{workspace_id}/api-keys/{key_id}": { + "delete": { "tags": [ - "Usage" + "Workspaces" ], - "operationId": "get_user_organization_timeseries", + "summary": "Revoke an API key", + "description": "Revokes a specific API key from a workspace.", + "operationId": "revoke_workspace_api_key", "parameters": [ { - "name": "org_id", + "name": "workspace_id", "in": "path", - "description": "Organization ID", + "description": "Workspace 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": "key_id", + "in": "path", + "description": "API Key ID", + "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], "responses": { - "200": { - "description": "Organization usage timeseries", + "204": { + "description": "API key revoked successfully" + }, + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserTimeSeriesMetrics" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "401": { - "description": "Unauthorized", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -8490,8 +8834,8 @@ } } }, - "403": { - "description": "Forbidden", + "404": { + "description": "API key not found", "content": { "application/json": { "schema": { @@ -8516,21 +8860,19 @@ "session_token": [] } ] - } - }, - "/v1/organizations/{org_id}/workspaces": { - "get": { + }, + "patch": { "tags": [ "Workspaces" ], - "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": "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": "org_id", + "name": "workspace_id", "in": "path", - "description": "Organization ID", + "description": "Workspace ID", "required": true, "schema": { "type": "string", @@ -8538,51 +8880,43 @@ } }, { - "name": "limit", - "in": "query", - "description": "Maximum number of results", - "required": false, - "schema": { - "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, + "name": "key_id", + "in": "path", + "description": "API Key ID", + "required": true, "schema": { - "$ref": "#/components/schemas/WorkspaceOrderDirection" + "type": "string", + "format": "uuid" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateApiKeyRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Paginated list of workspaces", + "description": "API key updated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListWorkspacesResponse" + "$ref": "#/components/schemas/ApiKeyResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -8598,7 +8932,27 @@ } }, "403": { - "description": "Forbidden - not a member of organization", + "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": { @@ -8623,19 +8977,31 @@ "session_token": [] } ] - }, - "post": { + } + }, + "/v1/workspaces/{workspace_id}/api-keys/{key_id}/spend-limit": { + "patch": { "tags": [ "Workspaces" ], - "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": "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": "org_id", + "name": "workspace_id", "in": "path", - "description": "Organization ID", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "key_id", + "in": "path", + "description": "API Key ID", "required": true, "schema": { "type": "string", @@ -8647,19 +9013,19 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateWorkspaceRequest" + "$ref": "#/components/schemas/UpdateApiKeySpendLimitRequest" } } }, "required": true }, "responses": { - "201": { - "description": "Workspace created successfully", + "200": { + "description": "Spend limit updated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkspaceResponse" + "$ref": "#/components/schemas/ApiKeyResponse" } } } @@ -8685,7 +9051,7 @@ } }, "403": { - "description": "Forbidden - not a member of organization", + "description": "Forbidden - not authorized to update this key", "content": { "application/json": { "schema": { @@ -8694,8 +9060,8 @@ } } }, - "409": { - "description": "Workspace name already exists in organization", + "404": { + "description": "API key or workspace not found", "content": { "application/json": { "schema": { @@ -8721,4710 +9087,1389 @@ } ] } - }, - "/v1/privacy/classify": { - "post": { - "tags": [ - "Privacy" + } + }, + "components": { + "schemas": { + "AcceptInvitationResponse": { + "type": "object", + "description": "Accept invitation response", + "required": [ + "organization_member", + "message" ], - "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": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "properties": { + "message": { + "type": "string" }, - "404": { - "description": "Model not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "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" }, - "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" - } - } - } + "refresh_token": { + "type": "string" }, - "500": { - "description": "Server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "refresh_token_expiration": { + "type": "string", + "format": "date-time" } - }, - "security": [ - { - "api_key": [] + } + }, + "AddOrganizationMemberRequest": { + "type": "object", + "description": "Request to add an organization member", + "required": [ + "user_id", + "role" + ], + "properties": { + "role": { + "$ref": "#/components/schemas/MemberRole" + }, + "user_id": { + "type": "string" } - ] - } - }, - "/v1/privacy/redact": { - "post": { - "tags": [ - "Privacy" + } + }, + "AdminAccessTokenResponse": { + "type": "object", + "description": "Admin access token response model", + "required": [ + "id", + "access_token", + "created_by_user_id", + "created_at", + "expires_at", + "name", + "reason" ], - "operationId": "privacy_redact", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PrivacyRedactRequestDoc" - } - } + "properties": { + "access_token": { + "type": "string" }, - "required": true - }, - "responses": { - "200": { - "description": "Privacy redaction completed successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PrivacyRedactResponseDoc" - } - } - } + "created_at": { + "type": "string", + "format": "date-time" }, - "400": { - "description": "Invalid request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "created_by_user_id": { + "type": "string" }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "expires_at": { + "type": "string", + "format": "date-time" }, - "404": { - "description": "Model not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "id": { + "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": { + "type": "string" }, - "500": { - "description": "Server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "api_key": [] + "reason": { + "type": "string" } - ] - } - }, - "/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/RerankRequest" - } - } + } + }, + "AdminFeatureRequestListResponse": { + "type": "object", + "required": [ + "requests", + "limit", + "offset", + "total" + ], + "properties": { + "limit": { + "type": "integer", + "format": "int64" }, - "required": true - }, - "responses": { - "200": { - "description": "Successful rerank", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RerankResponse" - } - } - } + "offset": { + "type": "integer", + "format": "int64" }, - "400": { - "description": "Invalid request (empty documents, invalid model, etc.)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "requests": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminFeatureRequestSummaryResponse" } }, - "401": { - "description": "Unauthorized (missing or invalid API key)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "total": { + "type": "integer", + "format": "int64" + } + } + }, + "AdminFeatureRequestSummaryResponse": { + "type": "object", + "required": [ + "target", + "uniqueUserCount", + "uniqueOrganizationCount", + "latestRequestedAt", + "recentVotes" + ], + "properties": { + "latestRequestedAt": { + "type": "string", + "format": "date-time" }, - "404": { - "description": "Model not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "recentVotes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FeatureRequestVoteResponse" } }, - "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" - } - } - } + "target": { + "$ref": "#/components/schemas/FeatureRequestTargetResponse" }, - "500": { - "description": "Server error (billing failure, provider error)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "ApiKeyAuth": [] + "uniqueOrganizationCount": { + "type": "integer", + "format": "int64" + }, + "uniqueUserCount": { + "type": "integer", + "format": "int64" } - ] - } - }, - "/v1/responses": { - "post": { - "tags": [ - "Responses" + } + }, + "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" ], - "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" - } - } + "properties": { + "created_at": { + "type": "string", + "format": "date-time" }, - "required": true - }, - "responses": { - "200": { - "description": "Response created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResponseObject" - } - } - } + "email_last_error": { + "type": [ + "string", + "null" + ] }, - "400": { - "description": "Invalid request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "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" }, - "402": { - "description": "Insufficient credits", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "email_status": { + "$ref": "#/components/schemas/InvitationEmailStatus" }, - "500": { - "description": "Server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "api_key": [] - } - ] - } - }, - "/v1/responses/{response_id}": { - "get": { - "tags": [ - "Responses" - ], - "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" - } - } - ], - "responses": { - "200": { - "description": "Response details", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResponseObject" - } - } - } + "expires_at": { + "type": "string", + "format": "date-time" }, - "401": { - "description": "Invalid or missing API key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "invitation_id": { + "type": "string" }, - "404": { - "description": "Response not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "invitation_status": { + "$ref": "#/components/schemas/InvitationStatus" }, - "501": { - "description": "Not implemented", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "api_key": [] + "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" } - ] + } }, - "delete": { - "tags": [ - "Responses" - ], - "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" - } - } + "AdminInvitationEmailResendResultResponse": { + "type": "object", + "description": "Admin invitation email resend result.", + "required": [ + "invitation_id", + "recipient_email", + "success", + "email_sent", + "email_status" ], - "responses": { - "200": { - "description": "Response deleted successfully" + "properties": { + "email_last_error": { + "type": [ + "string", + "null" + ] }, - "401": { - "description": "Invalid or missing API key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "email_message_id": { + "type": [ + "string", + "null" + ] }, - "404": { - "description": "Response not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "email_sent": { + "type": "boolean" }, - "501": { - "description": "Not implemented", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "api_key": [] + "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" } - ] - } - }, - "/v1/responses/{response_id}/cancel": { - "post": { - "tags": [ - "Responses" + } + }, + "AdminModelListResponse": { + "type": "object", + "description": "Response for admin model list endpoint", + "required": [ + "models", + "limit", + "offset", + "total" ], - "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" + "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" ], - "responses": { - "200": { - "description": "Response cancelled successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResponseObject" - } + "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." } - } + ] }, - "401": { - "description": "Invalid or missing API key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "costPerImage": { + "$ref": "#/components/schemas/DecimalPrice" }, - "404": { - "description": "Response not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "createdAt": { + "type": "string", + "format": "date-time" }, - "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" - } + "inputCostPerToken": { + "$ref": "#/components/schemas/DecimalPrice" + }, + "isActive": { + "type": "boolean" + }, + "metadata": { + "$ref": "#/components/schemas/ModelMetadata" + }, + "modelId": { + "type": "string" + }, + "outputCostPerToken": { + "$ref": "#/components/schemas/DecimalPrice" + }, + "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" ], - "responses": { - "200": { - "description": "List of input items", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResponseInputItemList" - } - } - } + "properties": { + "id": { + "type": "string" }, - "400": { - "description": "Invalid response ID", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "invited_by": { + "type": [ + "string", + "null" + ] }, - "401": { - "description": "Invalid or missing API key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "joined_at": { + "type": "string", + "format": "date-time" }, - "404": { - "description": "Response not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "organization_id": { + "type": "string" }, - "500": { - "description": "Server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "api_key": [] + "role": { + "$ref": "#/components/schemas/MemberRole" + }, + "user": { + "$ref": "#/components/schemas/AdminUserResponse" } - ] - } - }, - "/v1/score": { - "post": { - "tags": [ - "Score" + } + }, + "AdminOrganizationResponse": { + "type": "object", + "description": "Organization details for admin organization listing", + "required": [ + "id", + "name", + "created_at" ], - "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" - } - } + "properties": { + "created_at": { + "type": "string", + "format": "date-time" }, - "required": true - }, - "responses": { - "200": { - "description": "Successful score", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ScoreResponse" - } + "currentUsage": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/OrganizationUsage" } - } + ] }, - "400": { - "description": "Invalid request (empty texts, invalid model, etc.)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "description": { + "type": [ + "string", + "null" + ] }, - "401": { - "description": "Unauthorized (missing or invalid API key)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "id": { + "type": "string" }, - "404": { - "description": "Model not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "name": { + "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" - } + "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" }, - "500": { - "description": "Server error (billing failure, provider error)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "offset": { + "type": "integer", + "format": "int64" + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminServiceResponse" } + }, + "total": { + "type": "integer", + "format": "int64" } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - } - }, - "/v1/services": { - "get": { - "tags": [ - "Services" + } + }, + "AdminServiceResponse": { + "type": "object", + "description": "Platform service (admin CRUD) β€” single item", + "required": [ + "id", + "serviceName", + "displayName", + "unit", + "costPerUnit", + "isActive", + "createdAt", + "updatedAt" ], - "summary": "List platform services (public, no auth)", - "operationId": "list_services", - "parameters": [ - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } + "properties": { + "costPerUnit": { + "type": "integer", + "format": "int64", + "description": "Price per unit in nano-USD (scale 9)." }, - { - "name": "offset", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "Services list", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServiceListResponse" - } - } - } + "createdAt": { + "type": "string", + "format": "date-time" }, - "400": { - "description": "Invalid parameters", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "description": { + "type": [ + "string", + "null" + ] }, - "500": { - "description": "Internal server error", - "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" } } - } - }, - "/v1/services/{service_name}": { - "get": { - "tags": [ - "Services" - ], - "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" - } - } + }, + "AdminUserOrganizationDetails": { + "type": "object", + "description": "Organization details with spend limit and usage (for admin user listing)", + "required": [ + "id", + "name" ], - "responses": { - "200": { - "description": "Service details", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServiceResponse" - } + "properties": { + "currentUsage": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/OrganizationUsage" } - } + ] }, - "404": { - "description": "Service not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "description": { + "type": [ + "string", + "null" + ] }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "spendLimit": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SpendLimit" } - } + ] } } - } - }, - "/v1/signature/{chat_id}": { - "get": { - "tags": [ - "Attestation" + }, + "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" ], - "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" - } + "properties": { + "auth_provider": { + "type": "string" }, - { - "name": "model", - "in": "query", - "required": false, - "schema": { - "type": [ - "string", - "null" - ] - } + "avatar_url": { + "type": [ + "string", + "null" + ] }, - { - "name": "signing_algo", - "in": "query", - "required": false, - "schema": { - "type": [ - "string", - "null" - ] - } - } - ], - "responses": { - "200": { - "description": "Signature retrieved or unavailable due to disconnect", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SignatureResponse" - } - } - } + "created_at": { + "type": "string", + "format": "date-time" }, - "400": { - "description": "Invalid parameters", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "display_name": { + "type": [ + "string", + "null" + ] }, - "404": { - "description": "Signature not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "api_key": [] - } - ] - } - }, - "/v1/staking/farm/config": { - "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", - "responses": { - "200": { - "description": "Staking farm configuration", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StakingFarmConfigResponse" - } - } - } + "email": { + "type": "string" }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "id": { + "type": "string" }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "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" + ] } - }, - "security": [ - { - "session_token": [] - } - ] - } - }, - "/v1/users/me": { - "get": { - "tags": [ - "Users" + } + }, + "ApiKeyResponse": { + "type": "object", + "description": "API Key response model", + "required": [ + "id", + "key_prefix", + "workspace_id", + "created_by_user_id", + "created_at", + "is_active" ], - "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" - } - } - } + "properties": { + "created_at": { + "type": "string", + "format": "date-time" }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "created_by_user_id": { + "type": "string" }, - "404": { - "description": "User not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } + "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" } - } + ] }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } + "usage": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/DecimalPrice" } - } - } - }, - "security": [ - { - "session_token": [] + ] + }, + "workspace_id": { + "type": "string" } - ] + } }, - "patch": { - "tags": [ - "Users" + "AttestationResponse": { + "type": "object", + "required": [ + "gateway_attestation" ], - "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" + "properties": { + "gateway_attestation": { + "$ref": "#/components/schemas/DstackCpuQuote" + }, + "model_attestations": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": {}, + "propertyNames": { + "type": "string" } } }, - "required": true - }, - "responses": { - "200": { - "description": "Updated user profile", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserResponse" - } + "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." }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "session_token": [] + "tls_certificate": { + "type": [ + "string", + "null" + ], + "description": "TLS certificate file (PEM) from TLS_CERT_PATH; report_data binds via SHA256 of these exact bytes" } - ] - } - }, - "/v1/users/me/access-tokens": { - "post": { - "tags": [ - "Users" + } + }, + "AudioTranscriptionRequestSchema": { + "type": "object", + "description": "Audio transcription request schema for OpenAPI documentation\nThis represents the multipart/form-data fields", + "required": [ + "file", + "model" ], - "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" - } - } - } + "properties": { + "file": { + "type": "string", + "description": "Audio file (required) - binary audio data" }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "language": { + "type": [ + "string", + "null" + ], + "description": "Language code (optional) - e.g. \"en\", \"es\"" }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "refresh_token": [] + "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." } - ] - } - }, - "/v1/users/me/invitations": { - "get": { - "tags": [ - "Users" + } + }, + "AudioTranscriptionResponse": { + "type": "object", + "description": "Audio transcription response (with OpenAPI schema)", + "required": [ + "text" ], - "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" - } - } - } - } + "properties": { + "duration": { + "type": [ + "number", + "null" + ], + "format": "double", + "description": "Total audio duration in seconds" }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "language": { + "type": [ + "string", + "null" + ], + "description": "Detected language code" }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "session_token": [] + "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" } - ] - } - }, - "/v1/users/me/invitations/{invitation_id}/accept": { - "post": { - "tags": [ - "Users" + } + }, + "BillingCostsRequest": { + "type": "object", + "description": "Billing costs request (HuggingFace compatible)", + "required": [ + "requestIds" ], - "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": { + "properties": { + "requestIds": { + "type": "array", + "items": { "type": "string", "format": "uuid" - } + }, + "description": "Array of request IDs (inference IDs) to get costs for" } - ], - "responses": { - "200": { - "description": "Invitation accepted successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AcceptInvitationResponse" - } - } - } + } + }, + "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" }, - "400": { - "description": "Bad request - invitation expired or invalid", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "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" + } + } + }, + "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" }, - "403": { - "description": "Forbidden - invitation belongs to another user", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "active_paid_credit_limit_usd": { + "type": "number", + "format": "double", + "description": "Sum of active payment-type spend limits (caps), USD" }, - "404": { - "description": "Invitation not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "by_source": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BillingSourceBreakdown" } }, - "409": { - "description": "User is already a member", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "generated_at": { + "type": "string", + "format": "date-time" }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "session_token": [] + "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." } - ] - } - }, - "/v1/users/me/invitations/{invitation_id}/decline": { - "post": { - "tags": [ - "Users" + } + }, + "ChatChoice": { + "type": "object", + "required": [ + "index", + "message" ], - "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" - } + "properties": { + "finish_reason": { + "type": [ + "string", + "null" + ] + }, + "index": { + "type": "integer", + "format": "int64" + }, + "message": { + "$ref": "#/components/schemas/Message" } + } + }, + "ChatCompletionRequest": { + "type": "object", + "required": [ + "model", + "messages" ], - "responses": { - "200": { - "description": "Invitation declined successfully" + "properties": { + "frequency_penalty": { + "type": [ + "number", + "null" + ], + "format": "float" }, - "400": { - "description": "Bad request - invitation not pending", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "max_tokens": { + "type": [ + "integer", + "null" + ], + "format": "int64" }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" } }, - "403": { - "description": "Forbidden - invitation belongs to another user", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "model": { + "type": "string" }, - "404": { - "description": "Invitation 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/users/me/refresh-tokens": { - "get": { - "tags": [ - "Users" - ], - "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" - } - } - } - } + "n": { + "type": [ + "integer", + "null" + ], + "format": "int64" }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "presence_penalty": { + "type": [ + "number", + "null" + ], + "format": "float" }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } + "stop": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/StopSequences", + "description": "OpenAI `stop` accepts either a single string or an array of strings." } - } - } - }, - "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": "refresh_token_id", - "in": "path", - "description": "Refresh token ID to revoke", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "204": { - "description": "Refresh token revoked successfully" + ] }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "stream": { + "type": [ + "boolean", + "null" + ] }, - "404": { - "description": "Refresh token not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "temperature": { + "type": [ + "number", + "null" + ], + "format": "float" }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "top_p": { + "type": [ + "number", + "null" + ], + "format": "float" } }, - "security": [ - { - "session_token": [] - } - ] - } - }, - "/v1/users/me/status": { - "get": { - "tags": [ - "Users" + "additionalProperties": {} + }, + "ChatCompletionResponse": { + "type": "object", + "required": [ + "id", + "object", + "created", + "model", + "choices", + "usage" ], - "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" - } - } + "properties": { + "choices": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatChoice" } }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "created": { + "type": "integer", + "format": "int64" }, - "403": { - "description": "Current account is not eligible", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "id": { + "type": "string" }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "model": { + "type": "string" + }, + "object": { + "type": "string" + }, + "usage": { + "$ref": "#/components/schemas/CompletionUsage" } }, - "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", - "content": { - "application/json": { - "schema": {} - } - } - }, - "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": [ - { - "session_token": [] - } - ] - } - }, - "/v1/workspaces/{workspace_id}": { - "get": { - "tags": [ - "Workspaces" - ], - "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" - } - } - } - }, - "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": [] - } - ] - }, - "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": [] - } - ] + "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." + } }, - "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" - } - } + "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" ], - "responses": { - "200": { - "description": "Workspace deleted successfully" - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "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." }, - "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" - }, - "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" - ] - } - } - }, - { - "type": "object", - "required": [ - "id", - "response_id", - "created_at", - "server_label", - "name", - "arguments", - "model", - "type" - ], - "properties": { - "arguments": { - "type": "string" - }, - "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" - ] - } + "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": "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": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + { + "type": "array", + "items": { + "type": "array", + "items": { "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" - ] } } } ], - "description": "Conversation item (for responses)" + "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." }, - "ConversationItemFunction": { + "CompletionRequest": { "type": "object", "required": [ - "name", - "arguments" + "model", + "prompt" ], "properties": { - "arguments": { - "type": "string" + "best_of": { + "type": [ + "integer", + "null" + ], + "format": "int64" }, - "name": { + "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" + }, + "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": {} }, - "ConversationItemList": { + "CompletionResponse": { "type": "object", - "description": "List of conversation items", "required": [ + "id", "object", - "data", - "first_id", - "last_id", - "has_more" + "created", + "model", + "choices", + "usage" ], "properties": { - "data": { + "choices": { "type": "array", "items": { - "$ref": "#/components/schemas/ConversationItem" + "$ref": "#/components/schemas/CompletionChoice" } }, - "first_id": { - "type": "string" + "created": { + "type": "integer", + "format": "int64" }, - "has_more": { - "type": "boolean" + "id": { + "type": "string" }, - "last_id": { + "model": { "type": "string" }, "object": { "type": "string" + }, + "usage": { + "$ref": "#/components/schemas/CompletionUsage" } } }, - "ConversationItemWebSearchAction": { - "oneOf": [ - { - "type": "object", - "required": [ - "query", - "type" - ], - "properties": { - "query": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "search" - ] - } - } - } - ] - }, - "ConversationObject": { + "CompletionUsage": { "type": "object", - "description": "Conversation object (follows OpenAI spec)", + "description": "Usage for chat/completions endpoints.\nSerializes as prompt_tokens, completion_tokens, prompt_tokens_details, completion_tokens_details, total_tokens.", "required": [ - "id", - "object", - "created_at", - "metadata" + "prompt_tokens", + "completion_tokens", + "total_tokens" ], "properties": { - "created_at": { + "completion_tokens": { + "type": "integer", + "format": "int32" + }, + "completion_tokens_details": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/OutputTokensDetails" + } + ] + }, + "prompt_tokens": { "type": "integer", - "format": "int64" + "format": "int32" }, - "id": { - "type": "string" + "prompt_tokens_details": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/InputTokensDetails" + } + ] }, - "metadata": {}, - "object": { - "type": "string" + "total_tokens": { + "type": "integer", + "format": "int32" } } }, - "ConversationReference": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "string" - }, - "metadata": {} - } - } - ], - "description": "Conversation reference" - }, "ConversationResponseReference": { "type": "object", "description": "Conversation reference in response object", @@ -13489,279 +10534,103 @@ } } }, - "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", "required": [ "name" - ], - "properties": { - "description": { - "type": [ - "string", - "null" - ] - }, - "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" - ], - "description": "Responses is Standard-only for now. `auto` is accepted but normalized\nto Standard before the internal Chat Completions call." - }, - "store": { - "type": [ - "boolean", - "null" - ] - }, - "stream": { + ], + "properties": { + "description": { "type": [ - "boolean", + "string", "null" ] }, - "temperature": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "CreateReportingTokenRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "expires_at": { "type": [ - "number", + "string", "null" ], - "format": "float" + "format": "date-time", + "description": "Optional expiration time. Must be in the future." }, - "tool_choice": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/ResponseToolChoice" - } - ] + "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" }, - "tools": { + "created_by_user_id": { + "type": "string", + "format": "uuid" + }, + "expires_at": { "type": [ - "array", + "string", "null" ], - "items": { - "$ref": "#/components/schemas/ResponseTool" - } + "format": "date-time" }, - "top_p": { + "id": { + "type": "string", + "format": "uuid" + }, + "last_used_at": { "type": [ - "number", + "string", "null" ], - "format": "float" + "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." } } }, @@ -14004,7 +10873,7 @@ }, "report_data": { "type": "string", - "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)." + "description": "The report data that contains signing address and nonce" }, "request_nonce": { "type": "string", @@ -14088,23 +10957,6 @@ } } }, - "ExpiresAfter": { - "type": "object", - "description": "Expires after configuration", - "required": [ - "anchor", - "seconds" - ], - "properties": { - "anchor": { - "type": "string" - }, - "seconds": { - "type": "integer", - "format": "int64" - } - } - }, "FeatureRequestKind": { "type": "string", "enum": [ @@ -14202,102 +11054,6 @@ } } }, - "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": [ @@ -14571,13 +11327,6 @@ }, "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", @@ -14784,11 +11533,6 @@ "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" ], @@ -14798,11 +11542,6 @@ "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" @@ -14815,27 +11554,11 @@ "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" @@ -14844,11 +11567,6 @@ "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" @@ -15099,77 +11817,17 @@ "$ref": "#/components/schemas/ItaModelTokenItem" } }, - "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" + "nonce": { + "type": "string" }, - "reports": { + "policy_ids": { "type": "array", "items": { - "$ref": "#/components/schemas/AdminAmlReportResponse" + "type": "string" } }, - "total": { - "type": "integer", - "format": "int64" + "policy_must_match": { + "type": "boolean" } } }, @@ -15426,7 +12084,8 @@ "enum": [ "always", "never" - ] + ], + "description": "Simple MCP approval mode" }, "McpApprovalRequirement": { "oneOf": [ @@ -15444,14 +12103,8 @@ } } } - ] - }, - "McpApprovalResponseType": { - "type": "string", - "description": "Type marker for MCP approval response input", - "enum": [ - "mcp_approval_response" - ] + ], + "description": "Approval requirement for MCP tool calls" }, "McpDiscoveredTool": { "type": "object", @@ -15472,13 +12125,6 @@ } } }, - "McpListToolsType": { - "type": "string", - "description": "Type marker for MCP list tools input", - "enum": [ - "mcp_list_tools" - ] - }, "McpToolNameFilter": { "type": "object", "required": [ @@ -15492,7 +12138,8 @@ }, "uniqueItems": true } - } + }, + "description": "Filter for tool names that don't require approval" }, "MemberRole": { "type": "string", @@ -15873,22 +12520,6 @@ } } }, - "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", @@ -16082,7 +12713,6 @@ "type": "string" } }, - "textPricing": {}, "verifiable": { "type": "boolean" } @@ -16281,9 +12911,6 @@ }, "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": [ { @@ -16703,8 +13330,7 @@ }, "outputCostPerToken": { "$ref": "#/components/schemas/DecimalPrice" - }, - "textPricing": {} + } } }, "ModelsResponse": { @@ -17056,23 +13682,6 @@ } } }, - "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", @@ -17668,12 +14277,6 @@ "$ref": "#/components/schemas/DecimalPriceRequest" } ] - }, - "textPricing": { - "type": [ - "object", - "null" - ] } } }, @@ -17787,8 +14390,7 @@ "$ref": "#/components/schemas/DecimalPrice" } ] - }, - "textPricing": {} + } } }, "PricingFields": { @@ -17818,8 +14420,7 @@ }, "outputCostPerToken": { "$ref": "#/components/schemas/DecimalPrice" - }, - "textPricing": {} + } } }, "PrivacyClassifyRequestDoc": { @@ -18139,7 +14740,6 @@ "output_tokens", "total_tokens", "cache_read_tokens", - "cache_write_tokens", "input_cost", "output_cost", "total_cost", @@ -18153,11 +14753,6 @@ "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)" @@ -19025,20 +15620,6 @@ } } }, - "ResponseContent": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResponseContentPart" - } - } - ], - "description": "Content can be text or array of content parts" - }, "ResponseContentItem": { "oneOf": [ { @@ -19189,77 +15770,6 @@ ], "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": [ @@ -19316,68 +15826,6 @@ } } }, - "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": [ @@ -19541,7 +15989,7 @@ "tools": { "type": "array", "items": { - "$ref": "#/components/schemas/ResponseTool" + "$ref": "#/components/schemas/ClientManagedResponseTool" } }, "top_logprobs": { @@ -20068,6 +16516,13 @@ "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": [ @@ -20279,28 +16734,11 @@ "type" ], "properties": { - "filters": {}, - "search_context_size": { - "type": [ - "string", - "null" - ] - }, "type": { "type": "string", "enum": [ "web_search" ] - }, - "user_location": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/UserLocation" - } - ] } } }, @@ -20362,7 +16800,6 @@ }, { "type": "object", - "description": "Remote MCP server tool", "required": [ "server_label", "server_url", @@ -20378,9 +16815,14 @@ "type": "string" } }, + "authorization": { + "type": [ + "string", + "null" + ] + }, "require_approval": { - "$ref": "#/components/schemas/McpApprovalRequirement", - "description": "Tool approval requirement (default: \"always\")" + "$ref": "#/components/schemas/McpApprovalRequirement" }, "server_description": { "type": [ @@ -20392,8 +16834,7 @@ "type": "string" }, "server_url": { - "type": "string", - "description": "HTTPS endpoint for the remote MCP server" + "type": "string" }, "type": { "type": "string", @@ -21336,18 +17777,6 @@ } } }, - "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)", @@ -21399,13 +17828,6 @@ } } }, - "UpdateConversationRequest": { - "type": "object", - "description": "Request to update a conversation", - "properties": { - "metadata": {} - } - }, "UpdateModelApiRequest": { "type": "object", "description": "Request to update model pricing (admin endpoint)", @@ -21627,13 +18049,6 @@ }, "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", @@ -21682,18 +18097,6 @@ } } }, - "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)", @@ -21849,39 +18252,21 @@ }, "UpdateWorkspaceRequest": { "type": "object", - "description": "Request to update a workspace", - "properties": { - "description": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": [ - "string", - "null" - ] - }, - "settings": {} - } - }, - "UpsertAmlAllowlistEntryRequest": { - "type": "object", - "description": "Admin AML allowlist upsert request.", - "required": [ - "account_id" - ], + "description": "Request to update a workspace", "properties": { - "account_id": { - "type": "string" + "description": { + "type": [ + "string", + "null" + ] }, - "reason": { + "name": { "type": [ "string", "null" ] - } + }, + "settings": {} } }, "Usage": { @@ -22001,7 +18386,6 @@ "input_tokens", "output_tokens", "cache_read_tokens", - "cache_write_tokens", "total_tokens", "total_cost", "total_cost_display", @@ -22012,23 +18396,11 @@ "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" }, @@ -22078,12 +18450,6 @@ ], "description": "Response ID when called from Responses API" }, - "serviceTier": { - "type": [ - "string", - "null" - ] - }, "stop_reason": { "type": [ "string", @@ -22137,42 +18503,6 @@ } } }, - "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": [ @@ -22529,107 +18859,463 @@ "name": { "type": "string" }, - "organization_id": { - "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" + } } - } + ] }, - "VerifyRequest": { - "type": "object", - "properties": { - "request_hash": { - "type": [ - "string", - "null" - ] - } - } + "FunctionCallType": { + "type": "string", + "description": "Type marker for a client-replayed function call input item.", + "enum": [ + "function_call" + ] }, - "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" + "StatelessResponseToolSchema": { + "oneOf": [ + { + "type": "object", + "required": [ + "name", + "type" ], - "description": "VPC server app ID" + "properties": { + "description": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "parameters": {}, + "type": { + "type": "string", + "enum": [ + "function" + ] + } + } } - } + ] }, - "WebSearchAction": { + "StatelessResponseContentPartSchema": { "oneOf": [ { "type": "object", "required": [ - "query", + "text", "type" ], "properties": { - "query": { + "text": { "type": "string" }, "type": { "type": "string", "enum": [ - "search" + "input_text" + ] + } + } + }, + { + "type": "object", + "required": [ + "text", + "type" + ], + "properties": { + "text": { + "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" ] } } } - ], - "description": "Web search action details" + ] }, - "WorkspaceResponse": { + "StatelessCreateResponseRequestSchema": { "type": "object", - "description": "Workspace response model", + "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.", "required": [ - "id", - "name", - "organization_id", - "created_by_user_id", - "created_at", - "updated_at", - "is_active" + "model" ], "properties": { - "created_at": { - "type": "string", - "format": "date-time" + "include": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } }, - "created_by_user_id": { - "type": "string" + "input": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/StatelessResponseInputSchema" + } + ] }, - "description": { + "instructions": { "type": [ "string", "null" ] }, - "id": { - "type": "string" + "max_output_tokens": { + "type": [ + "integer", + "null" + ], + "format": "int64" }, - "is_active": { - "type": "boolean" + "max_tool_calls": { + "type": [ + "integer", + "null" + ], + "format": "int64" }, - "name": { + "metadata": {}, + "model": { "type": "string" }, - "organization_id": { - "type": "string" + "parallel_tool_calls": { + "type": [ + "boolean", + "null" + ] }, - "settings": {}, - "updated_at": { - "type": "string", - "format": "date-time" + "prompt_cache_key": { + "type": [ + "string", + "null" + ] + }, + "reasoning": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ResponseReasoningConfig" + } + ] + }, + "safety_identifier": { + "type": [ + "string", + "null" + ] + }, + "service_tier": { + "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" + ] + }, + "temperature": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "tool_choice": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ResponseToolChoice" + } + ] + }, + "tools": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/StatelessResponseToolSchema" + } + }, + "top_p": { + "type": [ + "number", + "null" + ], + "format": "float" } } } @@ -22698,13 +19384,9 @@ "name": "Models", "description": "Public model catalog and information" }, - { - "name": "Conversations", - "description": "Conversation management" - }, { "name": "Responses", - "description": "Response handling and streaming" + "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." }, { "name": "Organizations", @@ -22718,10 +19400,6 @@ "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/docs.json b/docs.json index 5e6bd91..da3a14f 100644 --- a/docs.json +++ b/docs.json @@ -165,11 +165,7 @@ { "group": "Responses", "pages": [ - "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" + "POST /v1/responses" ] }, { @@ -346,11 +342,6 @@ "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", @@ -367,8 +358,6 @@ "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", @@ -387,32 +376,6 @@ "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" - ] } ] } From 7d9e8eda06b0db549d4691847e1de8c996ba9a94 Mon Sep 17 00:00:00 2001 From: Coffee Date: Wed, 23 Sep 2026 13:31:40 +0800 Subject: [PATCH 33/36] docs: mark direct endpoints experimental --- cloud/guides/e2ee-chat-completions.mdx | 74 ++-------------- cloud/guides/integrations/aider-zed.mdx | 1 - cloud/guides/integrations/cline-roo-kilo.mdx | 1 - cloud/guides/integrations/continue.mdx | 1 - cloud/guides/integrations/cursor.mdx | 1 - cloud/guides/integrations/dify.mdx | 1 - cloud/guides/integrations/index.mdx | 2 +- cloud/guides/integrations/librechat.mdx | 1 - cloud/guides/integrations/litellm.mdx | 1 - cloud/guides/integrations/model-discovery.mdx | 58 +------------ cloud/guides/integrations/open-webui.mdx | 1 - cloud/guides/openai-compatibility.mdx | 17 +--- cloud/guides/opencode-goose.mdx | 1 - cloud/guides/specialized-endpoints.mdx | 8 +- cloud/models.mdx | 2 +- cloud/private-inference.mdx | 86 ++----------------- cloud/quickstart.mdx | 82 +----------------- cloud/verification/cloud-api/index.mdx | 2 +- .../verification/direct/image-provenance.mdx | 8 +- cloud/verification/direct/index.mdx | 22 +++-- .../verification/direct/model-attestation.mdx | 14 +-- .../direct/response-signatures.mdx | 14 +-- cloud/verification/direct/tls.mdx | 8 +- cloud/verification/index.mdx | 19 ++-- .../reference/quote-nonce-signer.mdx | 3 +- .../response-signature-verification.mdx | 1 - docs.json | 10 --- 27 files changed, 75 insertions(+), 364 deletions(-) diff --git a/cloud/guides/e2ee-chat-completions.mdx b/cloud/guides/e2ee-chat-completions.mdx index d1ad8dc..3a1571a 100644 --- a/cloud/guides/e2ee-chat-completions.mdx +++ b/cloud/guides/e2ee-chat-completions.mdx @@ -46,29 +46,6 @@ sequenceDiagram Note over Client: Decrypt with private key ``` -### Via Direct Completions - -With [direct completions](/cloud/private-inference#direct-completions), E2EE is simpler β€” encrypted data goes straight to the model's TEE with no intermediate gateway. - -```mermaid -sequenceDiagram - participant Client - participant ModelTEE as Model TEE (qwen35-122b.completions.near.ai) - - Client->>ModelTEE: GET /v1/attestation/report - Note over ModelTEE: Generate attestation report
with model's public key - ModelTEE->>Client: Model public key + attestation - - Note over Client: Generate ephemeral key pair - Note over Client: Encrypt messages with model's public key - - Client->>ModelTEE: POST /v1/chat/completions
with encryption headers - Note over ModelTEE: Decrypt with private key
Process with LLM
Encrypt response with client's public key - ModelTEE->>Client: Encrypted response - - Note over Client: Decrypt with private key -``` - --- ## Encryption Protocol @@ -187,9 +164,9 @@ 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. 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). +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. 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. See [NEAR model attestations](/cloud/verification/cloud-api/model-attestations). -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. +The gateway endpoint requires an API key (`Authorization: Bearer `); report retrieval is free and never counts against your usage. @@ -207,18 +184,6 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -H 'Authorization: Bearer ' ``` - - - -```bash -# Direct completions β€” no model parameter needed, the endpoint serves one model -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}" -``` - @@ -244,12 +209,6 @@ response = requests.get( ) response.raise_for_status() -# For direct completions: -# response = requests.get( -# "https://qwen35-122b.completions.near.ai/v1/attestation/report", -# params={"signing_algo": "ed25519", "nonce": nonce} -# ) - attestation = response.json() # NEAR AI Cloud gateway: model_reports = attestation.get("model_attestations", []) @@ -259,8 +218,6 @@ if not model_reports: # 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"] ``` @@ -301,7 +258,7 @@ console.log(`Retrieved ${modelReports.length} NEAR model-attestation candidate(s -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`. +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`. --- @@ -482,27 +439,6 @@ curl https://cloud-api.near.ai/v1/chat/completions \ }' ``` - - - -```bash -# Direct completions β€” no X-Model-Pub-Key needed -curl https://qwen35-122b.completions.near.ai/v1/chat/completions \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer YOUR_API_KEY" \ - -H "X-Signing-Algo: ed25519" \ - -H "X-Client-Pub-Key: YOUR_ED25519_PUBLIC_KEY_HEX" \ - -H "X-Encryption-Version: 2" \ - -d '{ - "model": "Qwen/Qwen3.5-122B-A10B", - "messages": [{ - "role": "user", - "content": "ENCRYPTED_CONTENT_HEX" - }], - "stream": false - }' -``` - @@ -755,7 +691,7 @@ E2EE is supported on the **Chat Completions API** (`/v1/chat/completions`), **Co ### Verification -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). +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. See [Verification](/cloud/verification) for the Gateway verification flow. --- @@ -885,5 +821,5 @@ Before using a model public key for encryption, verify the attestation report an ## See Also - [Private Inference](/cloud/private-inference) β€” How TEE isolation protects your data -- [Verification](/cloud/verification) β€” Select NEAR AI Cloud gateway or direct model attestation verification +- [Verification](/cloud/verification) β€” Verify NEAR AI Cloud Gateway requests - [Verification Policy](/cloud/verification/reference/verification-policy) β€” Define the evidence your application requires diff --git a/cloud/guides/integrations/aider-zed.mdx b/cloud/guides/integrations/aider-zed.mdx index ff96f1a..3feb5e8 100644 --- a/cloud/guides/integrations/aider-zed.mdx +++ b/cloud/guides/integrations/aider-zed.mdx @@ -150,7 +150,6 @@ If you only set `OPENAI_API_KEY` for Aider, either export `NEARAI_API_KEY="$OPEN - [Model Discovery and Refresh](/cloud/guides/integrations/model-discovery) - [OpenAI Compatibility](/cloud/guides/openai-compatibility) - [Available Models](/cloud/models) -- [Private Inference - Direct Completions](/cloud/private-inference#direct-completions) - [Continue](/cloud/guides/integrations/continue) - [Cline, Roo Code, and Kilo Code](/cloud/guides/integrations/cline-roo-kilo) diff --git a/cloud/guides/integrations/cline-roo-kilo.mdx b/cloud/guides/integrations/cline-roo-kilo.mdx index d2c1c32..c3f017a 100644 --- a/cloud/guides/integrations/cline-roo-kilo.mdx +++ b/cloud/guides/integrations/cline-roo-kilo.mdx @@ -164,7 +164,6 @@ Sources checked on 2026-06-23: Cline OpenAI Compatible provider docs, Roo Code O - [Model Discovery and Refresh](/cloud/guides/integrations/model-discovery) - [OpenAI Compatibility](/cloud/guides/openai-compatibility) -- [Private Inference - Direct Completions](/cloud/private-inference#direct-completions) - [Available Models](/cloud/models) - [Continue](/cloud/guides/integrations/continue) - [Aider and Zed](/cloud/guides/integrations/aider-zed) diff --git a/cloud/guides/integrations/continue.mdx b/cloud/guides/integrations/continue.mdx index 571088d..26ae0ef 100644 --- a/cloud/guides/integrations/continue.mdx +++ b/cloud/guides/integrations/continue.mdx @@ -130,7 +130,6 @@ models: - [Model Discovery and Refresh](/cloud/guides/integrations/model-discovery) - [OpenAI Compatibility](/cloud/guides/openai-compatibility) - [Available Models](/cloud/models) -- [Private Inference - Direct Completions](/cloud/private-inference#direct-completions) - [Cline, Roo Code, and Kilo Code](/cloud/guides/integrations/cline-roo-kilo) - [Aider and Zed](/cloud/guides/integrations/aider-zed) diff --git a/cloud/guides/integrations/cursor.mdx b/cloud/guides/integrations/cursor.mdx index 01f6418..176808d 100644 --- a/cloud/guides/integrations/cursor.mdx +++ b/cloud/guides/integrations/cursor.mdx @@ -129,7 +129,6 @@ If curl works, test Cursor with a plain text Chat or Plan prompt. If Cursor fail - [Model Discovery and Refresh](/cloud/guides/integrations/model-discovery) - [OpenAI Compatibility](/cloud/guides/openai-compatibility) -- [Private Inference - Direct Completions](/cloud/private-inference#direct-completions) - [Available Models](/cloud/models) - [Continue](/cloud/guides/integrations/continue) - [Cline, Roo Code, and Kilo Code](/cloud/guides/integrations/cline-roo-kilo) diff --git a/cloud/guides/integrations/dify.mdx b/cloud/guides/integrations/dify.mdx index b4a3272..5677a6c 100644 --- a/cloud/guides/integrations/dify.mdx +++ b/cloud/guides/integrations/dify.mdx @@ -128,7 +128,6 @@ If curl fails, fix the NEAR AI Cloud key, model ID, or base URL before changing - [Model Discovery and Refresh](/cloud/guides/integrations/model-discovery) - [OpenAI Compatibility](/cloud/guides/openai-compatibility) - [Available Models](/cloud/models) -- [Private Inference - Direct Completions](/cloud/private-inference#direct-completions) - [Open WebUI](/cloud/guides/integrations/open-webui) - [LibreChat](/cloud/guides/integrations/librechat) diff --git a/cloud/guides/integrations/index.mdx b/cloud/guides/integrations/index.mdx index 70cde8e..a79a86b 100644 --- a/cloud/guides/integrations/index.mdx +++ b/cloud/guides/integrations/index.mdx @@ -37,7 +37,7 @@ Use the OpenAI client guide when you are configuring code directly. Use the inte ## Model Discovery -NEAR AI Cloud can add new models over time. Use the [Model Discovery guide](/cloud/guides/integrations/model-discovery) to check the current model list, choose between gateway and direct completions endpoints, and copy the right model ID into clients that do not refresh automatically. +NEAR AI Cloud can add new models over time. Use the [Model Discovery guide](/cloud/guides/integrations/model-discovery) to check the current Gateway model list and copy the right model ID into clients that do not refresh automatically. ```bash curl https://cloud-api.near.ai/v1/models diff --git a/cloud/guides/integrations/librechat.mdx b/cloud/guides/integrations/librechat.mdx index 6a3c360..7456101 100644 --- a/cloud/guides/integrations/librechat.mdx +++ b/cloud/guides/integrations/librechat.mdx @@ -162,7 +162,6 @@ If curl fails, fix the NEAR AI Cloud key, model ID, or network path before chang - [Model Discovery and Refresh](/cloud/guides/integrations/model-discovery) - [OpenAI Compatibility](/cloud/guides/openai-compatibility) - [Available Models](/cloud/models) -- [Private Inference - Direct Completions](/cloud/private-inference#direct-completions) - [Open WebUI](/cloud/guides/integrations/open-webui) - [Dify](/cloud/guides/integrations/dify) - [LiteLLM Proxy](/cloud/guides/integrations/litellm) diff --git a/cloud/guides/integrations/litellm.mdx b/cloud/guides/integrations/litellm.mdx index 090edd4..aa2ea8c 100644 --- a/cloud/guides/integrations/litellm.mdx +++ b/cloud/guides/integrations/litellm.mdx @@ -186,7 +186,6 @@ If curl fails, fix the NEAR AI Cloud key, model ID, or network path before chang - [Model Discovery and Refresh](/cloud/guides/integrations/model-discovery) - [OpenAI Compatibility](/cloud/guides/openai-compatibility) - [Available Models](/cloud/models) -- [Private Inference - Direct Completions](/cloud/private-inference#direct-completions) - [LibreChat](/cloud/guides/integrations/librechat) - [Open WebUI](/cloud/guides/integrations/open-webui) diff --git a/cloud/guides/integrations/model-discovery.mdx b/cloud/guides/integrations/model-discovery.mdx index 2b0c79e..c346bc9 100644 --- a/cloud/guides/integrations/model-discovery.mdx +++ b/cloud/guides/integrations/model-discovery.mdx @@ -5,7 +5,7 @@ description: "Discover current NEAR AI Cloud models and refresh stale client mod --- -Use this guide when a client does not show a newly released NEAR AI Cloud model, or when you need to decide between the gateway model ID and a direct completions endpoint. +Use this guide when a client does not show a newly released NEAR AI Cloud model. Most tools should use: @@ -48,32 +48,7 @@ The response uses OpenAI-style model objects plus NEAR AI Cloud metadata. Import As checked on 2026-06-23, the live GLM 5.2 gateway record reports `top_provider.context_length: 500000`, `top_provider.max_completion_tokens: 131072`, and `max_output_length: 131072`. -## Discover direct completions endpoints - -Direct completions endpoints are listed at: - -```bash -curl https://completions.near.ai/endpoints -``` - -To find the GLM 5.2 direct completions endpoint: - -```bash -curl -fsSL https://completions.near.ai/endpoints \ - | jq '.endpoints[] | select(.domain == "glm-5-2.completions.near.ai")' -``` - -As checked on 2026-06-23, that endpoint is: - -```text -https://glm-5-2.completions.near.ai/v1 -``` - -The endpoints registry (`GET https://completions.near.ai/endpoints`) lists both `z-ai/glm-5.2` and `zai-org/GLM-5.2-FP8` for this domain, but the endpoint's own `GET https://glm-5-2.completions.near.ai/v1/models` currently returns only `z-ai/glm-5.2`. Use `z-ai/glm-5.2`. The `zai-org/GLM-5.2-FP8` served name still resolves at the direct endpoint, but treat it as a fallback only when your client needs that exact served model name. - -## Gateway vs direct completions - -Use the gateway by default: +## Use the Gateway ```text https://cloud-api.near.ai/v1 @@ -81,14 +56,6 @@ https://cloud-api.near.ai/v1 The gateway is the best fit for most third-party clients because one base URL can reach the full NEAR AI Cloud model catalog, and clients that auto-fetch models can use `GET /v1/models`. -Use direct completions only when you intentionally want one model's TEE endpoint: - -```text -https://glm-5-2.completions.near.ai/v1 -``` - -Direct completions can reduce routing hops and exposes the model-specific route set described in [Private Inference - Direct Completions](/cloud/private-inference#direct-completions). It is not a replacement for the gateway model catalog in clients that expect one provider to list every model. - ## Why client model lists go stale Client model lists can become stale for three common reasons: @@ -103,8 +70,6 @@ When NEAR releases a new model, check `https://cloud-api.near.ai/v1/models`, cop z-ai/glm-5.2 ``` -If you are configuring a direct completions endpoint, also check `https://completions.near.ai/endpoints` and confirm the endpoint's `models` array includes the model name you plan to send. - ## Tool behavior matrix | Behavior | Tools | Refresh action | @@ -137,34 +102,18 @@ curl https://cloud-api.near.ai/v1/chat/completions \ }' ``` -Use the direct completions endpoint only after confirming it lists the model: - -```bash -curl -fsS https://glm-5-2.completions.near.ai/v1/chat/completions \ - -H "Authorization: Bearer $NEARAI_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "model": "z-ai/glm-5.2", - "messages": [ - {"role": "user", "content": "Reply with only: near-ai-ok"} - ], - "max_tokens": 20 - }' -``` - ## Troubleshooting | Symptom | Likely cause | Fix | | --- | --- | --- | | model not listed | The client cached an older model list, uses a catalog that has not updated, or requires manual config. | Run the `/v1/models` curl check, copy `z-ai/glm-5.2`, and paste it into the client's custom model field. Re-save, reconnect, or restart the client. | | `401` | Missing, invalid, or expired NEAR AI Cloud API key. | Generate or copy a NEAR AI Cloud API key, store it in the client's secret field or environment variable, and retry without logging the key. | -| Wrong base URL includes `/chat/completions` | The client expects a base URL, but the full request endpoint was pasted. | Use `https://cloud-api.near.ai/v1` for gateway config, or `https://glm-5-2.completions.near.ai/v1` for direct completions config. Do not append `/chat/completions` to a base URL field. | +| Wrong base URL includes `/chat/completions` | The client expects a base URL, but the full request endpoint was pasted. | Use `https://cloud-api.near.ai/v1`. Do not append `/chat/completions` to a base URL field. | ## Related guides - [OpenAI Compatibility](/cloud/guides/openai-compatibility) - [Available Models](/cloud/models) -- [Private Inference - Direct Completions](/cloud/private-inference#direct-completions) - [Integrations Overview](/cloud/guides/integrations) ## Sources Checked @@ -172,5 +121,4 @@ curl -fsS https://glm-5-2.completions.near.ai/v1/chat/completions \ Sources checked on 2026-06-23: - [NEAR AI Cloud models API](https://cloud-api.near.ai/v1/models) -- [NEAR AI Cloud direct completions endpoints](https://completions.near.ai/endpoints) - [models.dev catalog](https://models.dev/) diff --git a/cloud/guides/integrations/open-webui.mdx b/cloud/guides/integrations/open-webui.mdx index d027659..8cd1b64 100644 --- a/cloud/guides/integrations/open-webui.mdx +++ b/cloud/guides/integrations/open-webui.mdx @@ -117,7 +117,6 @@ If either curl command fails, fix the NEAR AI Cloud key, model ID, or network pa - [Model Discovery and Refresh](/cloud/guides/integrations/model-discovery) - [OpenAI Compatibility](/cloud/guides/openai-compatibility) - [Available Models](/cloud/models) -- [Private Inference - Direct Completions](/cloud/private-inference#direct-completions) - [LibreChat](/cloud/guides/integrations/librechat) - [Dify](/cloud/guides/integrations/dify) diff --git a/cloud/guides/openai-compatibility.mdx b/cloud/guides/openai-compatibility.mdx index 753ce1b..0c2fab1 100644 --- a/cloud/guides/openai-compatibility.mdx +++ b/cloud/guides/openai-compatibility.mdx @@ -14,28 +14,17 @@ To use the OpenAI SDKs with NEAR AI Cloud, you simply need to override the `base ### Base URL -NEAR AI Cloud offers two base URL options: - -**Gateway** (routes to any model): +Use the NEAR AI Cloud Gateway base URL: ``` https://cloud-api.near.ai/v1 ``` -**Direct Completions** (connects straight to a model's TEE): -``` -https://{slug}.completions.near.ai/v1 -``` - -For example, `https://qwen35-122b.completions.near.ai/v1` connects directly to Qwen3.5-122B. See [completions.near.ai/endpoints](https://completions.near.ai/endpoints) for all available endpoints. - -Both URLs use the same API key. Direct completions offer lower latency and a simpler trust model since TLS terminates directly in the model's TEE. See [Private Inference β€” Direct Completions](/cloud/private-inference#direct-completions) for details. - ### API Key You can generate your API key in the [NEAR AI Cloud Dashboard](https://cloud.near.ai/dashboard/organizations). ### Request IDs -Gateway responses from `https://cloud-api.near.ai/v1` and direct completions responses from `https://{slug}.completions.near.ai/v1` include an `X-Request-Id` response header. Support may ask you for this value when debugging a request. +Gateway responses from `https://cloud-api.near.ai/v1` include an `X-Request-Id` response header. Support may ask you for this value when debugging a request. `X-Request-Id` is opaque support/debugging metadata; it is not W3C `traceparent` or distributed trace context. If you send your own `X-Request-Id`, use a non-sensitive UUID value. `X-Request-Id` values must not contain secrets or PII. `X-Org-Id` and `X-Workspace-Id` are internal tenant headers; public clients cannot set or override them. @@ -177,7 +166,7 @@ NEAR AI Cloud supports the following core OpenAI API features: NEAR AI Cloud also provides non-OpenAI extensions: - **Reranking** (`/v1/rerank`) - Score documents against a query -- **Privacy classification** (`/v1/privacy/classify`) - Detect personally identifiable information through the gateway, or through a privacy-filter direct completions backend when that backend is selected and available +- **Privacy classification** (`/v1/privacy/classify`) - Detect personally identifiable information through the gateway - **Privacy redaction** (`/v1/privacy/redact`) - Gateway-only endpoint for redacting personally identifiable information - **Attestation & Signatures** (`/v1/attestation/report`, `/v1/signature/{chat_id}`) - See [Verification](/cloud/verification) diff --git a/cloud/guides/opencode-goose.mdx b/cloud/guides/opencode-goose.mdx index 7f00994..7e64f69 100644 --- a/cloud/guides/opencode-goose.mdx +++ b/cloud/guides/opencode-goose.mdx @@ -146,7 +146,6 @@ Keep `max_tokens` generous on reasoning models. GLM 5.3 Flash spends its first t - [Model Discovery and Refresh](/cloud/guides/integrations/model-discovery) - [OpenAI Compatibility](/cloud/guides/openai-compatibility) - [Available Models](/cloud/models) -- [Private Inference - Direct Completions](/cloud/private-inference#direct-completions) - [Cline, Roo Code, and Kilo Code](/cloud/guides/integrations/cline-roo-kilo) ## Sources Checked diff --git a/cloud/guides/specialized-endpoints.mdx b/cloud/guides/specialized-endpoints.mdx index b9c799b..dbd9ee0 100644 --- a/cloud/guides/specialized-endpoints.mdx +++ b/cloud/guides/specialized-endpoints.mdx @@ -6,9 +6,9 @@ icon: "images" --- -NEAR AI Cloud is not limited to chat completions. The same gateway (`cloud-api.near.ai`) and [direct completions endpoints](/cloud/private-inference#direct-completions) serve specialized TEE-hosted models for embeddings, reranking, scoring, image generation, and audio transcription. Privacy classification is available through the gateway and may also be served by a direct completions endpoint when the selected privacy-filter backend supports it; privacy redaction remains gateway-only. Both modes use the same API key, the same OpenAI-compatible conventions, and the same hardware privacy guarantees for TEE-hosted models. +NEAR AI Cloud is not limited to chat completions. The NEAR AI Cloud Gateway (`cloud-api.near.ai`) serves specialized TEE-hosted models for embeddings, reranking, scoring, image generation, audio transcription, and privacy classification. -Gateway and direct completions responses include an `X-Request-Id` response header. Support may ask you for that opaque value when debugging a request. It is support/debugging metadata, not W3C `traceparent` or distributed trace context. If you send your own `X-Request-Id`, use a non-sensitive UUID value; `X-Request-Id` values must not contain secrets or PII. `X-Org-Id` and `X-Workspace-Id` are internal tenant headers; public clients cannot set or override them. +Gateway responses include an `X-Request-Id` response header. Support may ask you for that opaque value when debugging a request. It is support/debugging metadata, not W3C `traceparent` or distributed trace context. If you send your own `X-Request-Id`, use a non-sensitive UUID value; `X-Request-Id` values must not contain secrets or PII. `X-Org-Id` and `X-Workspace-Id` are internal tenant headers; public clients cannot set or override them. ## Embeddings @@ -133,8 +133,6 @@ curl https://cloud-api.near.ai/v1/privacy/redact \ `input` accepts a string or an array of strings for both endpoints. -`/v1/privacy/classify` can also be served on `https://{slug}.completions.near.ai/v1` when the selected privacy-filter backend supports it. - `/v1/privacy/redact` is gateway-only and is served at `https://cloud-api.near.ai/v1`. ## Vision (Image Input) @@ -142,5 +140,3 @@ curl https://cloud-api.near.ai/v1/privacy/redact \ `Qwen/Qwen3-VL-30B-A3B-Instruct` accepts images in standard OpenAI chat format (`image_url` content parts) on `/v1/chat/completions`. Check `input_modalities` in [`/v1/models`](https://cloud-api.near.ai/v1/models) to see which models accept images. --- - -Embeddings, reranking, scoring, image generation, image editing, and audio transcription also work on each matching model's own [direct completions endpoint](/cloud/private-inference#direct-completions) (for example, `qwen3-embedding.completions.near.ai`, `qwen3-reranker.completions.near.ai`, or `flux2-klein.completions.near.ai`), where TLS terminates directly inside the model's TEE. See [Private Inference](/cloud/private-inference#direct-completions) for the served direct route list, including `/v1/tokenize`, `/v1/score`, and `/v1/images/edits`. diff --git a/cloud/models.mdx b/cloud/models.mdx index ba1f5a1..90279c5 100644 --- a/cloud/models.mdx +++ b/cloud/models.mdx @@ -8,7 +8,7 @@ NEAR AI Cloud provides access to leading AI models, each optimized for different The catalog contains two kinds of models: -- **TEE-hosted models** (marked TEE) run on NEAR AI's own GPU fleet inside [Trusted Execution Environments](/cloud/private-inference). They support [attestation, signatures, and verification](/cloud/verification), are reachable through [direct completions endpoints](/cloud/private-inference#direct-completions), and nobody β€” not even NEAR β€” can see your prompts or outputs. +- **TEE-hosted models** (marked TEE) run on NEAR AI's own GPU fleet inside [Trusted Execution Environments](/cloud/private-inference). They support [attestation, signatures, and verification](/cloud/verification), and nobody β€” not even NEAR β€” can see your prompts or outputs. - **Third-party models** (OpenAI, Anthropic, Gemini, and others) are proxied to their providers through the NEAR AI gateway for convenience. They offer the same unified API and billing, but the TEE privacy and verifiability guarantees do not extend to the upstream provider.
diff --git a/cloud/private-inference.mdx b/cloud/private-inference.mdx index 467714e..994b574 100644 --- a/cloud/private-inference.mdx +++ b/cloud/private-inference.mdx @@ -54,7 +54,7 @@ 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. Each has its own TLS-attestation flow when your application requires evidence for the TLS endpoint of a particular connection: +For new integrations, use the NEAR AI Cloud Gateway. Its verification flow can establish the evidence required for a particular request path. **Gateway mode** routes through `cloud-api.near.ai`, which runs in its own TEE before forwarding to the model TEE: @@ -67,83 +67,29 @@ flowchart LR style C fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff ``` -**Direct completions mode** connects you straight to the model's TEE β€” one hop, one TEE to verify: - -```mermaid -flowchart LR - 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:** 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. +**Key insight:** TLS protects traffic in transit. When the Gateway 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 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. +2. **TLS endpoint verification**: Use Gateway 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. **Evidence-driven trust**: Use attestation and the workload policy appropriate for your application to decide which environment properties are required. This gives you the same OpenAI-compatible development flow while allowing your application to verify the properties it requires. -### Direct Completions +### Direct Completions (experimental) -With direct completions, each model is available at its own subdomain under `completions.near.ai`. Your request goes straight to the model's TEE β€” no gateway hop needed. + + Direct model endpoints are experimental. Do not use them for new integrations or production workflows that require verifiable deployment evidence or reliable response signatures. Use the NEAR AI Cloud Gateway instead. + -**Base URL format:** -``` -https://{slug}.completions.near.ai/v1 -``` - -**Available endpoints** (see [completions.near.ai/endpoints](https://completions.near.ai/endpoints) for the full list): - -| Subdomain | Model | Input | Output | -|-----------|-------|-------|--------| -| `glm-5-1.completions.near.ai` | GLM-5.1 | Text | Text | -| `glm-5-2.completions.near.ai` | GLM-5.2 (`z-ai/glm-5.2`, `zai-org/GLM-5.2-FP8`) | Text | Text | -| `qwen35-122b.completions.near.ai` | Qwen3.5-122B | Text | Text | -| `qwen3-6-35b.completions.near.ai` | Qwen3.6-35B | Text | Text | -| `qwen3-30b.completions.near.ai` | Qwen3-30B | Text | Text | -| `gpt-oss-120b.completions.near.ai` | GPT-OSS-120B | Text | Text | -| `gemma-4-31b.completions.near.ai` | Gemma 4 31B | Text | Text | -| `qwen3-vl-30b.completions.near.ai` | Qwen3-VL-30B | Text, Image | Text | -| `flux2-klein.completions.near.ai` | FLUX.2-klein-4B | Text | Image | -| `whisper-large-v3.completions.near.ai` | Whisper Large V3 | Audio | Text | -| `qwen3-embedding.completions.near.ai` | Qwen3-Embedding-0.6B | Text | Embedding | -| `qwen3-reranker.completions.near.ai` | Qwen3-Reranker-0.6B | Text | Score | -| `privacy-filter.completions.near.ai` | Privacy Filter | Text | Classification | - -Each subdomain exposes the served direct route set for that model: - -- `POST /v1/chat/completions` β€” Chat completions (with signing) -- `POST /v1/completions` β€” Text completions -- `POST /v1/tokenize` β€” Tokenization -- `POST /v1/embeddings` β€” Embeddings -- `POST /v1/rerank` β€” Reranking -- `POST /v1/score` β€” Scoring -- `POST /v1/images/generations` β€” Image generation -- `POST /v1/images/edits` β€” Image editing -- `POST /v1/audio/transcriptions` β€” Audio transcription -- `POST /v1/privacy/classify` β€” Privacy classification on privacy-filter deployments -- `GET /v1/models` β€” Available models -- `GET /v1/attestation/report` β€” TEE attestation (TDX + GPU) -- `GET /v1/signature/{chat_id}` β€” Retrieve cached signatures - -`/v1/privacy/classify` is also forwarded by direct completions endpoints when the selected privacy-filter backend serves it. `/v1/privacy/redact` remains a gateway privacy endpoint on `https://cloud-api.near.ai/v1`. - -Gateway and direct completions responses include an `X-Request-Id` response header. Support may ask you for that opaque value when debugging a request. It is support/debugging metadata, not W3C `traceparent` or distributed trace context. If you send your own `X-Request-Id`, use a non-sensitive UUID value; `X-Request-Id` values must not contain secrets or PII. `X-Org-Id` and `X-Workspace-Id` are internal tenant headers; public clients cannot set or override them. - -**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 [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 +Existing users can find endpoint names at [completions.near.ai/endpoints](https://completions.near.ai/endpoints). Independent requests to the same direct hostname can reach different serving instances. An attestation response, including `all_attestations[]`, applies only to the request that returned it and does not establish evidence for a later completion. A later signature lookup can return `404`; do not use direct response signatures as a production verification gate. ### The Inference Process -When you make a request to NEAR AI Cloud, your data flows through a secure pipeline designed to maintain privacy at every step. The exact flow depends on whether you use the gateway or a direct completions endpoint. +When you make a request to NEAR AI Cloud through the Gateway, your data flows through a secure pipeline designed to maintain privacy at every step. **Via Gateway (`cloud-api.near.ai`):** @@ -165,20 +111,6 @@ When you make a request to NEAR AI Cloud, your data flows through a secure pipel 6. **Verifiable Response:** 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, 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:** - Attestation reports provide environment evidence; supported flows can make response signatures available for integrity verification. - - 4. **Verifiable Response:** - 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. - --- ## Architecture Overview diff --git a/cloud/quickstart.mdx b/cloud/quickstart.mdx index 1d84968..a78b940 100644 --- a/cloud/quickstart.mdx +++ b/cloud/quickstart.mdx @@ -21,7 +21,7 @@ icon: "rocket" ## Make Your First API Call -NEAR AI Cloud uses an OpenAI-compatible API, making it easy to integrate with existing tools and libraries. You can connect through the **gateway** (`cloud-api.near.ai`) or **directly** to a model's own endpoint (`{slug}.completions.near.ai`). +NEAR AI Cloud uses an OpenAI-compatible API, making it easy to integrate with existing tools and libraries. Connect through the NEAR AI Cloud Gateway at `cloud-api.near.ai`. Replace `YOUR_API_KEY` with the API key you generated in the setup steps above. @@ -29,10 +29,8 @@ NEAR AI Cloud uses an OpenAI-compatible API, making it easy to integrate with ex **Request IDs** - Gateway and direct completions responses include an `X-Request-Id` response header. Support may ask you for it when debugging a request. The value is opaque support/debugging metadata, not W3C `traceparent` or distributed trace context. If you send your own `X-Request-Id`, use a non-sensitive UUID value; `X-Request-Id` values must not contain secrets or PII. `X-Org-Id` and `X-Workspace-Id` are internal tenant headers; public clients cannot set or override them. + Gateway responses include an `X-Request-Id` response header. Support may ask you for it when debugging a request. The value is opaque support/debugging metadata, not W3C `traceparent` or distributed trace context. If you send your own `X-Request-Id`, use a non-sensitive UUID value; `X-Request-Id` values must not contain secrets or PII. `X-Org-Id` and `X-Workspace-Id` are internal tenant headers; public clients cannot set or override them. - - The gateway routes your request to the appropriate model TEE: @@ -97,82 +95,6 @@ console.log(completion.choices[0].message.content); - - - -Direct completions connect you straight to the model's TEE β€” TLS terminates inside the model enclave with no intermediate gateway hop. - -Each model has its own subdomain. For example, Qwen3.5-122B is available at `qwen35-122b.completions.near.ai`: - - - - -```bash -curl https://qwen35-122b.completions.near.ai/v1/chat/completions \ --H "Content-Type: application/json" \ --H "Authorization: Bearer YOUR_API_KEY" \ --d '{ - "model": "Qwen/Qwen3.5-122B-A10B", - "messages": [{ - "role": "user", - "content": "Hello, NEAR AI!" - }] -}' -``` - - - - -```python -import openai - -client = openai.OpenAI( - base_url="https://qwen35-122b.completions.near.ai/v1", - api_key="YOUR_API_KEY" -) - -response = client.chat.completions.create( - model="Qwen/Qwen3.5-122B-A10B", - messages=[{ - "role": "user", "content": "Hello, NEAR AI!" - }] -) - -print(response.choices[0].message.content) -``` - - - - -```javascript -import OpenAI from 'openai'; - -const openai = new OpenAI({ - baseURL: 'https://qwen35-122b.completions.near.ai/v1', - apiKey: 'YOUR_API_KEY', -}); - -const completion = await openai.chat.completions.create({ - model: 'Qwen/Qwen3.5-122B-A10B', - messages: [{ - role: 'user', content: 'Hello, NEAR AI!' - }] -}); - -console.log(completion.choices[0].message.content); -``` - - - - - - **Available Endpoints** - - See the full list of available direct completions endpoints at [completions.near.ai/endpoints](https://completions.near.ai/endpoints). Examples include `qwen35-122b.completions.near.ai`, `qwen3-30b.completions.near.ai`, `gpt-oss-120b.completions.near.ai`, and more. - - - - ### Expected Response The API will return a JSON response containing the model's completion: diff --git a/cloud/verification/cloud-api/index.mdx b/cloud/verification/cloud-api/index.mdx index 028e1fb..ecaf93b 100644 --- a/cloud/verification/cloud-api/index.mdx +++ b/cloud/verification/cloud-api/index.mdx @@ -3,7 +3,7 @@ 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. +Use this guide when your application sends inference requests to `https://cloud-api.near.ai`. ## What you can verify diff --git a/cloud/verification/direct/image-provenance.mdx b/cloud/verification/direct/image-provenance.mdx index 4acf9f4..197c7c7 100644 --- a/cloud/verification/direct/image-provenance.mdx +++ b/cloud/verification/direct/image-provenance.mdx @@ -1,8 +1,12 @@ --- -title: "Image Provenance" -description: "Apply an image-provenance policy to the configuration measured by a direct model attestation." +title: "Experimental Image Provenance" +description: "Apply an image-provenance policy to configuration measured by a direct model attestation." --- + + This experimental check applies to the direct report under inspection. It does not establish deployment evidence for a separate inference request to the same hostname. + + 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 diff --git a/cloud/verification/direct/index.mdx b/cloud/verification/direct/index.mdx index 7f753bd..a691d86 100644 --- a/cloud/verification/direct/index.mdx +++ b/cloud/verification/direct/index.mdx @@ -1,21 +1,25 @@ --- -title: "Verify Direct Model Endpoints" -description: "Verify the model, connection, response, and software evidence for direct completions endpoints." +title: "Experimental Direct Model Endpoints" +description: "Experimental verification notes 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. + + Direct model endpoint verification is experimental and is not recommended for new integrations or production verification workflows. Use the [NEAR AI Cloud Gateway](/cloud/verification/cloud-api) instead. + -## What you can verify +Independent requests to the same direct hostname can reach different serving instances. An attestation report, including `all_attestations[]`, is evidence for the request that returned it; it does not establish deployment evidence for a later completion. A later response-signature request can return `404`, so do not use direct response signatures as a production verification gate. + +## Available experimental checks - Verify the trusted model environment reported by the direct endpoint. + Inspect the model environment reported by a direct endpoint. - Verify that your live HTTPS connection terminates at the attested model endpoint. + Bind one live HTTPS connection to a direct report. - Verify that a signed response came from the attested model environment. + Inspect an available signature for exact response bytes. Check that the software images in the attested environment meet your source and build policy. @@ -27,6 +31,6 @@ Use this guide when your application sends inference requests directly to `https 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. +4. Add TLS, response-signature, or image-provenance checks only for the direct request under inspection. -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. +Start with [Model attestation report](/cloud/verification/direct/model-attestation). Do not combine these experimental checks into a production deployment-verification conclusion for later independent requests. diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx index 51bc3e9..a7c9823 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -1,9 +1,13 @@ --- -title: "Model Attestation Report" -description: "Verify the root model report returned by a direct completions endpoint." +title: "Experimental Model Attestation Report" +description: "Inspect 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. + + This experimental report is evidence for the request that returned it. It does not establish deployment evidence for a separate inference request to the same hostname. + + +Use this page to inspect the model environment reported by a direct `https://.completions.near.ai` endpoint. ## Request a fresh report @@ -35,7 +39,7 @@ The root response is the model report. Relevant fields include: | `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. | +| `all_attestations[]` | Model-report entries in this direct response envelope. It is not a complete list of instances that can serve later requests. | ## Verify the report @@ -51,4 +55,4 @@ When a response signature is being verified, match both `signing_address` and `s ## 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. +The same direct model hostname can serve more than one instance. Do not assume two independent HTTP requests reached the same instance, or use a report as evidence for a separate completion sent later. 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 index b22dee3..46af938 100644 --- a/cloud/verification/direct/response-signatures.mdx +++ b/cloud/verification/direct/response-signatures.mdx @@ -1,13 +1,13 @@ --- -title: "Response Signatures" -description: "Verify an available direct-model response signature against exact bytes and the matching model report." +title: "Experimental Response Signatures" +description: "Inspect an available direct-model response signature against exact bytes and a 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 signature lookup can return `404` after a direct completion. Treat a missing signature as unavailable; direct response signatures are not a production verification gate. + - - 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. - +Use this experimental flow to inspect the signature over exact bytes for a completion sent directly to a model endpoint. ## Preserve exact request and response bytes @@ -47,7 +47,7 @@ Use the [manual response-signature helpers](/cloud/verification/reference/respon ## 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). +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). A matching signer does not establish that the report and completion came from the same serving instance. 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. diff --git a/cloud/verification/direct/tls.mdx b/cloud/verification/direct/tls.mdx index 0af2ab6..f525692 100644 --- a/cloud/verification/direct/tls.mdx +++ b/cloud/verification/direct/tls.mdx @@ -1,12 +1,12 @@ --- -title: "TLS Connection Binding" -description: "Bind a live direct-model TLS connection to the direct model attestation report." +title: "Experimental TLS Connection Binding" +description: "Bind one live direct-model TLS connection to a 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. +Use this experimental flow to bind one HTTPS connection to the direct model attestation report it returns. - 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. + Obtain the peer certificate and direct attestation report over the **same TLS connection**. This result does not establish deployment evidence for later independent completion or signature requests. Use the NEAR AI Cloud Gateway for production verification workflows. ## What this checks diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 1a1f179..47876e1 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -1,28 +1,25 @@ --- title: "Verification" -description: "Choose the verification guide that matches the inference endpoint your application calls." +description: "Verify requests sent through the NEAR AI Cloud Gateway." --- -Use these guides to verify the evidence returned for your AI requests. Start with the inference endpoint your application calls. +Use these guides to verify the evidence returned for requests sent through the NEAR AI Cloud Gateway. -## Choose your request path +## Verify a NEAR AI Cloud Gateway request - + 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. - - - 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. - + + Direct model endpoint verification is experimental. Use the NEAR AI Cloud Gateway for new integrations and production verification workflows. + ## 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. +The Gateway guide walks 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. ## Common verification concepts diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index c0f03e7..ddc76b7 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -21,7 +21,6 @@ Send this value with the attestation request. Reject a report unless the verifie | --- | --- | | 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 @@ -47,7 +46,7 @@ This layout applies only to NEAR-operated dstack reports; other providers have t ## 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. +`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. ```bash GPU_PAYLOAD="$(jq -er '.nvidia_payload | fromjson' report.json)" diff --git a/cloud/verification/reference/response-signature-verification.mdx b/cloud/verification/reference/response-signature-verification.mdx index ff11b48..6f20631 100644 --- a/cloud/verification/reference/response-signature-verification.mdx +++ b/cloud/verification/reference/response-signature-verification.mdx @@ -17,7 +17,6 @@ preflight_signer = { 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) diff --git a/docs.json b/docs.json index da3a14f..dd1f125 100644 --- a/docs.json +++ b/docs.json @@ -78,16 +78,6 @@ "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": [ From 60e44311c3ef36cb7aeaab2994c5f22c316333a4 Mon Sep 17 00:00:00 2001 From: Coffee Date: Wed, 23 Sep 2026 13:40:11 +0800 Subject: [PATCH 34/36] docs: remove direct endpoint discovery --- .github/INTEGRATION_GUIDE_TEMPLATE.md | 6 +++--- cloud/guides/fusion.mdx | 2 +- cloud/private-inference.mdx | 10 +--------- cloud/verification/index.mdx | 4 ---- 4 files changed, 5 insertions(+), 17 deletions(-) diff --git a/.github/INTEGRATION_GUIDE_TEMPLATE.md b/.github/INTEGRATION_GUIDE_TEMPLATE.md index b4a5c3c..513b74c 100644 --- a/.github/INTEGRATION_GUIDE_TEMPLATE.md +++ b/.github/INTEGRATION_GUIDE_TEMPLATE.md @@ -60,8 +60,9 @@ Use the gateway base URL by default: https://cloud-api.near.ai/v1 ``` -Only mention direct completions when the tool accepts arbitrary per-model base -URLs. Otherwise, link to Direct Completions instead of adding unverified config. +Use the Gateway in new integration guides. Do not add direct-completions +configuration or links; direct endpoints are experimental and are not +recommended for new integrations. ### Model ID @@ -109,7 +110,6 @@ Link to canonical NEAR AI Cloud docs instead of repeating long explanations: - OpenAI Compatibility: `/cloud/guides/openai-compatibility` - Model Discovery: `/cloud/guides/integrations/model-discovery` -- Direct Completions: `/cloud/private-inference#direct-completions` - Available Models: `/cloud/models` ### Sources Checked diff --git a/cloud/guides/fusion.mdx b/cloud/guides/fusion.mdx index ca91131..bfd16de 100644 --- a/cloud/guides/fusion.mdx +++ b/cloud/guides/fusion.mdx @@ -89,7 +89,7 @@ Use `tool_choice: "required"` when you want Fusion to run immediately. If you om | Field | Required | Description | |-------|----------|-------------| -| `analysis_models` | Yes* | Panel models to ask before synthesis. Required unless the deployment configures `FUSION_DEFAULT_ANALYSIS_MODELS`. Model names may include or omit a leading `~`; NEAR AI strips it before direct endpoint lookup. | +| `analysis_models` | Yes* | Panel models to ask before synthesis. Required unless the deployment configures `FUSION_DEFAULT_ANALYSIS_MODELS`. Model names may include or omit a leading `~`; NEAR AI ignores the prefix when resolving the model. | | `model` | No | Judge model. Defaults to the request `model` when omitted. | | `max_tool_calls` | No | Maximum inner web-search tool calls for panel and judge requests. | | `max_completion_tokens` | No | Token cap for panel and judge completions. | diff --git a/cloud/private-inference.mdx b/cloud/private-inference.mdx index 994b574..c972e5f 100644 --- a/cloud/private-inference.mdx +++ b/cloud/private-inference.mdx @@ -60,7 +60,7 @@ For new integrations, use the NEAR AI Cloud Gateway. Its verification flow can e ```mermaid flowchart LR - A["Your Machine"] -->|"πŸ”’ HTTPS"| B["NEAR AI Cloud gateway"] -->|"πŸ”’ HTTPS"| C["Model endpoint β†’ LLM"] + A["Your Machine"] -->|"πŸ”’ HTTPS"| B["NEAR AI Cloud gateway"] -->|"πŸ”’ HTTPS"| C["Model TEE β†’ LLM"] style A fill:#1a1a2e,stroke:#4fc3f7,stroke-width:2px,color:#fff style B fill:#1b5e20,stroke:#66bb6a,stroke-width:2px,color:#fff @@ -79,14 +79,6 @@ Here's why this works: This gives you the same OpenAI-compatible development flow while allowing your application to verify the properties it requires. -### Direct Completions (experimental) - - - Direct model endpoints are experimental. Do not use them for new integrations or production workflows that require verifiable deployment evidence or reliable response signatures. Use the NEAR AI Cloud Gateway instead. - - -Existing users can find endpoint names at [completions.near.ai/endpoints](https://completions.near.ai/endpoints). Independent requests to the same direct hostname can reach different serving instances. An attestation response, including `all_attestations[]`, applies only to the request that returned it and does not establish evidence for a later completion. A later signature lookup can return `404`; do not use direct response signatures as a production verification gate. - ### The Inference Process When you make a request to NEAR AI Cloud through the Gateway, your data flows through a secure pipeline designed to maintain privacy at every step. diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 47876e1..0164a07 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -13,10 +13,6 @@ Use these guides to verify the evidence returned for requests sent through the N - - Direct model endpoint verification is experimental. Use the NEAR AI Cloud Gateway for new integrations and production verification workflows. - - ## What happens next The Gateway guide walks 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. From 59412ea7a65b62544486da675877a69144076019 Mon Sep 17 00:00:00 2001 From: Coffee Date: Wed, 23 Sep 2026 13:42:35 +0800 Subject: [PATCH 35/36] docs: keep OpenCode Gateway-only --- api-reference/openapi.json | 11 +++++++++-- cloud/guides/integrations/opencode.mdx | 26 ++------------------------ 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/api-reference/openapi.json b/api-reference/openapi.json index 1361579..794aac4 100644 --- a/api-reference/openapi.json +++ b/api-reference/openapi.json @@ -3458,7 +3458,7 @@ "Billing" ], "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", + "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": { @@ -9975,7 +9975,7 @@ "type": "string", "format": "uuid" }, - "description": "Array of request IDs (inference IDs) to get costs for" + "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)." } } }, @@ -9992,6 +9992,13 @@ "$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." } } }, diff --git a/cloud/guides/integrations/opencode.mdx b/cloud/guides/integrations/opencode.mdx index 9a020cd..7ef2d5f 100644 --- a/cloud/guides/integrations/opencode.mdx +++ b/cloud/guides/integrations/opencode.mdx @@ -6,7 +6,7 @@ description: "Configure OpenCode to use NEAR AI Cloud as an OpenAI-compatible pr [OpenCode](https://opencode.ai/) is a terminal coding agent that resolves its providers from the public [models.dev](https://models.dev/) catalog. NEAR AI Cloud is already in that catalog as the `nearai` provider, so in the common case you do not write a provider block at all: set an API key, point `model` at a NEAR AI model, and OpenCode handles the rest. -You only need a manual provider entry in two cases: the model you want is newer than the catalog, or you want to reach a model's TEE directly instead of going through the gateway. Both are covered below. +You only need a manual provider entry when the model you want is newer than the catalog or your OpenCode version predates the catalog's `nearai` entry. Both cases use the Gateway configuration below. ## Prerequisites @@ -33,7 +33,7 @@ The catalog already sets the gateway base URL for the `nearai` provider: https://cloud-api.near.ai/v1 ``` -You only set `options.baseURL` yourself when overriding it, for example to use a direct completions endpoint. Do not append `/chat/completions`; OpenCode adds the path when it calls the API. +You only set `options.baseURL` yourself in a full provider block. Do not append `/chat/completions`; OpenCode adds the path when it calls the API. ## Model ID @@ -139,26 +139,6 @@ NEAR AI Cloud exposes 50+ models. OpenCode's documented `whitelist` and `blackli } ``` -### Direct completions - -To bypass the gateway and connect straight to a model's TEE, override `baseURL` with that model's direct endpoint: - -```json opencode.json -{ - "$schema": "https://opencode.ai/config.json", - "model": "nearai/z-ai/glm-5.3-flash", - "provider": { - "nearai": { - "options": { - "baseURL": "https://glm-5-3-flash.completions.near.ai/v1" - } - } - } -} -``` - -A direct endpoint serves exactly one model, so this override applies to the whole `nearai` provider. If you switch between models, keep the gateway base URL instead. See [Direct Completions](/cloud/private-inference#direct-completions). - ## Refresh models OpenCode does not call NEAR AI Cloud's `/v1/models`. Its picker comes from the models.dev catalog, so a newly released NEAR AI model appears only after the catalog updates. @@ -216,14 +196,12 @@ opencode run "Reply with only: near-ai-ok" | OpenCode reports an unknown provider or model | The provider key must be `nearai`, and `model` must be `nearai/` β€” including the slash inside the model ID, as in `nearai/z-ai/glm-5.3-flash`. | | The model replies but never calls tools | The manual model entry is missing `tool_call: true`. Catalog entries set this; hand-written ones default to off. | | Replies are empty or truncated mid-thought | A reasoning model hit the output limit. Raise `limit.output` in the model entry to match `max_output_length` from `/v1/models`. | -| A direct completions base URL rejects the model | Each `*.completions.near.ai` endpoint serves one model. Confirm with `curl https:///v1/models` that it advertises the ID you configured. | ## Related guides - [Model Discovery and Refresh](/cloud/guides/integrations/model-discovery) - [OpenAI Compatibility](/cloud/guides/openai-compatibility) - [Available Models](/cloud/models) -- [Private Inference - Direct Completions](/cloud/private-inference#direct-completions) - [Goose](/cloud/guides/opencode-goose) - [Cline, Roo Code, and Kilo Code](/cloud/guides/integrations/cline-roo-kilo) From 34af011529fcc8a178354cee0aa8d28fa17ae8b2 Mon Sep 17 00:00:00 2001 From: Pierre LE GUEN <26087574+PierreLeGuen@users.noreply.github.com> Date: Wed, 23 Sep 2026 09:49:58 +0000 Subject: [PATCH 36/36] docs: address verification review - Use the canonical z-ai/glm-5.3-flash in the E2EE examples. The gateway rejects the old alias when x-no-aliasing is set. - State the full 48-byte MRCONFIGID and document RTMR3 replay, including reports whose event digests are empty. - Note that instances of a model share one signing key, so a matching signer does not pin the serving instance. - Add Python and Node.js helpers that verify the quote with dcap-qvl and check debug mode, nonce, signer, MRCONFIGID and RTMR3. - Restore links to the verification example and verifier, and name dcap-qvl for quote verification. - Stop the NRAS snippet on a nonce mismatch and send the payload on stdin. - Clarify the signed bytes (uncompressed body), when the gateway signs, signing_algo defaults, helper runtimes, and missing model_attestations. --- cloud/guides/e2ee-chat-completions.mdx | 22 +- .../cloud-api/gateway-attestation.mdx | 8 +- .../cloud-api/model-attestations.mdx | 14 +- .../cloud-api/response-signatures.mdx | 10 +- .../verification/direct/model-attestation.mdx | 8 +- .../direct/response-signatures.mdx | 2 +- cloud/verification/index.mdx | 4 + .../reference/quote-nonce-signer.mdx | 272 +++++++++++++++++- .../response-signature-verification.mdx | 7 +- .../reference/verification-policy.mdx | 2 +- 10 files changed, 306 insertions(+), 43 deletions(-) diff --git a/cloud/guides/e2ee-chat-completions.mdx b/cloud/guides/e2ee-chat-completions.mdx index 3a1571a..f9c4ed2 100644 --- a/cloud/guides/e2ee-chat-completions.mdx +++ b/cloud/guides/e2ee-chat-completions.mdx @@ -85,7 +85,7 @@ from cryptography.hazmat.primitives import hashes ENDPOINT = "https://cloud-api.near.ai" API_KEY = os.environ["NEAR_AI_CLOUD_API_KEY"] -MODEL = "zai-org/GLM-5.1-FP8" +MODEL = "z-ai/glm-5.3-flash" # 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"] @@ -164,7 +164,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. 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. See [NEAR model attestations](/cloud/verification/cloud-api/model-attestations). +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. Reject a missing or empty `model_attestations[]`, 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. See [NEAR model attestations](/cloud/verification/cloud-api/model-attestations). The gateway endpoint requires an API key (`Authorization: Bearer `); report retrieval is free and never counts against your usage. @@ -175,7 +175,7 @@ The gateway endpoint requires an API key (`Authorization: Bearer `); re 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 'model=z-ai/glm-5.3-flash' \ --data-urlencode 'provider=near' \ --data-urlencode 'signing_algo=ed25519' \ --data-urlencode 'include_tls_fingerprint=false' \ @@ -200,7 +200,7 @@ response = requests.get( "x-no-aliasing": "true", }, params={ - "model": "zai-org/GLM-5.1-FP8", + "model": "z-ai/glm-5.3-flash", "provider": "near", "signing_algo": "ed25519", "include_tls_fingerprint": "false", @@ -231,7 +231,7 @@ const nonce = Array.from(crypto.getRandomValues(new Uint8Array(32)), (byte) => const response = await fetch( 'https://cloud-api.near.ai/v1/attestation/report?' + new URLSearchParams({ - model: 'zai-org/GLM-5.1-FP8', + model: 'z-ai/glm-5.3-flash', provider: 'near', signing_algo: 'ed25519', include_tls_fingerprint: 'false', @@ -258,7 +258,7 @@ console.log(`Retrieved ${modelReports.length} NEAR model-attestation candidate(s -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`. +Request `provider=near` with a canonical model ID and `x-no-aliasing: true`. Reject a missing or empty `model_attestations[]`, 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`. --- @@ -430,7 +430,7 @@ curl https://cloud-api.near.ai/v1/chat/completions \ -H "X-Encryption-Version: 2" \ -H "x-no-aliasing: true" \ -d '{ - "model": "zai-org/GLM-5.1-FP8", + "model": "z-ai/glm-5.3-flash", "messages": [{ "role": "user", "content": "ENCRYPTED_CONTENT_HEX" @@ -457,7 +457,7 @@ response = requests.post( "x-no-aliasing": "true", }, json={ - "model": "zai-org/GLM-5.1-FP8", + "model": "z-ai/glm-5.3-flash", "messages": [{ "role": "user", "content": encrypted_content # Hex-encoded encrypted message @@ -485,7 +485,7 @@ const response = await fetch('https://cloud-api.near.ai/v1/chat/completions', { 'x-no-aliasing': 'true', }, body: JSON.stringify({ - model: 'zai-org/GLM-5.1-FP8', + model: 'z-ai/glm-5.3-flash', messages: [{ role: 'user', content: encryptedContent @@ -706,7 +706,7 @@ Before using a model public key for encryption, verify the attestation report an 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 'model=z-ai/glm-5.3-flash' \ --data-urlencode 'provider=near' \ --data-urlencode 'signing_algo=ecdsa' \ --data-urlencode 'include_tls_fingerprint=false' \ @@ -782,7 +782,7 @@ Before using a model public key for encryption, verify the attestation report an -H "X-Model-Pub-Key: MODEL_ECDSA_PUBLIC_KEY_HEX" \ -H "x-no-aliasing: true" \ -d '{ - "model": "zai-org/GLM-5.1-FP8", + "model": "z-ai/glm-5.3-flash", "messages": [{"role": "user", "content": "ENCRYPTED_CONTENT_HEX"}], "stream": false }' diff --git a/cloud/verification/cloud-api/gateway-attestation.mdx b/cloud/verification/cloud-api/gateway-attestation.mdx index 061cda9..604ab5a 100644 --- a/cloud/verification/cloud-api/gateway-attestation.mdx +++ b/cloud/verification/cloud-api/gateway-attestation.mdx @@ -19,17 +19,17 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -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). +The response contains the gateway report at `gateway_attestation`. For a deployment-first completion, verify and retain it before sending the completion. Add `model=`, `provider=near`, and the `x-no-aliasing: true` header when you also need [NEAR model evidence](/cloud/verification/cloud-api/model-attestations). That request returns NEAR model reports in `model_attestations[]`; verify every entry 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. +1. Verify `intel_quote` with an Intel DCAP quote verifier, such as [`dcap-qvl`](https://github.com/Phala-Network/dcap-qvl), 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. +4. Replay `event_log` as described in [Replay the RTMR3 event log](/cloud/verification/reference/quote-nonce-signer#replay-the-rtmr3-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 48-byte MRCONFIGID to equal `01`, then that SHA-256 hash, then 15 zero bytes. See [Check the configuration measurement](/cloud/verification/reference/quote-nonce-signer#check-the-configuration-measurement). 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. diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index 422cc54..8e847cb 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -23,7 +23,7 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -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. +The response includes gateway evidence and NEAR model-report candidates in `model_attestations[]`. The field is omitted when no candidate is available. If your policy requires NEAR model evidence, reject a missing or empty `model_attestations[]`. 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. @@ -40,9 +40,9 @@ The NEAR report uses these fields: | `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. | +| `report_data` | Not currently returned in model reports. Read report data from 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. | +| `signing_public_key` | Model public key used by the E2EE flow; other flows do not need it. With `signing_algo=ed25519` it is the Ed25519 key for the default E2EE flow; with `ecdsa` it is the secp256k1 key for ECDSA 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. | @@ -50,17 +50,17 @@ The NEAR report uses these fields: 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. +1. Verify `intel_quote` with an Intel DCAP quote verifier, such as [`dcap-qvl`](https://github.com/Phala-Network/dcap-qvl), 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. +3. Replay `event_log` as described in [Replay the RTMR3 event log](/cloud/verification/reference/quote-nonce-signer#replay-the-rtmr3-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 48-byte MRCONFIGID to equal `01`, then that SHA-256 hash, then 15 zero bytes. See [Check the configuration measurement](/cloud/verification/reference/quote-nonce-signer#check-the-configuration-measurement). 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. + `model_attestations[]` is not an inventory of every model instance behind a URL. Instances of the same model share one signing key, including instances with different measured configurations, so a matching signer does not establish that the report and the completion came from the same serving instance. ## Relationship to NEAR AI Cloud gateway TLS diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx index b85496e..cc8881f 100644 --- a/cloud/verification/cloud-api/response-signatures.mdx +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -20,7 +20,7 @@ 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. +Do not parse and re-serialize JSON, normalize whitespace or line endings, omit stream delimiters, or reconstruct a streamed response from parsed events. The signature covers the uncompressed response body. Request `Accept-Encoding: identity`, or hash the body after your HTTP client decompresses it. ## Fetch and verify the signature @@ -35,7 +35,7 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/signature/' \ 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. +Use the same explicit `signing_algo` for the attestation request and the signature request. Their defaults differ: `/v1/signature` defaults to `ecdsa`, while `/v1/attestation/report` defaults to `ed25519` for the gateway report and `ecdsa` for model reports. 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. @@ -46,9 +46,9 @@ Then compare `text` with the expected payload: | `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. +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. Instances of the same model share one signing key, so a matching `provider_tee` signer does not establish that the verified report and the completion came from the same serving instance. -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. +The Gateway signs a response itself when it changes the bytes it relays: by default on streamed completions (it removes per-chunk usage data), when it resolves a model alias, and on the Responses API. 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. @@ -58,7 +58,7 @@ Use the [manual response-signature helpers](/cloud/verification/reference/respon ## 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. +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 a missing or empty `model_attestations[]`; verify every returned candidate and retain the verified results. After fetching the signature, use `signature_kind` to select the matching preflight result: diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx index a7c9823..55dc259 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -33,7 +33,7 @@ The root response is the model report. Relevant fields include: | `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. | +| `report_data` | Not currently returned in direct model reports. Read report data from 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. | @@ -43,10 +43,10 @@ The root response is the model report. Relevant fields include: ## Verify the report -1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. +1. Verify `intel_quote` with an Intel DCAP quote verifier, such as [`dcap-qvl`](https://github.com/Phala-Network/dcap-qvl), 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. +3. Replay `event_log` as described in [Replay the RTMR3 event log](/cloud/verification/reference/quote-nonce-signer#replay-the-rtmr3-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 48-byte MRCONFIGID to equal `01`, then that SHA-256 hash, then 15 zero bytes. See [Check the configuration measurement](/cloud/verification/reference/quote-nonce-signer#check-the-configuration-measurement). 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). diff --git a/cloud/verification/direct/response-signatures.mdx b/cloud/verification/direct/response-signatures.mdx index 46af938..e28a5f5 100644 --- a/cloud/verification/direct/response-signatures.mdx +++ b/cloud/verification/direct/response-signatures.mdx @@ -18,7 +18,7 @@ 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. +Do not parse and re-serialize JSON, normalize whitespace or line endings, omit stream delimiters, or reconstruct a streamed response from parsed events. The signature covers the uncompressed response body. Request `Accept-Encoding: identity`, or hash the body after your HTTP client decompresses it. ## Fetch and verify the signature diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 0164a07..8bacedf 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -5,6 +5,10 @@ description: "Verify requests sent through the NEAR AI Cloud Gateway." Use these guides to verify the evidence returned for requests sent through the NEAR AI Cloud Gateway. + + See an example implementation in the [NEAR AI Cloud Verification Example](https://github.com/near-examples/nearai-cloud-verification-example) repo. A more complete verifier is available in the [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) repo. + + ## Verify a NEAR AI Cloud Gateway request diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index ddc76b7..3bcb3cd 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -39,11 +39,31 @@ Always compare both `signing_address` and `signing_algo`. Do not compare identit 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. +- Without TLS binding, they are the decoded signing identity, zero-padded on the right to 32 bytes. Decode `signing_address` from hex after removing any `0x` prefix. An ECDSA address occupies 20 bytes; an Ed25519 public key occupies all 32 bytes. +- With TLS binding, they are `SHA-256(decoded signing identity || decoded tls_cert_fingerprint)`, where the fingerprint is the raw 32-byte SHA-256 of the certificate's SPKI, not its hex text. 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. +## Check the configuration measurement + +`info.tcb_info.app_compose` is the measured configuration. Hash its exact UTF-8 bytes without parsing or reserializing it. The verified quote's MRCONFIGID is 48 bytes: `01`, then that SHA-256 hash, then 15 zero bytes. + +```bash +jq -j '.info.tcb_info.app_compose' report.json | sha256sum +``` + +Use `jq -j`: `jq -r` appends a newline and produces a different hash. For a gateway report, read `.gateway_attestation.info.tcb_info.app_compose`. + +## Replay the RTMR3 event log + +`event_log` is a list of events. The gateway returns it as a JSON string; decode it first. Replay only the events whose `imr` is `3`, in order: + +1. Compute each event's digest as `SHA-384(event_type as 4 little-endian bytes || ":" || event as UTF-8 || ":" || hex-decoded event_payload)`. +2. If the event carries a non-empty `digest`, require it to equal the computed digest. Some reports return an empty `digest`; use the computed one. +3. Start from 48 zero bytes and extend once per event: `RTMR3 = SHA-384(RTMR3 || digest)`. + +Require the result to equal RTMR3 in the verified quote. Recomputing each digest also authenticates the event payloads, such as `app-id` and `compose-hash`. + ## 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. @@ -51,14 +71,252 @@ This layout applies only to NEAR-operated dstack reports; other providers have t ```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 \ +printf '%s' "$GPU_PAYLOAD" | jq -e --arg nonce "$NONCE" \ + '(.nonce | type == "string") and ((.nonce | ascii_downcase) == ($nonce | ascii_downcase))' >/dev/null && +printf '%s' "$GPU_PAYLOAD" | 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" + --data-binary @- ``` +The request is sent only when the payload nonce matches. The payload goes on standard input because an eight-GPU payload is close to the shell's argument-size limit. + 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. + +## Check a report in code + +These helpers run the quote, debug-mode, nonce, signer, configuration, and RTMR3 checks described above on one NEAR report. They use [`dcap-qvl`](https://github.com/Phala-Network/dcap-qvl) to verify the quote and read report data and measurements from the verified quote. Run them on `gateway_attestation`, on every `model_attestations[]` candidate, or on a direct model report. Pass `peer_spki_sha256` only when you observed the peer certificate on the same TLS connection that returned the report. GPU evidence is checked separately, as described above. + + + + +Requires Python 3.9 or later. + +```bash +pip install dcap-qvl requests +``` + +```python +import hashlib +import json +import struct + +import dcap_qvl + + +def _json(value): + return json.loads(value) if isinstance(value, str) else value + + +def _signing_identity(report): + algorithm = report["signing_algo"] + identity = bytes.fromhex(report["signing_address"].removeprefix("0x")) + if {"ecdsa": 20, "ed25519": 32}.get(algorithm) != len(identity): + raise ValueError("unsupported signing identity") + return identity + + +def replay_rtmr3(event_log): + rtmr3 = bytes(48) + for event in _json(event_log): + if event.get("imr") != 3: + continue + digest = hashlib.sha384( + struct.pack(" + + +Save the code as an ES module (a `.mjs` file, or set `"type": "module"` in `package.json`). + +```bash +npm install @phala/dcap-qvl +``` + +```javascript +import { createHash } from 'node:crypto'; +import { getCollateralAndVerify } from '@phala/dcap-qvl'; + +const hexBytes = (value) => Buffer.from(value.replace(/^0x/i, ''), 'hex'); +const parse = (value) => (typeof value === 'string' ? JSON.parse(value) : value); +const hash = (algorithm, ...parts) => { + const h = createHash(algorithm); + for (const part of parts) h.update(part); + return h.digest(); +}; + +export function replayRtmr3(eventLog) { + let rtmr3 = Buffer.alloc(48); + for (const event of parse(eventLog)) { + if (event.imr !== 3) continue; + const eventType = Buffer.alloc(4); + eventType.writeUInt32LE(event.event_type); + const digest = hash('sha384', eventType, ':', Buffer.from(event.event, 'utf8'), ':', + hexBytes(event.event_payload ?? '')); + if (event.digest && !hexBytes(event.digest).equals(digest)) { + throw new Error(`digest mismatch for event ${event.event}`); + } + rtmr3 = hash('sha384', rtmr3, digest); + } + return rtmr3; +} + +export async function checkNearReport(report, nonceHex, acceptedTcbStatuses, peerSpkiSha256) { + const verified = await getCollateralAndVerify(hexBytes(report.intel_quote)); + if (!acceptedTcbStatuses.includes(verified.status)) { + throw new Error(`TCB status ${verified.status} not accepted: ${verified.advisory_ids}`); + } + if (verified.report.type !== 'td10') throw new Error('expected a TDX quote'); + const quote = verified.report.data; + + if (quote.tdAttributes[0] & 1) throw new Error('TDX debug mode is enabled'); + + const identity = hexBytes(report.signing_address); + if ({ ecdsa: 20, ed25519: 32 }[report.signing_algo] !== identity.length) { + throw new Error('unsupported signing identity'); + } + const fingerprint = report.tls_cert_fingerprint; + if (peerSpkiSha256 !== undefined && (fingerprint ?? '').toLowerCase() !== peerSpkiSha256.toLowerCase()) { + throw new Error('TLS fingerprint does not match the peer certificate'); + } + const boundIdentity = fingerprint + ? hash('sha256', identity, hexBytes(fingerprint)) + : Buffer.concat([identity, Buffer.alloc(32 - identity.length)]); + if (!Buffer.from(quote.reportData).equals(Buffer.concat([boundIdentity, hexBytes(nonceHex)]))) { + throw new Error('report data does not bind the signer and nonce'); + } + if (report.request_nonce.toLowerCase() !== nonceHex.toLowerCase()) { + throw new Error('echoed nonce does not match'); + } + + const appCompose = parse(report.info.tcb_info).app_compose; + const expectedConfig = Buffer.concat([Buffer.from([1]), hash('sha256', Buffer.from(appCompose, 'utf8')), Buffer.alloc(15)]); + if (!Buffer.from(quote.mrConfigId).equals(expectedConfig)) { + throw new Error('MRCONFIGID does not match app_compose'); + } + + if (!replayRtmr3(report.event_log).equals(Buffer.from(quote.rtMr3))) { + throw new Error('event log does not match RTMR3'); + } + return verified.status; +} +``` + +```javascript +import { randomBytes } from 'node:crypto'; +const ACCEPTED_TCB_STATUSES = ['UpToDate']; // Set by your verification policy. + +const nonce = randomBytes(32).toString('hex'); +const response = await fetch( + 'https://cloud-api.near.ai/v1/attestation/report?' + + new URLSearchParams({ + model: 'z-ai/glm-5.3-flash', + provider: 'near', + signing_algo: 'ecdsa', + 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 body = await response.json(); + +console.log('gateway:', await checkNearReport(body.gateway_attestation, nonce, ACCEPTED_TCB_STATUSES)); +const candidates = body.model_attestations ?? []; +if (candidates.length === 0) throw new Error('No NEAR model evidence returned'); +for (const candidate of candidates) { + console.log('model:', await checkNearReport(candidate, nonce, ACCEPTED_TCB_STATUSES)); +} +``` + + + + +The helpers raise when the quote's TCB status is not in your accepted set; the error lists Intel's advisory IDs. Decide which statuses your [verification policy](/cloud/verification/reference/verification-policy) accepts. + +`dcap-qvl` fetches quote collateral from Phala's PCCS by default. To use Intel's PCS instead, pass `INTEL_PCS_URL` from the same package as the second argument to `get_collateral_and_verify` or `getCollateralAndVerify`. diff --git a/cloud/verification/reference/response-signature-verification.mdx b/cloud/verification/reference/response-signature-verification.mdx index 6f20631..7411d54 100644 --- a/cloud/verification/reference/response-signature-verification.mdx +++ b/cloud/verification/reference/response-signature-verification.mdx @@ -17,14 +17,15 @@ preflight_signer = { 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) +- [Experimental direct model endpoint response signatures](/cloud/verification/direct/response-signatures) -Install [`ethers`](https://www.npmjs.com/package/ethers) and [`tweetnacl`](https://www.npmjs.com/package/tweetnacl): +Install [`ethers`](https://www.npmjs.com/package/ethers) v6 and [`tweetnacl`](https://www.npmjs.com/package/tweetnacl), and save the code as an ES module (a `.mjs` file, or set `"type": "module"` in `package.json`): ```bash -npm install ethers tweetnacl +npm install ethers@6 tweetnacl ``` ```javascript @@ -119,7 +120,7 @@ export function verifyResponseSignature({ signature, expectedText, preflightSign -Install [`eth-account`](https://pypi.org/project/eth-account/) and [`PyNaCl`](https://pypi.org/project/PyNaCl/): +Requires Python 3.9 or later. Install [`eth-account`](https://pypi.org/project/eth-account/) and [`PyNaCl`](https://pypi.org/project/PyNaCl/): ```bash pip install eth-account pynacl diff --git a/cloud/verification/reference/verification-policy.mdx b/cloud/verification/reference/verification-policy.mdx index 0e61dc8..9ffbf03 100644 --- a/cloud/verification/reference/verification-policy.mdx +++ b/cloud/verification/reference/verification-policy.mdx @@ -10,7 +10,7 @@ Attestation verifies particular cryptographic statements. Your application must At minimum, specify: - acceptable Intel TDX verification status and advisory policy -- rejection of quotes with TDX debug mode enabled +- rejection of quotes with TDX debug mode enabled (bit 0 of `TD_ATTRIBUTES` in the verified quote) - required signer algorithm and identity matching rules - whether GPU evidence is required and the accepted verdict - accepted workload measurements and configuration rules