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 4d08caed17d..3cf3a91b39c 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 99 préréglages intégrés au registre, ou `custom` pour saisir une +1. **Choix d’un fournisseur** — sélectionnez l’un des 100 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 1f2090b4b86..641b9a93735 100644 --- a/docs-site/src/content/docs/fr/guides/providers.md +++ b/docs-site/src/content/docs/fr/guides/providers.md @@ -295,7 +295,7 @@ existante n'est pas concernée. ## 3. Catalogue des clés API -opencodex fournit 99 préréglages intégrés : 82 à clé, 13 OAuth, trois locaux et un préréglage par défaut de +opencodex fournit 100 préréglages intégrés : 83 à clé, 13 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 8f863b6f517..d49783ed4ec 100644 --- a/docs-site/src/content/docs/getting-started/quickstart.md +++ b/docs-site/src/content/docs/getting-started/quickstart.md @@ -18,7 +18,7 @@ ocx init `ocx init` walks you through: -1. **Pick a provider** — choose one of the 99 built-in registry presets or `custom` to type a base +1. **Pick a provider** — choose one of the 100 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 b8c6c7510c6..821a5b81178 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -456,7 +456,7 @@ The account list marks Kiro accounts excluded from automatic selection with a re ## 3. API-key catalog -opencodex ships 99 built-in presets: 82 key-based, 13 OAuth, three local, and one default +opencodex ships 100 built-in presets: 83 key-based, 13 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: @@ -548,10 +548,26 @@ region-pinned EU routes, is at [opper.ai/models](https://opper.ai/models). Opper | Xiaomi MiMo (OpenAI Chat) | `https://api.xiaomimimo.com/v1` | | Kilo | `https://api.kilo.ai/api/gateway` | | Opper | `https://api.opper.ai/v3/compat` | +| TokenLab | `https://api.tokenlab.sh/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` | | …and more | opencode zen, Vercel AI Gateway, Venice, NanoGPT, Synthetic, Qianfan, Alibaba, Parallel, ZenMux, LiteLLM | +**TokenLab** is an OpenAI-compatible API gateway operated by TOKENLAB AI INC. +Create a workspace [API key](https://tokenlab.sh/dashboard/api?tab=keys), then run +`ocx provider add tokenlab` or select **TokenLab** in the dashboard's **Add provider** picker. +The preset uses [Chat Completions](https://docs.tokenlab.sh/quickstart) and discovers models at +`GET /v1/models?category=chat`, keeping only entries that declare `tool-use` capability. +Image, video, audio, embedding and decision models are excluded from this chat preset. + +The [model catalog](https://docs.tokenlab.sh/api-reference/models/list-models) is public without +a key, but a supplied key is validated and scopes results to its model permissions and delivery +policy. Use a valid key with a funded workspace for inference. `gpt-5.6-terra` is the seeded +default; choose another discovered model if your key does not allow it. The provider and model +prefixes remain separate: `tokenlab/gpt-5.6-terra` sends `gpt-5.6-terra` upstream. +TokenLab's [terms](https://tokenlab.sh/tos) and [privacy policy](https://tokenlab.sh/privacy-policy) +apply to requests sent to this service. + The MiniMax and MiniMax (CN) provider cards can also show Coding Plan quota when the configured key has an active plan. The dashboard reads the plan's 5-hour window and, when present, weekly window; these are display observations and do not change model routing. 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 d2a3c3f4b86..3760250bc91 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. **プロバイダーを選択してください** — 99 個の組み込みレジストリプリセットのいずれか、または `custom` を選択してベース URL とアダプターを入力します。 +1. **プロバイダーを選択してください** — 100 個の組み込みレジストリプリセットのいずれか、または `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 f54fa25dd07..282f7335c05 100644 --- a/docs-site/src/content/docs/ja/guides/providers.md +++ b/docs-site/src/content/docs/ja/guides/providers.md @@ -193,7 +193,7 @@ Kiro のログインには Kiro CLI が必要です。Unix では `curl -fsSL ht ## 3. API キーカタログ -opencodex には組み込みプリセットが 99 個含まれています。キー方式 82、OAuth 13、ローカル 3、 +opencodex には組み込みプリセットが 100 個含まれています。キー方式 83、OAuth 13、ローカル 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 b0b22222a37..7ddac09c0d8 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. **프로바이더 선택** — 내장 레지스트리 프리셋 99개 중 하나를 고르거나 `custom`을 선택해 base URL과 adapter를 직접 입력합니다. +1. **프로바이더 선택** — 내장 레지스트리 프리셋 100개 중 하나를 고르거나 `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 8027e1ff1fc..a3184d953ce 100644 --- a/docs-site/src/content/docs/ko/guides/providers.md +++ b/docs-site/src/content/docs/ko/guides/providers.md @@ -190,7 +190,7 @@ Kiro 로그인에는 Kiro CLI가 필요합니다. Unix에서는 `curl -fsSL http ## 3. API 키 카탈로그 -opencodex에는 빌트인 프리셋이 99개 들어 있습니다. 키 방식 82개, OAuth 13개, 로컬 3개, +opencodex에는 빌트인 프리셋이 100개 들어 있습니다. 키 방식 83개, OAuth 13개, 로컬 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 945893e0457..9552ed50b42 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. **Выбор провайдера** — выберите один из 99 встроенных пресетов реестра или `custom`, чтобы +1. **Выбор провайдера** — выберите один из 100 встроенных пресетов реестра или `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 579005a20dd..4bc043e2fa7 100644 --- a/docs-site/src/content/docs/ru/guides/providers.md +++ b/docs-site/src/content/docs/ru/guides/providers.md @@ -206,7 +206,7 @@ Inline JSON и лишние позиционные аргументы откло ## 3. Каталог API-ключей -opencodex поставляется с 99 встроенными пресетами: 82 на основе ключей, 13 OAuth, три локальных и +opencodex поставляется с 100 встроенными пресетами: 83 на основе ключей, 13 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 2d45af871ca..21056695bba 100644 --- a/docs-site/src/content/docs/tr/getting-started/quickstart.md +++ b/docs-site/src/content/docs/tr/getting-started/quickstart.md @@ -14,7 +14,7 @@ ocx init `ocx init` adım adım size rehberlik eder: -1. **Bir sağlayıcı seçin** — yerleşik kayıt defterindeki 99 önayardan birini +1. **Bir sağlayıcı seçin** — yerleşik kayıt defterindeki 100 ö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 58c628b2ddc..76201a96233 100644 --- a/docs-site/src/content/docs/tr/guides/providers.md +++ b/docs-site/src/content/docs/tr/guides/providers.md @@ -327,7 +327,7 @@ olmayan bir makineden oturum açmak bundan etkilenmez. ## 3. API anahtarı kataloğu -opencodex 99 yerleşik önayar ile birlikte gelir: 82 anahtar tabanlı, 13 +opencodex 100 yerleşik önayar ile birlikte gelir: 83 anahtar tabanlı, 13 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 cb5abdcc458..b8561722c38 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 的 99 个预设中选择一个,或选择 `custom` 手动输入 base URL 和 adapter。 +1. **选择 provider** — 从内置 registry 的 100 个预设中选择一个,或选择 `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 5b8c902e5bf..0e8df8f42af 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -181,7 +181,7 @@ Kiro 登录需要 Kiro CLI:Unix 使用 `curl -fsSL https://cli.kiro.dev/instal ## 3. API 密钥目录 -opencodex 内置 99 个预设:82 个密钥预设、13 个 OAuth 预设、3 个本地预设,以及 1 个默认的 +opencodex 内置 100 个预设:83 个密钥预设、13 个 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 d7e601b025e..f0b02c6ab89 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 的 99 個預設中選擇一個,或選擇 `custom` 手動輸入 +1. **選擇 provider** —— 從內建 registry 的 100 個預設中選擇一個,或選擇 `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 5d68c93eb51..3b8c18a509e 100644 --- a/docs-site/src/content/docs/zh-tw/guides/providers.md +++ b/docs-site/src/content/docs/zh-tw/guides/providers.md @@ -249,7 +249,7 @@ database 並移除目前的 WAL、SHM 與 journal sidecar,再發布先前的 s ## 3. API 金鑰目錄 -opencodex 內建 99 個 preset:82 個 key-based、13 個 OAuth、3 個 local,以及 1 個預設 ChatGPT-forward +opencodex 內建 100 個 preset:83 個 key-based、13 個 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 2cd657d98f6..fd19e2040b0 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1229,6 +1229,7 @@ "openrouter-provider-routing.test.ts": "providers", "openrouter-quota-reset-cooldown-4024.test.ts": "providers", "opper-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", diff --git a/src/providers/registry/entries-extended.ts b/src/providers/registry/entries-extended.ts index 96defecfd92..7740782ea4f 100644 --- a/src/providers/registry/entries-extended.ts +++ b/src/providers/registry/entries-extended.ts @@ -1137,6 +1137,34 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [ modelInputModalities: OPPER_MODEL_INPUT_MODALITIES, note: "EU-hosted AI gateway: one OpenAI-compatible endpoint and one key in front of 30+ providers. Bare model ids are pools (claude-sonnet-4-6, gpt-5.5) and Opper picks the route per request; vendor/model ids (anthropic/claude-sonnet-4-6) pin one provider. The catalogue is discovered live from /v3/compat/models with your key; the public list is at opper.ai/models. Token rates are the model providers' rates with no markup; Opper charges a 3% fee when you buy credits.", }, + { + // Public contract checked 2026-09-29: https://docs.tokenlab.sh/api-reference/models/list-models + // A supplied key is validated and scopes the catalog; anonymous discovery is also public. + id: "tokenlab", + label: "TokenLab", + adapter: "openai-chat", + baseUrl: "https://api.tokenlab.sh/v1", + authKind: "key", + dashboardUrl: "https://tokenlab.sh/dashboard/api?tab=keys", + liveModels: true, + preserveCustomDestination: true, + defaultModel: "gpt-5.6-terra", + models: ["gpt-5.6-terra"], + modelContextWindows: { "gpt-5.6-terra": 1_050_000 }, + modelMaxOutputTokens: { "gpt-5.6-terra": 128_000 }, + modelInputModalities: { "gpt-5.6-terra": ["text", "image"] }, + modelDiscovery: { + path: "models", + query: { category: "chat" }, + filter: { + allOf: [ + { path: ["tokenlab", "category"], equalsAny: ["chat"] }, + { path: ["tokenlab", "capabilities"], containsAny: ["tool-use"] }, + ], + }, + }, + note: "OpenAI-compatible API gateway. Create a workspace API key at tokenlab.sh. Live discovery lists tool-capable chat models available to your key and delivery policy.", + }, { id: "opencode-free", label: "OpenCode Free", diff --git a/structure/ops/docs-and-release.md b/structure/ops/docs-and-release.md index 93536baaf5b..168ad3c622f 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 99 total: 82 key-based, 13 OAuth, three local, and one default +documented split is 100 total: 83 key-based, 13 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/structure/providers-and-adapters.md b/structure/providers-and-adapters.md index 24d10b72046..b35fe601b45 100644 --- a/structure/providers-and-adapters.md +++ b/structure/providers-and-adapters.md @@ -273,6 +273,16 @@ healthy request can retry; stable missing files still cache as empty. Symlinked directories pass through canonical confinement: inside-cwd targets load, outside targets do not. The 30-second, 128-entry cache rechecks capacity at insertion time. +## TokenLab chat provider + +The `tokenlab` key preset uses the existing OpenAI Chat adapter at +`https://api.tokenlab.sh/v1`. Registry-owned discovery requests the chat category and requires +the row's `tokenlab.capabilities` to include `tool-use`, excluding non-chat and unclassified +rows. A supplied key scopes the catalog to its model permissions and delivery policy; the +anonymous catalog does not establish authentication. Newly promoted preset collision protection +preserves an older same-named custom destination. `tests/providers/tokenlab-provider.test.ts` +covers derived entry points, scoped discovery, destination preservation and model routing. + ## TypeSafe JEV decision provider `src/providers/registry/entries-extended.ts` owns the canonical `jev` key preset at diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 784f61f6887..459506dab5b 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -1241,6 +1241,7 @@ "openrouter-provider-routing.test.ts": "providers", "openrouter-quota-reset-cooldown-4024.test.ts": "providers", "opper-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", diff --git a/tests/providers/provider-registry-parity.test.ts b/tests/providers/provider-registry-parity.test.ts index d6b75c5565f..9c3bd6e2aa2 100644 --- a/tests/providers/provider-registry-parity.test.ts +++ b/tests/providers/provider-registry-parity.test.ts @@ -44,7 +44,7 @@ const EXPECTED_KEY_PROVIDER_IDS = [ "deepseek", "cerebras", "chutes", "deepinfra", "hyperbolic", "nscale", "vultr", "jev", "baseten", "commandcode", "sambanova", "nebius", "crusoe", "digitalocean", "scaleway", "featherless", "novita", "together", "fireworks", "firepass", "moonshot", "huggingface", "nvidia", "venice", "zai", "zhipu-bigmodel", "zhipu-bigmodel-coding", "zhipu-bigmodel-responses", "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", "opper", + "minimax", "minimax-cn", "kimi-code", "opencode-zen", "vercel-ai-gateway", "opper", "tokenlab", "opencode-free", "xiaomi", "xiaomi-mimo", "kilo", "mimo-free", "mimo", "cloudflare-ai-gateway", "cloudflare-workers-ai", "gitlab-duo", "qoder", "qoder-cn", "codebuddy", "codebuddy-cn", "stepfun", "claude-cli", ]; diff --git a/tests/providers/tokenlab-provider.test.ts b/tests/providers/tokenlab-provider.test.ts new file mode 100644 index 00000000000..b6ec09b3873 --- /dev/null +++ b/tests/providers/tokenlab-provider.test.ts @@ -0,0 +1,113 @@ +import { afterEach, describe, expect, test } from "bun:test"; +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 { extractProviderModelItems, resolveProviderModelDiscovery } from "../../src/providers/model-discovery"; +import { PROVIDER_REGISTRY } from "../../src/providers/registry"; +import { routeModel } from "../../src/router"; +import type { OcxConfig, OcxProviderConfig } from "../../src/types"; +import { withStubbedProviderFetch } from "../helpers/catalog-provider-fetch"; + +const originalFetch = globalThis.fetch; +const baseUrl = "https://api.tokenlab.sh/v1"; + +afterEach(() => { + globalThis.fetch = originalFetch; + clearModelCache("tokenlab"); +}); + +function config(overrides: Partial = {}): OcxConfig { + const entry = PROVIDER_REGISTRY.find(row => row.id === "tokenlab"); + if (!entry) throw new Error("missing TokenLab preset"); + return withStubbedProviderFetch({ + port: 10100, + defaultProvider: "tokenlab", + providers: { tokenlab: { ...providerConfigSeed(entry), apiKey: "test-key", ...overrides } }, + }); +} + +describe("TokenLab provider", () => { + test("is available through ordinary dashboard, init and key-login entry points", () => { + expect(deriveInitProviders().find(row => row.id === "tokenlab")).toMatchObject({ + label: "TokenLab", kind: "key", adapter: "openai-chat", baseUrl, + }); + const preset = deriveProviderPresets().find(row => row.id === "tokenlab"); + expect(preset).toMatchObject({ auth: "key", dashboardUrl: "https://tokenlab.sh/dashboard/api?tab=keys" }); + expect(preset?.sponsor).toBeUndefined(); + expect(KEY_LOGIN_PROVIDERS.tokenlab?.defaultModel).toBe("gpt-5.6-terra"); + const provider = config().providers.tokenlab!; + expect(provider).not.toHaveProperty("modelDiscovery"); + expect(provider).not.toHaveProperty("preserveCustomDestination"); + }); + + test("routes a selected model without rewriting its upstream identity", () => { + const route = routeModel(config(), "tokenlab/gpt-5.6-terra"); + expect(route.modelId).toBe("gpt-5.6-terra"); + expect(route.provider).toMatchObject({ adapter: "openai-chat", baseUrl }); + }); + + test("scopes discovery to chat and preserves renamed and custom destinations", () => { + const provider = config().providers.tokenlab!; + for (const name of ["tokenlab", "my-tokenlab-workspace"]) { + const request = buildModelsRequest(provider, "test-key", name); + expect(request.url).toBe("https://api.tokenlab.sh/v1/models?category=chat"); + expect(request.headers.Authorization).toBe("Bearer test-key"); + } + const custom = config({ baseUrl: "https://gateway.example.test/v1" }); + expect(routeModel(custom, "tokenlab/custom-model").provider.baseUrl) + .toBe("https://gateway.example.test/v1"); + expect(buildModelsRequest(custom.providers.tokenlab!, "custom-key", "tokenlab").url) + .toBe("https://gateway.example.test/v1/models"); + expect(resolveProviderModelDiscovery("tokenlab", custom.providers.tokenlab!).spec).toBeUndefined(); + }); + + test("admits only explicit tool-capable chat rows from a mixed catalog", () => { + const discovery = resolveProviderModelDiscovery("tokenlab", config().providers.tokenlab!); + // Public TokenLab model-list shape. These are independent contract examples, not registry data. + const result = extractProviderModelItems({ object: "list", data: [ + { id: "eligible-chat", tokenlab: { category: "chat", capabilities: ["vision", "tool-use"] } }, + { id: "plain-chat", tokenlab: { category: "chat", capabilities: [] } }, + { id: "gpt-image-2", tokenlab: { category: "image", capabilities: ["tool-use"] } }, + { id: "video-model", tokenlab: { category: "video", capabilities: ["tool-use"] } }, + { id: "embedding-model", tokenlab: { category: "embedding", capabilities: [] } }, + { id: "jev-1.13", tokenlab: { category: "decision", capabilities: ["tool-use"] } }, + { id: "missing-capabilities", tokenlab: { category: "chat" } }, + { id: "missing-category", tokenlab: { capabilities: ["tool-use"] } }, + { id: "unclassified" }, + ] }, discovery); + expect(result.ok).toBe(true); + if (!result.ok) throw new Error(result.reason); + expect(result.items.map(row => row.id)).toEqual(["eligible-chat"]); + }); + + test("uses the authenticated live catalog without resurrecting an unavailable seeded model", async () => { + let requests = 0; + globalThis.fetch = (async (input, init) => { + requests++; + expect(String(input)).toBe("https://api.tokenlab.sh/v1/models?category=chat"); + expect(new Headers(init?.headers).get("authorization")).toBe("Bearer test-key"); + expect(init?.redirect).toBe("manual"); + return Response.json({ object: "list", data: [ + { id: "claude-sonnet-4-6", tokenlab: { category: "chat", capabilities: ["tool-use"] } }, + { id: "image-model", tokenlab: { category: "image", capabilities: [] } }, + ] }); + }) as typeof fetch; + const models = (await gatherRoutedModels(config())).filter(row => row.provider === "tokenlab"); + expect(requests).toBe(1); + expect(models.map(row => row.id)).toEqual(["claude-sonnet-4-6"]); + }); + + test("validates a supplied key through discovery and distinguishes rejection from an outage", async () => { + for (const [status, expected] of [[200, true], [401, false], [503, "unknown"]] as const) { + globalThis.fetch = (async (input, init) => { + expect(String(input)).toBe("https://api.tokenlab.sh/v1/models?category=chat"); + expect(new Headers(init?.headers).get("authorization")).toBe("Bearer test-key"); + expect(init?.redirect).toBe("error"); + return new Response(null, { status }); + }) as typeof fetch; + expect(await validateApiKey("tokenlab", KEY_LOGIN_PROVIDERS.tokenlab!, "test-key")).toBe(expected); + } + }); +});