diff --git a/docs-site/src/content/docs/getting-started/quickstart.md b/docs-site/src/content/docs/getting-started/quickstart.md index ed8d79858..1e8d9ec84 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 50 built-in registry presets or `custom` to type a base +1. **Pick a provider** — choose one of the 61 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 e14f0d6e0..699619dd2 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -185,7 +185,7 @@ selectors, then retry. Signing in from a machine with no existing `kiro-cli` ses ## 3. API-key catalog -opencodex ships 53 built-in presets: 42 key-based, seven OAuth, three local, and the default +opencodex ships 61 built-in presets: 50 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: @@ -202,6 +202,7 @@ validates the key, and stores it. Notable entries: | MiniMax · MiniMax (CN) | `https://api.minimax.io/v1` · `https://api.minimaxi.com/v1` | | DeepSeek | `https://api.deepseek.com` | | Cerebras | `https://api.cerebras.ai/v1` | +| Baseten Model APIs | `https://inference.baseten.co/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` | @@ -221,6 +222,12 @@ validates the key, and stores it. Notable entries: Most use the `openai-chat` adapter with a bearer key; a few that expose only an Anthropic-compatible endpoint (e.g. **Xiaomi MiMo**) use the `anthropic` adapter (`x-api-key`). +> **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 +> hosts and schemas and are not routed by this preset. +> Live discovery for this preset is capped at a 1 MiB response and 256 raw model rows. + > **Tencent Cloud Coding Plan usage restriction:** Tencent documents this subscription for > interactive coding tools only. General API automation, custom application backends, and > non-interactive batch use are prohibited and may cause the plan key to be suspended. 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 dbf80359e..ce659483d 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. **プロバイダー選択** — 組み込みレジストリのプリセット 50 個から一つを選ぶか、`custom` を選んで +1. **プロバイダー選択** — 組み込みレジストリのプリセット 61 個から一つを選ぶか、`custom` を選んで base URL とアダプターを直接入力します。 2. **API キー** — キーを貼り付けるか、`${ANTHROPIC_API_KEY}` のような環境変数を参照します。 3. **デフォルトモデル** — API キー、ローカル、custom プロバイダーではプリセット値を使うかモデル ID を直接入力します。 diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md index 67548d50e..c1f57aba4 100644 --- a/docs-site/src/content/docs/ja/guides/providers.md +++ b/docs-site/src/content/docs/ja/guides/providers.md @@ -116,7 +116,7 @@ Kiro のログインには Kiro CLI が必要です。`curl -fsSL https://cli.ki ## 3. API キーカタログ -opencodex v2.7.1 には組み込みプリセットが 50 個含まれています。キー方式 40、OAuth 6、ローカル 3、 +opencodex には組み込みプリセットが 61 個含まれています。キー方式 50、OAuth 7、ローカル 3、 デフォルト ChatGPT 転送プリセット 1 です。ダッシュボードの **Add provider** ピッカーはキー発行ページを開き、 入力したキーを検証した後保存します。主な項目は以下のとおりです: @@ -133,6 +133,7 @@ opencodex v2.7.1 には組み込みプリセットが 50 個含まれていま | MiniMax · MiniMax (CN) | `https://api.minimax.io/v1` · `https://api.minimaxi.com/v1` | | DeepSeek | `https://api.deepseek.com` | | Cerebras | `https://api.cerebras.ai/v1` | +| Baseten Model APIs | `https://inference.baseten.co/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` | @@ -152,6 +153,12 @@ opencodex v2.7.1 には組み込みプリセットが 50 個含まれていま 大半は bearer キーと共に `openai-chat` アダプターを使い、Anthropic 互換エンドポイントのみを公開する一部 (例: **Xiaomi MiMo**)は `anthropic` アダプター(`x-api-key`)を使います。 +> **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` +> エンドポイントはホストとスキーマが異なるため、このプリセットではルーティングされません。 +> このプリセットのライブディスカバリーは、レスポンス 1 MiB、モデルの生行 256 件が上限です。 + > **Tencent Cloud Coding Plan の利用制限:** Tencent はこのサブスクリプションを対話型 > コーディングツール専用としています。一般的な API 自動化、カスタムアプリのバックエンド、 > 非対話型バッチ利用は禁止されており、プランキーが停止される場合があります。 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 d478de1a2..df6c39f0e 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. **프로바이더 선택** — 내장 레지스트리 프리셋 50개 중 하나를 고르거나, `custom`을 선택해 +1. **프로바이더 선택** — 내장 레지스트리 프리셋 61개 중 하나를 고르거나, `custom`을 선택해 base URL과 adapter를 직접 입력합니다. 2. **API 키** — 키를 붙여넣거나, `${ANTHROPIC_API_KEY}`와 같은 환경 변수를 참조합니다. 3. **기본 모델** — API 키, 로컬, custom 프로바이더에서는 프리셋 값을 쓰거나 모델 id를 직접 입력합니다. diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md index 8cdae100a..6e8404778 100644 --- a/docs-site/src/content/docs/ko/guides/providers.md +++ b/docs-site/src/content/docs/ko/guides/providers.md @@ -116,7 +116,7 @@ Kiro 로그인에는 Kiro CLI가 필요합니다. `curl -fsSL https://cli.kiro.d ## 3. API 키 카탈로그 -opencodex v2.7.1에는 빌트인 프리셋이 50개 들어 있습니다. 키 방식 40개, OAuth 6개, 로컬 3개, +opencodex에는 빌트인 프리셋이 61개 들어 있습니다. 키 방식 50개, OAuth 7개, 로컬 3개, 기본 ChatGPT 포워드 프리셋 1개입니다. 대시보드의 **Add provider** 선택기는 키 발급 페이지를 열고, 입력한 키를 검증한 뒤 저장합니다. 주요 항목은 다음과 같습니다: @@ -133,6 +133,7 @@ opencodex v2.7.1에는 빌트인 프리셋이 50개 들어 있습니다. 키 방 | MiniMax · MiniMax (CN) | `https://api.minimax.io/v1` · `https://api.minimaxi.com/v1` | | DeepSeek | `https://api.deepseek.com` | | Cerebras | `https://api.cerebras.ai/v1` | +| Baseten Model APIs | `https://inference.baseten.co/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` | @@ -152,6 +153,12 @@ opencodex v2.7.1에는 빌트인 프리셋이 50개 들어 있습니다. 키 방 대부분은 bearer 키와 함께 `openai-chat` 어댑터를 사용하며, Anthropic 호환 엔드포인트만 노출하는 일부 (예: **Xiaomi MiMo**)는 `anthropic` 어댑터(`x-api-key`)를 사용합니다. +> **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` 엔드포인트는 +> 호스트와 스키마가 다르므로 이 프리셋으로 라우팅되지 않습니다. +> 이 프리셋의 실시간 검색은 응답 1 MiB와 원시 모델 행 256개로 제한됩니다. + > **Tencent Cloud Coding Plan 사용 제한:** Tencent는 이 구독을 대화형 코딩 도구 전용으로 > 안내합니다. 일반 API 자동화, 사용자 애플리케이션 백엔드 및 비대화형 일괄 호출은 금지되며 > 플랜 키가 정지될 수 있습니다. 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 74023c3a0..de1187710 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. **Выбор провайдера** — выберите один из 50 встроенных пресетов реестра или `custom`, чтобы +1. **Выбор провайдера** — выберите один из 61 встроенного пресета реестра или `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 f23e818d8..db5f17efb 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 поставляется с 53 встроенными пресетами: 42 на основе ключей, семь OAuth, три локальных и +opencodex поставляется с 61 встроенным пресетом: 50 на основе ключей, семь OAuth, три локальных и пресет ChatGPT-форварда по умолчанию. Селектор **Add provider** в дашборде открывает страницу выдачи ключей провайдера, проверяет ключ и сохраняет его. Наиболее заметные записи: @@ -143,6 +143,7 @@ opencodex поставляется с 53 встроенными пресетам | MiniMax · MiniMax (CN) | `https://api.minimax.io/v1` · `https://api.minimaxi.com/v1` | | DeepSeek | `https://api.deepseek.com` | | Cerebras | `https://api.cerebras.ai/v1` | +| Baseten Model APIs | `https://inference.baseten.co/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` | @@ -163,6 +164,12 @@ opencodex поставляется с 53 встроенными пресетам только Anthropic-совместимую конечную точку (например, **Xiaomi MiMo**), используют адаптер `anthropic` (`x-api-key`). +> **Область 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` используют другие хосты и +> схемы и этим пресетом не маршрутизируются. +> Для этого пресета live discovery ограничен ответом размером 1 MiB и 256 исходными строками моделей. + > **Ограничение Tencent Cloud Coding Plan:** Tencent разрешает использовать эту подписку только > в интерактивных инструментах программирования. Автоматизация общего API, серверы пользовательских > приложений и неинтерактивные пакетные вызовы запрещены и могут привести к блокировке ключа плана. 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 4c2f69e49..be5f979d5 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 的 50 个预设中选择一个,或选择 `custom` 手动输入 +1. **选择 provider** —— 从内置 registry 的 61 个预设中选择一个,或选择 `custom` 手动输入 base URL 和 adapter。 2. **API key** —— 粘贴一个 key,或引用一个环境变量,例如 `${ANTHROPIC_API_KEY}`。 3. **默认模型** —— 对于 API key、本地和 custom provider,可接受预设值或输入模型 id。 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 b749d3837..1d1b5d76a 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -108,7 +108,7 @@ Kiro 登录需要 Kiro CLI:使用 `curl -fsSL https://cli.kiro.dev/install | b ## 3. API 密钥目录 -opencodex v2.7.1 内置 50 个预设:40 个密钥预设、6 个 OAuth 预设、3 个本地预设,以及默认的 +opencodex 内置 61 个预设:50 个密钥预设、7 个 OAuth 预设、3 个本地预设,以及默认的 ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提供商的控制台,验证并保存密钥。 主要条目包括: @@ -125,6 +125,7 @@ ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提 | MiniMax · MiniMax (CN) | `https://api.minimax.io/v1` · `https://api.minimaxi.com/v1` | | DeepSeek | `https://api.deepseek.com` | | Cerebras | `https://api.cerebras.ai/v1` | +| Baseten Model APIs | `https://inference.baseten.co/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` | @@ -143,6 +144,12 @@ ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提 大多数使用带 bearer 密钥的 `openai-chat` adapter;少数仅暴露 Anthropic 兼容端点的提供商(例如 **Xiaomi MiMo**)使用 `anthropic` adapter(`x-api-key`)。 +> **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` 端点使用不同的主机和请求 schema,不由此预设路由。 +> 该预设的实时发现上限为 1 MiB 响应和 256 条原始模型记录。 + > **腾讯云 Coding Plan 使用限制:**腾讯将此订阅限定为交互式编程工具使用。禁止通用 API > 自动化、自定义应用后端和非交互式批量调用;违规使用可能导致套餐密钥被停用。 diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 663e8c8ec..cedc9706c 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -408,6 +408,43 @@ const NEURALWATT_REASONING_HISTORY_MODELS = [ "kimi-k2.6", "kimi-k2.7-code", "qwen3.5-397b", "qwen3.6-35b", ]; + +// 260728 Baseten Model APIs: `/v1/models` owns the live lineup, while these hints +// describe only capabilities that Baseten documents per slug. Unlisted live models +// intentionally inherit the empty provider ladder instead of being advertised with +// opencodex's generic reasoning defaults. Audio is omitted because the current proxy +// request model does not carry OpenAI `audio_url` parts. +// Evidence: https://docs.baseten.co/inference/model-apis/reasoning +// https://docs.baseten.co/inference/model-apis/vision +const BASETEN_FULL_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"]; +const BASETEN_MODEL_REASONING_EFFORTS: Record = { + "deepseek-ai/DeepSeek-V4-Pro": BASETEN_FULL_REASONING_EFFORTS, + "thinkingmachines/inkling": BASETEN_FULL_REASONING_EFFORTS, + "openai/gpt-oss-120b": BASETEN_FULL_REASONING_EFFORTS, + "moonshotai/Kimi-K3": ["low", "high", "max"], + "zai-org/GLM-5.2": ["high", "max"], + "zai-org/GLM-5.2-Fast": ["high", "max"], +}; +const BASETEN_MODEL_REASONING_EFFORT_MAP: Record> = { + "deepseek-ai/DeepSeek-V4-Pro": { none: "none", minimal: "minimal" }, + "thinkingmachines/inkling": { none: "none", minimal: "minimal" }, + "openai/gpt-oss-120b": { none: "none", minimal: "minimal" }, + "moonshotai/Kimi-K3": { none: "none" }, + "zai-org/GLM-5.2": { none: "none" }, + "zai-org/GLM-5.2-Fast": { none: "none" }, +}; +const BASETEN_MODEL_DEFAULT_REASONING_EFFORTS: Record = { + "deepseek-ai/DeepSeek-V4-Pro": "medium", + "thinkingmachines/inkling": "high", + "openai/gpt-oss-120b": "medium", + "moonshotai/Kimi-K3": "max", +}; +const BASETEN_MODEL_INPUT_MODALITIES: Record = { + "thinkingmachines/inkling": ["text", "image"], + "moonshotai/Kimi-K2.6": ["text", "image"], + "moonshotai/Kimi-K2.7-Code": ["text", "image"], + "moonshotai/Kimi-K3": ["text", "image"], +}; const UMANS_MODELS = [ "umans-coder", "umans-kimi-k2.7", @@ -828,6 +865,31 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ }, // llama-3.3-70b was deprecated by Cerebras on 2026-02-16. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md. { id: "cerebras", label: "Cerebras", baseUrl: "https://api.cerebras.ai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://cloud.cerebras.ai/platform/apikeys", defaultModel: "gpt-oss-120b" }, + { + id: "baseten", + label: "Baseten Model APIs", + baseUrl: "https://inference.baseten.co/v1", + adapter: "openai-chat", + authKind: "key", + dashboardUrl: "https://app.baseten.co/settings/api_keys", + liveModels: true, + preserveCustomDestination: true, + // Baseten's Chat Completions contract documents parallel_tool_calls as default-on. + parallelToolCalls: true, + // Baseten says models outside its reasoning table do not support reasoning. Keep + // unknown/new live slugs conservative until an official-docs registry refresh proves it. + reasoningEfforts: [], + modelReasoningEfforts: BASETEN_MODEL_REASONING_EFFORTS, + modelReasoningEffortMap: BASETEN_MODEL_REASONING_EFFORT_MAP, + modelDefaultReasoningEfforts: BASETEN_MODEL_DEFAULT_REASONING_EFFORTS, + modelInputModalities: BASETEN_MODEL_INPUT_MODALITIES, + modelDiscovery: { + path: "models", + maxResponseBytes: 1_048_576, + maxModels: 256, + }, + note: "Shared Model APIs only (personal API key, or team key with Call Model APIs access); dedicated Truss predict endpoints are outside this preset.", + }, // 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/baseten-provider.test.ts b/tests/baseten-provider.test.ts new file mode 100644 index 000000000..ec70495e2 --- /dev/null +++ b/tests/baseten-provider.test.ts @@ -0,0 +1,284 @@ +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 FIXTURE = readFileSync(join(import.meta.dir, "fixtures/baseten-models.json"), "utf8"); +const originalFetch = globalThis.fetch; + +afterEach(() => { + globalThis.fetch = originalFetch; + clearModelCache("baseten"); +}); + +function basetenEntry() { + const entry = PROVIDER_REGISTRY.find(row => row.id === "baseten"); + if (!entry) throw new Error("missing Baseten registry entry"); + return entry; +} + +function basetenConfig(overrides: Partial = {}): OcxConfig { + return { + port: 10100, + defaultProvider: "baseten", + providers: { + baseten: { + adapter: "openai-chat", + baseUrl: "https://inference.baseten.co/v1", + authMode: "key", + apiKey: "bt-test-key", + liveModels: true, + // Some test environments resolve public hosts through a NAT64 prefix. The fetch itself is + // fixture-only; this opt-in keeps this test focused on the provider contract. + allowPrivateNetwork: true, + ...overrides, + }, + }, + }; +} + +describe("Baseten Model APIs provider", () => { + test("registers the fixed shared Model APIs transport and bounded discovery policy", () => { + expect(basetenEntry()).toMatchObject({ + id: "baseten", + label: "Baseten Model APIs", + adapter: "openai-chat", + baseUrl: "https://inference.baseten.co/v1", + authKind: "key", + dashboardUrl: "https://app.baseten.co/settings/api_keys", + liveModels: true, + preserveCustomDestination: true, + parallelToolCalls: true, + reasoningEfforts: [], + modelInputModalities: { + "thinkingmachines/inkling": ["text", "image"], + "moonshotai/Kimi-K2.6": ["text", "image"], + "moonshotai/Kimi-K2.7-Code": ["text", "image"], + "moonshotai/Kimi-K3": ["text", "image"], + }, + modelDiscovery: { + path: "models", + maxResponseBytes: 1_048_576, + maxModels: 256, + }, + }); + expect(basetenEntry().modelReasoningEfforts).toEqual({ + "deepseek-ai/DeepSeek-V4-Pro": ["low", "medium", "high", "xhigh", "max"], + "thinkingmachines/inkling": ["low", "medium", "high", "xhigh", "max"], + "openai/gpt-oss-120b": ["low", "medium", "high", "xhigh", "max"], + "moonshotai/Kimi-K3": ["low", "high", "max"], + "zai-org/GLM-5.2": ["high", "max"], + "zai-org/GLM-5.2-Fast": ["high", "max"], + }); + expect(basetenEntry().modelReasoningEffortMap).toEqual({ + "deepseek-ai/DeepSeek-V4-Pro": { none: "none", minimal: "minimal" }, + "thinkingmachines/inkling": { none: "none", minimal: "minimal" }, + "openai/gpt-oss-120b": { none: "none", minimal: "minimal" }, + "moonshotai/Kimi-K3": { none: "none" }, + "zai-org/GLM-5.2": { none: "none" }, + "zai-org/GLM-5.2-Fast": { none: "none" }, + }); + expect(basetenEntry().modelDefaultReasoningEfforts).toEqual({ + "deepseek-ai/DeepSeek-V4-Pro": "medium", + "thinkingmachines/inkling": "high", + "openai/gpt-oss-120b": "medium", + "moonshotai/Kimi-K3": "max", + }); + expect(basetenEntry().note).toContain("Shared Model APIs only"); + expect(basetenEntry().note).toContain("personal API key"); + expect(basetenEntry().note).toContain("Truss predict endpoints"); + }); + + test("derives key-login, init, and dashboard presets without persisting trust policy", () => { + expect(KEY_LOGIN_PROVIDERS.baseten).toMatchObject({ + label: "Baseten Model APIs", + adapter: "openai-chat", + baseUrl: "https://inference.baseten.co/v1", + dashboardUrl: "https://app.baseten.co/settings/api_keys", + liveModels: true, + }); + expect(buildInitProviders()).toEqual(deriveInitProviders()); + expect(buildInitProviders().find(row => row.id === "baseten")).toMatchObject({ + kind: "key", + adapter: "openai-chat", + baseUrl: "https://inference.baseten.co/v1", + }); + expect(deriveProviderPresets().find(row => row.id === "baseten")).toMatchObject({ + auth: "key", + dashboardUrl: "https://app.baseten.co/settings/api_keys", + }); + + const seed = providerConfigSeed(basetenEntry()); + expect(seed).toMatchObject({ + adapter: "openai-chat", + baseUrl: "https://inference.baseten.co/v1", + authMode: "key", + liveModels: true, + parallelToolCalls: true, + reasoningEfforts: [], + }); + expect(seed.modelReasoningEfforts?.["deepseek-ai/DeepSeek-V4-Pro"]) + .toEqual(["low", "medium", "high", "xhigh", "max"]); + expect(seed.modelInputModalities?.["moonshotai/Kimi-K2.6"]) + .toEqual(["text", "image"]); + expect(seed).not.toHaveProperty("modelDiscovery"); + expect(seed).not.toHaveProperty("preserveCustomDestination"); + expect(KEY_LOGIN_PROVIDERS.baseten).not.toHaveProperty("modelDiscovery"); + expect(KEY_LOGIN_PROVIDERS.baseten).not.toHaveProperty("preserveCustomDestination"); + }); + + test("lists and validates models through the same Bearer-authenticated endpoint", async () => { + const request = buildModelsRequest( + basetenConfig().providers.baseten!, + "bt-model-list-key", + "baseten", + ); + expect(request).toEqual({ + url: "https://inference.baseten.co/v1/models", + headers: { Authorization: "Bearer bt-model-list-key" }, + }); + + globalThis.fetch = (async (input, init) => { + expect(String(input)).toBe("https://inference.baseten.co/v1/models"); + expect(new Headers(init?.headers).get("authorization")).toBe("Bearer bt-validation-key"); + expect(init?.redirect).toBe("error"); + return new Response(FIXTURE, { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + + expect(await validateApiKey( + "baseten", + KEY_LOGIN_PROVIDERS.baseten!, + "bt-validation-key", + )).toBe(true); + }); + + test("routes chat completions to the shared inference host with documented tool parallelism", () => { + const route = routeModel( + basetenConfig(), + "baseten/deepseek-ai/DeepSeek-V4-Pro", + ); + expect(route.provider.parallelToolCalls).toBe(true); + expect(route.modelId).toBe("deepseek-ai/DeepSeek-V4-Pro"); + + 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: {}, + }); + const body = JSON.parse(String(request.body)) as Record; + + expect(request.url).toBe("https://inference.baseten.co/v1/chat/completions"); + expect(request.headers.Authorization).toBe("Bearer bt-test-key"); + expect(body.model).toBe("deepseek-ai/DeepSeek-V4-Pro"); + expect(body.parallel_tool_calls).toBe(true); + }); + + test("forwards only the documented per-model reasoning effort ladders", () => { + const deepseekRoute = routeModel(basetenConfig(), "baseten/deepseek-ai/DeepSeek-V4-Pro"); + const deepseekBody = JSON.parse(String(createOpenAIChatAdapter(deepseekRoute.provider).buildRequest({ + modelId: deepseekRoute.modelId, + context: { messages: [{ role: "user", content: "ping", timestamp: 0 }] }, + stream: false, + options: { reasoning: "minimal" }, + }).body)) as Record; + expect(deepseekBody.reasoning_effort).toBe("minimal"); + + const glmRoute = routeModel(basetenConfig(), "baseten/zai-org/GLM-5.2"); + const glmBody = JSON.parse(String(createOpenAIChatAdapter(glmRoute.provider).buildRequest({ + modelId: glmRoute.modelId, + context: { messages: [{ role: "user", content: "ping", timestamp: 0 }] }, + stream: false, + options: { reasoning: "low" }, + }).body)) as Record; + // GLM 5.2 accepts only high/max, so a lower Codex rung clamps to its lowest real rung. + expect(glmBody.reasoning_effort).toBe("high"); + + const kimiRoute = routeModel(basetenConfig(), "baseten/moonshotai/Kimi-K2.6"); + const kimiBody = JSON.parse(String(createOpenAIChatAdapter(kimiRoute.provider).buildRequest({ + modelId: kimiRoute.modelId, + context: { messages: [{ role: "user", content: "ping", timestamp: 0 }] }, + stream: false, + options: { reasoning: "high" }, + }).body)) as Record; + // K2.6 uses chat_template_args instead of reasoning_effort; do not send an ignored claim. + expect(kimiBody).not.toHaveProperty("reasoning_effort"); + }); + + test("discovers slash-containing model ids from a local fixture and routes both selector forms", async () => { + globalThis.fetch = (async (input, init) => { + expect(String(input)).toBe("https://inference.baseten.co/v1/models"); + expect(new Headers(init?.headers).get("authorization")).toBe("Bearer bt-test-key"); + expect(init?.redirect).toBe("manual"); + return new Response(FIXTURE, { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + + const config = withStubbedProviderFetch(basetenConfig()); + const models = (await gatherRoutedModels(config)).filter(row => row.provider === "baseten"); + expect(models.map(row => row.id)).toEqual([ + "deepseek-ai/DeepSeek-V4-Pro", + "moonshotai/Kimi-K2.6", + ]); + expect(models[0]).toMatchObject({ + reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], + defaultReasoningEffort: "medium", + parallelToolCalls: true, + }); + expect(models[1]).toMatchObject({ + inputModalities: ["text", "image"], + reasoningEfforts: [], + parallelToolCalls: true, + }); + expect(routedSlug("baseten", models[0]!.id)).toBe("baseten/deepseek-ai-DeepSeek-V4-Pro"); + + expect(routeModel(config, "baseten/deepseek-ai/DeepSeek-V4-Pro").modelId) + .toBe("deepseek-ai/DeepSeek-V4-Pro"); + expect(routeModel(config, "baseten/deepseek-ai-DeepSeek-V4-Pro").modelId) + .toBe("deepseek-ai/DeepSeek-V4-Pro"); + }); + + test("does not retarget an older same-named custom provider", () => { + const customBaseUrl = routeModel(basetenConfig({ + baseUrl: "https://custom.example/v1", + }), "baseten/custom-model"); + expect(customBaseUrl.provider).toMatchObject({ + adapter: "openai-chat", + baseUrl: "https://custom.example/v1", + authMode: "key", + }); + + const customAdapter = routeModel(basetenConfig({ + adapter: "anthropic", + baseUrl: "https://custom.example/anthropic", + }), "baseten/custom-model"); + expect(customAdapter.provider).toMatchObject({ + adapter: "anthropic", + baseUrl: "https://custom.example/anthropic", + authMode: "key", + }); + }); +}); diff --git a/tests/fixtures/baseten-models.json b/tests/fixtures/baseten-models.json new file mode 100644 index 000000000..7852c4374 --- /dev/null +++ b/tests/fixtures/baseten-models.json @@ -0,0 +1,11 @@ +{ + "object": "list", + "data": [ + { + "id": "deepseek-ai/DeepSeek-V4-Pro" + }, + { + "id": "moonshotai/Kimi-K2.6" + } + ] +} diff --git a/tests/provider-registry-parity.test.ts b/tests/provider-registry-parity.test.ts index d2f7d5735..9d812ab59 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", "together", "fireworks", "firepass", "moonshot", + "deepseek", "cerebras", "baseten", "together", "fireworks", "firepass", "moonshot", "huggingface", "nvidia", "venice", "zai", "zhipu-bigmodel", "nanogpt", "synthetic", "siliconflow", "qwen-cloud", "tencent-coding-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",