diff --git a/docs-site/src/content/docs/getting-started/quickstart.md b/docs-site/src/content/docs/getting-started/quickstart.md index fa39fef46..f9bdcb466 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 69 built-in registry presets or `custom` to type a base +1. **Pick a provider** — choose one of the 71 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 5fce1c79c..ad4f39e7e 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -190,7 +190,7 @@ selectors, then retry. Signing in from a machine with no existing `kiro-cli` ses ## 3. API-key catalog -opencodex ships 69 built-in presets: 58 key-based, seven OAuth, three local, and one default +opencodex ships 71 built-in presets: 60 key-based, seven 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, and Command Code's public catalog reports keys as unverifiable. Notable entries: @@ -225,6 +225,8 @@ free-experimentation model. | Cerebras | `https://api.cerebras.ai/v1` | | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | +| Nscale Serverless Inference | `https://inference.api.nscale.com/v1` | +| Vultr Serverless Inference | `https://api.vultrinference.com/v1` | | Baseten Model APIs | `https://inference.baseten.co/v1` | | Command Code | `https://api.commandcode.ai/provider/v1` | | Together | `https://api.together.xyz/v1` | @@ -275,6 +277,15 @@ slash-containing native model ids, and caps live discovery at 256 KiB and 256 ra serverless text and vision-language chat only; Hyperbolic's separate image, audio, and GPU endpoints are out of scope. Create keys at [Hyperbolic](https://app.hyperbolic.ai). +**Nscale and Vultr discovery.** Both presets read the provider's authenticated `/v1/models` catalog, +preserve native ids, and cap discovery at 256 KiB and 256 raw rows. Nscale's catalog mixes chat, +image, and embedding models without a modality field, so the preset admits only +`meta-llama/Llama-3.1-8B-Instruct`, the model used by Nscale's official tool-calling API example. +Vultr currently documents tool calling only for `kimi-k2-instruct`, so its preset exposes only that +model. Other rows remain hidden until the provider publishes equivalent agent-tool evidence. Create +an Nscale service token in the [Nscale Console](https://console.nscale.com); copy Vultr's inference +key from the subscription overview in the [Vultr Console](https://my.vultr.com). + **Command Code discovery.** The preset reads Command Code's public `/provider/v1/models` list from the fixed Provider API host, preserves provider-native ids, and caps discovery at 256 KiB and 256 raw rows. The model catalog is unauthenticated, so the CLI login flow reports the key as unverifiable 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 41b7e8343..3af85b80e 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. **プロバイダーを選択してください** — 69 の組み込みレジストリ プリセットのいずれか、または `custom` を選択してベースを入力します +1. **プロバイダーを選択してください** — 71 の組み込みレジストリ プリセットのいずれか、または `custom` を選択してベースを入力します URLとアダプター。 2. **API キー** — キーを貼り付けるか、`${ANTHROPIC_API_KEY}` のような環境変数を参照します。 3. **デフォルト モデル** — キー、ローカル、カスタム プロバイダーの場合は、プリセットを受け入れるか、モデル ID を入力します。 diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md index d55a8a843..722f8306a 100644 --- a/docs-site/src/content/docs/ja/guides/providers.md +++ b/docs-site/src/content/docs/ja/guides/providers.md @@ -119,7 +119,7 @@ Kiro のログインには Kiro CLI が必要です。Unix では `curl -fsSL ht ## 3. API キーカタログ -opencodex には組み込みプリセットが 69 個含まれています。キー方式 58、OAuth 7、ローカル 3、 +opencodex には組み込みプリセットが 71 個含まれています。キー方式 60、OAuth 7、ローカル 3、 デフォルト ChatGPT 転送プリセット 1 です。ダッシュボードの **Add provider** ピッカーはキー発行ページを開き、 入力したキーを検証した後保存します(検証はプロバイダー固有で、Command Code の公開カタログはキーを 検証不能として報告します)。主な項目は以下のとおりです: @@ -154,6 +154,8 @@ Cline IDE/CLI のみで API からは使えません。`minimax/minimax-m2.5` | Cerebras | `https://api.cerebras.ai/v1` | | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | +| Nscale Serverless Inference | `https://inference.api.nscale.com/v1` | +| Vultr Serverless Inference | `https://api.vultrinference.com/v1` | | Baseten Model APIs | `https://inference.baseten.co/v1` | | Command Code | `https://api.commandcode.ai/provider/v1` | | Together | `https://api.together.xyz/v1` | @@ -198,6 +200,14 @@ Volcengine Agent Plan は `openai-responses` アダプターでネイティブ R vision-language chat のみを対象とし、別系統の image、audio、GPU endpoint は対象外です。キーは [Hyperbolic](https://app.hyperbolic.ai) で作成します。 +**Nscale と Vultr の discovery:** どちらの preset も認証付き `/v1/models` カタログを読み、ネイティブ ID を +保持し、discovery を 256 KiB と raw 256 行に制限します。Nscale のカタログには modality フィールドなしで +chat、image、embedding が混在するため、公式の tool-calling API 例で使われる +`meta-llama/Llama-3.1-8B-Instruct` のみを許可します。Vultr は現在 `kimi-k2-instruct` だけに tool calling を +明記しているため、そのモデルのみを公開します。ほかの行は同等の agent-tool 証拠が公開されるまで非表示です。 +Nscale の service token は [Nscale Console](https://console.nscale.com) で作成し、Vultr の inference key は +[Vultr Console](https://my.vultr.com) の subscription overview から取得します。 + **Command Code の discovery:** preset は Command Code の公開 `/provider/v1/models` リストを固定の Provider API ホストから読み、スラッシュを含むネイティブモデル ID を保持し、live discovery を 256 KiB と raw 256 行に制限します。モデルカタログは未認証のため、CLI ログインフローはキーを 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 9692a44e7..2f79bd740 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. **프로바이더 선택** — 내장 레지스트리 프리셋 69개 중 하나를 고르거나 `custom`을 선택해 base URL과 adapter를 직접 입력합니다. +1. **프로바이더 선택** — 내장 레지스트리 프리셋 71개 중 하나를 고르거나 `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 ce1609aba..1abf9ee55 100644 --- a/docs-site/src/content/docs/ko/guides/providers.md +++ b/docs-site/src/content/docs/ko/guides/providers.md @@ -119,7 +119,7 @@ Kiro 로그인에는 Kiro CLI가 필요합니다. Unix에서는 `curl -fsSL http ## 3. API 키 카탈로그 -opencodex에는 빌트인 프리셋이 69개 들어 있습니다. 키 방식 58개, OAuth 7개, 로컬 3개, +opencodex에는 빌트인 프리셋이 71개 들어 있습니다. 키 방식 60개, OAuth 7개, 로컬 3개, 기본 ChatGPT 포워드 프리셋 1개입니다. 대시보드의 **Add provider** 선택기는 키 발급 페이지를 열고, 입력한 키를 검증한 뒤 저장합니다(검증은 프로바이더별로 다르며, Command Code의 공개 카탈로그는 키를 검증 불가로 보고합니다). 주요 항목은 다음과 같습니다: @@ -155,6 +155,8 @@ Cline IDE/CLI에서만 제공되며 API로는 사용할 수 없습니다. `minim | Cerebras | `https://api.cerebras.ai/v1` | | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | +| Nscale Serverless Inference | `https://inference.api.nscale.com/v1` | +| Vultr Serverless Inference | `https://api.vultrinference.com/v1` | | Baseten Model APIs | `https://inference.baseten.co/v1` | | Command Code | `https://api.commandcode.ai/provider/v1` | | Together | `https://api.together.xyz/v1` | @@ -198,6 +200,14 @@ Bearer API 키를 사용합니다. registry가 소유하는 DeepInfra 모델 목 대상으로 하며 별도 image, audio, GPU 엔드포인트는 범위에서 제외합니다. 키는 [Hyperbolic](https://app.hyperbolic.ai)에서 생성합니다. +**Nscale 및 Vultr 검색:** 두 프리셋 모두 인증된 `/v1/models` 카탈로그를 읽고 네이티브 ID를 보존하며, +검색을 256 KiB와 원시 행 256개로 제한합니다. Nscale 카탈로그는 modality 필드 없이 chat, image, +embedding 모델을 함께 반환하므로 공식 도구 호출 API 예제에 사용된 +`meta-llama/Llama-3.1-8B-Instruct`만 허용합니다. Vultr는 현재 `kimi-k2-instruct`에만 도구 호출을 +문서화하므로 해당 모델만 노출합니다. 다른 행은 동등한 agent-tool 근거가 공개될 때까지 숨깁니다. +Nscale service token은 [Nscale Console](https://console.nscale.com)에서 만들고, Vultr inference key는 +[Vultr Console](https://my.vultr.com)의 구독 overview에서 복사합니다. + **Command Code 검색:** 프리셋은 Command Code의 공개 `/provider/v1/models` 목록을 고정된 Provider API 호스트에서 읽고, 슬래시가 포함된 네이티브 모델 ID를 보존하며 live discovery를 256 KiB와 raw 행 256개로 제한합니다. 모델 카탈로그는 인증이 없으므로 CLI 로그인 흐름은 키를 유효하다고 잘못 보고하지 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 ee54838ea..701d78d2a 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. **Выбор провайдера** — выберите один из 69 встроенных пресетов реестра или `custom`, чтобы +1. **Выбор провайдера** — выберите один из 71 встроенного пресета реестра или `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 2ef2dfdb1..06528475b 100644 --- a/docs-site/src/content/docs/ru/guides/providers.md +++ b/docs-site/src/content/docs/ru/guides/providers.md @@ -126,7 +126,7 @@ OAuth-провайдеры, чьи учётные данные содержат ## 3. Каталог API-ключей -opencodex поставляется с 69 встроенными пресетами: 58 на основе ключей, семь OAuth, три локальных и +opencodex поставляется с 71 встроенным пресетом: 60 на основе ключей, семь OAuth, три локальных и один пресет ChatGPT-форварда по умолчанию. Селектор **Add provider** в дашборде открывает страницу выдачи ключей провайдера, проверяет ключ и сохраняет его; проверка зависит от провайдера, а публичный каталог Command Code сообщает ключ как непроверенный. Наиболее заметные записи: @@ -161,6 +161,8 @@ opencodex поставляется с 69 встроенными пресетам | Cerebras | `https://api.cerebras.ai/v1` | | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | +| Nscale Serverless Inference | `https://inference.api.nscale.com/v1` | +| Vultr Serverless Inference | `https://api.vultrinference.com/v1` | | Baseten Model APIs | `https://inference.baseten.co/v1` | | Command Code | `https://api.commandcode.ai/provider/v1` | | Together | `https://api.together.xyz/v1` | @@ -207,6 +209,15 @@ Volcengine Agent Plan использует нативную конечную т строками. Он охватывает только serverless text и vision-language chat; отдельные image, audio и GPU endpoint в него не входят. Ключи создаются в [Hyperbolic](https://app.hyperbolic.ai). +**Discovery для Nscale и Vultr.** Оба пресета читают аутентифицированный каталог `/v1/models`, +сохраняют нативные id и ограничивают discovery размером 256 KiB и 256 исходными строками. Каталог +Nscale смешивает chat-, image- и embedding-модели без поля modality, поэтому пресет допускает только +`meta-llama/Llama-3.1-8B-Instruct` — модель из официального примера API с вызовом инструментов. +Vultr сейчас документирует tool calling только для `kimi-k2-instruct`, поэтому его пресет показывает +только эту модель. Остальные строки скрыты до появления равноценного подтверждения agent-tool. +Service token Nscale создаётся в [Nscale Console](https://console.nscale.com), а inference key Vultr +копируется со страницы подписки в [Vultr Console](https://my.vultr.com). + **Discovery для Command Code.** Пресет читает публичный список `/provider/v1/models` с фиксированного хоста Provider API, сохраняет нативные id моделей со знаком `/` и ограничивает live discovery размером 256 KiB и 256 исходными строками. Каталог моделей не требует аутентификации, поэтому CLI-флоу входа 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 0544b8f63..ce0b52ac6 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 的 69 个预设中选择一个,或选择 `custom` 手动输入 base URL 和 adapter。 +1. **选择 provider** — 从内置 registry 的 71 个预设中选择一个,或选择 `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 2680857dc..b86a9d72a 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -111,7 +111,7 @@ Kiro 登录需要 Kiro CLI:Unix 使用 `curl -fsSL https://cli.kiro.dev/instal ## 3. API 密钥目录 -opencodex 内置 69 个预设:58 个密钥预设、7 个 OAuth 预设、3 个本地预设,以及 1 个默认的 +opencodex 内置 71 个预设:60 个密钥预设、7 个 OAuth 预设、3 个本地预设,以及 1 个默认的 ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提供商的控制台,验证并保存密钥。 验证因提供商而异,Command Code 的公开目录会将密钥报告为无法验证。主要条目包括: @@ -145,6 +145,8 @@ Cline IDE/CLI 中提供,不能通过 API 使用;`minimax/minimax-m2.5` 是 | Cerebras | `https://api.cerebras.ai/v1` | | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | +| Nscale Serverless Inference | `https://inference.api.nscale.com/v1` | +| Vultr Serverless Inference | `https://api.vultrinference.com/v1` | | Baseten Model APIs | `https://inference.baseten.co/v1` | | Command Code | `https://api.commandcode.ai/provider/v1` | | Together | `https://api.together.xyz/v1` | @@ -185,6 +187,13 @@ OpenAI Chat Completions 提供商。registry 固定的 DeepInfra 模型列表 UR 并将实时发现限制为 256 KiB 和 256 条原始记录。它仅覆盖 serverless text 与 vision-language chat;独立的 image、audio 和 GPU 端点不在范围内。密钥可在 [Hyperbolic](https://app.hyperbolic.ai) 创建。 +**Nscale 与 Vultr 发现:**两个预设都会读取需要认证的 `/v1/models` 目录、保留原生模型 id,并将发现限制为 +256 KiB 和 256 条原始记录。Nscale 的目录没有 modality 字段,却混合了 chat、image 与 embedding 模型, +所以预设仅允许其官方工具调用 API 示例使用的 `meta-llama/Llama-3.1-8B-Instruct`。Vultr 目前只明确 +`kimi-k2-instruct` 支持工具调用,因此其预设仅暴露这一模型。其他记录会保持隐藏,直到提供商发布同等的 +agent-tool 证据。Nscale service token 可在 [Nscale Console](https://console.nscale.com) 创建;Vultr +inference key 可从 [Vultr Console](https://my.vultr.com) 的订阅概览复制。 + **Command Code 发现:**该预设从固定的 Provider API 主机读取 Command Code 公开的 `/provider/v1/models` 列表,保留含 `/` 的原生模型 id,并将实时发现限制为 256 KiB 和 256 条原始记录。 模型目录无需认证,因此 CLI 登录流程会将密钥报告为无法验证,而不是误报为有效。聊天请求使用已配置的 diff --git a/src/providers/free-directory.ts b/src/providers/free-directory.ts index 9d96063dd..7e2281271 100644 --- a/src/providers/free-directory.ts +++ b/src/providers/free-directory.ts @@ -134,7 +134,7 @@ const CONNECTABLE: Record = { monsterapi: openAi("https://api.monsterapi.ai/v1", "https://monsterapi.ai", { verification: "official" }), nebius: openAi("https://api.tokenfactory.nebius.com/v1", "https://studio.nebius.com", { verification: "official" }), novita: openAi("https://api.novita.ai/openai/v1", "https://novita.ai/settings/key-management", { supportLevel: "supported", verification: "official", modelsUrl: "https://api.novita.ai/openai/v1/models" }), - nscale: openAi("https://inference.api.nscale.com/v1", "https://console.nscale.com", { verification: "official" }), + nscale: openAi("https://inference.api.nscale.com/v1", "https://console.nscale.com", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.nscale.com/docs/use-cases/chat", modelsUrl: "https://inference.api.nscale.com/v1/models", lastVerified: "2026-08-03" }), nvidia: openAi("https://integrate.api.nvidia.com/v1", "https://build.nvidia.com", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.api.nvidia.com/nim/reference/llm-apis" }), publicai: openAi("https://api.publicai.co/v1", "https://publicai.co"), scaleway: openAi("https://api.scaleway.ai/v1", "https://console.scaleway.com/generative-api", { verification: "official" }), diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 91d3ad07d..a1fd69920 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -1148,6 +1148,69 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ }, note: "Serverless text and vision-language chat models only; Hyperbolic's separate image, audio, and GPU endpoints are out of scope.", }, + { + // Primary sources checked 2026-08-03: + // - docs.nscale.com documents the production OpenAI-compatible endpoint, bearer service + // tokens, /v1/models, and a tool-calling request using this exact Llama model id. + // - nscale.com/policies/terms-conditions identifies Nscale AS as the service operator and + // covers customers using its public-cloud inference offering. Maintainer: @olddonkey; + // no affiliation with Nscale. + id: "nscale", + label: "Nscale Serverless Inference", + baseUrl: "https://inference.api.nscale.com/v1", + adapter: "openai-chat", + authKind: "key", + dashboardUrl: "https://console.nscale.com", + defaultModel: "meta-llama/Llama-3.1-8B-Instruct", + models: ["meta-llama/Llama-3.1-8B-Instruct"], + liveModels: true, + preserveCustomDestination: true, + // Nscale documents tools but not parallel tool calls. Keep requests serialized. + parallelToolCalls: false, + // The API schema accepts reasoning_effort, but does not publish per-model tiers. + reasoningEfforts: [], + modelDiscovery: { + path: "models", + maxResponseBytes: 256 * 1024, + maxModels: 256, + filter: { + // Nscale's catalog mixes chat, image, and embedding rows without a modality field. + // Admit only the exact model used in its official tool-calling API example. + allOf: [{ path: ["id"], equalsAny: ["meta-llama/Llama-3.1-8B-Instruct"] }], + }, + }, + note: "Serverless OpenAI-compatible inference. Live discovery admits only the tool-capable model established by Nscale's official API example; other mixed-catalog rows remain hidden pending equivalent evidence.", + }, + { + // Primary sources checked 2026-08-03: + // - docs.vultr.com documents the fixed OpenAI-compatible base URL, per-subscription bearer + // key, /v1/models, and states that tool calling is currently limited to kimi-k2-instruct. + // - Vultr's official properties identify VULTR as a The Constant Company, LLC trademark and + // document customer API integrations. Maintainer: @olddonkey; no affiliation with Vultr. + id: "vultr", + label: "Vultr Serverless Inference", + baseUrl: "https://api.vultrinference.com/v1", + adapter: "openai-chat", + authKind: "key", + dashboardUrl: "https://my.vultr.com", + defaultModel: "kimi-k2-instruct", + models: ["kimi-k2-instruct"], + liveModels: true, + preserveCustomDestination: true, + parallelToolCalls: false, + reasoningEfforts: [], + modelDiscovery: { + path: "models", + maxResponseBytes: 256 * 1024, + maxModels: 256, + filter: { + // Vultr explicitly limits tool calling to this model. A coding agent must not select + // another chat model that cannot complete its tool loop. + allOf: [{ path: ["id"], equalsAny: ["kimi-k2-instruct"] }], + }, + }, + note: "Serverless Inference subscription API. Live discovery exposes only kimi-k2-instruct because Vultr documents it as the sole tool-calling model.", + }, { id: "baseten", label: "Baseten Model APIs", diff --git a/tests/fixtures/nscale-models.json b/tests/fixtures/nscale-models.json new file mode 100644 index 000000000..aec020b1d --- /dev/null +++ b/tests/fixtures/nscale-models.json @@ -0,0 +1,28 @@ +{ + "object": "list", + "data": [ + { + "id": "meta-llama/Llama-3.1-8B-Instruct", + "object": "model", + "owned_by": "nscale", + "context_length": 131072 + }, + { + "id": "black-forest-labs/FLUX.1-schnell", + "object": "model", + "owned_by": "nscale" + }, + { + "id": "BAAI/bge-m3", + "object": "model", + "owned_by": "nscale", + "context_length": 8192 + }, + { + "id": "Qwen/Qwen3-235B-A22B", + "object": "model", + "owned_by": "nscale", + "context_length": 131072 + } + ] +} diff --git a/tests/fixtures/vultr-models.json b/tests/fixtures/vultr-models.json new file mode 100644 index 000000000..ef032876a --- /dev/null +++ b/tests/fixtures/vultr-models.json @@ -0,0 +1,20 @@ +{ + "object": "list", + "data": [ + { + "id": "kimi-k2-instruct", + "object": "model", + "owned_by": "vultr" + }, + { + "id": "deepseek-r1", + "object": "model", + "owned_by": "vultr" + }, + { + "id": "qwen2.5-coder-32b-instruct", + "object": "model", + "owned_by": "vultr" + } + ] +} diff --git a/tests/nscale-vultr-provider.test.ts b/tests/nscale-vultr-provider.test.ts new file mode 100644 index 000000000..5ea6b3dae --- /dev/null +++ b/tests/nscale-vultr-provider.test.ts @@ -0,0 +1,259 @@ +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 { 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 NSCALE_BASE_URL = "https://inference.api.nscale.com/v1"; +const VULTR_BASE_URL = "https://api.vultrinference.com/v1"; +const NSCALE_MODEL = "meta-llama/Llama-3.1-8B-Instruct"; +const VULTR_MODEL = "kimi-k2-instruct"; +const API_KEY = "provider-test-key"; +const fixtures = { + nscale: readFileSync(join(import.meta.dir, "fixtures/nscale-models.json"), "utf8"), + vultr: readFileSync(join(import.meta.dir, "fixtures/vultr-models.json"), "utf8"), +}; +const originalFetch = globalThis.fetch; + +afterEach(() => { + globalThis.fetch = originalFetch; + clearModelCache("nscale"); + clearModelCache("vultr"); +}); + +function entry(providerId: "nscale" | "vultr") { + const value = PROVIDER_REGISTRY.find(row => row.id === providerId); + if (!value) throw new Error(`missing ${providerId} registry entry`); + return value; +} + +function config( + providerId: "nscale" | "vultr", + overrides: Partial = {}, +): OcxConfig { + const baseUrl = providerId === "nscale" ? NSCALE_BASE_URL : VULTR_BASE_URL; + return { + port: 10100, + defaultProvider: providerId, + providers: { + [providerId]: { + adapter: "openai-chat", + baseUrl, + authMode: "key", + apiKey: API_KEY, + liveModels: true, + // Fetches are fixture-only; this avoids public-host DNS classification affecting tests. + allowPrivateNetwork: true, + ...overrides, + }, + }, + }; +} + +describe("Nscale and Vultr provider presets", () => { + test("register fixed transports with bounded, evidence-gated discovery", () => { + expect(entry("nscale")).toMatchObject({ + id: "nscale", + label: "Nscale Serverless Inference", + adapter: "openai-chat", + baseUrl: NSCALE_BASE_URL, + authKind: "key", + dashboardUrl: "https://console.nscale.com", + defaultModel: NSCALE_MODEL, + models: [NSCALE_MODEL], + liveModels: true, + preserveCustomDestination: true, + parallelToolCalls: false, + reasoningEfforts: [], + modelDiscovery: { + path: "models", + maxResponseBytes: 256 * 1024, + maxModels: 256, + filter: { + allOf: [{ path: ["id"], equalsAny: [NSCALE_MODEL] }], + }, + }, + }); + expect(entry("vultr")).toMatchObject({ + id: "vultr", + label: "Vultr Serverless Inference", + adapter: "openai-chat", + baseUrl: VULTR_BASE_URL, + authKind: "key", + dashboardUrl: "https://my.vultr.com", + defaultModel: VULTR_MODEL, + models: [VULTR_MODEL], + liveModels: true, + preserveCustomDestination: true, + parallelToolCalls: false, + reasoningEfforts: [], + modelDiscovery: { + path: "models", + maxResponseBytes: 256 * 1024, + maxModels: 256, + filter: { + allOf: [{ path: ["id"], equalsAny: [VULTR_MODEL] }], + }, + }, + }); + }); + + test("derives CLI, dashboard, and key-login presets without persisting trust policy", () => { + expect(buildInitProviders()).toEqual(deriveInitProviders()); + for (const providerId of ["nscale", "vultr"] as const) { + const baseUrl = providerId === "nscale" ? NSCALE_BASE_URL : VULTR_BASE_URL; + expect(KEY_LOGIN_PROVIDERS[providerId]).toMatchObject({ + adapter: "openai-chat", + baseUrl, + liveModels: true, + reasoningEfforts: [], + }); + expect(buildInitProviders().find(row => row.id === providerId)).toMatchObject({ + kind: "key", + adapter: "openai-chat", + baseUrl, + }); + expect(deriveProviderPresets().find(row => row.id === providerId)).toMatchObject({ + auth: "key", + }); + + const seed = providerConfigSeed(entry(providerId)); + expect(seed).toMatchObject({ + adapter: "openai-chat", + baseUrl, + authMode: "key", + liveModels: true, + parallelToolCalls: false, + reasoningEfforts: [], + }); + expect(seed).not.toHaveProperty("modelDiscovery"); + expect(seed).not.toHaveProperty("preserveCustomDestination"); + expect(KEY_LOGIN_PROVIDERS[providerId]).not.toHaveProperty("modelDiscovery"); + expect(KEY_LOGIN_PROVIDERS[providerId]).not.toHaveProperty("preserveCustomDestination"); + } + }); + + test("lists and validates models with the provider bearer key", async () => { + for (const providerId of ["nscale", "vultr"] as const) { + const baseUrl = providerId === "nscale" ? NSCALE_BASE_URL : VULTR_BASE_URL; + expect(buildModelsRequest(config(providerId).providers[providerId]!, API_KEY, providerId)).toEqual({ + url: `${baseUrl}/models`, + headers: { Authorization: `Bearer ${API_KEY}` }, + }); + + globalThis.fetch = (async (input, init) => { + expect(String(input)).toBe(`${baseUrl}/models`); + expect(new Headers(init?.headers).get("authorization")).toBe(`Bearer ${API_KEY}`); + expect(init?.redirect).toBe("error"); + return new Response(fixtures[providerId], { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + + expect(await validateApiKey(providerId, KEY_LOGIN_PROVIDERS[providerId]!, API_KEY)).toBe(true); + } + }); + + test("filters mixed catalogs to officially documented tool-capable models", async () => { + for (const providerId of ["nscale", "vultr"] as const) { + const baseUrl = providerId === "nscale" ? NSCALE_BASE_URL : VULTR_BASE_URL; + globalThis.fetch = (async (input, init) => { + expect(String(input)).toBe(`${baseUrl}/models`); + expect(new Headers(init?.headers).get("authorization")).toBe(`Bearer ${API_KEY}`); + expect(init?.redirect).toBe("manual"); + return new Response(fixtures[providerId], { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + + const routed = (await gatherRoutedModels(withStubbedProviderFetch(config(providerId)))) + .filter(row => row.provider === providerId); + expect(routed.map(row => row.id)).toEqual([ + providerId === "nscale" ? NSCALE_MODEL : VULTR_MODEL, + ]); + expect(routed[0]).toMatchObject({ + reasoningEfforts: [], + }); + if (providerId === "nscale") { + expect(routed[0]?.contextWindow).toBe(131_072); + } + } + }); + + test("preserves namespaced Nscale ids in raw and flattened selector forms", () => { + expect(routedSlug("nscale", NSCALE_MODEL)).toBe("nscale/meta-llama-Llama-3.1-8B-Instruct"); + for (const selector of [ + `nscale/${NSCALE_MODEL}`, + routedSlug("nscale", NSCALE_MODEL), + ]) { + expect(routeModel(config("nscale"), selector).modelId).toBe(NSCALE_MODEL); + } + }); + + test("routes tool requests to the fixed chat endpoints without parallel or reasoning claims", () => { + for (const [providerId, modelId] of [ + ["nscale", NSCALE_MODEL], + ["vultr", VULTR_MODEL], + ] as const) { + const baseUrl = providerId === "nscale" ? NSCALE_BASE_URL : VULTR_BASE_URL; + const route = routeModel(config(providerId), `${providerId}/${modelId}`); + const request = createOpenAIChatAdapter(route.provider).buildRequest({ + modelId: route.modelId, + context: { + messages: [{ role: "user", content: "ping", timestamp: 0 }], + tools: [{ name: "lookup", description: "Lookup", parameters: { type: "object" } }], + }, + stream: true, + options: { reasoning: "high" }, + }); + const body = JSON.parse(String(request.body)) as Record; + + expect(request.url).toBe(`${baseUrl}/chat/completions`); + expect(request.headers.Authorization).toBe(`Bearer ${API_KEY}`); + expect(body.model).toBe(modelId); + expect(body.tools).toBeArray(); + expect(body.parallel_tool_calls).toBe(false); + expect(body).not.toHaveProperty("reasoning_effort"); + } + }); + + test("does not retarget older same-named custom providers or apply fixed discovery", () => { + for (const providerId of ["nscale", "vultr"] as const) { + const custom = config(providerId, { baseUrl: "https://custom.example/v1" }); + expect(routeModel(custom, `${providerId}/custom-model`).provider).toMatchObject({ + adapter: "openai-chat", + baseUrl: "https://custom.example/v1", + authMode: "key", + }); + expect(buildModelsRequest(custom.providers[providerId]!, "custom-key", providerId)).toEqual({ + url: "https://custom.example/v1/models", + headers: { Authorization: "Bearer custom-key" }, + }); + + expect(routeModel(config(providerId, { + adapter: "anthropic", + baseUrl: "https://custom.example/anthropic", + }), `${providerId}/custom-model`).provider).toMatchObject({ + adapter: "anthropic", + baseUrl: "https://custom.example/anthropic", + authMode: "key", + }); + } + }); +}); diff --git a/tests/provider-registry-parity.test.ts b/tests/provider-registry-parity.test.ts index a1be64d29..75299e14b 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", "baseten", "commandcode", "together", "fireworks", "firepass", "moonshot", + "deepseek", "cerebras", "deepinfra", "hyperbolic", "nscale", "vultr", "baseten", "commandcode", "together", "fireworks", "firepass", "moonshot", "huggingface", "nvidia", "venice", "zai", "zhipu-bigmodel", "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",