From fa872589fac091d9aea26a728e3bc56c0a93ee7a Mon Sep 17 00:00:00 2001 From: olddonkey Date: Sat, 8 Aug 2026 13:01:35 -0700 Subject: [PATCH 1/3] feat(providers): add Chutes preset --- .../docs/getting-started/quickstart.md | 2 +- .../src/content/docs/guides/providers.md | 11 +- .../docs/ja/getting-started/quickstart.md | 2 +- .../src/content/docs/ja/guides/providers.md | 10 +- .../docs/ko/getting-started/quickstart.md | 2 +- .../src/content/docs/ko/guides/providers.md | 10 +- .../docs/ru/getting-started/quickstart.md | 2 +- .../src/content/docs/ru/guides/providers.md | 11 +- .../docs/zh-cn/getting-started/quickstart.md | 2 +- .../content/docs/zh-cn/guides/providers.md | 9 +- src/providers/registry.ts | 34 +++ tests/chutes-provider.test.ts | 221 ++++++++++++++++++ tests/fixtures/chutes-models.json | 51 ++++ tests/provider-registry-parity.test.ts | 2 +- 14 files changed, 357 insertions(+), 12 deletions(-) create mode 100644 tests/chutes-provider.test.ts create mode 100644 tests/fixtures/chutes-models.json diff --git a/docs-site/src/content/docs/getting-started/quickstart.md b/docs-site/src/content/docs/getting-started/quickstart.md index a45def328..4264d025b 100644 --- a/docs-site/src/content/docs/getting-started/quickstart.md +++ b/docs-site/src/content/docs/getting-started/quickstart.md @@ -13,7 +13,7 @@ ocx init `ocx init` walks you through: -1. **Pick a provider** — choose one of the 76 built-in registry presets or `custom` to type a base +1. **Pick a provider** — choose one of the 79 built-in registry presets or `custom` to type a base URL and adapter. 2. **API key** — paste a key, or reference an environment variable like `${ANTHROPIC_API_KEY}`. 3. **Default model** — for key, local, and custom providers, accept the preset or enter a model id. diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 07494f6a0..f737f2ca9 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -217,7 +217,7 @@ selectors, then retry. Signing in from a machine with no existing `kiro-cli` ses ## 3. API-key catalog -opencodex ships 76 built-in presets: 64 key-based, eight OAuth, three local, and one default +opencodex ships 79 built-in presets: 67 key-based, eight OAuth, three local, and one default ChatGPT-forward preset. The dashboard's **Add provider** picker opens a key provider's dashboard, validates the key, and stores it; validation is provider-specific. Notable entries: @@ -249,6 +249,7 @@ free-experimentation model. | MiniMax · MiniMax (CN) | `https://api.minimax.io/v1` · `https://api.minimaxi.com/v1` | | DeepSeek | `https://api.deepseek.com` | | Cerebras | `https://api.cerebras.ai/v1` | +| Chutes | `https://llm.chutes.ai/v1` | | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | | Nscale Serverless Inference | `https://inference.api.nscale.com/v1` | @@ -307,6 +308,13 @@ Volcengine Agent Plan uses its native Responses endpoint through `openai-respons > opencodex is the documented use; pointing other automation at a plan key is not. The > pay-as-you-go `volcengine` route carries no such restriction. +**Chutes discovery.** The `chutes` preset uses Chutes' fixed shared OpenAI-compatible LLM gateway. +It reads the public `/v1/models` catalog, keeps only rows whose `supported_features` advertise +`tools`, preserves slash-containing model ids and safe live metadata, and caps discovery at 256 KiB +and 128 raw rows. Because that catalog is public, it cannot prove a supplied key is valid; chat +requests still use the configured Bearer key. User-deployed custom Chute hosts and Chutes' non-LLM +APIs remain custom-provider territory. Create a key from the [Chutes dashboard](https://chutes.ai/auth/start). + **DeepInfra discovery.** The key-based `deepinfra` OpenAI Chat Completions provider uses the `openai-chat` adapter with a Bearer API key. Its registry-owned model-list URL keeps only rows tagged `chat`, preserves slash-containing native model ids, and caps live discovery at 512 KiB and 512 raw @@ -506,4 +514,3 @@ Providers with a live probe: OpenAI/Codex, Anthropic, xAI, Cursor, Kimi, Google Antigravity, OpenRouter, DeepSeek, ClinePass, Z.AI, MiniMax, Moonshot, Venice, Synthetic, DeepInfra, Neuralwatt, and any a6api-backed custom provider. - diff --git a/docs-site/src/content/docs/ja/getting-started/quickstart.md b/docs-site/src/content/docs/ja/getting-started/quickstart.md index 01cbec27d..b31bde1c6 100644 --- a/docs-site/src/content/docs/ja/getting-started/quickstart.md +++ b/docs-site/src/content/docs/ja/getting-started/quickstart.md @@ -13,7 +13,7 @@ ocx init `ocx init` では次の手順を説明します。 -1. **プロバイダーを選択してください** — 76 個の組み込みレジストリプリセットのいずれか、または `custom` を選択してベース URL とアダプターを入力します。 +1. **プロバイダーを選択してください** — 79 個の組み込みレジストリプリセットのいずれか、または `custom` を選択してベース URL とアダプターを入力します。 2. **API キー** — キーを貼り付けるか、`${ANTHROPIC_API_KEY}` のような環境変数を参照します。 3. **デフォルト モデル** — キー、ローカル、カスタム プロバイダーの場合は、プリセットを受け入れるか、モデル ID を入力します。 4. **プロキシ ポート** — デフォルトは `10100` です。 diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md index 7608ccbda..017cb73e1 100644 --- a/docs-site/src/content/docs/ja/guides/providers.md +++ b/docs-site/src/content/docs/ja/guides/providers.md @@ -144,7 +144,7 @@ Kiro のログインには Kiro CLI が必要です。Unix では `curl -fsSL ht ## 3. API キーカタログ -opencodex には組み込みプリセットが 76 個含まれています。キー方式 64、OAuth 8、ローカル 3、 +opencodex には組み込みプリセットが 79 個含まれています。キー方式 67、OAuth 8、ローカル 3、 デフォルト ChatGPT 転送プリセット 1 です。ダッシュボードの **Add provider** ピッカーはキー発行ページを開き、 入力したキーを検証した後保存します(検証はプロバイダー固有です)。主な項目は以下のとおりです: @@ -176,6 +176,7 @@ Cline IDE/CLI のみで API からは使えません。`minimax/minimax-m2.5` | MiniMax · MiniMax (CN) | `https://api.minimax.io/v1` · `https://api.minimaxi.com/v1` | | DeepSeek | `https://api.deepseek.com` | | Cerebras | `https://api.cerebras.ai/v1` | +| Chutes | `https://llm.chutes.ai/v1` | | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | | Nscale Serverless Inference | `https://inference.api.nscale.com/v1` | @@ -221,6 +222,13 @@ Volcengine Agent Plan は `openai-responses` アダプターでネイティブ R > 含まれます。Coding Plan のデフォルトは `ark-code-latest`、Agent Plan は > `deepseek-v4-pro` です。 +**Chutes の discovery:** `chutes` preset は Chutes の固定された共有 OpenAI 互換 LLM gateway を使います。 +公開 `/v1/models` catalog から `supported_features` が `tools` を示す行だけを残し、スラッシュを含む +model id と安全な live metadata を保持します。discovery は 256 KiB と raw 128 行に制限されます。 +catalog は公開されているため、入力したキーの有効性は証明できませんが、chat request は設定済みの +Bearer キーで認証されます。ユーザーが deploy した custom Chute host と LLM 以外の API は custom +provider の範囲です。キーは [Chutes dashboard](https://chutes.ai/auth/start) で作成します。 + **DeepInfra の discovery:** キー方式の OpenAI Chat Completions プロバイダー `deepinfra` は、 `openai-chat` アダプターと Bearer API キーを使います。registry が所有する DeepInfra のモデル一覧 URL から `chat` タグを持つ行だけを残し、スラッシュを含むネイティブモデル ID を保持します。live discovery は diff --git a/docs-site/src/content/docs/ko/getting-started/quickstart.md b/docs-site/src/content/docs/ko/getting-started/quickstart.md index 90947707c..fb263b2cd 100644 --- a/docs-site/src/content/docs/ko/getting-started/quickstart.md +++ b/docs-site/src/content/docs/ko/getting-started/quickstart.md @@ -13,7 +13,7 @@ ocx init `ocx init`은 다음 과정을 안내합니다: -1. **프로바이더 선택** — 내장 레지스트리 프리셋 76개 중 하나를 고르거나 `custom`을 선택해 base URL과 adapter를 직접 입력합니다. +1. **프로바이더 선택** — 내장 레지스트리 프리셋 79개 중 하나를 고르거나 `custom`을 선택해 base URL과 adapter를 직접 입력합니다. 2. **API 키** — 키를 붙여넣거나 `${ANTHROPIC_API_KEY}` 같은 환경 변수를 참조합니다. 3. **기본 모델** — 키, 로컬, custom 프로바이더에서는 프리셋을 그대로 쓰거나 모델 ID를 직접 입력합니다. 4. **프록시 포트** — 기본값은 `10100`입니다. diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md index de7c575be..bc69eef82 100644 --- a/docs-site/src/content/docs/ko/guides/providers.md +++ b/docs-site/src/content/docs/ko/guides/providers.md @@ -143,7 +143,7 @@ Kiro 로그인에는 Kiro CLI가 필요합니다. Unix에서는 `curl -fsSL http ## 3. API 키 카탈로그 -opencodex에는 빌트인 프리셋이 76개 들어 있습니다. 키 방식 64개, OAuth 8개, 로컬 3개, +opencodex에는 빌트인 프리셋이 79개 들어 있습니다. 키 방식 67개, OAuth 8개, 로컬 3개, 기본 ChatGPT 포워드 프리셋 1개입니다. 대시보드의 **Add provider** 선택기는 키 발급 페이지를 열고, 입력한 키를 검증한 뒤 저장합니다(검증은 프로바이더별로 다릅니다). 주요 항목은 다음과 같습니다: @@ -176,6 +176,7 @@ Cline IDE/CLI에서만 제공되며 API로는 사용할 수 없습니다. `minim | MiniMax · MiniMax (CN) | `https://api.minimax.io/v1` · `https://api.minimaxi.com/v1` | | DeepSeek | `https://api.deepseek.com` | | Cerebras | `https://api.cerebras.ai/v1` | +| Chutes | `https://llm.chutes.ai/v1` | | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | | Nscale Serverless Inference | `https://inference.api.nscale.com/v1` | @@ -220,6 +221,13 @@ Volcengine Agent Plan은 `openai-responses` 어댑터로 네이티브 Responses > `doubao-seed-2-1-pro-260628`이며 정적 카탈로그에는 현재 DeepSeek와 GLM 텍스트 모델도 > 포함됩니다. Coding Plan의 기본값은 `ark-code-latest`, Agent Plan은 `deepseek-v4-pro`입니다. +**Chutes 검색:** `chutes` 프리셋은 Chutes의 고정된 공유 OpenAI 호환 LLM gateway를 사용합니다. +공개 `/v1/models` catalog에서 `supported_features`가 `tools`를 명시한 행만 유지하고, 슬래시가 포함된 +model id와 안전한 live metadata를 보존합니다. 검색은 256 KiB와 raw 128행으로 제한됩니다. 이 catalog는 +공개되어 있어 입력한 키의 유효성을 증명할 수 없지만, chat request는 설정된 Bearer 키로 인증됩니다. +사용자가 배포한 custom Chute host와 LLM 이외의 API는 custom provider로 설정해야 합니다. 키는 +[Chutes dashboard](https://chutes.ai/auth/start)에서 생성합니다. + **DeepInfra 검색:** 키 기반 OpenAI Chat Completions 제공자인 `deepinfra`는 `openai-chat` 어댑터와 Bearer API 키를 사용합니다. registry가 소유하는 DeepInfra 모델 목록 URL에서 `chat` 태그가 있는 행만 유지하고, 슬래시가 포함된 네이티브 모델 ID를 보존합니다. live discovery는 512 KiB 및 raw 512행으로 diff --git a/docs-site/src/content/docs/ru/getting-started/quickstart.md b/docs-site/src/content/docs/ru/getting-started/quickstart.md index 243bc6bad..e7f05ff4c 100644 --- a/docs-site/src/content/docs/ru/getting-started/quickstart.md +++ b/docs-site/src/content/docs/ru/getting-started/quickstart.md @@ -13,7 +13,7 @@ ocx init `ocx init` проведёт вас по следующим шагам: -1. **Выбор провайдера** — выберите один из 76 встроенных пресетов реестра или `custom`, чтобы +1. **Выбор провайдера** — выберите один из 79 встроенных пресетов реестра или `custom`, чтобы ввести базовый URL и адаптер вручную. 2. **API-ключ** — вставьте ключ или сошлитесь на переменную окружения вида `${ANTHROPIC_API_KEY}`. 3. **Модель по умолчанию** — для провайдеров с ключом, локальных и `custom` примите значение из diff --git a/docs-site/src/content/docs/ru/guides/providers.md b/docs-site/src/content/docs/ru/guides/providers.md index 6ab93356e..3d2241dfa 100644 --- a/docs-site/src/content/docs/ru/guides/providers.md +++ b/docs-site/src/content/docs/ru/guides/providers.md @@ -154,7 +154,7 @@ OAuth-провайдеры, чьи учётные данные содержат ## 3. Каталог API-ключей -opencodex поставляется с 76 встроенными пресетами: 64 на основе ключей, восемь OAuth, три локальных и +opencodex поставляется с 79 встроенными пресетами: 67 на основе ключей, восемь OAuth, три локальных и один пресет ChatGPT-форварда по умолчанию. Селектор **Add provider** в дашборде открывает страницу выдачи ключей провайдера, проверяет ключ и сохраняет его; проверка зависит от провайдера. Наиболее заметные записи: @@ -187,6 +187,7 @@ opencodex поставляется с 76 встроенными пресетам | MiniMax · MiniMax (CN) | `https://api.minimax.io/v1` · `https://api.minimaxi.com/v1` | | DeepSeek | `https://api.deepseek.com` | | Cerebras | `https://api.cerebras.ai/v1` | +| Chutes | `https://llm.chutes.ai/v1` | | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | | Nscale Serverless Inference | `https://inference.api.nscale.com/v1` | @@ -239,6 +240,14 @@ Volcengine Agent Plan использует нативную конечную т > DeepSeek и GLM. Для Coding Plan модель по умолчанию — `ark-code-latest`, для Agent Plan — > `deepseek-v4-pro`. +**Discovery для Chutes.** Пресет `chutes` использует фиксированный общий OpenAI-совместимый LLM +gateway Chutes. Из публичного каталога `/v1/models` он оставляет только строки, где +`supported_features` содержит `tools`, сохраняет нативные id со знаком `/` и безопасные live metadata, +а также ограничивает discovery размером 256 KiB и 128 исходными строками. Публичный каталог не может +подтвердить корректность введённого ключа, но chat-запросы всё равно аутентифицируются настроенным +Bearer-ключом. Пользовательские Chute host и API не для LLM требуют custom provider. Ключ создаётся в +[дашборде Chutes](https://chutes.ai/auth/start). + **Discovery для DeepInfra.** `deepinfra` — провайдер OpenAI Chat Completions с аутентификацией по ключу; он использует адаптер `openai-chat` и Bearer API-ключ. Принадлежащий registry URL списка моделей DeepInfra оставляет только строки с тегом `chat`, сохраняет нативные id моделей со знаком diff --git a/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md b/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md index 238a97205..93840d87f 100644 --- a/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md +++ b/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md @@ -13,7 +13,7 @@ ocx init `ocx init` 会引导你完成: -1. **选择 provider** — 从内置 registry 的 76 个预设中选择一个,或选择 `custom` 手动输入 base URL 和 adapter。 +1. **选择 provider** — 从内置 registry 的 79 个预设中选择一个,或选择 `custom` 手动输入 base URL 和 adapter。 2. **API key** — 粘贴一个 key,或引用一个环境变量,例如 `${ANTHROPIC_API_KEY}`。 3. **默认模型** — 对于 key、本地和 custom provider,接受预设值或输入模型 id。 4. **代理端口** — 默认为 `10100`。 diff --git a/docs-site/src/content/docs/zh-cn/guides/providers.md b/docs-site/src/content/docs/zh-cn/guides/providers.md index e51280066..2aeef2a61 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -132,7 +132,7 @@ Kiro 登录需要 Kiro CLI:Unix 使用 `curl -fsSL https://cli.kiro.dev/instal ## 3. API 密钥目录 -opencodex 内置 76 个预设:64 个密钥预设、8 个 OAuth 预设、3 个本地预设,以及 1 个默认的 +opencodex 内置 79 个预设:67 个密钥预设、8 个 OAuth 预设、3 个本地预设,以及 1 个默认的 ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提供商的控制台,验证并保存密钥。 验证因提供商而异。主要条目包括: @@ -164,6 +164,7 @@ Cline IDE/CLI 中提供,不能通过 API 使用;`minimax/minimax-m2.5` 是 | MiniMax · MiniMax (CN) | `https://api.minimax.io/v1` · `https://api.minimaxi.com/v1` | | DeepSeek | `https://api.deepseek.com` | | Cerebras | `https://api.cerebras.ai/v1` | +| Chutes | `https://llm.chutes.ai/v1` | | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | | Nscale Serverless Inference | `https://inference.api.nscale.com/v1` | @@ -206,6 +207,12 @@ Cline IDE/CLI 中提供,不能通过 API 使用;`minimax/minimax-m2.5` 是 > 文本模型。Coding Plan 默认使用 `ark-code-latest`,Agent Plan 默认使用 > `deepseek-v4-pro`。 +**Chutes 发现:**`chutes` 预设使用 Chutes 固定的共享 OpenAI 兼容 LLM gateway。它读取公开的 +`/v1/models` 目录,仅保留 `supported_features` 包含 `tools` 的记录,保留含 `/` 的原生 model id 与 +安全的实时 metadata,并把发现限制为 256 KiB 和 128 条原始记录。由于该目录公开,它无法证明输入的 +密钥有效;chat 请求仍会使用已配置的 Bearer 密钥认证。用户自行部署的 custom Chute host 与非 LLM API +需要使用 custom provider。密钥可在 [Chutes dashboard](https://chutes.ai/auth/start) 创建。 + **DeepInfra 发现:**`deepinfra` 是使用 `openai-chat` adapter 和 Bearer API 密钥的密钥型 OpenAI Chat Completions 提供商。registry 固定的 DeepInfra 模型列表 URL 仅保留带 `chat` 标签的记录, 同时保留含 `/` 的原生模型 id,并把实时发现限制为 512 KiB 和 512 条原始记录。 diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 39353726e..b3f8d9519 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -1393,6 +1393,40 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ }, // llama-3.3-70b was deprecated by Cerebras on 2026-02-16. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md. { id: "cerebras", label: "Cerebras", baseUrl: "https://api.cerebras.ai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://cloud.cerebras.ai/platform/apikeys", defaultModel: "gpt-oss-120b" }, + { + // Primary sources checked 2026-08-08: + // - https://chutes.ai/pricing documents the shared llm.chutes.ai/v1 OpenAI-compatible + // gateway, Bearer API keys, and chat completions. Its public + // https://llm.chutes.ai/v1/models response supplies supported_features for filtering. + // - https://chutes.ai/terms identifies Chutes Global Corp as the platform operator, applies + // to API consumers, and directs production/high-volume automated inference to PAYGO. + // Maintainer: @olddonkey; no affiliation with Chutes. + id: "chutes", + label: "Chutes", + baseUrl: "https://llm.chutes.ai/v1", + adapter: "openai-chat", + authKind: "key", + dashboardUrl: "https://chutes.ai/auth/start", + liveModels: true, + preserveCustomDestination: true, + // The public model catalog cannot prove that a supplied Bearer key is valid. + apiKeyValidation: "unknown", + // Chutes documents tool calling, but not a provider-wide parallel tool-call contract. + parallelToolCalls: false, + // The live catalog reports reasoning support, but not a stable effort ladder. + reasoningEfforts: [], + modelDiscovery: { + path: "models", + maxResponseBytes: 256 * 1024, + maxModels: 128, + filter: { + // The shared LLM catalog also contains rows without native tool support. Codex needs a + // complete agent loop, so admit only rows whose live metadata advertises tools. + allOf: [{ path: ["supported_features"], containsAny: ["tools"] }], + }, + }, + note: "Shared OpenAI-compatible LLM gateway only; live discovery exposes tool-capable rows. User-deployed custom Chute endpoints and non-LLM APIs require a custom provider.", + }, { id: "deepinfra", label: "DeepInfra", diff --git a/tests/chutes-provider.test.ts b/tests/chutes-provider.test.ts new file mode 100644 index 000000000..f46ec8c04 --- /dev/null +++ b/tests/chutes-provider.test.ts @@ -0,0 +1,221 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { createOpenAIChatAdapter } from "../src/adapters/openai-chat"; +import { gatherRoutedModels } from "../src/codex/catalog"; +import { clearModelCache } from "../src/codex/model-cache"; +import { buildInitProviders } from "../src/cli/init"; +import { buildModelsRequest } from "../src/oauth"; +import { KEY_LOGIN_PROVIDERS, validateApiKey } from "../src/oauth/key-providers"; +import { + deriveInitProviders, + deriveProviderPresets, + providerConfigSeed, +} from "../src/providers/derive"; +import { resolveProviderModelDiscovery } from "../src/providers/model-discovery"; +import { PROVIDER_REGISTRY } from "../src/providers/registry"; +import { routedSlug } from "../src/providers/slug-codec"; +import { routeModel } from "../src/router"; +import type { OcxConfig, OcxProviderConfig } from "../src/types"; +import { withStubbedProviderFetch } from "./helpers/catalog-provider-fetch"; + +const CHUTES_FIXTURE = readFileSync(join(import.meta.dir, "fixtures/chutes-models.json"), "utf8"); +const BASE_URL = "https://llm.chutes.ai/v1"; +const MODELS_URL = `${BASE_URL}/models`; +const TEST_KEY = "chutes-test-key"; +const originalFetch = globalThis.fetch; + +afterEach(() => { + globalThis.fetch = originalFetch; + clearModelCache("chutes"); +}); + +function registryEntry() { + const entry = PROVIDER_REGISTRY.find(row => row.id === "chutes"); + if (!entry) throw new Error("missing chutes registry entry"); + return entry; +} + +function providerConfig(overrides: Partial = {}): OcxConfig { + return { + port: 10100, + defaultProvider: "chutes", + providers: { + chutes: { + adapter: "openai-chat", + baseUrl: BASE_URL, + authMode: "key", + apiKey: TEST_KEY, + liveModels: true, + // Discovery stays fixture-only; this avoids platform-specific public-DNS classification. + allowPrivateNetwork: true, + ...overrides, + }, + }, + }; +} + +describe("Chutes provider", () => { + test("registers the fixed shared LLM transport with bounded tool-model discovery", () => { + expect(registryEntry()).toMatchObject({ + id: "chutes", + label: "Chutes", + adapter: "openai-chat", + baseUrl: BASE_URL, + authKind: "key", + dashboardUrl: "https://chutes.ai/auth/start", + liveModels: true, + preserveCustomDestination: true, + apiKeyValidation: "unknown", + parallelToolCalls: false, + reasoningEfforts: [], + modelDiscovery: { + path: "models", + maxResponseBytes: 262_144, + maxModels: 128, + filter: { + allOf: [{ path: ["supported_features"], containsAny: ["tools"] }], + }, + }, + }); + expect(registryEntry().note).toContain("custom Chute endpoints"); + }); + + test("derives CLI and dashboard presets without persisting registry trust policy", () => { + const entry = registryEntry(); + expect(buildInitProviders()).toEqual(deriveInitProviders()); + expect(KEY_LOGIN_PROVIDERS.chutes).toMatchObject({ + adapter: "openai-chat", + baseUrl: BASE_URL, + dashboardUrl: entry.dashboardUrl, + liveModels: true, + apiKeyValidation: "unknown", + reasoningEfforts: [], + }); + expect(buildInitProviders().find(row => row.id === "chutes")).toMatchObject({ + kind: "key", + adapter: "openai-chat", + baseUrl: BASE_URL, + }); + expect(deriveProviderPresets().find(row => row.id === "chutes")).toMatchObject({ + auth: "key", + dashboardUrl: entry.dashboardUrl, + }); + + const seed = providerConfigSeed(entry); + expect(seed).toMatchObject({ + adapter: "openai-chat", + baseUrl: BASE_URL, + authMode: "key", + liveModels: true, + parallelToolCalls: false, + reasoningEfforts: [], + }); + expect(seed).not.toHaveProperty("modelDiscovery"); + expect(seed).not.toHaveProperty("preserveCustomDestination"); + expect(KEY_LOGIN_PROVIDERS.chutes).not.toHaveProperty("modelDiscovery"); + expect(KEY_LOGIN_PROVIDERS.chutes).not.toHaveProperty("preserveCustomDestination"); + }); + + test("uses the documented Bearer endpoint without treating its public catalog as key proof", async () => { + expect(buildModelsRequest(providerConfig().providers.chutes!, TEST_KEY, "chutes")).toEqual({ + url: MODELS_URL, + headers: { Authorization: `Bearer ${TEST_KEY}` }, + }); + + globalThis.fetch = (async () => { + throw new Error("public-catalog validation must not fetch"); + }) as typeof fetch; + expect(await validateApiKey("chutes", KEY_LOGIN_PROVIDERS.chutes!, TEST_KEY)).toBe("unknown"); + }); + + test("filters to tool-capable rows and preserves safe live metadata and slash ids", async () => { + globalThis.fetch = (async (input, init) => { + expect(String(input)).toBe(MODELS_URL); + expect(new Headers(init?.headers).get("authorization")).toBe(`Bearer ${TEST_KEY}`); + expect(init?.redirect).toBe("manual"); + return new Response(CHUTES_FIXTURE, { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + + const config = withStubbedProviderFetch(providerConfig()); + const models = (await gatherRoutedModels(config)).filter(row => row.provider === "chutes"); + expect(models.map(row => row.id)).toEqual([ + "moonshotai/Kimi-K2.6-TEE", + "Qwen/Qwen3-32B-TEE", + ]); + const qwen = models.find(row => row.id === "Qwen/Qwen3-32B-TEE"); + const kimi = models.find(row => row.id === "moonshotai/Kimi-K2.6-TEE"); + expect(qwen).toMatchObject({ + owned_by: "sglang", + contextWindow: 40_960, + inputModalities: ["text"], + capabilities: ["json_mode", "tools", "structured_outputs", "reasoning"], + reasoningEfforts: [], + }); + expect(qwen).not.toHaveProperty("parallelToolCalls"); + expect(kimi).toMatchObject({ + contextWindow: 262_144, + inputModalities: ["text", "image"], + capabilities: ["json_mode", "structured_outputs", "tools", "reasoning"], + reasoningEfforts: [], + }); + + for (const modelId of models.map(row => row.id)) { + expect(routeModel(config, `chutes/${modelId}`).modelId).toBe(modelId); + expect(routeModel(config, routedSlug("chutes", modelId)).modelId).toBe(modelId); + } + }); + + test("routes tool requests to the fixed host without unsupported reasoning or parallel fields", () => { + const modelId = "Qwen/Qwen3-32B-TEE"; + const route = routeModel(providerConfig(), `chutes/${modelId}`); + const request = createOpenAIChatAdapter(route.provider).buildRequest({ + modelId: route.modelId, + context: { + messages: [{ role: "user", content: "ping", timestamp: 0 }], + tools: [{ + name: "ping", + description: "Return pong", + parameters: { type: "object", properties: {} }, + }], + }, + stream: true, + options: { reasoning: "high" }, + }); + const body = JSON.parse(String(request.body)) as Record; + + expect(request.url).toBe(`${BASE_URL}/chat/completions`); + expect(request.headers.Authorization).toBe(`Bearer ${TEST_KEY}`); + expect(body.model).toBe(modelId); + expect(body.parallel_tool_calls).toBe(false); + expect(body).not.toHaveProperty("reasoning_effort"); + }); + + test("does not retarget an older same-named custom provider or adapter", () => { + const customConfig = providerConfig({ baseUrl: "https://custom.example/v1" }); + const route = routeModel(customConfig, "chutes/custom-model"); + expect(route.provider).toMatchObject({ + adapter: "openai-chat", + baseUrl: "https://custom.example/v1", + authMode: "key", + }); + expect(resolveProviderModelDiscovery("chutes", customConfig.providers.chutes!).spec).toBeUndefined(); + expect(buildModelsRequest(customConfig.providers.chutes!, "custom-key", "chutes")).toEqual({ + url: "https://custom.example/v1/models", + headers: { Authorization: "Bearer custom-key" }, + }); + + const customAdapter = routeModel(providerConfig({ + adapter: "anthropic", + baseUrl: "https://custom.example/anthropic", + }), "chutes/custom-model"); + expect(customAdapter.provider).toMatchObject({ + adapter: "anthropic", + baseUrl: "https://custom.example/anthropic", + authMode: "key", + }); + }); +}); diff --git a/tests/fixtures/chutes-models.json b/tests/fixtures/chutes-models.json new file mode 100644 index 000000000..dbc264b36 --- /dev/null +++ b/tests/fixtures/chutes-models.json @@ -0,0 +1,51 @@ +{ + "object": "list", + "data": [ + { + "id": "Qwen/Qwen3-32B-TEE", + "object": "model", + "owned_by": "sglang", + "context_length": 40960, + "max_model_len": 40960, + "max_output_length": 40960, + "input_modalities": ["text"], + "output_modalities": ["text"], + "supported_features": ["json_mode", "tools", "structured_outputs", "reasoning"], + "confidential_compute": true + }, + { + "id": "moonshotai/Kimi-K2.6-TEE", + "object": "model", + "owned_by": "sglang", + "context_length": 262144, + "max_model_len": 262144, + "max_output_length": 65536, + "input_modalities": ["text", "image", "video"], + "output_modalities": ["text"], + "supported_features": ["json_mode", "structured_outputs", "tools", "reasoning"], + "confidential_compute": true + }, + { + "id": "unsloth/Mistral-Nemo-Instruct-2407-TEE", + "object": "model", + "owned_by": "sglang", + "context_length": null, + "max_model_len": 131072, + "input_modalities": null, + "output_modalities": null, + "supported_features": null, + "confidential_compute": true + }, + { + "id": "Nemotron-3-Nano-Omni-30B-TEE", + "object": "model", + "owned_by": "sglang", + "context_length": 131072, + "max_model_len": 131072, + "input_modalities": null, + "output_modalities": null, + "supported_features": null, + "confidential_compute": true + } + ] +} diff --git a/tests/provider-registry-parity.test.ts b/tests/provider-registry-parity.test.ts index 5fa854e22..f8656cec4 100644 --- a/tests/provider-registry-parity.test.ts +++ b/tests/provider-registry-parity.test.ts @@ -31,7 +31,7 @@ function nativeTemplate(): Record { const EXPECTED_KEY_PROVIDER_IDS = [ "anthropic-apikey", "openai-apikey", "umans", "opencode-go", "neuralwatt", "openrouter", "cline-pass", "cline", "orcarouter", "bizrouter", "groq", "google", "google-vertex", "azure-openai", - "deepseek", "cerebras", "deepinfra", "hyperbolic", "nscale", "vultr", "baseten", "commandcode", "sambanova", "nebius", "digitalocean", "scaleway", "together", "fireworks", "firepass", "moonshot", + "deepseek", "cerebras", "chutes", "deepinfra", "hyperbolic", "nscale", "vultr", "baseten", "commandcode", "sambanova", "nebius", "digitalocean", "scaleway", "together", "fireworks", "firepass", "moonshot", "huggingface", "nvidia", "venice", "zai", "zhipu-bigmodel", "zhipu-bigmodel-coding", "nanogpt", "synthetic", "siliconflow", "qwen-cloud", "tencent-coding-plan", "volcengine", "volcengine-coding-plan", "volcengine-agent-plan", "qianfan", "alibaba", "alibaba-token-plan", "alibaba-token-plan-intl", "parallel", "zenmux", "litellm", "ollama-cloud", "mistral", "minimax", "minimax-cn", "kimi-code", "opencode-zen", "vercel-ai-gateway", From 9d86216b75b347536a08b69b5973210b49346e37 Mon Sep 17 00:00:00 2001 From: olddonkey Date: Sat, 8 Aug 2026 13:20:12 -0700 Subject: [PATCH 2/3] test(providers): harden Chutes coverage --- tests/chutes-provider.test.ts | 16 ++++++++++++++-- tests/fixtures/chutes-models.json | 12 ++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/chutes-provider.test.ts b/tests/chutes-provider.test.ts index f46ec8c04..226d5fe86 100644 --- a/tests/chutes-provider.test.ts +++ b/tests/chutes-provider.test.ts @@ -47,8 +47,6 @@ function providerConfig(overrides: Partial = {}): OcxConfig { authMode: "key", apiKey: TEST_KEY, liveModels: true, - // Discovery stays fixture-only; this avoids platform-specific public-DNS classification. - allowPrivateNetwork: true, ...overrides, }, }, @@ -143,11 +141,13 @@ describe("Chutes provider", () => { const config = withStubbedProviderFetch(providerConfig()); const models = (await gatherRoutedModels(config)).filter(row => row.provider === "chutes"); expect(models.map(row => row.id)).toEqual([ + "example/sparse-tool-model", "moonshotai/Kimi-K2.6-TEE", "Qwen/Qwen3-32B-TEE", ]); const qwen = models.find(row => row.id === "Qwen/Qwen3-32B-TEE"); const kimi = models.find(row => row.id === "moonshotai/Kimi-K2.6-TEE"); + const sparse = models.find(row => row.id === "example/sparse-tool-model"); expect(qwen).toMatchObject({ owned_by: "sglang", contextWindow: 40_960, @@ -162,6 +162,13 @@ describe("Chutes provider", () => { capabilities: ["json_mode", "structured_outputs", "tools", "reasoning"], reasoningEfforts: [], }); + expect(sparse).toMatchObject({ + owned_by: "fixture", + capabilities: ["tools"], + reasoningEfforts: [], + }); + expect(sparse).not.toHaveProperty("contextWindow"); + expect(sparse).not.toHaveProperty("inputModalities"); for (const modelId of models.map(row => row.id)) { expect(routeModel(config, `chutes/${modelId}`).modelId).toBe(modelId); @@ -208,6 +215,11 @@ describe("Chutes provider", () => { headers: { Authorization: "Bearer custom-key" }, }); + const nearMissConfig = providerConfig({ baseUrl: "https://llm.chutes.ai/v2" }); + expect( + resolveProviderModelDiscovery("chutes", nearMissConfig.providers.chutes!).spec, + ).toBeUndefined(); + const customAdapter = routeModel(providerConfig({ adapter: "anthropic", baseUrl: "https://custom.example/anthropic", diff --git a/tests/fixtures/chutes-models.json b/tests/fixtures/chutes-models.json index dbc264b36..5068cc792 100644 --- a/tests/fixtures/chutes-models.json +++ b/tests/fixtures/chutes-models.json @@ -25,6 +25,18 @@ "supported_features": ["json_mode", "structured_outputs", "tools", "reasoning"], "confidential_compute": true }, + { + "id": "example/sparse-tool-model", + "object": "model", + "owned_by": "fixture", + "context_length": null, + "max_model_len": null, + "max_output_length": null, + "input_modalities": null, + "output_modalities": null, + "supported_features": ["tools"], + "confidential_compute": true + }, { "id": "unsloth/Mistral-Nemo-Instruct-2407-TEE", "object": "model", From 09397db970b3f63e1060772beaa5e5f474faefed Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Mon, 10 Aug 2026 06:56:17 +0200 Subject: [PATCH 3/3] fix(providers): rebase on dev and correct standalone preset counts Rebase onto current dev and update docs from the batch 79/67 figures to the correct standalone 77/65 counts for this single-provider PR. --- docs-site/src/content/docs/getting-started/quickstart.md | 2 +- docs-site/src/content/docs/guides/providers.md | 2 +- docs-site/src/content/docs/ja/getting-started/quickstart.md | 2 +- docs-site/src/content/docs/ja/guides/providers.md | 2 +- docs-site/src/content/docs/ko/getting-started/quickstart.md | 2 +- docs-site/src/content/docs/ko/guides/providers.md | 2 +- docs-site/src/content/docs/ru/getting-started/quickstart.md | 2 +- docs-site/src/content/docs/ru/guides/providers.md | 2 +- docs-site/src/content/docs/zh-cn/getting-started/quickstart.md | 2 +- docs-site/src/content/docs/zh-cn/guides/providers.md | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs-site/src/content/docs/getting-started/quickstart.md b/docs-site/src/content/docs/getting-started/quickstart.md index 4264d025b..a30f0b295 100644 --- a/docs-site/src/content/docs/getting-started/quickstart.md +++ b/docs-site/src/content/docs/getting-started/quickstart.md @@ -13,7 +13,7 @@ ocx init `ocx init` walks you through: -1. **Pick a provider** — choose one of the 79 built-in registry presets or `custom` to type a base +1. **Pick a provider** — choose one of the 77 built-in registry presets or `custom` to type a base URL and adapter. 2. **API key** — paste a key, or reference an environment variable like `${ANTHROPIC_API_KEY}`. 3. **Default model** — for key, local, and custom providers, accept the preset or enter a model id. diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index f737f2ca9..4ec6eb536 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -217,7 +217,7 @@ selectors, then retry. Signing in from a machine with no existing `kiro-cli` ses ## 3. API-key catalog -opencodex ships 79 built-in presets: 67 key-based, eight OAuth, three local, and one default +opencodex ships 77 built-in presets: 65 key-based, eight OAuth, three local, and one default ChatGPT-forward preset. The dashboard's **Add provider** picker opens a key provider's dashboard, validates the key, and stores it; validation is provider-specific. Notable entries: diff --git a/docs-site/src/content/docs/ja/getting-started/quickstart.md b/docs-site/src/content/docs/ja/getting-started/quickstart.md index b31bde1c6..c83c08a96 100644 --- a/docs-site/src/content/docs/ja/getting-started/quickstart.md +++ b/docs-site/src/content/docs/ja/getting-started/quickstart.md @@ -13,7 +13,7 @@ ocx init `ocx init` では次の手順を説明します。 -1. **プロバイダーを選択してください** — 79 個の組み込みレジストリプリセットのいずれか、または `custom` を選択してベース URL とアダプターを入力します。 +1. **プロバイダーを選択してください** — 77 個の組み込みレジストリプリセットのいずれか、または `custom` を選択してベース URL とアダプターを入力します。 2. **API キー** — キーを貼り付けるか、`${ANTHROPIC_API_KEY}` のような環境変数を参照します。 3. **デフォルト モデル** — キー、ローカル、カスタム プロバイダーの場合は、プリセットを受け入れるか、モデル ID を入力します。 4. **プロキシ ポート** — デフォルトは `10100` です。 diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md index 017cb73e1..a8f4ae201 100644 --- a/docs-site/src/content/docs/ja/guides/providers.md +++ b/docs-site/src/content/docs/ja/guides/providers.md @@ -144,7 +144,7 @@ Kiro のログインには Kiro CLI が必要です。Unix では `curl -fsSL ht ## 3. API キーカタログ -opencodex には組み込みプリセットが 79 個含まれています。キー方式 67、OAuth 8、ローカル 3、 +opencodex には組み込みプリセットが 77 個含まれています。キー方式 65、OAuth 8、ローカル 3、 デフォルト ChatGPT 転送プリセット 1 です。ダッシュボードの **Add provider** ピッカーはキー発行ページを開き、 入力したキーを検証した後保存します(検証はプロバイダー固有です)。主な項目は以下のとおりです: diff --git a/docs-site/src/content/docs/ko/getting-started/quickstart.md b/docs-site/src/content/docs/ko/getting-started/quickstart.md index fb263b2cd..102b03791 100644 --- a/docs-site/src/content/docs/ko/getting-started/quickstart.md +++ b/docs-site/src/content/docs/ko/getting-started/quickstart.md @@ -13,7 +13,7 @@ ocx init `ocx init`은 다음 과정을 안내합니다: -1. **프로바이더 선택** — 내장 레지스트리 프리셋 79개 중 하나를 고르거나 `custom`을 선택해 base URL과 adapter를 직접 입력합니다. +1. **프로바이더 선택** — 내장 레지스트리 프리셋 77개 중 하나를 고르거나 `custom`을 선택해 base URL과 adapter를 직접 입력합니다. 2. **API 키** — 키를 붙여넣거나 `${ANTHROPIC_API_KEY}` 같은 환경 변수를 참조합니다. 3. **기본 모델** — 키, 로컬, custom 프로바이더에서는 프리셋을 그대로 쓰거나 모델 ID를 직접 입력합니다. 4. **프록시 포트** — 기본값은 `10100`입니다. diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md index bc69eef82..78623f0e6 100644 --- a/docs-site/src/content/docs/ko/guides/providers.md +++ b/docs-site/src/content/docs/ko/guides/providers.md @@ -143,7 +143,7 @@ Kiro 로그인에는 Kiro CLI가 필요합니다. Unix에서는 `curl -fsSL http ## 3. API 키 카탈로그 -opencodex에는 빌트인 프리셋이 79개 들어 있습니다. 키 방식 67개, OAuth 8개, 로컬 3개, +opencodex에는 빌트인 프리셋이 77개 들어 있습니다. 키 방식 65개, OAuth 8개, 로컬 3개, 기본 ChatGPT 포워드 프리셋 1개입니다. 대시보드의 **Add provider** 선택기는 키 발급 페이지를 열고, 입력한 키를 검증한 뒤 저장합니다(검증은 프로바이더별로 다릅니다). 주요 항목은 다음과 같습니다: diff --git a/docs-site/src/content/docs/ru/getting-started/quickstart.md b/docs-site/src/content/docs/ru/getting-started/quickstart.md index e7f05ff4c..b4fc1b364 100644 --- a/docs-site/src/content/docs/ru/getting-started/quickstart.md +++ b/docs-site/src/content/docs/ru/getting-started/quickstart.md @@ -13,7 +13,7 @@ ocx init `ocx init` проведёт вас по следующим шагам: -1. **Выбор провайдера** — выберите один из 79 встроенных пресетов реестра или `custom`, чтобы +1. **Выбор провайдера** — выберите один из 77 встроенных пресетов реестра или `custom`, чтобы ввести базовый URL и адаптер вручную. 2. **API-ключ** — вставьте ключ или сошлитесь на переменную окружения вида `${ANTHROPIC_API_KEY}`. 3. **Модель по умолчанию** — для провайдеров с ключом, локальных и `custom` примите значение из diff --git a/docs-site/src/content/docs/ru/guides/providers.md b/docs-site/src/content/docs/ru/guides/providers.md index 3d2241dfa..8b61458ce 100644 --- a/docs-site/src/content/docs/ru/guides/providers.md +++ b/docs-site/src/content/docs/ru/guides/providers.md @@ -154,7 +154,7 @@ OAuth-провайдеры, чьи учётные данные содержат ## 3. Каталог API-ключей -opencodex поставляется с 79 встроенными пресетами: 67 на основе ключей, восемь OAuth, три локальных и +opencodex поставляется с 77 встроенными пресетами: 65 на основе ключей, восемь OAuth, три локальных и один пресет ChatGPT-форварда по умолчанию. Селектор **Add provider** в дашборде открывает страницу выдачи ключей провайдера, проверяет ключ и сохраняет его; проверка зависит от провайдера. Наиболее заметные записи: diff --git a/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md b/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md index 93840d87f..a817e743b 100644 --- a/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md +++ b/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md @@ -13,7 +13,7 @@ ocx init `ocx init` 会引导你完成: -1. **选择 provider** — 从内置 registry 的 79 个预设中选择一个,或选择 `custom` 手动输入 base URL 和 adapter。 +1. **选择 provider** — 从内置 registry 的 77 个预设中选择一个,或选择 `custom` 手动输入 base URL 和 adapter。 2. **API key** — 粘贴一个 key,或引用一个环境变量,例如 `${ANTHROPIC_API_KEY}`。 3. **默认模型** — 对于 key、本地和 custom provider,接受预设值或输入模型 id。 4. **代理端口** — 默认为 `10100`。 diff --git a/docs-site/src/content/docs/zh-cn/guides/providers.md b/docs-site/src/content/docs/zh-cn/guides/providers.md index 2aeef2a61..6611f818a 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -132,7 +132,7 @@ Kiro 登录需要 Kiro CLI:Unix 使用 `curl -fsSL https://cli.kiro.dev/instal ## 3. API 密钥目录 -opencodex 内置 79 个预设:67 个密钥预设、8 个 OAuth 预设、3 个本地预设,以及 1 个默认的 +opencodex 内置 77 个预设:65 个密钥预设、8 个 OAuth 预设、3 个本地预设,以及 1 个默认的 ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提供商的控制台,验证并保存密钥。 验证因提供商而异。主要条目包括: