diff --git a/docs-site/src/content/docs/getting-started/quickstart.md b/docs-site/src/content/docs/getting-started/quickstart.md index 6dbd3135c..9db9cd110 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 66 built-in registry presets or `custom` to type a base +1. **Pick a provider** — choose one of the 68 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 d0357ee0a..05095178e 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 66 built-in presets: 55 key-based, seven OAuth, three local, and the default +opencodex ships 68 built-in presets: 57 key-based, seven OAuth, three local, and the default ChatGPT-forward preset. The dashboard's **Add provider** picker opens a key provider's dashboard, validates the key, and stores it. Notable entries: @@ -210,6 +210,8 @@ validates the key, and stores it. Notable entries: | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | | Baseten Model APIs | `https://inference.baseten.co/v1` | +| DigitalOcean Serverless Inference | `https://inference.do-ai.run/v1` | +| Scaleway Generative APIs | `https://api.scaleway.ai/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` | @@ -258,6 +260,18 @@ 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). +**DigitalOcean discovery.** The preset uses a model access key against the fixed shared Serverless +Inference host and intersects the authenticated `/v1/models` response with DigitalOcean's +docs-backed Chat Completions allowlist. Unknown, Responses-only, embedding, and media-generation +ids fail closed. Discovery is capped at 256 KiB and 256 raw rows; agent-specific and dedicated +hosts are out of scope. Create a key in the [DigitalOcean Control Panel](https://cloud.digitalocean.com/model-studio/manage-keys). + +**Scaleway discovery.** The preset intersects the authenticated model list with Scaleway's current +Serverless chat-model allowlist, excluding embeddings and audio transcription, and caps discovery +at 128 KiB and 128 raw rows. It uses the default Project's shared endpoint; project-qualified URLs +and dedicated deployments require a custom provider. Create an API key in the +[Scaleway console](https://console.scaleway.com/generative-api). + > **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 fee4fbd26..f129296e9 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. **プロバイダーを選択してください** — 66 の組み込みレジストリ プリセットのいずれか、または `custom` を選択してベースを入力します +1. **プロバイダーを選択してください** — 68 の組み込みレジストリ プリセットのいずれか、または `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 4245b1e9a..ac869eacc 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 には組み込みプリセットが 66 個含まれています。キー方式 55、OAuth 7、ローカル 3、 +opencodex には組み込みプリセットが 68 個含まれています。キー方式 57、OAuth 7、ローカル 3、 デフォルト ChatGPT 転送プリセット 1 です。ダッシュボードの **Add provider** ピッカーはキー発行ページを開き、 入力したキーを検証した後保存します。主な項目は以下のとおりです: @@ -139,6 +139,8 @@ opencodex には組み込みプリセットが 66 個含まれています。キ | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | | Baseten Model APIs | `https://inference.baseten.co/v1` | +| DigitalOcean Serverless Inference | `https://inference.do-ai.run/v1` | +| Scaleway Generative APIs | `https://api.scaleway.ai/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` | @@ -181,6 +183,17 @@ Volcengine Agent Plan は `openai-responses` アダプターでネイティブ R vision-language chat のみを対象とし、別系統の image、audio、GPU endpoint は対象外です。キーは [Hyperbolic](https://app.hyperbolic.ai) で作成します。 +**DigitalOcean の discovery:** preset は model access key を固定の共有 Serverless Inference ホストで使い、 +認証済み `/v1/models` の応答と DigitalOcean の公式ドキュメントで確認した Chat Completions allowlist の +積集合だけを公開します。未知、Responses 専用、embedding、media generation の id は fail closed で除外し、 +discovery を 256 KiB と raw 256 行に制限します。agent 固有 host と dedicated host は対象外です。キーは +[DigitalOcean Control Panel](https://cloud.digitalocean.com/model-studio/manage-keys) で作成します。 + +**Scaleway の discovery:** 認証済みモデル一覧と現在の Serverless chat-model allowlist の積集合だけを +公開し、embedding と audio transcription を除外して、discovery を 128 KiB と raw 128 行に制限します。 +default Project の共有 endpoint を使用します。Project id 付き URL と dedicated deployment は custom +provider で設定してください。API キーは [Scaleway console](https://console.scaleway.com/generative-api) で作成します。 + > **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 0784c85a1..64b5e44eb 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. **프로바이더 선택** — 내장 레지스트리 프리셋 66개 중 하나를 고르거나 `custom`을 선택해 base URL과 adapter를 직접 입력합니다. +1. **프로바이더 선택** — 내장 레지스트리 프리셋 68개 중 하나를 고르거나 `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 bc0c9dc83..b793e3452 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에는 빌트인 프리셋이 66개 들어 있습니다. 키 방식 55개, OAuth 7개, 로컬 3개, +opencodex에는 빌트인 프리셋이 68개 들어 있습니다. 키 방식 57개, OAuth 7개, 로컬 3개, 기본 ChatGPT 포워드 프리셋 1개입니다. 대시보드의 **Add provider** 선택기는 키 발급 페이지를 열고, 입력한 키를 검증한 뒤 저장합니다. 주요 항목은 다음과 같습니다: @@ -139,6 +139,8 @@ opencodex에는 빌트인 프리셋이 66개 들어 있습니다. 키 방식 55 | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | | Baseten Model APIs | `https://inference.baseten.co/v1` | +| DigitalOcean Serverless Inference | `https://inference.do-ai.run/v1` | +| Scaleway Generative APIs | `https://api.scaleway.ai/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` | @@ -180,6 +182,17 @@ Bearer API 키를 사용합니다. registry가 소유하는 DeepInfra 모델 목 대상으로 하며 별도 image, audio, GPU 엔드포인트는 범위에서 제외합니다. 키는 [Hyperbolic](https://app.hyperbolic.ai)에서 생성합니다. +**DigitalOcean 검색:** 프리셋은 model access key를 고정된 공유 Serverless Inference 호스트에 사용하고, +인증된 `/v1/models` 응답과 DigitalOcean 공식 문서로 확인한 Chat Completions allowlist의 교집합만 +노출합니다. 알 수 없는 ID, Responses 전용, embedding 및 media-generation ID는 fail closed로 제외하며, +discovery를 256 KiB와 raw 행 256개로 제한합니다. agent 전용 및 dedicated 호스트는 범위에서 제외합니다. +키는 [DigitalOcean Control Panel](https://cloud.digitalocean.com/model-studio/manage-keys)에서 생성합니다. + +**Scaleway 검색:** 인증된 모델 목록과 현재 Serverless chat-model allowlist의 교집합만 노출하여 embedding과 +audio transcription을 제외하고, discovery를 128 KiB와 raw 행 128개로 제한합니다. 기본 Project의 공유 +endpoint를 사용합니다. Project ID가 포함된 URL과 dedicated deployment는 custom provider로 설정하세요. +API 키는 [Scaleway console](https://console.scaleway.com/generative-api)에서 생성합니다. + > **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/ru/getting-started/quickstart.md b/docs-site/src/content/docs/ru/getting-started/quickstart.md index 33a7e41ee..7f1d20abe 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. **Выбор провайдера** — выберите один из 66 встроенных пресетов реестра или `custom`, чтобы +1. **Выбор провайдера** — выберите один из 68 встроенных пресетов реестра или `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 1fc5ecfad..34350d6ce 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 поставляется с 66 встроенными пресетами: 55 на основе ключей, семь OAuth, три локальных и +opencodex поставляется с 68 встроенными пресетами: 57 на основе ключей, семь OAuth, три локальных и пресет ChatGPT-форварда по умолчанию. Селектор **Add provider** в дашборде открывает страницу выдачи ключей провайдера, проверяет ключ и сохраняет его. Наиболее заметные записи: @@ -146,6 +146,8 @@ opencodex поставляется с 66 встроенными пресетам | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | | Baseten Model APIs | `https://inference.baseten.co/v1` | +| DigitalOcean Serverless Inference | `https://inference.do-ai.run/v1` | +| Scaleway Generative APIs | `https://api.scaleway.ai/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` | @@ -190,6 +192,19 @@ Volcengine Agent Plan использует нативную конечную т строками. Он охватывает только serverless text и vision-language chat; отдельные image, audio и GPU endpoint в него не входят. Ключи создаются в [Hyperbolic](https://app.hyperbolic.ai). +**Discovery для DigitalOcean.** Пресет использует model access key на фиксированном общем хосте +Serverless Inference и публикует только пересечение аутентифицированного ответа `/v1/models` с +подтверждённым документацией allowlist для Chat Completions. Неизвестные, Responses-only, +embedding- и media-generation id исключаются по принципу fail closed. Discovery ограничен 256 KiB +и 256 исходными строками; agent-specific и dedicated хосты не входят в область пресета. Ключи +создаются в [DigitalOcean Control Panel](https://cloud.digitalocean.com/model-studio/manage-keys). + +**Discovery для Scaleway.** Пресет публикует пересечение аутентифицированного списка моделей с +текущим allowlist serverless chat-моделей Scaleway, исключая embedding и audio transcription, и +ограничивает discovery размером 128 KiB и 128 исходными строками. Используется общий endpoint +Project по умолчанию; URL с Project ID и dedicated deployment настраиваются как custom provider. +API-ключ создаётся в [консоли Scaleway](https://console.scaleway.com/generative-api). + > **Область 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 bcbfebbdf..02178fd28 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 的 66 个预设中选择一个,或选择 `custom` 手动输入 base URL 和 adapter。 +1. **选择 provider** — 从内置 registry 的 68 个预设中选择一个,或选择 `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 2f01c9037..806e479b6 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 内置 66 个预设:55 个密钥预设、7 个 OAuth 预设、3 个本地预设,以及默认的 +opencodex 内置 68 个预设:57 个密钥预设、7 个 OAuth 预设、3 个本地预设,以及默认的 ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提供商的控制台,验证并保存密钥。 主要条目包括: @@ -131,6 +131,8 @@ ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提 | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | | Baseten Model APIs | `https://inference.baseten.co/v1` | +| DigitalOcean Serverless Inference | `https://inference.do-ai.run/v1` | +| Scaleway Generative APIs | `https://api.scaleway.ai/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` | @@ -169,6 +171,17 @@ OpenAI Chat Completions 提供商。registry 固定的 DeepInfra 模型列表 UR 并将实时发现限制为 256 KiB 和 256 条原始记录。它仅覆盖 serverless text 与 vision-language chat;独立的 image、audio 和 GPU 端点不在范围内。密钥可在 [Hyperbolic](https://app.hyperbolic.ai) 创建。 +**DigitalOcean 发现:**该预设使用 model access key 访问固定的共享 Serverless Inference 主机,只公开 +已鉴权 `/v1/models` 响应与 DigitalOcean 官方文档确认的 Chat Completions allowlist 的交集。未知、 +Responses-only、embedding 和 media-generation 模型 id 会按 fail closed 原则排除。发现上限为 256 KiB +和 256 条原始记录;agent 专属及 dedicated 主机不在范围内。密钥可在 +[DigitalOcean Control Panel](https://cloud.digitalocean.com/model-studio/manage-keys) 创建。 + +**Scaleway 发现:**该预设只公开已鉴权模型列表与当前 Serverless chat-model allowlist 的交集,排除 +embedding 与 audio transcription,并将发现限制为 128 KiB 和 128 条原始记录。它使用默认 Project 的 +共享 endpoint;带 Project ID 的 URL 和 dedicated deployment 需要配置为 custom provider。API 密钥可在 +[Scaleway 控制台](https://console.scaleway.com/generative-api) 创建。 + > **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/free-directory.ts b/src/providers/free-directory.ts index c390d00ed..4fb7efff3 100644 --- a/src/providers/free-directory.ts +++ b/src/providers/free-directory.ts @@ -135,7 +135,7 @@ const CONNECTABLE: Record = { nscale: openAi("https://inference.api.nscale.com/v1", "https://console.nscale.com", { verification: "official" }), 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" }), + scaleway: openAi("https://api.scaleway.ai/v1", "https://console.scaleway.com/generative-api", { supportLevel: "supported", verification: "official", documentationUrl: "https://www.scaleway.com/en/docs/generative-apis/api-cli/using-generative-apis/", modelsUrl: "https://api.scaleway.ai/v1/models", lastVerified: "2026-08-01" }), sensenova: openAi("https://token.sensenova.cn/v1", "https://console.sensenova.cn", { verification: "official" }), stepfun: openAi("https://api.stepfun.com/v1", "https://platform.stepfun.com", { verification: "official" }), together: openAi("https://api.together.xyz/v1", "https://api.together.xyz/settings/api-keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.together.ai/reference/models-1" }), diff --git a/src/providers/registry.ts b/src/providers/registry.ts index e336c2f39..f37e21300 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -544,6 +544,58 @@ const BASETEN_MODEL_INPUT_MODALITIES: Record = { "moonshotai/Kimi-K2.7-Code": ["text", "image"], "moonshotai/Kimi-K3": ["text", "image"], }; + +// 260801 DigitalOcean and Scaleway expose OpenAI-shaped `/v1/models` rows with only +// id/object/created/owned_by, while their shared serverless catalogs also contain +// non-chat and endpoint-specific models. Fail closed by intersecting live discovery +// with ids that the providers' current first-party model tables establish for Chat +// Completions. A newly listed id therefore needs a docs-backed registry refresh before +// it can enter the Codex catalog. +// Evidence: https://docs.digitalocean.com/products/inference/details/models/ +// https://docs.digitalocean.com/reference/api/reference/serverless-inference/ +// https://www.scaleway.com/en/docs/generative-apis/reference-content/supported-models/ +const DIGITALOCEAN_CHAT_COMPLETION_MODELS = [ + "arcee-trinity-large-thinking", + "openai-gpt-5.6-sol", + "openai-gpt-5.6-terra", + "openai-gpt-5.6-luna", + "qwen3-coder-flash", + "qwen3.5-397b-a17b", + "deepseek-v4-pro", + "deepseek-4-flash", + "deepseek-3.2", + "gemma-4-31B-it", + "minimax-m2.5", + "kimi-k3", + "kimi-k2.6", + "kimi-k2.5", + "llama3.3-70b-instruct", + "llama-4-maverick", + "mistral-3-14B", + "nemotron-3-ultra-550b", + "nvidia-nemotron-3-super-120b", + "nemotron-3-nano-omni", + "nemotron-nano-12b-v2-vl", + "mimo-v2.5-pro", + "glm-5.2", + "glm-5.1", + "glm-5", + // The API reference uses this native slash id in its Chat Completions example. + "meta-llama/Meta-Llama-3.1-8B-Instruct", +] as const; +const SCALEWAY_SERVERLESS_CHAT_MODELS = [ + "glm-5.2", + "gpt-oss-120b", + "qwen3.6-35b-a3b", + "qwen3.5-397b-a17b", + "qwen3-235b-a22b-instruct-2507", + "qwen3-coder-30b-a3b-instruct", + "gemma-4-26b-a4b-it", + "llama-3.3-70b-instruct", + "mistral-medium-3.5-128b", + "mistral-small-3.2-24b-instruct-2506", + "pixtral-12b-2409", +] as const; const UMANS_MODELS = [ "umans-coder", "umans-kimi-k2.7", @@ -1045,6 +1097,49 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ }, note: "Shared Model APIs only (personal API key, or team key with Call Model APIs access); dedicated Truss predict endpoints are outside this preset.", }, + { + id: "digitalocean", + label: "DigitalOcean Serverless Inference", + baseUrl: "https://inference.do-ai.run/v1", + adapter: "openai-chat", + authKind: "key", + dashboardUrl: "https://cloud.digitalocean.com/model-studio/manage-keys", + liveModels: true, + preserveCustomDestination: true, + // The Chat Completions contract documents function calls but not universal parallel support. + parallelToolCalls: false, + modelDiscovery: { + path: "models", + maxResponseBytes: 256 * 1024, + maxModels: 256, + filter: { + allOf: [{ path: ["id"], equalsAny: DIGITALOCEAN_CHAT_COMPLETION_MODELS }], + }, + }, + note: "Shared Serverless Inference Chat Completions only; agent-specific, dedicated, Responses-only, embedding, and media-generation models are outside this preset.", + }, + { + id: "scaleway", + label: "Scaleway Generative APIs", + baseUrl: "https://api.scaleway.ai/v1", + adapter: "openai-chat", + authKind: "key", + dashboardUrl: "https://console.scaleway.com/generative-api", + liveModels: true, + freeTier: true, + preserveCustomDestination: true, + // Parallel support varies by model; avoid advertising it as a provider-wide capability. + parallelToolCalls: false, + modelDiscovery: { + path: "models", + maxResponseBytes: 128 * 1024, + maxModels: 128, + filter: { + allOf: [{ path: ["id"], equalsAny: SCALEWAY_SERVERLESS_CHAT_MODELS }], + }, + }, + note: "Shared Generative APIs Serverless Chat Completions only; project-qualified and dedicated deployment hosts require a custom provider.", + }, // 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/digitalocean-scaleway-provider.test.ts b/tests/digitalocean-scaleway-provider.test.ts new file mode 100644 index 000000000..f2e010d2b --- /dev/null +++ b/tests/digitalocean-scaleway-provider.test.ts @@ -0,0 +1,315 @@ +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 { FREE_PROVIDER_DIRECTORY } from "../src/providers/free-directory"; +import { PROVIDER_REGISTRY, type ProviderRegistryEntry } 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 DIGITALOCEAN_FIXTURE = readFileSync(join(import.meta.dir, "fixtures/digitalocean-models.json"), "utf8"); +const SCALEWAY_FIXTURE = readFileSync(join(import.meta.dir, "fixtures/scaleway-models.json"), "utf8"); +const originalFetch = globalThis.fetch; + +type ProviderId = "digitalocean" | "scaleway"; + +const PROVIDERS: Record = { + digitalocean: { + baseUrl: "https://inference.do-ai.run/v1", + dashboardUrl: "https://cloud.digitalocean.com/model-studio/manage-keys", + fixture: DIGITALOCEAN_FIXTURE, + key: "digitalocean-test-key", + modelsUrl: "https://inference.do-ai.run/v1/models", + }, + scaleway: { + baseUrl: "https://api.scaleway.ai/v1", + dashboardUrl: "https://console.scaleway.com/generative-api", + fixture: SCALEWAY_FIXTURE, + key: "scaleway-test-key", + modelsUrl: "https://api.scaleway.ai/v1/models", + }, +}; + +afterEach(() => { + globalThis.fetch = originalFetch; + clearModelCache("digitalocean"); + clearModelCache("scaleway"); +}); + +function registryEntry(id: ProviderId): ProviderRegistryEntry { + const entry = PROVIDER_REGISTRY.find(row => row.id === id); + if (!entry) throw new Error(`missing ${id} registry entry`); + return entry; +} + +function providerConfig(id: ProviderId, overrides: Partial = {}): OcxConfig { + const provider = PROVIDERS[id]; + return { + port: 10100, + defaultProvider: id, + providers: { + [id]: { + adapter: "openai-chat", + baseUrl: provider.baseUrl, + authMode: "key", + apiKey: provider.key, + liveModels: true, + // Discovery stays fixture-only; this avoids platform-specific public-DNS classification. + allowPrivateNetwork: true, + ...overrides, + }, + }, + }; +} + +function discoveryAllowlist(entry: ProviderRegistryEntry): readonly unknown[] { + const predicate = entry.modelDiscovery?.filter?.allOf?.[0]; + if (!predicate || !("equalsAny" in predicate)) throw new Error(`missing ${entry.id} discovery allowlist`); + return predicate.equalsAny; +} + +describe("DigitalOcean and Scaleway providers", () => { + test("registers fixed OpenAI transports with bounded fail-closed chat discovery", () => { + expect(registryEntry("digitalocean")).toMatchObject({ + id: "digitalocean", + label: "DigitalOcean Serverless Inference", + adapter: "openai-chat", + baseUrl: PROVIDERS.digitalocean.baseUrl, + authKind: "key", + dashboardUrl: PROVIDERS.digitalocean.dashboardUrl, + liveModels: true, + preserveCustomDestination: true, + parallelToolCalls: false, + modelDiscovery: { + path: "models", + maxResponseBytes: 262_144, + maxModels: 256, + }, + }); + const digitaloceanModels = discoveryAllowlist(registryEntry("digitalocean")); + expect(digitaloceanModels).toHaveLength(26); + expect(digitaloceanModels).toContain("openai-gpt-5.6-sol"); + expect(digitaloceanModels).toContain("meta-llama/Meta-Llama-3.1-8B-Instruct"); + expect(digitaloceanModels).not.toContain("openai-gpt-5.5"); + expect(digitaloceanModels).not.toContain("openai-gpt-image-2"); + expect(registryEntry("digitalocean").note).toContain("Responses-only"); + + expect(registryEntry("scaleway")).toMatchObject({ + id: "scaleway", + label: "Scaleway Generative APIs", + adapter: "openai-chat", + baseUrl: PROVIDERS.scaleway.baseUrl, + authKind: "key", + dashboardUrl: PROVIDERS.scaleway.dashboardUrl, + liveModels: true, + freeTier: true, + preserveCustomDestination: true, + parallelToolCalls: false, + modelDiscovery: { + path: "models", + maxResponseBytes: 131_072, + maxModels: 128, + }, + }); + const scalewayModels = discoveryAllowlist(registryEntry("scaleway")); + expect(scalewayModels).toHaveLength(11); + expect(scalewayModels).toContain("qwen3.6-35b-a3b"); + expect(scalewayModels).not.toContain("qwen3-embedding-8b"); + expect(scalewayModels).not.toContain("whisper-large-v3"); + expect(registryEntry("scaleway").note).toContain("project-qualified"); + }); + + test("derives CLI and dashboard presets without persisting registry trust policy", () => { + expect(buildInitProviders()).toEqual(deriveInitProviders()); + + for (const id of ["digitalocean", "scaleway"] as const) { + const entry = registryEntry(id); + const provider = PROVIDERS[id]; + expect(KEY_LOGIN_PROVIDERS[id]).toMatchObject({ + adapter: "openai-chat", + baseUrl: provider.baseUrl, + dashboardUrl: provider.dashboardUrl, + liveModels: true, + }); + expect(buildInitProviders().find(row => row.id === id)).toMatchObject({ + kind: "key", + adapter: "openai-chat", + baseUrl: provider.baseUrl, + }); + expect(deriveProviderPresets().find(row => row.id === id)).toMatchObject({ + auth: "key", + dashboardUrl: provider.dashboardUrl, + }); + + const seed = providerConfigSeed(entry); + expect(seed).toMatchObject({ + adapter: "openai-chat", + baseUrl: provider.baseUrl, + authMode: "key", + liveModels: true, + parallelToolCalls: false, + }); + if (id === "scaleway") expect(seed.freeTier).toBe(true); + else expect(seed.freeTier).toBeUndefined(); + expect(seed).not.toHaveProperty("modelDiscovery"); + expect(seed).not.toHaveProperty("preserveCustomDestination"); + expect(KEY_LOGIN_PROVIDERS[id]).not.toHaveProperty("modelDiscovery"); + expect(KEY_LOGIN_PROVIDERS[id]).not.toHaveProperty("preserveCustomDestination"); + } + + expect(FREE_PROVIDER_DIRECTORY.find(row => row.id === "scaleway")).toMatchObject({ + supportLevel: "supported", + verification: "official", + documentationUrl: "https://www.scaleway.com/en/docs/generative-apis/api-cli/using-generative-apis/", + modelsUrl: PROVIDERS.scaleway.modelsUrl, + lastVerified: "2026-08-01", + }); + }); + + test("lists and validates models through each Bearer-authenticated endpoint", async () => { + for (const id of ["digitalocean", "scaleway"] as const) { + const provider = PROVIDERS[id]; + expect(buildModelsRequest(providerConfig(id).providers[id]!, provider.key, id)).toEqual({ + url: provider.modelsUrl, + headers: { Authorization: `Bearer ${provider.key}` }, + }); + + globalThis.fetch = (async (input, init) => { + expect(String(input)).toBe(provider.modelsUrl); + expect(new Headers(init?.headers).get("authorization")).toBe(`Bearer ${provider.key}`); + expect(init?.redirect).toBe("error"); + return new Response(provider.fixture, { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + + expect(await validateApiKey(id, KEY_LOGIN_PROVIDERS[id]!, provider.key)).toBe(true); + } + }); + + test("intersects mixed live catalogs with the docs-backed chat allowlists", async () => { + globalThis.fetch = (async (input, init) => { + const url = String(input); + expect(new Headers(init?.headers).get("authorization")).toBeTruthy(); + expect(init?.redirect).toBe("manual"); + if (url === PROVIDERS.digitalocean.modelsUrl) { + return new Response(DIGITALOCEAN_FIXTURE, { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + if (url === PROVIDERS.scaleway.modelsUrl) { + return new Response(SCALEWAY_FIXTURE, { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + throw new Error(`unexpected discovery URL: ${url}`); + }) as typeof fetch; + + const config = withStubbedProviderFetch({ + port: 10100, + defaultProvider: "digitalocean", + providers: { + digitalocean: providerConfig("digitalocean").providers.digitalocean!, + scaleway: providerConfig("scaleway").providers.scaleway!, + }, + } satisfies OcxConfig); + const models = await gatherRoutedModels(config); + const digitaloceanModels = models.filter(row => row.provider === "digitalocean"); + const scalewayModels = models.filter(row => row.provider === "scaleway"); + + expect(digitaloceanModels.map(row => row.id)).toEqual([ + "deepseek-v4-pro", + "meta-llama/Meta-Llama-3.1-8B-Instruct", + "openai-gpt-5.6-sol", + ]); + expect(digitaloceanModels[1]).toMatchObject({ owned_by: "digitalocean" }); + expect(scalewayModels.map(row => row.id)).toEqual([ + "glm-5.2", + "gpt-oss-120b", + "qwen3.6-35b-a3b", + ]); + expect(scalewayModels[2]).toMatchObject({ owned_by: "qwen" }); + + const slashModel = "meta-llama/Meta-Llama-3.1-8B-Instruct"; + expect(routeModel(config, `digitalocean/${slashModel}`).modelId).toBe(slashModel); + expect(routeModel(config, routedSlug("digitalocean", slashModel)).modelId).toBe(slashModel); + expect(routeModel(config, "scaleway/qwen3.6-35b-a3b").modelId).toBe("qwen3.6-35b-a3b"); + }); + + test("routes tool requests to the fixed chat hosts without claiming parallel support", () => { + const cases = [ + ["digitalocean", "openai-gpt-5.6-sol"], + ["scaleway", "qwen3.6-35b-a3b"], + ] as const; + + for (const [providerId, modelId] of cases) { + const route = routeModel(providerConfig(providerId), `${providerId}/${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: {}, + }); + const body = JSON.parse(String(request.body)) as Record; + + expect(request.url).toBe(`${PROVIDERS[providerId].baseUrl}/chat/completions`); + expect(request.headers.Authorization).toBe(`Bearer ${PROVIDERS[providerId].key}`); + expect(body.model).toBe(modelId); + expect(body.parallel_tool_calls).toBe(false); + } + }); + + test("does not retarget older same-named custom providers or adapters", () => { + for (const id of ["digitalocean", "scaleway"] as const) { + const customConfig = providerConfig(id, { baseUrl: "https://custom.example/v1" }); + const route = routeModel(customConfig, `${id}/custom-model`); + expect(route.provider).toMatchObject({ + adapter: "openai-chat", + baseUrl: "https://custom.example/v1", + authMode: "key", + }); + expect(buildModelsRequest(customConfig.providers[id]!, "custom-key", id)).toEqual({ + url: "https://custom.example/v1/models", + headers: { Authorization: "Bearer custom-key" }, + }); + + const customAdapter = routeModel(providerConfig(id, { + adapter: "anthropic", + baseUrl: "https://custom.example/anthropic", + }), `${id}/custom-model`); + expect(customAdapter.provider).toMatchObject({ + adapter: "anthropic", + baseUrl: "https://custom.example/anthropic", + authMode: "key", + }); + } + }); +}); diff --git a/tests/fixtures/digitalocean-models.json b/tests/fixtures/digitalocean-models.json new file mode 100644 index 000000000..02d74de3a --- /dev/null +++ b/tests/fixtures/digitalocean-models.json @@ -0,0 +1,47 @@ +{ + "object": "list", + "data": [ + { + "id": "meta-llama/Meta-Llama-3.1-8B-Instruct", + "object": "model", + "created": 1686935002, + "owned_by": "digitalocean" + }, + { + "id": "openai-gpt-5.6-sol", + "object": "model", + "created": 1785542400, + "owned_by": "openai" + }, + { + "id": "deepseek-v4-pro", + "object": "model", + "created": 1785456000, + "owned_by": "digitalocean" + }, + { + "id": "openai-gpt-5.5", + "object": "model", + "created": 1785369600, + "owned_by": "openai" + }, + { + "id": "qwen3-embedding-0.6b", + "object": "model", + "created": 1785283200, + "owned_by": "digitalocean" + }, + { + "id": "openai-gpt-image-2", + "object": "model", + "created": 1785196800, + "owned_by": "openai" + }, + { + "id": "future-chat-unverified", + "object": "model", + "created": 1785110400, + "owned_by": "digitalocean" + } + ] +} diff --git a/tests/fixtures/scaleway-models.json b/tests/fixtures/scaleway-models.json new file mode 100644 index 000000000..f6aaf5d4d --- /dev/null +++ b/tests/fixtures/scaleway-models.json @@ -0,0 +1,47 @@ +{ + "object": "list", + "data": [ + { + "id": "glm-5.2", + "object": "model", + "created": 1782864000, + "owned_by": "zai-org" + }, + { + "id": "qwen3.6-35b-a3b", + "object": "model", + "created": 1780272000, + "owned_by": "qwen" + }, + { + "id": "gpt-oss-120b", + "object": "model", + "created": 1754265600, + "owned_by": "openai" + }, + { + "id": "gpt-oss-20b", + "object": "model", + "created": 1754265600, + "owned_by": "openai" + }, + { + "id": "whisper-large-v3", + "object": "model", + "created": 1700784000, + "owned_by": "openai" + }, + { + "id": "qwen3-embedding-8b", + "object": "model", + "created": 1748736000, + "owned_by": "qwen" + }, + { + "id": "future-chat-unverified", + "object": "model", + "created": 1785110400, + "owned_by": "scaleway" + } + ] +} diff --git a/tests/provider-registry-parity.test.ts b/tests/provider-registry-parity.test.ts index b56b9756f..e8fb4d208 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", "orcarouter", "bizrouter", "groq", "google", "google-vertex", "azure-openai", - "deepseek", "cerebras", "deepinfra", "hyperbolic", "baseten", "together", "fireworks", "firepass", "moonshot", + "deepseek", "cerebras", "deepinfra", "hyperbolic", "baseten", "digitalocean", "scaleway", "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", @@ -451,7 +451,7 @@ describe("provider registry parity", () => { expect(nvidia?.freeTier).toBe(true); expect(nvidia?.authKind).toBe("key"); expect(nvidia?.keyOptional).toBeUndefined(); - expect(freeTierProviders).toEqual(["nvidia", "cloudflare-workers-ai"]); + expect(freeTierProviders).toEqual(["scaleway", "nvidia", "cloudflare-workers-ai"]); }); test("freeTier propagates through config seed, enrich backfill, and presets without overwriting user config", async () => {