diff --git a/docs-site/src/content/docs/fr/getting-started/quickstart.md b/docs-site/src/content/docs/fr/getting-started/quickstart.md index 986a1c11ef9..bf01104632d 100644 --- a/docs-site/src/content/docs/fr/getting-started/quickstart.md +++ b/docs-site/src/content/docs/fr/getting-started/quickstart.md @@ -13,7 +13,7 @@ ocx init `ocx init` vous accompagne dans les étapes suivantes : -1. **Choix d’un fournisseur** — sélectionnez l’un des 106 préréglages intégrés au registre, ou `custom` pour saisir une +1. **Choix d’un fournisseur** — sélectionnez l’un des 107 préréglages intégrés au registre, ou `custom` pour saisir une URL de base et un adaptateur. 2. **Clé API** — collez une clé ou référencez une variable d’environnement telle que `${ANTHROPIC_API_KEY}`. 3. **Modèle par défaut** — pour les fournisseurs clés, locaux et personnalisés, acceptez le préréglage ou saisissez un identifiant de modèle. diff --git a/docs-site/src/content/docs/fr/guides/providers.md b/docs-site/src/content/docs/fr/guides/providers.md index 8d8a85f0a8d..dbc026ea5bc 100644 --- a/docs-site/src/content/docs/fr/guides/providers.md +++ b/docs-site/src/content/docs/fr/guides/providers.md @@ -310,7 +310,7 @@ existante n'est pas concernée. ## 3. Catalogue des clés API -opencodex fournit 106 préréglages intégrés : 85 à clé, 17 OAuth, trois locaux et un préréglage par défaut de +opencodex fournit 107 préréglages intégrés : 86 à clé, 17 OAuth, trois locaux et un préréglage par défaut de transfert ChatGPT. Dans le tableau de bord, le sélecteur **Ajouter un fournisseur** ouvre le tableau de bord du fournisseur à clé, valide la clé et l'enregistre ; la validation dépend du fournisseur. Parmi les entrées notables : diff --git a/docs-site/src/content/docs/getting-started/quickstart.md b/docs-site/src/content/docs/getting-started/quickstart.md index 910fb0e181e..cfb968ec901 100644 --- a/docs-site/src/content/docs/getting-started/quickstart.md +++ b/docs-site/src/content/docs/getting-started/quickstart.md @@ -20,7 +20,7 @@ ocx init `ocx init` walks you through: -1. **Pick a provider** — choose one of the 106 built-in registry presets or `custom` to type a base +1. **Pick a provider** — choose one of the 107 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 ebd12465265..daa8b9d67d8 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -744,7 +744,7 @@ On upgrade, startup adds Pro to a saved model list only when it exactly matches ## 3. API-key catalog -opencodex ships 106 built-in presets: 85 key-based, 17 OAuth, three local, and one default +opencodex ships 107 built-in presets: 86 key-based, 17 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: @@ -837,6 +837,7 @@ region-pinned EU routes, is at [opper.ai/models](https://opper.ai/models). Opper | TokenLab | `https://api.tokenlab.sh/v1` | | Modelflare | `https://modelflare.dev/v1` | | AI Stupid Level | `https://aistupidlevel.info/v1` | +| cheapaimodel | `https://api.cheapaimodel.ai/v1` | | OpenGateway | `https://apis.opengateway.ai/v1` | | GitLab Duo | `https://cloud.gitlab.com/ai/v1/proxy/openai/v1` | | Cloudflare AI Gateway | `https://gateway.ai.cloudflare.com/v1/{account-id}/{gateway}/anthropic` | @@ -952,6 +953,21 @@ output tokens, which hold for any model the router picks. The router accepts tex routing mode or pinned model at the preset destination the proxy hands images to the vision sidecar instead of sending them. Its Anthropic Messages endpoint is a passthrough to Anthropic without routing, so the preset does not use it. +**cheapaimodel** is a pay-per-token API gateway for open-weight models (DeepSeek, GLM, Kimi) and +Grok, operated by LINKOVA PTE. LTD. Create an [API key](https://console.cheapaimodel.ai/keys) on a +prepaid USD balance, then select **cheapaimodel** in `ocx init` or the dashboard's **Add provider** +picker, or run `ocx provider add cheapaimodel`, and give it the key directly or as the reference +`${CHEAPAIMODEL_API_KEY}`. The preset uses [Chat Completions](https://cheapaimodel.ai/docs/api/create-chat-completion) +for every client and discovers models from the authenticated +[`GET /v1/models`](https://cheapaimodel.ai/docs/api/list-models), which answers 401 without a valid +key. The seeded models are `deepseek-v4-1-flash` (the default), `glm-5.3`, `glm-5.3-flash`, `kimi-k3` +and `grok-4.7`; `glm-5.3` is text only, so images for it go to the vision sidecar. `kimi-k3` accepts +only its own sampling values, so the preset does not forward `temperature` or `top_p` to it. The +preset sends every request through Chat Completions, the one interface every catalog model supports; +it does not use the gateway's Responses or Anthropic Messages endpoints. cheapaimodel's +[terms](https://cheapaimodel.ai/terms) and [privacy policy](https://cheapaimodel.ai/privacy) apply to +requests sent to this service. + The provider editor also accepts an optional `inputModalities` fallback: a nonempty array containing `text`, `image`, `audio`, or `video`. Exact per-model declarations take precedence. Invalid values are rejected on save; a malformed value in a hand-edited config loads as `["text"]` with a warning. 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 82f68cdf175..cfd4f95bca9 100644 --- a/docs-site/src/content/docs/ja/getting-started/quickstart.md +++ b/docs-site/src/content/docs/ja/getting-started/quickstart.md @@ -18,7 +18,7 @@ ocx init `ocx init` では次の手順を説明します。 -1. **プロバイダーを選択してください** — 106 個の組み込みレジストリプリセットのいずれか、または `custom` を選択してベース URL とアダプターを入力します。 +1. **プロバイダーを選択してください** — 107 個の組み込みレジストリプリセットのいずれか、または `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 e94d4842f7c..1d68dbc3b9c 100644 --- a/docs-site/src/content/docs/ja/guides/providers.md +++ b/docs-site/src/content/docs/ja/guides/providers.md @@ -210,7 +210,7 @@ Kiro のログインには Kiro CLI が必要です。Unix では `curl -fsSL ht ## 3. API キーカタログ -opencodex には組み込みプリセットが 106 個含まれています。キー方式 85、OAuth 17、ローカル 3、 +opencodex には組み込みプリセットが 107 個含まれています。キー方式 86、OAuth 17、ローカル 3、 デフォルト ChatGPT 転送プリセット 1 です。ダッシュボードの **Add provider** ピッカーはキー発行ページを開き、 入力したキーを検証した後保存します(検証はプロバイダー固有です)。主な項目は以下のとおりです: diff --git a/docs-site/src/content/docs/ko/getting-started/quickstart.md b/docs-site/src/content/docs/ko/getting-started/quickstart.md index 8165688935d..bceda554cfe 100644 --- a/docs-site/src/content/docs/ko/getting-started/quickstart.md +++ b/docs-site/src/content/docs/ko/getting-started/quickstart.md @@ -18,7 +18,7 @@ ocx init `ocx init`은 다음 과정을 안내합니다: -1. **프로바이더 선택** — 내장 레지스트리 프리셋 106개 중 하나를 고르거나 `custom`을 선택해 base URL과 adapter를 직접 입력합니다. +1. **프로바이더 선택** — 내장 레지스트리 프리셋 107개 중 하나를 고르거나 `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 aa3b3f3aff1..a0379d62368 100644 --- a/docs-site/src/content/docs/ko/guides/providers.md +++ b/docs-site/src/content/docs/ko/guides/providers.md @@ -213,7 +213,7 @@ Kiro 로그인에는 Kiro CLI가 필요합니다. Unix에서는 `curl -fsSL http ## 3. API 키 카탈로그 -opencodex에는 빌트인 프리셋이 106개 들어 있습니다. 키 방식 85개, OAuth 17개, 로컬 3개, +opencodex에는 빌트인 프리셋이 107개 들어 있습니다. 키 방식 86개, OAuth 17개, 로컬 3개, 기본 ChatGPT 포워드 프리셋 1개입니다. 대시보드의 **Add provider** 선택기는 키 발급 페이지를 열고, 입력한 키를 검증한 뒤 저장합니다(검증은 프로바이더별로 다릅니다). 주요 항목은 다음과 같습니다: diff --git a/docs-site/src/content/docs/ru/getting-started/quickstart.md b/docs-site/src/content/docs/ru/getting-started/quickstart.md index 2769427a577..33aae9d93b5 100644 --- a/docs-site/src/content/docs/ru/getting-started/quickstart.md +++ b/docs-site/src/content/docs/ru/getting-started/quickstart.md @@ -18,7 +18,7 @@ ocx init `ocx init` проведёт вас по следующим шагам: -1. **Выбор провайдера** — выберите один из 106 встроенных пресетов реестра или `custom`, чтобы +1. **Выбор провайдера** — выберите один из 107 встроенных пресетов реестра или `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 526b10bd03e..e3f8880e7d6 100644 --- a/docs-site/src/content/docs/ru/guides/providers.md +++ b/docs-site/src/content/docs/ru/guides/providers.md @@ -222,7 +222,7 @@ Inline JSON и лишние позиционные аргументы откло ## 3. Каталог API-ключей -opencodex поставляется с 106 встроенными пресетами: 85 на основе ключей, 17 OAuth, три локальных и +opencodex поставляется с 107 встроенными пресетами: 86 на основе ключей, 17 OAuth, три локальных и один пресет ChatGPT-форварда по умолчанию. Селектор **Add provider** в дашборде открывает страницу выдачи ключей провайдера, проверяет ключ и сохраняет его; проверка зависит от провайдера. Наиболее заметные записи: diff --git a/docs-site/src/content/docs/tr/getting-started/quickstart.md b/docs-site/src/content/docs/tr/getting-started/quickstart.md index 61a9c46577b..e346dcca33a 100644 --- a/docs-site/src/content/docs/tr/getting-started/quickstart.md +++ b/docs-site/src/content/docs/tr/getting-started/quickstart.md @@ -13,7 +13,7 @@ ocx init `ocx init` adım adım size rehberlik eder: -1. **Bir sağlayıcı seçin** — yerleşik kayıt defterindeki 106 önayardan birini +1. **Bir sağlayıcı seçin** — yerleşik kayıt defterindeki 107 önayardan birini veya bir temel URL ile adaptör yazmak için `custom` seçeneğini belirleyin. 2. **API anahtarı** — bir anahtar yapıştırın veya `${ANTHROPIC_API_KEY}` gibi bir ortam değişkenine başvurun. diff --git a/docs-site/src/content/docs/tr/guides/providers.md b/docs-site/src/content/docs/tr/guides/providers.md index c25565978bf..a6518b27291 100644 --- a/docs-site/src/content/docs/tr/guides/providers.md +++ b/docs-site/src/content/docs/tr/guides/providers.md @@ -342,7 +342,7 @@ olmayan bir makineden oturum açmak bundan etkilenmez. ## 3. API anahtarı kataloğu -opencodex 106 yerleşik önayar ile birlikte gelir: 85 anahtar tabanlı, 17 +opencodex 107 yerleşik önayar ile birlikte gelir: 86 anahtar tabanlı, 17 OAuth, üç yerel ve bir varsayılan ChatGPT iletme önayarı. Kontrol panelinin **Sağlayıcı ekle** seçicisi bir anahtar sağlayıcısının kontrol panelini açar, anahtarı doğrular ve saklar; doğrulama sağlayıcıya özgüdür. Dikkate değer 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 66c4c0e6054..dd1dc17cced 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 @@ -18,7 +18,7 @@ ocx init `ocx init` 会引导你完成: -1. **选择 provider** — 从内置 registry 的 106 个预设中选择一个,或选择 `custom` 手动输入 base URL 和 adapter。 +1. **选择 provider** — 从内置 registry 的 107 个预设中选择一个,或选择 `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 4cbf796c7fb..6a7d3d5ce10 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -220,7 +220,7 @@ API Key 提供商,也不负责让 WorkBuddy 桌面客户端使用 OpenCodex ## 3. API 密钥目录 -opencodex 内置 106 个预设:85 个密钥预设、17 个 OAuth 预设、3 个本地预设,以及 1 个默认的 +opencodex 内置 107 个预设:86 个密钥预设、17 个 OAuth 预设、3 个本地预设,以及 1 个默认的 ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提供商的控制台,验证并保存密钥。 验证因提供商而异。主要条目包括: diff --git a/docs-site/src/content/docs/zh-tw/getting-started/quickstart.md b/docs-site/src/content/docs/zh-tw/getting-started/quickstart.md index f7591a8a0ea..1b97eb24aaf 100644 --- a/docs-site/src/content/docs/zh-tw/getting-started/quickstart.md +++ b/docs-site/src/content/docs/zh-tw/getting-started/quickstart.md @@ -13,7 +13,7 @@ ocx init `ocx init` 會引導你完成: -1. **選擇 provider** —— 從內建 registry 的 106 個預設中選擇一個,或選擇 `custom` 手動輸入 +1. **選擇 provider** —— 從內建 registry 的 107 個預設中選擇一個,或選擇 `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-tw/guides/providers.md b/docs-site/src/content/docs/zh-tw/guides/providers.md index 5f8d0e32108..af01df1a2c7 100644 --- a/docs-site/src/content/docs/zh-tw/guides/providers.md +++ b/docs-site/src/content/docs/zh-tw/guides/providers.md @@ -264,7 +264,7 @@ database 並移除目前的 WAL、SHM 與 journal sidecar,再發布先前的 s ## 3. API 金鑰目錄 -opencodex 內建 106 個 preset:85 個 key-based、17 個 OAuth、3 個 local,以及 1 個預設 ChatGPT-forward +opencodex 內建 107 個 preset:86 個 key-based、17 個 OAuth、3 個 local,以及 1 個預設 ChatGPT-forward preset。儀表板的 **Add provider** picker 會開啟 key provider 的 dashboard、驗證金鑰並儲存;驗證方式 依 provider 而異。主要條目如下。 diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 79ff88d6093..34db196479f 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1368,7 +1368,7 @@ "opengateway-key-validation.test.ts": "providers", "openrouter-provider-routing.test.ts": "providers", "openrouter-quota-reset-cooldown-4024.test.ts": "providers", - "opper-provider.test.ts": "providers", "modelflare-provider.test.ts": "providers", "aistupidlevel-provider.test.ts": "providers", + "opper-provider.test.ts": "providers", "modelflare-provider.test.ts": "providers", "aistupidlevel-provider.test.ts": "providers", "cheapaimodel-provider.test.ts": "providers", "tokenlab-protocols.test.ts": "providers", "gpt61-terra-provider-rows.test.ts": "providers", "tokenlab-provider.test.ts": "providers", "optional-shutdown-hooks.test.ts": "lib", diff --git a/src/providers/registry/entries-extended.ts b/src/providers/registry/entries-extended.ts index ec33c812c31..2f8d5cfdc14 100644 --- a/src/providers/registry/entries-extended.ts +++ b/src/providers/registry/entries-extended.ts @@ -1322,6 +1322,42 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [ inputModalities: ["text"], note: "Smart Router that sends each request to a model chosen from AI Stupid Level's benchmarks, using the provider keys saved on your AI Stupid Level account (OpenAI, Anthropic, Google, DeepSeek, Kimi, GLM), so each provider bills you directly with no markup. Routing modes: auto-coding (the default), auto-tooling, auto-reasoning, auto-best, plus value and speed modes. A real model id from the live list, such as claude-opus-5-5, pins one model. The X-AISM-Model response header names the model that answered. Text only.", }, + { + // cheapaimodel, operated by LINKOVA PTE. LTD. (Singapore). Contract checked 2026-10-28: + // https://cheapaimodel.ai/docs/api/create-chat-completion and + // https://cheapaimodel.ai/docs/api/list-models. GET /v1/models answers 401 without a valid key, + // so the default discovery URL doubles as key validation. Chat Completions is the one interface + // every catalog model supports, so every inbound stays on Chat and the preset does not use the + // gateway's Responses or Anthropic Messages endpoints. Context windows and + // input modalities are from the live catalog at https://cheapaimodel.ai/models. kimi-k3 rejects + // any temperature other than 0.6 or 1 and rejects top_p 0.9 and 1.0, so both are dropped for it. + // deepseek-v4-1-flash takes reasoning_effort none, low, high and max, and an omitted effort runs + // at high (https://cheapaimodel.ai/blog/deepseek-v4-1-flash-reasoning-effort), so it keeps the + // DeepSeek Flash ladder and sends an explicit none instead of dropping it. + // The env reference derived from the id is ${CHEAPAIMODEL_API_KEY}. + id: "cheapaimodel", + label: "cheapaimodel", + adapter: "openai-chat", + baseUrl: "https://api.cheapaimodel.ai/v1", + authKind: "key", + dashboardUrl: "https://console.cheapaimodel.ai/keys", + liveModels: true, + preserveCustomDestination: true, + defaultModel: "deepseek-v4-1-flash", + models: ["deepseek-v4-1-flash", "glm-5.3", "glm-5.3-flash", "kimi-k3", "grok-4.7"], + modelContextWindows: { + "deepseek-v4-1-flash": 1_000_000, "glm-5.3": 1_000_000, "glm-5.3-flash": 1_000_000, "kimi-k3": 1_000_000, "grok-4.7": 500_000, + }, + modelInputModalities: { + "deepseek-v4-1-flash": ["text", "image"], "glm-5.3": ["text"], "glm-5.3-flash": ["text", "image"], + "kimi-k3": ["text", "image"], "grok-4.7": ["text", "image"], + }, + noTemperatureModels: ["kimi-k3"], + noTopPModels: ["kimi-k3"], + modelReasoningEfforts: { "deepseek-v4-1-flash": deepseekThinkingEffortsFor("deepseek-v4-1-flash") }, + modelReasoningEffortMap: { "deepseek-v4-1-flash": { ...deepseekReasoningMapFor("deepseek-v4-1-flash"), none: "none" } }, + note: "Pay-per-token API gateway for open-weight models (DeepSeek, GLM, Kimi) and Grok, billed from a prepaid USD balance. Create an API key at console.cheapaimodel.ai. Live discovery lists the models your key can call; deepseek-v4-1-flash is the default. Chat Completions only.", + }, // Xiaomi retires mimo-v2.5 and mimo-v2.5-pro on 2026-10-21 with no redirect // (https://mimo.mi.com/docs/en-US/updates/deprecate), so the first-party presets default to V2.6. // Saved defaults are not rewritten; V2.5 stays listed until it stops answering. diff --git a/structure/ops/docs-and-release.md b/structure/ops/docs-and-release.md index e6a62651755..f95d18edb41 100644 --- a/structure/ops/docs-and-release.md +++ b/structure/ops/docs-and-release.md @@ -86,7 +86,7 @@ Manual navigation is defined in `docs-site/astro.config.mjs`. When adding a publ sidebar and either add localized copies or intentionally accept Starlight fallback behavior. Provider preset totals are recounted from the current registry when a preset lands. The -documented split is 106 total: 85 key-based, 17 OAuth, three local, and one default +documented split is 107 total: 86 key-based, 17 OAuth, three local, and one default ChatGPT-forward preset. The English provider guide, all seven translated copies, and all eight quickstarts carry the same counts. diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 74bcb8a8604..62cc4169a9e 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -1138,6 +1138,7 @@ "modelflare-provider.test.ts": "providers", "gpt61-terra-provider-rows.test.ts": "providers", "aistupidlevel-provider.test.ts": "providers", + "cheapaimodel-provider.test.ts": "providers", "tokenlab-provider.test.ts": "providers", "optional-shutdown-hooks.test.ts": "lib", "orca-import.test.ts": "codex-integration", "orcarouter-provider.test.ts": "providers", "cc-switch-import.test.ts": "providers", "outbound-body-guard.test.ts": "server", "owned-service-home.test.ts": "server", diff --git a/tests/providers/cheapaimodel-provider.test.ts b/tests/providers/cheapaimodel-provider.test.ts new file mode 100644 index 00000000000..3e75c3ad70a --- /dev/null +++ b/tests/providers/cheapaimodel-provider.test.ts @@ -0,0 +1,143 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { createOpenAIChatAdapter } from "../../src/adapters/openai-chat"; +import { gatherRoutedModels } from "../../src/codex/catalog"; +import { clearModelCache } from "../../src/codex/model-cache"; +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 { routeModel } from "../../src/router"; +import { resolveWireProtocolOverride } from "../../src/server/adapter-resolve"; +import type { OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../../src/types"; +import { isModelVisionSidecarConsumer } from "../../src/vision/eligibility"; +import { withStubbedProviderFetch } from "../helpers/catalog-provider-fetch"; + +const originalFetch = globalThis.fetch; +const baseUrl = "https://api.cheapaimodel.ai/v1"; +// The chat models the authenticated GET /v1/models listed on 2026-10-28. +const MODELS = ["deepseek-v4-1-flash", "glm-5.3", "glm-5.3-flash", "kimi-k3", "grok-4.7"] as const; + +afterEach(() => { + globalThis.fetch = originalFetch; + clearModelCache("cheapaimodel"); +}); + +function entry() { + const row = PROVIDER_REGISTRY.find(candidate => candidate.id === "cheapaimodel"); + if (!row) throw new Error("missing cheapaimodel preset"); + return row; +} + +function config(overrides: Partial = {}): OcxConfig { + return withStubbedProviderFetch({ + port: 10100, + defaultProvider: "cheapaimodel", + providers: { cheapaimodel: { ...providerConfigSeed(entry()), apiKey: "test-key", ...overrides } }, + }); +} + +describe("cheapaimodel provider", () => { + test("is offered by ocx provider add, ocx init and the dashboard picker", () => { + expect(deriveInitProviders().find(row => row.id === "cheapaimodel")).toMatchObject({ + label: "cheapaimodel", kind: "key", adapter: "openai-chat", baseUrl, + }); + const preset = deriveProviderPresets().find(row => row.id === "cheapaimodel"); + expect(preset).toMatchObject({ auth: "key", dashboardUrl: "https://console.cheapaimodel.ai/keys" }); + expect(preset?.sponsor).toBeUndefined(); + expect(KEY_LOGIN_PROVIDERS.cheapaimodel?.defaultModel).toBe("deepseek-v4-1-flash"); + }); + + test("seeds the catalog models with their windows, and only glm-5.3 as text-only", () => { + const seeded = config().providers.cheapaimodel!; + expect(seeded).toMatchObject({ adapter: "openai-chat", baseUrl, defaultModel: "deepseek-v4-1-flash", liveModels: true }); + expect(entry().models).toEqual([...MODELS]); + expect(seeded.modelContextWindows).toEqual({ + "deepseek-v4-1-flash": 1_000_000, "glm-5.3": 1_000_000, "glm-5.3-flash": 1_000_000, "kimi-k3": 1_000_000, "grok-4.7": 500_000, + }); + for (const model of MODELS) { + expect(isModelVisionSidecarConsumer(seeded, model)).toBe(model === "glm-5.3"); + } + }); + + test.each(MODELS)("%s routes over Chat with its upstream id unchanged", model => { + const route = routeModel(config(), `cheapaimodel/${model}`); + expect(route.modelId).toBe(model); + expect(route.provider).toMatchObject({ adapter: "openai-chat", baseUrl }); + for (const inbound of ["responses", "chat", "anthropic"] as const) { + expect(resolveWireProtocolOverride("cheapaimodel", model, config().providers.cheapaimodel!, inbound).adapter) + .toBe("openai-chat"); + } + }); + + test("drops temperature and top_p for kimi-k3 only", () => { + const provider = config().providers.cheapaimodel!; + const wire = (modelId: string): Record => { + const parsed: OcxParsedRequest = { + modelId, + context: { messages: [{ role: "user", content: "hi", timestamp: 0 }] }, + stream: false, + options: { temperature: 0.5, topP: 0.9 }, + }; + return JSON.parse(createOpenAIChatAdapter(provider).buildRequest(parsed).body as string); + }; + expect(wire("kimi-k3")).not.toHaveProperty("temperature"); + expect(wire("kimi-k3")).not.toHaveProperty("top_p"); + expect(wire("deepseek-v4-1-flash")).toMatchObject({ temperature: 0.5, top_p: 0.9 }); + }); + + test("sends deepseek-v4-1-flash an explicit none and its documented effort ladder", () => { + const provider = config().providers.cheapaimodel!; + const effort = (reasoning: string | undefined): unknown => { + const parsed: OcxParsedRequest = { + modelId: "deepseek-v4-1-flash", + context: { messages: [{ role: "user", content: "hi", timestamp: 0 }] }, + stream: false, + options: reasoning === undefined ? {} : { reasoning }, + }; + return JSON.parse(createOpenAIChatAdapter(provider).buildRequest(parsed).body as string).reasoning_effort; + }; + // Omitting the field means high upstream, so none must reach the wire to turn reasoning off. + expect(effort("none")).toBe("none"); + expect(effort("low")).toBe("low"); + expect(effort("medium")).toBe("high"); + expect(effort("high")).toBe("high"); + expect(effort("xhigh")).toBe("high"); + expect(effort("max")).toBe("max"); + expect(effort(undefined)).toBeUndefined(); + }); + + test("keeps a same-named custom destination instead of retargeting it", () => { + const custom = config({ baseUrl: "https://relay.example.test/v1" }); + expect(routeModel(custom, "cheapaimodel/custom-model").provider.baseUrl).toBe("https://relay.example.test/v1"); + }); + + test("lists models from the authenticated live catalog", async () => { + let requests = 0; + globalThis.fetch = (async (input, init) => { + requests++; + expect(String(input)).toBe("https://api.cheapaimodel.ai/v1/models"); + expect(new Headers(init?.headers).get("authorization")).toBe("Bearer test-key"); + expect(init?.body ?? undefined).toBeUndefined(); + return Response.json({ object: "list", data: [ + { id: "deepseek-v4-1-flash", object: "model", created: 1626777600, owned_by: "custom" }, + { id: "kimi-k3", object: "model", created: 1626777600, owned_by: "custom" }, + ] }); + }) as typeof fetch; + const models = (await gatherRoutedModels(config())).filter(row => row.provider === "cheapaimodel"); + expect(requests).toBe(1); + expect(models.map(row => row.id).sort()).toEqual(["deepseek-v4-1-flash", "kimi-k3"]); + }); + + test("validates a key against the key-gated model list and separates rejection from an outage", async () => { + expect(buildModelsRequest(config().providers.cheapaimodel!, "test-key", "cheapaimodel").url) + .toBe("https://api.cheapaimodel.ai/v1/models"); + for (const [status, expected] of [[200, true], [401, false], [503, "unknown"]] as const) { + globalThis.fetch = (async (input, init) => { + expect(String(input)).toBe("https://api.cheapaimodel.ai/v1/models"); + expect(new Headers(init?.headers).get("authorization")).toBe("Bearer test-key"); + return new Response(status === 200 ? JSON.stringify({ object: "list", data: [] }) : null, { status }); + }) as typeof fetch; + expect(await validateApiKey("cheapaimodel", KEY_LOGIN_PROVIDERS.cheapaimodel!, "test-key")).toBe(expected); + } + }); +});