From bb4d8b933950c13cefa16d02cfbefed74172c3ad Mon Sep 17 00:00:00 2001 From: barckcode Date: Sat, 10 Oct 2026 22:17:54 +0200 Subject: [PATCH 1/9] docs(api): start the runs API reference branch Co-Authored-By: Claude Opus 5.5 From 6517617bffb095b2c29b77b1ec76c400792853a5 Mon Sep 17 00:00:00 2001 From: barckcode Date: Sat, 10 Oct 2026 22:24:18 +0200 Subject: [PATCH 2/9] docs(api): document Runs, Workspaces and platform tokens in the API reference Adds the Runs tag (create, list, retrieve, events stream or JSON pages, cancel) and a read-only Workspaces tag (list, retrieve by id) to the OpenAPI spec, with the Run, RunEvent (ten kinds), RunResult and Workspace schemas taken from cloud-api origin/main. Documents the platform token (nan_pat_) as a second bearer scheme next to the API key and marks which operations accept which. The workspace routes use api.nan.builders outside /v1. Tests pin the surface, credentials, limits and SSE frames, and validate every new example against its schema. Co-Authored-By: Claude Opus 5.5 --- src/data/openapi.json | 3409 ++++++++++++++++++++++++++++------- src/lib/openapiRuns.test.ts | 334 ++++ src/lib/openapiSpec.test.ts | 25 +- 3 files changed, 3090 insertions(+), 678 deletions(-) create mode 100644 src/lib/openapiRuns.test.ts diff --git a/src/data/openapi.json b/src/data/openapi.json index 0a75d77..06f0167 100644 --- a/src/data/openapi.json +++ b/src/data/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "NaN API", "version": "1.0.0", - "description": "Open models on a shared EU inference cluster. Zero logs.\n\nThe NaN API is OpenAI-compatible: predictable, resource-oriented URLs, JSON request and response bodies, and standard HTTP verbs and status codes. Point any OpenAI SDK at our base URL and your existing code keeps working. Change the base URL and the API key, and that's it.\n\nOne schema across every model, so you only learn the API once. Change the `model` field to switch models; everything else stays the same.\n\n- Base URL: `https://api.nan.builders/v1`\n- OpenAPI spec: this document. Import it into Postman, Insomnia, or your own tooling.\n\nIf you use the [Helmcode](https://helmcode.com) enterprise service, the base URL is `https://api.helmcode.com/v1` instead. Every other endpoint is identical.\n\n## Authentication\n\nEvery request authenticates with an API key, sent as a Bearer token:\n\n```\nAuthorization: Bearer $NAN_API_KEY\n```\n\nYou must be a NaN community member. Generate your key from user settings, under \"API Keys\", on the [platform](https://cloud.nan.builders/). The key is personal and non-transferable. Keep it secret: never embed one in client-side code or commit it to source control. Requests must go over HTTPS; calls over plain HTTP fail.\n\n## Making requests\n\nThe API is OpenAI-compatible, so point an official OpenAI SDK at our base URL and change nothing else:\n\n```python\nfrom openai import OpenAI\n\nclient = OpenAI(\n api_key=\"$NAN_API_KEY\",\n base_url=\"https://api.nan.builders/v1\",\n)\n\nresp = client.chat.completions.create(\n model=\"deepseek-v4-flash\",\n messages=[{\"role\": \"user\", \"content\": \"Hello\"}],\n)\nprint(resp.choices[0].message.content)\n```\n\n## Streaming\n\nChat responses can stream token-by-token. Set `\"stream\": true` on `/chat/completions` and the response arrives as Server-Sent Events: each event is a `data:` line carrying a `chat.completion.chunk`, with the new text in `choices[0].delta.content`. A final `data: [DONE]` line ends the stream. `/chat/completions` streams incrementally on every chat model. `/responses` streams incrementally only on `deepseek-v4-flash`; on `qwen3.6` and `gemma4` it holds the whole answer and sends it in one burst at the end.\n\n## Rate limits\n\n{{RATE_LIMITS}}\n\nImage endpoints run on their own budget, separate from the model endpoints: 20 requests per minute and 100 requests per month. The usage endpoint is metered separately too: {{USAGE_RATE_LIMIT}} requests per minute per member. Exceed any limit and you get a `429`.\n\n## Errors\n\nNaN uses conventional HTTP status codes: `2xx` on success, `4xx` for a problem with the request (a missing parameter, an invalid key, an unavailable model) and `5xx` for a server-side error. Every error returns a JSON body in the OpenAI shape:\n\n```json\n{\n \"error\": {\n \"message\": \"The model 'foo' does not exist.\",\n \"type\": \"invalid_request_error\",\n \"param\": \"model\",\n \"code\": \"model_not_found\"\n }\n}\n```\n\n`message` is human-readable, `param` names the offending field when applicable, and `code` is a short machine-readable string you can branch on.\n\n| Status | Meaning | `code` |\n| --- | --- | --- |\n| `400` | Invalid or malformed parameter (`param` says which); content blocked by the safety filter; a `max_tokens` above what the model can generate (a generic \"Invalid request\" message today); or a request that does not fit the model's context window. On the models hosted on the cluster (`qwen3.6`, `gemma4`) the overflow message is \"Context length exceeded for model '...'\" with the model's limit; on the other models it is usually that text, but it can arrive as the generic \"Invalid request. Check your request parameters.\" | `\"400\"` (with `type: invalid_request_error`) · `content_policy_violation` |\n| `401` | Missing or invalid API key, or a key whose tier does not reach the requested model (`glm5.3`): \"This API key does not have access to the requested model\", `type: auth_error`. Measured 2026-09-12. | `invalid_api_key` |\n| `402` | The token allowance is spent on a model that carries one. Not retryable: the counter returns to zero when that model's quota period does, the calendar month for the models counted per month and your billing period for `glm5.3`. | `monthly_cap_reached` |\n| `403` | Your tier can't access this endpoint. Image generation requires inference membership. A model your tier cannot reach answers `401`, not this. | `tier_restricted` |\n| `404` | The requested model doesn't exist. | `model_not_found` |\n| `429` | Rate limit hit (`rpm_limit`, `max_parallel_requests`), the rolling 4h token budget of `glm5.3`, or a quota exhausted. | `rate_limit_exceeded` · `insufficient_quota` · `quota_exceeded` |\n| `500` | Something went wrong on our side (includes upstream model errors). | (none) |\n| `524` | Timeout, typical with large audio files on `/audio/transcriptions`. | (none) |\n\nRetry `429` and `5xx` responses with exponential backoff. Don't retry `400`, `401`, `403`, or `404` blindly (a context overflow is not retried automatically either: shorten the input or pick a model with a larger window): they'll fail the same way every time until you change the request. `402` cannot be fixed by repetition either: it clears when that model's quota period resets.\n\n## Model catalog\n\nEvery endpoint takes a `model` id. Capabilities vary by model:\n\n| Model | Use for | Capabilities |\n| --- | --- | --- |\n| `deepseek-v4-flash` | Chat, vision, reasoning | Streaming, tool calling, reasoning, image input, 1M-token context. 3B tokens/month per member |\n| `mimo-v2.6-flash` | Chat, vision, audio | Streaming, tool calling, reasoning, image input, audio input, 1M-token context. 1.0B tokens/month per member |\n| `qwen3.8-flash` | Chat, vision, agents | Streaming, tool calling, reasoning (on by default), vision, 1M-token context. 500M tokens/month per member |\n| `glm5.3-flash` | Chat, vision, agents | Streaming, tool calling, reasoning, vision, 1M-token context. 2B tokens/month per member |\n| `qwen3.6` | Chat, agents | Streaming, tool calling, vision, reasoning (opt-out, returns `reasoning_content`) |\n| `gemma4` | Chat, vision, agents | Streaming, tool calling, vision, reasoning (opt-in) |\n| `glm5.3` | Coding, long-horizon agents | Streaming, tool calling, reasoning trace, image, audio and video input, 1M-token context. Premium tier only |\n| `qwen3-embedding` | Embeddings | 4096-dimension vectors |\n| `rerank` | RAG reranking | Qwen3-Reranker-8B, 100+ languages |\n| `kokoro` | Text-to-speech | Multiple voices and audio formats |\n| `whisper` | Speech-to-text | Transcription with word/segment timestamps |\n| `flux-2-klein` | Image generation | Text-to-image and image-to-image |\n| `qwen-image-2.1` | Image generation (text→image) | 512-1280 px, 1-4 per request, seed 0-2147483647. 100 images/month per member (shared pool with flux-2-klein) |\n\n`glm5.3` is served only to keys on the GLM 5.3 premium tier; every other model is available to any inference member. Call [List models](#tag/Models) for the exact set available to your key.\n\n## Versioning & compatibility\n\nThe API tracks the OpenAI API surface, so OpenAI SDKs and tools work against `https://api.nan.builders/v1` unchanged. This reference documents the stable public `/v1` endpoints, and we add capabilities without breaking existing fields.", + "description": "Open models on a shared EU inference cluster. Zero logs.\n\nThe NaN API is OpenAI-compatible: predictable, resource-oriented URLs, JSON request and response bodies, and standard HTTP verbs and status codes. Point any OpenAI SDK at our base URL and your existing code keeps working. Change the base URL and the API key, and that's it.\n\nOne schema across every model, so you only learn the API once. Change the `model` field to switch models; everything else stays the same.\n\n- Base URL: `https://api.nan.builders/v1`\n- OpenAPI spec: this document. Import it into Postman, Insomnia, or your own tooling.\n\nIf you use the [Helmcode](https://helmcode.com) enterprise service, the base URL is `https://api.helmcode.com/v1` instead. Every other endpoint is identical.\n\n## Authentication\n\nEvery request authenticates with an API key, sent as a Bearer token:\n\n```\nAuthorization: Bearer $NAN_API_KEY\n```\n\nYou must be a NaN community member. Generate your key from user settings, under \"API Keys\", on the [platform](https://cloud.nan.builders/). The key is personal and non-transferable. Keep it secret: never embed one in client-side code or commit it to source control. Requests must go over HTTPS; calls over plain HTTP fail.\n\n### Platform tokens\n\nFor scripts that only manage [Runs](#tag/runs) and list your [Workspaces](#tag/workspaces), create a **platform token** (`nan_pat_...`) under \"Tokens\" in your settings, and send it the same way: `Authorization: Bearer $NAN_TOKEN`. A platform token never works for inference: the model endpoints refuse it. An API key works on both. Each endpoint lists the credentials it accepts.\n\n## Making requests\n\nThe API is OpenAI-compatible, so point an official OpenAI SDK at our base URL and change nothing else:\n\n```python\nfrom openai import OpenAI\n\nclient = OpenAI(\n api_key=\"$NAN_API_KEY\",\n base_url=\"https://api.nan.builders/v1\",\n)\n\nresp = client.chat.completions.create(\n model=\"deepseek-v4-flash\",\n messages=[{\"role\": \"user\", \"content\": \"Hello\"}],\n)\nprint(resp.choices[0].message.content)\n```\n\n## Streaming\n\nChat responses can stream token-by-token. Set `\"stream\": true` on `/chat/completions` and the response arrives as Server-Sent Events: each event is a `data:` line carrying a `chat.completion.chunk`, with the new text in `choices[0].delta.content`. A final `data: [DONE]` line ends the stream. `/chat/completions` streams incrementally on every chat model. `/responses` streams incrementally only on `deepseek-v4-flash`; on `qwen3.6` and `gemma4` it holds the whole answer and sends it in one burst at the end.\n\n## Rate limits\n\n{{RATE_LIMITS}}\n\nImage endpoints run on their own budget, separate from the model endpoints: 20 requests per minute and 100 requests per month. The usage endpoint is metered separately too: {{USAGE_RATE_LIMIT}} requests per minute per member. Exceed any limit and you get a `429`.\n\n## Errors\n\nNaN uses conventional HTTP status codes: `2xx` on success, `4xx` for a problem with the request (a missing parameter, an invalid key, an unavailable model) and `5xx` for a server-side error. Every error returns a JSON body in the OpenAI shape:\n\n```json\n{\n \"error\": {\n \"message\": \"The model 'foo' does not exist.\",\n \"type\": \"invalid_request_error\",\n \"param\": \"model\",\n \"code\": \"model_not_found\"\n }\n}\n```\n\n`message` is human-readable, `param` names the offending field when applicable, and `code` is a short machine-readable string you can branch on.\n\n| Status | Meaning | `code` |\n| --- | --- | --- |\n| `400` | Invalid or malformed parameter (`param` says which); content blocked by the safety filter; a `max_tokens` above what the model can generate (a generic \"Invalid request\" message today); or a request that does not fit the model's context window. On the models hosted on the cluster (`qwen3.6`, `gemma4`) the overflow message is \"Context length exceeded for model '...'\" with the model's limit; on the other models it is usually that text, but it can arrive as the generic \"Invalid request. Check your request parameters.\" | `\"400\"` (with `type: invalid_request_error`) · `content_policy_violation` |\n| `401` | Missing or invalid API key, or a key whose tier does not reach the requested model (`glm5.3`): \"This API key does not have access to the requested model\", `type: auth_error`. Measured 2026-09-12. | `invalid_api_key` |\n| `402` | The token allowance is spent on a model that carries one. Not retryable: the counter returns to zero when that model's quota period does, the calendar month for the models counted per month and your billing period for `glm5.3`. | `monthly_cap_reached` |\n| `403` | Your tier can't access this endpoint. Image generation requires inference membership. A model your tier cannot reach answers `401`, not this. | `tier_restricted` |\n| `404` | The requested model doesn't exist. | `model_not_found` |\n| `429` | Rate limit hit (`rpm_limit`, `max_parallel_requests`), the rolling 4h token budget of `glm5.3`, or a quota exhausted. | `rate_limit_exceeded` · `insufficient_quota` · `quota_exceeded` |\n| `500` | Something went wrong on our side (includes upstream model errors). | (none) |\n| `524` | Timeout, typical with large audio files on `/audio/transcriptions`. | (none) |\n\nRetry `429` and `5xx` responses with exponential backoff. Don't retry `400`, `401`, `403`, or `404` blindly (a context overflow is not retried automatically either: shorten the input or pick a model with a larger window): they'll fail the same way every time until you change the request. `402` cannot be fixed by repetition either: it clears when that model's quota period resets.\n\n## Model catalog\n\nEvery endpoint takes a `model` id. Capabilities vary by model:\n\n| Model | Use for | Capabilities |\n| --- | --- | --- |\n| `deepseek-v4-flash` | Chat, vision, reasoning | Streaming, tool calling, reasoning, image input, 1M-token context. 3B tokens/month per member |\n| `mimo-v2.6-flash` | Chat, vision, audio | Streaming, tool calling, reasoning, image input, audio input, 1M-token context. 1.0B tokens/month per member |\n| `qwen3.8-flash` | Chat, vision, agents | Streaming, tool calling, reasoning (on by default), vision, 1M-token context. 500M tokens/month per member |\n| `glm5.3-flash` | Chat, vision, agents | Streaming, tool calling, reasoning, vision, 1M-token context. 2B tokens/month per member |\n| `qwen3.6` | Chat, agents | Streaming, tool calling, vision, reasoning (opt-out, returns `reasoning_content`) |\n| `gemma4` | Chat, vision, agents | Streaming, tool calling, vision, reasoning (opt-in) |\n| `glm5.3` | Coding, long-horizon agents | Streaming, tool calling, reasoning trace, image, audio and video input, 1M-token context. Premium tier only |\n| `qwen3-embedding` | Embeddings | 4096-dimension vectors |\n| `rerank` | RAG reranking | Qwen3-Reranker-8B, 100+ languages |\n| `kokoro` | Text-to-speech | Multiple voices and audio formats |\n| `whisper` | Speech-to-text | Transcription with word/segment timestamps |\n| `flux-2-klein` | Image generation | Text-to-image and image-to-image |\n| `qwen-image-2.1` | Image generation (text→image) | 512-1280 px, 1-4 per request, seed 0-2147483647. 100 images/month per member (shared pool with flux-2-klein) |\n\n`glm5.3` is served only to keys on the GLM 5.3 premium tier; every other model is available to any inference member. Call [List models](#tag/Models) for the exact set available to your key.\n\n## Versioning & compatibility\n\nThe API tracks the OpenAI API surface, so OpenAI SDKs and tools work against `https://api.nan.builders/v1` unchanged. This reference documents the stable public `/v1` endpoints, plus the two read-only [Workspaces](#tag/workspaces) endpoints under `/api/workspaces`, and we add capabilities without breaking existing fields.", "contact": { "name": "NaN", "url": "https://nan.builders" @@ -57,6 +57,14 @@ { "name": "Usage", "description": "Query your own token usage." + }, + { + "name": "Runs", + "description": "Give an agent (Pi or Hermes) a task inside one of your workspaces and follow it. A run keeps going if the client disconnects. Accepts a member API key or a platform token. Guide: [Agent runs](https://nan.builders/docs/runs)." + }, + { + "name": "Workspaces", + "description": "Read-only access to your own workspaces, for scripts that start runs by workspace name. Served at `https://api.nan.builders/api/workspaces`, outside `/v1`. Creating, starting, stopping and deleting workspaces is done from the platform, not from this API. Guide: [Workspaces](https://nan.builders/docs/workspaces)." } ], "paths": { @@ -1578,949 +1586,3002 @@ } ] } - } - }, - "components": { - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer", - "description": "Personal, non-transferable API key. Send as `Authorization: Bearer ` on every request." - } }, - "schemas": { - "Model": { - "type": "object", - "description": "An available model, in the OpenAI-compatible model shape.", - "properties": { - "id": { - "type": "string", - "description": "The model's unique slug. Pass this value as the `model` field in your requests.", - "example": "deepseek-v4-flash" - }, - "object": { - "type": "string", - "enum": [ - "model" - ], - "description": "The object type, which is always `model`.", - "example": "model" - }, - "created": { - "type": "integer", - "description": "The Unix timestamp (in seconds) when the model became available.", - "example": 1677610602 + "/runs": { + "post": { + "operationId": "createRun", + "tags": [ + "Runs" + ], + "summary": "Create a run", + "security": [ + { + "bearerAuth": [] }, - "owned_by": { - "type": "string", - "description": "The organization that owns the model.", - "example": "nan" + { + "platformToken": [] } - } - }, - "ContentPart": { - "description": "One part of a multimodal message. Use an array of these as `content` for vision input.", - "oneOf": [ + ], + "description": "Gives an agent a task in one of your workspaces. The run is queued and starts as soon as there is a free place: a workspace runs 1 (Micro and the free workspace), 2 (Nano), 3 (Basic), 5 (Medium) or 8 (Large) runs at once, and you can have at most 5 runs working across all your workspaces (8 on Premium). Extra runs wait, first in, first out.\n\nLimits: up to 20 queued runs per workspace and 50 in total (`429 run_queue_full`), and 60 new runs per rolling hour (`429 rate_limited`).\n\nCreating runs needs a plan with inference (`403 tier_restricted`): the run uses the workspace's inference key and counts against your plan's usage. The workspace must be `running` and have the agent installed.\n\n**Idempotency.** Send an `Idempotency-Key` header to make retries safe: the same key with the same body returns the run already created (`200`), the same key with a different body answers `409 idempotency_conflict`.\n\nAuthentication: a member API key (`sk-...`) or a platform token (`nan_pat_...`).", + "parameters": [ { - "type": "object", - "required": [ - "type", - "text" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "text" - ] + "name": "Idempotency-Key", + "in": "header", + "required": false, + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9._:-]{1,128}$" + }, + "description": "Optional. 1 to 128 characters of `A-Z a-z 0-9 . _ : -`." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateRunRequest" }, - "text": { - "type": "string" + "example": { + "workspace": "develop", + "agent": "pi", + "prompt": "add tests for the date parser and make them pass", + "cwd": "/home/nan/projects/api", + "git_isolation": null, + "timeout_seconds": 1800 + } + } + } + }, + "responses": { + "201": { + "description": "The run was created (`state: queued`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Run" + }, + "example": { + "id": "6f1c2a9b-3d4e-4f5a-8b6c-7d8e9f0a1b2c", + "object": "run", + "workspace": { + "id": "3b0e8f52-1c7a-4d29-9e61-0a4f2b7c8d15", + "name": "develop" + }, + "agent": "pi", + "model": null, + "trigger": "api", + "state": "queued", + "queue_position": 1, + "prompt_preview": "add tests for the date parser and make them pass", + "cwd": "/home/nan/projects/api", + "git_isolation": null, + "timeout_seconds": 1800, + "cancel_requested": false, + "exit_code": null, + "error_code": null, + "result": null, + "usage": { + "tokens_in": null, + "tokens_out": null, + "source": null + }, + "last_seq": 0, + "created_at": "2026-10-10T12:00:00Z", + "started_at": null, + "finished_at": null + } } } }, - { - "type": "object", - "required": [ - "type", - "image_url" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "image_url" - ] - }, - "image_url": { - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "Public URL or `data:` URI of the image." - } + "200": { + "description": "Idempotent replay: the run already created with this `Idempotency-Key` and body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Run" + }, + "example": { + "id": "6f1c2a9b-3d4e-4f5a-8b6c-7d8e9f0a1b2c", + "object": "run", + "workspace": { + "id": "3b0e8f52-1c7a-4d29-9e61-0a4f2b7c8d15", + "name": "develop" + }, + "agent": "pi", + "model": null, + "trigger": "api", + "state": "queued", + "queue_position": 1, + "prompt_preview": "add tests for the date parser and make them pass", + "cwd": "/home/nan/projects/api", + "git_isolation": null, + "timeout_seconds": 1800, + "cancel_requested": false, + "exit_code": null, + "error_code": null, + "result": null, + "usage": { + "tokens_in": null, + "tokens_out": null, + "source": null + }, + "last_seq": 0, + "created_at": "2026-10-10T12:00:00Z", + "started_at": null, + "finished_at": null } } } - } - ] - }, - "Message": { - "type": "object", - "required": [ - "role" - ], - "description": "A single message in a conversation. The `role` determines which other fields apply.", - "properties": { - "role": { - "type": "string", - "enum": [ - "system", - "user", - "assistant", - "tool" - ], - "description": "The role of the message author: `system`, `user`, `assistant`, or `tool`." }, - "content": { - "description": "The contents of the message. A plain string, or an array of content parts for multimodal input. Can be `null` on an `assistant` message that only returns tool calls.", - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "$ref": "#/components/schemas/ContentPart" + "400": { + "description": "A field is invalid; `param` names it (`workspace`, `agent`, `prompt`, `cwd`, `timeout_seconds`, `model`, `trigger`, `Idempotency-Key`, or an unknown field).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": { + "message": "must be between 60 and 7200", + "type": "invalid_request_error", + "param": "timeout_seconds", + "code": "invalid_request" + } } } - ] + } }, - "name": { - "type": "string", - "description": "An optional name for the author of this message." + "401": { + "$ref": "#/components/responses/Unauthorized" }, - "tool_calls": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ToolCall" - }, - "description": "The tool calls generated by the model. Present on `assistant` messages when the model decides to invoke one or more tools." + "403": { + "description": "`tier_restricted`: your plan does not include inference. `workspace_key_not_allowed`: the bearer is the key inside a workspace.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "tier_restricted": { + "value": { + "error": { + "message": "your membership does not include agent runs", + "type": "invalid_request_error", + "param": null, + "code": "tier_restricted" + } + } + }, + "workspace_key_not_allowed": { + "value": { + "error": { + "message": "workspace keys cannot manage runs", + "type": "invalid_request_error", + "param": null, + "code": "workspace_key_not_allowed" + } + } + } + } + } + } }, - "tool_call_id": { - "type": "string", - "description": "The id of the tool call this message answers. Required when `role` is `tool`." + "404": { + "description": "No workspace with that name or id among yours.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": { + "message": "workspace not found", + "type": "invalid_request_error", + "param": null, + "code": "not_found" + } + } + } + } }, - "reasoning_content": { - "type": "string", - "description": "The model's reasoning trace. Present only for reasoning models; safe to display or ignore." + "409": { + "description": "`workspace_not_running`: start the workspace first. `agent_not_installed`: install the agent from the workspace's Software panel. `no_inference_key`: the workspace has no inference key. `idempotency_conflict`: the `Idempotency-Key` was used with a different body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": { + "message": "the workspace is not running", + "type": "invalid_request_error", + "param": null, + "code": "workspace_not_running" + } + } + } + } + }, + "429": { + "description": "`run_queue_full`: 20 queued runs in the workspace or 50 in total. `rate_limited`: 60 runs created in the last hour.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": { + "message": "too many queued runs", + "type": "rate_limit_exceeded", + "param": null, + "code": "run_queue_full" + } + } + } + } } - } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl https://api.nan.builders/v1/runs \\\n -H \"Authorization: Bearer $NAN_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Idempotency-Key: fix-date-parser-1\" \\\n -d '{ \"workspace\": \"develop\", \"agent\": \"pi\", \"prompt\": \"add tests for the date parser and make them pass\", \"cwd\": \"/home/nan/projects/api\" }'" + } + ] }, - "Tool": { - "type": "object", - "required": [ - "type", - "function" + "get": { + "operationId": "listRuns", + "tags": [ + "Runs" ], - "description": "A function the model may call. Provide these in the request's `tools` array.", - "properties": { - "type": { - "type": "string", - "enum": [ - "function" - ], - "description": "The type of the tool. Currently only `function` is supported." + "summary": "List runs", + "security": [ + { + "bearerAuth": [] }, - "function": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string", - "pattern": "^[a-zA-Z0-9_-]{1,64}$", - "description": "The name of the function to call. Up to 64 characters; letters, digits, underscores and dashes. A name outside that pattern is rejected with a `400` before the request reaches the model, on every model." - }, + { + "platformToken": [] + } + ], + "description": "Your runs, newest first, with optional filters. Reading, following and cancelling runs does not need a plan with inference.\n\nAuthentication: a member API key (`sk-...`) or a platform token (`nan_pat_...`).", + "parameters": [ + { + "name": "workspace", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Only the runs of this workspace (name or id)." + }, + { + "name": "state", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "queued", + "starting", + "running", + "succeeded", + "failed", + "cancelled", + "timed_out" + ] + }, + "description": "Only the runs in this state. An unknown state answers `400`." + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + }, + "description": "Runs per page, 1 to 100. Out of range answers `400`." + }, + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Opaque: the `next_cursor` of the previous page." + } + ], + "responses": { + "200": { + "description": "One page of runs.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunList" + }, + "example": { + "object": "list", + "data": [ + { + "id": "6f1c2a9b-3d4e-4f5a-8b6c-7d8e9f0a1b2c", + "object": "run", + "workspace": { + "id": "3b0e8f52-1c7a-4d29-9e61-0a4f2b7c8d15", + "name": "develop" + }, + "agent": "pi", + "model": null, + "trigger": "api", + "state": "queued", + "queue_position": 1, + "prompt_preview": "add tests for the date parser and make them pass", + "cwd": "/home/nan/projects/api", + "git_isolation": null, + "timeout_seconds": 1800, + "cancel_requested": false, + "exit_code": null, + "error_code": null, + "result": null, + "usage": { + "tokens_in": null, + "tokens_out": null, + "source": null + }, + "last_seq": 0, + "created_at": "2026-10-10T12:00:00Z", + "started_at": null, + "finished_at": null + } + ], + "next_cursor": null + } + } + } + }, + "400": { + "description": "Invalid filter or cursor; `param` names it.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": { + "message": "limit must be between 1 and 100", + "type": "invalid_request_error", + "param": "limit", + "code": "invalid_request" + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "description": "The bearer is the key that lives inside a workspace. Agents cannot manage runs: use your API key or a platform token.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": { + "message": "workspace keys cannot manage runs", + "type": "invalid_request_error", + "param": null, + "code": "workspace_key_not_allowed" + } + } + } + } + }, + "404": { + "description": "The `workspace` filter names no workspace of yours.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": { + "message": "workspace not found", + "type": "invalid_request_error", + "param": null, + "code": "not_found" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl \"https://api.nan.builders/v1/runs?workspace=develop&state=running&limit=20\" \\\n -H \"Authorization: Bearer $NAN_TOKEN\"" + } + ] + } + }, + "/runs/{id}": { + "get": { + "operationId": "getRun", + "tags": [ + "Runs" + ], + "summary": "Retrieve a run", + "security": [ + { + "bearerAuth": [] + }, + { + "platformToken": [] + } + ], + "description": "One run, including the full `prompt`. Poll it, or follow the events stream, to see how it ends.\n\nAuthentication: a member API key (`sk-...`) or a platform token (`nan_pat_...`).", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The run id. A malformed id answers `404`." + } + ], + "responses": { + "200": { + "description": "The run.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Run" + }, + "example": { + "id": "6f1c2a9b-3d4e-4f5a-8b6c-7d8e9f0a1b2c", + "object": "run", + "workspace": { + "id": "3b0e8f52-1c7a-4d29-9e61-0a4f2b7c8d15", + "name": "develop" + }, + "agent": "pi", + "model": null, + "trigger": "api", + "state": "succeeded", + "queue_position": null, + "prompt_preview": "add tests for the date parser and make them pass", + "cwd": "/home/nan/projects/api", + "git_isolation": null, + "timeout_seconds": 1800, + "cancel_requested": false, + "exit_code": 0, + "error_code": null, + "result": { + "status": "succeeded", + "summary": "Added 6 tests for the date parser; all pass.", + "branch": "nan-run/6f1c2a9b", + "commit": "0a1b2c3d4e5f60718293a4b5c6d7e8f901234567", + "changed_files": 2, + "artifacts": [] + }, + "usage": { + "tokens_in": 15234, + "tokens_out": 1812, + "source": "litellm" + }, + "last_seq": 42, + "created_at": "2026-10-10T12:00:00Z", + "started_at": "2026-10-10T12:00:02Z", + "finished_at": "2026-10-10T12:03:41Z", + "prompt": "add tests for the date parser and make them pass" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "description": "The bearer is the key that lives inside a workspace. Agents cannot manage runs: use your API key or a platform token.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": { + "message": "workspace keys cannot manage runs", + "type": "invalid_request_error", + "param": null, + "code": "workspace_key_not_allowed" + } + } + } + } + }, + "404": { + "description": "No run with that id among yours (another member's run is also `404`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": { + "message": "run not found", + "type": "invalid_request_error", + "param": null, + "code": "not_found" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl https://api.nan.builders/v1/runs/$RUN_ID \\\n -H \"Authorization: Bearer $NAN_TOKEN\"" + } + ] + } + }, + "/runs/{id}/events": { + "get": { + "operationId": "getRunEvents", + "tags": [ + "Runs" + ], + "summary": "Follow a run's events", + "security": [ + { + "bearerAuth": [] + }, + { + "platformToken": [] + } + ], + "description": "The run's log, as a live stream or as JSON pages.\n\n**Stream.** Send `Accept: text/event-stream` (with `curl`, add `-N`). The server first replays every stored event after your resume point, then sends new ones as they happen. Frames:\n\n- `id: ` + `event: run_event` + `data: `: one event of the log.\n- `event: state` + `data: {\"state\", \"error_code\"}`: the run changed state.\n- `event: end` + `data: `: the run is over and every event has been sent. The server closes the stream after it.\n- `: hb`: a comment line every 15 seconds that keeps the connection open. Ignore it.\n\nTo resume after a dropped connection, send the last `id` you received as the `Last-Event-ID` header (it wins over `after`): you get the rest without gaps or repeats. A stream lasts at most 30 minutes; if it closes before `end`, reconnect with `Last-Event-ID`. Up to 10 open streams per member (`429 too_many_streams`).\n\n**JSON pages.** Without that `Accept` header the same endpoint answers one page of events after `after`. Pass the page's `next_after` as `after` until `done` is `true`.\n\nAuthentication: a member API key (`sk-...`) or a platform token (`nan_pat_...`).", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The run id. A malformed id answers `404`." + }, + { + "name": "Accept", + "in": "header", + "schema": { + "type": "string" + }, + "description": "`text/event-stream` for the live stream; anything else for a JSON page." + }, + { + "name": "Last-Event-ID", + "in": "header", + "schema": { + "type": "integer", + "minimum": 0 + }, + "description": "Stream only: resume after this `seq`. Wins over `after`." + }, + { + "name": "after", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "description": "Return the events with `seq` greater than this." + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "default": 500 + }, + "description": "JSON pages only: events per page, 1 to 1000." + } + ], + "responses": { + "200": { + "description": "The stream (`text/event-stream`) or one page of events (`application/json`).", + "content": { + "text/event-stream": { + "schema": { + "type": "string", + "description": "Server-sent events: `run_event`, `state` and `end` frames, and `: hb` comments." + }, + "example": "id: 1\nevent: run_event\ndata: {\"seq\":1,\"ts\":\"2026-10-10T12:00:02.120Z\",\"type\":\"started\",\"data\":{\"agent\":\"pi\",\"agent_version\":\"1.0.2\",\"model\":null,\"cwd\":\"/home/nan/projects/api\",\"worktree\":null,\"branch\":null,\"unit\":\"nan-run-6f1c2a9b\"}}\n\nevent: state\ndata: {\"state\":\"running\",\"error_code\":null}\n\nid: 2\nevent: run_event\ndata: {\"seq\":2,\"ts\":\"2026-10-10T12:00:05.310Z\",\"type\":\"message\",\"data\":{\"role\":\"assistant\",\"text\":\"Looking at the parser\",\"final\":false}}\n\n: hb\n\nevent: end\ndata: {\"id\":\"6f1c2a9b-3d4e-4f5a-8b6c-7d8e9f0a1b2c\",\"object\":\"run\",\"state\":\"succeeded\",...}\n\n" + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunEventPage" + }, + "example": { + "data": [ + { + "seq": 1, + "ts": "2026-10-10T12:00:02.120Z", + "type": "started", + "data": { + "agent": "pi", + "agent_version": "1.0.2", + "model": null, + "cwd": "/home/nan/projects/api", + "worktree": null, + "branch": null, + "unit": "nan-run-6f1c2a9b" + } + }, + { + "seq": 2, + "ts": "2026-10-10T12:00:05.310Z", + "type": "message", + "data": { + "role": "assistant", + "text": "Looking at the parser", + "final": false + } + } + ], + "next_after": 2, + "done": false + } + } + } + }, + "400": { + "description": "`after`, `limit` or `Last-Event-ID` is not a valid number.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": { + "message": "after must be a non-negative integer", + "type": "invalid_request_error", + "param": "after", + "code": "invalid_request" + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "description": "The bearer is the key that lives inside a workspace. Agents cannot manage runs: use your API key or a platform token.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": { + "message": "workspace keys cannot manage runs", + "type": "invalid_request_error", + "param": null, + "code": "workspace_key_not_allowed" + } + } + } + } + }, + "404": { + "description": "No run with that id among yours (another member's run is also `404`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": { + "message": "run not found", + "type": "invalid_request_error", + "param": null, + "code": "not_found" + } + } + } + } + }, + "429": { + "description": "More than 10 open streams for your account.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": { + "message": "too many open run streams", + "type": "rate_limit_exceeded", + "param": null, + "code": "too_many_streams" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL (stream)", + "source": "curl -N https://api.nan.builders/v1/runs/$RUN_ID/events \\\n -H \"Authorization: Bearer $NAN_TOKEN\" \\\n -H \"Accept: text/event-stream\"" + } + ] + } + }, + "/runs/{id}/cancel": { + "post": { + "operationId": "cancelRun", + "tags": [ + "Runs" + ], + "summary": "Cancel a run", + "security": [ + { + "bearerAuth": [] + }, + { + "platformToken": [] + } + ], + "description": "Stops the run. A queued run is cancelled at once; a running one stops within a few seconds and ends as `cancelled`. Safe to repeat.\n\nAuthentication: a member API key (`sk-...`) or a platform token (`nan_pat_...`).", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The run id. A malformed id answers `404`." + } + ], + "responses": { + "202": { + "description": "Cancellation accepted (`cancel_requested: true`). Follow the run to see it end.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Run" + }, + "example": { + "id": "6f1c2a9b-3d4e-4f5a-8b6c-7d8e9f0a1b2c", + "object": "run", + "workspace": { + "id": "3b0e8f52-1c7a-4d29-9e61-0a4f2b7c8d15", + "name": "develop" + }, + "agent": "pi", + "model": null, + "trigger": "api", + "state": "running", + "queue_position": null, + "prompt_preview": "add tests for the date parser and make them pass", + "cwd": "/home/nan/projects/api", + "git_isolation": null, + "timeout_seconds": 1800, + "cancel_requested": true, + "exit_code": null, + "error_code": null, + "result": null, + "usage": { + "tokens_in": null, + "tokens_out": null, + "source": null + }, + "last_seq": 0, + "created_at": "2026-10-10T12:00:00Z", + "started_at": "2026-10-10T12:00:02Z", + "finished_at": null + } + } + } + }, + "200": { + "description": "The run had already ended: nothing changed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Run" + }, + "example": { + "id": "6f1c2a9b-3d4e-4f5a-8b6c-7d8e9f0a1b2c", + "object": "run", + "workspace": { + "id": "3b0e8f52-1c7a-4d29-9e61-0a4f2b7c8d15", + "name": "develop" + }, + "agent": "pi", + "model": null, + "trigger": "api", + "state": "succeeded", + "queue_position": null, + "prompt_preview": "add tests for the date parser and make them pass", + "cwd": "/home/nan/projects/api", + "git_isolation": null, + "timeout_seconds": 1800, + "cancel_requested": false, + "exit_code": 0, + "error_code": null, + "result": { + "status": "succeeded", + "summary": "Added 6 tests for the date parser; all pass.", + "branch": "nan-run/6f1c2a9b", + "commit": "0a1b2c3d4e5f60718293a4b5c6d7e8f901234567", + "changed_files": 2, + "artifacts": [] + }, + "usage": { + "tokens_in": 15234, + "tokens_out": 1812, + "source": "litellm" + }, + "last_seq": 42, + "created_at": "2026-10-10T12:00:00Z", + "started_at": "2026-10-10T12:00:02Z", + "finished_at": "2026-10-10T12:03:41Z", + "prompt": "add tests for the date parser and make them pass" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "description": "The bearer is the key that lives inside a workspace. Agents cannot manage runs: use your API key or a platform token.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": { + "message": "workspace keys cannot manage runs", + "type": "invalid_request_error", + "param": null, + "code": "workspace_key_not_allowed" + } + } + } + } + }, + "404": { + "description": "No run with that id among yours (another member's run is also `404`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": { + "message": "run not found", + "type": "invalid_request_error", + "param": null, + "code": "not_found" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X POST https://api.nan.builders/v1/runs/$RUN_ID/cancel \\\n -H \"Authorization: Bearer $NAN_TOKEN\"" + } + ] + } + }, + "/api/workspaces": { + "servers": [ + { + "url": "https://api.nan.builders", + "description": "Production. These routes live outside `/v1`." + } + ], + "get": { + "operationId": "listWorkspaces", + "tags": [ + "Workspaces" + ], + "summary": "List workspaces", + "security": [ + { + "bearerAuth": [] + }, + { + "platformToken": [] + } + ], + "description": "Your own workspaces, and only yours. No keys, environment values or agent configuration are ever included. Use it to find the `name` or `workspace_uuid` to pass as `workspace` when you create a run.\n\nAuthentication: a member API key (`sk-...`) or a platform token (`nan_pat_...`). The key that lives inside a workspace is not accepted.", + "responses": { + "200": { + "description": "Your workspaces (a JSON array, empty if you have none).", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Workspace" + } + }, + "example": [ + { + "workspace_uuid": "3b0e8f52-1c7a-4d29-9e61-0a4f2b7c8d15", + "name": "develop", + "agent_selection": [ + "pi", + "hermes" + ], + "status": "running", + "ssh_command": "ssh 3b0e8f52-1c7a-4d29-9e61-0a4f2b7c8d15@ssh.nan.builders -p 30222", + "vcpus": 2, + "memory_mib": 4096, + "disk_gib": 20, + "base_image_ver": "2026.10.05-180da44", + "tier": "nano", + "created_at": "2026-10-01T09:30:00Z", + "updated_at": "2026-10-10T08:12:00Z", + "state_reason": null, + "grace_until": null, + "slot": { + "id": 412, + "tier": "nano", + "status": "active", + "cancel_at_period_end": false, + "current_period_end": "2026-11-01T09:30:00Z" + }, + "inference_attached": true, + "rescue": { + "allowed": false, + "active_until": null, + "reason": null, + "rescues_left_today": null + }, + "backup": { + "status": "off", + "stale": false + } + } + ] + } + } + }, + "401": { + "description": "Missing or invalid credentials.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformError" + }, + "example": { + "error": "unauthorized" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl https://api.nan.builders/api/workspaces \\\n -H \"Authorization: Bearer $NAN_TOKEN\"" + } + ] + } + }, + "/api/workspaces/{id}": { + "servers": [ + { + "url": "https://api.nan.builders", + "description": "Production. These routes live outside `/v1`." + } + ], + "get": { + "operationId": "getWorkspace", + "tags": [ + "Workspaces" + ], + "summary": "Retrieve a workspace", + "security": [ + { + "platformToken": [] + } + ], + "description": "One of your own workspaces, plus `env_var_keys`: the names (never the values) of the variables in its environment file. Only your own workspaces are returned: another member's workspace is never shown.\n\nAuthentication: a platform token (`nan_pat_...`). Member API keys (`sk-...`) are not accepted on this route: use the list instead.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The `workspace_uuid`." + } + ], + "responses": { + "200": { + "description": "The workspace.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkspaceDetail" + }, + "example": { + "workspace_uuid": "3b0e8f52-1c7a-4d29-9e61-0a4f2b7c8d15", + "name": "develop", + "agent_selection": [ + "pi", + "hermes" + ], + "status": "running", + "ssh_command": "ssh 3b0e8f52-1c7a-4d29-9e61-0a4f2b7c8d15@ssh.nan.builders -p 30222", + "vcpus": 2, + "memory_mib": 4096, + "disk_gib": 20, + "base_image_ver": "2026.10.05-180da44", + "tier": "nano", + "created_at": "2026-10-01T09:30:00Z", + "updated_at": "2026-10-10T08:12:00Z", + "state_reason": null, + "grace_until": null, + "slot": { + "id": 412, + "tier": "nano", + "status": "active", + "cancel_at_period_end": false, + "current_period_end": "2026-11-01T09:30:00Z" + }, + "inference_attached": true, + "rescue": { + "allowed": false, + "active_until": null, + "reason": null, + "rescues_left_today": null + }, + "backup": { + "status": "off", + "stale": false + }, + "env_var_keys": [ + "OPENAI_API_KEY", + "TELEGRAM_BOT_TOKEN" + ] + } + } + } + }, + "400": { + "description": "The id is not a valid UUID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformError" + }, + "example": { + "error": "invalid workspace id" + } + } + } + }, + "401": { + "description": "Missing or invalid credentials.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformError" + }, + "example": { + "error": "unauthorized" + } + } + } + }, + "403": { + "description": "The workspace belongs to another member.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformError" + }, + "example": { + "error": "forbidden" + } + } + } + }, + "404": { + "description": "No workspace with that id.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformError" + }, + "example": { + "error": "workspace not found" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl https://api.nan.builders/api/workspaces/$WORKSPACE_ID \\\n -H \"Authorization: Bearer $NAN_TOKEN\"" + } + ] + } + } + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "description": "Member API key (`sk-...`), from \"API Keys\" in your settings on the platform. Send it as `Authorization: Bearer `. It works on every inference endpoint, on the Runs endpoints and on the workspace list. Personal and non-transferable. The key that lives inside a workspace (the one its agents use) is refused by the Runs endpoints with `403 workspace_key_not_allowed`." + }, + "platformToken": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "nan_pat_...", + "description": "Platform token (`nan_pat_...`), from \"Tokens\" in your settings on the platform. Send it as `Authorization: Bearer `. It works ONLY on the platform API: the Runs endpoints (`/v1/runs...`) and the two Workspaces endpoints. It never works for inference: inference endpoints refuse it (`401`, or `403 token_scope_denied`). Shown once when you create it; up to 10 active; expires after 30, 90 or 365 days, or never; revocable at any time. Any member with an active subscription can create one; creating runs still needs a plan with inference." + } + }, + "schemas": { + "Model": { + "type": "object", + "description": "An available model, in the OpenAI-compatible model shape.", + "properties": { + "id": { + "type": "string", + "description": "The model's unique slug. Pass this value as the `model` field in your requests.", + "example": "deepseek-v4-flash" + }, + "object": { + "type": "string", + "enum": [ + "model" + ], + "description": "The object type, which is always `model`.", + "example": "model" + }, + "created": { + "type": "integer", + "description": "The Unix timestamp (in seconds) when the model became available.", + "example": 1677610602 + }, + "owned_by": { + "type": "string", + "description": "The organization that owns the model.", + "example": "nan" + } + } + }, + "ContentPart": { + "description": "One part of a multimodal message. Use an array of these as `content` for vision input.", + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "text" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "text" + ] + }, + "text": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "image_url" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "image_url" + ] + }, + "image_url": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "Public URL or `data:` URI of the image." + } + } + } + } + } + ] + }, + "Message": { + "type": "object", + "required": [ + "role" + ], + "description": "A single message in a conversation. The `role` determines which other fields apply.", + "properties": { + "role": { + "type": "string", + "enum": [ + "system", + "user", + "assistant", + "tool" + ], + "description": "The role of the message author: `system`, `user`, `assistant`, or `tool`." + }, + "content": { + "description": "The contents of the message. A plain string, or an array of content parts for multimodal input. Can be `null` on an `assistant` message that only returns tool calls.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContentPart" + } + } + ] + }, + "name": { + "type": "string", + "description": "An optional name for the author of this message." + }, + "tool_calls": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ToolCall" + }, + "description": "The tool calls generated by the model. Present on `assistant` messages when the model decides to invoke one or more tools." + }, + "tool_call_id": { + "type": "string", + "description": "The id of the tool call this message answers. Required when `role` is `tool`." + }, + "reasoning_content": { + "type": "string", + "description": "The model's reasoning trace. Present only for reasoning models; safe to display or ignore." + } + } + }, + "Tool": { + "type": "object", + "required": [ + "type", + "function" + ], + "description": "A function the model may call. Provide these in the request's `tools` array.", + "properties": { + "type": { + "type": "string", + "enum": [ + "function" + ], + "description": "The type of the tool. Currently only `function` is supported." + }, + "function": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,64}$", + "description": "The name of the function to call. Up to 64 characters; letters, digits, underscores and dashes. A name outside that pattern is rejected with a `400` before the request reaches the model, on every model." + }, "description": { "type": "string", - "description": "What the function does, used by the model to decide when and how to call it. Be descriptive." + "description": "What the function does, used by the model to decide when and how to call it. Be descriptive." + }, + "strict": { + "type": "boolean", + "default": false, + "description": "Request that the generated arguments follow `parameters` exactly (fields, types and required keys); enforced where the model supports strict decoding. Combined with a forced `tool_choice`, it is the way to get schema-shaped output on `deepseek-v4-flash`, which returns arguments that follow the schema." + }, + "parameters": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "object" + ], + "description": "The root type of the schema. Must be `object`." + } + }, + "additionalProperties": true, + "description": "The parameters the function accepts, described as a JSON Schema object. When present, its root must be `\"type\": \"object\"`: a root of any other type, or with no `type`, is rejected with a `400` before the request reaches the model, on every model. A function that takes no arguments can omit `parameters` or send `{\"type\": \"object\", \"properties\": {}}`." + } + } + } + } + }, + "ToolChoice": { + "description": "Controls tool use: `\"auto\"` (default), `\"none\"`, `\"required\"`, or force one with `{ \"type\": \"function\", \"function\": { \"name\": \"...\" } }`.", + "oneOf": [ + { + "type": "string", + "enum": [ + "auto", + "none", + "required" + ] + }, + { + "type": "object" + } + ] + }, + "ToolCall": { + "type": "object", + "description": "A tool the model chose to call. Execute it, then send the result back in a `tool` message referencing `id`.", + "properties": { + "id": { + "type": "string", + "description": "A unique id for this tool call. Pass it as `tool_call_id` in your follow-up `tool` message." + }, + "type": { + "type": "string", + "enum": [ + "function" + ], + "description": "The type of the tool call. Always `function`." + }, + "function": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the function to call." }, - "strict": { - "type": "boolean", - "default": false, - "description": "Request that the generated arguments follow `parameters` exactly (fields, types and required keys); enforced where the model supports strict decoding. Combined with a forced `tool_choice`, it is the way to get schema-shaped output on `deepseek-v4-flash`, which returns arguments that follow the schema." + "arguments": { + "type": "string", + "description": "The function arguments as a JSON-encoded string. Parse and validate it before use; models can occasionally emit malformed JSON." + } + } + } + } + }, + "ResponseFormat": { + "description": "Force structured output. What each mode guarantees:\n\n- `json_object`: syntactically valid JSON only, NOT its shape. Describe the schema you want in the prompt. On `deepseek-v4-flash` the prompt must also contain the word JSON, otherwise `400`.\n- `json_schema` with `strict: true`: the output is constrained to the schema (fields, types and required keys).\n\n`json_schema` works on `qwen3.6` and `gemma4`. On `deepseek-v4-flash` it is rejected with a `400` before the request reaches the model; `json_object` still works there. For output that follows a schema on `deepseek-v4-flash`, define one function tool whose `parameters` is your JSON Schema (root `type: object`, `strict: true`), force it with `tool_choice: {\"type\": \"function\", \"function\": {\"name\": \"...\"}}` and read `choices[0].message.tool_calls[0].function.arguments`.", + "oneOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "json_object" + ] + } + } + }, + { + "type": "object", + "required": [ + "type", + "json_schema" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "json_schema" + ] }, - "parameters": { + "json_schema": { "type": "object", "required": [ - "type" + "name", + "schema" ], "properties": { - "type": { - "type": "string", - "enum": [ - "object" - ], - "description": "The root type of the schema. Must be `object`." + "name": { + "type": "string" + }, + "strict": { + "type": "boolean" + }, + "schema": { + "type": "object" } + } + } + } + } + ] + }, + "Usage": { + "type": "object", + "description": "Token accounting for the request.", + "properties": { + "prompt_tokens": { + "type": "integer", + "description": "The number of tokens in the prompt (input)." + }, + "completion_tokens": { + "type": "integer", + "description": "The number of tokens in the generated completion (output)." + }, + "total_tokens": { + "type": "integer", + "description": "The total tokens used: `prompt_tokens` plus `completion_tokens`. This is what counts against your quota." + } + } + }, + "ChatCompletion": { + "type": "object", + "description": "The model's response to a chat request. Represents a single, non-streamed completion.", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for the completion." + }, + "object": { + "type": "string", + "enum": [ + "chat.completion" + ], + "description": "The object type, always `chat.completion`." + }, + "created": { + "type": "integer", + "description": "The Unix timestamp (in seconds) when the completion was created." + }, + "model": { + "type": "string", + "description": "The model that produced the completion." + }, + "choices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "index": { + "type": "integer", + "description": "The index of this choice in the `choices` array." }, - "additionalProperties": true, - "description": "The parameters the function accepts, described as a JSON Schema object. When present, its root must be `\"type\": \"object\"`: a root of any other type, or with no `type`, is rejected with a `400` before the request reaches the model, on every model. A function that takes no arguments can omit `parameters` or send `{\"type\": \"object\", \"properties\": {}}`." + "finish_reason": { + "type": "string", + "enum": [ + "stop", + "length", + "tool_calls" + ], + "description": "Why the model stopped: `stop` (natural end or a stop sequence), `length` (hit `max_tokens`), or `tool_calls` (the model called a tool)." + }, + "message": { + "$ref": "#/components/schemas/Message" + } + } + }, + "description": "The list of completion choices. Contains a single choice unless you request more." + }, + "usage": { + "$ref": "#/components/schemas/Usage", + "description": "Token usage for the request." + } + } + }, + "ChatCompletionChunk": { + "type": "object", + "description": "One Server-Sent Event chunk when `stream: true`. New text arrives in `choices[0].delta.content`; the stream ends with `data: [DONE]`.", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "chat.completion.chunk" + ], + "description": "The object type, always `chat.completion.chunk`." + }, + "created": { + "type": "integer", + "description": "The Unix timestamp (in seconds) when the chunk was created. Identical across all chunks of one response." + }, + "model": { + "type": "string", + "description": "The model producing the response." + }, + "choices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "index": { + "type": "integer" + }, + "delta": { + "type": "object", + "properties": { + "role": { + "type": "string" + }, + "content": { + "type": "string" + }, + "reasoning_content": { + "type": "string" + }, + "tool_calls": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ToolCall" + } + } + }, + "description": "The incremental update for this chunk. `role` is set on the first chunk; `content` carries the new text to append." + }, + "finish_reason": { + "type": [ + "string", + "null" + ], + "description": "`null` on intermediate chunks; set to the stop reason on the final content chunk." + } + } + } + } + } + }, + "Error": { + "type": "object", + "description": "OpenAI-standard error envelope: a non-2xx HTTP status with a JSON body describing the problem.", + "properties": { + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "type": { + "type": [ + "string", + "null" + ] + }, + "param": { + "type": [ + "string", + "null" + ] + }, + "code": { + "type": [ + "string", + "null" + ] } } } + }, + "example": { + "error": { + "message": "...", + "type": null, + "param": null, + "code": "..." + } } }, - "ToolChoice": { - "description": "Controls tool use: `\"auto\"` (default), `\"none\"`, `\"required\"`, or force one with `{ \"type\": \"function\", \"function\": { \"name\": \"...\" } }`.", - "oneOf": [ - { + "Embedding": { + "type": "object", + "description": "A single embedding vector for one input.", + "properties": { + "object": { "type": "string", "enum": [ - "auto", - "none", - "required" - ] + "embedding" + ], + "description": "The object type, always `embedding`." }, - { - "type": "object" + "index": { + "type": "integer", + "description": "The position of this embedding in the request `input` array." + }, + "embedding": { + "type": "array", + "items": { + "type": "number" + }, + "description": "The embedding vector: a list of 4096 floats. Cosine similarity between vectors measures semantic closeness." } - ] + } }, - "ToolCall": { + "EmbeddingList": { "type": "object", - "description": "A tool the model chose to call. Execute it, then send the result back in a `tool` message referencing `id`.", + "description": "A list of embeddings, one per input.", "properties": { - "id": { - "type": "string", - "description": "A unique id for this tool call. Pass it as `tool_call_id` in your follow-up `tool` message." - }, - "type": { + "object": { "type": "string", "enum": [ - "function" + "list" ], - "description": "The type of the tool call. Always `function`." + "description": "Always `list`." }, - "function": { + "model": { + "type": "string", + "description": "The model used to create the embeddings." + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Embedding" + }, + "description": "The embeddings, in the same order as the request `input`." + }, + "usage": { "type": "object", + "description": "Token usage for the request.", "properties": { - "name": { - "type": "string", - "description": "The name of the function to call." + "prompt_tokens": { + "type": "integer", + "description": "Tokens in the input." }, - "arguments": { - "type": "string", - "description": "The function arguments as a JSON-encoded string. Parse and validate it before use; models can occasionally emit malformed JSON." + "total_tokens": { + "type": "integer", + "description": "Total tokens used." } } } } }, - "ResponseFormat": { - "description": "Force structured output. What each mode guarantees:\n\n- `json_object`: syntactically valid JSON only, NOT its shape. Describe the schema you want in the prompt. On `deepseek-v4-flash` the prompt must also contain the word JSON, otherwise `400`.\n- `json_schema` with `strict: true`: the output is constrained to the schema (fields, types and required keys).\n\n`json_schema` works on `qwen3.6` and `gemma4`. On `deepseek-v4-flash` it is rejected with a `400` before the request reaches the model; `json_object` still works there. For output that follows a schema on `deepseek-v4-flash`, define one function tool whose `parameters` is your JSON Schema (root `type: object`, `strict: true`), force it with `tool_choice: {\"type\": \"function\", \"function\": {\"name\": \"...\"}}` and read `choices[0].message.tool_calls[0].function.arguments`.", - "oneOf": [ - { + "RerankResult": { + "type": "object", + "description": "One reranked document.", + "properties": { + "index": { + "type": "integer", + "description": "The position of this document in the request `documents` array." + }, + "relevance_score": { + "type": "number", + "description": "Relevance to the query, in the range [0, 1]. Higher is more relevant. Results are returned sorted by this, descending." + }, + "document": { "type": "object", - "required": [ - "type" - ], "properties": { - "type": { + "text": { "type": "string", - "enum": [ - "json_object" - ] + "description": "The document text." } - } + }, + "description": "The document, echoed back." + } + } + }, + "RerankResponse": { + "type": "object", + "description": "The reranked documents, most-relevant first.", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for the rerank request." }, - { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RerankResult" + }, + "description": "The documents, sorted by descending `relevance_score`. Each carries its original `index`." + }, + "meta": { "type": "object", - "required": [ - "type", - "json_schema" - ], + "description": "Request metadata, including token accounting.", "properties": { - "type": { - "type": "string", - "enum": [ - "json_schema" - ] + "billed_units": { + "type": "object", + "properties": { + "total_tokens": { + "type": "integer" + } + } }, - "json_schema": { + "tokens": { "type": "object", - "required": [ - "name", - "schema" - ], "properties": { - "name": { - "type": "string" - }, - "strict": { - "type": "boolean" - }, - "schema": { - "type": "object" + "input_tokens": { + "type": "integer" } } } } } - ] + } + }, + "Transcription": { + "type": "object", + "description": "A transcription of the supplied audio.", + "properties": { + "text": { + "type": "string", + "description": "The full transcribed text." + }, + "language": { + "type": "string", + "description": "The detected (or supplied) language, as an ISO-639-1 code." + }, + "task": { + "type": "string", + "description": "The task performed, always `transcribe`." + }, + "duration": { + "type": "number", + "description": "The audio duration in seconds. Present with `verbose_json`." + }, + "segments": { + "type": "array", + "description": "Time-aligned segments. Present with `verbose_json`.", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "start": { + "type": "number", + "description": "Segment start time in seconds." + }, + "end": { + "type": "number", + "description": "Segment end time in seconds." + }, + "text": { + "type": "string" + }, + "avg_logprob": { + "type": "number" + }, + "no_speech_prob": { + "type": "number" + } + } + } + }, + "words": { + "type": [ + "array", + "null" + ], + "description": "Per-word timestamps `[{ word, start, end, probability }]`. Populated only when `timestamp_granularities[]=word`." + } + } + }, + "ResponseObject": { + "type": "object", + "description": "A response produced by the Responses API.", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for the response." + }, + "created_at": { + "type": "integer", + "description": "The Unix timestamp (in seconds) when the response was created." + }, + "model": { + "type": "string", + "description": "The model that produced the response." + }, + "object": { + "type": "string", + "enum": [ + "response" + ], + "description": "The object type, always `response`." + }, + "status": { + "type": "string", + "description": "The response status, e.g. `completed`." + }, + "output": { + "type": "array", + "description": "The output items. May contain `reasoning` blocks (reasoning models) followed by a `message` block with the reply text in `content[].text`.", + "items": { + "type": "object" + } + }, + "usage": { + "type": "object", + "description": "Token usage for the request.", + "properties": { + "input_tokens": { + "type": "integer" + }, + "output_tokens": { + "type": "integer" + }, + "total_tokens": { + "type": "integer" + } + } + } + } }, - "Usage": { + "ImageResponse": { "type": "object", - "description": "Token accounting for the request.", + "description": "The generated image(s).", "properties": { - "prompt_tokens": { - "type": "integer", - "description": "The number of tokens in the prompt (input)." - }, - "completion_tokens": { + "created": { "type": "integer", - "description": "The number of tokens in the generated completion (output)." + "description": "The Unix timestamp (in seconds) when the images were created." }, - "total_tokens": { - "type": "integer", - "description": "The total tokens used: `prompt_tokens` plus `completion_tokens`. This is what counts against your quota." + "data": { + "type": "array", + "description": "The generated images. Each item has a `url` (default) or a `b64_json` payload, depending on `response_format`.", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "A temporary URL to the image, valid ~60 minutes. Returned when `response_format` is `url`." + }, + "b64_json": { + "type": "string", + "description": "The base64-encoded image bytes. Returned when `response_format` is `b64_json`." + } + } + } } } }, - "ChatCompletion": { + "Completion": { "type": "object", - "description": "The model's response to a chat request. Represents a single, non-streamed completion.", + "description": "A completion for the provided prompt (legacy).", "properties": { "id": { - "type": "string", - "description": "A unique identifier for the completion." + "type": "string" }, "object": { "type": "string", "enum": [ - "chat.completion" + "text_completion" ], - "description": "The object type, always `chat.completion`." + "description": "Always `text_completion`." }, "created": { "type": "integer", "description": "The Unix timestamp (in seconds) when the completion was created." }, "model": { - "type": "string", - "description": "The model that produced the completion." + "type": "string" }, "choices": { "type": "array", "items": { "type": "object", "properties": { + "text": { + "type": "string", + "description": "The generated text." + }, "index": { - "type": "integer", - "description": "The index of this choice in the `choices` array." + "type": "integer" }, "finish_reason": { "type": "string", "enum": [ "stop", - "length", - "tool_calls" + "length" ], - "description": "Why the model stopped: `stop` (natural end or a stop sequence), `length` (hit `max_tokens`), or `tool_calls` (the model called a tool)." + "description": "Why generation stopped." }, - "message": { - "$ref": "#/components/schemas/Message" + "logprobs": { + "type": [ + "object", + "null" + ] } } - }, - "description": "The list of completion choices. Contains a single choice unless you request more." + } }, "usage": { - "$ref": "#/components/schemas/Usage", - "description": "Token usage for the request." + "$ref": "#/components/schemas/Usage" + } + } + }, + "UsageReport": { + "type": "object", + "description": "Your token usage for the requested window.", + "properties": { + "object": { + "type": "string", + "enum": [ + "usage.report" + ], + "description": "The object type, always `usage.report`." + }, + "start_date": { + "type": "string", + "format": "date", + "description": "First day of the effective window actually served, `YYYY-MM-DD`, after any clamping: the window your totals cover." + }, + "end_date": { + "type": "string", + "format": "date", + "description": "Last day of the effective window actually served, `YYYY-MM-DD`, after any clamping: the window your totals cover." + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UsageRow" + }, + "description": "One row per (date, model) pair in the window, ordered by date ascending, then model ascending. Long windows come back paginated." + }, + "totals": { + "$ref": "#/components/schemas/UsageTotals", + "description": "Totals over the full requested window, not just the current page." + }, + "all_time": { + "$ref": "#/components/schemas/UsageAllTime", + "description": "Your lifetime usage, always present." + }, + "has_more": { + "type": "boolean", + "description": "Whether more pages follow. When `true`, pass `next_cursor` back as the `cursor` parameter." + }, + "next_cursor": { + "type": [ + "string", + "null" + ], + "description": "The cursor for the next page, or `null` on the last page." + } + } + }, + "UsageRow": { + "type": "object", + "description": "Usage for one model on one day.", + "properties": { + "date": { + "type": "string", + "description": "The UTC day this row covers, `YYYY-MM-DD`." + }, + "model": { + "type": "string", + "description": "The model id, as in the Model catalog." + }, + "prompt_tokens": { + "type": "integer", + "description": "Input tokens used that day on that model." + }, + "completion_tokens": { + "type": "integer", + "description": "Output tokens generated that day on that model." + }, + "total_tokens": { + "type": "integer", + "description": "`prompt_tokens` plus `completion_tokens`." + }, + "api_requests": { + "type": "integer", + "description": "Requests made that day on that model. Request counts are only available from 2026-09-02 onward; older days report `0`." + } + } + }, + "UsageTotals": { + "type": "object", + "description": "Totals over the full requested window, independent of pagination.", + "properties": { + "prompt_tokens": { + "type": "integer", + "description": "Total input tokens in the window." + }, + "completion_tokens": { + "type": "integer", + "description": "Total output tokens in the window." + }, + "total_tokens": { + "type": "integer", + "description": "`prompt_tokens` plus `completion_tokens`." + }, + "api_requests": { + "type": "integer", + "description": "Total requests in the window. Request counts are only available from 2026-09-02 onward; older days report `0`." + }, + "by_model": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UsageModelTotals" + }, + "description": "The same totals, broken down per model." + } + } + }, + "UsageModelTotals": { + "type": "object", + "description": "Window totals for one model.", + "properties": { + "model": { + "type": "string", + "description": "The model id, as in the Model catalog." + }, + "prompt_tokens": { + "type": "integer", + "description": "Input tokens used on this model in the window." + }, + "completion_tokens": { + "type": "integer", + "description": "Output tokens generated on this model in the window." + }, + "total_tokens": { + "type": "integer", + "description": "`prompt_tokens` plus `completion_tokens`." + }, + "api_requests": { + "type": "integer", + "description": "Requests made on this model in the window. Request counts are only available from 2026-09-02 onward; older days report `0`." + } + } + }, + "UsageAllTime": { + "type": "object", + "description": "Your lifetime usage across all models.", + "properties": { + "prompt_tokens": { + "type": "integer", + "description": "Total input tokens ever used." + }, + "completion_tokens": { + "type": "integer", + "description": "Total output tokens ever generated." + }, + "total_tokens": { + "type": "integer", + "description": "`prompt_tokens` plus `completion_tokens`." + }, + "api_requests": { + "type": "integer", + "description": "Total requests recorded for this member. Request counts are only available from 2026-09-02 onward; older days report `0`." + }, + "cached_at": { + "type": "string", + "description": "When the all-time summary was last computed, as an ISO 8601 UTC timestamp." + } + } + }, + "PlatformError": { + "type": "object", + "description": "Error body of the Workspaces endpoints (outside `/v1`): a short `error` string, plus `message` on `token_scope_denied`.", + "properties": { + "error": { + "type": "string", + "description": "Short error, for example `unauthorized`, `forbidden`, `workspace not found` or `token_scope_denied`." + }, + "message": { + "type": "string", + "description": "Present on `token_scope_denied`: what the token can be used for." } - } + }, + "required": [ + "error" + ] }, - "ChatCompletionChunk": { + "CreateRunRequest": { "type": "object", - "description": "One Server-Sent Event chunk when `stream: true`. New text arrives in `choices[0].delta.content`; the stream ends with `data: [DONE]`.", + "description": "Body of `POST /v1/runs`. Unknown fields are refused with `400`.", + "required": [ + "workspace", + "agent", + "prompt" + ], + "additionalProperties": false, "properties": { - "id": { - "type": "string" + "workspace": { + "type": "string", + "description": "Name or id of one of your workspaces.", + "example": "develop" }, - "object": { + "agent": { "type": "string", "enum": [ - "chat.completion.chunk" + "pi", + "hermes" ], - "description": "The object type, always `chat.completion.chunk`." + "description": "The agent. It must be installed in the workspace, otherwise `409 agent_not_installed`." }, - "created": { + "prompt": { + "type": "string", + "minLength": 1, + "description": "The task. 1 to 32768 bytes of UTF-8 (32 KiB), no NUL bytes. Called Task in the portal.", + "example": "add tests for the date parser and make them pass" + }, + "cwd": { + "type": "string", + "default": "/home/nan", + "maxLength": 512, + "description": "Working directory: an absolute path inside `/home/nan`, without `.` or `..` segments. Called Folder in the portal." + }, + "git_isolation": { + "type": [ + "boolean", + "null" + ], + "default": null, + "description": "`null` (auto): when `cwd` is inside a git repository, the agent works on its own branch `nan-run/` in a separate worktree, commits its changes there and never pushes; outside a repository it works in the folder. `true`: always a separate branch (fails with `config_error` outside a repository). `false`: always in the folder. Called Separate branch in the portal." + }, + "timeout_seconds": { "type": "integer", - "description": "The Unix timestamp (in seconds) when the chunk was created. Identical across all chunks of one response." + "minimum": 60, + "maximum": 7200, + "default": 1800, + "description": "Time limit. The run is stopped (`timed_out`) when it reaches it. Called Time limit in the portal." }, "model": { - "type": "string", - "description": "The model producing the response." + "type": [ + "string", + "null" + ], + "pattern": "^[A-Za-z0-9._:/-]{1,128}$", + "default": null, + "description": "Model for the agent. `null` uses the agent's own default." }, - "choices": { - "type": "array", - "items": { - "type": "object", - "properties": { - "index": { - "type": "integer" - }, - "delta": { - "type": "object", - "properties": { - "role": { - "type": "string" - }, - "content": { - "type": "string" - }, - "reasoning_content": { - "type": "string" - }, - "tool_calls": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ToolCall" - } - } - }, - "description": "The incremental update for this chunk. `role` is set on the first chunk; `content` carries the new text to append." - }, - "finish_reason": { - "type": [ - "string", - "null" - ], - "description": "`null` on intermediate chunks; set to the stop reason on the final content chunk." - } - } - } + "trigger": { + "type": "string", + "enum": [ + "api", + "cli", + "ui" + ], + "default": "api", + "description": "Who started the run. Informational only." } } }, - "Error": { + "RunArtifact": { "type": "object", - "description": "OpenAI-standard error envelope: a non-2xx HTTP status with a JSON body describing the problem.", + "description": "A file the agent left in its run's `artifacts/` folder.", "properties": { - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "type": { - "type": [ - "string", - "null" - ] - }, - "param": { - "type": [ - "string", - "null" - ] - }, - "code": { - "type": [ - "string", - "null" - ] - } - } - } - }, - "example": { - "error": { - "message": "...", - "type": null, - "param": null, - "code": "..." + "path": { + "type": "string", + "description": "Path relative to the run folder, under `artifacts/`.", + "example": "artifacts/report.md" + }, + "bytes": { + "type": "integer" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" } } }, - "Embedding": { + "RunResult": { "type": "object", - "description": "A single embedding vector for one input.", + "description": "How the run ended, written once it is over. Text from the agent is data: render it as plain text.", "properties": { - "object": { + "status": { "type": "string", "enum": [ - "embedding" + "succeeded", + "failed", + "cancelled", + "timed_out" + ] + }, + "summary": { + "type": "string", + "description": "The agent's final answer, up to 4 KiB." + }, + "branch": { + "type": [ + "string", + "null" ], - "description": "The object type, always `embedding`." + "description": "The `nan-run/...` branch with the agent's commit, when it worked on a separate branch and changed something. `null` otherwise (with no changes the branch is deleted).", + "example": "nan-run/6f1c2a9b" }, - "index": { - "type": "integer", - "description": "The position of this embedding in the request `input` array." + "commit": { + "type": [ + "string", + "null" + ], + "description": "The commit on `branch`." }, - "embedding": { + "changed_files": { + "type": "integer" + }, + "artifacts": { "type": "array", "items": { - "type": "number" - }, - "description": "The embedding vector: a list of 4096 floats. Cosine similarity between vectors measures semantic closeness." + "$ref": "#/components/schemas/RunArtifact" + } } } }, - "EmbeddingList": { + "Run": { "type": "object", - "description": "A list of embeddings, one per input.", + "description": "A run. Every response returns this object; `prompt` (the full task) is only in `GET /v1/runs/{id}`.", "properties": { + "id": { + "type": "string", + "format": "uuid" + }, "object": { "type": "string", "enum": [ - "list" - ], - "description": "Always `list`." + "run" + ] + }, + "workspace": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "`null` once the workspace is deleted." + } + } + }, + "agent": { + "type": "string", + "enum": [ + "pi", + "hermes" + ] }, "model": { + "type": [ + "string", + "null" + ] + }, + "trigger": { "type": "string", - "description": "The model used to create the embeddings." + "enum": [ + "api", + "cli", + "ui" + ] }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Embedding" - }, - "description": "The embeddings, in the same order as the request `input`." + "state": { + "type": "string", + "enum": [ + "queued", + "starting", + "running", + "succeeded", + "failed", + "cancelled", + "timed_out" + ], + "description": "`queued` waits for a free place (per-workspace and per-member limits), then `starting`, `running`, and one of the four final states." + }, + "queue_position": { + "type": [ + "integer", + "null" + ], + "description": "1-based position while `queued`, otherwise `null`." + }, + "prompt_preview": { + "type": "string", + "description": "The first 200 characters of the task." + }, + "prompt": { + "type": "string", + "description": "The full task. Only in `GET /v1/runs/{id}`." + }, + "cwd": { + "type": "string" + }, + "git_isolation": { + "type": [ + "boolean", + "null" + ] + }, + "timeout_seconds": { + "type": "integer" + }, + "cancel_requested": { + "type": "boolean" + }, + "exit_code": { + "type": [ + "integer", + "null" + ] + }, + "error_code": { + "type": [ + "string", + "null" + ], + "enum": [ + "unreachable", + "access_denied", + "admin_unavailable", + "host_key_mismatch", + "workspace_not_running", + "workspace_deleted", + "workspace_unavailable", + "not_entitled", + "no_inference_key", + "agent_not_installed", + "config_error", + "install_failed", + "busy", + "agent_failed", + "timed_out", + "cancelled", + "interrupted", + "protocol_error", + "executor_error", + null + ], + "description": "Why the run did not succeed, when there is a reason." + }, + "result": { + "oneOf": [ + { + "$ref": "#/components/schemas/RunResult" + }, + { + "type": "null" + } + ] }, "usage": { "type": "object", - "description": "Token usage for the request.", + "description": "Tokens the run used, once known (`null` until then).", "properties": { - "prompt_tokens": { - "type": "integer", - "description": "Tokens in the input." + "tokens_in": { + "type": [ + "integer", + "null" + ] }, - "total_tokens": { - "type": "integer", - "description": "Total tokens used." + "tokens_out": { + "type": [ + "integer", + "null" + ] + }, + "source": { + "type": [ + "string", + "null" + ], + "enum": [ + "litellm", + "agent", + null + ], + "description": "Where the counts come from: `litellm` (measured by the NaN gateway) or `agent` (reported by the agent)." } } - } - } - }, - "RerankResult": { - "type": "object", - "description": "One reranked document.", - "properties": { - "index": { - "type": "integer", - "description": "The position of this document in the request `documents` array." }, - "relevance_score": { - "type": "number", - "description": "Relevance to the query, in the range [0, 1]. Higher is more relevant. Results are returned sorted by this, descending." + "last_seq": { + "type": "integer", + "description": "`seq` of the last stored event." }, - "document": { - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "The document text." - } - }, - "description": "The document, echoed back." + "created_at": { + "type": "string", + "format": "date-time" + }, + "started_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "finished_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" } } }, - "RerankResponse": { + "RunList": { "type": "object", - "description": "The reranked documents, most-relevant first.", "properties": { - "id": { + "object": { "type": "string", - "description": "A unique identifier for the rerank request." + "enum": [ + "list" + ] }, - "results": { + "data": { "type": "array", "items": { - "$ref": "#/components/schemas/RerankResult" + "$ref": "#/components/schemas/Run" }, - "description": "The documents, sorted by descending `relevance_score`. Each carries its original `index`." + "description": "Newest first. `prompt` is not included: use `prompt_preview`." }, - "meta": { - "type": "object", - "description": "Request metadata, including token accounting.", - "properties": { - "billed_units": { - "type": "object", - "properties": { - "total_tokens": { - "type": "integer" - } - } - }, - "tokens": { - "type": "object", - "properties": { - "input_tokens": { - "type": "integer" - } - } - } - } + "next_cursor": { + "type": [ + "string", + "null" + ], + "description": "Pass it as `cursor` for the next page. `null` on the last page." } } }, - "Transcription": { + "RunEventDataStarted": { "type": "object", - "description": "A transcription of the supplied audio.", + "description": "`data` of a `started` event. The run started.", "properties": { - "text": { + "agent": { "type": "string", - "description": "The full transcribed text." + "enum": [ + "pi", + "hermes" + ] }, - "language": { - "type": "string", - "description": "The detected (or supplied) language, as an ISO-639-1 code." + "agent_version": { + "type": "string" }, - "task": { - "type": "string", - "description": "The task performed, always `transcribe`." + "model": { + "type": [ + "string", + "null" + ] }, - "duration": { - "type": "number", - "description": "The audio duration in seconds. Present with `verbose_json`." + "cwd": { + "type": "string", + "description": "Where the agent works (inside the worktree when there is one)." }, - "segments": { - "type": "array", - "description": "Time-aligned segments. Present with `verbose_json`.", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "start": { - "type": "number", - "description": "Segment start time in seconds." - }, - "end": { - "type": "number", - "description": "Segment end time in seconds." - }, - "text": { - "type": "string" - }, - "avg_logprob": { - "type": "number" - }, - "no_speech_prob": { - "type": "number" - } - } - } + "worktree": { + "type": [ + "string", + "null" + ], + "description": "The separate worktree, or `null` when the agent works in the folder." }, - "words": { + "branch": { "type": [ - "array", + "string", "null" ], - "description": "Per-word timestamps `[{ word, start, end, probability }]`. Populated only when `timestamp_granularities[]=word`." + "description": "`nan-run/...` when there is a worktree, otherwise `null`." + }, + "unit": { + "type": "string" } } }, - "ResponseObject": { + "RunEventDataLog": { "type": "object", - "description": "A response produced by the Responses API.", + "description": "`data` of a `log` event. A line of output that is not a message.", "properties": { - "id": { + "stream": { "type": "string", - "description": "A unique identifier for the response." - }, - "created_at": { - "type": "integer", - "description": "The Unix timestamp (in seconds) when the response was created." + "enum": [ + "agent_stdout", + "agent_stderr", + "nan-run" + ] }, - "model": { + "text": { "type": "string", - "description": "The model that produced the response." - }, - "object": { + "description": "Up to 8 KiB." + } + } + }, + "RunEventDataMessage": { + "type": "object", + "description": "`data` of a `message` event. Assistant text, sent incrementally: each event carries only its own chunk, and the last chunk of a message has `final: true` (its text may be empty). Concatenate the chunks to rebuild the message.", + "properties": { + "role": { "type": "string", "enum": [ - "response" - ], - "description": "The object type, always `response`." + "assistant" + ] }, - "status": { + "text": { "type": "string", - "description": "The response status, e.g. `completed`." - }, - "output": { - "type": "array", - "description": "The output items. May contain `reasoning` blocks (reasoning models) followed by a `message` block with the reply text in `content[].text`.", - "items": { - "type": "object" - } + "description": "This chunk only, up to 16 KiB." }, - "usage": { - "type": "object", - "description": "Token usage for the request.", - "properties": { - "input_tokens": { - "type": "integer" - }, - "output_tokens": { - "type": "integer" - }, - "total_tokens": { - "type": "integer" - } - } + "final": { + "type": "boolean" } } }, - "ImageResponse": { + "RunEventDataToolCall": { "type": "object", - "description": "The generated image(s).", + "description": "`data` of a `tool_call` event. The agent called a tool.", "properties": { - "created": { - "type": "integer", - "description": "The Unix timestamp (in seconds) when the images were created." + "id": { + "type": "string" }, - "data": { - "type": "array", - "description": "The generated images. Each item has a `url` (default) or a `b64_json` payload, depending on `response_format`.", - "items": { - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "A temporary URL to the image, valid ~60 minutes. Returned when `response_format` is `url`." - }, - "b64_json": { - "type": "string", - "description": "The base64-encoded image bytes. Returned when `response_format` is `b64_json`." - } - } - } + "name": { + "type": "string" + }, + "args_preview": { + "type": "string", + "description": "Arguments, up to 2 KiB." } } }, - "Completion": { + "RunEventDataToolResult": { "type": "object", - "description": "A completion for the provided prompt (legacy).", + "description": "`data` of a `tool_result` event. What a tool answered. `id` matches its `tool_call`.", "properties": { "id": { "type": "string" }, - "object": { - "type": "string", - "enum": [ - "text_completion" - ], - "description": "Always `text_completion`." + "name": { + "type": "string" }, - "created": { - "type": "integer", - "description": "The Unix timestamp (in seconds) when the completion was created." + "is_error": { + "type": "boolean" }, + "output_preview": { + "type": "string", + "description": "Output, up to 4 KiB." + } + } + }, + "RunEventDataUsage": { + "type": "object", + "description": "`data` of a `usage` event. Tokens used so far, as the agent reports them (cumulative).", + "properties": { "model": { - "type": "string" + "type": [ + "string", + "null" + ] }, - "choices": { - "type": "array", - "items": { - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "The generated text." - }, - "index": { - "type": "integer" - }, - "finish_reason": { - "type": "string", - "enum": [ - "stop", - "length" - ], - "description": "Why generation stopped." - }, - "logprobs": { - "type": [ - "object", - "null" - ] - } - } - } + "prompt_tokens": { + "type": "integer" }, - "usage": { - "$ref": "#/components/schemas/Usage" + "completion_tokens": { + "type": "integer" + } + } + }, + "RunEventDataError": { + "type": "object", + "description": "`data` of a `error` event. Something went wrong.", + "properties": { + "code": { + "type": "string", + "enum": [ + "unreachable", + "access_denied", + "admin_unavailable", + "host_key_mismatch", + "workspace_not_running", + "workspace_deleted", + "workspace_unavailable", + "not_entitled", + "no_inference_key", + "agent_not_installed", + "config_error", + "install_failed", + "busy", + "agent_failed", + "timed_out", + "cancelled", + "interrupted", + "protocol_error", + "executor_error" + ] + }, + "message": { + "type": "string", + "description": "Up to 2 KiB." + } + } + }, + "RunEventDataTruncated": { + "type": "object", + "description": "`data` of a `truncated` event. The run hit its event limit (20000 events) or size limit (16 MiB): later events are dropped, except `artifact`, `error` and `finished`.", + "properties": { + "reason": { + "type": "string", + "enum": [ + "event_cap", + "size_cap" + ] } } }, - "UsageReport": { + "RunEventDataFinished": { "type": "object", - "description": "Your token usage for the requested window.", + "description": "`data` of a `finished` event. The run ended. Always the last event.", "properties": { - "object": { + "status": { "type": "string", "enum": [ - "usage.report" - ], - "description": "The object type, always `usage.report`." - }, - "start_date": { - "type": "string", - "format": "date", - "description": "First day of the effective window actually served, `YYYY-MM-DD`, after any clamping: the window your totals cover." - }, - "end_date": { - "type": "string", - "format": "date", - "description": "Last day of the effective window actually served, `YYYY-MM-DD`, after any clamping: the window your totals cover." - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UsageRow" - }, - "description": "One row per (date, model) pair in the window, ordered by date ascending, then model ascending. Long windows come back paginated." - }, - "totals": { - "$ref": "#/components/schemas/UsageTotals", - "description": "Totals over the full requested window, not just the current page." - }, - "all_time": { - "$ref": "#/components/schemas/UsageAllTime", - "description": "Your lifetime usage, always present." + "succeeded", + "failed", + "cancelled", + "timed_out" + ] }, - "has_more": { - "type": "boolean", - "description": "Whether more pages follow. When `true`, pass `next_cursor` back as the `cursor` parameter." + "exit_code": { + "type": "integer" }, - "next_cursor": { + "error_code": { "type": [ "string", "null" ], - "description": "The cursor for the next page, or `null` on the last page." + "enum": [ + "unreachable", + "access_denied", + "admin_unavailable", + "host_key_mismatch", + "workspace_not_running", + "workspace_deleted", + "workspace_unavailable", + "not_entitled", + "no_inference_key", + "agent_not_installed", + "config_error", + "install_failed", + "busy", + "agent_failed", + "timed_out", + "cancelled", + "interrupted", + "protocol_error", + "executor_error", + null + ] + }, + "summary": { + "type": "string" } } }, - "UsageRow": { + "RunEvent": { "type": "object", - "description": "Usage for one model on one day.", + "description": "One event of a run's log. `type` says what `data` holds. Everything in `data` comes from the agent inside your workspace: treat it as untrusted text, never as markup. Secrets from the workspace's environment file are masked as `***`.", "properties": { - "date": { - "type": "string", - "description": "The UTC day this row covers, `YYYY-MM-DD`." - }, - "model": { - "type": "string", - "description": "The model id, as in the Model catalog." - }, - "prompt_tokens": { + "seq": { "type": "integer", - "description": "Input tokens used that day on that model." + "description": "1, 2, 3... in order, per run. Use it to resume (`after`, `Last-Event-ID`)." }, - "completion_tokens": { - "type": "integer", - "description": "Output tokens generated that day on that model." + "ts": { + "type": "string", + "format": "date-time" }, - "total_tokens": { - "type": "integer", - "description": "`prompt_tokens` plus `completion_tokens`." + "type": { + "type": "string", + "enum": [ + "started", + "log", + "message", + "tool_call", + "tool_result", + "usage", + "artifact", + "error", + "truncated", + "finished" + ], + "description": "- `started`: The run started.\n- `log`: A line of output that is not a message.\n- `message`: Assistant text, sent incrementally: each event carries only its own chunk, and the last chunk of a message has `final: true` (its text may be empty). Concatenate the chunks to rebuild the message.\n- `tool_call`: The agent called a tool.\n- `tool_result`: What a tool answered. `id` matches its `tool_call`.\n- `usage`: Tokens used so far, as the agent reports them (cumulative).\n- `artifact`: A file the agent left in `artifacts/`.\n- `error`: Something went wrong.\n- `truncated`: The run hit its event limit (20000 events) or size limit (16 MiB): later events are dropped, except `artifact`, `error` and `finished`.\n- `finished`: The run ended. Always the last event." }, - "api_requests": { - "type": "integer", - "description": "Requests made that day on that model. Request counts are only available from 2026-09-02 onward; older days report `0`." + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/RunEventDataStarted" + }, + { + "$ref": "#/components/schemas/RunEventDataLog" + }, + { + "$ref": "#/components/schemas/RunEventDataMessage" + }, + { + "$ref": "#/components/schemas/RunEventDataToolCall" + }, + { + "$ref": "#/components/schemas/RunEventDataToolResult" + }, + { + "$ref": "#/components/schemas/RunEventDataUsage" + }, + { + "$ref": "#/components/schemas/RunArtifact" + }, + { + "$ref": "#/components/schemas/RunEventDataError" + }, + { + "$ref": "#/components/schemas/RunEventDataTruncated" + }, + { + "$ref": "#/components/schemas/RunEventDataFinished" + } + ], + "description": "Depends on `type`." + } + }, + "example": { + "seq": 4, + "ts": "2026-10-10T12:00:09.412Z", + "type": "tool_call", + "data": { + "id": "call_01", + "name": "bash", + "args_preview": "{\"command\":\"go test ./...\"}" } } }, - "UsageTotals": { + "RunEventPage": { "type": "object", - "description": "Totals over the full requested window, independent of pagination.", "properties": { - "prompt_tokens": { - "type": "integer", - "description": "Total input tokens in the window." - }, - "completion_tokens": { - "type": "integer", - "description": "Total output tokens in the window." - }, - "total_tokens": { - "type": "integer", - "description": "`prompt_tokens` plus `completion_tokens`." + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RunEvent" + } }, - "api_requests": { + "next_after": { "type": "integer", - "description": "Total requests in the window. Request counts are only available from 2026-09-02 onward; older days report `0`." + "description": "The last `seq` returned (or your `after` when the page is empty). Pass it as `after` for the next page." }, - "by_model": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UsageModelTotals" - }, - "description": "The same totals, broken down per model." + "done": { + "type": "boolean", + "description": "`true` when the run is over and every event has been returned." } } }, - "UsageModelTotals": { + "WorkspaceSlot": { "type": "object", - "description": "Window totals for one model.", + "description": "The purchased slot paying for the workspace.", "properties": { - "model": { - "type": "string", - "description": "The model id, as in the Model catalog." + "id": { + "type": "integer" }, - "prompt_tokens": { - "type": "integer", - "description": "Input tokens used on this model in the window." + "tier": { + "type": "string" }, - "completion_tokens": { - "type": "integer", - "description": "Output tokens generated on this model in the window." + "status": { + "type": "string" }, - "total_tokens": { - "type": "integer", - "description": "`prompt_tokens` plus `completion_tokens`." + "cancel_at_period_end": { + "type": "boolean" }, - "api_requests": { - "type": "integer", - "description": "Requests made on this model in the window. Request counts are only available from 2026-09-02 onward; older days report `0`." + "current_period_end": { + "type": [ + "string", + "null" + ], + "format": "date-time" } } }, - "UsageAllTime": { + "Workspace": { "type": "object", - "description": "Your lifetime usage across all models.", + "description": "One of your workspaces. Never carries keys, environment values or agent configuration.", "properties": { - "prompt_tokens": { - "type": "integer", - "description": "Total input tokens ever used." + "workspace_uuid": { + "type": "string", + "format": "uuid", + "description": "The workspace id. Accepted by `workspace` in `POST /v1/runs`." }, - "completion_tokens": { - "type": "integer", - "description": "Total output tokens ever generated." + "name": { + "type": "string", + "description": "Unique among your workspaces. Accepted by `workspace` in `POST /v1/runs`." }, - "total_tokens": { - "type": "integer", - "description": "`prompt_tokens` plus `completion_tokens`." + "agent_selection": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Agents and tools chosen for the workspace." }, - "api_requests": { - "type": "integer", - "description": "Total requests recorded for this member. Request counts are only available from 2026-09-02 onward; older days report `0`." + "status": { + "type": "string", + "enum": [ + "provisioning", + "running", + "stopped", + "crashed", + "unknown" + ], + "description": "Runs can only be created while it is `running`." }, - "cached_at": { + "ssh_command": { "type": "string", - "description": "When the all-time summary was last computed, as an ISO 8601 UTC timestamp." + "description": "The command to connect over SSH." + }, + "vcpus": { + "type": "integer" + }, + "memory_mib": { + "type": "integer" + }, + "disk_gib": { + "type": "integer" + }, + "base_image_ver": { + "type": "string" + }, + "tier": { + "type": "string", + "enum": [ + "free", + "micro", + "nano", + "basic", + "medium", + "large" + ], + "description": "Size. `free` is the Micro-sized workspace included with Premium." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "state_reason": { + "type": [ + "string", + "null" + ], + "enum": [ + "dunning", + "canceled", + "disputed", + "churn", + "banned", + "pending_capacity", + "rescue", + null + ], + "description": "Why the workspace is not running normally, or `null`." + }, + "grace_until": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When an open grace period ends (the data is deleted then), or `null`." + }, + "slot": { + "oneOf": [ + { + "$ref": "#/components/schemas/WorkspaceSlot" + }, + { + "type": "null" + } + ], + "description": "`null` for the free workspace." + }, + "inference_attached": { + "type": "boolean", + "description": "Whether the workspace has an inference key (never the key itself). Runs need it." + }, + "rescue": { + "type": "object", + "description": "Rescue sessions during a grace period.", + "properties": { + "allowed": { + "type": "boolean" + }, + "active_until": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "reason": { + "type": [ + "string", + "null" + ] + }, + "rescues_left_today": { + "type": [ + "integer", + "null" + ] + } + } + }, + "backup": { + "type": [ + "object", + "null" + ], + "description": "The backup add-on.", + "properties": { + "status": { + "type": "string", + "enum": [ + "off", + "pending_payment", + "active", + "disabling", + "disabled", + "failed" + ] + }, + "stale": { + "type": "boolean", + "description": "`true` when the last snapshot is older than expected." + } + } } } + }, + "WorkspaceDetail": { + "allOf": [ + { + "$ref": "#/components/schemas/Workspace" + }, + { + "type": "object", + "properties": { + "env_var_keys": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Names of the variables in the workspace's environment file. Never the values." + } + } + } + ] } }, "responses": { diff --git a/src/lib/openapiRuns.test.ts b/src/lib/openapiRuns.test.ts new file mode 100644 index 0000000..0348d6c --- /dev/null +++ b/src/lib/openapiRuns.test.ts @@ -0,0 +1,334 @@ +import { describe, expect, it } from 'vitest'; +import spec from '../data/openapi.json'; +import { resolveSpec } from './apiDoc'; +import { DEFAULT_RATE_LIMITS } from './rateLimits'; +import { openapiToText } from './openapiToText'; + +/** + * The Runs and Workspaces surface of the API reference. + * + * Shapes come from cloud-api origin/main: internal/runs/api.go (limits, + * states, error codes), internal/runs/protocol (event kinds and their data), + * internal/handlers/runs.go + runs_sse.go (routes, bodies, statuses, SSE + * frames), internal/handlers/workspace.go + internal/workspace/models.go + * (the workspace view) and the platform-token middleware + * (internal/middleware/auth.go, handlers.PlatformTokenAPIRoutes). + * + * There is no OpenAPI validator among the dependencies, so the examples are + * checked against their schemas with the small validator below: enough of + * JSON Schema for what this spec uses ($ref, type, enum, required, + * properties, additionalProperties: false, items, oneOf, allOf, pattern, + * minimum/maximum, minLength). + */ + +type Schema = Record; +const s = spec as any; + +function deref(schema: Schema): Schema { + if (schema && typeof schema.$ref === 'string') { + const target = schema.$ref + .replace(/^#\//, '') + .split('/') + .reduce((node: any, part: string) => node?.[part], s); + expect(target, schema.$ref).toBeDefined(); + return deref(target); + } + return schema; +} + +function typeOf(v: unknown): string { + if (v === null) return 'null'; + if (Array.isArray(v)) return 'array'; + if (typeof v === 'number') return Number.isInteger(v) ? 'integer' : 'number'; + return typeof v; +} + +/** Errors of `value` against `schema`, as `path: reason` strings. */ +function validate(value: unknown, schema: Schema, path = '$'): string[] { + const sc = deref(schema); + const errors: string[] = []; + if (sc.oneOf) { + const ok = sc.oneOf.filter((alt: Schema) => validate(value, alt, path).length === 0).length; + if (ok !== 1) errors.push(`${path}: matches ${ok} of oneOf`); + return errors; + } + if (sc.allOf) { + // Merge the parts into one object schema, so one part's fields are not + // "unknown" to the other. + const parts = sc.allOf.map(deref); + const merged = { + type: 'object', + required: parts.flatMap((p: Schema) => p.required ?? []), + properties: Object.assign({}, ...parts.map((p: Schema) => p.properties ?? {})), + }; + return validate(value, merged, path); + } + if (sc.type) { + const types = Array.isArray(sc.type) ? sc.type : [sc.type]; + const t = typeOf(value); + if (!types.includes(t) && !(t === 'integer' && types.includes('number'))) { + return [`${path}: ${t} is not ${types.join('|')}`]; + } + } + if (sc.enum && !sc.enum.includes(value)) errors.push(`${path}: ${JSON.stringify(value)} not in enum`); + if (typeof value === 'string') { + if (sc.pattern && !new RegExp(sc.pattern).test(value)) errors.push(`${path}: does not match ${sc.pattern}`); + if (sc.minLength !== undefined && value.length < sc.minLength) errors.push(`${path}: too short`); + if (sc.maxLength !== undefined && value.length > sc.maxLength) errors.push(`${path}: too long`); + } + if (typeof value === 'number') { + if (sc.minimum !== undefined && value < sc.minimum) errors.push(`${path}: below minimum`); + if (sc.maximum !== undefined && value > sc.maximum) errors.push(`${path}: above maximum`); + } + if (typeOf(value) === 'object') { + const obj = value as Record; + for (const key of sc.required ?? []) if (!(key in obj)) errors.push(`${path}: missing ${key}`); + for (const [key, v] of Object.entries(obj)) { + const prop = sc.properties?.[key]; + if (prop) errors.push(...validate(v, prop, `${path}.${key}`)); + else if (sc.properties && sc.additionalProperties !== true) errors.push(`${path}: unknown property ${key}`); + } + } + if (Array.isArray(value) && sc.items) value.forEach((v, i) => errors.push(...validate(v, sc.items, `${path}[${i}]`))); + return errors; +} + +const RUN_OPS: Array<[string, string, string]> = [ + ['/runs', 'post', 'createRun'], + ['/runs', 'get', 'listRuns'], + ['/runs/{id}', 'get', 'getRun'], + ['/runs/{id}/events', 'get', 'getRunEvents'], + ['/runs/{id}/cancel', 'post', 'cancelRun'], +]; +const WS_OPS: Array<[string, string, string]> = [ + ['/api/workspaces', 'get', 'listWorkspaces'], + ['/api/workspaces/{id}', 'get', 'getWorkspace'], +]; +const op = (path: string, method: string) => s.paths[path][method]; + +describe('openapi.json: Runs and Workspaces tags', () => { + it('declares both tags with a link to their guide', () => { + const tags = Object.fromEntries(s.tags.map((t: any) => [t.name, t.description])); + expect(tags.Runs).toContain('https://nan.builders/docs/runs'); + expect(tags.Workspaces).toContain('https://nan.builders/docs/workspaces'); + }); + + it.each([...RUN_OPS.map((o) => [...o, 'Runs']), ...WS_OPS.map((o) => [...o, 'Workspaces'])])( + '%s %s (%s) is tagged %s', + (path, method, id, tag) => { + expect(op(path, method).operationId).toBe(id); + expect(op(path, method).tags).toEqual([tag]); + }, + ); +}); + +describe('openapi.json: credentials', () => { + const schemes = s.components.securitySchemes; + + it('documents the API key and the platform token as two bearer schemes', () => { + expect(schemes.bearerAuth).toMatchObject({ type: 'http', scheme: 'bearer' }); + expect(schemes.bearerAuth.description).toContain('`sk-...`'); + expect(schemes.platformToken).toMatchObject({ type: 'http', scheme: 'bearer', bearerFormat: 'nan_pat_...' }); + expect(schemes.platformToken.description).toMatch(/never works for inference/); + expect(schemes.platformToken.description).toContain('token_scope_denied'); + }); + + it('inference operations take the API key only (the global default)', () => { + expect(s.security).toEqual([{ bearerAuth: [] }]); + for (const [path, item] of Object.entries(s.paths) as Array<[string, any]>) { + if (path.startsWith('/runs') || path.startsWith('/api/')) continue; + for (const method of ['get', 'post']) if (item[method]) expect(item[method].security, path).toBeUndefined(); + } + }); + + it.each(RUN_OPS)('%s %s accepts an API key or a platform token', (path, method) => { + expect(op(path, method).security).toEqual([{ bearerAuth: [] }, { platformToken: [] }]); + expect(op(path, method).description).toContain('a member API key (`sk-...`) or a platform token (`nan_pat_...`)'); + }); + + it('the workspace list accepts both, one workspace by id only a platform token', () => { + expect(op('/api/workspaces', 'get').security).toEqual([{ bearerAuth: [] }, { platformToken: [] }]); + expect(op('/api/workspaces/{id}', 'get').security).toEqual([{ platformToken: [] }]); + expect(op('/api/workspaces/{id}', 'get').description).toMatch(/API keys \(`sk-\.\.\.`\) are not accepted/); + }); + + it('the overview explains platform tokens', () => { + expect(s.info.description).toContain('### Platform tokens'); + expect(s.info.description).toMatch(/never works for inference/); + }); +}); + +describe('openapi.json: the Runs contract (cloud-api)', () => { + const body = s.components.schemas.CreateRunRequest; + + it('POST /runs body: fields, limits and defaults', () => { + expect(body.required).toEqual(['workspace', 'agent', 'prompt']); + expect(body.additionalProperties).toBe(false); + expect(Object.keys(body.properties)).toEqual([ + 'workspace', 'agent', 'prompt', 'cwd', 'git_isolation', 'timeout_seconds', 'model', 'trigger', + ]); + expect(body.properties.agent.enum).toEqual(['pi', 'hermes']); + expect(body.properties.prompt.description).toContain('32768 bytes'); + expect(body.properties.cwd.default).toBe('/home/nan'); + expect(body.properties.git_isolation.type).toEqual(['boolean', 'null']); + expect(body.properties.timeout_seconds).toMatchObject({ minimum: 60, maximum: 7200, default: 1800 }); + expect(body.properties.trigger).toMatchObject({ enum: ['api', 'cli', 'ui'], default: 'api' }); + expect(body.properties.model.pattern).toBe('^[A-Za-z0-9._:/-]{1,128}$'); + }); + + it('POST /runs: Idempotency-Key, 201/200 and every documented error code', () => { + const create = op('/runs', 'post'); + const idem = create.parameters.find((p: any) => p.name === 'Idempotency-Key'); + expect(idem).toMatchObject({ in: 'header', required: false }); + expect(idem.schema.pattern).toBe('^[A-Za-z0-9._:-]{1,128}$'); + expect(Object.keys(create.responses).sort()).toEqual(['200', '201', '400', '401', '403', '404', '409', '429']); + const text = JSON.stringify(create.responses); + for (const code of [ + 'tier_restricted', 'workspace_key_not_allowed', 'workspace_not_running', 'agent_not_installed', + 'no_inference_key', 'idempotency_conflict', 'run_queue_full', 'rate_limited', + ]) { + expect(text, code).toContain(code); + } + expect(create.description).toMatch(/20 queued runs per workspace and 50 in total/); + expect(create.description).toMatch(/60 new runs per rolling hour/); + }); + + it('GET /runs: filters and the list object', () => { + const params = Object.fromEntries(op('/runs', 'get').parameters.map((p: any) => [p.name, p])); + expect(Object.keys(params)).toEqual(['workspace', 'state', 'limit', 'cursor']); + expect(params.limit.schema).toMatchObject({ minimum: 1, maximum: 100, default: 20 }); + expect(params.state.schema.enum).toEqual(['queued', 'starting', 'running', 'succeeded', 'failed', 'cancelled', 'timed_out']); + expect(Object.keys(s.components.schemas.RunList.properties)).toEqual(['object', 'data', 'next_cursor']); + }); + + it('the Run object has the fields the handler renders, in its order', () => { + expect(Object.keys(s.components.schemas.Run.properties)).toEqual([ + 'id', 'object', 'workspace', 'agent', 'model', 'trigger', 'state', 'queue_position', 'prompt_preview', + 'prompt', 'cwd', 'git_isolation', 'timeout_seconds', 'cancel_requested', 'exit_code', 'error_code', + 'result', 'usage', 'last_seq', 'created_at', 'started_at', 'finished_at', + ]); + expect(Object.keys(s.components.schemas.RunResult.properties)).toEqual([ + 'status', 'summary', 'branch', 'commit', 'changed_files', 'artifacts', + ]); + }); + + it('events: the ten event kinds, the message chunks and the page shape', () => { + const event = s.components.schemas.RunEvent; + expect(Object.keys(event.properties)).toEqual(['seq', 'ts', 'type', 'data']); + expect(event.properties.type.enum).toEqual([ + 'started', 'log', 'message', 'tool_call', 'tool_result', 'usage', 'artifact', 'error', 'truncated', 'finished', + ]); + expect(event.properties.data.oneOf).toHaveLength(10); + expect(s.components.schemas.RunEventDataMessage.description).toMatch(/incrementally.*`final: true`/s); + expect(Object.keys(s.components.schemas.RunEventPage.properties)).toEqual(['data', 'next_after', 'done']); + }); + + it('events: SSE frames, heartbeat, resume, lifetime and stream cap', () => { + const events = op('/runs/{id}/events', 'get'); + for (const needle of ['event: run_event', 'event: state', 'event: end', '`: hb`', '15 seconds', 'Last-Event-ID', '30 minutes', '10 open streams']) { + expect(events.description, needle).toContain(needle); + } + expect(Object.keys(events.responses['200'].content)).toEqual(['text/event-stream', 'application/json']); + const params = Object.fromEntries(events.parameters.map((p: any) => [p.name, p])); + expect(params.limit.schema).toMatchObject({ minimum: 1, maximum: 1000, default: 500 }); + expect(params['Last-Event-ID'].in).toBe('header'); + expect(JSON.stringify(events.responses['429'])).toContain('too_many_streams'); + const sse = events.responses['200'].content['text/event-stream'].example as string; + expect(sse).toMatch(/^id: 1\nevent: run_event\ndata: \{/); + expect(sse).toContain(': hb\n\n'); + }); + + it('cancel: 202 accepted, 200 when the run had already ended', () => { + expect(Object.keys(op('/runs/{id}/cancel', 'post').responses).sort()).toEqual(['200', '202', '401', '403', '404']); + }); +}); + +describe('openapi.json: the Workspaces contract (cloud-api)', () => { + it('both routes live on api.nan.builders, outside /v1, and nothing else under /api', () => { + const apiPaths = Object.keys(s.paths).filter((p) => p.startsWith('/api/')); + expect(apiPaths).toEqual(['/api/workspaces', '/api/workspaces/{id}']); + for (const p of apiPaths) { + expect(s.paths[p].servers).toEqual([expect.objectContaining({ url: 'https://api.nan.builders' })]); + expect(Object.keys(s.paths[p]).filter((k) => k !== 'servers')).toEqual(['get']); + } + expect(JSON.stringify(s)).not.toContain('cloud-api.nan.builders'); + }); + + it('only the caller\'s own workspaces, and never a secret', () => { + expect(op('/api/workspaces', 'get').description).toMatch(/Your own workspaces, and only yours/); + expect(op('/api/workspaces/{id}', 'get').description).toMatch(/Only your own workspaces are returned/); + const props = Object.keys(s.components.schemas.Workspace.properties); + for (const secret of ['agent_configs', 'hermes_env', 'member_uuid', 'api_key', 'token']) { + expect(props).not.toContain(secret); + } + expect(s.components.schemas.WorkspaceDetail.allOf[1].properties.env_var_keys.description).toMatch(/Never the values/); + }); +}); + +describe('openapi.json: every Runs and Workspaces example matches its schema', () => { + const cases: Array<[string, unknown, Schema]> = []; + for (const [path, method] of [...RUN_OPS, ...WS_OPS]) { + const o = op(path, method); + const req = o.requestBody?.content?.['application/json']; + if (req?.example) cases.push([`${method} ${path} request`, req.example, req.schema]); + for (const [status, r] of Object.entries(o.responses) as Array<[string, any]>) { + const json = r.content?.['application/json']; + if (!json) continue; + if (json.example !== undefined) cases.push([`${method} ${path} ${status}`, json.example, json.schema]); + for (const [name, ex] of Object.entries(json.examples ?? {}) as Array<[string, any]>) { + cases.push([`${method} ${path} ${status} ${name}`, ex.value, json.schema]); + } + } + } + cases.push(['RunEvent example', s.components.schemas.RunEvent.example, s.components.schemas.RunEvent]); + + it('finds the examples to check', () => { + expect(cases.length).toBeGreaterThan(20); + }); + + it.each(cases)('%s', (_name, example, schema) => { + expect(validate(example, schema)).toEqual([]); + }); + + it('the SSE example frames carry valid events', () => { + const sse = op('/runs/{id}/events', 'get').responses['200'].content['text/event-stream'].example as string; + const frames = sse.split('\n\n').filter((f) => f.includes('event: run_event')); + expect(frames.length).toBeGreaterThan(0); + for (const frame of frames) { + const data = JSON.parse(/^data: (.*)$/m.exec(frame)![1]); + expect(validate(data, { $ref: '#/components/schemas/RunEvent' })).toEqual([]); + expect(frame).toContain(`id: ${data.seq}\n`); + } + }); + + it('the validator itself rejects a wrong example', () => { + expect(validate({ workspace: 'w', agent: 'codex', prompt: 'x' }, s.components.schemas.CreateRunRequest)).not.toEqual([]); + expect(validate({ workspace: 'w', agent: 'pi', prompt: 'x', extra: 1 }, s.components.schemas.CreateRunRequest)).not.toEqual([]); + expect(validate({ timeout_seconds: 30 }, s.components.schemas.CreateRunRequest)).not.toEqual([]); + }); +}); + +describe('the text the docs API serves for the reference', () => { + it('includes the Runs and Workspaces operations', () => { + const text = openapiToText(resolveSpec(DEFAULT_RATE_LIMITS)); + for (const heading of [ + '### POST /runs: Create a run', + '### GET /runs/{id}/events: Follow a run', + '### POST /runs/{id}/cancel: Cancel a run', + '### GET /api/workspaces: List workspaces', + '### GET /api/workspaces/{id}: Retrieve a workspace', + ]) { + expect(text, heading).toContain(heading); + } + }); + + it('no em-dashes in what this PR adds', () => { + const added = JSON.stringify([ + s.tags.filter((t: any) => ['Runs', 'Workspaces'].includes(t.name)), + ...[...RUN_OPS, ...WS_OPS].map(([p, m]) => op(p, m)), + s.components.securitySchemes, + Object.entries(s.components.schemas).filter(([k]) => /^(Run|Workspace|CreateRun|PlatformError)/.test(k)), + ]); + expect(added).not.toContain('—'); + }); +}); diff --git a/src/lib/openapiSpec.test.ts b/src/lib/openapiSpec.test.ts index 1f5d42e..f00970b 100644 --- a/src/lib/openapiSpec.test.ts +++ b/src/lib/openapiSpec.test.ts @@ -17,13 +17,16 @@ import { DEFAULT_RATE_LIMITS, formatTokens, getRateLimitsConfig, USAGE_REQUESTS_ * this mostly watches for is anything from there creeping back in. * * The endpoint surface was checked against the real backend by probing each - * route: the 11 listed here answer 401 (they exist and want auth) while - * /v1/moderations, /v1/batches and /v1/files answer 404 (not enabled on NaN). + * route: the 11 inference routes listed here answer 401 (they exist and want + * auth) while /v1/moderations, /v1/batches and /v1/files answer 404 (not + * enabled on NaN). The Runs and Workspaces operations come from cloud-api + * (internal/handlers/runs.go, PlatformTokenAPIRoutes) and are pinned in + * openapiRuns.test.ts. */ const raw = JSON.stringify(spec); -/** The 11 public routes verified against api.nan.builders. */ +/** The public operations: the 11 inference routes verified against api.nan.builders, plus Runs and Workspaces. */ const PUBLIC_SURFACE: Array<[string, string]> = [ ['/models', 'get'], ['/chat/completions', 'post'], @@ -36,6 +39,13 @@ const PUBLIC_SURFACE: Array<[string, string]> = [ ['/images/generations', 'post'], ['/images/edits', 'post'], ['/usage', 'get'], + ['/runs', 'post'], + ['/runs', 'get'], + ['/runs/{id}', 'get'], + ['/runs/{id}/events', 'get'], + ['/runs/{id}/cancel', 'post'], + ['/api/workspaces', 'get'], + ['/api/workspaces/{id}', 'get'], ]; /** NaN's real catalogue (src/data/modelos.json + the API reference). */ @@ -104,7 +114,14 @@ describe('openapi.json: the surface is what the backend actually serves', () => for (const absent of ['/moderations', '/batches', '/files']) { expect(documented).not.toContain(absent); } - expect(documented.length).toBe(PUBLIC_SURFACE.length); + // Every documented operation is on the list, and nothing else is. + const operations = Object.entries(spec.paths as Record>).flatMap( + ([path, item]) => + Object.keys(item) + .filter((k) => ['get', 'post', 'put', 'patch', 'delete'].includes(k)) + .map((method) => `${method} ${path}`), + ); + expect(operations.sort()).toEqual(PUBLIC_SURFACE.map(([p, m]) => `${m} ${p}`).sort()); }); }); From 0c8209c96557b924ba01cd539ca79589ac6eab7b Mon Sep 17 00:00:00 2001 From: barckcode Date: Sat, 10 Oct 2026 22:26:36 +0200 Subject: [PATCH 3/9] docs(api): name the API key scheme apiKey next to platformToken Scalar shows the scheme names in its auth selector. Co-Authored-By: Claude Opus 5.5 --- runs-anchor.png | Bin 0 -> 76755 bytes src/data/openapi.json | 16 ++++++++-------- src/lib/openapiRuns.test.ts | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 runs-anchor.png diff --git a/runs-anchor.png b/runs-anchor.png new file mode 100644 index 0000000000000000000000000000000000000000..1bfaf2195bef9e3f5ecc7f6c70f2de7f332b005e GIT binary patch literal 76755 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⪼M7dvmk=uS1$ z`Ogs%i~DYoW3J}><_pqkFSD|KLj^HiWmNRB3eC|I!xpXk;e(UjBr5uX7wn8<|Di4e zpE+8XAjYwGTt$7iU8$e+o`LM$`HPq#J?~HuF2V4XhLY{z;1H~~%+l)@oh|z)Uzh8r z!r|o$^W#6XN7daP?O!>3u5no}^k~2fiLTe%!kcj`i{wgX0$pl@G6ZN!$M(IX`fsvX`g8~PcYU?h>n(ss;il5Q~ps>g2g$NHq2NWJE6#e&3{8*CtTVBZpuWs(hwk7v2Mn`m++;D4)K z)F0#BwpLu8fR93_WZ)xv@uPgU#YWhG0-snjiXYv^?TmFIbBCv<7^tYiWSj)W1z*BO z!|_qQ)?(|am6=+(?6mY$Y|_12`h&8|mrHssR{!bSmCHiGcz278-Om zR##={QrrdJD`Ht0%CK|U&mtDjH+Xp3hi&4s@TJ)%xS5m`{WjKL{p>X2x|5`PKp4iT zQE>D?o~i>Llb^o!?TEhG-j%nB*ad;U*QCU)1(P-S194~|v8m7PwbUU^s&^@T1GviNoxSH$$oRdbNf|ru<;O+d8%*_;15Y1EMMW+| zy1>(>HeLf(Z~DPocM4oLN7&c`zp~dcjk zP;4PiKMte`zC`|nIR7v_vDJoj*+s?0gW-K~k{p@^?DXChlGvOJ-3g8Mn{(!0+_2$> z@9+mCP9l_0abd@SgT#Xm+u?rd4;MExCRi;zKar->L>s8=^;Rw1Og+D-Xcb)}5Hb1y zwJ1-3O}hJuCNJaTu@@mo6zsx#EM@H6i)5m%p*or;bl1K)f6U7xI*7Vl#s|x97OUA> zbeDM|ms5RwY^>;$-S$9}A*2@W4vT|HNjI(E61IrqB>9&1B6!0oAXf5or~*WyjmZPwB3I+2s9jo`6 zgyKwTdb-vN@xxDdYEk*yOnksH1B?9BpV)>Hx$he!Akln!TIcShzpXo88Yk<;#9vY8 z<cNB%n%{d@sJ=nUqb|(r&ClYP04dqW!$>MSCaZF& zok!V5^z9q6i6=hmjL(;ir8-|dInw+g*-c;0+a7{SZNim6$0UFM(@eR3etuj&!0xBz@2|SKb;NMSv3C5a!@Ibl2&mYC9zCLM_!3(WYu2>TM1{@Q z7YDzN@vYl-)>Sa-A8Ct;rFACsK4>|1kDx;(%eR=D%3Mu0>-C5Gns$V_*D98NdUE=< z{z_oIKl&qbSB|_7$xs#2jIfg}xP}DTw{ml?7fCBx4>pbzY-?tG9?OaE*s520eUv0( zAWx?3Gxpiw=@_Y6v?vT`0g|>mz ztUW*fWLiZ%imCGwkIuO8)SE7G!Bhp`VR#S!8*{o`9=2cm-FzZ>2_4YdT~hkZIE6UOZfM+ul6U0Qrd`n@#TN&$S0 zdW7<`Fw8@IEAavqOvXh41|P<=N_@PB?@avQ_j_X|@q6MP?f%evYh!xK5oPy$wVoe* zS~D%%=b-4B@oTC0?)Vuv(KuBm;2zgGWOK;JcJFe`@BOhC$c|oqv%Q>qm5TfhgYd~O z7mMyzeznPx-6Ja>c>D8>MN`9Cl`YpRKj%)|e8plKTkv@#bA`X}?X$Amo{hB+PyKA) z|Nh&%4L(khyr@ZR9DX>(O`ZIA@Qu_c-6j zSbx0lT1$MM`~Jn8*Sx~=;8bXl)5Z%Ovr6yyIMQz8Wn+B>9Cj@ zA0OWW>vt)Py}Lo;h#wh1o%K5S5K@1-i(uNon^9msAa+LDv8YD{(=h8vGw=eZ;luah zm|zoJ9t;&g((caGmA~Y&iR2PEubk^*^K<9m_=jX9y_e?ViJyK;r}*aWzEU5oP~y(! zkRm&=jxhLalg`f-x^7xGVKx1<4eBMB+=^M#dBFU5Ms|Evwdhpc<%u zVXfVnV~nVmH`HeX4ULVTv)zagLg$_dN64+)_h3{-YLRp4D$OyKQUb$ET#Wy@-GK=? zEt~1CG^JyWJk_!fWL>yw6Up;prF3Fty$^pwPFB86+H1K@Ajd#gw;#ntHwMF7{IlZA zYZ@BPKY!T4>b1L@6Ck((qMa~#hCj*FJV(4U_bcR7vK~}I%9Cj7?r;Nkc=>&YJHUP4De`x10PTa{`^ zWQy!4jetx4eERe$hN^HW?Mm$m0jE2vo|b~*B$ynPe=|eHPtVVcJ#VfNXcM@UF%tB& zp|OuG#?3Nzl&cyz24D;|RaJ#Ol(e*y!?`z?)FHi3QA&IC{{2l5f|1FflBEah0uF({ z2^bvNY`bR6WlcGcjJ%jG@{H$eXBiNBwp;i%_7&o%p{ffi?E`Y0o}InF;K6XVBLbQw zyR9_#9R+RH2$o%ttdX3oZoB=*Q=xkFq+{w}(hs$sLYLwYwN%Nj(ub^4Y>A2bP26?# zf?U6|c8x`9S1uG-=Rr%t>sp4jR$$!9ih&!`oosQm_wYk`XTJnliJ+q`!K@Yj32?Il z*Fy#zMP73PosN}pRlajaku+xG|B%QN~Uc88n_wPWUe10PAmCbq`JJ5_GZ2#qqv z+(5z_d}aa3zn*=M6fuqq7eKtY`X{sRK7Owf{vz8YT(5d{7O0k$5pIh*3P>X1iO0vq z$FuEkBtr)5qRoZv2()=Y!wIPzH=-Q9hkjIL5P zr5!KH|ArFFwdJ{ohlJvaD;@;oD}AZq(#CJbM`glPwOHCF;jM#Pud^jilEK_vq$ zKyUsI`T>UYPKfe2di3Zw)5V#Y^X%*d+oEZ0SW&|ZsvaB~5!ono!$~{t!`~1ls8~j< zju3)OP_x{=geO-9h3y&|U;=WC;FqhL`i)VpVA?Rs#f-Y{9svg9fvEd@f(?(wkcEuQ zaSnZ)^}deu-BuW}L0d$c=~qDT3BUU0d-Gh?v%M1!hN>C%J<&3R)h{NO&of+~ZUb94 z1CA_7Bl;}~uP})@DN=0%1Tpz$xn1>i7qXl6$M>^FteV|z z!!XF;AsPcJwT;diS)j1P5`aY6F>(AH;?I%nbUO)`xN+O`gt-eIg201eF9ceFV}Q)4 zyL#zgziQmLu{Y}xIi`BYjzTu}D(*4a3!$ZpNL$iO)N%qH9(rG5Ll@MEnnY`9T5C~| zS>0VGI5zB`{?WC57uO=ezN1lt@HRIZAhGpv!dzZW%_jQFl`CKrontJzkG42XLl~Bc z;^sSLC4VL1fW_Oukpf8rlrOK!|MDJ$waVQwkNnv^n`Zjb~4sg=Lu&@&&Fbr9@E$11zzd;@!`+Mv<974noI$_mR z_*D0~E;Ti^>xWlb=SJjt5mmVk6Cih6gH!DL(L+?isKmoR%FCF3ez|x6^3&Q?H zG%d*N3KevCal9luF(_yRR>lk(^my1$gyF6 z*Uy^`aO~;{rJKKur>Lwd1RZp8`ayB=eCU&SOu*I$mPk^(Pj7GQ!rJsmi@J(=m@R_9 zHvq0XSjZ!T;_}P_AV#?e7nc#-F#*R2c%xh`bcSQH*GtTxX&PhrHWwBKrYF5mbg1|J zn5zm&QGNzo#Q3O(e@1`Z>bws5m>)aFA-Af?7I} zOzyy}CWbN<(PqdZara6}ObkKjvaqnT$gTbL&)d9w)hNq+mxqyRz@P6pSE1Oj~K*f^Hka|JXh>9p`@a*S>O5kz>aR zKEett->t~g*ViL(z_oN22dCcG^A9R3D}xs$v-G}AsG;tdKTVL9>Ow}S*0pM>A6PgP zax7Id(Sw}`14UtoRKzW0Tt>BT_=Dcpog&1l?}C?R;w*HJ5HnW@f${jb&=!7sw?qzf zY8iiHwwdnz1V-{nArDhKnC`-?_vt)B7?41uIX3p`$xkgeTZ#BV@iU9erCvckZH}^L z6W{A__wDM@U9}Fvq}Xz@O*YXP2u{rX3~M6@)r7dYxD;w@DR8#~K9Fqvfg>bH9pfZy z>Lc5a@LIH7B1u>|N!^+tyLBs!8B%dxyYAL57frgA!lUzLf2|7_uZVSHqNC|%4y?1W zq)5lM1(?v&(~tSK^shNKL+%dCt&{JCZKpq087_da^)(Bg6u2;7KBb$&&9(7VB{AM; zeCfz}r?n4|Es^$M4UPWD%fiu`oLWD(*TaczZ2Aog3g4``IP{+R`9Fm$N8O{Nh`QS>oJwA^m3+5Gwh_vN3V4e(W{FEeLHyaGEVRuIigRgv-efwu3Z}NgMKE=w}pINy2*a0C8A>E zZboEa8GwscVn^xAsh|7+(K}!-ybSZM(9lqvU+9c(6+U>U(_ij^fJ;b9G;!>sBnyQU5F+(PVJg5NBSm|$u*B9o=dZ3%AtA{6ShZuC5 zAH28Xn-b7$@EbM_f|N^kly4rxTr@10q@*lSJiB>6T$ZTP3uVDobA<$PZNs;GDO?^H z-bWT;J8U-)xzxJ`ParI{S9eJ4{8ZuXZ-nClF*~r|1N4r15a}C&pXY#+f!A^)TtIXQ z$Xl+~;_fP1QMHP>kN_6t*_Vg{hZr@TPo};idzx~+?iCX3PdINlLXlG0Q7NCz1}-^r zGA{dQ?B2UuQE!jRFH!r3@j0b}#$Yqnbi?p& zwolZ$tf)gjqTVvJy~tOVmb~DVe*8GmUUv*KsoY23M-sY|m`BfR>QP;=s29qNN5{w8 z^t!Bq(Tx*4E;Y3qG3Ea5-rk^OIiWN@J#&~7wm_i9vo9po_%qG*i}rfaG&eTEr;02) z;i!Sc_CkY>`hmr$j{}YcS=^Pse`uIP*Ei{Qw#4 zDm-;PTjNhi?mD%fs{g4lrDxGn=F;k_lzw1;n$yB$98*bs`f*_uB%<*%GKyD>B_=+e zW%;}eb!Fm0ayj6!Q>QQ=k*g4&HumASNp*2KD50*wTr^BGfJ$2|h8Bj!EtJCcIQRg2 z&~@xA7rLDU$T65U%A&f`tKU&lk^#pqZ zo^|G9=ASjtac_v8Dr;1WnQN;3d<&q=)%PJ+b{7mcH{7_s&|5G*S4j8k5V>Qe+8;#} z4zi@%8JU((Tq2nA5^-+r4Cn*88fX0!c=!IT=}XxVYU0^gnlaMvU-zEgdzqASgUgU_ z+hP7_03Sfzl_r@=ct=QQmdezNR^Y~jL=BC5WsdRpB zPJWNH!5&FP0C)!-#U>Q(0sL8$Medxucsj_5WI5w&637N6KQkawA52XRQ=elIyVl|$ z`@{re&trsx_9)Q}Lp*ShQ{+S|E)I<;g_ThqV{|WFvT;06%X@(S0C_W;?B~e1R80fL z{%_tz8WjCA)4T!~Mysv+;{OqgQO}*BFbraSgGtf&*zzIWJ(PZF2iY*{CV1(T|3p3T z=3?IoYaBc#mHUF=sY35~Owc|Cqx7Z($sks5QptbEu+(ToDq2LB*16c(6(*nD`Le!h zGxPChRMSUHS8?Rk{+>{RRd~vLB}q!vp@2(QOZ>P9hXy6rmN zD`@PnbyGwY5G%C_*Hk3Ryt`P7{ekiH42A$Ui%-NG!PZ5=&gz)~eG(6k2HX3&(MEbG zw)P;pw+Z?z_X~$#wIHw;zvxgyO^}FGAWKHpS)2O1V*kPd?&|5i7-zM1hU)5=kYmEd zbWdKpwWUVL>t{MgCMVw!(%^KxkH5iL2jfge2F<7FoNrd1&1o`?!aNR3g@Vi|Eb1mimLFy7=@$Cz zmfqNj7`jQDoPS|@lj>*9ps&Bjy16RWkLHaU(@ zt#iwLE_W<7r8G)!l0SFuMutkh7Bll9XucuiIpT#^ zoii*$^(HxYKb@RkqkK8^w#Vmn7&MY2U+cBPD^`q)PAvV5OcV6BXi=T-A-@zaHn>51 zOFOPM>G$z?zU}a?mg>hNx+xLsLDD??Gs~F_9 za844i$;%m6Zv`-E6!%|P@w&EsYR352;+bwYcaE9n(om^yFH- zABjEFK)7&f2ro)FB8-R(7^A_(nB?TjwKISXTCk(n#FCIG^_$YhrJz%%S~ zQVH@M()W?C=KEo^ln@?OnZ}cZzozqOE-r1INh^QS2%h3gWFv{$o1nfQGp>?a2(MKM+K->DKYo*jNVZ z7&!rZ_~~Ab>|BEdR+GG!!0FS&z{bQov?AaJc=|$kOZ~3u8X)g8(-(&)3pMBTMj!-% zzSx&|=@-3TFRa`F(Op$j8~C2`8ZR-}iHYH76=IK_gT7YAvq`i0)#YZE`D~6yX;27V zbBo^;^vncJ7zs=e7bG7#eeq&jok_%WkxYdsaW3+&VV!yC!u@;jl*9j?qBY(SvE!Y} z`(sDND`w|>)ShN1!a^Z5v;(E?Ht?BZehYmBZk&8OLk?-*D=OueG#a z+ev{!8;ag!+oC00g>7wvvU@Um6r!I^p0hQDaB6N4cI0>1?)-+AeOIcQ!;Ks5GfmLW z8-9Hod3&$4_|m)EXAcbWo>6=dm6F13J$Z4=UmFp;iS^f=Y3?1_S%mdf3@EOdjhtNf zIxdQVsBS%;4g1s($iP{NFwHA;L zw%uJ*KOTe?D+JmH+pc#G_qQ)!zY7pN-kqc*$EXMBJ?}ZSFa!cmJ7tDp$5AE|jMeD+J(^uW>0t}@k{k1*AaYHTNDF}9J*iwm}YMq)BIOu_rY;F6OwIQ)WEA5T;(fIW$G-a1tzDZ zIS+hxmbx^FM$IF7fJIoi1bFIO=4e=P7&L0dYH!aleP=6r+)aQ)Y)Hug3#6-qE-xRLgn2xqS`XA;5UqTo=nW^t6BF$Em zS7}DesV9tYint}5y4`cU<2^~5~^Vg%xu7GJEC{|ixU04vs>+b?f z6nNLKU_SEoi+=`j^TG>1x4uiv+PigH!XY{C-iIrvUMl^piD`S6k|OFI7N+{>(rABXZCzc!@|T>g9}oT(-S-Hu3KnB} zMEL1-c&oQ^#gD1-P^8bB`OY>r&T(FDOisCJGg6LUxFNu?wz6Pz@cZqKhHvlYcHfv) z$lg7dwT)!C#VI{gUfqal?iImRbu!pSQ}cxU(P~wKm*>forEX+4Eaz{7nkymUQP$`G zJ{!O&AAiBuEufra0T;l+K==*(e@COgZ^R}hB>c#?dN3;EhO?D}sA?S`@L9NvH`F9sgNDM=_SPCD-LPK^nr`GI zLJV;?>583kygTYR75l>HQldjYnhZ`*#|(|zPpPuD*4Ab}=Ht0k_w34BV*EuhhuzLb z>n&(cMpIrEIU(4+R&;V@Lu5GlkbtesrAr^tqm&DR-h{@r?bDN0jUrMDb8|#h>#c6x za%^+$!S3Ak)n8q>70Pqr8VFq{AqipY)(D`GgiN!3(e?S}o4_(W+;=*x%zMPgyS;li zxudmS^5#t?MMb~K_CmXXEPdkn4-Z#wd3@TsV_%{9Kz`bJ|sEC#qVG&IttTLNCR*Y`M0Ag%Kp{tIh4W##sso?n%8&N^p+u~@9Hd>bA80vWq| zPXu^Z&2bB(GyEng5k1j>S+jW8+~}O%Zs1sxSsiUGn>=gOJ{u%itVYDWbqhNlh>QP# z%=QCRgBg^05`B$}WD-JW%jBtqgOjdA-bW#aaz1bpr*PE%)mDvy&t5dG8QN}W;nLS( zcQrdpdQU|{qW$AX7n9_qr0>c4*JK|+vWJ;mWDGM>O@xLf3t9O2(dYGmDF(Iuk?L=B zs{A$O$j@g2!&weim-Ph4!Y&SJ95I3HmnTbpR`9c=BxMN`HxuP(R=O1HRrEzD5 z!FdMchGtNU3*9UMDz;7qVM*={vInt`elrnQ6y z2=#3X+nLJf_Y*-HP9f*Dhha7h(JSHQjpL4pRzm&`g;AQy`|ytW{UuGFJ*+N=w-#9A z@ep0Qs$M2jR=BJ41=+>LVtKWavW}fI!O2ZDeK1bH)OI zr&=TZtp!xPfaqxYfCqSK6|6pw&zt0yln4jJB_3YG4V#^v?Yxn9oqzXeI~0J>X(<#g zkP#No_CJe<$UWh3`<>salDg`4R!^McMl;{6)t0*@J5j!5T6r(Cwu}?Q0#K=>F2jmO z$v({^VDLtAEqxl2(e!I#*AVf(>rIp0BA@N5nD z+3!5oBn&z%l~7_5Eyj+Y6jg2q%iq@ROiWCakdW|gkZYOzkw^JWFGAS)7y=4rIuiAy z(n_lgSqTBv%&E2!f)iN=jZnNEkP8StApT8nhJ}>W6QG>V8Is2*$I01b3N;}FnRJkQ z^q;=jDH64UfgjhC?@z=vZzNwQB@xYIQgn_ucH>4JtD4gTvxL!$oghS;3(ir>PzRBw zqs{sGy$&Py!pq(ny`sId{YLqxI*5%VNx3y{}&b@Z=cH18V(cvffagdYp2%|gsPJmGo7E+B+rdv;@DY8i6HR}%8VSl( zd=;zsn1AbRrT>M+@|)%#G9~kKD#&;PBpJ%c4)a+~LrBo}K>2K0XQu-IT#RQ4A@7(6 zVz!|)V=I}?rY>*<spj)j&+5}*(xR}gfpn@>e3-tVal;3)GX8G3( zc`z+wGueOFR9A1z)-)i73Z{`|jyp{h(S#K}R9bNTQdRYEdTL98M6n0a^$4z`N7drQ z{9e5>H#1|*&`6iuPPoGy1~Be&YJv(X%CExy<17ykYWIf$ zs{9cRtzVaZ83%iMR)aJ();BgLA_8aHP_9?uQK!cG`tYBP`7z3sjlhr)raame*2<9) z&H#t0<>fWvt=kvInn$_{E5{E}$ht!^GMV8n}KaD9rz!==rL7glZNx?_5r6ZKT%mdHJmH%9Xl)o$F=g>O`6v zFsOZGWZG{f%|Aq`knbe9Ty^Zkja{zS6_eL?;LeV@3h=8U==93=s%igGeR5&9<d?&I?*@+lE&f!m_>!Li2kK^F^;c+ zpx^M$%7Suzl~rTo%=z=5%45F6yAseFTq@Fk{AkGk_%_9)sJ516*rEA@Ii;Q5G5NwC z^Chz#+l8KF=H#bT-fUs4X1$(wkS=2&AYI&ms_vd0XF4b~`99x?kA8Lh zJ!7AoW$ml?Tp!;Ch1i}jk1uQ2#M)}=J$qH-mZRg^?9eefVLI2^wU=*V=dEIdKUjA- zW0kmpvpYDXIDW)%?8EKbx36-Op|-9GN^_j-ke3(Er&3TH3u2nU0@h5*+Tu9+^%a<7 zxPioeM8A9IIPBGcBH!^L9UA>5boumR7e7~?LFn&WjQaNNE1R60)Y8^Qm*+fOYj`*< zElpUrIvbp&^HEs~5vj^qe4L!kRfbNmSIo@H!p<5VAHSS41hZ`1R4|)-Vl8{`$A|Mw zW1B*41m%c;ZF)+Inw(q{Yyi^Ksn_{qQ&Zvkuh4Nrs-VZ9iZJ+OX#QD*Xc${tmIy(I zROrg#gbrbEo3FLFHEXu@Vr#r8#{TrS!&ru=;Dcer;!*(=S%_e_3USI9H( zRN>>c=Ud73^7SeVj}kGJ-Ts1~Iy}PXHQnOad%mkAhlq$Imp-5UNJJE{_~w>N@)Ye| z(nlB%I5`#jzG-i7hj~VQ75UUd)L4;bNA^z-0$}56s;gVaOc|4vA4LwWuv%BirRoZI z`0S;;MWk_lOkN*JA1NuEnvX|g{Lu+C-A@Oc`G$`n|M4aWF$U8pZ{n_psLIInj*m;e zx?LznNlBS>8Bwc_{pGtrg9sCLeuVoI8QUo@a(yVkJG@VorKbABfOVf6K!rE`#^nwe zqq+Cj;mHLCDFvf8NlRm6P``7W|8rT{90p@t&zSR{AKW~VC#I~deCOiJGlbt>U4_Ff z<{Z_|ZKyXjJ=vx)vwwI;e5xbRKjdsTh5a}>7&N}uZ=$w+xP5D^j+@RYwdZxD(n=hP zvh24g4v_kqRy002Oe!YH;Te@Iq*E+;)4_k<=nbijKslf4_#NCrXVwW<4AIB; zaUJ^}@o-S6eGy_|d3SXaGUW29Ahsva=Unwhv4UC7WIH|IjweZPFhE576u}72FsSSe z|9g6R(Z#svY7-Dq6UnKlu8Q!nu*`$E#np~0i2AHRqP64{`a%>B+IR0BqMX|(Ho_2j zFBzGBnf}$Qwy3b2)-ujlNGCC4di7fHNPE}AWpLOq(r#-yKQxXZygVdfuvyv2)~jPq zs%!d!0RePqnM$d`4~F+RY@date!^FiyKf`H);7LU$;)#hbO4)2N7sYZ?-H94_+wP7 z>ubjvPZga^T9KPyw@Ei7QMvre8tuDf|30zBgpMs{(k~KnKMz$aun!~A!0lydNJvdp z6%LT_H!oF3&$AViA^m3lt=Ic~P8lgM+q{ z@+}QK{r&wY05MOmAP)X6&WYM&u;*M$OJk$rty`>fayy2H4^ZEeFq=L*^IC#Ice`YU zTH|#eC4Y-ELSuKl?v!!d@7zB)Xv~$6>trMUM#FonzR;!Z+X?F;;ZxE%qHAJeR0j?q zrYq>f*YmNeN#BJW8S}~bb!BtqMMT22bi_Wd9swnE$;YVBYjpo;2viw`DBlBInP>xx zn-AB0q3Zd0-BED`fdWVs65k_p_h5zA#CrvobLQuttT`4J7H;WeWm#jRr=~6{*?Gck z&UqrBaPXm~=A#c$HTNls%w~)u0sD4t+d>>)e$w-yfs{}`g?rQ3jL*bl$45(~4gHJ3 zCT2Q$UYGXgO<2`7+U$M*Ud}inWP#Cx(5Di)98T)k!tVOs-F+I%8EE~L9}=!#Vg4*+ zKd7R`?y3XVJ-Frdu&1C4R`>-;G8~0Qz&_c!RWykE@cGil>guF&FJzd&bra;$$rE3_ z5&S*6r|0W#TLYCTQp@7@Z382t>IB(ngGXEgI7N|23tz-w$%&qllB6W& zBF1aiDqC6 zuJ@KkcX`u9*4oV$?3EH1`%MHwJ5r1S?Jo!o*adW5{_~BAibcNJ(e=jP+^VV?xR<6M z`37v&o^aWqx^<9E{q~Bw5dZ{8AIs3_$z&?Z$$_jqgU}B{0mYX^g*=$mqV7jCYTZ{V z`txTv+|m{o7wLE{vfrq!1J|^({ZNpf|3>H`&)KujIR-tc!%_72Gwj$Vuso3_3|=LZ z_*o_o%+`$xg%{!uraHMcYisq>yr$~v1cfM) z_%71~-wF*28~pjR-o5^0eY9fQot$U6eMoe!SYVax=-}h!ZKv{W*Yp?t*3wd~LPD*LEG;=XIZ{&6Q>%$zuDdE% z`8^dDcP#0do;K^?VQve&;G65l{1N0C-`A08X%YVZ3M+%%H`^!@tUr(e+3;0q+SyMe7N@h|th{lTPd50q%QkcM+=cl5U5sgTwA^k6)_sf3zW9 zB;K&3;-{pb5DIqM#c;c7UTTysE+%F%Bd|K<-Uk)3a+0`p zYxs=5bW-yzAE%K1-S;KpG^{xjKC2AJrU>av5uV3RC3UeX$$*Dn#{30By^UL7m007VBPh6FjPQ7&8(p(uH*42)K9VJ2lisCjiGcq)ZjIzfC zw&jaOEsYNiiE6$koG?p)GDbO;h6=&TA9vEw=(a!hUvtk%vnb0PWs2oX8s0sQEgqG8 zd6wjtYQ#vTJLxW3)=JxZN4-W1$5mTL6_|cWs@^s0xs4?1 z0TN}jIBMX!mwhx zjC4T|Bh}8to!1QmNlPc$m96rk3ZowD$f^Z7J-aBl{PeIX$#xhl0p0-p(Yyf^0Vvq5 zr+YV)(jFtE%gQpleZ$42Qh4*nW0Or_c`((=n0iHj6r5Ksla7{;A4lK<{tKnMt*kH* zNEFrpX3^&KkByxuRud;N){l*k2SO*YfFR01zX%cGKK2ol3`{EBA|fIwR7QSuoQ1`fuKQ{wXH8+6ir)5^ zYR0O=-sywPU)U=asaR?m?4Z-?^f)2A6& zwIw>?kfv(-Yd}owneRF>nX<*`=;%7`pa7#-%!)CSVvDamX*QN45BEBn4#Gp4YR|HP zuPQtS^&_H6?}7h95Wd=^%Af%%|L6gC^gQb`m6>Q34<0y>eobhrI5q8- zy?6%4MJ@9W$(Ad!%gSvkUl`N!8ZFcau^?+cSIxRdg~ouRI91PK-OEkl{A#!CSa9OW zl8p}aD_7>wVKS@$S9Au6$_S;FDZ~Zg;b)oEUjPL7ze9@)VV~F59@LPfzG421&K{Ws zbzi>R&Yvf%`b5PR(qG`|e|x9y@AUL(NSo#OIXEn42LYy`=bu0ap>ToVO<%I9ZDaG2 z^t(E!_(_!qB6w*D48*Nl*AcQI^CTRJ0l`77&;0$V76fUy48DR5$!t4C&60!;0(kAl z>aUW-slGl55)*hYs}^pLA^i(DQ=tSqcwpD==A}kw*O)Gewce$7`i5yEOUc}g->2^1 z%gLO1?MlqWCQtnrJ#?kGP;op>!!6w#Gtg6~ooshQM%3nD2Vn)n>C;GYluMdidy#CQHh51;=sR&+G>-Q+(QYLo?S ze!ls@FbrT&8ZfK#2Yh&7VZoT0>E~}-aT5`nSamN-)@C_lSMse=xVX8Qo$wVs!<)W- zy`5vkkYi|gk8BIOIQ6!>c*{gg6vYd%$J0im^~eVg#CA!DJNw+Wxj~t8`VN!QKyX7` zVq$x2jVW4c=0lWpbk4Jb@fs@rNgotku-k0j-(SF|IKtOLV0BTy6BH2WZxG&$+CJ3O zKj$O7INghZxy8k>MEUrD(X?9QAj_G4lIjFV3D}sKaZzSwW@-rmy=lqP|MjbjPh0Gg zj?T8N$8YR+A)NLxp)$u=s+p*IXoBOTW#)f*6!Ud?^Qubsw=*`TUp^tsdb9hsM6Zo$Bi2>RLb*V4yx4&_ZbWw0zxK~&l?fqGSSiX6RFO0z#bp7c;+le&rtvgdYx1^ znA#%_$v1LfxOKtIt?U$5s$R)(ScQTfQ( z0~JEll$405e8izoMd~`Fk#qR%vyYUo zgQLICD;g)Ai$0m$ef?TqxV^Ou9~R8MqYxULXEwZLQ3#oZJ9!xhon3E5*0G1tNvHMC z_}>N4%40iy+|JI9lT%)*rUMN1096N%h-esom8;=00w=fvt#Sh7+>s?9HTuIJd- z6gKm*{;b7Xb2~IvaV}zoB*>-2$G5&~zZo8W?B17`FJ4gW+=iJ=bZ%ZAjUvp|nM`E| z7pC|jKuppo0fPD?MuL(~;6vrWOgZQF9VGxciAC)fxT|aC4!IoOL4B9-WepGh9yyW~ z!egbx6K7=h{dsoV*2sI#vjMlE)Tp|8mWH7{>RqYrujxG{I>T8w-FWV`nSuHh5TOfs zJB~4kScm79k*)y)2E~d2c{w?#FbW*=I>Fsaw6@2oh4Hka#Z)%wUhhq#JZ5HQ5C^Gl zW=`S4E66)Xws-IE>FN1tv-sd5>#6+O+Ur$AG9?u#7o5 z)mL^6X(pG~B_yh{Pm;YHf{Bf{yZhT#A@e8(!b=rJ*K$u&(;SObau(Q81h+9y(d`*8 zRKNljgQHOCwg1?dNIS|T#Yx1y`R!;e3fymi!t4=B>Rks zf^_Mz?@hBq%8Gf+g~YzEUmK(2gnP8v`K)c#Aqp4I$r_{tW19y&V9@Rcp0~N9#BvNq z2akYt{KzrBFn2=X>X6yh4EY~dbo;;!1KtZI`cZ!$h8@mkQ-=>9&i-^cQYO={tS=!c z>DKJWU!Dg=b#w{p9jS6FmzRoQnGhs9Jx3J&Y^FiNXQ`u>IBY;Vio0=u{jRcMEWLX4V5-+P-07 zT-u*mC@Hm_H!FYsOwG%)oBV=-0c>F*jGu#N1hRxR`60$cy`p#j{xOPKdmuA6Beu(& zxgi}ckfSbG01D^&T!ZLLe)Z(Yj4jYMD|7P~?(Vm<4bFdy#UISM*Un5&uf6zFEr@Ni zjEas;@ZrjJC}Si`#>Nb}_)Uo1%FHckVYs2s&HXze!5uRL(239g{Q}d1tB*&Bki|?! z_Y;y$NJSn)a98{Kg7Z(+crY^oooC}FU05i|DhuT?Ns5dd8{}&3Ty6VcK5!@Zp2VB^ z2eX5QSfrpt&Ex-%o__xKUdFGiD#LOB?*<1g`^(L%6DBFDC*o;U3UXtS|D5CPbse1= z)*kWotL*xdsR?SNk=CJfoIl82lM}!^&Ivci2&hJQ|qIz@eA2bfBp6?;MS+V zAN%9tJ5pw(U95?nxFx`}pXx?1d$`GN*T=xUNVbTms;Wi^twWS;j|+9&d1?BQt1Air z=`&|KYmC((^aLVNs@PN$Y0|LI~Td5<^#&%(n>7Tw+ih5fw zUOFNruL(#*D*cH2A~_ax7{`5u--&y|H<+22GN#Z%N|tQqmcit?l8Ua0g{|QT|H4C` zTM7y>g`p7*Hzg$Wao=;!@rq{s^mPp_^XU(jy z0BW5#i$)rw2~*ZQU`W(a?MlSlyF^tx$RTsf2R;MnS(a>SuX2(7IV&OO4KHlCB@V>J z)?k}*V-G-%P2i?-MBq&J4Wrtj_N>qUTh;V3i9k3lH|m_kmC#qO>K9g*mYzO)W@KzU zv+NqH@9*b#F016}xU{%;!LNa)@(z+M7P6EPA?Pu!nLqIiisw*Nr)2E75BRc`$a}(d@#|` zIdt?~e)xfa$5oBdzu4RnX>ICRjvfrESyA3fSt-EiN45E04dnFdDRcw5F?b9|IwGy=iY3ObC9jpaMr+H~d(8m&?i zc80xvJ<-eVwS8jZ$oy1igxY1ohk6up*%ahKn6_U))C(euThe-5R$xqN1^LOOYnqO( z=;}^PE)FGlF#ePqEy7y_K>W(-pX%bY;edC+e%4^<4EGfU#&WOw(q*F5VUrDsIz zu={|+WPX@CL~VgqAOa)G)KsVO7AAYom9DO?u(091#{^EDIRiQew^ZQTC-plr zJD3<5PcbkI4-RJf;qpO^qLOXU2mU9>caZjm%+FiiSp|jgRGQy5?me#EK(_wg12WC# z{sD@?rY@eIMquGM*fF1H4N6W<4)XP75xN+{RGjxJH&@!iLe4`=&2F9g_6IZa2!f(i z()?xo2x-z842Zp%Qtu@vFrHyx7?{*p;`;vmJDB2o{+tVrN0K&a?*=X%uJ_%xMa0oj z)kC`ay+ZCe!4HKSr0^8~J=u=51L8^Y`oo7qp<- zgmoztk9&n8!}2o_FlnfdGtuK0(rD?j60btDo#H8u?YiM$12p3)Akx z1)IIz%Qg9jK?Jzwz725vtz&q2v{>Zj#J{=YBL$Yp%bM=G0# zj*bxeR7e*?K!G+|zkUhrB8c>Th<-5b%IDR3s_p1l3a#CEYuc|~c8F?Od3Y9(xsEn6 zP8lV6{PmDqXhMyI9TqI1b4MhOwDg?wQs0iV$$W5(G_$hmv&chzi^B$~7@yHbL2(%D z@6VV=qsyB2-xn7B?r??<#*NoxPoM@&wvp#HcXtPnE>vWm*p9@el=jfIckYkqX z8XA-`Ir;e=5XQ~R%iF^9-;5aVNoX^^BHbp64(AQG@w43A`lq}666PTD#Bq*O5oE+u zH(t;QKiUp(`hK;46$uGRYHBLbP-7O;D)=;)x$%R~EW1$byjc%%*hsxq2Qm$}0X+PnA0nfSc!?n;PgD(j_$QT$i~{)aORV2++|-)?C1xNMmM>mZw0T3Y&y zG;O?BHEU#{Q*}m;u}XDKjeIoP;Otkpyn+QC^z7|n(ZJbu9-s&ZM-xT^{Mw%_C41W| z08X^PL`F#o_wrFX0bB93pj)5BlP7(=VvIACKaEMoF_V6lEWIo;n{v@!=E^5=2{G9h zpZ~+Wakz3wPe6)Ee5m=!VXmq_+w9T}Srh8J)q(SzglVJjkeK=EP|aH;Dab~{9CO#M zU9e+_;|aI~P4rjDR#E55i7q1Q4$M8$352=sn*p) z`suhrC<8<2{In<9S;mYM3U9_7FLD2Kjk<&;|8IlGySbWrdaP&8;Igo__LYpTu4Znz zXgPXZ@fzYoZYDN-7PCBM=D}b9DoH{Dk7;*leX3CBm7H4-9&nlyv6@yrJ$W`(!w9tz z_03}8gZ`%j{QcXkZEY{c_BeNHVpOZ`NoE)rG~yA2KQ3-u)i4qtb1I7|D7})M@=r~7 ziCAT=c*b_Ns5vNHo9D-^BOA77iLlM85jG@WgsO&GiMwGX2d^uN{WBjH( z^&S^S`SZ_6v}&~+HVQcQ-;5w-EMwk@=gDS9SkNmPCstRxvu;!TXwA&{MSc=9JK}0j=%@f-@5I5`bEfv65#&C`2URf%qnnvN7e_hv z#J`hn{61?Nb;W3iP#-d@$Gv=c6Lh8A+yfCBx9!C|dr-K_=xt_bU&Gl$NSWW~cI42~ z2OayPgr`0my$3cS^6`YBv$#Y8@$Q7P|I^MQ&sG^`;Xsz!=nYy zI#m+6Tdq6!m;c=fdlgNHgDKRmUcKm=4dC9LAA~TJ152Z?Og%nP`9eeS^4LRXk3>X_ z$`mQxyqR|GWjHFseS4_Xz8jS9ymt0%G6m=7ii(kuk=2!zOt(vZhRt!#W>wc?X$~KL z-U(o~v9WQI!^1WN61g~WT54**EmGV@JW5AsiXt)!VTZxZ{TcwgKa-!YudkfEd}3zZ z0{A8ZOias&IGTUEz8l>sj$!SzQU=$ju9wYM>Ymp{mM3@Lb|d$UV!I)Cqs>CStk>1) z@nzy`f%GhYThh1Nlj~QBkVN|L!%B3Gw{BUXzmg;7WMoV)|AG@s#?!cEbVuBhyVkWz z*~yGLuTdRo*}}-tP8bd=w=deMA7_+PP++{j^O&%cfWgy~;`?U(VGnsy(%^qP zwj}-M-cYjY-tJND%gS2wY>)ZdJra++Esnc>11BO-NPTgrCJ1K<0Y8?^m6T)#S+qOH z_>6b_ct%j`=h|8?#2N!d0w?fGSJ#8S(q$?Uh&xR5mEblcdh8h5(EC`0b=B2ThYGb^ z^(`zcDx~z7>pE8rn&X6Jmtqp^5MzaV=YY+z=*yt~tuh&+tc8NYGd#-4#^%DAcsp34 zV9trjM4c~#GkcW09cRtt=5Z1d*O-{A3kpV2XjOn(AxjE>+yU+^ps_?QKK+inNNC~5 zT|>X4VLw|(Bt7aAicU1{=;4T}PIp;iy_%_`RVED<%O!ON=iy>3=GPSW*qX459mACt&JIW1hr}qORhRP zeZt(QLJ+rDLO6`Od5oH$E6U1BOG*ZIWI30R(5;I6*>)~$NqlENYEhA6GRe&8OL5O5 z9N3Gw%50k+sh@Aztmk>UtiftZjWT85OUK~>eYR0zIj}l6mhjg+N=RT#3WEaFMX*%) z$`#lv{=|@FGK;3-A6#jv^#Zv|A0_R@bYWK3RBMyn3p(1Qn_%ns*GIqk^K6Kiw-PTo zh*A%O68!fZAi6a4?;z8EQzidTQ7gw+M6|TDwST@aNR?7D#Qzk|iT`Es{XhM+|JCP2 zYj75vP}sfw(}@rXHI3YI0otc$VnQEIFn1v(ou8fc#HC#9f=hHSozNOgMgQqgK2|0? zzVa4=Lz}@bnz<>dsXUG!20-gMO8boCpO4?;#k}()2vYs_Y~1^f{V35aI}MDdy!jS6;FI;GSFyhEa;E&xKsV_keAX7C=TPiy8S^338MXiRasWIl9UtBDKR01?4 zTW^754T!F5ZCzY4H{IUr$7M8M2Hzo2cIn#%uJ4)Wyj(V77n9dMMzIzz=gsmHzOn9)ho?a zrps>6o@Gvfatcys=djsDs1|Z^bCnCMAqh%EyRb6d_XJZkOa#yK@p;-C1l3`Hljvt% z{ZG?ApddoBgiHhQX!Yn|rWXJ9bwoqF6A8(3X=$mKm*s+p2W*c_wmfpdVK+001X|Vi z_0Kb}85!L{O8tkDgoK3JZ%oY09fX0V4Q7Dn_k4_}Aaw z#(D5WA07MGM?XTCJS>+&catFf8kG2MB6R9BH0^_frzDRjB_$~^)K^t$5*0uA-++=O z+FlsB=_m*s=jK-YLD-f5?D(XP)_-B~MBn!P{{vzCHwPc<90}|!IRACzQ2ZTI{O1Y4 z|J&g_?QBM8O8C7AUcJ3{haKoeiQB$>Nll}#z=p7bvv}M>jogk|n4)?cn zJ9;(Co{5TzLfhi~=;fzAi_v(Qf0?zfmu6<=SDUGD4w30FlnKFgU zW&E2($qBWe>2HEWG+*N%*s@XWS-~wy{Wa%+2`H+-RC;zUDv+$wf5TA zLVv&C_xn8ebKjq5{SSEdnc7FnZ!oJCY4l*%r6(9S)CvS+F-(hMOf` z9~|Gu?Vh?_Wu#PU$d;aEZ_tjchCx%YkZn{%;*6_e@vWyB@=|o7*OlH!^##ybn7D|t zbB&92$-AW$uYaeo&9Oh%rW5pUW|h0Yy;}z9y#f8{`SW;Kum!~wuJpMjF}=<$D1f2c zV$LV}`OOUCxyXN1zY;>c(i*_y>4D~mR=AjRj6OmRJ8x4x+|CPzb8Jk^EL^k@v#&~c z4Mu5#C^Pc?`;qHu#1_mC)z+(UNA69R{dp8HR5UQJ^VjT*cd-wH} z3fISq-p&hgf${MJaKP#64mLJ4;F$hlWr)x~Ma|&Q5aMCeaHGz`%UW9t{yTm>7O z8pIsEyuAVXG1S)|S`QI%Lm?__Z*ND4_hUzg;PD^R_%@-!@!}_$OzirgSS0&Cw&W&E=_D?u(bi|D@j zBPuG2)&Mc^;9!AYj$ekb;kTPehqE2Xt_7&Qs4W*3HXquF!u8CQnogeW-Hpz_ z^o;{bJxgY?vXx(QXYv2ZcsnnB$HagWsAiafdCl8y6}fEKK~c4{LON%EFy+fP2Jys8 zuaC1(9Pf0G*uqWm)W?9L=j!>N2~+7(GD-XAl#HEpu9v0x+#?7J#fp0Wsur3knsqk( zzP#IcxVR3Vz8V`EN~_9#?Q>>&N(xZ$Qfg`^gs`ROI#Wxkt0p=5NARXoTc@GYVVsI| zTI38SA%Kt6u*phD_!3RvxXXxJ8;&vR(lrw5A0ES<$?u+YA=3A8Z3h!Bb&(YfSwrK2*%@KkVD)ikO0rGNYPMv5+{@0+ zhQAx|rYTr#n^woZlY;zfidD8fOLnv~FLfa+i0u7*0C8G7t=u#*gK^E|=l z<|?ArVB~-L#Dl4TXU7h$?0fh0H8l7Hu!@l;GaRxWz0UaY($9DS5izGZ&{tJeRd{FC zV;KLnQ^GP@3=(KmYn-RQzq;W3O0{wjL0Or&nbj}zCY%Chg{UzD1NqyHcIBMl=YLC} zWKS{B8Uk_@E1o~WnS#nU&taa7O_$Xuml$|7iJ?c{p&{cy#zU>BEr0OOQM&e|)N$OXT0BO;uA93pe*O_t35tA~E&Kjwspni0B9~ z7?P=><=(8iR__Ya3iURbZ>Dqa*wp2@X_Lbd1yNny@NALQ&YF8aFgH>Az-ZxpT(@+Q z!zUJYj1uQ3kgf6i0wtdz-}DX1MY5cKS*$S-*NQb_HKRK(>Oq)TR=&MW z^0E3)#ad=)gBfn3Fi~*^Yb+b4GIP;oV~oh;wK; zkR-0@9>4l0{6EIEs`BU0$Eqh41O-tWB2iq9&Yr_pl6j!5Syx*-cu%RBHv(y@g6ld8 z1tqbk2Qta3HUN-CUmlIyz45JV<1%_gWJjKAL~ar3mO_5KPPJE zf@gF4b}g5&yyK0v8nUk~LmM%YnoyD28C(0DB#o4Y^4>$P?dboMqx^?w$23N8-FB74 zp{nobcnQo%a?BsOIcH$jM%u-XhnHCZ=kMD6_)u?SGwsi!2}~#t6%}urVc6@}J}+JrU4QCRwNrj} z@MhFG&c1vxq2p{BaJ zuymcSnquE`>}u0b$>R{g#f;KN_Somod* zHWcX19bNyQ4k|Z_HP-*`-&$AxXNBwf*FH*FAWiZY{Q&=@fd2Oo5{{Vv)tCJj7902C z?|giGeSLk(xoA*eb^G0i7f~s4Z^*Wr+z&5WOUAWblb#GD&vDl2={;{leiHr$<6oZz zNr6lJwj#u|vCJGx*Op)I9Z5BGMkV0+>r*6|+Zcb8Bq1&?Kej-a{*#X;qnX(sbUm0_rmv%x06^D z>p7I3no6vJkN=kmN3q-CI!W}>B@+{wgT8H;bKxb5n$vn`%e0-DfdMX(G;fSV8HJ!y z>ZFFYVW79;tS@ny9p`{Xno&qf%C`0^rly(p(^>-#D@zA|i=j2Wpe}b^7ZrWy!`Rp& z1DZ8i0!+59#TYWuMW3O z>%4hdSQwhSXceiq_B^=+4Wwt?V56djug9Ys`U zot-G`Ax_B-V*qySmjcvuDU{L$ZY+4P=mMQ8z&e0pjZAR_4!lL!$n5TfhSl8}?{Y*&O14>ui!URZfv}I|+m6Cj;oE>ZZ2ndiR)_5o zzw%uqrf1ed$dmLaF_H1|+gfoBCbl!+EK~-25|hv)DJ$=Fw;BuiCE^;bzg*wi$KF!C zTL{Cc$GHeyjrpa*t>jyvCH#H`6e$aA|MCpX?f++L;lJs^*SvV^?Mmc^|E?x^BXG^} z81&c6?H9K3_lfcU(fhCg8M`xQa-QCrL&pLHz?Jn+gXG&_{7*7G9IR!}LlqPJ82zh0 zR#a@;x>Y0JUR6trY0?hw*M3S$N9-d1**@8*@bUbG3n(q$owfdUgMUheVYlqeRufqU zs2-^`*QEc_0`RrNBok^#fOw%XHeP#_FBU8*U^MxuXU zV5tj*N~T)Y^xR`=iB?y}*)BSpyuJIkU3jS7Z_OQE^I*G@(xO=GzX?Z^kSLN8j7g%i z2fG1Rsb#a7_>!9iYM9W?vKZ0(Ymt92u^m8J-a~h&yL%tJ7m9^Wj$7O=+t}Eoq@=*v ziAd(@kHo)&%}R@Zi>w)niD+mxgBf-=Az^#7U->f{plxniTR+3UiN&ZKjEqWo zoCJ+%^v>=Im^iV1dsSn7CfL^sVgK;1zv+6NkWH(V>50 zTa5YD(#QQkSYu%>rO^NLLVEgXo29z&ooK2tNDdz>1iIwU%IA7z2@+0Z58b1$n5T+2 zaJ`CD%qE{Ei*!Ojd2fxBmB9g_MJ)?;!Ki!5Q7Uu;GD|0!=#I9AWigPJhmh4Od@x*p z48o#gGjFi1$3WH6b_@P9(L`SatIuY zQ~m8gLvJp4`oA9gugPAIoN$@=_Qf2S?<&2SxjbQ5hXyk?XaU1|%wu41!HSA$Z1Mw% zC;77{(S4*Q#92a8vcU=ra((lQ{%7vryT>5y=IRQh+`SWUtjh9x4-|87)WevUm6?g* zKSh$fl#~E3ufNgrCr<<%=DxkTn9D$trhuyu%)V=<>F70RXoBPl^FD&TT{S-`e43Ak zr>1>T170m$GYqFxE(cBsvVNZ)yks#N0X^E#ANrLY_r3#8-I_L0~0*OsH5Xcx||>gQp?;8^qU03Fw@&l!7{|$ z?ZJaOtri=Yem_~0e!uGlMTKK_!V+@Xrj83$tfq@+m zaA2GysH)!57QWZ^^gS0n2az4em_R?YYjU!*7~hKFqhHp+DT2JbM|Nj<`N`9#eIClO zJ-Bz<%8Ij{jY+I>LH5Y5w`?D%9wm)ce?R3#?!n@}XX~-s`5ycWZ@$l{p*3whLjSwq z5m(hFiJqd_vqwHfW#ZvkXbS*IIZS=XD!D{f0aOm0B9~7FpEePj))f-UhpP#q5&tR0 zTbdC#025!omaRTOMHLzvIyyKg7alYm$YY-K!v*3xJb`+-HoixE${^^NU%Lhqgq(e4 zUEQOLoaU6io)JW1WycPi$w%3R#6xaR3Tz4ZhiveFjN4^+{vNlpnXixANv`}gZh!i3#_jbz9$Hz~uUK?m&>=tZ z*H}DH&t_(5Xm(a%;8*@vsHd+lASg&y;wLBX{kH#NDD6{v8aq=3{`!1fc(KVg9kwj& zq0rO}+(8Ik%I?VK`tB!B&eF^MNQ9CDHSI@* z9I3x`rJ8-%?S@g2k}U9SQOP;B<6`a}^paSyW7QZxj z`}glNi@f@s{_Wag;RdB1;a^|>+0Kt9S0#TdZ?^IbF)o2kI z{EmY5`e9mH^&F9y1HVhc=7Hd?MV&$K-;1%cC&?OF{ra+VY)47_!((InssmMimupQ& z{~G!H^Z3R8pxM;})m)EM&hcW&9INga8(3Vun%F52GCTC_I;k|xnavUNBB0%W(KRUSm)dtHV z499olThXw7Q&m}U-t z$#sh(LtrU6Eumcc;9ozf7oNl)qodB~UF{X`pI|=FQRpn9so9DO5(dCshxlF$AL2Cl z0JXUVb~w8U!1QJa6A-Kwam(Q?sTG#kq#8eX;&{p8f^ejnj{Wz~tB6CSKrDk;zIFyL zK#a1V{Q0b2rpeV}t*s|m0ZsNXuJy%5(zUtcU30B|A}Ib{9yS*=_# zGz7hQ3A!X`AZ#p>2+UVOr=|L03Acy5smRO^T*kTpLsr>xs2ua== zG+G5R5InL_h?pkrnXIn5flyM0OX>wtov9wu|+}b(=pS_~EynH6S0GQ<=vO^g) zg}YJd#AtgKTDf9|Saz-SM`e5h>=83q%?VjSa>J|n>)(Zo)}Ka_Rn6=h8&oxMIdpe- zBl3Mgs7VovtKHe?E0F*5KVWg~T03HMjtjQRci_s0j%f-SH)_5r9b0K>X^d^;iO8@Z+z%r1=bEf9@&^<&nWy019p1kljGobV+@4}>9N{~m`vOUbs z(4dm-5_ ze7nhL+Y~?a5LMTN^2stqxVe3uv@|q^+OtB}SdzvC+Bjhp_W!>TQQ{ zDRyU+`{Ayt645RVJ)Gb_Jy zNFkEGOVu8Yn$$2JwfftC&g_>xEMU`-Wx-CbEmA1e1PB{1R!}djoOo6L`EwQ?Xi$Qu z(y-o+^7S$3*nE7pOLLZ$m9kT4jhh2Fh~fpXLu?_ZmgY7F_X-aWXJ?}6&LRkTE%|qF zy6N%RR4+oR!W@*ETJ?rV4+h#97OOCO<>s*Qa>fT~n>cPHE5eJZq>|7_dUW2Z?=UoH z<8LaK*@x_X`Y!Vw+Po4(Zid5D&?F z9mEH6^&C=P8%XZ!8g;prgzPkSa}1Jo+Ffv=^s|6YQD(p1c^t0k-7i)?8d?O@jBGUs z)nC1^6co#Lq2lG9FTr0|FlAi3>(Bav&V6*aXLj0=KrGZu3uRE=d63g*{s7@=l$V8? znx8>Tbo2>TDj0VIYKwcU@L-`e1j3{wgy8Jw>l>wY-NB&)EXo#UGfTch)CXy3s*YS( z79~VPyDGlZO!YSXlK#;2KFa?z)6i!Hr)u+d*Pd9ZUr_fUy&5zpbW!||LJoD zgcwC}=fiJ?G;sas5E2vVw60}?gM$izoQb+Z^_NG|w}2-2J1_yc)juQzDIP9$b*hD6 zWDXrLRWYIZ@GCDNy(rz%T(1J9ukro+V`t7(VxqS9CsO@=+n3?yH&G``)E{cG#NLO< z2^>ro`FWoJgM3)t4Yzga$B%|Tt6QfyO(Yc*6b53Vlsx@1U#!FiL`5YfXJe_!bNh|AtHdz2>^!6STwYS* zXUl(@y=Qlu;aM6H`zZ~kt&Mt@|Mo!CQ=1gMy(tV|Eyn2*&LMcmiSM3HT_zp-TOB!O zvl&qelR4uA7!T|}>D%WUcaPjK2-vKxoquthz_45EUjy7?0-Oprw7h@+QaBjD#Pj@n zK9`mnetI3)6t=oWL+ev`?L`cJTjYK~84FwqhX)p$8XrHeZ3S#JHSM%nOETmL{AV6M ziK`Y}#cn{nf5kM-^f7&-)rs>5EX>W}n1J5a1{a;l=YF*NEmfzIuZ6G9HYIBNpMcQ5 zmx{_6r3(bplr&5VL0k`)@s3VE7St2|h}|)VF-LvOo>|u<@KcgrOjwvSt*I8DqZ6FZ zFTv0c7nfS-eI&&2Xl2ipRpVmfm)JDpCtu6pUrs)`^>Jqkev@@lwjW-+!|B1xoAt=u z9ZjSKww&QI8HZc7^FrcPOn1gB=#y9v}3tj{U;_XGC{s` zJxFv^4@_#<-Z&4lMt$16rGso<+whJ!k#0>gxSs5xUj4`wo?L^xT1;$W(Jy>$J5Vw{t8-E*avb zD9p@-S>e0<{sBs+hMKsE zolW!KWUF3YgZCueLKQ{ZX8_|k^!cWfbFCTgrfEwn$gI>pocWZpe42{Jr8KFhIl&+H z)KZJB#Rcv-)v`>R-JQ0F7OJoHF~S=pIz}0K)WYBl?3}p!rkU8bG1=2|4_%IjrpDtH zm}DNcv+HffN%MHm#m3j{zSFJ27nJI!;pu~>9YJ!Q@{7Rj>uKstxd=OA02C*hK|$7l zrL=3rq;rcFId3$+R~)$vK=VSHu_J)>SM-Uks&n&?cbbLufG?j+C4+R+R3I1e{xeoq zR&)eY1HQ4ZW%a_4=}9CKwG@ktc_$j^>!n~-?1l%{(7muq`CgRo3HJ`2G)Yg>Ou}Ti z{mx;b%Bl#au3#v7N|)U?CjXGzrx*Cl8e2%HC7)p+tmxWNbhfahYEN2LRtz=8CTmlR zmTwZ9lJ>I(rET9uM%IyLtUlgOBVYdolNKT$ov#lI;YT_J;dPVBXxU8zK4P(u`>y%0 zf10zotDmm&&|EQxNU*$cZxXJVIijqjNRI&+ikYOu+dV`aGz)t(NWC7g!V}{0Gv9 zth@WV1L1kneI8_GF&?Asj1A~>!_zi-FgnF~oe`bHgXsVAE+;^9a)kBAosXfKZyKl?41~IX(Nj~>t zLFVW=A0Ia`_=Ihqm6^Ht$Vkl*v+4q}BMJq!Dtan@UsJl-nVeC*A~-mRnS@V1Iu`fF zqz}04VKI@-OWzB7QWa01fFt(GJ$}qc`9f}2gvBaBN`F_rk-Jcn_q_)BY1OH|#_O~1NKY85vbczcCEEkveFld{`c*r)zzvM%xLxI=8#-ptvLWBj>1I(b64a;hcW0*?qdIsE{q5xw_?t96#T(}s;lIcWirvJo*&=y9`F91T@lAv9bdB<@rVK43Q`6p{@R5$P1ACh<0#LqO3{^vrqFDG#! zk@?Q)@{TVGbUi0;?V- z(p3cw28x)@zuZO#!WkACjyFGG?d9H~+jo+I;)Vgg*bwiWzb)1C_;~6`PZ(m>fO!`O zDjt}R%`X9C_9wsB%7@m6@HlO{)FxE&s^+XWdVXFmuI9SB*rg3+Fb3<>V5H$KXqJoq zsVl?h)gYI;tbC~OM~7EvV;);56+OCM;9tpOE6Be-b&V%vY`T;R02j8RV0~6UYT=X= z4$LWh0O3Ou)pSDmvHo-6xH)LMJWttG^wv z@GwwxJ^Y4S9IA@bJm;fI7F<{J)ha9EJP$XOM1G<5Dd6TWyX#44c!5d71fN*hyuD%Hiy%y$yGo6ZZC9 z(a6p0W4Ovx>&N?0`0-HGp8i_ZiGZy$&(+SWOfGx&qB+LX#(Yz0Xj^_sP0jUkk71Fe zG15uTt5-@wyI}NH&-2?WY!`KY{A5ZOoh*~Mg9Dk5jJ&R>rsf4DB~4vXC6Dq5XE*+A z%UOJbt@wC0IXF2TS*B$a5DWudwTG1xqIAJaG~?WuMk;w^iCCr zb6sE&`LQkECx&@>k*R8;`IqUAvmffo;Naq-6B2U#l9K@{gw^d?c(jCFi{WZ%Z*7Kc z)6>l@HzA=NTX;d>z{jihddw#;P!;1JRUP$2t($#y@AjQs86A_ZtiUPJ-<~D1oCI0W zd}`RP+(Sg_nH9@qhGHrX%Kp2gZn5^_l*gT&3(@x|dD=ieLmPyQFvF&A82P+9V@>Ru zec;}V9ZHc&6H_5At#$_&)uam!jZ1@bglswlOX3hV<)m5lkc~6GJDYmHae2X~6(@+S)9wsK5+g-sWX>gq_M~}kMiGI|G(Dla#EsFNAWl=9G z2fuR)^|hGAVC0AJi)+XrGXFlTlauq9EExztSmBnIm2If64|mipK9eC)ja(DgT+^0{)|4+=yZOe@GSkP?pE5!IQlai zgt>ceDlCUx+^h3u$1<&&$H}Dl=;a6Iut`f2#po3X*wK38b zMj(dy45Nu3+q~#j+17CDzN+JfQ4Gf#o)UIT%gYn(7!Rbm39n3l4vmp#$kEtMLPC}- zb54{HQdzMlNFn=#LLAZl{&p@$%hl=6HeD+yIbR9%V%47Cix<*Q zfT@r6^4d#pUcuq>UBG6^mr?jhEq2?)D-uRws@+FhCrt`K+_?<)ihVBhdeo}5Et^tf z78|tSvzB6K5*F4Kl$_{!)ElK*9U|ana|mQ7S;>XHqrpEuZJb;c&B{B%=e+VByN(ak zZ+fY%yeA-kf6j3jLwJ@s_wLC$kCrZ6#A;zO`q;3Z^RSOYJf0@&fkz|WB}PWD4w&u6 zrl9tYxAH?$zWE`_hIzK*L=@vbTqYKTt5*ZEJ3+ES=T~}-v75{|^lbx~58qwdlT%KA z?39xFd{(a9#mYiU%k=ur5x1qpE6hbxVXFeh`$I1z#YUc&(U9t7pl4)br z0K$Drcu+w}V@?2>l2Bl!lvm7T3L5g`ywUoEY2WjOulKS`StT1RY>M4W#inzKolN5U z*xY#ckhNE7dPc_WVS8FbjByyd)RGw%3{vJ>v9$&6pe`q-gRFWZ5Qw$xlWD85OF2zp zxMe(ME}{)Z9oc?1JBHVD!j}POk@EmAk|K9MqB{+8a}Rvgo~}I8rx!pSp~}>g2tC^6z5VJkg^CLqYeBls&4+wv6T@D9>*<-U zxUIY)Rtya)j#-0tLO=AbRyjtmEp&b^bT(+^DooxY*@N!+c$yvNlCj0X zz0t@h+DE#zO2rjwAB%Z%^NzYGYv+K?%0DPWHY1@J$Gg}D(TIv5_4iCI?xpI%Jwy%Cpj_CDdDOobVa1)k+>!%;tFh&ax#N>}mIFgWr8xLlxdWXNr>YEZOzUdVr*VCf|8C!#+^?W9Nw14a55SzMUEZC1&XOCafYoHw@nKtr5<%HeUxMAu;uoc!r-!3Vfbr4m@g~zXCUJ=zn!#z5Ncez0J6yg z#?`|KP)pAi?KA!3k3Z5)n}a-ePHA(U=I9-#*tf6C-Tj8>Oj2Zg&4!hu2M=mq6e1NC z;pJi45p#o-HeJt3`zp1o2zYYR8*nN_yFcI-M_J zj)eiJzaaT{C{7+8bXB>k7WgCDp@T*ABGQ(VnYpUC*gNz4m)z$Caj8BG@`V|OhIW=I zN+4d9I&*e09c$wbzephLrr9S`sJ|f5ee5gDM)`=O&MBK#Z-FKK>xU20dU}OnVMiGl zVkb_f`vVi)1mGZiJc#{rTC9p2s)8oA;C`O8tLtm$Pw1tsH8ng!LUhNEpVJY>1+wQSvNeF;IW6BrcekqK`lp?zOp*U;Qh|Kzdz?qhpY87U9G zVsX=EpfI2uucDcpCOtK5#L3AaC*!Ul7cQhS_YA)l?%1F0M>mkTG<8)}(HsaYlXT_U zap&%FkJwWTrrYXQeq^!~*L$n~w9Gau%z5dsQ6%@pW*_R>g5K4YIobBWmj^7angw}> zysNEUGno~fS!)<#kJvAqAUIVovTvpI|D)6YUj0f*=4J&YkMF55oej=SpI)d+|LFBO zvbsX?B`G(=&hG7$T#TM0;he;6MabK8Vrzr5FlvT`SVjiMlM)Y{%r-T|dqjEleN|tb zHa0fKf!}dY+!<_hdt#bw?yc!$SzE}G8se5|Lb=`=M+{i*l$6VXp z^GN<0)ig6B6Ve!^R3j^ohR-X=@o*$wa3*QhJP6gR=(Fn@<45nXTSQbu=qta8m8MQv z^*yQtA4*@J2dkYJHNTFG3ih2qP&;5Wapr)nO!%%E~I8sk&d4qwm@ya5Yhehi5*1vNw z^m(F}te8W`;``&;v`bkUbDg!y%UW7Q8g~EYB$*A_6IF2rBFm?^5~5;beNK3-j0_9h zeGwm~bC8XfRVd`L@a=ipp_q(oSFfsBIapcA5(tFMFctflX zO+v!3%Xhb`YDQ|>x!HI2dI2mW{w&IVtqNT63DhI$99(~Wk$_lSl z*QBOIEwZ#~M*O~`wY9a~y^}q*R&jCt6zAefWdGC=7r%d!9j=GOs3-}O7hX5)?U8Jj zw0x5gH0>lWqHXYzsnXbeC4Y)AzNl-zw3wCpzDHXt2np~}|3F{H-CMJDBt@~fPqbj} z!x%+8sfrIaFK}>jhK(;REvdgtVonR0x8s+U%s5a#9OQnW32)-JYo6c7ZFi73Y`~NZ zp^Y6ROSB$dM6Y{9StFwyzF^g#F^H}_PO-GiP;T<}^Lypxg)DI_2J16GWHp`~>FhS) z51se6%D`_41T(j)wPg=XEQs2NSG*n?iu_?9+eWdehv8sVh4O(&>DMs{QmTE`)vuW} z7t2=&N=i`%Z-_)O`fF{cRnR=bjt>>d_woqw`(7yb!}jD-BTH)5CW{U7R%kXZl# literal 0 HcmV?d00001 diff --git a/src/data/openapi.json b/src/data/openapi.json index 06f0167..9acf6d2 100644 --- a/src/data/openapi.json +++ b/src/data/openapi.json @@ -18,7 +18,7 @@ ], "security": [ { - "bearerAuth": [] + "apiKey": [] } ], "tags": [ @@ -1596,7 +1596,7 @@ "summary": "Create a run", "security": [ { - "bearerAuth": [] + "apiKey": [] }, { "platformToken": [] @@ -1838,7 +1838,7 @@ "summary": "List runs", "security": [ { - "bearerAuth": [] + "apiKey": [] }, { "platformToken": [] @@ -2014,7 +2014,7 @@ "summary": "Retrieve a run", "security": [ { - "bearerAuth": [] + "apiKey": [] }, { "platformToken": [] @@ -2140,7 +2140,7 @@ "summary": "Follow a run's events", "security": [ { - "bearerAuth": [] + "apiKey": [] }, { "platformToken": [] @@ -2339,7 +2339,7 @@ "summary": "Cancel a run", "security": [ { - "bearerAuth": [] + "apiKey": [] }, { "platformToken": [] @@ -2511,7 +2511,7 @@ "summary": "List workspaces", "security": [ { - "bearerAuth": [] + "apiKey": [] }, { "platformToken": [] @@ -2742,7 +2742,7 @@ }, "components": { "securitySchemes": { - "bearerAuth": { + "apiKey": { "type": "http", "scheme": "bearer", "description": "Member API key (`sk-...`), from \"API Keys\" in your settings on the platform. Send it as `Authorization: Bearer `. It works on every inference endpoint, on the Runs endpoints and on the workspace list. Personal and non-transferable. The key that lives inside a workspace (the one its agents use) is refused by the Runs endpoints with `403 workspace_key_not_allowed`." diff --git a/src/lib/openapiRuns.test.ts b/src/lib/openapiRuns.test.ts index 0348d6c..92c586a 100644 --- a/src/lib/openapiRuns.test.ts +++ b/src/lib/openapiRuns.test.ts @@ -126,15 +126,15 @@ describe('openapi.json: credentials', () => { const schemes = s.components.securitySchemes; it('documents the API key and the platform token as two bearer schemes', () => { - expect(schemes.bearerAuth).toMatchObject({ type: 'http', scheme: 'bearer' }); - expect(schemes.bearerAuth.description).toContain('`sk-...`'); + expect(schemes.apiKey).toMatchObject({ type: 'http', scheme: 'bearer' }); + expect(schemes.apiKey.description).toContain('`sk-...`'); expect(schemes.platformToken).toMatchObject({ type: 'http', scheme: 'bearer', bearerFormat: 'nan_pat_...' }); expect(schemes.platformToken.description).toMatch(/never works for inference/); expect(schemes.platformToken.description).toContain('token_scope_denied'); }); it('inference operations take the API key only (the global default)', () => { - expect(s.security).toEqual([{ bearerAuth: [] }]); + expect(s.security).toEqual([{ apiKey: [] }]); for (const [path, item] of Object.entries(s.paths) as Array<[string, any]>) { if (path.startsWith('/runs') || path.startsWith('/api/')) continue; for (const method of ['get', 'post']) if (item[method]) expect(item[method].security, path).toBeUndefined(); @@ -142,12 +142,12 @@ describe('openapi.json: credentials', () => { }); it.each(RUN_OPS)('%s %s accepts an API key or a platform token', (path, method) => { - expect(op(path, method).security).toEqual([{ bearerAuth: [] }, { platformToken: [] }]); + expect(op(path, method).security).toEqual([{ apiKey: [] }, { platformToken: [] }]); expect(op(path, method).description).toContain('a member API key (`sk-...`) or a platform token (`nan_pat_...`)'); }); it('the workspace list accepts both, one workspace by id only a platform token', () => { - expect(op('/api/workspaces', 'get').security).toEqual([{ bearerAuth: [] }, { platformToken: [] }]); + expect(op('/api/workspaces', 'get').security).toEqual([{ apiKey: [] }, { platformToken: [] }]); expect(op('/api/workspaces/{id}', 'get').security).toEqual([{ platformToken: [] }]); expect(op('/api/workspaces/{id}', 'get').description).toMatch(/API keys \(`sk-\.\.\.`\) are not accepted/); }); From 696c2eb42cf9122ebda43ef27fe0172b318c8e7d Mon Sep 17 00:00:00 2001 From: barckcode Date: Sat, 10 Oct 2026 22:29:33 +0200 Subject: [PATCH 4/9] docs(runs): move the API details to the API reference The guide keeps one end-to-end example (create a run, follow it with curl -N) and the API keys vs tokens explanation, and links to the reference for the endpoints, fields, events, errors and credentials. Tests check the links and that the reference has the runs operations. Co-Authored-By: Claude Opus 5.5 --- src/content/docs-es/runs.mdx | 87 ++++-------------------------- src/content/docs/runs.mdx | 87 ++++-------------------------- src/tests/layouts/runsDocs.test.ts | 68 +++++++++++++++-------- 3 files changed, 67 insertions(+), 175 deletions(-) diff --git a/src/content/docs-es/runs.mdx b/src/content/docs-es/runs.mdx index 75ed923..9c24bbb 100644 --- a/src/content/docs-es/runs.mdx +++ b/src/content/docs-es/runs.mdx @@ -302,106 +302,39 @@ Un **token** es lo que conviene darle a un script que solo gestiona runs. No sir - Cada uno caduca a los **30, 90 o 365 días**, o nunca. - Puedes revocar cualquiera en cualquier momento. -Para crear runs sigue haciendo falta un plan con inferencia, uses la credencial que uses. +Para crear runs sigue haciendo falta un plan con inferencia, uses la credencial que uses. Qué credencial acepta cada endpoint está en la [referencia de la API](/es/docs/api#description/platform-tokens). La clave que hay dentro de un workspace, la que usan sus agentes para la inferencia, no puede gestionar runs: la API le responde con un `403`. Un agente que trabaja en tu workspace no puede lanzar runs nuevos por su cuenta, salvo que le des una de tus propias API keys o tokens. -### Crea un run +### Crea un run y síguelo + +Un ejemplo de principio a fin: crea un run, guarda su `id` (con [`jq`](https://jqlang.org/)) y sigue su log en directo hasta que termine. ```bash export NAN_TOKEN="nan_pat_..." -curl https://api.nan.builders/v1/runs \ +RUN_ID=$(curl -s https://api.nan.builders/v1/runs \ -H "Authorization: Bearer $NAN_TOKEN" \ -H "Content-Type: application/json" \ - -H "Idempotency-Key: fix-date-parser-1" \ -d '{ "workspace": "develop", "agent": "pi", "prompt": "añade tests al parser de fechas y haz que pasen", - "cwd": "/home/nan/projects/api", - "git_isolation": null, - "timeout_seconds": 1800 - }' -``` - -| Campo | Obligatorio | Qué es | -|---|---|---| -| `workspace` | Sí | Nombre o id del workspace. | -| `agent` | Sí | `pi` o `hermes`. | -| `prompt` | Sí | La tarea, hasta 32 KiB. | -| `cwd` | No | La carpeta, `/home/nan` por defecto. | -| `git_isolation` | No | Rama aparte: `null` para auto (por defecto), `true` para on, `false` para off. | -| `timeout_seconds` | No | El tiempo máximo, de 60 a 7200. 1800 (30 minutos) por defecto. | -| `model` | No | El modelo del agente. Por defecto, el suyo. | - -Los campos equivalen al formulario del portal: **Agent** es `agent`, **Task** es `prompt`, **Folder** es `cwd`, **Separate branch** es `git_isolation` y **Time limit** es `timeout_seconds`. - -La respuesta es el run, con su `id` y su `state`. La cabecera `Idempotency-Key` es opcional: si un reintento manda la misma clave con el mismo cuerpo, recibes el run que ya se creó en lugar de uno nuevo. - -### Síguelo + "cwd": "/home/nan/projects/api" + }' | jq -r .id) -Los eventos de un run llegan como un stream de server-sent events cuando pides `text/event-stream`. Con `curl -N` los ves según llegan: - -```bash curl -N https://api.nan.builders/v1/runs/$RUN_ID/events \ -H "Authorization: Bearer $NAN_TOKEN" \ -H "Accept: text/event-stream" ``` -Cada evento del stream es uno de estos: - -- `event: run_event`, con una línea `id:`: un evento del log (un mensaje, una llamada a una herramienta, su resultado...). Manda el último `id` que hayas visto en la cabecera `Last-Event-ID` para retomar tras un corte, sin perder ni repetir nada. -- `event: state`: el run ha cambiado de estado. -- `event: end`: el run ha terminado y su log se ha enviado entero. Los datos son el run terminado. El stream se cierra después. - -Cada 15 segundos el stream manda una línea de comentario para mantener la conexión abierta. Un stream dura como mucho 30 minutos: si se cierra antes del `end`, vuelve a conectarte con `Last-Event-ID` y sigue donde lo dejaste. Sin la cabecera `Accept`, el mismo endpoint responde con páginas de JSON: pasa el `next_after` de cada página como `?after=` para pedir la siguiente. - -Para ver cómo ha terminado: - -```bash -curl https://api.nan.builders/v1/runs/$RUN_ID \ - -H "Authorization: Bearer $NAN_TOKEN" -``` - -### Cancélalo - -```bash -curl -X POST https://api.nan.builders/v1/runs/$RUN_ID/cancel \ - -H "Authorization: Bearer $NAN_TOKEN" -``` - -### Lista tus runs - -```bash -curl "https://api.nan.builders/v1/runs?workspace=develop&state=running&limit=20" \ - -H "Authorization: Bearer $NAN_TOKEN" -``` - -Todos los filtros son opcionales. Si hay más runs de los que caben en una página, la respuesta trae un `next_cursor`: pásalo como `?cursor=` para pedir la siguiente. - -### Errores - -Los errores usan el mismo formato que la API de inferencia: +Los campos equivalen al formulario del portal: **Agent** es `agent`, **Task** es `prompt`, **Folder** es `cwd`, **Separate branch** es `git_isolation` y **Time limit** es `timeout_seconds`. -```json -{"error": {"message": "...", "type": "...", "param": null, "code": "tier_restricted"}} -``` +El stream manda un frame `run_event` por cada evento del log, un frame `state` cuando el run cambia de estado y un frame `end` con el run terminado, y después se cierra. -| Estado y código | Qué hacer | -|---|---| -| `400 invalid_request` | Hay un campo incorrecto. `param` te dice cuál. | -| `403 tier_restricted` | Tu plan no incluye inferencia. Mejóralo para crear runs. | -| `403 workspace_key_not_allowed` | Has usado la clave de dentro de un workspace. Usa una API key o un token. | -| `404` | El workspace o el run no existe, o no es tuyo. | -| `409 idempotency_conflict` | Has repetido una `Idempotency-Key` con otro cuerpo. Usa una clave nueva. | -| `409 workspace_not_running` | Enciende primero el workspace. | -| `409 agent_not_installed` | Instala el agente desde el panel de **Software**, o elige el otro. | -| `409 no_inference_key` | El workspace no tiene clave de inferencia. Comprueba que tu plan incluye inferencia. | -| `429 run_queue_full` | La cola está llena (20 por workspace, 50 en total). Espera a que terminen algunos runs. | -| `429 rate_limited` | Más de 60 runs en la última hora. Vuelve a intentarlo más tarde. | +Referencia completa: [Referencia de la API → Runs](/es/docs/api#tag/runs). Ahí están todos los endpoints (listar, consultar, cancelar), los campos de cada petición y respuesta, los tipos de evento, cómo retomar un stream, los reintentos idempotentes y todos los códigos de error. Para sacar el nombre o el id de un workspace desde un script, consulta [Referencia de la API → Workspaces](/es/docs/api#tag/workspaces). ## Preguntas frecuentes diff --git a/src/content/docs/runs.mdx b/src/content/docs/runs.mdx index 1fedabe..ee8cf22 100644 --- a/src/content/docs/runs.mdx +++ b/src/content/docs/runs.mdx @@ -302,106 +302,39 @@ A **token** is the one to give a script that only manages runs. It cannot call t - Each one expires after **30, 90 or 365 days**, or never. - You can revoke any of them at any time. -Creating runs still needs a plan with inference, whatever credential you use. +Creating runs still needs a plan with inference, whatever credential you use. Which credential each endpoint accepts is in the [API reference](/docs/api#description/platform-tokens). The key inside a workspace, the one its agents use for inference, cannot manage runs: the API answers it with `403`. An agent working in your workspace cannot start new runs on its own, unless you give it one of your own API keys or tokens. -### Create a run +### Create a run and follow it + +An end-to-end example: create a run, keep its `id` (with [`jq`](https://jqlang.org/)) and follow its log live until it ends. ```bash export NAN_TOKEN="nan_pat_..." -curl https://api.nan.builders/v1/runs \ +RUN_ID=$(curl -s https://api.nan.builders/v1/runs \ -H "Authorization: Bearer $NAN_TOKEN" \ -H "Content-Type: application/json" \ - -H "Idempotency-Key: fix-date-parser-1" \ -d '{ "workspace": "develop", "agent": "pi", "prompt": "add tests for the date parser and make them pass", - "cwd": "/home/nan/projects/api", - "git_isolation": null, - "timeout_seconds": 1800 - }' -``` - -| Field | Required | What it is | -|---|---|---| -| `workspace` | Yes | Name or id of the workspace. | -| `agent` | Yes | `pi` or `hermes`. | -| `prompt` | Yes | The task, up to 32 KiB. | -| `cwd` | No | The folder, `/home/nan` by default. | -| `git_isolation` | No | Separate branch: `null` for auto (the default), `true` for on, `false` for off. | -| `timeout_seconds` | No | The time limit, from 60 to 7200. 1800 (30 minutes) by default. | -| `model` | No | The model for the agent. Its own by default. | - -The fields match the portal form: **Agent** is `agent`, **Task** is `prompt`, **Folder** is `cwd`, **Separate branch** is `git_isolation` and **Time limit** is `timeout_seconds`. - -The answer is the run, with its `id` and its `state`. The `Idempotency-Key` header is optional: if a retry sends the same key with the same body, you get the run that was already created instead of a second one. - -### Follow it + "cwd": "/home/nan/projects/api" + }' | jq -r .id) -The events of a run come as a stream of server-sent events when you ask for `text/event-stream`. With `curl -N` you see them as they arrive: - -```bash curl -N https://api.nan.builders/v1/runs/$RUN_ID/events \ -H "Authorization: Bearer $NAN_TOKEN" \ -H "Accept: text/event-stream" ``` -Each frame is one of these: - -- `event: run_event`, with an `id:` line: one event of the log (a message, a tool call, its result...). Send the last `id` you saw as the `Last-Event-ID` header to resume after a dropped connection, without missing or repeating anything. -- `event: state`: the run changed state. -- `event: end`: the run is over and its log has been sent in full. The data is the finished run. The stream closes after it. - -Every 15 seconds the stream sends a comment line to keep the connection open. A stream lasts 30 minutes at most: if it closes before the `end`, reconnect with `Last-Event-ID` and carry on. Without the `Accept` header, the same endpoint answers pages of JSON instead: pass the `next_after` of each page as `?after=` to get the next one. - -To see how it ended: - -```bash -curl https://api.nan.builders/v1/runs/$RUN_ID \ - -H "Authorization: Bearer $NAN_TOKEN" -``` - -### Cancel it - -```bash -curl -X POST https://api.nan.builders/v1/runs/$RUN_ID/cancel \ - -H "Authorization: Bearer $NAN_TOKEN" -``` - -### List your runs - -```bash -curl "https://api.nan.builders/v1/runs?workspace=develop&state=running&limit=20" \ - -H "Authorization: Bearer $NAN_TOKEN" -``` - -All filters are optional. If there are more runs than fit in a page, the answer has a `next_cursor`: pass it as `?cursor=` to get the next page. - -### Errors - -Errors use the same format as the inference API: +The fields match the portal form: **Agent** is `agent`, **Task** is `prompt`, **Folder** is `cwd`, **Separate branch** is `git_isolation` and **Time limit** is `timeout_seconds`. -```json -{"error": {"message": "...", "type": "...", "param": null, "code": "tier_restricted"}} -``` +The stream sends one `run_event` frame per event of the log, a `state` frame when the run changes state, and an `end` frame with the finished run, after which it closes. -| Status and code | What to do | -|---|---| -| `400 invalid_request` | A field is wrong. `param` says which one. | -| `403 tier_restricted` | Your plan does not include inference. Upgrade it to create runs. | -| `403 workspace_key_not_allowed` | You used the key from inside a workspace. Use an API key or a token. | -| `404` | The workspace or the run does not exist, or is not yours. | -| `409 idempotency_conflict` | You reused an `Idempotency-Key` with a different body. Use a new key. | -| `409 workspace_not_running` | Start the workspace first. | -| `409 agent_not_installed` | Install the agent from the **Software** panel, or pick the other one. | -| `409 no_inference_key` | The workspace has no inference key. Check that your plan includes inference. | -| `429 run_queue_full` | The queue is full (20 per workspace, 50 in total). Wait for some runs to end. | -| `429 rate_limited` | More than 60 runs in the last hour. Try again later. | +Full reference: [API reference → Runs](/docs/api#tag/runs). It has every endpoint (list, retrieve, cancel), the request and response fields, the event types, how to resume a stream, idempotent retries and every error code. To find a workspace's name or id from a script, see [API reference → Workspaces](/docs/api#tag/workspaces). ## FAQ diff --git a/src/tests/layouts/runsDocs.test.ts b/src/tests/layouts/runsDocs.test.ts index 4ac44ef..248ebb5 100644 --- a/src/tests/layouts/runsDocs.test.ts +++ b/src/tests/layouts/runsDocs.test.ts @@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import { parseFrontmatter } from '@astrojs/markdown-remark'; import { mdxToText } from '../../lib/mdxToText'; +import openapi from '../../data/openapi.json'; /** * The Agent runs guide: what members read before they start their first run, @@ -174,7 +175,7 @@ describe.each(Object.entries(pages))('agent runs guide (%s)', (locale, page) => }); test('says runs need inference, count as usage and mask secrets', () => { - expect(body).toContain('tier_restricted'); + expect(flat).toMatch(/\*\*(A plan with inference|Un plan con inferencia)\.\*\*/); expect(body).toContain('`***`'); }); @@ -194,29 +195,34 @@ describe.each(Object.entries(pages))('agent runs guide (%s)', (locale, page) => expect(flat).toMatch(/2 (seconds|segundos)/); }); - test('documents the API with working curl examples: create, follow, cancel', () => { - expect(body).toContain('-H "Idempotency-Key:'); + test('keeps one end-to-end API example: create a run, then follow it with curl -N', () => { + expect(body).toContain('curl -s https://api.nan.builders/v1/runs'); + expect(body).toContain('| jq -r .id)'); expect(body).toContain('curl -N https://api.nan.builders/v1/runs/$RUN_ID/events'); expect(body).toContain('-H "Accept: text/event-stream"'); - expect(body).toContain('curl -X POST https://api.nan.builders/v1/runs/$RUN_ID/cancel'); - // One table, rows back to back: a paragraph between two rows splits it and - // turns the rest into loose text with pipes. - const rows = ['workspace', 'agent', 'prompt', 'cwd', 'git_isolation', 'timeout_seconds', 'model']; - const table = rows.map((f) => `\\| \`${f}\` \\|[^\\n]*`).join('\\n'); - expect(body).toMatch(new RegExp(table)); - for (const frame of ['event: run_event', 'event: state', 'event: end']) expect(body).toContain(frame); - expect(body).toContain('Last-Event-ID'); - expect(body).toContain('next_cursor'); - expect(body).toContain('"error": {"message"'); - for (const code of ['400 invalid_request', '404', '409 idempotency_conflict', '429 run_queue_full', '429 rate_limited']) expect(body, code).toContain(`| \`${code}\` |`); - }); - - test('the create example is valid JSON with the documented fields', () => { + for (const frame of ['`run_event`', '`state`', '`end`']) expect(body).toContain(frame); + }); + + test('the create example is valid JSON that the API reference schema accepts', () => { const json = /-d '(\{[\s\S]*?\})'/.exec(body)![1]; const req = JSON.parse(json); - expect(Object.keys(req).sort()).toEqual(['agent', 'cwd', 'git_isolation', 'prompt', 'timeout_seconds', 'workspace']); - expect(req.timeout_seconds).toBeGreaterThanOrEqual(60); - expect(req.timeout_seconds).toBeLessThanOrEqual(7200); + const schema = (openapi as any).components.schemas.CreateRunRequest; + for (const key of schema.required) expect(req, key).toHaveProperty(key); + for (const key of Object.keys(req)) expect(schema.properties, key).toHaveProperty(key); + expect(schema.properties.agent.enum).toContain(req.agent); + }); + + test('sends readers to the API reference for the full Runs and Workspaces contract', () => { + expect(body).toContain(`(${page.prefix}/docs/api#tag/runs)`); + expect(body).toContain(`(${page.prefix}/docs/api#tag/workspaces)`); + expect(body).toContain(`(${page.prefix}/docs/api#description/platform-tokens)`); + }); + + test('leaves the endpoint, error and event tables to the reference', () => { + expect(body).not.toMatch(/^### (Errors|Errores|Cancel it|Cancélalo|List your runs|Lista tus runs)$/m); + expect(body).not.toMatch(/^\| `(400 invalid_request|409 idempotency_conflict|429 run_queue_full)` \|/m); + expect(body).not.toMatch(/^\| `timeout_seconds` \|/m); + expect(body).not.toContain('curl -X POST https://api.nan.builders/v1/runs/$RUN_ID/cancel'); }); test('explains API keys vs tokens', () => { @@ -225,7 +231,6 @@ describe.each(Object.entries(pages))('agent runs guide (%s)', (locale, page) => expect(flat).toMatch(/\*\*(No, never|No, nunca)\*\*/); expect(flat).toMatch(/\*\*10\*\*/); expect(flat).toMatch(/\*\*30, 90 (or|o) 365/); - expect(body).toContain('workspace_key_not_allowed'); expect(flat).toMatch(/(spend your plan|gastan tu plan)/); }); @@ -359,3 +364,24 @@ describe.each(Object.entries(pages))('CLI authentication for runs (%s)', (locale expect(cli).toContain('`NAN_TOKEN`, `--token-file` o'.replace(' o', locale === 'en' ? ' or' : ' o')); }); }); + +describe('the API reference documents what the guide links to', () => { + const spec = openapi as any; + + test('the Runs and Workspaces tags exist', () => { + const tags = spec.tags.map((t: any) => t.name); + expect(tags).toEqual(expect.arrayContaining(['Runs', 'Workspaces'])); + }); + + test('every runs operation is in the reference', () => { + expect(spec.paths['/runs'].post.operationId).toBe('createRun'); + expect(spec.paths['/runs'].get.operationId).toBe('listRuns'); + expect(spec.paths['/runs/{id}'].get.operationId).toBe('getRun'); + expect(spec.paths['/runs/{id}/events'].get.operationId).toBe('getRunEvents'); + expect(spec.paths['/runs/{id}/cancel'].post.operationId).toBe('cancelRun'); + }); + + test('the overview has the Platform tokens section the guide links to', () => { + expect(spec.info.description).toContain('### Platform tokens'); + }); +}); From c06efcf992dbaa9906de2d64454d026492adcb83 Mon Sep 17 00:00:00 2001 From: barckcode Date: Sat, 10 Oct 2026 22:36:50 +0200 Subject: [PATCH 5/9] docs(api): address QA and UX review on the runs reference - Only GET /runs/{id} returns the prompt: drop it from the cancel example. - GET /runs with an unknown workspace filter is an empty page, not 404. - GET /api/workspaces/{id} with a token: 403 forbidden or token_scope_denied (malformed id), never the handler's 400. - Runs-specific 401 naming both credentials; 128 KiB body cap; the per-workspace cap never exceeds the per-member total. - The guide example prints the create error instead of following /runs/null/events. - Wording fixes; remove a stray screenshot. Co-Authored-By: Claude Opus 5.5 --- runs-anchor.png | Bin 76755 -> 0 bytes src/content/docs-es/runs.mdx | 12 ++-- src/content/docs/runs.mdx | 12 ++-- src/data/openapi.json | 94 ++++++++++++++--------------- src/lib/openapiRuns.test.ts | 33 ++++++++++ src/tests/layouts/runsDocs.test.ts | 8 ++- 6 files changed, 97 insertions(+), 62 deletions(-) delete mode 100644 runs-anchor.png diff --git a/runs-anchor.png b/runs-anchor.png deleted file mode 100644 index 1bfaf2195bef9e3f5ecc7f6c70f2de7f332b005e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76755 zcmd4(1yGf3*gpVbAd1o{4JsW1(kUTGh_rM`mvo1Olr+-aCEYE80s_({A>G}*m->FQ zyR+Z!?9P8?ch9^tuP}$_JaOOG^{eXyynZEyf=qynfPjGVQd~q10Ra*IdGq<*oAAHr zC!xy-21-CT>h2st;X|BJJG3jG#^RLB`kC)p7XydSuUQua};#s|`PUd6Ib{ zW8#vLUkNzukK0_|dW9~E zJNO23ZTCLWDqC@;U9bWl6?NRBwCcC>AJvks{%D<*c1$ zU|=vEEpofQa(nOp_vKDwH~U@i-KV`x4sVMtu}@<@_vU@0I%AlMi;A}9zlYAx&G~FA z7Hc+|jhDs68{nnjGN@u-Yc;z+s+|qOq5E)6%Iipm{`X;ABID30p^I;cA!?Je^(%yt za-vWlrKbn^H8tHJaXjmheZtSk&83Um=pi7ja&WRc|2>45kXhH~WN3PtpPYPl_N!kf zO6ZN9>qeJjpP4(=?td@3D|TQX-Dvmq0``c@&y?oZd2&yva>fS;u@cyR-20ljAifIs z%XQy>-J^U(gB*pIk58@Agu#BfBm9xe(Z=-jw5+UbCkj)vzti1k7#RJu=B_AC zKSa?`QocwPhz<#PWoAYnXee|iBPS=PsOSn#ecO=H_pd1czpeAJ@5 zjfd+w2a{8l1OivTh7Fj5rW>3jH+mzU1cipmsP-aVamZe4y&NAJst|mC^Tesn{gOKw zmx+;%XQuJo14mnmTdT=cQ&Y3k9Ent!=)he?Pi;LN0UMG|9A)D>9%ROqZ(?#w*2o*3qng3O&f>nCm77$sW^!e=6##OK- zs3(C#qsjH<#!T}y!m|)bkMo^@Dswf&)0Ad@w=;H&Nlxyw!LNR3xb(54599dnHE8Z) zIMQJH2m8jUw#-I*5Q3qhq46zzRs(gxmi;6ihsW;9rMm5SOqyl( zn*^)F^eSar6N`Z%QgJ2k-M$#iFD`Pu?a+R1Z#+CP|1CHu^!mcCs<9CXg@CMsBlE_s zJ0rz22$4-5*V7*#vbG&z8%ICI)Y6*ClDu|086Vi~C z6Hj+$Gg1URG~~x3)`#*K?6;>IzjsjIAHk;h7DCKOLh`-s%E&W;*GVEx$TvMBLnev4 z)?!L^R>o7@dUZgwogWuB8ZJ()*1EmQ{MXsxx>mjYrrY^s$(E3?u;ND~#jhnLB@3Di z>NVoUnr@j4Uw)mht_{;I$}>&Z*{RV_j~08Q5sl&Dp9qOlt>Svh_T%uhdB+VpP8XjX z(JIpBxbk+iCfb;!?oc8&BkxRgdDH8-Hy7CPxqJK$PC;i&jyho1`yf^tsgD$T~b z;5B;ukn%CAp0RPQZu_2UrAdH{!}j#A92raI7jrzJ0;Sp6oLAQBaQyE&>7TjUiYBL25U~JglJxC>or#MR8sgJ$6HCL z#N6sSeS{tSyjH}yp*@nnz4%cjyVWGOF} zc|VUKSu!OQf6h=_lqucDh$z6@a#-kf9WIZ@9ZNDD$i#?#*B*!ikGsFh+;rntuKRse zIg6<}Uu<$j?TXf+&vf@CSq-^0YpoYM!msPY)}Vt^Nyn!?i%1vUG#x1%&XY%~$m~wy zseTzrIq>0eFDComQcnU8R}UN{t44Wgdpk5v1fl3nDP~#ePEdo#b<>OB_`59v=^+spJ z={I?DpX%(^&Bsb!iBk#e5NQunzDo3@kD9#2_Letlt*+y1{`WizhJ@JI*tobfx4XY| zR|in<<1xBkoRl^-U6tuehEKNXv_<;J>MOeH<;i8CU$iC9Aq?DWJmJH{Mz#DVGf-hH z&s?SdBf7q3gIX^0ZfpMBPV9}x!kII$bknr2mU;*;ULCIVU(V#=p*(nSww&@ryxAzL z{@v_cBvrERS)=peM1#|QmBm!xcDGDXMsF;$-50M~TwGlJt|*-IR22+??2wSbUCgyH z@djNFmvh~TiHVhfrABxLzjLHFW-?z61{03*aB=C9-s}kj*x4@=#pA%ZQJVJ5hj0EC z(oucwCeFZ}MSnt$A_4acao#d1#0>)x5g$Dy0-m7ji}IDp+HxW1zM;OV4yBd!0roBD&W=U_^5a3}9P;Pv+x!tS&P1Kk44&ro6iC9_wb~W!kUbWWjJh`8V~ zY5xAXD-3NW2tih7E@ z#d*6pSRB(a(d0%*$zd{-J6tgOn}+u5`e-qoV%Kx@k+qeTg`X*;`y+7Tj2d+{wVPiN z;KEw#v`Tds$4d3unLpw)V3;epVk4==K1Qr_k)l3mQ}Ap;c=z__xIpxPh{l0c%)>i0tdOhb$s{bFQXvhjh(#{QYLpM>OFBzCI&;-#lu zhGt^ zVqe+X@@StG;4 z!^eV?6s;U2n@~J7evHLp85(ccLiyu(ydeE%Z1n4E48(9nZo9RQjXhD@ZfU*7dXY%P zMl-Tj?-QcFiaG952=cy@lMTRp5{_0nGgf0I-!A;Uvf>#ZZ%CR^q3zgbA67~uV?-KAcZ+)lATX@bS>oMUvdrTP*P+luQSB7G#al*5BO9iw@u5yd_Hxb*n zQd0#lT6E_<&V~wHj+2chpO1v0R=>ns^&tH{)Ov?p6IoI`DqVI@y0f#9hWw89g`n-$ zqzbRA05|v9#(23#y?sw4723_G$W>zU$nY?1P9~)n?@G*U z5?S=~$#@pE^JCp-`_jJRHkpL zIAqjnqSrv3@3K_dRp|%@R69$mmCAd5(K9NHRmr-Fnzx;uRuqu@eVYm89G%Ry*tuOy zfoT%&#>s~gVh<1p{%HZ*TfcsyN?Zop_L$;6B(o=NtQVB{G!gGCuja^I)Y@--wUPZw#_FXB|a5fc;hRT*x7W!LO}x!i4)uU6d@#`5BYipg-ke9o&)-}`t( z{I25StQ${~xoqm7`*v6cUSh0OnT>C3Zu050inO_`3hJ`49S-KmJi^Q5MF1|2t;L}b zJ5qExZa7?JJg78T1r<9L-*dSqA(Zc}uy6+81hU;gz7$?3Ui%IF3*^I%anr*!#q;BB z$x0msdG8DU`8-CHQk(P_Sl~0J3scQf12<_GCDOi z)e=QdA>-Sq<#DN=Slf@s<`WgAI$ytCo};;*vKXz8^Z@l1;(mFJkT|htd3lBsvB8uq znHW(ZnB&vfNRLpg)%<({9Z{t|YGXSrB&3i{ws4D9qo`KoUg`3vUMvpU`xn~LtqcS= z=@h6#n@xIq6EctIj!`{M`_?E?Lo*pVdQY?-H^oDx2AGrnb=a=jwY$vdawBeLZ}CSK zv;VIyAeJRgZWkx_8P($$VNLlgrNi|F<1?GAk1+g9zAVF%_4lWKvo2*n&v+j2dUjJ}JvgD?8K;79iM4}zDzo5umF-h(0F;UfSMm0q@ z-W)GCB;|LlmnKcavkL)?Zfa~yzURi;gS@9FjS=aC9TXBmz-~q@lv-QsI~V-wspyaq zx_-0kOTkZUzItE%XjLno(&7e$s9d=N`4hrGv(wH)n{!SRLImaI55wwWSH(g{%~EFm zb@EylgZjzR&lD<>uo_^J*uVaChzt4*u~4Z!BJFNW{`u7B-k%6CK)Lb6ErrV0lep^d z^r|!1xTI4>O>L&eN|=FI`Q=)r#ncjrMuBcm1)z3^1_lb<#B9=R;Z-rpK3zJz?n|t& z&e{_VLC-$gcHnUxYcw8QBIdSLEv1+$@`}1zM)25|(h$vOQbUxp;tbx{)ECn#)NE8O zH5@4{`)W7c98<}B;c|25TBAs%5MieF(Jw*Sy`ISLOQ1aHde1Z~w?t^%tkZ1|^eP;3 zdSSH*vI@oJ2VY+u#|FHYnqil(-14=WuP#paf%19Z1sRFO>Yl&+Os!4M9G>y|$mcy) zsl6m_J6d0)@oTa;7X1juwoqI$-_IA#J;W%&avoS%Si7wnMvq!#5TX$X>QG5|J=2>6 zMMMs;bkGPr0{X1Z4)TKVm?Zp!AH|V9_V=i*sHo7>lUGjYiSFTK*(3CO_G^56vGWm6 z>N9$JTR4d)5>eO#Qgrf5>Xhv}9{lCj%)7F8zYa+HH|2(>BP4t~xF61pQ9Q%nxK&>N zBK)c~beDZ^SmXLDsa`A5UX|3WT7#BGm$|CC@MZHHN>{5YfmY~tkGWif(Ux;-s83SG zuWC#3>deQN$a{0hHy@-k1ty9H;a+Z)kCpVVt++N;AhR3np4^lbYQ+=bS02>>5Zn{T z8Z)FdS<;@wVRwkFp93YE@l8qJ@GuU!R-SeOuB>dhOo}rO{mG1M-`EZJ^CMzwrsxN% z{%uL-qSOS+g|T&O%J1Hl0PG76t~_~0NvU)(a)0J<^A+aY8AtOTg=?EN*OtJ#4x3#B z1^tB@E~ARv(WMCC$CLbv5fLpd%=%*W+I-Hvn~2EbkJ>ug?Ut6R9F8qUuP)D>oSp3f z9$uW>yAF}Y%{ksYZ~jUS%)UUeU~6+zzUG~d!?Cwx%nlvdw|UDd$Mj+{%Q)e7LLQV8 z5u>PxSKU}94<(m?i5kUwzd@*XghfM z20hn|;z8e3p2_iWMtw^;&sQwSx}apnZ_;g-sH!>zNi)}3JjxlQ2}5n|Sei&m12PR0 zdG>KhzlQxOC*ubs^^Bq43&IR+H;W;Mt**N z@qlmNzQwT`LMd%gd6(^s%3xl zKEQVQwEQ#WtL_)Q(W0b)VtPBMwQ^Ze`#PX0j})rJ@VOkd&3{+M+{JWB%k2(wd)>yU zTtuYw{Ah#A%-q~^s_sct}=&<)tkCq!Ju@zCxJRW~{5lz`;rx_|rkW1T}# zN{}kup1WVGetg{Ne181$npC=Uh$f!X%1bV#0_*2PTuZh@A<>{NROO?+J-Lm5M9Lyo z*Unwq_>8^?a*Yy+Fs*A9Ev=t3Gg=KeeNb@D>Q-YIHB?WI4}PoHTF-lA?T!{P@Puj5 zV+3g_OBsQzj7G}uytfcA7d|#O#EjFvZW(~yE6Y3$Y~kwN-10u4&1%c*A>33N`E0#X zUf_2PH>1g}!yBy^yB0d-!wo=8q1G5AVl#J5VRp@yBJe}256a;Z6%&KrVALIJHRnsj zZazT`y7#5XnBkxHSYw5?Gm7hBk|PyI-&o;(SvTEyE*1N+uwl<6w4A z;PDOIx67@E>p=TKP6aWRRm934(8Yz@Voso?P z>5Nyp4#<{8^{M5>j#^sn=LnJ(e7{C>hfXMJ*Lm;mpr0QUG-`0KNY$a0EF6{J!r8py z`GQY+(KWb<6`yiW@tD`cW_csydMjd&7&H^mIRl`tq>%=kfLoA(yp6wp1MUkBPD2w&V8hZgs22(a~J{3!QpzqeNGA#l_pl zYWk8ZwZFI;Z=ZF2`r+ROdX^X>iPnoqz<=Oc0t^6JO_j91RNR+&rJ;yh>{ODd~%h)4m@!TByRO-&Nsue)Q;< zuI#+x^CzaJbWy1W-Lc^zdE52D5BFzuPH}OsEB!(IMqIRD&Y+}{#LY1L+2lrSO_;!Y zAJ4rJB?3MK`md8C5omI^F_Em>jyBbN4>GcIa+qA1+Fk0_);${cINi4tQ!43D1I*~B zYdLCdJVgV&pKQI_V<_)eVEOT~R0beA$hB}T_7_tT&Q(BSenc~z5I^T(%&+9IoNg(O zuq`KjZr^Z_|0;=FM{;wrmNgmRZk4d#(cF`G*`DvJU%FaiSjowsUvwBv)H?32GxW=n zi5fj)WnJ&j@GDr~I@4* zkOz>t2+-2qnfv57QEseWoyT4I3FeHd)aR4WQLJRY)80kU%gPN^dUz3Pukw6kjm$Jc zp+VLWKt=nbmzmyzZR-j&nm6F;#B>L+Y{;6T2Yvz03_PH_aJedRJe*&!Fg>y)KYJh6 z5J{zvB5EXp;f`h$B9Z8hc-nauV7c#`LXxdTL2*_YQGdT5O}g1@gXHtBRI`ABkkExz z3cTw>RQO0bK`L@P*>)vGPkvpm5}KVi)dL8#bPw6p^oMfvY;IR0ZZ#I~%6{>=1sTgX2w%aAu2T!Z(6CG?Mx!w5H-1R_Wm9n>1_S5)S zkLt@4@jct?Z;OeFo}P8}GQNhgw6mR$lS5%ImBLRv!+p9__L_mD8dN&F3NP`<@`?)V z(1&3mZAuq+Pxr59G>eKL#gUiJ`@>G%Zt z=}0OSzDZ_KtKzT}LL=hLO-qvnvRLo1O)&!d{2X*`|D>rpJK?l4kLw${YVp;_@)uf_ zCW=%!D&n7sdF&4&oF=Nw2}>7(E&?Ko-}K^K)!!T$MQ~l}j)Ut9Arq=)W0fA-Gthm3cy#fxz`|q<Drsp-9Kd{L}fQlddY(`x_m)Ha9J zn3B9a784J>rg6*2h?1BX(ka%i-3))VYUSI}?7+~_?eX&EJb5h{`{UhRJG-gDZ0Q1W z;SYDq&Y4Kp!IA(kLPtku?({%Hxdn5*RPOCmxgisa4agH>p(Mer+W^SXNO=9}89w>z z4CVG`XJ;$_a_1a`Umk&dMIg4GN zhKGjgzLBJwx(%AnWEkIxU zKZjo6S7@CRH^!6D)tEmf^9r6e0_9mhzC*yDpWN@my9BTYnf@H5f7|Ou?4RHI`?K7> zqn%Fbw+K5CtnQh+x1P#6ztosi)Z_jmZBU%m;(G=RApXyvh5X{g{(BJC+`pNHgIlR~oj-m^{G~R+e)#+EfbcqNygT;<6SHaKSGAbk1282m z`qLG!F3Paz!&APtwxW{Sd+hvf+PO70J{}jpq>a$aIW#<6Sy`Epk&(sk?CdO15$^l< z!MPi*VtH!)kGELuH)2VLkcIzVUh`}2+uA57!AhSeYHG~nrJ$R%HY9^k&tt#wQSf~6 zM@*ZgNGFHO5s_F35%RrzU*w~fUL|r`=<0%UM|S5oi+%AFj8b9^Vxlc0wr^(hCl~<}xO!!TIY+5%{0&Tb#+5sd9BuC0>%8EQ8r>dwtbdQ#0mzx<_^T!bj%q5{SQwg>POe zE91FgVq$_ry0*ORw*~2fXXX^mbPBQ$SHAV>2g%ov_IK#}(Wb#+_n)fQ~i!xhG?R)O~R_Fx06Ra>-b=dx3A z=pEp-un(5#WI!SXJWn7z1uErBEBNeYKJoGBBqY&}j%sSZcIT-qW|}sK@*@A+4Z#EC z2+Q!`pYDiS7dJWXwzjrX)G9k{**+Jy7|QJ&%45Cr0RQ7eA=LJh2yfw4P*=flrQ+qi zxHz?eJyca?&&?gjq*3R1v=Il(*YKxn3ZF|f5H>4EH#fJ;80c^!Vq%|Y7vJstc7K45 zkMB=FRu;@D1>_UI1Qf4bfLl1MFFfi9IV|YaYkqJ3sy0$|f<%sjf`YnwVsY`YKgL(6 zy}vicZMtJ;{iUryNc->Pvj!H4wAt(q-+EVE;OkW`XkH$y4kq&z<$j+S#}VwTmd*I4 zn6IEws!M|@8A8N`{(S-z$(g=H@3V>F%-u{oR`J&`w}CudKA2ZVDDGRVE<3xzn66 z+qo*oPBAtvTPfil{& zkb{kJ`G1AFSpl{uDXC?f?2si^^IJ=;jUO?$A1^$fF*1pFo2&^iFffoy0ZgRX^gxIK zb&7g<&gy96;VuRtp&!K`8(kJjvqXD(_UjhV?I75Pyc2zW9O5kgBXjaYfqoAjQog<; z57ri7AYe?WpGjcFKo9@c-k#p0e|3Qf>X^i>pYd$0^z>#^b(JUt&nn94{^h6oXQ+v9 z?!+)@9c@^dkC#=-S+~d^G}v#NSX%-t!r6OSZomLn74|aB1sH*$zW(OoVt_I@RM9l@ zBYl17Qg5KTQgJMKd3m2`nR;7=35 zxU3ym=5GPt_-M-xHkPFJ?;xn*!fuvH z)=W15T_9Ys?Y=}ti>a9zi}OK^OiKFU!2zRY!^!V2ZntiE0|S?dV<8TBLiW41{`~z1 z6hc2gzdu;nsv8*J>U87s$A=;J?rF7t{aS4?MgRH^G@n&2n$h&*igd}y&W=t?d;a$A0^|jvG=-wsSy+Ug{t-1txs>F8L_9H- z>dwif9>3Q@jWHiQP!MQqYy0~4)yZy#pa&Zd&#%YTGG@HsKnEUrRAg$KV#vuDV>+I# z^v`2A{OJ9^VhG_g1y51&H0pu5w6VdN8ISpIhhT@dUxN+yvWc0IksTg?XAFAUBMJ)a zb11)0C@9pOX*GV+mbO{NvtXO?N{BUkXq~~Y0YEhiJKy=ea2pZPV!Z5L`oxU?f*+h9P*t>h>uqVEkR03%J&(y zZ2H^b{NdaG-Nu#y0#D`7s#7^K0ywwcc1Gy$&UyH3`yv2IC??`6yPu~3PLuoPSKjS^{w++Kf&b=9RoeRM;EqAO6*HZOS;EJ8a#;UZ?S3Q4q!h*ED+=GyH|>% zEpFh`q(kf!k1Ke^>NyW{buU=b*pRHP%uX_#U7678^p$OnxOLKEpmny6m z)|Y3MtcI_kJ$O>=Ltuh}qLq2d$LF^3@4ZF32A5;|_Q1uaCVuK`-31haq3b%bp<%=B z*sGn{TWFzr{QT2S`%75VdM(kcZ##ZRJdtF+^*4%rIW@qFkTI~nyi8>ewkvpou2-FuPq*f9A@X^hejl~^uaBme?Y|J}QoKlkqC z4LRSF6US0vF)_8U#aYhPteToP3=G5!pQZM`%v}*1x<<^0)7pXjes@~#+K7w%VXTpq zRN=uhcJ>!U-mZZ&$xdg#bTa0Q|7FcRK5NB&EDVy-A+{n{C@95*-j05%^VA0B?TWdJdm?FY0|7AM7a)xcA-v(fm>Q z!NEa1_ooV;z&CJ-F1TxzAw&j1KkCe|C;kwM-QAxOmyk7@fh6TZ;gA4CX(gx-GQPn+ zd(zc0FK3ZF}pkiaVtGecD{s)Hv|8GDE5CyoW%VF0%ce1f4#?@Zq*6L=wI1S zpTYhA+mVI;mB;^YV--8~x3wWPM@dNuh)2T06Z44#)aPU#2PLv}((bXb8p)X6mrdAM zSm)qI0jG97*?ImKT%BcPvLO^h;Jmk}1yrP|$s=(-BvhiYGW~TZIzJP+9{J7#-%|ib zT=DjwB?~H7!j0G!s;8{130yYWfj6KeTEjGj{pLio%kis!2Nlk}Z;VSq0-BfIY92IP z2vNY}yiIW?IcxeSq6u}d{B0-2U$}BfJ}`^E0xV#tNP`MG94s+M)tYx0P)iP??ohue z(S|K`ABUER;A{!b@J}B)y?Y4~^gy2cGWbb+0l^;Z=^Z;f>#2$fk$E?ua%XVWYa}pu`3m+ z)BHQPd%!-MMG)K$^HnR?ii+xYDxg7%H|w_f5^-5~$FrRutja@8{u%!0+#e$YBoR&? z%Z?PeS}DHb^{ZWADnlwyk*c|D=MDBsReF9TEqWF4QwSKPom$UN#Zn@ zKuFYVD3_UAT|)y5iQo`m;{dP;h`)Y785|iQd1mw)n1=PjQw!+0m*JGOw7N4D1AuW4*_VkJ@DpyMg1H2X%!Nm@_)oIMWP0R z+1Xm}UTF zSu3!4OajjbDJj2!RVqF;cBxTmA`0RA+v!7eN`)tdDzD(IVI(5l8L}200Kq7w@R``Z zzqSUB#%M3DCHPFwZfyvI7&wgVMeD?`UqP2ZPpR(w2BX^FZE^~YQMICDU_g#&;M=#l z=iXmyb8DvPld`Sas`eq^ z!?U(l1uRtHyJNSaHOs21oZvnvpdTF`vaz%GkBd^Mom)HJQ@_vuq*wm~*j z*T7H^fYL+fDvq|Mf?hYe)k{K_$;$a+9LoUe%GN5pMQEmSt04<)lnZf*x05jO><7L>n9MA zu@#7PVE@DX)7nxMUTC&-0t)`qcV9mo@?u6bdB?t|l8UV{>c_OR3kF?^o2y6nOfj>d zz-1+@UYGR#{rhTaYNhAU`atT#Xu@mifXv_9YP=AxcfURHJ%g}m${_eqeaYS za`3Bq8@iZRrT61R5CEM)t zTU}p=Q4Pfg!2?K!1!B{z{eDew8SeAr6CD+bR*7~R^eeFMef|5m&#kvc`s%ZrPfx3o^q z&e9_?#%2ZwZN7M&3PVCl_hnH{CTnC}U3v5EL4DRYN~>r&DmNRKdmrKD zi-nJ$I|hJFPD5kl-AJ*Phikp>vy+|K#%bbLw3A8V7G2}Fn&Tw-hY()#D!~Oy!!(2}E-sw_4D1D5 zqWy@Aj|a>g=(#)UyYnM+z#<v(C_4`Es}t8jN~g0wzl5n+s$CsX>|lG9Vo*;%u5q(;V0-J z|K`n`IaWIu)yteG`2@N67ZS}9=+1yNVOAs7J;1#Tt}|e!9#$`CrO$Q|R8=PeWnlSH zWTLn~fl9)w-GZQ|Hr|QthvvbzULzW`^cUsGnp`x%9Hz$&OUrFgvyk@)J?U_@`*ok# zV>4;0uc@R8zK7azZ$?yDc)lkgtlILCm)A|mLeL*I*et(-)zB97Eg>Pn)WqaZT3SSe z<{KO>?-9;>Fvwr$E6|kYq|A&K=)lCwm;U}zxF{^jk09HGH8nNuXz+Vnfp9PnqSeEP z4}ravfi4YFKU@h3pYtxWLbzq9jt%>yhqc$CDvD~$hE z{)hPFX$O*2agqG-@o^_7r+W_r0J>~V0>0?Fgc1zgXqU0cIH#Z>0T2}Qf4CL^zrMb{ z{~m7McLb?}jJN+`wgaAduK&;HUoXPW5g`Be?|>pA%~(qRi^cVSCYFE(A_XoabU(@q z3Xo4BTlku}r8tT29~?fj0Gkj1AvAP%2|jakbEvdwa~iNr%1TPG-5=Q41VJM%z)!VZ zWvMhB#aIc=%FZsz{&I`-ufD;OKErr#W1>?25CQVA10y4t{&K~F`^HWtM|eZSTIv&L zefm+a*ng1) zoAZho0=q3Op76QTwKgtObtrMH>)o-;gHuye!*($zxC{)R7k?=K5eXJL`oWii8XL!E z`T}OmUguxJ^ku#x9pvajzkK}o5%#HiMeZf&w_7vKelGzK-?)SsAHcpa9Zaq@V+hbM(^AX*Kk%DI?_+~786+X*OK|_VY11{&=9Ikc~w>K zSZN0M05GnM{~!b4?#fCOo2io6_lM;MTuU9{dfy(10U`BSHXbx+Yi|#J$Z7zI<&KIu zPK;LOhMXM3>#NJ8Bo4qB_0V(z_v0Lop3=T|*E=(?zn3z9vv|YN?RX~%rExxEI^gn= zw;O+htCyIol)_MgVVX?uw+5GBB<>xB|AgB1Z5NizfB-A-PMVuN6cmO+h<^#VI)?f{ z)kSGI`2FS56Jc=xMqMnxwR`^k6I~23kpw&u(ofS+jpCmf>zbJ47Z+l$2oEiiklR2udcqx%CT&*RNCkzV|~iP@~TF z<>5yAw{OS?^>WIi=$M#8Lu@RD<*3JvB>T+jk^*#RcqW_p$25EUIme;uRR6L0<1@Eg zJCN^!2uPjj=oWMp$e4fYF}D;F@+vB_u>R`eB7noAtIL0Sc1CfT&cn@p=Nc-dirOz9 zKfj<=Jamh2M{~16J6jeIzyntCT;1H-Q0eGQrF*2H7UQHWbw(=7$Xq9sQ}tQOP^tCC zzdG)FPw_6cx={3%?~Kn_$4RUaEdCWFI@KXfGKw>`)TcBT#_lr+A3sifR*QiIfpVUl z{@KAp0NMaPd;(|Z=LICEQUtyS5u2E3of|;}>XV_Kt}a+GVb;JVa9-?Hs)>n#sVW0L z0iVPX=u^<>XA=38JUtAxc4VX(Xos#l5C-FINCKoM$dIr9M+#6{_NQX@E*rEYHF5aztu~$$@97sRDY(N!$Z~JmU!r^1wz|F zccKc1i@iKpt3OlpPgUx^U~00kv3+8EsgbXc2Uy+R1|V`hUEek>%a|Hpq*4fl-CR0F z1xopR-k26oP}~^CKJznj#uxuBts$nSyU}p|!CqhnN;1KSdj`PPxldUTK}J06=m^zn z>Of^3>F>vo>jd+Pf}#OzcKlD-*#^+qyu7_{-Mk5H1!h&DmwbExfK3Im9__Q+leON0 zYK3pCZ1swu#)QMwwPR3nx!k<`mM(P5HH49N*wXY=RxBk&?Rx8UbR@6~t9K-RIY6wc zVey}CeCBdFhy2csYjrh>=6cFz7<$J>Zx=w#d*zAl#DZQfJkW6J;CyO8*S z%A^5b2u8%j#DtMNKtVY?JQNcCnmK3+i2&<`4h#YU+N9zH+OxAe)Q{+r`hu(o2orpK zeI=HCC8VUKp~;`D%G)9vhKfgoXu)Nl@nAEU{Iu~5SlQM#H9e=&yuDssk5&=^-5jRd zC73lSGSc18-B$6yok@*yZ@2pJq3LC!`z-ME&a9cohM_A=P^xtFKdH&coZj3pB8(UC z&;&&bF#>q~Wai6AIhMCTcF&HtnchAI7|f_%Q?j!zmCRcUAv)uSf`~XXhyd3cU>$$8Ql`5oxxr&mK9 zEG%n)%RfwLSoEb{!GM3HYiCP~Hkf=cQ-zf>UGKob%PZxqy$U)xQIMo-rPAkk=>&F+ zfm@V5tF(tAE#E}U&CGmezClI@2I+T*V=U2iSifddYnsoV+ip%|!2liYot1&Ca&K?% z9SsFnJg^-4#&is#g-;sOeA6LE&d=X8cSr{I%9U>Owqk0)igG6;34(rrHEsiuA_Lg$ zX=!PYHiLvuHz)#N4)?(zBou(tlM{S=d}NO(_%4NX)Bsm%S3D%Y0m!XO?>)Ia4-Q^h zmY{1qP2>`+s~CC}3Vu;jA&>zR6Af0^3^Xf@tRQvmYwLCINr>;-nn`SWye(eMObgKC zTQ@bk6S?#bR+xp>V9P7z!i1N5`o(GU^$S#wIfv)>x<=u-*xTA-L$|-OFGRo!XSqEE zlmjMhY@t7aoUymJCsYRt-9ri~NL2J3<-Vh@f0c|r0L%UcDO zJzOsq`p(5fWkI|ZuYAr__l@=P;5Z&;d=b|WFRXD zZsE(*Ux-c&2M`zd@})RM;P|^ZOna0V^$YE0fv%rHd%mZSeziTk-Yin*am_EK4^u6< zj(UlXAcxu?d?y6CNj6hl)^!aS6K$^`AW9HC>#Z`%b$`N#6Z0&Q^P`g3rZDS$TOBk6 zmFS5(BYk%pX{P(7b5T)I>@ds@e120Zlvu>+3WGtr0^rdriD!WZ#l>UU_I>xj)oalk zCuYH@lmvVw-#a&lRAtNfZI&KPzX2@%NDOs;6{cDtHBTALT00yl`abmc_BIvc7??Pc zmg5ahSY4SgA}dkucl;{$=`0M{zG*-47|49Nwsw8&mj{@AZjRe#=~lG4(Rok&L#8hPc^wEd{V^xP7*__xCV&`n97&m2*3$G3_`WyBC{stEFql- zH8{D#iR``J*BYB;b^{!Fv0(*XGNCg@fA3jSuV56AR~$lpeVrsmd?}=yFVF1oo2!op zKdV)X|7r+)C^;K@cwh@!Z>ZCKWaKvSwrDcYe!E>k7lelX5w+0D*pZ{U@g!bcW_K5Y z-af5c#u2Vkt1#MzniT=3vdt(o~gU85@G4nQLW_XUNR+YEe2 z$kk4i8&<>0;&iRH&;SOzG&eW5xyg|Ep|m(cW0VP zh8IIczKrql8+HFVke0;ik~yv9qUlwhQaR>3f0JV|0D#exP_2DbW$Ex-(x>@)AJA{L z**NBmyx9PZ5A7cwii$Zp)*Rp}R3SaU_v8qaDGdYi<9vRUwD7`Lr=(q1mU_A$J)sVU zaVnzFd;60?c~bfV#qegy`$P^r!k%t!ZYCvN7Z^&ov;YEu79}Mv{+fuSz8oX~Q|K5q z@{q0xzz)pu+6bG!N8N|=kVS-ug`FK&ls4+T?Jk3m5tq~6?O;jFN#z$5kT-B~bN9xv;>iF8N4Jor0@cpgx`0g* z_tE3h{*3R5+;+&9r8}~n-Joh!q4SFN?HwPB^@?CBC8`k;nu>HCuIu;ULXf>dV7C0R5T^k?1F@H@O}mNc zR$xE?hM2E*mFeg!kz@Bu1r47EKW>CyS@d@6pBQW}WmX#x{GA3r_- zIFWatDTP7WEJHf}c?kGbuo7CKS5)DB43;VYCGbU1{Xj2&9fgUia7$*5LNwR*oeIcx8BsqsCio1~ ze-bCSAJZgbif(2_I9}g({S?ulx*X~~!uU+5P@^9FlCz*c^2PP{j*h#P^w{f}{5Nj= z1fz?N?{$W}b5KEnDVz{sFhnQB2cnq}h{wo<6ksl%%GG^+x##YM*>f(kXTp5aVKWL3 z*k0bggB_l^uUcA9pRFZm@#T8p{g)Zn^YdfUK61s6rRV-4*u=!a&M@%@x#z5hE=S#X zd|3e$E&k7i5_1Qzv4nB9+JgkMlE&79a?vN}_N4#+Z%*?_$s`ddv9cJ^ftB`5I93q0bxzxK1P|Cj-_@UNjlmQC+IKgiqDAA11iu|eL0>a4xn1_(DdHlD^FVDFRJ6=>p7@HRSZ|6iDT z?|7{H_J6#c5ZNMInOTX--Xnzwk)2W*B_T7)E`*{aS&@h{AzL`fC?h1vE;D=2-_w2H zpU3ytANS*W+}B0C&)55P9LIAVuVTG-vK`bgnGuK@dX##lk?{aM1(QLFY*5~EJ$G3} z@t!+=pMI5jNzL#XM?*}tR;H`b-Mx4qn#tDT)GdpIgoG(vqtkLl+sAZW-S#(Fw5ZO< z9qEu~0#Wl}0GKnfRRY@zUg$4$7ZO0E-n>yC_FE-bn;^~ulzE$=M-Wh%%RgiNvF`3} zok=Al#TEc=qHeqtFn)QBwjLej%gEzWB?sLq6S;dJf!0vDGn_&H!i-C!R-8l@v?LG2 z!^Ga6@`x3kaXC4XEzYkWPPC3IKdpN-osmcOvNS)xu~tCe_!@bXtkV$hSh_~oPBPL^ zvQb?E&((c&h6EGB)%QaqEy?N30*j+5_#QEOs-aUQ_3mcpGZd+p4btmn; z_x2eOjG4vL!rkF5TC4steqa-TQE4 zLhO)g)CZ=3(+6&c{cHv^v6d+gjnqzMZ9J!z%TK;{H(i&~D#JY|u=v@a1-)|niD!|b zkhBT&j-(E#yb`_Y`4Ys8W+<;HtuN4%^t7}<$Nk&YyopGZW&CZ{L_P zE_P*`Z@qra{?TFn->=&Cy8v2#Lq5#0&S=|06JEq{#gH^MAW&X;-qmt%Y*6&d+L}O5 z-a?oiaTRH5V z6Ee()Z4RF}cGvKL!Hmlu(+PWKMn?9hUbsp7Dtv1iD#$h+uy}lExEES1&K}Ih&`2k>&!4P z#h5kL9>U)C&d~AZ&A4q0`>+iPI^+z|J)yr{M-_iYqI&3O8Smvv_Ee?*KP&(?BsHn~ zt#1p16aNM~6@fQ`EAG+bb6V$eLj#RMhd4?sdrDRy`@4GcrtA8rK>cURiw?i?|Anzg z!H)IbE%HW&T~7(|bD}daN2(lTy&;9`D$$Tons{KJsL@z*XO_FEdKItzzMr{qA+@sU z5d)%eyo*#kDegnOuE}1BmKfKV#NFT;{#-GOP#i?-;(z5$SIn~Hvu%Cc4-`h zHfWjcH<}K>l!vaw&^4pJU50R{0NUp9aWBw2&UNB6G*M^`Aj;k!?z4G`v&WY_brAiJ zr9Cu=?a6Ac_V&j;82}kCtQRH(*6PkkSis%#?|Yi0v9`!nX6A9wv&Ds8_5y+inVqm# zO!uJ5fNRuQrw*Red82im_Oa+Ij9&WLW~N8Vw#Sw8@gyYEP{Q{>^xNvhe1bD+o(M#} zUT0C^NtShOnC8Z>kJqERO>&YSrGI~KYT~msjB8Dat<9QHdl|^1@K$}il;N%En?z;q zFX8X1%hoWaL{N%q)kUU0Y}1)p1h@`FG6m-d1hDE6#nST=TSLK_I&V%{tKWojY=ADK zY&WSb0BqXU9Sx-I4OYa7RM{{8fQ;fi)3*YX;t5HhMJmQ=Ovn8mZ;i4*ttymMI|x;- zqsc%I5zLu5;9pF2pG|hXe8;0AEA*_;Rs|`F?m98Hf;L=7W;X~uB?YI>oc_+n2no@B z(P6}x07^CquT^ISvtz$MwcJAyHG0%KQ-!We786(F^RO@bInv` z=n`-D0~|T`LR>Z2?2P**VtTcbNKPklGaw)!SntF>+b($fWROLyzZ$H#tqa z)T3_xGQr9QsbbB_pKku3@Z33b&^Xl9ruQ`!&uKI^4bTt_a1m@c0-WDr}U~? zXcLnlSfBv?2O`_32uO6Cn8&6`+`UdPU+;<4O;+V&y&S^+8}@v((TSV)nOeFF=W@*N z0^=o0s;a7Tip0jnF&{n*8dP4oUuVWZHRG+@8gNd05WzS4gK27K8kXb)n*0X-Rfg{=OboG39yp`d7)({PVeZ*=>JjShEeo6cTV3n&O+@n zsiT2Gp~9%GNv3Q{9(rz$?^cu^AZ4I;4+z+&>8e;6$uP9kf|MWKK_9$abkQl7FJFcb zFZarmtlL_EZG6?g7-f-AraN24yL+q!aZxp`K{%vL%_?e@LK~+?u{PiNusT^uv>-k0 zXxJNcK_jSVFZ4yc1s_AksUe(t#S{rK4575gcNrPPZa?88M>GpRP3Xv2^Bq2se+wCY z08qI2xkyer_8e&-)lglg&vf|6$uU-3Sm<>}pZC1gT?=M;>-}Zh`F6@TgXDWtVg0&s z<3>}0oZ(ow?6P0q#hFnYW@O71!z%`?$SO{uIxja@<+Lk^3)`4#%31vCGrNVn(?lwf ziUj0{WlE9gUHLG0`K^NGy@jzn04EAPs#}0Ffh-@t^nqJ&qP%Jij$}Zw`HTOgtUQd0 zoS!ETW+G=VF4j|~q96DX=vv}2g4>MMI<=KV&d%2wGiY4_pnRT&(j~Gi7~E1fCG^iW zB+B!(igxLj3+vH!H(#Clo4*900Wc+@He4B2hOD8U501Uw)r=EP8Zu9=Jri8HRu@Tx z#2JEeJG)FGmo;7Qsk>w16a4o)_h(sTy@B28giy8alX?xJ8}Tp~i<0KvN*`!-+! zOn`)v5~xTIfUV{>r|H*g_>4q?0oYi3n7~oYe+gGvrMHA)m+$U1!rJEQf>9e(gm# zh*MX<-zz{Q%jEh{p9wODtApUbQJfd%J(Cg>@f_GvE#bO+a1ZcCVR-lwQUl{o-VEz{ zv3x%vmbSGPL`pJQg!Mea$C;yd+9UIrh9P^YTaz z4@tHlrOg4*6ZviMlh;~U$u)%;zOpr|{bLn=>wjBq>8op`eKtz~#{jZorHOoVR(ZbR z?f)Ej7Q#cqw&aTE4jg?8=#|HC?c9Ot!f-aOD5=g1CML`a?}LI!W(Eq<`4T;szRs~; zEZy0)Fr#MyvC0Tr1g`<=WMlftESfadQuIIqQ7&JmP26oH#avm)5L(byRI+K zq|1xz$} z?|K~&ep1N$M%#HRrl)=aE?)`HKpIpl%4ljCHQ2IO=y8iS){X!8Y}K~yX3NQumsCx6 zZg}tPt9bZLnD%kgcc(?8EeY}Jh6;L-k#~KkSY$s8jDF>N5-Dok^WmE0#iD^1q|59m zyf02R@Xeenx%h}QPx;|d{pG!yDYm*E;`_Yrh-B8^-?1&tL`1PVVGDiiOwNZ79W8b1 z6VIDE34=r_>wc5Pv5U<}mm&N~zGwy1DCCdpwc&Fp#hpcWa~~)jrX6cZgW8XbzOft% z4PZ*T4M1${Q#CI(EBiN2^yy@H-uqd|fBt-KbB<9;&;~fs=J&e!cK2@EK|v?$I=Rh0 z;1Gu0t1vVv@iI1U=W36eu`&Si8kjlq2qRRZZc#Xq1Xj2GccuLl8hE4I3 zV>_wdQgiU6JRU2xujY1ibQ~S^IL$$f^ejFV08`e#-{Z#uc23lSqaVkc)f$m5={>hu zsu>w4**0>PnXk@|zQqvNkq56adB^jnaRRH6wiKk#Zdq#B8ML%%|=4tN4)=7s4~ z0kt7?4S*`8u6)tyuVsYdCNa@hUgPttRaB=vtI2H{7oo=4>)5L*?$G@(uiJcA79|SA zY47gjdvjNLD-b|%tuoW;Pp)6u5kE{QS^PnUx+z3gfcfkM|o87*ZXBOTY-1eiv7> z+Ke%I`n-^l$QEF1c+eP6J8|Ebwav2qmCr)DjLvCh*4o*a0gW4KV00btZFC)%^4S&>D0QCJI5zV z4xCt@wdsRk4<2mPp`(YWHdx&#L|VdUU0>qEQy%5@AKoFLxVOE#dmY?KNLpSpt>7!4 zV(Z%TWMp%%U&mAEjvHz( z3=UF1xzFOif{*$`t^fF&0jk4~rYVEA0`+SrzGnN+h~e459muOajT;6<1`d3$k!C80 zSdkVJA`Elk$6NhLd~EHF&vW5*+WX&cm^A;aV+Vw}bUbON(L5s_^(=J2kdHmOFpbar z#I+8p3)xrdE_8Jn|3|+AlfC=dn>*kjykA_rhR|ck`#>Nk$jrq{*lXt7)qvNc+27p$ z6`=bb{Q%R;E=aIdo4D?1ZnrY~l$QxeNUrNt*_acv#3?R`rdZKjgyH<(cP`S@+X3(F zI|xa>#{Ho4z^ehx9^Cnwo?VGbX&?r}m`)B<`p;R{JW}yG1q2$h3DZKS!fQ>F0QBF( zi}UqG9&EDPVvK%VsM}(U|2-XE(^S44CPqemPzL#=ZaO%4K!ncj!ZpJA6UwJEQc^c< zZH+En8Xq4A4%O6t_Xh?>a4Ac0&Php0k>8tpGhyc9#&qZq`=JdBXgTGaN7TWZ(0V}l z$|eI_^nqyaW7@ASL-)dw4AML@>U(DO1~h^Nc&qU~rF$2DoUea&Tng_c-c^o`b%l4x zrlElTjbYPhG%p>JdBS1%1`R!5L}rcb&$1yN6no%Pi;VH6$TvHV(9s=d_Ad$uu`DT( zCWav@b2nh@Qm-}oC!f@xBmr${8hJF`Q?rPCq`qRLmR{I<`kI{!AwmWxDNH_bYjReQ+Nn$%7 zT2ozp1^#VtlJws3>nry0FJD$qViZq0$a{23apYe|)*8W@6+^XKOax(cWW?@Q{x)Qn zv0v|U8{ZE=?zrTiKYy+P$B!rQe9Q*I+~sutrB^w;DCu?nt;xpkIX(!P@)r=%+p>#6 zoGh-5r2azaK0;*RxZDo!z@6P)TpyDS46YqK5x@*^C{hxOQS$QwL5G)NHNlUFV(=4; z&2w)IX7|oM^ZcDq^KAnudDogYi*CM4OY@lQ{)ujuWHx>ReYcGJ{1;?zqiw2w1#Wp; zAwB3bZ(ic|r1i_$r~G#}*9*7j%3|oKA&bT|@+bFN8CcKU5B0|>a?ve|*=Pc()13PA z2O-&{A72CuTe|xE>pB^03*4g72tQDYC@gmnjIZ~U76X&OHNtN^U(TF^$ia2^S%uB_ z^>3k9*W8GT+ZhabDWvnwf_UK`Z2_m_rjr(pTK)o<2GVuWn)_+^O&H7}E34VB=Cdvh zU4T3?Pg`w)xH`{|P&q?>xHp$usB@sYbt zX8zH(1*0HtuHy&Ug@nkTxd~9;o$S7jkA39Soxk(wDpQ+IIt`I%H}Pox$_PnKO9y#u zWZ)+J*QN5|Oz@Ae`O&h%P8y$SaGQOSJ9`C7$jp^+=Z)-d;7&QX*$fUp8#Y2{a6g-? z2?zif3kZ1l5VtdLVL@NEHH77EeYghh0<oqxQbAEQ>((h^)O zMA0@Wbw7yil7?mwZy`D&n&+fM0ir0b;)(9UK`^c09??PPgoZX?DvRJJNXgpN4}oh% zdc>uOsY6_3mP0V7sr|dzmabb=^t{+L=AT(|Tfpnk0%$tLZ?xTlNH|Cfw}Il`I--r# zK6uDK1G_;D^PKMUMWQ$|rcXI^cS0Bdxx}3g!L-&?7Ljl1>9$(NL~?ZuU@~sguhH#! zxV!g1NcI}S0p41l3qKV_!gKrf3<}Hl_VzgO>zquw)fFdPCtu=`f&;Do{+))Z6uqG4 z*;Bl{BxfsndRRHeIsbC7 zkF_M!n}u2TkeukDwkKsdhmDMlVP80)9c5BJV6i-b%(%=5Ik8iD=CG(^ryIXP zX5Uzm2lTl>uE9v|HgliuQ$Wv4kO5@GT9*&yFpBViq(b-d(V$O8X}CU#0fBVBSysqi zgd!&6NAkyX(YxYieUG8l!EED!xCzMWOUTpgeU=dq6ghs?b{)-CLhn( zcaM}f{n7NiIeoW+K1u)1Guikt;;W(kwUQekw-~FCPlS{r$eGJNNGo1m~=uuETn<46g-3TYmibfrzAxpls05LA(GvVbmac z%k?;oG;`2;0)3T*LR|W!q$Hb!-6?*a0FQi-!S|r8G8Q0{mSX-BZ=!d8A^|n2s z-6kV=CnNd6$3Qm)zHxtWEq3{^R>0B5p^Q-o4`Huc$n>3!)sw_}9;`meGA*WH8|oCL zB-a6ujm`%cz@9(0C1Wrue))0>T08z-B4Uj=%+0(J5$rc5fj>x-w&CFe73KNBOV8q@ z5=>2;NnGF9crt(JJ|5-6H>8B0 zN~a85y>Im1{iAw0?nkBliYGg`DviWe3|$^VOhp6)1OcN|X7*5ruphm|j!Y5JSoBO3 zapD=#n3?H^3Pf%>zs$S(mC2baJTWA+YZ+-&I_*A)r+A^#%ocaXKp9x^)2pu`YiBGs znk6m+bDb@j?qZE z9n~~>(#B@KU;GRvHz#7qQ`;(P535U-`NZNA>i@ahPjPUY?a`-QZ+T3?ElK+@O3K-M z!Ag7ffs~YE7y1Ca9TZ1S-9ODBw$JnEUd|-BUbe26F&&tGm#F2D%~ecmTYoV#@!*k8 z?b~0NPG$tNU@%nMdqC0ZAhfIL7f=(yA z9`b<2%0D|lG4WsZTVIvzT!<;l=p@vH@8G}Ms(y%{widd@fEz}Z&pB<6DY0>ji~F*i z845e(mwxN=c_!A==g6P=3H~;%*urU3V&ye_U&5X(#u0FskAw1-dw!m^zd^AlD8772;(zY?F;T|>%T$EJ zFltGw=tW3ogC6_u?dxD^4GM|UyO0hk0Pl{ZI;Du(48W|;;~fWY^VTKvL`&?ZgN`hw zDAoVDk>j?8(fTIsP>j8{!-iE8}4J0Mh#o30Mgp1kNk#?=aq(ahL zbhiPTrAYF{q~AYsR=j0Nza3WVLl7#GdfOCa#AsC`0_TN^v$GcdO5ddh3{ zk+Y-X^_dJKcT9~|Y>nhD4mvqcRr3%gd?A(fm}H`(&?2|o_Fj>uXo{Ui51;*w-iohz zA330L&eAebW05Dxd|>%DW%Lwkv$xzyMS3Vy{EIOt!biBpv;mILtrlhltKhaN& z^3~^uWV?)a(6vxWb)>t%HHUu_VEhv~$It3Hv>;ujQxUnnTv5S3TB@M%L63WOh@kM1 zSLgNpKU%(v+~}%5ix;GH9<+^xr*~5}m?Z_kYaE0n^0!um`fp z#LU2?!`=fTU2tJ&W+#q?%7YTi)|jQ~RQaR=>sw3rD7<4bu=eL&%Qv zj+Eb}Wn?reE6KFS)RAR03j|m7ILiThRJe4XfWSb3usv}dW9_`!@9uecSigmqDM^p# z=jYD1#^D;?J-A|Ven+PJmC$CZ(P-nEK`nmssm`l_nW9QIu z?!mZFnWF;meUAjLYPjaGC&`h7SW{3?bmju#p2Mwp$W2E@+2tUK8wYFQ5@@H1*8*in zAW5*4E*GiZM9Ds z)*tM&s)wDiv9a;V8DeuP(=+l-NePK(s}=_3EA)Ebx0qEg=Z7@tQH{M%|I2sWN7`#a zQ(_%ys=9f&uP>u@o!&{9bQ=C<@8}rLeI?%VC)QD9Asu@{#kQ3M%@uFbfRCpj)O}5?K9fg?P5rKS09Ne}btcaKYoy3pC2o^~hef ziFwKEt&CKWAaGT9;qy!Dy}=D^Gs~Df(C#Q{JHP(C zYw++HXOf;IM!PafzxPn)!TZQ78^H)OAY=_RG?b+^g2CDc4<1Cd6-=SduL1Cl1vAhb z1~Zmi%rqx|aH*1>v-F{SvCo!I;1z*4r|hh)8om|Xy$SIMg}$rqI%qeR7u2-FpS!wl ztS{f@QhlU6X%D#p8khHGrTk^lT??^{`5G&_)EAqDzUN-chTI`&m~5S$|Jf^X@-lD+ zn;}2+*-qVhojX@ERJ-z+cpk%dgXp%bnU)b9c_-kgHp;y#=k49x-0;CjOPB5I1Lc%d zc`|nEulDWnaq0SDwL{gr0;L zwuXb!gN*}|-nNMF3*wAD6O3dYmSpgVtX_Q5U05z!g^6%FTg_4?s+W;^7}qcG_uZ8uvf^&noHte{(|=(GKXpM48W3=pYvh-?A& z!d`xNv~};wo+zD-!w3dig^WlGq1PG^(qLrDba&HA$KzCg1;xG(7FLY5`T+O@Qld}i zh^U_UsHpG2XUs}SKHVKZChLa!W>(_HQO5ti81Hb1Hr1Dz`M6p+uAQJ0H-8rw*t#N% z+^+d`S=PTT72%SMtSmUUef*W~`)b%iI_kF&FE6ywRRgiePBjO>h`CgeKXG(_ z!T=H)f*q@~xC>L$Y`?uMTxQ4nBhO4@{9-yzGP_&>y@@v>oM?sPe}B5oMQfpZLG{u_^@E-Qx(^PLkm%e#s~8-T z%A;#&SStc)0DpjPv>V}Fz!Vmno}~Ze<``6N0w;XQL29ZE7RVS|24HR5W*wBHcc!k3 zm1~5Agive0I2UPDd68O>$Av44Z|{Qwr{G@yhg?0UcrM}?Pu)xYr1om|R&2(IKnV7E z+TT!YV`$ZmB$M`9Sy?$CC+{?F7}4u6417?d>|y>R?jPPxCQsM}Y|5M9EXFO3f=HBw zU$8!kXNuqd7rjrddvwz^Zdnyb9A*@%_p$65g(!}r+Pk(h!sDW2XNbO_%z+ny7xE_c z1sA#di`O6I9|XT~6!glx2ZDZFEc(cNM{4DFhdBSqXfw{S@GUI-i8~$6<7!!wbc85F z{rN6Jf}{diH%ym}zPrtZCBhgz0a>6RWPFO1Kt@(7J#uJQb;T;7G*^!1m)`RtXp?FC zfB*AmWNBM3_Mbn8cR4=z&p(ud5bQ@f7-X-I3b9QE>fgUN#6?4p0pl~A&EWMLuS^33VG?F-SaTr)Mo zw+|@tBZd_LhQ?Ka;fOZ#mS)|xouC`d!JhXwURQ8a>E*Oab8`bN11n9=mlIv)F2c8e zd+zHPB=ZU0N_wX8@2p*A?sG~jGu7ns7ey~@raUiYTnYC&oOL~8Zt;S8^O>LL@;?+b zW*L4VVR=q2P9y7eK4RD`A=ve);$2PWk>(yOt7&BcHRXNiKh#tbdG*aN=adZ&ZfO0D zpEG(aG<16UjJvUO!`h~MtYCKrnL!oVi@XBwvgre>Tz{^85hmDl>D6DR?!?)ylMz@yyr(gqwH+&rFonI zn;6PFDakZw3g7{`Cv2?x1B3pXQzXXyyJVmF0QEqn^`4VI6Th{N7JnFgezL84lG2q+ z-1)>M=dbVHC;O&}W+xB!)*cvDhCLdZ79k-iS`T9*Blc?0R|LasS@eI%REz2CMYe*8 z$?*8g@iFib!M~CEi;hA}Tzs2u1$Kdm@i$pn$IHaMRzCt;0nJazD3X4oo&QVwE-D*0 zy8}ovGxf|n+2L%!zk9p#%-r^6I0E&+3n* z1VV4v@y_rEk*Gox!PZU(hqZ@OCvEE6+!vVKJl4eRI{P*^ih-^j3gWG@-f=9+kr$s6 zej(zdkm(tzQ>RFL1Sh_-$xS|49Dj|_Okrtha{s;khU(ecGh}!FYeZCZKkHB`XkK68dx97?cy6o$-3vHK<)5JGzy&AdM`la=w#<%PTM4w=<4UJzH4Am(+Yo_g` zJr*yM1_F?)Xl+%T`1bt!EVx6yd)o{m>=S0iE~d0mm~({>=>{yB?6E-H01b^^e*r+= zmbZF5;##CWyU55wF=}oE`36%rB=mOF)oCnS|Cc*c0Kcie8_Ha;0N7N7njufI}EHqh-)mMYTIIwKk>kvwF83WmLwHWAB=2goI6`uW7q_cK}5V)9Uj_^|2##5+R=J=hZ1$ zr1E?78czOsEQsz)_17iXvO?&OW5seDn4)*AvUsy8*?Q3Fu`Hg*%5*|(O6{*-ChSS% zQw;n2@Gih6c*b)%9ZMdtQsmT`GYCebAkaujw!ib)(iQ4FIM$LJCt{AL>jqvq=R|g=^>>^yf`R2_V-cN}s^t7~Y%hMdh(lHEgg+)cOK9fFci`&xh zD=>6V$Uzef_YsuiOXv$w-l3BfG|6w2G^ZYNt?Yyd^e;BF{Li}?veDk&4zL?L_Y@*~G!d=idR1DyyL{7nR%G^al9{do2L=dPPz&M*q;o!z8Amu5SBd za?3b6^5uIHr6Mq718{HlVAFZ(r;F)29M^^)8+9StTEfmK-0|c5XB^?*>AF?m>&o4z za)7~+ul!C(0!|exptUhGs1{3_;itLVqA2!I2g%iagP6sERApf_12XVnA?PQw*p(ph zoRE7p^*6dc^7Vv0L4L(qZS4Zfs(q3Umuq){Qb@OAp>Dvxz252O=~;qL;cZ$PPpP<7 z^)1gsGlTPE?RvKROoMU>bsmZF_rgYR-Y(4*+iBQ99Gz z89_YJMry%p|6RZ2I$~PW^*zZ}7ypHBi5h1L6AMzVEI_kDaU%FkenRgF!6Z>@Rh~`S zWj}1F*|vc$^Y{DMy)z;kDTmY5GaJb;Ax8xroPd#HQLgA2Qd-`K*?SJHDRqcLAl=^c z`bq89EfH_FoHCJsZPzJZ2Y#ORJ*%dB-_L#C%AeEx8-GbqNbqmFgdD-Av(B)e$L!BU zLV0tM)3egKy;I(sn^$8auA+JRSWwVQu)bE`>&NL(NdJFWfJ_biX>fa4Va+nQCnu*D zS?UUv1r!s>Yfc)n!w@Z>)~MEcrEtib<|X94xVYsvuyFj2OP#ru z$4TjC4vJ^RF{TG%ZV)96@@-=bkH~w*EJy5Xu*FsPz~Je>U#8g7eBHr8)T)|fG!@h) zmfPpXPW-ZMGAKV zPEfA%=!M&|i0A~N+>CqBb-d$WPw5&aLfE{;TU$Wh&+ylPfjq9P)5z?7YkCp-d^P0~ zl`Uxf)6Gt~8A1<2rKYP3a&%k4L~DM|smM=Sj$LDQRTfKC_W!*%(Meb)D*4>GGv~F| z4ag1KLBjvMM7Z?tVpAe-5T)#`z8%~`p3@kyX?ZOI_lCxl4eGH|JO2Ezf`JMKDh%eD zI8ECj1M`K`b0%0ZFMdl|MTL@CVjZZEfPtV|Qp3yBCnO$#mqd(K<&NH$!(h`uy3mcg zb_nu_7g%21_6fv)0~co*hkH3#T!s8ni5mfwDY_B}GU#JpcKTp-H_25nyra#Dg8;%% zU&3Exi3bMx1YN-0d9K}EgZl4b<_nsn<&0P1R#Bho%(&Q?7#`kqf9Gy-aq()`)2E`h zqxrB3nN}@z)ucJg#IW&n*`<)X8MN=q;HKhyTw7CPS$Y{-Fqn)n02wmbK@*IRQ*qg` zCFu`ZaZJS$ZaH>R)n6d@#1^v_#(%@Z;akwA4Udn9QYZ&SH{}Kgi%KkPn{2%Z_I-RW zYH|*C|NmXK-Sh91SG?VtH?1nuxI^C0I9hw9_4u^S)eo{7t`AFV25q{=3Rk}S=N;~0 z>eaf`HP~a=dxS*aoy%;yGgUw8c@rd}uj9Fan9NMO=?%?pId1Mf<>!%ShRo{`Ox&qw zuk|mLt3EbvwSdZ{EC1j4XO=IG_hyHxOlJS&{%cKT4!33J?Ehi57dPe3$;nC7JKBUK z3K9FRXyk8VVs-(cHgP;rhWYsF=g%B8JYlsCu5hRjq8{&)UKeCNm17>EQhWs5{O9ve z*~49UFM7c%kJVZDxFJ|!8#@mep`y17w->bk9bb4G9_(MzJ$>NdM)_IUX#PLVkorKB z+V3dmA_jhgZxOxSj|$%hcJ}{tHeyd)3(L-)knHLyW2^~~URnHC48Q_)_QE`wmE4k<%BeYTbYwaz2c02myJe|bK*n<-FocDj$T<;WiJQd-`&QuwPkzpJa6P>x_=j{f(rxcHzy7@3 zMKC!lVBel5Ah=yCHh^0SnqHjtUc4ovrR{hQdF z%H0Z<=ILn@ZGtn_SAEKLYU*L!$=unsVXD<%MdMCpHwIHhHqb=}|bf_Y|(oG4vrN-DAe95lQT=+i>@YKP=m&1n%Zzdtc&&Djwv2>_m_Oxz z3L-u9wKtOY@Bwe{LGNXOOZQ6Q|D3!JplE<0>Ml4~<+2L|&m5odSq4SEBFORBNjV41 z%Dt5$e9|Ic3Yo=27=tgun-#D*l9QLG671BHL_{i|Vu~Z~RM?fxut1gdMuyR45CgPB zfr&u3c9Q8_zRX(&G@Znzz0I3+eLEX~+UCs6TYrDzP~C^tgioBbmd3mH*Q1QJwrq@k z=UF%F=h^j+k|>io4<;!6)wOj8S^@s(M4OrY2*p85`1Q?F6q~-etjw+DRF#M?6mr&*i`+j7;cUVnEpuTf;Gr` z#qeO)FTFL+W3u9qm>v3i$X=CXJ1e8MwdO2*S2f*;7E!;gpHaToNDp0(5_8~y$%V32 z$SwHa%i3{+yQo^sN2bKU4he}4fJ&7KyPcu1A{1%m`E0K2AE#I_Tfa_2DO4ftI?>q= zE8BqHx{X%9bd1nH3vj?$UgI71ewkmg1N?HZK*f!cIfX&I<8}U{71|Nk1@p;^vnuMf zzqUwc-DkA9{IMOiB>EVOK*yLZv^z`p@rF1R0$ zcu3nW(hsTZXcLD76OiQ+=ve#YFE)9?u_c=ToHZ+yL0aa9FZ zz3yo*^Qr}I3gK|m=g&JE6mE}Nt{Z70QV&rIaiZ2QN=s#Ai(4lwxvfqzo+KrIly^T< z>gF@9t9A&^5m9RJT^9rVKXa+R9= zxr;gnQcbV0l0S0TquWNK<4R|)=}@$Mm19P8!x9>7E5(X|$|-F?k;L%(N5fh2pxV@6 zGr&q5>0j(}ed8-E3A^aAyRZN;M38R`#y4K+19COucTGQTHO3xbV5Xgw(Mx$T;@kQ zB+qOuz0F*H^XZ_DYQV-3yVPA&(114ZSJ+n34=vg2jfHoGwQXRVAtA!=0{`N=gneY* zmE>ci{kLh4&tUvfR9V8FoPn~F`~~TcUvGCVB&`@{2Ew?k#()9p^eap9lkcZ2ihKUN z-19K^`6MLgqKft+(xqtku2&G2Whw_pe)0C3=oydGei^ax6TYA(tg_s*V&Abe)f*@u z7B8{RQQJl+4zPSByP%kWkoe8v^M-fskP)QA^7{I3Yt7_t zB|6*BpUfQ{8I}zBM4S00@z}4Zn5nOWa?+1yw+_vTW%RuiOHAAK;I}z<^|V!*Sm*Vc zEni=S3klo;NuA?ZHF@Ah&e=aY-dMfP@1%<>e?| zdHJ>_k^hyKw~z@Lss=JJt1geedtw)ep7C%No5sRb^DXp0hgnj$GCoT z3?7&@XHE4b${j|2%@a5Mj31$$!#LnpG{Nk>g2;=&woUbe0(GCMl~$Hu|L46u)hjGD ztA4pb;~Pi*HCkWY!S?UptMop)PyAZ)xxEqB>&Go;T5p&zu{ zKuh0}`KX-Ch~htf`qeP3m^J`TJO6I|12o-7-(|ghcQc)FU){NhiuX_7Y^M^JzbXll zv6GSc$@ic9G09IC-*q5=8SQ2Cx01-dCJ8?qM1YXe@pCSIPVk(t*4W9owhFmIQ~;aU z73`sh|7I!rDxJ+mXM`8f>lWJTu5qf6S_>a6R^UC!BRLn8e8E~;@hXODu#-QKm7{gO zi$z&xr~l2Fd39Q&>UjYd505qV-^Rw~anpoxuBQgC`hP;HO<^xuQiG- z@5M}b5JG-D)xBbG|KK^p_iT1Kv4e!4H8sZM6|L{H=5Yh>9e$>ek3;Ou(W> zGz)8Zp+k$1c9?46xf@2ZOx{lqGgdP(g%l|$n3o^x)Qmbp{a6PM@@khL%^w)?!q}E7 zmZmEl0t*Bp~xSm;fECqh{O=x8M0-UObB@TRBT%HY!;iHqr;C-|V1>d5Y8r~gE) z&7ix-+>AIrmgx7m(FL)xg7(o`Qm*G$&$wLru-==@7NR^_!UAgkg^KQTKzaf-AdU#I8avYH$Eu6HZLK;>M6A>)wgmN zgl023kK5?7u-FgD?@^zr<$kR8e^`L8)tlnti7UCV?F*Z&Qzl(S+>r98YQ4|QGg}_d zv!{r?9y5pPefBp8hlW0VWrcl0WvmNKY}qgUBlJbLd(DxtvAz2Paj7Y&v({vGo@b+{ z51Dwp5*)nZ=tLlV*!eNfhGFBvnK&{swF#Y)?;0g9J6ULG0%K@*i>QVfrfP?bt)dg4 zN#H(K$&A8Ew`NgOeOmYBYQm&T;E4jMR}1DV%MI6l3cdI!NTkTq8Z(*{mXP%lnRYs+ zbWUkaZm`Bs?nNwhswNXMa|LHCAh)2&4hof-easw>LD9j&;=XHQN`R>k-!J^62-weu zj=PgqS6s-T(Tr(#KmmcUGP&as6{iI@zYH~$L$X45WPN+zy45A^nX&J9N0h|$nUtVR zgIMwj_9#sB;us;r_ZcrWznPWs0nY6e5<2@bFBgUk45E+1R?n&(b>|r5%$H^P7yqMG z$eozoPof*bw)20^{o5iRZ)hx8D@_yjCkxH{h8vKbJdkQcXA<)<(=$?Nc6HA&xxyA z>nU(WD}KL!q5dLcCmWegS6Vgm6QgFmKIDZ#o2Xq@}_Zd4K_ z48J$rOt(o&2I;oSatZsoBt=T;B`65N*=r⪼M7dvmk=uS1$ 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 diff --git a/src/content/docs-es/runs.mdx b/src/content/docs-es/runs.mdx index 9c24bbb..c4573b7 100644 --- a/src/content/docs-es/runs.mdx +++ b/src/content/docs-es/runs.mdx @@ -302,7 +302,7 @@ Un **token** es lo que conviene darle a un script que solo gestiona runs. No sir - Cada uno caduca a los **30, 90 o 365 días**, o nunca. - Puedes revocar cualquiera en cualquier momento. -Para crear runs sigue haciendo falta un plan con inferencia, uses la credencial que uses. Qué credencial acepta cada endpoint está en la [referencia de la API](/es/docs/api#description/platform-tokens). +Para crear runs sigue haciendo falta un plan con inferencia, uses la credencial que uses. La [referencia de la API](/es/docs/api#description/platform-tokens) indica qué credencial acepta cada endpoint. La clave que hay dentro de un workspace, la que usan sus agentes para la inferencia, no puede gestionar runs: la API le responde con un `403`. Un agente que trabaja en tu workspace no puede lanzar runs nuevos por su cuenta, salvo que le des una de tus propias API keys o tokens. @@ -310,12 +310,12 @@ La clave que hay dentro de un workspace, la que usan sus agentes para la inferen ### Crea un run y síguelo -Un ejemplo de principio a fin: crea un run, guarda su `id` (con [`jq`](https://jqlang.org/)) y sigue su log en directo hasta que termine. +Un ejemplo de principio a fin: crea un run, guarda su `id` (con [`jq`](https://jqlang.org/)) y sigue su log en directo hasta que termine. Si el run no se puede crear, el script muestra el error y se para. ```bash export NAN_TOKEN="nan_pat_..." -RUN_ID=$(curl -s https://api.nan.builders/v1/runs \ +RESP=$(curl -s https://api.nan.builders/v1/runs \ -H "Authorization: Bearer $NAN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ @@ -323,14 +323,16 @@ RUN_ID=$(curl -s https://api.nan.builders/v1/runs \ "agent": "pi", "prompt": "añade tests al parser de fechas y haz que pasen", "cwd": "/home/nan/projects/api" - }' | jq -r .id) + }') +RUN_ID=$(echo "$RESP" | jq -r '.id // empty') +[ -n "$RUN_ID" ] || { echo "$RESP"; exit 1; } curl -N https://api.nan.builders/v1/runs/$RUN_ID/events \ -H "Authorization: Bearer $NAN_TOKEN" \ -H "Accept: text/event-stream" ``` -Los campos equivalen al formulario del portal: **Agent** es `agent`, **Task** es `prompt`, **Folder** es `cwd`, **Separate branch** es `git_isolation` y **Time limit** es `timeout_seconds`. +Los campos equivalen al formulario del portal: **Agent** es `agent`, **Task** es `prompt`, **Folder** es `cwd`, y los opcionales **Separate branch** y **Time limit** son `git_isolation` y `timeout_seconds`. El stream manda un frame `run_event` por cada evento del log, un frame `state` cuando el run cambia de estado y un frame `end` con el run terminado, y después se cierra. diff --git a/src/content/docs/runs.mdx b/src/content/docs/runs.mdx index ee8cf22..efcba10 100644 --- a/src/content/docs/runs.mdx +++ b/src/content/docs/runs.mdx @@ -302,7 +302,7 @@ A **token** is the one to give a script that only manages runs. It cannot call t - Each one expires after **30, 90 or 365 days**, or never. - You can revoke any of them at any time. -Creating runs still needs a plan with inference, whatever credential you use. Which credential each endpoint accepts is in the [API reference](/docs/api#description/platform-tokens). +Creating runs still needs a plan with inference, whatever credential you use. The [API reference](/docs/api#description/platform-tokens) lists which credential each endpoint accepts. The key inside a workspace, the one its agents use for inference, cannot manage runs: the API answers it with `403`. An agent working in your workspace cannot start new runs on its own, unless you give it one of your own API keys or tokens. @@ -310,12 +310,12 @@ The key inside a workspace, the one its agents use for inference, cannot manage ### Create a run and follow it -An end-to-end example: create a run, keep its `id` (with [`jq`](https://jqlang.org/)) and follow its log live until it ends. +An end-to-end example: create a run, keep its `id` (with [`jq`](https://jqlang.org/)) and follow its log live until it ends. If the run cannot be created, the script prints the error and stops. ```bash export NAN_TOKEN="nan_pat_..." -RUN_ID=$(curl -s https://api.nan.builders/v1/runs \ +RESP=$(curl -s https://api.nan.builders/v1/runs \ -H "Authorization: Bearer $NAN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ @@ -323,14 +323,16 @@ RUN_ID=$(curl -s https://api.nan.builders/v1/runs \ "agent": "pi", "prompt": "add tests for the date parser and make them pass", "cwd": "/home/nan/projects/api" - }' | jq -r .id) + }') +RUN_ID=$(echo "$RESP" | jq -r '.id // empty') +[ -n "$RUN_ID" ] || { echo "$RESP"; exit 1; } curl -N https://api.nan.builders/v1/runs/$RUN_ID/events \ -H "Authorization: Bearer $NAN_TOKEN" \ -H "Accept: text/event-stream" ``` -The fields match the portal form: **Agent** is `agent`, **Task** is `prompt`, **Folder** is `cwd`, **Separate branch** is `git_isolation` and **Time limit** is `timeout_seconds`. +The fields match the portal form: **Agent** is `agent`, **Task** is `prompt`, **Folder** is `cwd`, and the optional **Separate branch** and **Time limit** are `git_isolation` and `timeout_seconds`. The stream sends one `run_event` frame per event of the log, a `state` frame when the run changes state, and an `end` frame with the finished run, after which it closes. diff --git a/src/data/openapi.json b/src/data/openapi.json index 9acf6d2..def5605 100644 --- a/src/data/openapi.json +++ b/src/data/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "NaN API", "version": "1.0.0", - "description": "Open models on a shared EU inference cluster. Zero logs.\n\nThe NaN API is OpenAI-compatible: predictable, resource-oriented URLs, JSON request and response bodies, and standard HTTP verbs and status codes. Point any OpenAI SDK at our base URL and your existing code keeps working. Change the base URL and the API key, and that's it.\n\nOne schema across every model, so you only learn the API once. Change the `model` field to switch models; everything else stays the same.\n\n- Base URL: `https://api.nan.builders/v1`\n- OpenAPI spec: this document. Import it into Postman, Insomnia, or your own tooling.\n\nIf you use the [Helmcode](https://helmcode.com) enterprise service, the base URL is `https://api.helmcode.com/v1` instead. Every other endpoint is identical.\n\n## Authentication\n\nEvery request authenticates with an API key, sent as a Bearer token:\n\n```\nAuthorization: Bearer $NAN_API_KEY\n```\n\nYou must be a NaN community member. Generate your key from user settings, under \"API Keys\", on the [platform](https://cloud.nan.builders/). The key is personal and non-transferable. Keep it secret: never embed one in client-side code or commit it to source control. Requests must go over HTTPS; calls over plain HTTP fail.\n\n### Platform tokens\n\nFor scripts that only manage [Runs](#tag/runs) and list your [Workspaces](#tag/workspaces), create a **platform token** (`nan_pat_...`) under \"Tokens\" in your settings, and send it the same way: `Authorization: Bearer $NAN_TOKEN`. A platform token never works for inference: the model endpoints refuse it. An API key works on both. Each endpoint lists the credentials it accepts.\n\n## Making requests\n\nThe API is OpenAI-compatible, so point an official OpenAI SDK at our base URL and change nothing else:\n\n```python\nfrom openai import OpenAI\n\nclient = OpenAI(\n api_key=\"$NAN_API_KEY\",\n base_url=\"https://api.nan.builders/v1\",\n)\n\nresp = client.chat.completions.create(\n model=\"deepseek-v4-flash\",\n messages=[{\"role\": \"user\", \"content\": \"Hello\"}],\n)\nprint(resp.choices[0].message.content)\n```\n\n## Streaming\n\nChat responses can stream token-by-token. Set `\"stream\": true` on `/chat/completions` and the response arrives as Server-Sent Events: each event is a `data:` line carrying a `chat.completion.chunk`, with the new text in `choices[0].delta.content`. A final `data: [DONE]` line ends the stream. `/chat/completions` streams incrementally on every chat model. `/responses` streams incrementally only on `deepseek-v4-flash`; on `qwen3.6` and `gemma4` it holds the whole answer and sends it in one burst at the end.\n\n## Rate limits\n\n{{RATE_LIMITS}}\n\nImage endpoints run on their own budget, separate from the model endpoints: 20 requests per minute and 100 requests per month. The usage endpoint is metered separately too: {{USAGE_RATE_LIMIT}} requests per minute per member. Exceed any limit and you get a `429`.\n\n## Errors\n\nNaN uses conventional HTTP status codes: `2xx` on success, `4xx` for a problem with the request (a missing parameter, an invalid key, an unavailable model) and `5xx` for a server-side error. Every error returns a JSON body in the OpenAI shape:\n\n```json\n{\n \"error\": {\n \"message\": \"The model 'foo' does not exist.\",\n \"type\": \"invalid_request_error\",\n \"param\": \"model\",\n \"code\": \"model_not_found\"\n }\n}\n```\n\n`message` is human-readable, `param` names the offending field when applicable, and `code` is a short machine-readable string you can branch on.\n\n| Status | Meaning | `code` |\n| --- | --- | --- |\n| `400` | Invalid or malformed parameter (`param` says which); content blocked by the safety filter; a `max_tokens` above what the model can generate (a generic \"Invalid request\" message today); or a request that does not fit the model's context window. On the models hosted on the cluster (`qwen3.6`, `gemma4`) the overflow message is \"Context length exceeded for model '...'\" with the model's limit; on the other models it is usually that text, but it can arrive as the generic \"Invalid request. Check your request parameters.\" | `\"400\"` (with `type: invalid_request_error`) · `content_policy_violation` |\n| `401` | Missing or invalid API key, or a key whose tier does not reach the requested model (`glm5.3`): \"This API key does not have access to the requested model\", `type: auth_error`. Measured 2026-09-12. | `invalid_api_key` |\n| `402` | The token allowance is spent on a model that carries one. Not retryable: the counter returns to zero when that model's quota period does, the calendar month for the models counted per month and your billing period for `glm5.3`. | `monthly_cap_reached` |\n| `403` | Your tier can't access this endpoint. Image generation requires inference membership. A model your tier cannot reach answers `401`, not this. | `tier_restricted` |\n| `404` | The requested model doesn't exist. | `model_not_found` |\n| `429` | Rate limit hit (`rpm_limit`, `max_parallel_requests`), the rolling 4h token budget of `glm5.3`, or a quota exhausted. | `rate_limit_exceeded` · `insufficient_quota` · `quota_exceeded` |\n| `500` | Something went wrong on our side (includes upstream model errors). | (none) |\n| `524` | Timeout, typical with large audio files on `/audio/transcriptions`. | (none) |\n\nRetry `429` and `5xx` responses with exponential backoff. Don't retry `400`, `401`, `403`, or `404` blindly (a context overflow is not retried automatically either: shorten the input or pick a model with a larger window): they'll fail the same way every time until you change the request. `402` cannot be fixed by repetition either: it clears when that model's quota period resets.\n\n## Model catalog\n\nEvery endpoint takes a `model` id. Capabilities vary by model:\n\n| Model | Use for | Capabilities |\n| --- | --- | --- |\n| `deepseek-v4-flash` | Chat, vision, reasoning | Streaming, tool calling, reasoning, image input, 1M-token context. 3B tokens/month per member |\n| `mimo-v2.6-flash` | Chat, vision, audio | Streaming, tool calling, reasoning, image input, audio input, 1M-token context. 1.0B tokens/month per member |\n| `qwen3.8-flash` | Chat, vision, agents | Streaming, tool calling, reasoning (on by default), vision, 1M-token context. 500M tokens/month per member |\n| `glm5.3-flash` | Chat, vision, agents | Streaming, tool calling, reasoning, vision, 1M-token context. 2B tokens/month per member |\n| `qwen3.6` | Chat, agents | Streaming, tool calling, vision, reasoning (opt-out, returns `reasoning_content`) |\n| `gemma4` | Chat, vision, agents | Streaming, tool calling, vision, reasoning (opt-in) |\n| `glm5.3` | Coding, long-horizon agents | Streaming, tool calling, reasoning trace, image, audio and video input, 1M-token context. Premium tier only |\n| `qwen3-embedding` | Embeddings | 4096-dimension vectors |\n| `rerank` | RAG reranking | Qwen3-Reranker-8B, 100+ languages |\n| `kokoro` | Text-to-speech | Multiple voices and audio formats |\n| `whisper` | Speech-to-text | Transcription with word/segment timestamps |\n| `flux-2-klein` | Image generation | Text-to-image and image-to-image |\n| `qwen-image-2.1` | Image generation (text→image) | 512-1280 px, 1-4 per request, seed 0-2147483647. 100 images/month per member (shared pool with flux-2-klein) |\n\n`glm5.3` is served only to keys on the GLM 5.3 premium tier; every other model is available to any inference member. Call [List models](#tag/Models) for the exact set available to your key.\n\n## Versioning & compatibility\n\nThe API tracks the OpenAI API surface, so OpenAI SDKs and tools work against `https://api.nan.builders/v1` unchanged. This reference documents the stable public `/v1` endpoints, plus the two read-only [Workspaces](#tag/workspaces) endpoints under `/api/workspaces`, and we add capabilities without breaking existing fields.", + "description": "Open models on a shared EU inference cluster. Zero logs.\n\nThe NaN API is OpenAI-compatible: predictable, resource-oriented URLs, JSON request and response bodies, and standard HTTP verbs and status codes. Point any OpenAI SDK at our base URL and your existing code keeps working. Change the base URL and the API key, and that's it.\n\nOne schema across every model, so you only learn the API once. Change the `model` field to switch models; everything else stays the same.\n\n- Base URL: `https://api.nan.builders/v1`\n- OpenAPI spec: this document. Import it into Postman, Insomnia, or your own tooling.\n\nIf you use the [Helmcode](https://helmcode.com) enterprise service, the base URL is `https://api.helmcode.com/v1` instead. Every other endpoint is identical.\n\n## Authentication\n\nEvery inference request authenticates with an API key, sent as a Bearer token:\n\n```\nAuthorization: Bearer $NAN_API_KEY\n```\n\nYou must be a NaN community member. Generate your key from user settings, under \"API Keys\", on the [platform](https://cloud.nan.builders/). The key is personal and non-transferable. Keep it secret: never embed one in client-side code or commit it to source control. Requests must go over HTTPS; calls over plain HTTP fail.\n\n### Platform tokens\n\nFor scripts that only manage [Runs](#tag/runs) and list your [Workspaces](#tag/workspaces), create a **platform token** (`nan_pat_...`) under \"Tokens\" in your settings, and send it the same way: `Authorization: Bearer $NAN_TOKEN`. A platform token never works for inference: the model endpoints refuse it. An API key works on both. Each endpoint lists the credentials it accepts.\n\n## Making requests\n\nThe API is OpenAI-compatible, so point an official OpenAI SDK at our base URL and change nothing else:\n\n```python\nfrom openai import OpenAI\n\nclient = OpenAI(\n api_key=\"$NAN_API_KEY\",\n base_url=\"https://api.nan.builders/v1\",\n)\n\nresp = client.chat.completions.create(\n model=\"deepseek-v4-flash\",\n messages=[{\"role\": \"user\", \"content\": \"Hello\"}],\n)\nprint(resp.choices[0].message.content)\n```\n\n## Streaming\n\nChat responses can stream token-by-token. Set `\"stream\": true` on `/chat/completions` and the response arrives as Server-Sent Events: each event is a `data:` line carrying a `chat.completion.chunk`, with the new text in `choices[0].delta.content`. A final `data: [DONE]` line ends the stream. `/chat/completions` streams incrementally on every chat model. `/responses` streams incrementally only on `deepseek-v4-flash`; on `qwen3.6` and `gemma4` it holds the whole answer and sends it in one burst at the end.\n\n## Rate limits\n\n{{RATE_LIMITS}}\n\nImage endpoints run on their own budget, separate from the model endpoints: 20 requests per minute and 100 requests per month. The usage endpoint is metered separately too: {{USAGE_RATE_LIMIT}} requests per minute per member. Exceed any limit and you get a `429`.\n\n## Errors\n\nNaN uses conventional HTTP status codes: `2xx` on success, `4xx` for a problem with the request (a missing parameter, an invalid key, an unavailable model) and `5xx` for a server-side error. Every error returns a JSON body in the OpenAI shape:\n\n```json\n{\n \"error\": {\n \"message\": \"The model 'foo' does not exist.\",\n \"type\": \"invalid_request_error\",\n \"param\": \"model\",\n \"code\": \"model_not_found\"\n }\n}\n```\n\n`message` is human-readable, `param` names the offending field when applicable, and `code` is a short machine-readable string you can branch on.\n\n| Status | Meaning | `code` |\n| --- | --- | --- |\n| `400` | Invalid or malformed parameter (`param` says which); content blocked by the safety filter; a `max_tokens` above what the model can generate (a generic \"Invalid request\" message today); or a request that does not fit the model's context window. On the models hosted on the cluster (`qwen3.6`, `gemma4`) the overflow message is \"Context length exceeded for model '...'\" with the model's limit; on the other models it is usually that text, but it can arrive as the generic \"Invalid request. Check your request parameters.\" | `\"400\"` (with `type: invalid_request_error`) · `content_policy_violation` |\n| `401` | Missing or invalid API key, or a key whose tier does not reach the requested model (`glm5.3`): \"This API key does not have access to the requested model\", `type: auth_error`. Measured 2026-09-12. | `invalid_api_key` |\n| `402` | The token allowance is spent on a model that carries one. Not retryable: the counter returns to zero when that model's quota period does, the calendar month for the models counted per month and your billing period for `glm5.3`. | `monthly_cap_reached` |\n| `403` | Your tier can't access this endpoint. Image generation requires inference membership. A model your tier cannot reach answers `401`, not this. | `tier_restricted` |\n| `404` | The requested model doesn't exist. | `model_not_found` |\n| `429` | Rate limit hit (`rpm_limit`, `max_parallel_requests`), the rolling 4h token budget of `glm5.3`, or a quota exhausted. | `rate_limit_exceeded` · `insufficient_quota` · `quota_exceeded` |\n| `500` | Something went wrong on our side (includes upstream model errors). | (none) |\n| `524` | Timeout, typical with large audio files on `/audio/transcriptions`. | (none) |\n\nRetry `429` and `5xx` responses with exponential backoff. Don't retry `400`, `401`, `403`, or `404` blindly (a context overflow is not retried automatically either: shorten the input or pick a model with a larger window): they'll fail the same way every time until you change the request. `402` cannot be fixed by repetition either: it clears when that model's quota period resets.\n\n## Model catalog\n\nEvery endpoint takes a `model` id. Capabilities vary by model:\n\n| Model | Use for | Capabilities |\n| --- | --- | --- |\n| `deepseek-v4-flash` | Chat, vision, reasoning | Streaming, tool calling, reasoning, image input, 1M-token context. 3B tokens/month per member |\n| `mimo-v2.6-flash` | Chat, vision, audio | Streaming, tool calling, reasoning, image input, audio input, 1M-token context. 1.0B tokens/month per member |\n| `qwen3.8-flash` | Chat, vision, agents | Streaming, tool calling, reasoning (on by default), vision, 1M-token context. 500M tokens/month per member |\n| `glm5.3-flash` | Chat, vision, agents | Streaming, tool calling, reasoning, vision, 1M-token context. 2B tokens/month per member |\n| `qwen3.6` | Chat, agents | Streaming, tool calling, vision, reasoning (opt-out, returns `reasoning_content`) |\n| `gemma4` | Chat, vision, agents | Streaming, tool calling, vision, reasoning (opt-in) |\n| `glm5.3` | Coding, long-horizon agents | Streaming, tool calling, reasoning trace, image, audio and video input, 1M-token context. Premium tier only |\n| `qwen3-embedding` | Embeddings | 4096-dimension vectors |\n| `rerank` | RAG reranking | Qwen3-Reranker-8B, 100+ languages |\n| `kokoro` | Text-to-speech | Multiple voices and audio formats |\n| `whisper` | Speech-to-text | Transcription with word/segment timestamps |\n| `flux-2-klein` | Image generation | Text-to-image and image-to-image |\n| `qwen-image-2.1` | Image generation (text→image) | 512-1280 px, 1-4 per request, seed 0-2147483647. 100 images/month per member (shared pool with flux-2-klein) |\n\n`glm5.3` is served only to keys on the GLM 5.3 premium tier; every other model is available to any inference member. Call [List models](#tag/Models) for the exact set available to your key.\n\n## Versioning & compatibility\n\nThe API tracks the OpenAI API surface, so OpenAI SDKs and tools work against `https://api.nan.builders/v1` unchanged. This reference documents the stable public `/v1` endpoints, plus the two read-only [Workspaces](#tag/workspaces) endpoints under `/api/workspaces`, and we add capabilities without breaking existing fields.", "contact": { "name": "NaN", "url": "https://nan.builders" @@ -64,7 +64,7 @@ }, { "name": "Workspaces", - "description": "Read-only access to your own workspaces, for scripts that start runs by workspace name. Served at `https://api.nan.builders/api/workspaces`, outside `/v1`. Creating, starting, stopping and deleting workspaces is done from the platform, not from this API. Guide: [Workspaces](https://nan.builders/docs/workspaces)." + "description": "Read-only access to your own workspaces, for scripts that need a workspace's name, id or status. Served at `https://api.nan.builders/api/workspaces`, outside `/v1`. Creating, starting, stopping and deleting workspaces is done from the platform, not from this API. Guide: [Workspaces](https://nan.builders/docs/workspaces)." } ], "paths": { @@ -1602,7 +1602,7 @@ "platformToken": [] } ], - "description": "Gives an agent a task in one of your workspaces. The run is queued and starts as soon as there is a free place: a workspace runs 1 (Micro and the free workspace), 2 (Nano), 3 (Basic), 5 (Medium) or 8 (Large) runs at once, and you can have at most 5 runs working across all your workspaces (8 on Premium). Extra runs wait, first in, first out.\n\nLimits: up to 20 queued runs per workspace and 50 in total (`429 run_queue_full`), and 60 new runs per rolling hour (`429 rate_limited`).\n\nCreating runs needs a plan with inference (`403 tier_restricted`): the run uses the workspace's inference key and counts against your plan's usage. The workspace must be `running` and have the agent installed.\n\n**Idempotency.** Send an `Idempotency-Key` header to make retries safe: the same key with the same body returns the run already created (`200`), the same key with a different body answers `409 idempotency_conflict`.\n\nAuthentication: a member API key (`sk-...`) or a platform token (`nan_pat_...`).", + "description": "Gives an agent a task in one of your workspaces. The run is queued and starts as soon as there is a free place: a workspace runs 1 (Micro and the free workspace), 2 (Nano), 3 (Basic), 5 (Medium) or 8 (Large) runs at once, never more than your per-member total: at most 5 runs working across all your workspaces (8 on Premium). Extra runs wait, first in, first out.\n\nLimits: up to 20 queued runs per workspace and 50 in total (`429 run_queue_full`), and 60 new runs per rolling hour (`429 rate_limited`).\n\nCreating runs needs a plan with inference (`403 tier_restricted`): the run uses the workspace's inference key and counts against your plan's usage. The workspace must be `running` and have the agent installed.\n\n**Idempotency.** Send an `Idempotency-Key` header to make retries safe: the same key with the same body returns the run already created (`200`), the same key with a different body answers `409 idempotency_conflict`.\n\nAuthentication: a member API key (`sk-...`) or a platform token (`nan_pat_...`).", "parameters": [ { "name": "Idempotency-Key", @@ -1715,7 +1715,7 @@ } }, "400": { - "description": "A field is invalid; `param` names it (`workspace`, `agent`, `prompt`, `cwd`, `timeout_seconds`, `model`, `trigger`, `Idempotency-Key`, or an unknown field).", + "description": "A field is invalid; `param` names it (`workspace`, `agent`, `prompt`, `cwd`, `timeout_seconds`, `model`, `trigger`, `Idempotency-Key`, or an unknown field). The body is limited to 128 KiB, and must be one JSON object.", "content": { "application/json": { "schema": { @@ -1733,7 +1733,7 @@ } }, "401": { - "$ref": "#/components/responses/Unauthorized" + "$ref": "#/components/responses/RunsUnauthorized" }, "403": { "description": "`tier_restricted`: your plan does not include inference. `workspace_key_not_allowed`: the bearer is the key inside a workspace.", @@ -1852,7 +1852,7 @@ "schema": { "type": "string" }, - "description": "Only the runs of this workspace (name or id)." + "description": "Only the runs of this workspace (name or id). A workspace that is not yours gives an empty page." }, { "name": "state", @@ -1957,7 +1957,7 @@ } }, "401": { - "$ref": "#/components/responses/Unauthorized" + "$ref": "#/components/responses/RunsUnauthorized" }, "403": { "description": "The bearer is the key that lives inside a workspace. Agents cannot manage runs: use your API key or a platform token.", @@ -1976,24 +1976,6 @@ } } } - }, - "404": { - "description": "The `workspace` filter names no workspace of yours.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - }, - "example": { - "error": { - "message": "workspace not found", - "type": "invalid_request_error", - "param": null, - "code": "not_found" - } - } - } - } } }, "x-codeSamples": [ @@ -2083,7 +2065,7 @@ } }, "401": { - "$ref": "#/components/responses/Unauthorized" + "$ref": "#/components/responses/RunsUnauthorized" }, "403": { "description": "The bearer is the key that lives inside a workspace. Agents cannot manage runs: use your API key or a platform token.", @@ -2264,7 +2246,7 @@ } }, "401": { - "$ref": "#/components/responses/Unauthorized" + "$ref": "#/components/responses/RunsUnauthorized" }, "403": { "description": "The bearer is the key that lives inside a workspace. Agents cannot manage runs: use your API key or a platform token.", @@ -2441,14 +2423,13 @@ "last_seq": 42, "created_at": "2026-10-10T12:00:00Z", "started_at": "2026-10-10T12:00:02Z", - "finished_at": "2026-10-10T12:03:41Z", - "prompt": "add tests for the date parser and make them pass" + "finished_at": "2026-10-10T12:03:41Z" } } } }, "401": { - "$ref": "#/components/responses/Unauthorized" + "$ref": "#/components/responses/RunsUnauthorized" }, "403": { "description": "The bearer is the key that lives inside a workspace. Agents cannot manage runs: use your API key or a platform token.", @@ -2677,19 +2658,6 @@ } } }, - "400": { - "description": "The id is not a valid UUID.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PlatformError" - }, - "example": { - "error": "invalid workspace id" - } - } - } - }, "401": { "description": "Missing or invalid credentials.", "content": { @@ -2704,14 +2672,24 @@ } }, "403": { - "description": "The workspace belongs to another member.", + "description": "The workspace belongs to another member (`forbidden`), or the id is not a UUID: a platform token is only accepted on a well-formed id (`token_scope_denied`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlatformError" }, - "example": { - "error": "forbidden" + "examples": { + "forbidden": { + "value": { + "error": "forbidden" + } + }, + "token_scope_denied": { + "value": { + "error": "token_scope_denied", + "message": "Platform tokens only work on the platform API (/v1/runs and GET /api/workspaces). Use a signed-in session or an inference API key for this endpoint." + } + } } } } @@ -2751,7 +2729,7 @@ "type": "http", "scheme": "bearer", "bearerFormat": "nan_pat_...", - "description": "Platform token (`nan_pat_...`), from \"Tokens\" in your settings on the platform. Send it as `Authorization: Bearer `. It works ONLY on the platform API: the Runs endpoints (`/v1/runs...`) and the two Workspaces endpoints. It never works for inference: inference endpoints refuse it (`401`, or `403 token_scope_denied`). Shown once when you create it; up to 10 active; expires after 30, 90 or 365 days, or never; revocable at any time. Any member with an active subscription can create one; creating runs still needs a plan with inference." + "description": "Platform token (`nan_pat_...`), from \"Tokens\" in your settings on the platform. Send it as `Authorization: Bearer `. It works only on the platform API: the Runs endpoints (`/v1/runs...`) and the two Workspaces endpoints. It never works for inference: inference endpoints refuse it (`401`, or `403 token_scope_denied`). Shown once when you create it; up to 10 active; expires after 30, 90 or 365 days, or never; revocable at any time. Any member with an active subscription can create one; creating runs still needs a plan with inference." } }, "schemas": { @@ -3978,7 +3956,7 @@ }, "usage": { "type": "object", - "description": "Tokens the run used, once known (`null` until then).", + "description": "Tokens the run used. Each field is `null` until it is known.", "properties": { "tokens_in": { "type": [ @@ -4190,7 +4168,7 @@ }, "RunEventDataError": { "type": "object", - "description": "`data` of a `error` event. Something went wrong.", + "description": "`data` of an `error` event. Something went wrong.", "properties": { "code": { "type": "string", @@ -4642,6 +4620,24 @@ } } } + }, + "RunsUnauthorized": { + "description": "Missing or invalid credentials: no `Authorization` header, an unknown or revoked API key or platform token, or an expired one (`invalid_api_key`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "error": { + "message": "Invalid authentication credentials. Provide a valid NaN API key in the Authorization header.", + "type": "invalid_request_error", + "param": null, + "code": "invalid_api_key" + } + } + } + } } } }, diff --git a/src/lib/openapiRuns.test.ts b/src/lib/openapiRuns.test.ts index 92c586a..7f68c8c 100644 --- a/src/lib/openapiRuns.test.ts +++ b/src/lib/openapiRuns.test.ts @@ -193,6 +193,33 @@ describe('openapi.json: the Runs contract (cloud-api)', () => { expect(create.description).toMatch(/60 new runs per rolling hour/); }); + it('only GET /runs/{id} returns the full prompt (the handler renders it nowhere else)', () => { + for (const [path, method] of RUN_OPS) { + for (const [status, r] of Object.entries(op(path, method).responses) as Array<[string, any]>) { + const ex = r.content?.['application/json']?.example; + if (!ex || typeof ex !== 'object') continue; + const runs = Array.isArray(ex.data) ? ex.data : [ex]; + for (const run of runs) { + if (run?.object !== 'run') continue; + expect('prompt' in run, `${method} ${path} ${status}`).toBe(path === '/runs/{id}' && method === 'get'); + } + } + } + }); + + it('GET /runs: an unknown workspace filter is an empty page, not a 404', () => { + expect(op('/runs', 'get').responses['404']).toBeUndefined(); + const ws = op('/runs', 'get').parameters.find((p: any) => p.name === 'workspace'); + expect(ws.description).toMatch(/empty page/); + }); + + it('runs 401s describe both credentials', () => { + for (const [path, method] of RUN_OPS) { + expect(op(path, method).responses['401']).toEqual({ $ref: '#/components/responses/RunsUnauthorized' }); + } + expect(s.components.responses.RunsUnauthorized.description).toMatch(/API key or platform token/); + }); + it('GET /runs: filters and the list object', () => { const params = Object.fromEntries(op('/runs', 'get').parameters.map((p: any) => [p.name, p])); expect(Object.keys(params)).toEqual(['workspace', 'state', 'limit', 'cursor']); @@ -254,6 +281,12 @@ describe('openapi.json: the Workspaces contract (cloud-api)', () => { expect(JSON.stringify(s)).not.toContain('cloud-api.nan.builders'); }); + it('GET /api/workspaces/{id} with a token: 403 forbidden or token_scope_denied, never the handler 400', () => { + const r = op('/api/workspaces/{id}', 'get').responses; + expect(Object.keys(r).sort()).toEqual(['200', '401', '403', '404']); + expect(Object.keys(r['403'].content['application/json'].examples)).toEqual(['forbidden', 'token_scope_denied']); + }); + it('only the caller\'s own workspaces, and never a secret', () => { expect(op('/api/workspaces', 'get').description).toMatch(/Your own workspaces, and only yours/); expect(op('/api/workspaces/{id}', 'get').description).toMatch(/Only your own workspaces are returned/); diff --git a/src/tests/layouts/runsDocs.test.ts b/src/tests/layouts/runsDocs.test.ts index 248ebb5..2b49d64 100644 --- a/src/tests/layouts/runsDocs.test.ts +++ b/src/tests/layouts/runsDocs.test.ts @@ -133,7 +133,7 @@ describe.each(Object.entries(pages))('agent runs guide (%s)', (locale, page) => test('maps the portal labels to the CLI flags and the API fields', () => { expect(flat).toMatch(/\*\*Agent\*\* (is|es) `--agent`, \*\*Folder\*\* (is|es) `--cwd`, \*\*Separate branch\*\* (is|es) `--worktree` \/ `--no-worktree` (and|y) \*\*Time limit\*\* (is|es) `--timeout`/); - expect(flat).toMatch(/\*\*Agent\*\* (is|es) `agent`, \*\*Task\*\* (is|es) `prompt`, \*\*Folder\*\* (is|es) `cwd`, \*\*Separate branch\*\* (is|es) `git_isolation` (and|y) \*\*Time limit\*\* (is|es) `timeout_seconds`/); + expect(flat).toMatch(/\*\*Agent\*\* (is|es) `agent`, \*\*Task\*\* (is|es) `prompt`, \*\*Folder\*\* (is|es) `cwd`, (and the optional|y los opcionales) \*\*Separate branch\*\* (and|y) \*\*Time limit\*\* (are|son) `git_isolation` (and|y) `timeout_seconds`/); }); test('describes the portal form with its limits', () => { @@ -196,8 +196,10 @@ describe.each(Object.entries(pages))('agent runs guide (%s)', (locale, page) => }); test('keeps one end-to-end API example: create a run, then follow it with curl -N', () => { - expect(body).toContain('curl -s https://api.nan.builders/v1/runs'); - expect(body).toContain('| jq -r .id)'); + expect(body).toContain('RESP=$(curl -s https://api.nan.builders/v1/runs'); + // A failed create must show its error, not follow /runs/null/events. + expect(body).toContain(`RUN_ID=$(echo "$RESP" | jq -r '.id // empty')`); + expect(body).toContain('[ -n "$RUN_ID" ] || { echo "$RESP"; exit 1; }'); expect(body).toContain('curl -N https://api.nan.builders/v1/runs/$RUN_ID/events'); expect(body).toContain('-H "Accept: text/event-stream"'); for (const frame of ['`run_event`', '`state`', '`end`']) expect(body).toContain(frame); From 1f38cdc3fa7b77d3a975ac786fa3f422ad6b6e0b Mon Sep 17 00:00:00 2001 From: barckcode Date: Sat, 10 Oct 2026 22:39:57 +0200 Subject: [PATCH 6/9] docs(runs): make the API example safe to paste, document session_required The create-and-follow example no longer exits the member's shell when the create fails: it prints the error instead of following the log. The Workspaces 401s show session_required for keys those routes refuse, and the runs 401 notes that the message may vary. Co-Authored-By: Claude Opus 5.5 --- src/content/docs-es/runs.mdx | 14 ++++++----- src/content/docs/runs.mdx | 14 ++++++----- src/data/openapi.json | 40 ++++++++++++++++++++++-------- src/tests/layouts/runsDocs.test.ts | 6 +++-- 4 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/content/docs-es/runs.mdx b/src/content/docs-es/runs.mdx index c4573b7..82e71d5 100644 --- a/src/content/docs-es/runs.mdx +++ b/src/content/docs-es/runs.mdx @@ -310,7 +310,7 @@ La clave que hay dentro de un workspace, la que usan sus agentes para la inferen ### Crea un run y síguelo -Un ejemplo de principio a fin: crea un run, guarda su `id` (con [`jq`](https://jqlang.org/)) y sigue su log en directo hasta que termine. Si el run no se puede crear, el script muestra el error y se para. +Un ejemplo de principio a fin: crea un run, guarda su `id` (con [`jq`](https://jqlang.org/)) y sigue su log en directo hasta que termine. Si el run no se puede crear, muestra el error en lugar de seguir el log. ```bash export NAN_TOKEN="nan_pat_..." @@ -325,11 +325,13 @@ RESP=$(curl -s https://api.nan.builders/v1/runs \ "cwd": "/home/nan/projects/api" }') RUN_ID=$(echo "$RESP" | jq -r '.id // empty') -[ -n "$RUN_ID" ] || { echo "$RESP"; exit 1; } - -curl -N https://api.nan.builders/v1/runs/$RUN_ID/events \ - -H "Authorization: Bearer $NAN_TOKEN" \ - -H "Accept: text/event-stream" +if [ -z "$RUN_ID" ]; then + echo "$RESP" +else + curl -N "https://api.nan.builders/v1/runs/$RUN_ID/events" \ + -H "Authorization: Bearer $NAN_TOKEN" \ + -H "Accept: text/event-stream" +fi ``` Los campos equivalen al formulario del portal: **Agent** es `agent`, **Task** es `prompt`, **Folder** es `cwd`, y los opcionales **Separate branch** y **Time limit** son `git_isolation` y `timeout_seconds`. diff --git a/src/content/docs/runs.mdx b/src/content/docs/runs.mdx index efcba10..b3a3e04 100644 --- a/src/content/docs/runs.mdx +++ b/src/content/docs/runs.mdx @@ -310,7 +310,7 @@ The key inside a workspace, the one its agents use for inference, cannot manage ### Create a run and follow it -An end-to-end example: create a run, keep its `id` (with [`jq`](https://jqlang.org/)) and follow its log live until it ends. If the run cannot be created, the script prints the error and stops. +An end-to-end example: create a run, keep its `id` (with [`jq`](https://jqlang.org/)) and follow its log live until it ends. If the run cannot be created, it prints the error instead of following the log. ```bash export NAN_TOKEN="nan_pat_..." @@ -325,11 +325,13 @@ RESP=$(curl -s https://api.nan.builders/v1/runs \ "cwd": "/home/nan/projects/api" }') RUN_ID=$(echo "$RESP" | jq -r '.id // empty') -[ -n "$RUN_ID" ] || { echo "$RESP"; exit 1; } - -curl -N https://api.nan.builders/v1/runs/$RUN_ID/events \ - -H "Authorization: Bearer $NAN_TOKEN" \ - -H "Accept: text/event-stream" +if [ -z "$RUN_ID" ]; then + echo "$RESP" +else + curl -N "https://api.nan.builders/v1/runs/$RUN_ID/events" \ + -H "Authorization: Bearer $NAN_TOKEN" \ + -H "Accept: text/event-stream" +fi ``` The fields match the portal form: **Agent** is `agent`, **Task** is `prompt`, **Folder** is `cwd`, and the optional **Separate branch** and **Time limit** are `git_isolation` and `timeout_seconds`. diff --git a/src/data/openapi.json b/src/data/openapi.json index def5605..c7bb91b 100644 --- a/src/data/openapi.json +++ b/src/data/openapi.json @@ -2553,14 +2553,24 @@ } }, "401": { - "description": "Missing or invalid credentials.", + "description": "Missing or invalid credentials (`unauthorized`), or the key that lives inside a workspace (`session_required`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlatformError" }, - "example": { - "error": "unauthorized" + "examples": { + "unauthorized": { + "value": { + "error": "unauthorized" + } + }, + "session_required": { + "value": { + "error": "session_required", + "message": "This endpoint requires a signed-in session. API keys only work for inference (https://api.nan.builders/v1)." + } + } } } } @@ -2659,14 +2669,24 @@ } }, "401": { - "description": "Missing or invalid credentials.", + "description": "Missing or invalid credentials (`unauthorized`), or an API key (`sk-...`), which this route does not accept (`session_required`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlatformError" }, - "example": { - "error": "unauthorized" + "examples": { + "unauthorized": { + "value": { + "error": "unauthorized" + } + }, + "session_required": { + "value": { + "error": "session_required", + "message": "This endpoint requires a signed-in session. API keys only work for inference (https://api.nan.builders/v1)." + } + } } } } @@ -3668,15 +3688,15 @@ }, "PlatformError": { "type": "object", - "description": "Error body of the Workspaces endpoints (outside `/v1`): a short `error` string, plus `message` on `token_scope_denied`.", + "description": "Error body of the Workspaces endpoints (outside `/v1`): a short `error` string, plus `message` on `token_scope_denied` and `session_required`.", "properties": { "error": { "type": "string", - "description": "Short error, for example `unauthorized`, `forbidden`, `workspace not found` or `token_scope_denied`." + "description": "Short error, for example `unauthorized`, `session_required`, `forbidden`, `workspace not found` or `token_scope_denied`." }, "message": { "type": "string", - "description": "Present on `token_scope_denied`: what the token can be used for." + "description": "Present on `token_scope_denied` and `session_required`: what the credential can be used for." } }, "required": [ @@ -4622,7 +4642,7 @@ } }, "RunsUnauthorized": { - "description": "Missing or invalid credentials: no `Authorization` header, an unknown or revoked API key or platform token, or an expired one (`invalid_api_key`).", + "description": "Missing or invalid credentials: no `Authorization` header, an unknown or revoked API key or platform token, or an expired one (`invalid_api_key`). The message may vary.", "content": { "application/json": { "schema": { diff --git a/src/tests/layouts/runsDocs.test.ts b/src/tests/layouts/runsDocs.test.ts index 2b49d64..b2a5a49 100644 --- a/src/tests/layouts/runsDocs.test.ts +++ b/src/tests/layouts/runsDocs.test.ts @@ -199,8 +199,10 @@ describe.each(Object.entries(pages))('agent runs guide (%s)', (locale, page) => expect(body).toContain('RESP=$(curl -s https://api.nan.builders/v1/runs'); // A failed create must show its error, not follow /runs/null/events. expect(body).toContain(`RUN_ID=$(echo "$RESP" | jq -r '.id // empty')`); - expect(body).toContain('[ -n "$RUN_ID" ] || { echo "$RESP"; exit 1; }'); - expect(body).toContain('curl -N https://api.nan.builders/v1/runs/$RUN_ID/events'); + // Safe to paste into an interactive shell: no `exit` that would close it. + expect(body).toContain('if [ -z "$RUN_ID" ]; then\n echo "$RESP"\nelse'); + expect(body).not.toMatch(/\bexit 1\b/); + expect(body).toContain('curl -N "https://api.nan.builders/v1/runs/$RUN_ID/events"'); expect(body).toContain('-H "Accept: text/event-stream"'); for (const frame of ['`run_event`', '`state`', '`end`']) expect(body).toContain(frame); }); From 939457a77689d89c33d56c553f9458c82138250b Mon Sep 17 00:00:00 2001 From: barckcode Date: Sat, 10 Oct 2026 22:41:54 +0200 Subject: [PATCH 7/9] docs(api): name the invalid-session 401 on the Workspaces routes Co-Authored-By: Claude Opus 5.5 --- src/data/openapi.json | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/data/openapi.json b/src/data/openapi.json index c7bb91b..906fbc9 100644 --- a/src/data/openapi.json +++ b/src/data/openapi.json @@ -2553,7 +2553,7 @@ } }, "401": { - "description": "Missing or invalid credentials (`unauthorized`), or the key that lives inside a workspace (`session_required`).", + "description": "No credentials (`unauthorized`), an unknown, revoked or expired one (`invalid session`), or the key that lives inside a workspace (`session_required`).", "content": { "application/json": { "schema": { @@ -2565,6 +2565,11 @@ "error": "unauthorized" } }, + "invalid_session": { + "value": { + "error": "invalid session" + } + }, "session_required": { "value": { "error": "session_required", @@ -2669,7 +2674,7 @@ } }, "401": { - "description": "Missing or invalid credentials (`unauthorized`), or an API key (`sk-...`), which this route does not accept (`session_required`).", + "description": "No credentials (`unauthorized`), an unknown, revoked or expired one (`invalid session`), or an API key (`sk-...`), which this route does not accept (`session_required`).", "content": { "application/json": { "schema": { @@ -2681,6 +2686,11 @@ "error": "unauthorized" } }, + "invalid_session": { + "value": { + "error": "invalid session" + } + }, "session_required": { "value": { "error": "session_required", @@ -3692,7 +3702,7 @@ "properties": { "error": { "type": "string", - "description": "Short error, for example `unauthorized`, `session_required`, `forbidden`, `workspace not found` or `token_scope_denied`." + "description": "Short error, for example `unauthorized`, `invalid session`, `session_required`, `forbidden`, `workspace not found` or `token_scope_denied`." }, "message": { "type": "string", From 9c2cce19912c0931cd13330befa7cb574513c091 Mon Sep 17 00:00:00 2001 From: barckcode Date: Sat, 10 Oct 2026 23:08:23 +0200 Subject: [PATCH 8/9] docs(api): align the Workspaces operations with what is deployed - GET /api/workspaces/{id} accepts an API key or a platform token (cloud-api #325); the workspace key is refused on both routes. - A foreign workspace answers the same 404 as an unknown id (#324); a non-UUID id is a 404 from the edge. No 403 remains. - 401 examples are the real /api Auth bodies (unauthorized, invalid session, session_required), pinned by tests. - api.nan.builders serves these routes to bearer credentials only, with no cookies and no browser CORS (nan-devops #805). Co-Authored-By: Claude Opus 5.5 --- src/data/openapi.json | 62 ++++++++++++++++++------------------- src/lib/openapiRuns.test.ts | 46 ++++++++++++++++++++++----- 2 files changed, 69 insertions(+), 39 deletions(-) diff --git a/src/data/openapi.json b/src/data/openapi.json index 906fbc9..91e6e96 100644 --- a/src/data/openapi.json +++ b/src/data/openapi.json @@ -64,7 +64,7 @@ }, { "name": "Workspaces", - "description": "Read-only access to your own workspaces, for scripts that need a workspace's name, id or status. Served at `https://api.nan.builders/api/workspaces`, outside `/v1`. Creating, starting, stopping and deleting workspaces is done from the platform, not from this API. Guide: [Workspaces](https://nan.builders/docs/workspaces)." + "description": "Read-only access to your own workspaces, for scripts that need a workspace's name, id or status. Served at `https://api.nan.builders/api/workspaces`, outside `/v1`, to bearer credentials only (no cookies, no browser CORS). Creating, starting, stopping and deleting workspaces is done from the platform, not from this API. Guide: [Workspaces](https://nan.builders/docs/workspaces)." } ], "paths": { @@ -2498,7 +2498,7 @@ "platformToken": [] } ], - "description": "Your own workspaces, and only yours. No keys, environment values or agent configuration are ever included. Use it to find the `name` or `workspace_uuid` to pass as `workspace` when you create a run.\n\nAuthentication: a member API key (`sk-...`) or a platform token (`nan_pat_...`). The key that lives inside a workspace is not accepted.", + "description": "Your own workspaces, and only yours. No keys, environment values or agent configuration are ever included. Use it to find the `name` or `workspace_uuid` to pass as `workspace` when you create a run.\n\nAuthentication: a member API key (`sk-...`) or a platform token (`nan_pat_...`). The key that lives inside a workspace is not accepted.\n\nOn `api.nan.builders` this route takes a bearer credential only: cookies are ignored, and there is no browser CORS support (`OPTIONS` answers `404`), so call it from a server or a script, not from a web page.", "responses": { "200": { "description": "Your workspaces (a JSON array, empty if you have none).", @@ -2553,7 +2553,7 @@ } }, "401": { - "description": "No credentials (`unauthorized`), an unknown, revoked or expired one (`invalid session`), or the key that lives inside a workspace (`session_required`).", + "description": "No credentials (`unauthorized`); an unknown, revoked or expired API key or platform token (`invalid session`); or the key that lives inside a workspace (`session_required`).", "content": { "application/json": { "schema": { @@ -2604,11 +2604,14 @@ ], "summary": "Retrieve a workspace", "security": [ + { + "apiKey": [] + }, { "platformToken": [] } ], - "description": "One of your own workspaces, plus `env_var_keys`: the names (never the values) of the variables in its environment file. Only your own workspaces are returned: another member's workspace is never shown.\n\nAuthentication: a platform token (`nan_pat_...`). Member API keys (`sk-...`) are not accepted on this route: use the list instead.", + "description": "One of your own workspaces, plus `env_var_keys`: the names (never the values) of the variables in its environment file. Only your own workspaces are returned: another member's workspace answers `404`, exactly like an id that does not exist.\n\nAuthentication: a member API key (`sk-...`) or a platform token (`nan_pat_...`). The key that lives inside a workspace is not accepted.\n\nOn `api.nan.builders` this route takes a bearer credential only: cookies are ignored, and there is no browser CORS support (`OPTIONS` answers `404`), so call it from a server or a script, not from a web page.", "parameters": [ { "name": "id", @@ -2618,7 +2621,7 @@ "type": "string", "format": "uuid" }, - "description": "The `workspace_uuid`." + "description": "The `workspace_uuid`, in lowercase. Anything else is answered `404` before it reaches the API." } ], "responses": { @@ -2674,7 +2677,7 @@ } }, "401": { - "description": "No credentials (`unauthorized`), an unknown, revoked or expired one (`invalid session`), or an API key (`sk-...`), which this route does not accept (`session_required`).", + "description": "No credentials (`unauthorized`); an unknown, revoked or expired API key or platform token (`invalid session`); or the key that lives inside a workspace (`session_required`).", "content": { "application/json": { "schema": { @@ -2701,41 +2704,38 @@ } } }, - "403": { - "description": "The workspace belongs to another member (`forbidden`), or the id is not a UUID: a platform token is only accepted on a well-formed id (`token_scope_denied`).", + "404": { + "description": "No workspace of yours with that id: it does not exist, belongs to another member (the answer is identical), or the id is not a lowercase UUID (answered by the edge in the OpenAI error shape).", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PlatformError" + "oneOf": [ + { + "$ref": "#/components/schemas/PlatformError" + }, + { + "$ref": "#/components/schemas/Error" + } + ] }, "examples": { - "forbidden": { + "workspace_not_found": { "value": { - "error": "forbidden" + "error": "workspace not found" } }, - "token_scope_denied": { + "malformed_id": { "value": { - "error": "token_scope_denied", - "message": "Platform tokens only work on the platform API (/v1/runs and GET /api/workspaces). Use a signed-in session or an inference API key for this endpoint." + "error": { + "message": "Not found.", + "type": "invalid_request_error", + "code": "404" + } } } } } } - }, - "404": { - "description": "No workspace with that id.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PlatformError" - }, - "example": { - "error": "workspace not found" - } - } - } } }, "x-codeSamples": [ @@ -2753,7 +2753,7 @@ "apiKey": { "type": "http", "scheme": "bearer", - "description": "Member API key (`sk-...`), from \"API Keys\" in your settings on the platform. Send it as `Authorization: Bearer `. It works on every inference endpoint, on the Runs endpoints and on the workspace list. Personal and non-transferable. The key that lives inside a workspace (the one its agents use) is refused by the Runs endpoints with `403 workspace_key_not_allowed`." + "description": "Member API key (`sk-...`), from \"API Keys\" in your settings on the platform. Send it as `Authorization: Bearer `. It works on every inference endpoint, on the Runs endpoints and on the two Workspaces endpoints. Personal and non-transferable. The key that lives inside a workspace (the one its agents use) is refused by the Runs endpoints with `403 workspace_key_not_allowed`." }, "platformToken": { "type": "http", @@ -3698,15 +3698,15 @@ }, "PlatformError": { "type": "object", - "description": "Error body of the Workspaces endpoints (outside `/v1`): a short `error` string, plus `message` on `token_scope_denied` and `session_required`.", + "description": "Error body of the Workspaces endpoints (outside `/v1`): a short `error` string, plus `message` on `session_required`.", "properties": { "error": { "type": "string", - "description": "Short error, for example `unauthorized`, `invalid session`, `session_required`, `forbidden`, `workspace not found` or `token_scope_denied`." + "description": "Short error: `unauthorized`, `invalid session`, `session_required` or `workspace not found`." }, "message": { "type": "string", - "description": "Present on `token_scope_denied` and `session_required`: what the credential can be used for." + "description": "Present on `session_required`: what the credential can be used for." } }, "required": [ diff --git a/src/lib/openapiRuns.test.ts b/src/lib/openapiRuns.test.ts index 7f68c8c..755f433 100644 --- a/src/lib/openapiRuns.test.ts +++ b/src/lib/openapiRuns.test.ts @@ -146,10 +146,12 @@ describe('openapi.json: credentials', () => { expect(op(path, method).description).toContain('a member API key (`sk-...`) or a platform token (`nan_pat_...`)'); }); - it('the workspace list accepts both, one workspace by id only a platform token', () => { - expect(op('/api/workspaces', 'get').security).toEqual([{ apiKey: [] }, { platformToken: [] }]); - expect(op('/api/workspaces/{id}', 'get').security).toEqual([{ platformToken: [] }]); - expect(op('/api/workspaces/{id}', 'get').description).toMatch(/API keys \(`sk-\.\.\.`\) are not accepted/); + it('both Workspaces operations accept an API key or a platform token, never a workspace key (cloud-api #325)', () => { + for (const [path, method] of WS_OPS) { + expect(op(path, method).security).toEqual([{ apiKey: [] }, { platformToken: [] }]); + expect(op(path, method).description).toContain('a member API key (`sk-...`) or a platform token (`nan_pat_...`)'); + expect(op(path, method).description).toMatch(/key that lives inside a workspace is not accepted/); + } }); it('the overview explains platform tokens', () => { @@ -281,15 +283,43 @@ describe('openapi.json: the Workspaces contract (cloud-api)', () => { expect(JSON.stringify(s)).not.toContain('cloud-api.nan.builders'); }); - it('GET /api/workspaces/{id} with a token: 403 forbidden or token_scope_denied, never the handler 400', () => { + it('GET /api/workspaces/{id}: a foreign workspace is the same 404 as an unknown one, never 403 (cloud-api #324)', () => { const r = op('/api/workspaces/{id}', 'get').responses; - expect(Object.keys(r).sort()).toEqual(['200', '401', '403', '404']); - expect(Object.keys(r['403'].content['application/json'].examples)).toEqual(['forbidden', 'token_scope_denied']); + expect(Object.keys(r).sort()).toEqual(['200', '401', '404']); + expect(r['404'].content['application/json'].examples.workspace_not_found.value).toEqual({ error: 'workspace not found' }); + expect(r['404'].description).toMatch(/belongs to another member \(the answer is identical\)/); + // A non-UUID id never reaches the API on api.nan.builders: the edge answers 404 in the OpenAI shape. + expect(r['404'].content['application/json'].examples.malformed_id.value).toEqual({ + error: { message: 'Not found.', type: 'invalid_request_error', code: '404' }, + }); + expect(JSON.stringify(s.paths['/api/workspaces/{id}'])).not.toMatch(/forbidden|token_scope_denied/); + }); + + /** + * The 401 bodies of cloud-api's /api Auth (internal/middleware/auth.go), + * byte for byte: no credential, a credential that resolves to nobody, and a + * key the route refuses (the workspace key). + */ + it.each(WS_OPS)('%s %s: the 401 examples are the real bodies', (path, method) => { + const ex = op(path, method).responses['401'].content['application/json'].examples; + expect(Object.keys(ex)).toEqual(['unauthorized', 'invalid_session', 'session_required']); + expect(ex.unauthorized.value).toEqual({ error: 'unauthorized' }); + expect(ex.invalid_session.value).toEqual({ error: 'invalid session' }); + expect(ex.session_required.value).toEqual({ + error: 'session_required', + message: 'This endpoint requires a signed-in session. API keys only work for inference (https://api.nan.builders/v1).', + }); + }); + + it.each(WS_OPS)('%s %s: says api.nan.builders is bearer-only with no browser CORS', (path, method) => { + const d = op(path, method).description; + expect(d).toMatch(/bearer credential only: cookies are ignored/); + expect(d).toMatch(/no browser CORS support \(`OPTIONS` answers `404`\)/); }); it('only the caller\'s own workspaces, and never a secret', () => { expect(op('/api/workspaces', 'get').description).toMatch(/Your own workspaces, and only yours/); - expect(op('/api/workspaces/{id}', 'get').description).toMatch(/Only your own workspaces are returned/); + expect(op('/api/workspaces/{id}', 'get').description).toMatch(/Only your own workspaces are returned: another member's workspace answers `404`/); const props = Object.keys(s.components.schemas.Workspace.properties); for (const secret of ['agent_configs', 'hermes_env', 'member_uuid', 'api_key', 'token']) { expect(props).not.toContain(secret); From cd70a8655830b5cbad313563e94d0ef75044d600 Mon Sep 17 00:00:00 2001 From: barckcode Date: Sat, 10 Oct 2026 23:29:19 +0200 Subject: [PATCH 9/9] fix(api-doc): type path-level servers and print them in the text rendering The Workspaces operations override the spec-wide /v1 base URL with a path-level servers entry (OpenAPI 3 allows it). The Spec type in openapiToText did not, so astro check failed in CI; and the text rendering served at /api/docs would have sent agents to /v1/api/workspaces. Type the Path Item with optional servers and print the override per operation. Co-Authored-By: Claude Opus 5.5 --- src/lib/openapiToText.ts | 28 +++++++++++++++++++----- src/lib/openapiToTextPathServers.test.ts | 27 +++++++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 src/lib/openapiToTextPathServers.test.ts diff --git a/src/lib/openapiToText.ts b/src/lib/openapiToText.ts index f2d2dc7..f812df3 100644 --- a/src/lib/openapiToText.ts +++ b/src/lib/openapiToText.ts @@ -28,13 +28,22 @@ interface Spec { info?: { title?: string; description?: string; version?: string }; servers?: Array<{ url?: string; description?: string }>; tags?: Array<{ name?: string; description?: string }>; - paths?: Record>; + paths?: Record; components?: { schemas?: Record; responses?: Record; }; } +/** + * A Path Item: its operations keyed by HTTP method, plus an optional + * path-level `servers` override (OpenAPI 3: e.g. /api/workspaces lives on + * https://api.nan.builders, outside the spec-wide /v1 base URL). + */ +type PathItem = { servers?: Array<{ url?: string; description?: string }> } & { + [method: string]: Operation | Array<{ url?: string; description?: string }> | undefined; +}; + interface Operation { operationId?: string; summary?: string; @@ -196,11 +205,19 @@ function responseDescription(spec: Spec, res: ResponseOrRef | undefined): string return ''; } -function renderOperation(spec: Spec, path: string, method: string, op: Operation): string { +function renderOperation( + spec: Spec, + path: string, + method: string, + op: Operation, + pathServer?: string, +): string { const out: string[] = []; const title = op.summary || op.operationId || `${method.toUpperCase()} ${path}`; out.push(`### ${method.toUpperCase()} ${path}: ${title}`); + // A path-level server overrides the spec-wide base URL for this operation. + if (pathServer && pathServer !== spec.servers?.[0]?.url) out.push(`Base URL: \`${pathServer}\``); if (op.deprecated) out.push('**Deprecated.**'); // The operation is a `###`, so its subsections drop to `#####` so they do // not compete with the `####` this same function emits (Parameters, etc.). @@ -297,15 +314,16 @@ export function openapiToText(spec: Spec): string { const tagOrder: string[] = (spec.tags ?? []).map((t) => t.name ?? '').filter(Boolean); for (const [path, item] of Object.entries(spec.paths ?? {})) { + const pathServer = item?.servers?.[0]?.url; for (const method of HTTP_METHODS) { - const op = item?.[method]; - if (!op) continue; + const op = item?.[method] as Operation | undefined; + if (!op || Array.isArray(op)) continue; const tag = op.tags?.[0] || 'Endpoints'; if (!byTag.has(tag)) { byTag.set(tag, []); if (!tagOrder.includes(tag)) tagOrder.push(tag); } - byTag.get(tag)!.push(renderOperation(spec, path, method, op)); + byTag.get(tag)!.push(renderOperation(spec, path, method, op, pathServer)); } } diff --git a/src/lib/openapiToTextPathServers.test.ts b/src/lib/openapiToTextPathServers.test.ts new file mode 100644 index 0000000..31f787d --- /dev/null +++ b/src/lib/openapiToTextPathServers.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; +import rawSpec from '../data/openapi.json'; +import { openapiToText } from './openapiToText'; + +// /api/workspaces lives on https://api.nan.builders, outside the spec-wide +// https://api.nan.builders/v1 base URL, through a path-level `servers` +// override. The text rendering served at /api/docs must not send agents to +// /v1/api/workspaces, a route that does not exist. +describe('openapiToText: path-level servers', () => { + const text = openapiToText(rawSpec as Parameters[0]); + + it('prints the override for the workspace operations', () => { + for (const path of ['/api/workspaces', '/api/workspaces/{id}']) { + const start = text.indexOf(`### GET ${path}:`); + expect(start).toBeGreaterThan(-1); + const section = text.slice(start, text.indexOf('\n### ', start + 1) === -1 ? undefined : text.indexOf('\n### ', start + 1)); + expect(section).toContain('Base URL: `https://api.nan.builders`'); + } + }); + + it('does not print a per-operation base URL for operations on the default server', () => { + const start = text.indexOf('### POST /runs:'); + expect(start).toBeGreaterThan(-1); + const end = text.indexOf('\n### ', start + 1); + expect(text.slice(start, end === -1 ? undefined : end)).not.toContain('Base URL:'); + }); +});