diff --git a/.vitepress/theme/generatedApiReferenceSpecs.ts b/.vitepress/theme/generatedApiReferenceSpecs.ts index ed42139c..5a34edfc 100644 --- a/.vitepress/theme/generatedApiReferenceSpecs.ts +++ b/.vitepress/theme/generatedApiReferenceSpecs.ts @@ -15,7 +15,7 @@ export const generatedApiReferenceSpecs: Record = { ], "response": { "status": "200 OK", - "code": "{\n \"org_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"balance\": \"42.125000\",\n \"credit_limit\": \"10.000000\",\n \"alert_threshold\": \"5.000000\"\n}" + "code": "{\n \"org_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"balance\": \"42.125\",\n \"credit_limit\": \"10\",\n \"alert_threshold\": \"5\"\n}" } }, "account/history": { @@ -70,7 +70,7 @@ export const generatedApiReferenceSpecs: Record = { "title": "Request body", "schema": "ImageEditParams", "fields": [ - { "name": "model", "type": "string", "required": true, "description": "Use gpt-image-2 on this endpoint." }, + { "name": "model", "type": "string", "required": true, "description": "One of gpt-image-2, gpt-image-2.5-flare, or gpt-image-2.5-sunburst." }, { "name": "prompt", "type": "string", "required": true, "description": "Text description of the requested edit." }, { "name": "image", "type": "file | file[]", "required": true, "description": "One or more source image files. Repeat the multipart field for multiple images." }, { "name": "mask", "type": "file", "required": false, "description": "Optional mask image for inpainting." }, @@ -120,6 +120,15 @@ export const generatedApiReferenceSpecs: Record = { { "name": "Audio", "type": "string", "required": false, "description": "audio/mpeg, audio/mp4, audio/wav, audio/x-wav, audio/ogg, audio/aac, audio/flac — maximum 50 MB." }, { "name": "Video", "type": "string", "required": false, "description": "video/mp4, video/quicktime, video/webm — maximum 500 MB." } ] + }, + { + "title": "Response", + "fields": [ + { "name": "url", "type": "string", "required": true, "description": "Stored media URL to pass to a supported model input." }, + { "name": "filename · size", "type": "string · integer", "required": true, "description": "Original multipart filename and file size in bytes." }, + { "name": "type · content_type", "type": "string", "required": true, "description": "Detected MIME type used when storing the file." }, + { "name": "duration_seconds", "type": "number", "required": false, "description": "Audio and video only: media duration in seconds, when it can be read from the file." } + ] } ], "examples": [ @@ -131,7 +140,7 @@ export const generatedApiReferenceSpecs: Record = { ], "response": { "status": "200 OK", - "code": "{\n \"url\": \"https://media.sandbase.ai/uploads/2026/08/550e8400-e29b-41d4-a716-446655440000.mp4\",\n \"filename\": \"reference.mp4\",\n \"size\": 1048576,\n \"type\": \"video/mp4\",\n \"content_type\": \"video/mp4\"\n}" + "code": "{\n \"url\": \"https://media.sandbase.ai/uploads/2026/08/550e8400-e29b-41d4-a716-446655440000.mp4\",\n \"filename\": \"reference.mp4\",\n \"size\": 1048576,\n \"type\": \"video/mp4\",\n \"content_type\": \"video/mp4\",\n \"duration_seconds\": 5.04\n}" }, "notes": [ { "title": "MIME detection", "description": "The server detects the file type from its bytes and filename. The optional type field does not force a MIME type." } @@ -471,7 +480,7 @@ export const generatedApiReferenceSpecs: Record = { "title": "Request body", "schema": "ImageGenerateParams", "fields": [ - { "name": "model", "type": "string", "required": true, "description": "Use gpt-image-2 on this endpoint." }, + { "name": "model", "type": "string", "required": true, "description": "One of gpt-image-2, gpt-image-2.5-flare, or gpt-image-2.5-sunburst." }, { "name": "prompt", "type": "string", "required": true, "description": "Text description of the image to generate." }, { "name": "n", "type": "integer", "required": false, "description": "Number of images to generate when supported." }, { "name": "size", "type": "string", "required": false, "description": "Output dimensions supported by the model." }, @@ -721,6 +730,15 @@ export const generatedApiReferenceSpecs: Record = { "description": "Opaque task ID returned by a billable operation, including an x-task-id response header when present." } ] + }, + { + "title": "Response", + "fields": [ + { "name": "settled", "type": "boolean", "required": true, "description": "Whether final cost settlement has completed. cost is not final until it is true." }, + { "name": "cost · estimated_cost", "type": "string", "required": true, "description": "USD amounts with six decimal places. estimated_cost is a best-effort reference before settlement." }, + { "name": "usage", "type": "object", "required": true, "description": "Token counts recorded for the task." }, + { "name": "pricing", "type": "object", "required": false, "description": "After settlement, when a discount was recorded: original_cost, discount_amount, discount_percent, and optional discount_type. original_cost minus discount_amount equals cost." } + ] } ], "examples": [ diff --git a/admin/rate-limits.md b/admin/rate-limits.md index dd283e97..bd4145bf 100644 --- a/admin/rate-limits.md +++ b/admin/rate-limits.md @@ -5,14 +5,13 @@ description: SandBase API rate limiting — how limits are enforced and how to h # Rate Limits -SandBase enforces rate limits to ensure fair usage and platform stability. Limiting is applied as a 1-minute sliding window using two layers, and a request must pass both to be served: +SandBase enforces rate limits to ensure fair usage and platform stability. The request limit is set per organization: an optional requests-per-minute cap, counted over a 1-minute sliding window and shared by every API key in the organization. -1. **Per-key limit** — an optional per-minute request cap on an individual API key. When unset, the key is only subject to the global limit. -2. **Global limit** — a platform-wide per-minute request cap. +It applies to `POST /v1/run`, `POST /v1/chat/completions`, `POST /v1/responses`, `POST /v1/messages`, `POST /v1/upload`, and the Gemini `generateContent`, `streamGenerateContent`, and `POST /v1beta/interactions` endpoints. Polling endpoints such as `GET /v1/run/{id}` are not counted. ## How to find your effective limit -SandBase does not publish a single fixed requests-per-minute number because the effective limit can vary by workspace, key, and platform capacity. Your key is governed by the lower of any custom per-key limit and the current platform-wide protection limit. +SandBase does not publish a single fixed requests-per-minute number because the limit is assigned per organization. Every API key in the organization counts toward the same limit, so creating more keys does not raise it. If your workload needs a guaranteed or higher limit, contact SandBase support with the organization, application, expected steady rate, and expected burst rate. Do not design a production client around an undocumented numeric default. @@ -22,11 +21,13 @@ When a limit is exceeded, the API aborts the request with HTTP 429. On the `/v1/ ```json { - "error": "API key rate limit exceeded" + "error": "organization rate limit exceeded" } ``` -The message is `"API key rate limit exceeded"` when the per-key limit is hit, or `"global rate limit exceeded"` when the platform-wide limit is hit. +Protocol-compatible endpoints such as `POST /v1/messages` and the Gemini endpoints return the same `429` in their own error envelope. + +API operations that are currently free to call also share a daily call allowance per organization that resets at 00:00 UTC. When it is used up, those calls return `429` with `"daily free API limit exceeded"`; retrying before the reset does not help. ::: warning Platform-generated rate-limit responses do not currently add `Retry-After` or remaining-quota headers. A compatible @@ -42,11 +43,6 @@ client-side backoff with jitter. 4. **Add jitter** — randomize retry delays so multiple workers do not retry at the same moment. 5. **Retry only safe operations** — repeating a generation can add cost, and state-changing requests can duplicate work. -## Per-Key Limit - -A per-key request cap can be configured for an individual key. When present, it is enforced independently of and in -addition to the platform-wide limit. +## Changing your limit -::: info -Setting a per-key rate limit is not currently exposed through the public key-management API. Contact support if you need a custom per-key limit configured. -::: +The organization limit is not currently exposed through the public API or Console. Contact support if your workload needs a different limit. diff --git a/agents/openai-compatibility.md b/agents/openai-compatibility.md index 8994b948..95e80755 100644 --- a/agents/openai-compatibility.md +++ b/agents/openai-compatibility.md @@ -226,7 +226,7 @@ SandBase runs every Session in its hosted cloud; there is no environment to choo | Area | Status | Details | |---|---|---| | `beta.agents.create / retrieve / update / list / delete` | Compatible, with extensions | Responses add `status`, `version`, `row_version`, `projection_status`, `runtime_profile`, `skills`, `mcp_connections`, and more. `delete` archives the Agent and returns `agent.deleted`. | -| `beta.agents.sessions.create / retrieve / update / list / delete` | Compatible, with limits | Requires a saved `agent_id`; `agent` only accepts `{"model": ...}`. `update` only accepts `metadata`. `delete` archives the Session. | +| `beta.agents.sessions.create / retrieve / update / list / delete` | Compatible, with limits | Accepts a saved `agent_id`, an inline `agent`, or both; `agent` accepts `model`, `instructions`, `runtime_profile`, `service_tier`, `tools`, `text`, `reasoning`, and `multi_agent`. `stream=True` streams the first turn and needs an initial `input`. `update` only accepts `metadata`. `delete` archives the Session. | | `sessions.events.stream` | Compatible | Live-only, like OpenAI. `Last-Event-ID` or `?after=` returns `400`. | | `sessions.events.create` | Compatible, with limits | Exactly one `agent.session.input.message` or `agent.session.input.cancel` event per request; the message has one user `input_text` part or a plain string. | | `sessions.items.list`, `sessions.turns.list / retrieve` | Compatible | Cursor pagination with `limit` 1-100, `after`, `order`. | @@ -243,9 +243,9 @@ These OpenAI SDK calls fail against SandBase. The message is the actual `error.m | SDK call | Result | |---|---| -| `sessions.create(...)` with `agent` but no `agent_id` (inline Agent) | `400`: `a saved SandBase agent_id and openai_hosted environment are required; standalone inline agents are unsupported` | -| `sessions.create(..., stream=True)` (also `with_result_collection()`) | `400`: `creation streaming is not supported; create the session then subscribe to events before sending input` | -| `sessions.create(..., environment={"type": "self_hosted"})` | `400`, same message as the inline Agent case | +| `sessions.create(..., stream=True)` without an initial `input` | `400 invalid_request` | +| `sessions.create(..., environment={"type": "self_hosted"})` | `400`: `only openai_hosted environment is supported` | +| `sessions.create(..., agent={...})` with a field other than `model`, `instructions`, `runtime_profile`, `service_tier`, `tools`, `text`, `reasoning`, and `multi_agent` | `400 invalid_request` | | `sessions.create(..., vault_ids=[...])` | `400 invalid_request` | | `sessions.update(..., agent={...})` | `400` (openai-python 3.13.0 has no `agent` parameter and raises `TypeError`) | | `agents.create(..., multi_agent={"enabled": True})` | `400`: `multi_agent is not supported` | diff --git a/api-reference/authentication.md b/api-reference/authentication.md index ee741a11..afa1804d 100644 --- a/api-reference/authentication.md +++ b/api-reference/authentication.md @@ -167,7 +167,12 @@ API keys are scoped to an organization. When you authenticate with a key: | 401 | `API key has been revoked` | Key was disabled | | 401 | `API key has expired` | Key past expiration date | | 402 or protocol-specific 403 | `API key spending limit exceeded` | Key-level spending limit reached; inspect the endpoint's documented error contract | -| 403 | `insufficient_scope` | Scoped key is not authorized for this endpoint | +| 403 | `API key is not authorized for this endpoint` | Scoped key used on an endpoint outside its scope | + +These messages arrive in the flat `{"error":""}` body, except `403` scope errors. On model and account +endpoints those use `{"error":{"code":"insufficient_scope","message":"API key is not authorized for this endpoint","request_id":"..."}}`; +on Agents platform resources they use the [Agents error envelope](/api-reference/errors#agents-platform-errors) with +`code` `insufficient_scope`. ## Security Best Practices @@ -217,8 +222,8 @@ const client = new OpenAI({ ## Rate Limiting by Key -An API key may have an optional per-key RPM cap. Every request is also subject to the current platform-wide RPM -protection, so creating more keys does not bypass the global limit. The API does not publish a universal numeric +Request limits are assigned per organization and shared by all of its API keys, so creating more keys does not raise +the limit. The API does not publish a universal numeric default or remaining-quota headers. See [Rate Limiting](/guides/rate-limiting) for details on limits and handling 429 responses. diff --git a/api-reference/errors.md b/api-reference/errors.md index 120b1bb2..634104fe 100644 --- a/api-reference/errors.md +++ b/api-reference/errors.md @@ -12,7 +12,7 @@ SandBase uses standard HTTP status codes, but it does not expose one universal e Core middleware and several `/v1/*` endpoints return a flat error: ```json -{"error":"API key rate limit exceeded"} +{"error":"organization rate limit exceeded"} ``` Agents platform resources (`/v1/agents*`, `/v1/services*`, `/v1/schedules*`, `/v1/mcp-connections*`, `/v1/secrets*`, `/v1/skills*`) return an OpenAI-style object with a stable `code`. See [Agents platform errors](#agents-platform-errors): @@ -46,7 +46,7 @@ Use the [OpenAPI specification](/openapi.yaml) for the exact status codes and sc | `404` | Model or resource is unavailable to the caller | Verify the current model ID or resource ID. | | `409` | Resource state conflict | Read the latest state before deciding whether a retry is safe. | | `422` | Semantically invalid resource input | Correct the documented field values. | -| `429` | Per-key or platform-wide request protection | Retry with bounded backoff and jitter when the operation is safe to repeat. | +| `429` | Organization request limit, video concurrency limit, or daily free API allowance | Retry with bounded backoff and jitter when the operation is safe to repeat. | | `500`, `502`, `503`, `504` | SandBase or upstream failure | Retry only when the operation is safe to repeat. | Not every endpoint documents every status in this table. The operation's OpenAPI response map is authoritative. @@ -59,7 +59,8 @@ Documented authentication messages include: - `invalid API key` - `API key has been revoked` - `API key has expired` -- `insufficient_scope` + +A scoped key used outside its scope returns `403` with a nested body instead: `{"error":{"code":"insufficient_scope","message":"API key is not authorized for this endpoint","request_id":"..."}}`. Agents platform resources return the same `code` in their own envelope. Use `Authorization: Bearer $SANDBASE_API_KEY` for standard endpoints. `POST /v1/messages` also accepts `x-api-key`; protocol-compatible Google endpoints document their own supported key headers. @@ -67,11 +68,13 @@ A revoked key cannot be re-enabled. Create a replacement, update the consuming a ## Rate limits -Documented flat messages include `API key rate limit exceeded` and `global rate limit exceeded`. SandBase does not publish one universal numeric limit because the effective limit can vary by key and platform capacity. +`organization rate limit exceeded` (`429`) means your organization reached its assigned requests-per-minute limit, which all of its API keys share. Protocol-compatible endpoints return it in their own envelope. SandBase does not publish one universal numeric limit because the limit is assigned per organization. + +`daily free API limit exceeded` (`429`) means your organization used its daily allowance for API operations that are currently free. It resets at 00:00 UTC, so do not retry before then. If a response includes `Retry-After`, respect it. Otherwise use bounded exponential backoff with jitter. See [Rate limits](/guides/rate-limiting) for request-smoothing patterns. -Video submissions have two additional checks. `organization concurrency limit exceeded` (`429`) means your organization already has as many tasks in progress as its account level allows; it clears when a running task finishes. `insufficient credit for estimated video cost` (`402`) means the estimated cost is more than your balance plus credit line; top up before retrying. See [Concurrency limits](/guides/concurrency-limits). +Task submissions have two additional checks. `organization concurrency limit exceeded` (`429`) means your organization already has as many tasks in progress as its account level allows; it clears when a running task finishes. It applies to every model type on `POST /v1/run` and `/v1/api/*`, and to video submissions on other endpoints. `insufficient credit for estimated video cost` (`402`) means the estimated cost is more than your balance plus credit line; top up before retrying. See [Concurrency limits](/guides/concurrency-limits). ## Retry safety @@ -112,6 +115,7 @@ Agents, Sessions, Services, Schedules, Skills, MCP connections, and Secrets retu | 409 | `session_input_pending` | Earlier input is still being delivered | Wait; do not resend with a new key | | 409 | `session_input_expired` | Input admission expired | Inspect history before sending again | | 409 | `session_environment_unavailable` | The execution environment is unavailable | Wait or create a new Session | +| 413 | `request_too_large` | The request body, or the Session configuration frozen from it, exceeds the size limit | Send a smaller body; do not retry unchanged | | 429 | `concurrency_limit` | A Service or Schedule already has a pending Run | Wait for it to finish or cancel it | | 429 | `quota_exceeded` | Agents quota exceeded | Retry later | | 502 | `executor_rejected`, `executor_contract_error` | The execution layer rejected the request or answered unexpectedly | Check the Agent configuration, then retry | diff --git a/api-reference/sessions/create.md b/api-reference/sessions/create.md index e5a1686c..23d5d79d 100644 --- a/api-reference/sessions/create.md +++ b/api-reference/sessions/create.md @@ -1,6 +1,6 @@ --- title: Create Session -description: Create a Session for a saved Agent with POST /v1/agents/sessions. +description: Create a Session for a saved or inline Agent with POST /v1/agents/sessions, optionally streaming the first turn. aside: false outline: false apiReference: @@ -8,7 +8,7 @@ apiReference: operation: Sessions method: POST path: /v1/agents/sessions - description: "Create a Session for a saved Agent version. The Agent configuration, Skill versions, and MCP connections are frozen for the life of the Session. Sessions always run in SandBase's hosted cloud, so there is no execution environment to choose. Inline Agents and stream=true return 400: create the Session, open the event stream, then send input. Execution admission (balance and organization status) is checked here. The OpenAI SDK requires an environment argument; see OpenAI compatibility (/agents/openai-compatibility)." + description: "Create a Session for a saved Agent version, or for an inline Agent defined in the request. The Agent configuration, Skill versions, and MCP connections are frozen for the life of the Session. Sessions always run in SandBase's hosted cloud, so there is no execution environment to choose. With stream=true and an initial input, the response is a 201 Server-Sent Events stream of the first turn instead of a JSON Session. Execution admission (balance and organization status) is checked here. The OpenAI SDK requires an environment argument; see OpenAI compatibility (/agents/openai-compatibility)." groups: - title: Headers fields: @@ -21,12 +21,16 @@ apiReference: fields: - name: agent_id type: string - required: true - description: Saved Agent ID (agt_ prefix). The Agent must be active with projection_status ready. + required: false + description: Saved Agent ID (agt_ prefix). The Agent must be active with projection_status ready. Send agent_id, agent, or both. + - name: agent + type: object + required: false + description: "Agent configuration. Accepted fields are model, instructions, runtime_profile (codex, claude, or mcode; default codex), service_tier, tools, text, reasoning, and multi_agent, validated as in Create Agent. Without agent_id it defines an inline Agent and model is required; inline Agents carry no Skills or MCP connections. With agent_id, each field you send replaces that field of the saved version for this Session only. Unknown fields return 400." - name: version type: integer required: false - description: Agent version. Defaults to the current version. + description: Agent version. Defaults to the current version. Not allowed with an inline Agent. - name: title type: string required: false @@ -43,6 +47,14 @@ apiReference: type: object required: false description: Up to 16 string pairs. + - name: stream + type: boolean + required: false + description: "When true, requires a non-empty input and returns 201 with a text/event-stream body. Events use the Stream events format (event: , data: ); the first, agent.session.created, carries the Session snapshot, followed by the turn's item and output events. Validation, admission, and other errors before the stream starts return the normal JSON error. Default false." + - name: environment + type: object + required: false + description: 'OpenAI SDK compatibility only. Omit it in direct HTTP calls; {"type":"openai_hosted"} is accepted and any other type returns 400.' - name: model_provider type: object required: false diff --git a/api-reference/volcengine-contents-generations.md b/api-reference/volcengine-contents-generations.md index d869a2de..486f5efe 100644 --- a/api-reference/volcengine-contents-generations.md +++ b/api-reference/volcengine-contents-generations.md @@ -54,6 +54,5 @@ Quick orientation: - Supported `model` values are `bytedance/seedance/2.0-official` and `bytedance/seedance/2.5-official`. Matching is exact. - Retrieve a task with `GET /api/v3/contents/generations/tasks/{task_id}` and poll until `status` leaves `queued` and `running`. -- Cancel a queued task, or remove a terminal task from subsequent lookups, with - `DELETE /api/v3/contents/generations/tasks/{task_id}`. The operation returns `204 No Content`; running and - already-cancelled tasks cannot be deleted. +- Only create and retrieve are available. Listing tasks and cancelling or deleting a task are not supported; keep the + returned task `id` to read the result. diff --git a/for-agents/errors.md b/for-agents/errors.md index afd20e93..f6312dea 100644 --- a/for-agents/errors.md +++ b/for-agents/errors.md @@ -14,7 +14,7 @@ specific message string. Core `/v1/*` middleware and several model or resource endpoints return a flat error: ```json -{"error":"API key rate limit exceeded"} +{"error":"organization rate limit exceeded"} ``` Agents platform resources (`/v1/agents*`, `/v1/services*`, `/v1/schedules*`, `/v1/mcp-connections*`, `/v1/secrets*`, @@ -47,7 +47,7 @@ Anthropic-compatible envelope: | `403` | The credential lacks permission, or a compatibility surface maps a spending failure to 403 | Inspect the documented envelope and use an authorized key. | | `404` | Model or resource not found | Verify the current model name or resource ID. | | `409` | Resource state conflict | Read the latest resource state before deciding whether a retry is safe. | -| `429` | Per-key or platform-wide request protection | Retry with bounded exponential backoff and jitter. | +| `429` | Organization request limit, video concurrency limit, or daily free API allowance | Retry with bounded exponential backoff and jitter. | | `500`, `502`, `503`, `504` | SandBase or upstream transient failure | Retry only when the operation is safe to repeat. | ## Authentication and rate-limit messages @@ -58,11 +58,13 @@ Documented authentication messages include: - `invalid API key` - `API key has been revoked` - `API key has expired` -- `insufficient_scope` -Documented rate-limit messages include `API key rate limit exceeded` and `global rate limit exceeded`. A `429` -response does not currently include `Retry-After` or `X-RateLimit-*` headers, and there is no published universal -RPM or concurrency default. +A scoped key used outside its scope returns `403` with a nested body instead: `{"error":{"code":"insufficient_scope","message":"API key is not authorized for this endpoint","request_id":"..."}}`. Agents platform resources return the same `code` in their own envelope. + +Documented rate-limit messages are `organization rate limit exceeded` (the organization's assigned requests-per-minute +limit, shared by all of its API keys) and `daily free API limit exceeded` (the organization's daily allowance for +currently free API operations, reset at 00:00 UTC). A `429` response does not currently include `Retry-After` or +`X-RateLimit-*` headers, and there is no published universal RPM or concurrency default. ## Model and resource lookup @@ -110,6 +112,7 @@ Agents, Sessions, Services, Schedules, Skills, MCP connections, and Secrets retu | 409 | `session_input_pending` | Earlier input is still being delivered | Wait; do not resend with a new key | | 409 | `session_input_expired` | Input admission expired | Inspect history before sending again | | 409 | `session_environment_unavailable` | The execution environment is unavailable | Wait or create a new Session | +| 413 | `request_too_large` | The request body, or the Session configuration frozen from it, exceeds the size limit | Send a smaller body; do not retry unchanged | | 429 | `concurrency_limit` | A Service or Schedule already has a pending Run | Wait for it to finish or cancel it | | 429 | `quota_exceeded` | Agents quota exceeded | Retry later | | 502 | `executor_rejected`, `executor_contract_error` | The execution layer rejected the request or answered unexpectedly | Check the Agent configuration, then retry | diff --git a/for-agents/full.md b/for-agents/full.md index c6a6b581..9b073a02 100644 --- a/for-agents/full.md +++ b/for-agents/full.md @@ -394,7 +394,7 @@ curl -X POST https://api.sandbase.ai/v1/agents/sessions \ -d '{"agent_id": "agt_..."}' ``` -Required: `agent_id`. Optional: `version`, `title`, `input` (a string, or an array with exactly one user message containing one `input_text` part), `model` or `agent.model`, `metadata`, `model_provider` (`{"type":"custom","base_url":"https://...","api_key":"..."}`; omitted means your calling key). `stream: true` and inline Agents return 400. Sessions always run in SandBase's hosted cloud, so there is no environment to choose; the OpenAI SDK still requires `environment={"type": "openai_hosted"}`. Returns `201` with `status` (`idle`, `in_progress`, `failed`), `lifecycle_status` (`active`, `archived`), `failure`, `source` (`direct`, `service`, `schedule`), and `row_version`. +Send `agent_id` (saved Agent), `agent` (inline Agent or per-Session overrides of `model`, `instructions`, `runtime_profile`, `service_tier`, `tools`, `text`, `reasoning`, and `multi_agent`), or both. Optional: `version` (saved Agents only), `title`, `input` (a string, or an array with exactly one user message containing one `input_text` part), `model` or `agent.model`, `metadata`, `model_provider` (`{"type":"custom","base_url":"https://...","api_key":"..."}`; omitted means your calling key). `stream: true` with a non-empty `input` returns `201` as a Server-Sent Events stream of the first turn, starting with `agent.session.created`. Sessions always run in SandBase's hosted cloud, so there is no environment to choose; the OpenAI SDK still requires `environment={"type": "openai_hosted"}`. Returns `201` with `status` (`idle`, `in_progress`, `failed`), `lifecycle_status` (`active`, `archived`), `failure`, `source` (`direct`, `service`, `schedule`), and `row_version`. ### GET /v1/agents/sessions/{session_id}/events — Stream Events (SSE) diff --git a/for-agents/index.md b/for-agents/index.md index e8d0fc49..385837aa 100644 --- a/for-agents/index.md +++ b/for-agents/index.md @@ -89,8 +89,8 @@ curl https://api.sandbase.ai/v1/tasks/{task_id}/cost \ ## Rate Limits -There is no published universal numeric default. Requests are subject to an optional per-key RPM cap and the -current platform-wide RPM protection. A `429` response does not include quota or `Retry-After` headers; use bounded +There is no published universal numeric default. Requests are subject to an optional requests-per-minute limit +assigned to your organization and shared by all of its API keys. A `429` response does not include quota or `Retry-After` headers; use bounded exponential backoff with jitter. See the [Error Guide](./errors). --- diff --git a/guides/concurrency-limits.md b/guides/concurrency-limits.md index 771ce82a..7f5319de 100644 --- a/guides/concurrency-limits.md +++ b/guides/concurrency-limits.md @@ -1,19 +1,22 @@ --- title: Concurrency limits -description: How many video tasks your organization can run at once, how the limit scales with your account level, and how to handle 429 and 402 on video submissions. +description: How many tasks your organization can run at once, how the limit scales with your account level, and how to handle 429 and 402 on video submissions. --- # Concurrency limits -Concurrency limits control how many video tasks your organization can run at the same time. Every organization has an account level, and the level sets its concurrency limit. When you reach the limit, new video submissions are rejected with `429` until one of your running tasks finishes. They are not queued. +Concurrency limits control how many tasks your organization can have in progress at the same time. Every organization has an account level, and the level sets its concurrency limit. When you reach the limit, new submissions on the checked endpoints below are rejected with `429` until one of your running tasks finishes. They are not queued. -The limit applies to the whole organization, across all API keys and all video models. It only affects video submissions. Chat completions, image, audio, and embedding requests are never rejected by the concurrency limit. +The limit applies to the whole organization, across all API keys. Which submissions are checked depends on the endpoint: + +- `POST /v1/run` and Platform API calls through `/v1/api/{vendor}/...` are checked for every model type, including image, audio, LLM, and data API operations. +- Other endpoints, such as Chat Completions, Responses, Messages, OpenAI Images, and the protocol-compatible Gemini and Volcengine endpoints, check only video submissions. ## How it works -When you submit a video task, SandBase counts the tasks your organization currently has in progress, meaning tasks in the `pending` or `running` state. If that count is already at your limit, the submission is rejected immediately with `429`. No task is created and nothing is charged. +When you submit a checked task, SandBase counts the tasks your organization currently has in progress, meaning tasks in the `pending` or `running` state. If that count is already at your limit, the submission is rejected immediately with `429`. No task is created and nothing is charged. -The count covers every in-progress request in your organization, not just video. A streaming chat completion that is still open when you submit a video uses one slot. If the same organization runs long streaming chats and video jobs, account for both when planning capacity. +The count covers every in-progress request in your organization, not just video. A streaming chat completion that is still open when you submit a checked task uses one slot. If the same organization runs long streaming chats and video jobs, account for all of them when planning capacity. A slot frees up as soon as a task reaches a final state (`completed`, `failed`, or `timeout`). There is nothing to release on your side. Because slots free up when tasks finish, not on a timer, this `429` is different from a per-minute [rate limit](/guides/rate-limiting). Waiting a fixed interval doesn't guarantee a slot. Wait for one of your own tasks to finish instead. diff --git a/guides/rate-limiting.md b/guides/rate-limiting.md index a18435cb..7ba9b6fc 100644 --- a/guides/rate-limiting.md +++ b/guides/rate-limiting.md @@ -5,9 +5,9 @@ description: Handle SandBase API rate limits safely with bounded retries, jitter # Rate limits -SandBase can limit requests per API key and across the platform. Upstream model providers can also return their own -`429 Too Many Requests` responses. Limits may differ by account, key, endpoint, model, and provider, so do not hardcode -a quota unless SandBase has assigned one to your workload. +SandBase can assign your organization a requests-per-minute limit. It is shared by every API key in the organization +and counted over a one-minute sliding window. Upstream model providers can also return their own +`429 Too Many Requests` responses. Do not hardcode a quota unless SandBase has assigned one to your workload. ## Handle a 429 response @@ -17,11 +17,13 @@ Platform-generated `/v1/*` rate-limit responses use a flat error body: HTTP/1.1 429 Too Many Requests Content-Type: application/json -{"error":"API key rate limit exceeded"} +{"error":"organization rate limit exceeded"} ``` -The message can instead report a platform-wide limit. A provider-originated response may use the compatible endpoint's -error shape and may include provider rate-limit headers. +The organization limit applies to `POST /v1/run`, `POST /v1/chat/completions`, `POST /v1/responses`, `POST /v1/messages`, `POST /v1/upload`, and the Gemini `generateContent`, `streamGenerateContent`, and `POST /v1beta/interactions` endpoints. Polling endpoints such as +`GET /v1/run/{id}` are not counted. Protocol-compatible endpoints return the same `429` in their own error envelope, +such as the Anthropic or Google error shape. A provider-originated response may use the compatible endpoint's error +shape and may include provider rate-limit headers. `Retry-After` and remaining-quota headers are not guaranteed. If `Retry-After` is present, honor it. Otherwise, use exponential backoff with jitter and a maximum attempt count. @@ -88,6 +90,21 @@ async def run_one(client, prompt): Concurrency is not the same as requests per minute. Tune both against observed latency and the capacity assigned to your application. +## Daily allowance for free APIs + +API operations that are currently free to call share a daily call allowance per organization. The count resets at +00:00 UTC. When the allowance is used up, further calls to free operations return: + +```http +HTTP/1.1 429 Too Many Requests +Content-Type: application/json + +{"error":"daily free API limit exceeded"} +``` + +Retrying within the same UTC day does not help. Resume after the reset, or contact SandBase support if your workload +needs a higher allowance. Paid API operations are not counted. + ::: tip Video tasks Video submissions are also limited by how many tasks your organization can run at once, based on its account level. That `429` says `organization concurrency limit exceeded` and clears when a running task finishes, not on a timer. See diff --git a/model-api-reference/official-native-api/index.md b/model-api-reference/official-native-api/index.md index d5385f6d..d682a3c7 100644 --- a/model-api-reference/official-native-api/index.md +++ b/model-api-reference/official-native-api/index.md @@ -62,7 +62,8 @@ option. Pick `2.5-official` when you need 1080p. | --- | --- | | `POST` | `/api/v3/contents/generations/tasks` | | `GET` | `/api/v3/contents/generations/tasks/{task_id}` | -| `DELETE` | `/api/v3/contents/generations/tasks/{task_id}` | + +Listing tasks and cancelling or deleting a task are not supported. Store the returned task `id` to retrieve the result. Authenticate every request with the same API key used to create the task: @@ -118,18 +119,6 @@ curl https://api.sandbase.ai/api/v3/contents/generations/tasks/158d2649-e01d-45b The generated video is available at `content.video_url`. -## Cancel or remove a task - -Cancel a queued task, or remove a terminal task from subsequent lookups, with the same API key: - -```bash -curl -X DELETE https://api.sandbase.ai/api/v3/contents/generations/tasks/TASK_ID \ - -H "Authorization: Bearer $SANDBASE_API_KEY" -``` - -The operation returns `204 No Content`. Running and already-cancelled tasks cannot be deleted; the API returns an -error instead. Deleting a task does not affect other tasks or model availability. - ## Choose an input workflow The API supports seven common input workflows. Each item in `content` has a `type`; the combination of item types and diff --git a/public/llms-full.txt b/public/llms-full.txt index 63d55787..4647bd45 100644 --- a/public/llms-full.txt +++ b/public/llms-full.txt @@ -320,9 +320,9 @@ Error bodies are protocol-specific. Core middleware may return {"error":"message | invalid API key | 401 | Key is unknown | | API key has been revoked | 401 | Key is disabled | | API key has expired | 401 | Key is past expiration | -| insufficient_scope | 403 | Scoped key is not authorized for this endpoint | -| API key rate limit exceeded | 429 | Optional per-key RPM cap exceeded; back off with jitter | -| global rate limit exceeded | 429 | Platform-wide RPM protection exceeded; back off with jitter | +| insufficient_scope | 403 | Scoped key not authorized; nested {"error":{"code":"insufficient_scope","message":...}} body | +| organization rate limit exceeded | 429 | Organization RPM limit exceeded (shared by all keys); back off with jitter | +| daily free API limit exceeded | 429 | Daily allowance for free API operations used up; resets 00:00 UTC | | API key spending limit exceeded | 402 or protocol-specific 403 | Key spending limit reached | | HTTP 400 | Invalid or unsupported input; fix the request using the endpoint and model schema | | HTTP 404 | Model or resource not found; verify current IDs before retrying | diff --git a/public/openapi.yaml b/public/openapi.yaml index 6094848a..db9ab294 100644 --- a/public/openapi.yaml +++ b/public/openapi.yaml @@ -620,31 +620,6 @@ paths: '404': { description: Model or upstream resource not found, content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } } '429': { description: Provider rate limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } } '500': { description: Prediction lifecycle or provider error, content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } } - get: - operationId: listVolcengineContentsGenerationTasks - tags: [Official Native API] - summary: List content generation tasks - description: Lists non-deleted native video tasks created by the same organization and API key, newest first. - security: - - BearerAuth: [] - parameters: - - { name: page_num, in: query, schema: { type: integer, minimum: 1, maximum: 500, default: 1 } } - - { name: page_size, in: query, schema: { type: integer, minimum: 1, maximum: 500, default: 10 } } - - name: filter.task_ids - in: query - description: Task IDs, supplied as repeated parameters or a comma-separated value. - schema: { type: array, items: { type: string } } - style: form - explode: true - - { name: filter.model, in: query, schema: { type: string } } - - { name: filter.status, in: query, schema: { type: string, enum: [queued, running, cancelled, succeeded, failed, expired] } } - responses: - '200': { description: Task page, content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineTaskList' } } } } - '401': { description: 'Missing, invalid, revoked, or expired API key', content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } } - '402': { description: Organization balance and credit are exhausted, content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } } - '403': { description: API Key spending limit reached or organization disabled, content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } } - '500': { description: Task lookup failed, content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } } - /api/v3/contents/generations/tasks/{task_id}: parameters: - name: task_id @@ -665,22 +640,6 @@ paths: '403': { description: API Key spending limit reached or organization disabled, content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } } '404': { description: 'Task is unknown, deleted, not native, or not owned by the current API key', content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } } '500': { description: Task lookup failed, content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } } - delete: - operationId: deleteVolcengineContentsGenerationTask - tags: [Official Native API] - summary: Cancel or delete a content generation task - description: Cancels a queued task or hides a terminal task. Running and already-cancelled tasks cannot be deleted. - security: - - BearerAuth: [] - responses: - '204': { description: Task cancelled or deleted } - '400': { description: 'Task is running, already cancelled, or otherwise cannot be deleted in its current state', content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } } - '401': { description: 'Missing, invalid, revoked, or expired API key', content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } } - '402': { description: Organization balance and credit are exhausted, content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } } - '403': { description: API Key spending limit reached or organization disabled, content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } } - '404': { description: 'Task is unknown, deleted, not native, or not owned by the current API key', content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } } - '500': { description: Task update failed, content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } } - # --- Account ---------------------------------------------------- /v1/account/balance: get: @@ -1664,7 +1623,7 @@ paths: operationId: createSession tags: [Sessions] summary: Create session - description: 'Create a Session for a saved Agent. The Agent configuration, Skills, and MCP connections are frozen for the lifetime of the Session. stream=true is rejected: create the Session, open the event stream, then send input. Execution admission (balance and organization status) is checked here.' + description: 'Create a Session for a saved Agent, or for an inline Agent defined in the request. The Agent configuration, Skills, and MCP connections are frozen for the lifetime of the Session. With stream=true and an initial input, the 201 response is a Server-Sent Events stream of the first turn instead of a JSON Session. Execution admission (balance and organization status) is checked here.' security: - BearerAuth: [] - AgentsApiKey: [] @@ -1678,11 +1637,13 @@ paths: schema: { $ref: '#/components/schemas/SessionCreateRequest' } responses: '201': - description: 'Session created' + description: 'Session created. With stream=true, a text/event-stream of the first turn whose first event, agent.session.created, carries the Session.' content: application/json: schema: { $ref: '#/components/schemas/Session' } - '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } } + text/event-stream: + schema: { type: string, description: 'Server-Sent Events in the Stream events format: an event line with the event type and a data line with its JSON payload.' } + '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, an unsupported option, or stream=true without input)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } } '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } } '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } } '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } } @@ -4231,6 +4192,10 @@ components: content_type: type: string description: Detected MIME type used when storing the file. + duration_seconds: + type: number + exclusiveMinimum: 0 + description: Media duration in seconds. Returned for audio and video when the duration can be read from the file; omitted otherwise. CreateAssetRequest: type: object @@ -4308,7 +4273,28 @@ components: example: '0.001234' usage: $ref: '#/components/schemas/TaskCostUsage' - + pricing: + $ref: '#/components/schemas/TaskCostPricing' + TaskCostPricing: + type: object + description: Present only after settlement when a discount breakdown was recorded. original_cost minus discount_amount equals cost. + required: [original_cost, discount_amount, discount_percent] + properties: + original_cost: + type: string + pattern: '^\d+\.\d{6}$' + description: USD cost before discounts, with six decimal places. + discount_amount: + type: string + pattern: '^\d+\.\d{6}$' + description: USD discount applied, with six decimal places. + discount_percent: + type: number + minimum: 0 + description: Discount percentage, for example 25 for 25 percent off. + discount_type: + type: string + description: Kind of discount applied. Omitted when not recorded. TaskCostUsage: type: object required: [prompt_tokens, completion_tokens, total_tokens, cached_tokens, cache_creation_tokens, reasoning_tokens] @@ -5142,13 +5128,6 @@ components: code: { type: string } message: { type: string } - VolcengineTaskList: - type: object - required: [items, total] - properties: - items: { type: array, items: { $ref: '#/components/schemas/VolcengineTask' } } - total: { type: integer, minimum: 0 } - VolcengineError: type: object required: [error] @@ -5449,7 +5428,7 @@ components: code: type: string description: Stable machine-readable code. Unknown future codes should be treated by HTTP status. - enum: [unauthorized, insufficient_scope, authorization_unavailable, org_disabled, spending_limit_exceeded, quota_exceeded, concurrency_limit, admission_unavailable, invalid_request, not_found, conflict, session_input_pending, session_input_expired, session_environment_unavailable, tenant_provisioning_failed, tenant_unavailable, executor_rejected, executor_unavailable, executor_contract_error, skill_not_found, skill_unavailable, skill_runtime_incompatible, storage_unavailable, internal_error] + enum: [unauthorized, insufficient_scope, authorization_unavailable, org_disabled, spending_limit_exceeded, quota_exceeded, concurrency_limit, admission_unavailable, invalid_request, not_found, conflict, session_input_pending, session_input_expired, session_environment_unavailable, tenant_provisioning_failed, tenant_unavailable, executor_rejected, executor_unavailable, executor_contract_error, skill_not_found, skill_unavailable, skill_runtime_incompatible, storage_unavailable, internal_error, request_too_large] param: { type: 'null' } message: { type: string } AgentsCursorList: @@ -5707,12 +5686,27 @@ components: SessionCreateRequest: type: object additionalProperties: false - required: [agent_id] + description: Send agent_id, agent, or both. + anyOf: + - { required: [agent_id] } + - { required: [agent] } properties: - agent_id: { type: string, description: "Saved Agent ID (`agt_` prefix). Inline Agents are not supported." } - version: { type: integer, minimum: 1, description: "Agent version. Defaults to the current version." } - title: { type: string } - agent: { type: object, additionalProperties: false, properties: { model: { type: string } }, description: "OpenAI SDK form of the model override." } + agent_id: { type: string, description: "Saved Agent ID (`agt_` prefix)." } + version: { type: integer, minimum: 1, description: "Agent version. Defaults to the current version. Not allowed with an inline Agent." } + title: { type: string, maxLength: 512 } + agent: + type: object + additionalProperties: false + description: "Agent configuration, validated as in Create Agent. Without agent_id it defines an inline Agent and model is required; inline Agents carry no Skills or MCP connections. With agent_id, each field sent replaces that field of the saved version for this Session only." + properties: + model: { type: string, maxLength: 200 } + instructions: { type: [string, 'null'] } + runtime_profile: { type: string, enum: [codex, claude, mcode], description: Defaults to codex for an inline Agent. } + service_tier: { type: [string, 'null'] } + tools: { type: array, items: { type: object } } + text: { type: object } + reasoning: { type: object } + multi_agent: { type: object } model: { type: string, maxLength: 200, description: "Model override for this Session. Must equal agent.model when both are sent." } environment: { type: object, additionalProperties: false, properties: { type: { type: string, const: openai_hosted } }, description: "OpenAI SDK compatibility only. Sessions always run in the SandBase hosted cloud, so omit this field in direct HTTP calls. The OpenAI SDK requires it; {\"type\":\"openai_hosted\"} is accepted and any other value returns 400." } input: @@ -5724,7 +5718,7 @@ components: maxItems: 1 items: { type: object, required: [role, content], properties: { role: { type: string, const: user }, content: { type: array, items: { type: object, required: [type, text], properties: { type: { type: string, const: input_text }, text: { type: string } } } } } } metadata: { type: object, maxProperties: 16, additionalProperties: { type: string, maxLength: 512 } } - stream: { type: boolean, const: false, description: "true returns 400." } + stream: { type: boolean, default: false, description: "true requires a non-empty input and returns a 201 text/event-stream of the first turn." } model_provider: { $ref: '#/components/schemas/ModelProvider' } SessionFailure: type: object diff --git a/scripts/validate-doc-content.mjs b/scripts/validate-doc-content.mjs index dc4f9d6c..2655bce2 100644 --- a/scripts/validate-doc-content.mjs +++ b/scripts/validate-doc-content.mjs @@ -218,8 +218,9 @@ const dataForSeoCount = dataForSeoSidebar.match(/"operationCount":\s*(\d+)/)?.[1 assert.equal(Number(dataForSeoCount), dataForSeoPages, 'DataForSEO sidebar count must match its operation pages') const nativeReference = readFileSync('model-api-reference/official-native-api/index.md', 'utf8') -assert.match(nativeReference, /`DELETE`\s*\|\s*`\/api\/v3\/contents\/generations\/tasks\/\{task_id\}`/, 'Official Native API must document task deletion') -assert.match(nativeReference, /Cancel or remove a task/, 'Official Native API must explain task cancellation semantics') +// Task listing and deletion were removed from the apiserver; the reference must not advertise them. +assert.doesNotMatch(nativeReference, /`DELETE`\s*\|\s*`\/api\/v3\/contents\/generations\/tasks/, 'Official Native API must not document the removed task deletion') +assert.match(nativeReference, /Listing tasks and cancelling or deleting a task are not supported/, 'Official Native API must state that only create and retrieve are available') const seedanceAssetReference = readFileSync('model-api-reference/official-native-api/bytedance/media-assets.md', 'utf8') assert.match(seedanceAssetReference, /`POST \/v1\/assets`/, 'Seedance Asset guide must document Asset registration') diff --git a/scripts/validate-public-api-surface.mjs b/scripts/validate-public-api-surface.mjs index 352023e6..a6926442 100644 --- a/scripts/validate-public-api-surface.mjs +++ b/scripts/validate-public-api-surface.mjs @@ -308,10 +308,11 @@ assert.match(modelApiOverview, /\/api-reference\/gemini-interactions/, 'Model AP const volcengineCollection = openapiDocument.paths['/api/v3/contents/generations/tasks'] const volcengineTask = openapiDocument.paths['/api/v3/contents/generations/tasks/{task_id}'] assert.ok(volcengineCollection?.post, 'Public OpenAPI must expose Volcengine task creation') -assert.ok(volcengineCollection?.get, 'Public OpenAPI must expose Volcengine task listing') assert.ok(volcengineTask?.get, 'Public OpenAPI must expose Volcengine task lookup') -assert.ok(volcengineTask?.delete, 'Public OpenAPI must expose Volcengine task deletion') -for (const operation of [volcengineCollection.post, volcengineCollection.get, volcengineTask.get, volcengineTask.delete]) { +// List and delete were removed from the apiserver (sandbase-monorepo 2f8deef1b2); keep them out of the contract. +assert.ok(!volcengineCollection?.get, 'Public OpenAPI must not expose the removed Volcengine task listing') +assert.ok(!volcengineTask?.delete, 'Public OpenAPI must not expose the removed Volcengine task deletion') +for (const operation of [volcengineCollection.post, volcengineTask.get]) { assert.deepEqual(operation.security, [{ BearerAuth: [] }], 'Volcengine tasks must remain Bearer-only') } assert.equal( @@ -320,8 +321,7 @@ assert.equal( 'Volcengine task creation must preserve its implemented HTTP 200 response', ) assert.ok(!volcengineCollection.post.responses['201'] && !volcengineCollection.post.responses['202'], 'Volcengine task creation is not a 201 or 202 operation') -assert.ok(!volcengineTask.delete.responses['204']?.content, 'Volcengine task deletion 204 must not document a body') -for (const operation of [volcengineCollection.post, volcengineCollection.get, volcengineTask.get, volcengineTask.delete]) { +for (const operation of [volcengineCollection.post, volcengineTask.get]) { for (const [status, response] of Object.entries(operation.responses)) { if (Number(status) < 400) continue assert.equal( @@ -332,9 +332,9 @@ for (const operation of [volcengineCollection.post, volcengineCollection.get, vo } } assert.deepEqual( - volcengineCollection.get.parameters.find((parameter) => parameter.name === 'filter.status')?.schema?.enum, + openapiDocument.components.schemas.VolcengineTask?.properties?.status?.enum, ['queued', 'running', 'cancelled', 'succeeded', 'failed', 'expired'], - 'Volcengine task listing must document every implemented native status', + 'Volcengine task lookup must document every implemented native status', ) assert.match(officialNativeSidebar, /\/model-api-reference\/official-native-api\//, 'Official Native API sidebar must link to its reference') assert.doesNotMatch(sidebar, /\/api-reference\/volcengine-contents-generations/, 'Inference navigation must not duplicate the Official Native API reference')