From 9ad12831276a78b9ce2307d1a10cf7342b57ab76 Mon Sep 17 00:00:00 2001 From: olddonkey Date: Sat, 8 Aug 2026 13:37:54 -0700 Subject: [PATCH 1/2] feat(providers): add Novita AI 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 | 11 +- .../docs/ko/getting-started/quickstart.md | 2 +- .../src/content/docs/ko/guides/providers.md | 15 +- .../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 | 10 +- src/providers/registry.ts | 43 ++++ tests/fixtures/novita-models.json | 57 +++++ tests/novita-provider.test.ts | 231 ++++++++++++++++++ tests/provider-registry-parity.test.ts | 2 +- 14 files changed, 384 insertions(+), 17 deletions(-) create mode 100644 tests/fixtures/novita-models.json create mode 100644 tests/novita-provider.test.ts diff --git a/docs-site/src/content/docs/getting-started/quickstart.md b/docs-site/src/content/docs/getting-started/quickstart.md index d77e7ec23..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 78 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 f08e6ca72..509ecd646 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -230,7 +230,7 @@ selectors, then retry. Signing in from a machine with no existing `kiro-cli` ses ## 3. API-key catalog -opencodex ships 78 built-in presets: 66 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: @@ -274,6 +274,7 @@ free-experimentation model. | DigitalOcean Serverless Inference | `https://inference.do-ai.run/v1` | | Scaleway Generative APIs | `https://api.scaleway.ai/v1` | | Featherless AI | `https://api.featherless.ai/v1` | +| Novita AI | `https://api.novita.ai/openai/v1` | | Together | `https://api.together.xyz/v1` | | Fireworks | `https://api.fireworks.ai/inference/v1` | | Moonshot (Kimi API) · Kimi (coding) | `https://api.moonshot.ai/v1` · `https://api.kimi.com/coding/v1` | @@ -394,6 +395,14 @@ tens-of-thousands-model catalog is never downloaded or cached in full. Because ` individual plans for interactive/prototyping use; arbitrary applications require a Scale plan. Create a key in the [Featherless dashboard](https://featherless.ai/account/api-keys). +**Novita discovery.** The key-based preset uses the `openai-chat` adapter and sends its Bearer key +only to Novita's fixed OpenAI-compatible host. Its public model list is filtered to rows that report +both `model_type: chat` and the `chat/completions` endpoint, with discovery capped at 512 KiB and 256 +raw rows. Because the catalog is public, login reports the key as unverifiable instead of treating a +successful list response as proof. Model capabilities vary, so the preset does not advertise +provider-wide parallel tool calls or OpenAI `reasoning_effort`. Create a key in +[Novita's key manager](https://novita.ai/settings/key-management). + > **Baseten scope:** The preset covers Baseten's shared [Model APIs](https://docs.baseten.co/inference/model-apis/overview) > only. Use a personal [API key](https://docs.baseten.co/organization/api-keys) for local use, or a team key > with **Call Model APIs** access for shared/production use. Dedicated Truss `predict` endpoints use different 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 1c2e6e349..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. **プロバイダーを選択してください** — 78 個の組み込みレジストリプリセットのいずれか、または `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 aa5035320..2a58f4ee1 100644 --- a/docs-site/src/content/docs/ja/guides/providers.md +++ b/docs-site/src/content/docs/ja/guides/providers.md @@ -157,7 +157,7 @@ Kiro のログインには Kiro CLI が必要です。Unix では `curl -fsSL ht ## 3. API キーカタログ -opencodex には組み込みプリセットが 78 個含まれています。キー方式 66、OAuth 8、ローカル 3、 +opencodex には組み込みプリセットが 79 個含まれています。キー方式 67、OAuth 8、ローカル 3、 デフォルト ChatGPT 転送プリセット 1 です。ダッシュボードの **Add provider** ピッカーはキー発行ページを開き、 入力したキーを検証した後保存します(検証はプロバイダー固有です)。主な項目は以下のとおりです: @@ -201,6 +201,7 @@ Cline IDE/CLI のみで API からは使えません。`minimax/minimax-m2.5` | DigitalOcean Serverless Inference | `https://inference.do-ai.run/v1` | | Scaleway Generative APIs | `https://api.scaleway.ai/v1` | | Featherless AI | `https://api.featherless.ai/v1` | +| Novita AI | `https://api.novita.ai/openai/v1` | | Together | `https://api.together.xyz/v1` | | Fireworks | `https://api.fireworks.ai/inference/v1` | | Moonshot (Kimi API) · Kimi (coding) | `https://api.moonshot.ai/v1` · `https://api.kimi.com/coding/v1` | @@ -300,6 +301,14 @@ raw 100 行が上限で、数万件の catalog 全体を download / cache しま 用途に限られ、任意の application には Scale plan が必要です。キーは [Featherless dashboard](https://featherless.ai/account/api-keys) で作成します。 +**Novita の discovery:** キー方式のプリセットは `openai-chat` adapter を使用し、Bearer key は +Novita の固定 OpenAI 互換 host にだけ送信します。公開 model list から `model_type: chat` と +`chat/completions` endpoint の両方を報告する row だけを残し、discovery を 512 KiB と raw 256 行に +制限します。catalog は公開されているため、login は list 成功を key の証明にせず「検証不能」と報告します。 +model ごとに capability が異なるため、provider 全体の parallel tool call と OpenAI +`reasoning_effort` は宣伝しません。キーは +[Novita key manager](https://novita.ai/settings/key-management) で作成します。 + > **Baseten の対象範囲:** このプリセットは Baseten の共有 [Model APIs](https://docs.baseten.co/inference/model-apis/overview) > のみを対象とします。ローカル利用では個人の [API キー](https://docs.baseten.co/organization/api-keys)を、 > 共有/本番利用では **Call Model APIs** 権限を持つチームキーを使用してください。専用 Truss `predict` 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 bebbb30dd..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. **프로바이더 선택** — 내장 레지스트리 프리셋 78개 중 하나를 고르거나 `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 65261a1bb..f58efd01f 100644 --- a/docs-site/src/content/docs/ko/guides/providers.md +++ b/docs-site/src/content/docs/ko/guides/providers.md @@ -156,7 +156,7 @@ Kiro 로그인에는 Kiro CLI가 필요합니다. Unix에서는 `curl -fsSL http ## 3. API 키 카탈로그 -opencodex에는 빌트인 프리셋이 78개 들어 있습니다. 키 방식 66개, OAuth 8개, 로컬 3개, +opencodex에는 빌트인 프리셋이 79개 들어 있습니다. 키 방식 67개, OAuth 8개, 로컬 3개, 기본 ChatGPT 포워드 프리셋 1개입니다. 대시보드의 **Add provider** 선택기는 키 발급 페이지를 열고, 입력한 키를 검증한 뒤 저장합니다(검증은 프로바이더별로 다릅니다). 주요 항목은 다음과 같습니다: @@ -201,6 +201,7 @@ Cline IDE/CLI에서만 제공되며 API로는 사용할 수 없습니다. `minim | DigitalOcean Serverless Inference | `https://inference.do-ai.run/v1` | | Scaleway Generative APIs | `https://api.scaleway.ai/v1` | | Featherless AI | `https://api.featherless.ai/v1` | +| Novita AI | `https://api.novita.ai/openai/v1` | | Together | `https://api.together.xyz/v1` | | Fireworks | `https://api.fireworks.ai/inference/v1` | | Moonshot (Kimi API) · Kimi (coding) | `https://api.moonshot.ai/v1` · `https://api.kimi.com/coding/v1` | @@ -291,12 +292,12 @@ discovery를 256 KiB와 raw 행 256개로 제한합니다. agent 전용 및 dedi Project ID가 포함된 URL과 dedicated deployment는 custom provider로 설정하세요. API 키는 [Scaleway console](https://console.scaleway.com/generative-api)에서 생성합니다. -**Featherless 검색:** 고정된 OpenAI 호환 호스트에서 인증하고, chat 및 현재 plan으로 필터링한 인기 모델의 -첫 100개만 요청합니다. 각 행이 plan 사용 가능, Hugging Face gate 없음, `features.tool_use: true`를 -독립적으로 보고하지 않으면 fail closed로 제외합니다. 검색은 128 KiB와 raw 100행으로 제한되어 수만 개의 -전체 catalog를 다운로드하거나 캐시하지 않습니다. `/v1/models`는 문서상 인증 여부와 관계없이 호출할 수 있어 입력한 키의 유효성을 증명할 수 없지만, chat request는 설정된 Bearer 키로 인증됩니다. 개인 plan은 interactive/prototype 용도로 제한되며 임의의 -application에는 Scale plan이 필요합니다. 키는 -[Featherless dashboard](https://featherless.ai/account/api-keys)에서 생성합니다. +**Novita 검색:** 키 기반 프리셋은 `openai-chat` adapter를 사용하며 Bearer key를 Novita의 고정 +OpenAI 호환 host에만 보냅니다. 공개 model list에서 `model_type: chat`과 `chat/completions` endpoint를 +모두 보고하는 행만 유지하고 discovery를 512 KiB와 raw 256행으로 제한합니다. catalog가 공개되어 있으므로 +login은 list 성공을 key 증명으로 간주하지 않고 검증 불가로 보고합니다. model별 capability가 다르므로 +provider 전체 parallel tool call이나 OpenAI `reasoning_effort`를 광고하지 않습니다. 키는 +[Novita key manager](https://novita.ai/settings/key-management)에서 생성합니다. > **Baseten 범위:** 이 프리셋은 Baseten의 공유 [Model APIs](https://docs.baseten.co/inference/model-apis/overview)만 > 지원합니다. 로컬 사용에는 개인 [API 키](https://docs.baseten.co/organization/api-keys)를, 공유/프로덕션 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 ec7dc20dc..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. **Выбор провайдера** — выберите один из 78 встроенных пресетов реестра или `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 38f7e5e52..157c3514d 100644 --- a/docs-site/src/content/docs/ru/guides/providers.md +++ b/docs-site/src/content/docs/ru/guides/providers.md @@ -167,7 +167,7 @@ Inline JSON и лишние позиционные аргументы откло ## 3. Каталог API-ключей -opencodex поставляется с 78 встроенными пресетами: 66 на основе ключей, восемь OAuth, три локальных и +opencodex поставляется с 79 встроенными пресетами: 67 на основе ключей, восемь OAuth, три локальных и один пресет ChatGPT-форварда по умолчанию. Селектор **Add provider** в дашборде открывает страницу выдачи ключей провайдера, проверяет ключ и сохраняет его; проверка зависит от провайдера. Наиболее заметные записи: @@ -212,6 +212,7 @@ opencodex поставляется с 78 встроенными пресетам | DigitalOcean Serverless Inference | `https://inference.do-ai.run/v1` | | Scaleway Generative APIs | `https://api.scaleway.ai/v1` | | Featherless AI | `https://api.featherless.ai/v1` | +| Novita AI | `https://api.novita.ai/openai/v1` | | Together | `https://api.together.xyz/v1` | | Fireworks | `https://api.fireworks.ai/inference/v1` | | Moonshot (Kimi API) · Kimi (coding) | `https://api.moonshot.ai/v1` · `https://api.kimi.com/coding/v1` | @@ -326,6 +327,14 @@ Project ID и dedicated deployment настраиваются как custom prov Индивидуальные plan предназначены для interactive/prototype; произвольные приложения требуют Scale plan. Ключ создаётся в [дашборде Featherless](https://featherless.ai/account/api-keys). +**Discovery для Novita.** Пресет с ключом использует adapter `openai-chat` и отправляет Bearer key +только на фиксированный OpenAI-совместимый host Novita. Из публичного списка моделей остаются лишь +строки, одновременно указывающие `model_type: chat` и endpoint `chat/completions`; discovery ограничен +512 KiB и 256 исходными строками. Поскольку catalog публичный, login сообщает, что ключ невозможно +проверить, а не считает успешный список доказательством. Возможности зависят от модели, поэтому пресет +не заявляет provider-wide parallel tool calls или OpenAI `reasoning_effort`. Ключ создаётся в +[Novita key manager](https://novita.ai/settings/key-management). + > **Область Baseten:** пресет поддерживает только общие [Model APIs](https://docs.baseten.co/inference/model-apis/overview) > Baseten. Для локальной работы используйте личный [API-ключ](https://docs.baseten.co/organization/api-keys), > а для общего/промышленного использования — командный ключ с правом **Call Model APIs**. Выделенные конечные точки Truss `predict` используют другие хосты и 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 107012b99..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 的 78 个预设中选择一个,或选择 `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 2380130b5..ed5c26523 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -145,7 +145,7 @@ Kiro 登录需要 Kiro CLI:Unix 使用 `curl -fsSL https://cli.kiro.dev/instal ## 3. API 密钥目录 -opencodex 内置 78 个预设:66 个密钥预设、8 个 OAuth 预设、3 个本地预设,以及 1 个默认的 +opencodex 内置 79 个预设:67 个密钥预设、8 个 OAuth 预设、3 个本地预设,以及 1 个默认的 ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提供商的控制台,验证并保存密钥。 验证因提供商而异。主要条目包括: @@ -189,6 +189,7 @@ Cline IDE/CLI 中提供,不能通过 API 使用;`minimax/minimax-m2.5` 是 | DigitalOcean Serverless Inference | `https://inference.do-ai.run/v1` | | Scaleway Generative APIs | `https://api.scaleway.ai/v1` | | Featherless AI | `https://api.featherless.ai/v1` | +| Novita AI | `https://api.novita.ai/openai/v1` | | Together | `https://api.together.xyz/v1` | | Fireworks | `https://api.fireworks.ai/inference/v1` | | Moonshot (Kimi API) · Kimi (coding) | `https://api.moonshot.ai/v1` · `https://api.kimi.com/coding/v1` | @@ -280,6 +281,13 @@ Hugging Face gate,且 `features.tool_use: true`。发现上限为 128 KiB 和 缓存包含数万模型的完整目录。由于 `/v1/models` 在文档中可带或不带鉴权调用,它无法证明输入的密钥有效;chat 请求仍会使用已配置的 Bearer 密钥认证。个人 plan 仅适用于 interactive/prototype 用途;任意 application 需要使用 Scale plan。密钥可在 [Featherless dashboard](https://featherless.ai/account/api-keys) 创建。 +**Novita 发现:**密钥预设使用 `openai-chat` adapter,并只向 Novita 的固定 OpenAI 兼容主机发送 +Bearer key。公开模型列表只保留同时报告 `model_type: chat` 和 `chat/completions` endpoint 的记录, +发现上限为 512 KiB 和 256 条原始记录。由于 catalog 是公开的,login 会报告密钥无法验证,而不会把 +成功列出模型当作密钥有效的证明。模型能力各不相同,因此预设不会声明 provider-wide parallel tool calls +或 OpenAI `reasoning_effort`。密钥可在 +[Novita key manager](https://novita.ai/settings/key-management) 创建。 + > **Baseten 范围:**该预设仅覆盖 Baseten 的共享 [Model APIs](https://docs.baseten.co/inference/model-apis/overview)。 > 本地使用可选择个人 [API 密钥](https://docs.baseten.co/organization/api-keys);共享或生产用途请使用具备 > **Call Model APIs** 权限的团队密钥。 diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 4950c92b1..5cb0c124f 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -1730,6 +1730,49 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ }, note: "Authenticated first page of popular chat models only; live discovery admits at most 100 plan-available, ungated rows whose metadata explicitly reports tool use.", }, + { + // Primary sources checked 2026-08-08: + // - https://novita.ai/docs/api-reference/model-apis-llm-create-chat-completion and + // https://novita.ai/docs/api-reference/model-apis-llm-list-models document the fixed + // OpenAI-compatible Chat Completions and model-list endpoints. + // - https://novita.ai/docs/api-reference/basic-authentication documents Bearer API keys. + // - https://novita.ai/legal/terms-of-service (updated 2026-08-05) expressly covers AI + // inference APIs, third-party Model Providers, and customer Input/Output processing. + // - https://huggingface.co/docs/inference-providers/main/providers/novita lists Novita as an + // Inference Providers partner for chat/VLM traffic, independently supporting routing use. + // - https://tsdr.uspto.gov/statusview/sn99255805 is the official use-in-commerce record + // connecting the NOVITA AI mark to Hivemind Labs, Inc., a Delaware corporation. The mark + // application is now abandoned; it is cited only as the public operator-identity record. + // Maintainer: @olddonkey; no affiliation with Novita AI or Hivemind Labs, Inc. + id: "novita", + label: "Novita AI", + baseUrl: "https://api.novita.ai/openai/v1", + adapter: "openai-chat", + authKind: "key", + dashboardUrl: "https://novita.ai/settings/key-management", + liveModels: true, + preserveCustomDestination: true, + // The live catalog is public even though the reference shows an Authorization header, so a + // successful model fetch cannot prove that a supplied key is valid. + apiKeyValidation: "unknown", + // The request reference documents tools but not a provider-wide parallel-tool contract. + parallelToolCalls: false, + // Novita exposes model-specific thinking flags, not an OpenAI reasoning_effort contract. + reasoningEfforts: [], + modelDiscovery: { + path: "models", + maxResponseBytes: 512 * 1024, + maxModels: 256, + filter: { + // Require both Novita's chat classification and the exact configured wire endpoint. + allOf: [ + { path: ["model_type"], equalsAny: ["chat"] }, + { path: ["endpoints"], containsAny: ["chat/completions"] }, + ], + }, + }, + note: "Public live catalog filtered to rows that explicitly report chat type and Chat Completions support; key validity remains unknown until an authenticated inference request.", + }, // FREEZE 2026-07-10: exact serverless ids remain auth-gated/unverified. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md. { id: "together", label: "Together", baseUrl: "https://api.together.xyz/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://api.together.xyz/settings/api-keys" }, { id: "fireworks", label: "Fireworks", baseUrl: "https://api.fireworks.ai/inference/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://fireworks.ai/account/api-keys" }, diff --git a/tests/fixtures/novita-models.json b/tests/fixtures/novita-models.json new file mode 100644 index 000000000..45f959468 --- /dev/null +++ b/tests/fixtures/novita-models.json @@ -0,0 +1,57 @@ +{ + "data": [ + { + "id": "moonshotai/kimi-k3", + "object": "model", + "owned_by": "unknown", + "model_type": "chat", + "context_size": 1048576, + "max_output_tokens": 1048576, + "features": ["serverless", "function-calling", "structured-outputs", "reasoning"], + "endpoints": ["chat/completions", "anthropic", "responses"], + "input_modalities": ["text", "image", "video"], + "output_modalities": ["text"] + }, + { + "id": "zai-org/glm-5.2", + "object": "model", + "owned_by": "unknown", + "model_type": "chat", + "context_size": 1048576, + "max_output_tokens": 131072, + "features": ["function-calling", "structured-outputs", "reasoning", "serverless"], + "endpoints": ["chat/completions", "anthropic"], + "input_modalities": ["text"], + "output_modalities": ["text"] + }, + { + "id": "example/sparse-chat-model", + "object": "model", + "owned_by": "fixture", + "model_type": "chat", + "context_size": null, + "features": null, + "endpoints": ["chat/completions"], + "input_modalities": null, + "output_modalities": null + }, + { + "id": "baai/bge-m3", + "object": "model", + "owned_by": "unknown", + "model_type": "embedding", + "context_size": 8192, + "features": ["serverless"], + "endpoints": ["embeddings"] + }, + { + "id": "example/responses-only-model", + "object": "model", + "owned_by": "fixture", + "model_type": "chat", + "context_size": 131072, + "features": ["function-calling"], + "endpoints": ["responses"] + } + ] +} diff --git a/tests/novita-provider.test.ts b/tests/novita-provider.test.ts new file mode 100644 index 000000000..1b8320dec --- /dev/null +++ b/tests/novita-provider.test.ts @@ -0,0 +1,231 @@ +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 NOVITA_FIXTURE = readFileSync(join(import.meta.dir, "fixtures/novita-models.json"), "utf8"); +const BASE_URL = "https://api.novita.ai/openai/v1"; +const MODELS_URL = `${BASE_URL}/models`; +const TEST_KEY = "novita-test-key"; +const originalFetch = globalThis.fetch; + +afterEach(() => { + globalThis.fetch = originalFetch; + clearModelCache("novita"); +}); + +function registryEntry() { + const entry = PROVIDER_REGISTRY.find(row => row.id === "novita"); + if (!entry) throw new Error("missing novita registry entry"); + return entry; +} + +function providerConfig(overrides: Partial = {}): OcxConfig { + return { + port: 10100, + defaultProvider: "novita", + providers: { + novita: { + adapter: "openai-chat", + baseUrl: BASE_URL, + authMode: "key", + apiKey: TEST_KEY, + liveModels: true, + ...overrides, + }, + }, + }; +} + +describe("Novita AI provider", () => { + test("registers a fixed transport with bounded Chat Completions discovery", () => { + expect(registryEntry()).toMatchObject({ + id: "novita", + label: "Novita AI", + adapter: "openai-chat", + baseUrl: BASE_URL, + authKind: "key", + dashboardUrl: "https://novita.ai/settings/key-management", + liveModels: true, + preserveCustomDestination: true, + apiKeyValidation: "unknown", + parallelToolCalls: false, + reasoningEfforts: [], + modelDiscovery: { + path: "models", + maxResponseBytes: 524_288, + maxModels: 256, + filter: { + allOf: [ + { path: ["model_type"], equalsAny: ["chat"] }, + { path: ["endpoints"], containsAny: ["chat/completions"] }, + ], + }, + }, + }); + expect(registryEntry().note).toContain("key validity remains unknown"); + }); + + test("derives CLI and dashboard presets without persisting registry trust policy", () => { + const entry = registryEntry(); + expect(buildInitProviders()).toEqual(deriveInitProviders()); + expect(KEY_LOGIN_PROVIDERS.novita).toMatchObject({ + adapter: "openai-chat", + baseUrl: BASE_URL, + dashboardUrl: entry.dashboardUrl, + liveModels: true, + apiKeyValidation: "unknown", + reasoningEfforts: [], + }); + expect(buildInitProviders().find(row => row.id === "novita")).toMatchObject({ + kind: "key", + adapter: "openai-chat", + baseUrl: BASE_URL, + }); + expect(deriveProviderPresets().find(row => row.id === "novita")).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.novita).not.toHaveProperty("modelDiscovery"); + expect(KEY_LOGIN_PROVIDERS.novita).not.toHaveProperty("preserveCustomDestination"); + }); + + test("uses the documented Bearer endpoint without treating its public catalog as key proof", async () => { + expect(buildModelsRequest(providerConfig().providers.novita!, TEST_KEY, "novita")).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("novita", KEY_LOGIN_PROVIDERS.novita!, TEST_KEY)).toBe("unknown"); + }); + + test("filters by chat type and endpoint while preserving safe 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(NOVITA_FIXTURE, { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + + const config = withStubbedProviderFetch(providerConfig()); + const models = (await gatherRoutedModels(config)).filter(row => row.provider === "novita"); + expect(models.map(row => row.id)).toEqual([ + "example/sparse-chat-model", + "moonshotai/kimi-k3", + "zai-org/glm-5.2", + ]); + + const kimi = models.find(row => row.id === "moonshotai/kimi-k3"); + const glm = models.find(row => row.id === "zai-org/glm-5.2"); + const sparse = models.find(row => row.id === "example/sparse-chat-model"); + expect(kimi).toMatchObject({ + owned_by: "unknown", + contextWindow: 1_048_576, + inputModalities: ["text", "image"], + capabilities: ["serverless", "function-calling", "structured-outputs", "reasoning"], + reasoningEfforts: [], + }); + expect(glm).toMatchObject({ + contextWindow: 1_048_576, + inputModalities: ["text"], + reasoningEfforts: [], + }); + expect(sparse).toMatchObject({ owned_by: "fixture", reasoningEfforts: [] }); + expect(sparse).not.toHaveProperty("contextWindow"); + expect(sparse).not.toHaveProperty("inputModalities"); + + for (const modelId of models.map(row => row.id)) { + expect(routeModel(config, `novita/${modelId}`).modelId).toBe(modelId); + expect(routeModel(config, routedSlug("novita", modelId)).modelId).toBe(modelId); + } + }); + + test("routes tool requests without unsupported reasoning or parallel fields", () => { + const modelId = "moonshotai/kimi-k3"; + const route = routeModel(providerConfig(), `novita/${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).not.toHaveProperty("parallel_tool_calls"); + 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, "novita/custom-model"); + expect(route.provider).toMatchObject({ + adapter: "openai-chat", + baseUrl: "https://custom.example/v1", + authMode: "key", + }); + expect(resolveProviderModelDiscovery("novita", customConfig.providers.novita!).spec).toBeUndefined(); + expect(buildModelsRequest(customConfig.providers.novita!, "custom-key", "novita")).toEqual({ + url: "https://custom.example/v1/models", + headers: { Authorization: "Bearer custom-key" }, + }); + + const nearMissConfig = providerConfig({ baseUrl: "https://api.novita.ai/openai/v2" }); + expect( + resolveProviderModelDiscovery("novita", nearMissConfig.providers.novita!).spec, + ).toBeUndefined(); + + const customAdapter = routeModel(providerConfig({ + adapter: "anthropic", + baseUrl: "https://custom.example/anthropic", + }), "novita/custom-model"); + expect(customAdapter.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 049f05157..7b903f837 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", "chutes", "deepinfra", "hyperbolic", "nscale", "vultr", "baseten", "commandcode", "sambanova", "nebius", "digitalocean", "scaleway", "featherless", "together", "fireworks", "firepass", "moonshot", + "deepseek", "cerebras", "chutes", "deepinfra", "hyperbolic", "nscale", "vultr", "baseten", "commandcode", "sambanova", "nebius", "digitalocean", "scaleway", "featherless", "novita", "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 d2cdc68831cc7614733762ca4ce20e211a73eb25 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:56:42 +0200 Subject: [PATCH 2/2] docs(providers): state Novita model ids must not be rewritten Document that slash-delimited ids from Novita discovery must be preserved exactly as returned before routing. --- docs-site/src/content/docs/guides/providers.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 509ecd646..14bb0c08c 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -398,10 +398,11 @@ Create a key in the [Featherless dashboard](https://featherless.ai/account/api-k **Novita discovery.** The key-based preset uses the `openai-chat` adapter and sends its Bearer key only to Novita's fixed OpenAI-compatible host. Its public model list is filtered to rows that report both `model_type: chat` and the `chat/completions` endpoint, with discovery capped at 512 KiB and 256 -raw rows. Because the catalog is public, login reports the key as unverifiable instead of treating a -successful list response as proof. Model capabilities vary, so the preset does not advertise -provider-wide parallel tool calls or OpenAI `reasoning_effort`. Create a key in -[Novita's key manager](https://novita.ai/settings/key-management). +raw rows. Model ids must be preserved exactly as Novita returns them, including slash-delimited ids, +and must not be normalized or rewritten before routing. Because the catalog is public, login reports +the key as unverifiable instead of treating a successful list response as proof. Model capabilities +vary, so the preset does not advertise provider-wide parallel tool calls or OpenAI `reasoning_effort`. +Create a key in [Novita's key manager](https://novita.ai/settings/key-management). > **Baseten scope:** The preset covers Baseten's shared [Model APIs](https://docs.baseten.co/inference/model-apis/overview) > only. Use a personal [API key](https://docs.baseten.co/organization/api-keys) for local use, or a team key