From a7d57b5b554390b55e06e0edaead27d517756077 Mon Sep 17 00:00:00 2001 From: borjaperfra Date: Tue, 13 Oct 2026 16:31:04 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(docs):=20la=20p=C3=A1gina=20de=20Codex?= =?UTF-8?q?=20mandaba=20poner=20wire=5Fapi=20=3D=20"chat",=20que=20ya=20no?= =?UTF-8?q?=20arranca?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex 0.154 retiró "chat": con él el fichero no carga y ni la CLI ni la app abren. El /responses del clúster ya emite por partes con todos los modelos, que era la razón para recomendar "chat". La página pasa a "responses" con model_context_window, explica los perfiles por modelo que escribe el CLI, y los problemas conocidos recogen el 401 de un modelo retirado (mimo-v2.5 ya es mimo-v2.6-flash; el CLI 0.1.30 cambia el perfil). Sustituye a #93, que se había quedado atrás respecto a main. Co-Authored-By: Claude Opus 5.5 --- src/content/docs-es/codex.mdx | 32 +++++++++++++++++++++++++++----- src/content/docs/codex.mdx | 32 +++++++++++++++++++++++++++----- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/content/docs-es/codex.mdx b/src/content/docs-es/codex.mdx index 3dca3ac..e85e9f7 100644 --- a/src/content/docs-es/codex.mdx +++ b/src/content/docs-es/codex.mdx @@ -32,17 +32,19 @@ Edita `~/.codex/config.toml`: ```toml model_provider = "nan" model = "glm5.3-flash" +model_context_window = 1048576 [model_providers.nan] name = "NaN" base_url = "https://api.nan.builders/v1" env_key = "NAN_API_KEY" -wire_api = "chat" +wire_api = "responses" ``` -Tres detalles que importan: +Cuatro detalles que importan: -- **`wire_api = "chat"`** hace que Codex use `/chat/completions`. Es lo que quieres con la mayoría de modelos: el endpoint `/responses` del clúster solo va emitiendo la respuesta por partes con `deepseek-v4-flash`, y con los demás contesta de una sola vez, así que con `"responses"` verías la respuesta aparecer de golpe al final. +- **`wire_api = "responses"`** es el único valor que Codex sigue aceptando. Desde la 0.154, `"chat"` no es que vaya peor: el fichero no carga y Codex no abre. Esta página recomendaba `"chat"` porque `/responses` contestaba de una sola vez; ahora el `/responses` del clúster emite la respuesta por partes con todos los modelos, así que esa razón ya no existe. +- **`model_context_window`** hace falta porque Codex no tiene metadatos de los modelos del clúster: sin él supone una ventana y compacta la conversación mucho antes de tiempo. Pon la del modelo que uses; están en [Modelos](/es/docs/models). - **El identificador del proveedor no puede ser `openai`, `ollama` ni `lmstudio`**, que están reservados. Por eso se llama `nan`. - **`base_url` termina en `/v1`** y nada más. No añadas la ruta del endpoint. @@ -64,7 +66,24 @@ Puedes cambiarlo sin tocar el fichero: codex --model deepseek-v4-flash ``` -O dejar varios proveedores declarados y elegir con `--profile` si prefieres perfiles separados por tarea. +Con una pega: la `model_context_window` del fichero no cambia con el modelo, así que un modelo de 262.144 tokens se queda con la ventana del que tengas declarado, y Codex compacta contra una ventana que no existe. + +Para tener los modelos del clúster a mano, cada uno con su ventana, escribe un fichero por modelo en `~/.codex/`. Codex lo llama perfil: tu `config.toml` con ese fichero encima. + +```toml +# ~/.codex/nan-qwen36.config.toml +model = "qwen3.6" +model_provider = "nan" +model_context_window = 262144 +``` + +```bash +codex --profile nan-qwen36 +``` + +Ojo con el nombre: Codex quiere "un nombre simple" y **rechaza los puntos**, así que `qwen3.6` no vale como perfil aunque sí como modelo. + +El resto del `config.toml` (tu proveedor, tus MCP, tus permisos) sigue aplicando; el perfil solo pisa lo que nombra. El [CLI de NaN](/es/docs/nan-cli) los escribe por ti, uno por cada modelo de chat del clúster, con el prefijo `nan-` y sin los puntos: `nan-glm53-flash`, `nan-deepseek-v4-flash`, `nan-mimo-v26-flash`, `nan-qwen36`... Al desconectar Codex se los lleva. ## Modelo recomendado @@ -74,7 +93,10 @@ O dejar varios proveedores declarados y elegir con `--profile` si prefieres perf - **Las funciones en la nube de Codex no aplican.** Al declarar un proveedor propio, todo va contra NaN desde tu máquina. - **El razonamiento se ve distinto según el modelo.** Los modelos del clúster emiten su traza de razonamiento a su manera, y Codex no siempre la presenta como con los modelos de OpenAI. -- **Si cambias `wire_api` a `"responses"`**, solo `deepseek-v4-flash` sigue mostrando la respuesta poco a poco. Con los demás modelos aparece de golpe al final. No es un cuelgue: es que con esos modelos ese endpoint todavía no emite la respuesta por partes. +- **`wire_api = "chat"` en un fichero antiguo.** Codex 0.154 lo retiró: si lo tienes, ni la CLI ni la app de escritorio abren. Cámbialo a `"responses"`, o pasa por el Setup del CLI de NaN, que lo corrige solo. +- **`model_context_window` repetida.** Las versiones del CLI de NaN anteriores a la 0.1.20 la añadían al final del fichero en cada pasada del setup. Dos son una clave duplicada y Codex no carga el fichero: la CLI dice `duplicate key` y la app abre un diálogo de error y nada más. La 0.1.20 deja una sola y la pone donde se lee. +- **`401 This API key does not have access to the requested model`.** Pese al mensaje, la clave suele estar bien: el modelo es uno que el clúster no sirve. O el `model_provider` apunta al clúster pero el `model` sigue siendo de OpenAI, o el modelo se ha retirado: `mimo-v2.5` ahora es `mimo-v2.6-flash`. Comprueba el id en [Modelos](/es/docs/models). El CLI de NaN 0.1.30 cambia el perfil `nan-mimo-v25` por `nan-mimo-v26-flash` la próxima vez que pases por su Setup. +- **`Model metadata for ... not found. Defaulting to fallback metadata`** en cada turno. Es ruido: Codex lo imprime antes de leer tu `model_context_window`, y la ventana que acaba usando es la tuya. diff --git a/src/content/docs/codex.mdx b/src/content/docs/codex.mdx index 71149f2..f8689d4 100644 --- a/src/content/docs/codex.mdx +++ b/src/content/docs/codex.mdx @@ -32,17 +32,19 @@ Edit `~/.codex/config.toml`: ```toml model_provider = "nan" model = "glm5.3-flash" +model_context_window = 1048576 [model_providers.nan] name = "NaN" base_url = "https://api.nan.builders/v1" env_key = "NAN_API_KEY" -wire_api = "chat" +wire_api = "responses" ``` -Three details that matter: +Four details that matter: -- **`wire_api = "chat"`** makes Codex use `/chat/completions`. That is what you want with most models: the cluster's `/responses` endpoint streams incrementally only on `deepseek-v4-flash`, and on the others it answers in one go, so with `"responses"` you would see the answer appear all at once at the end. +- **`wire_api = "responses"`** is the only value Codex still takes. Since 0.154, `"chat"` does not just work worse: the file does not load and Codex does not open. This page used to recommend `"chat"` because `/responses` answered in one go; the cluster's `/responses` streams the answer on every model now, so that reason is gone. +- **`model_context_window`** is needed because Codex has no metadata for the cluster's models: without it, it guesses a window and compacts the conversation long before it has to. Use the one for the model you run; they are in [Models](/docs/models). - **The provider identifier cannot be `openai`, `ollama` or `lmstudio`**, which are reserved. That is why it is called `nan`. - **`base_url` ends at `/v1`** and nothing more. Do not add the endpoint path. @@ -64,7 +66,24 @@ You can change it without touching the file: codex --model deepseek-v4-flash ``` -Or leave several providers declared and pick with `--profile` if you prefer separate profiles per task. +With one catch: the `model_context_window` in the file does not follow the model, so a 262,144-token model keeps the window of whichever one you declared, and Codex compacts against a window that is not there. + +To keep the cluster's models at hand, each with its own window, write one file per model in `~/.codex/`. Codex calls it a profile: your `config.toml` with that file layered on top. + +```toml +# ~/.codex/nan-qwen36.config.toml +model = "qwen3.6" +model_provider = "nan" +model_context_window = 262144 +``` + +```bash +codex --profile nan-qwen36 +``` + +Mind the name: Codex wants "a plain name" and **rejects dots**, so `qwen3.6` is not usable as a profile even though it is fine as a model. + +The rest of your `config.toml` - your provider, your MCP servers, your permissions - still applies; the profile only overrides what it names. The [NaN CLI](/docs/nan-cli) writes them for you, one per chat model on the cluster, prefixed `nan-` and with the dots dropped: `nan-glm53-flash`, `nan-deepseek-v4-flash`, `nan-mimo-v26-flash`, `nan-qwen36`... Disconnecting Codex takes them away again. ## Recommended model @@ -74,6 +93,9 @@ Or leave several providers declared and pick with `--profile` if you prefer sepa - **Codex's cloud features do not apply.** Once you declare a provider of your own, everything goes to NaN from your machine. - **Reasoning looks different depending on the model.** The cluster's models emit their reasoning trace their own way, and Codex does not always present it the way it does with OpenAI's models. -- **If you change `wire_api` to `"responses"`**, only `deepseek-v4-flash` keeps showing the answer gradually. With the other models it appears all at once at the end. It is not a hang: on those models that endpoint does not stream yet. +- **`wire_api = "chat"` in an older file.** Codex 0.154 dropped it: if you have it, neither the CLI nor the desktop app opens. Change it to `"responses"`, or go through the NaN CLI's Setup, which repairs it for you. +- **A repeated `model_context_window`.** NaN CLI versions before 0.1.20 appended one at the end of the file on every setup run. Two of them is a duplicate key and Codex will not load the file: the CLI says `duplicate key` and the desktop app opens an error dialog and nothing else. 0.1.20 leaves one and puts it where it is read from. +- **`401 This API key does not have access to the requested model`.** Despite the wording, the key is usually fine: the model is one the cluster does not serve. Either `model_provider` points at the cluster but `model` is still an OpenAI one, or the model has been retired - `mimo-v2.5` is now `mimo-v2.6-flash`. Check the id against [Models](/docs/models). NaN CLI 0.1.30 swaps the `nan-mimo-v25` profile for `nan-mimo-v26-flash` the next time you run its Setup. +- **`Model metadata for ... not found. Defaulting to fallback metadata`** on every turn. It is noise: Codex prints it before it reads your `model_context_window`, and the window it ends up using is yours. From 04cae37c14746036cfa6d4be18cc6fe83c7c21bb Mon Sep 17 00:00:00 2001 From: borjaperfra Date: Tue, 13 Oct 2026 16:42:33 +0200 Subject: [PATCH 2/2] =?UTF-8?q?docs(api):=20/responses=20con=20los=20siete?= =?UTF-8?q?=20modelos=20de=20chat,=20y=20cu=C3=A1les=20emiten=20por=20part?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-medido el 13-oct contra el clúster: los siete modelos de chat contestan en /responses. deepseek-v4-flash, glm5.3-flash, qwen3.8-flash, mimo-v2.6-flash y glm5.3 emiten la respuesta por partes; qwen3.6 y gemma4 la siguen reteniendo (~59 deltas en 0,1 s tras 15-17 s sin nada). La referencia decía que solo había tres modelos y que solo deepseek-v4-flash emitía por partes. La página de Codex decía que emitía con todos los modelos, que no es verdad para esos dos: ahora lo dice, y los problemas conocidos explican que no es un cuelgue. Los tests fijan lo nuevo en lugar de la frase vieja. Co-Authored-By: Claude Opus 5.5 --- src/content/docs-es/codex.mdx | 3 ++- src/content/docs/codex.mdx | 3 ++- src/data/openapi.json | 10 +++++++--- src/lib/openapiSpec.test.ts | 37 ++++++++++++++++++++++++++--------- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/content/docs-es/codex.mdx b/src/content/docs-es/codex.mdx index e85e9f7..5c9dc52 100644 --- a/src/content/docs-es/codex.mdx +++ b/src/content/docs-es/codex.mdx @@ -43,7 +43,7 @@ wire_api = "responses" Cuatro detalles que importan: -- **`wire_api = "responses"`** es el único valor que Codex sigue aceptando. Desde la 0.154, `"chat"` no es que vaya peor: el fichero no carga y Codex no abre. Esta página recomendaba `"chat"` porque `/responses` contestaba de una sola vez; ahora el `/responses` del clúster emite la respuesta por partes con todos los modelos, así que esa razón ya no existe. +- **`wire_api = "responses"`** es el único valor que Codex sigue aceptando. Desde la 0.154, `"chat"` no es que vaya peor: el fichero no carga y Codex no abre. Esta página recomendaba `"chat"` porque `/responses` contestaba de una sola vez; ahora emite la respuesta por partes, salvo con `qwen3.6` y `gemma4` (mira abajo). - **`model_context_window`** hace falta porque Codex no tiene metadatos de los modelos del clúster: sin él supone una ventana y compacta la conversación mucho antes de tiempo. Pon la del modelo que uses; están en [Modelos](/es/docs/models). - **El identificador del proveedor no puede ser `openai`, `ollama` ni `lmstudio`**, que están reservados. Por eso se llama `nan`. - **`base_url` termina en `/v1`** y nada más. No añadas la ruta del endpoint. @@ -96,6 +96,7 @@ El resto del `config.toml` (tu proveedor, tus MCP, tus permisos) sigue aplicando - **`wire_api = "chat"` en un fichero antiguo.** Codex 0.154 lo retiró: si lo tienes, ni la CLI ni la app de escritorio abren. Cámbialo a `"responses"`, o pasa por el Setup del CLI de NaN, que lo corrige solo. - **`model_context_window` repetida.** Las versiones del CLI de NaN anteriores a la 0.1.20 la añadían al final del fichero en cada pasada del setup. Dos son una clave duplicada y Codex no carga el fichero: la CLI dice `duplicate key` y la app abre un diálogo de error y nada más. La 0.1.20 deja una sola y la pone donde se lee. - **`401 This API key does not have access to the requested model`.** Pese al mensaje, la clave suele estar bien: el modelo es uno que el clúster no sirve. O el `model_provider` apunta al clúster pero el `model` sigue siendo de OpenAI, o el modelo se ha retirado: `mimo-v2.5` ahora es `mimo-v2.6-flash`. Comprueba el id en [Modelos](/es/docs/models). El CLI de NaN 0.1.30 cambia el perfil `nan-mimo-v25` por `nan-mimo-v26-flash` la próxima vez que pases por su Setup. +- **Con `qwen3.6` y `gemma4` la respuesta sigue llegando de golpe** al final del turno. No es un cuelgue: con esos dos el `/responses` del clúster retiene la respuesta hasta tenerla completa. El resto de modelos la va emitiendo según se escribe. - **`Model metadata for ... not found. Defaulting to fallback metadata`** en cada turno. Es ruido: Codex lo imprime antes de leer tu `model_context_window`, y la ventana que acaba usando es la tuya. diff --git a/src/content/docs/codex.mdx b/src/content/docs/codex.mdx index f8689d4..7e72ac9 100644 --- a/src/content/docs/codex.mdx +++ b/src/content/docs/codex.mdx @@ -43,7 +43,7 @@ wire_api = "responses" Four details that matter: -- **`wire_api = "responses"`** is the only value Codex still takes. Since 0.154, `"chat"` does not just work worse: the file does not load and Codex does not open. This page used to recommend `"chat"` because `/responses` answered in one go; the cluster's `/responses` streams the answer on every model now, so that reason is gone. +- **`wire_api = "responses"`** is the only value Codex still takes. Since 0.154, `"chat"` does not just work worse: the file does not load and Codex does not open. This page used to recommend `"chat"` because `/responses` answered in one go; it streams the answer now, except on `qwen3.6` and `gemma4` (see below). - **`model_context_window`** is needed because Codex has no metadata for the cluster's models: without it, it guesses a window and compacts the conversation long before it has to. Use the one for the model you run; they are in [Models](/docs/models). - **The provider identifier cannot be `openai`, `ollama` or `lmstudio`**, which are reserved. That is why it is called `nan`. - **`base_url` ends at `/v1`** and nothing more. Do not add the endpoint path. @@ -96,6 +96,7 @@ The rest of your `config.toml` - your provider, your MCP servers, your permissio - **`wire_api = "chat"` in an older file.** Codex 0.154 dropped it: if you have it, neither the CLI nor the desktop app opens. Change it to `"responses"`, or go through the NaN CLI's Setup, which repairs it for you. - **A repeated `model_context_window`.** NaN CLI versions before 0.1.20 appended one at the end of the file on every setup run. Two of them is a duplicate key and Codex will not load the file: the CLI says `duplicate key` and the desktop app opens an error dialog and nothing else. 0.1.20 leaves one and puts it where it is read from. - **`401 This API key does not have access to the requested model`.** Despite the wording, the key is usually fine: the model is one the cluster does not serve. Either `model_provider` points at the cluster but `model` is still an OpenAI one, or the model has been retired - `mimo-v2.5` is now `mimo-v2.6-flash`. Check the id against [Models](/docs/models). NaN CLI 0.1.30 swaps the `nan-mimo-v25` profile for `nan-mimo-v26-flash` the next time you run its Setup. +- **On `qwen3.6` and `gemma4` the answer still arrives all at once** at the end of the turn. It is not a hang: on those two the cluster's `/responses` holds the answer until it is complete. Every other model streams it as it is written. - **`Model metadata for ... not found. Defaulting to fallback metadata`** on every turn. It is noise: Codex prints it before it reads your `model_context_window`, and the window it ends up using is yours. diff --git a/src/data/openapi.json b/src/data/openapi.json index 9a188cd..26511a9 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 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), [Automations](#tag/automations) and [Approvals](#tag/approvals), 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`. An API key works on both inference and these endpoints. Each endpoint lists the credentials it accepts.\n\nA platform token carries **scopes**, and each endpoint names the one it needs: `runs` (runs and approvals), `workspaces:read`, `automations:read` (read automations, templates, triggers and deliveries) and `automations:run` (run an existing automation). New tokens get `runs` and `workspaces:read`. A token without the scope gets `403`:\n\n```json\n{\n \"error\": {\n \"message\": \"This platform token does not have the \\\"automations:run\\\" scope this endpoint requires. Create a token with that scope in the Tokens section of Settings at cloud.nan.builders.\",\n \"type\": \"permission_error\",\n \"param\": null,\n \"code\": \"token_scope_denied\"\n }\n}\n```\n\nA platform token never works for inference: the model endpoints answer `401`:\n\n```json\n{\n \"error\": {\n \"message\": \"Platform tokens (nan_pat_) cannot be used for inference. Use an inference API key (sk-) instead. Platform tokens only work on the platform API (/v1/runs and GET /api/workspaces).\",\n \"type\": \"invalid_request_error\",\n \"param\": null,\n \"code\": \"token_not_valid_for_inference\"\n }\n}\n```\n\n### Browser sign-in\n\nSome actions need you signed in through your browser at [cloud.nan.builders](https://cloud.nan.builders/), and no API key, platform token or CLI session can do them: approving a run, creating, editing or deleting automations and their triggers, writing secrets, and creating a token with an `automations:` scope. Through the API they answer `403`:\n\n```json\n{\n \"error\": {\n \"message\": \"This action needs a browser session: sign in at cloud.nan.builders in your browser and try again from there. CLI sessions, API keys and platform tokens cannot approve runs, change automations or secrets, or create tokens with automations scopes.\",\n \"type\": \"permission_error\",\n \"param\": null,\n \"code\": \"browser_session_required\"\n }\n}\n```\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. A platform token (`nan_pat_...`) sent to an inference endpoint answers `token_not_valid_for_inference`. | `invalid_api_key` · `token_not_valid_for_inference` |\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. On the platform endpoints: a platform token without the scope the endpoint needs, or an action that needs a [browser sign-in](#description/browser-sign-in). | `tier_restricted` · `token_scope_denied` · `browser_session_required` |\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), [Automations](#tag/automations) and [Approvals](#tag/approvals), 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`. An API key works on both inference and these endpoints. Each endpoint lists the credentials it accepts.\n\nA platform token carries **scopes**, and each endpoint names the one it needs: `runs` (runs and approvals), `workspaces:read`, `automations:read` (read automations, templates, triggers and deliveries) and `automations:run` (run an existing automation). New tokens get `runs` and `workspaces:read`. A token without the scope gets `403`:\n\n```json\n{\n \"error\": {\n \"message\": \"This platform token does not have the \\\"automations:run\\\" scope this endpoint requires. Create a token with that scope in the Tokens section of Settings at cloud.nan.builders.\",\n \"type\": \"permission_error\",\n \"param\": null,\n \"code\": \"token_scope_denied\"\n }\n}\n```\n\nA platform token never works for inference: the model endpoints answer `401`:\n\n```json\n{\n \"error\": {\n \"message\": \"Platform tokens (nan_pat_) cannot be used for inference. Use an inference API key (sk-) instead. Platform tokens only work on the platform API (/v1/runs and GET /api/workspaces).\",\n \"type\": \"invalid_request_error\",\n \"param\": null,\n \"code\": \"token_not_valid_for_inference\"\n }\n}\n```\n\n### Browser sign-in\n\nSome actions need you signed in through your browser at [cloud.nan.builders](https://cloud.nan.builders/), and no API key, platform token or CLI session can do them: approving a run, creating, editing or deleting automations and their triggers, writing secrets, and creating a token with an `automations:` scope. Through the API they answer `403`:\n\n```json\n{\n \"error\": {\n \"message\": \"This action needs a browser session: sign in at cloud.nan.builders in your browser and try again from there. CLI sessions, API keys and platform tokens cannot approve runs, change automations or secrets, or create tokens with automations scopes.\",\n \"type\": \"permission_error\",\n \"param\": null,\n \"code\": \"browser_session_required\"\n }\n}\n```\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 on `deepseek-v4-flash`, `glm5.3-flash`, `qwen3.8-flash`, `mimo-v2.6-flash` and `glm5.3`; 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. A platform token (`nan_pat_...`) sent to an inference endpoint answers `token_not_valid_for_inference`. | `invalid_api_key` · `token_not_valid_for_inference` |\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. On the platform endpoints: a platform token without the scope the endpoint needs, or an action that needs a [browser sign-in](#description/browser-sign-in). | `tier_restricted` · `token_scope_denied` · `browser_session_required` |\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" @@ -1029,7 +1029,7 @@ "Responses" ], "summary": "Create response", - "description": "Creates a model response using the OpenAI-style Responses API. Models: `deepseek-v4-flash`, `qwen3.6`, `gemma4`. On `deepseek-v4-flash` the `output` array carries a `reasoning` item before the `message`.\n\nStreaming (`stream: true`) depends on the model. `deepseek-v4-flash` streams incrementally, with the full event sequence: `response.created`, the reasoning summary deltas, `response.output_text.delta` as the answer is generated, and `response.completed`. On `qwen3.6` and `gemma4` the answer is held until it is complete and then arrives in one burst (`response.output_text.delta` events followed by `response.completed`); for token-by-token streaming on those two, use [Create chat completion](#tag/Chat) with `stream: true`.", + "description": "Creates a model response using the OpenAI-style Responses API. Models: `deepseek-v4-flash`, `glm5.3-flash`, `qwen3.8-flash`, `mimo-v2.6-flash`, `qwen3.6`, `gemma4`, `glm5.3`. On `deepseek-v4-flash` the `output` array carries a `reasoning` item before the `message`.\n\nStreaming (`stream: true`) depends on the model. `deepseek-v4-flash`, `glm5.3-flash`, `qwen3.8-flash`, `mimo-v2.6-flash` and `glm5.3` stream incrementally, with the full event sequence: `response.created`, the reasoning summary deltas, `response.output_text.delta` as the answer is generated, and `response.completed`. On `qwen3.6` and `gemma4` the answer is held until it is complete and then arrives in one burst (`response.output_text.delta` events followed by `response.completed`); for token-by-token streaming on those two, use [Create chat completion](#tag/Chat) with `stream: true`.", "requestBody": { "required": true, "content": { @@ -1045,8 +1045,12 @@ "type": "string", "enum": [ "deepseek-v4-flash", + "glm5.3-flash", + "qwen3.8-flash", + "mimo-v2.6-flash", "qwen3.6", - "gemma4" + "gemma4", + "glm5.3" ], "description": "The model to use.", "example": "qwen3.6" diff --git a/src/lib/openapiSpec.test.ts b/src/lib/openapiSpec.test.ts index 451bdbd..577755d 100644 --- a/src/lib/openapiSpec.test.ts +++ b/src/lib/openapiSpec.test.ts @@ -720,14 +720,30 @@ describe('documented /responses: models and streaming', () => { const op = (spec.paths as any)['/responses'].post; const body = op.requestBody.content['application/json'].schema.properties; - it('lists deepseek-v4-flash next to qwen3.6 and gemma4, in text and in the enum', () => { - expect(op.description).toContain('Models: `deepseek-v4-flash`, `qwen3.6`, `gemma4`.'); - expect(body.model.enum).toEqual(['deepseek-v4-flash', 'qwen3.6', 'gemma4']); + // Re-measured 2026-10-13: every chat model answers on /responses, and five + // of them stream; qwen3.6 and gemma4 still hold the answer and send it in + // one burst (~59 deltas inside 0.1s, after 15-17s of nothing). + const RESPONSES_MODELS = [ + 'deepseek-v4-flash', + 'glm5.3-flash', + 'qwen3.8-flash', + 'mimo-v2.6-flash', + 'qwen3.6', + 'gemma4', + 'glm5.3', + ]; + + it('lists every chat model, in text and in the enum', () => { + expect(op.description).toContain( + 'Models: ' + RESPONSES_MODELS.map((m) => '`' + m + '`').join(', ') + '.', + ); + expect(body.model.enum).toEqual(RESPONSES_MODELS); for (const id of body.model.enum) expect(NAN_MODELS, id).toContain(id); }); it('says which models stream incrementally and which send one burst', () => { - expect(op.description).toMatch(/`deepseek-v4-flash` streams incrementally/); + expect(op.description).toMatch(/`glm5\.3` stream incrementally/); + expect(op.description).toContain('`mimo-v2.6-flash`'); expect(op.description).toContain('`response.created`'); expect(op.description).toMatch(/On `qwen3\.6` and `gemma4` the answer is held until it is complete/); expect(op.description).toContain('[Create chat completion](#tag/Chat)'); @@ -737,8 +753,9 @@ describe('documented /responses: models and streaming', () => { it('drops the stale single-terminal-event claim everywhere', () => { expect(raw).not.toMatch(/single terminal event/); - expect(spec.info.description).toContain( - '`/responses` streams incrementally only on `deepseek-v4-flash`', + expect(spec.info.description).not.toContain('incrementally only on'); + expect(spec.info.description).toMatch( + /`\/responses` streams incrementally on `deepseek-v4-flash`.*; on `qwen3\.6` and `gemma4` it holds/, ); }); @@ -748,9 +765,11 @@ describe('documented /responses: models and streaming', () => { resolve(dirname(fileURLToPath(import.meta.url)), `../content/docs${locale}/codex.mdx`), 'utf-8', ); - expect(page('')).toContain('streams incrementally only on `deepseek-v4-flash`'); - expect(page('')).not.toContain('It is not a hang: that endpoint does not stream yet.'); - expect(page('-es')).toContain('solo va emitiendo la respuesta por partes con `deepseek-v4-flash`'); + expect(page('')).toContain('`wire_api = "responses"`'); + expect(page('')).not.toContain('wire_api = "chat"\n'); + expect(page('')).toContain('On `qwen3.6` and `gemma4` the answer still arrives all at once'); + expect(page('-es')).toContain('`wire_api = "responses"`'); + expect(page('-es')).toContain('Con `qwen3.6` y `gemma4` la respuesta sigue llegando de golpe'); }); });