From 69ce3120979f6e231ddb6c60c10ffdd8e2c87520 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:23:58 +0900 Subject: [PATCH 1/5] chore(release): open dev at 2.72.0 before releasing 2.71.0 (#6226) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- desktop/src-tauri/Cargo.lock | 2 +- desktop/src-tauri/Cargo.toml | 2 +- desktop/src-tauri/tauri.conf.json | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index db6440014d..8d650cfc89 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -2645,7 +2645,7 @@ dependencies = [ [[package]] name = "opencodex-desktop" -version = "2.71.0" +version = "2.72.0" dependencies = [ "base64 0.22.1", "dbus", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index d88de95341..0b995e022c 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "opencodex-desktop" -version = "2.71.0" +version = "2.72.0" description = "OpenCodex desktop shell" authors = ["OpenCodex contributors"] license = "MIT" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 0535146625..c3058739b7 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenCodex", - "version": "2.71.0", + "version": "2.72.0", "identifier": "com.opencodex.desktop", "build": { "frontendDist": "../ui", diff --git a/package.json b/package.json index 59b49ff6ae..f3f51f4cbc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.71.0", + "version": "2.72.0", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From 86ac102611e86ec12b3700b8ec8db2bd4deca2ff Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 29 Sep 2026 16:14:14 +0900 Subject: [PATCH 2/5] docs(devlog): record the 2.71.0 release (#6229) * docs(devlog): record the 2.71.0 landing on dev * docs(devlog): script the 2.71.0 release * docs(devlog): fold the 2.71.0 release audit * docs(devlog): record the 2.71.0 release outcome * docs(devlog): close the 2.71.0 release unit --- .../260929_release_2_71_0/000_plan.md | 0 .../260929_release_2_71_0/001_consultation.md | 0 .../260929_release_2_71_0/010_integration.md | 0 .../011_wp1_execution.md | 0 .../260929_release_2_71_0/020_verification.md | 0 .../021_wp2_execution.md | 0 .../260929_release_2_71_0/022_wp2_results.md | 0 .../260929_release_2_71_0/030_land.md | 0 .../031_wp3_execution.md | 0 .../260929_release_2_71_0/032_wp3_results.md | 18 ++++++ .../260929_release_2_71_0/040_release.md | 5 +- .../041_wp4_execution.md | 59 +++++++++++++++++++ .../_fin/260929_release_2_71_0/090_outcome.md | 38 ++++++++++++ 13 files changed, 117 insertions(+), 3 deletions(-) rename devlog/{_plan => _fin}/260929_release_2_71_0/000_plan.md (100%) rename devlog/{_plan => _fin}/260929_release_2_71_0/001_consultation.md (100%) rename devlog/{_plan => _fin}/260929_release_2_71_0/010_integration.md (100%) rename devlog/{_plan => _fin}/260929_release_2_71_0/011_wp1_execution.md (100%) rename devlog/{_plan => _fin}/260929_release_2_71_0/020_verification.md (100%) rename devlog/{_plan => _fin}/260929_release_2_71_0/021_wp2_execution.md (100%) rename devlog/{_plan => _fin}/260929_release_2_71_0/022_wp2_results.md (100%) rename devlog/{_plan => _fin}/260929_release_2_71_0/030_land.md (100%) rename devlog/{_plan => _fin}/260929_release_2_71_0/031_wp3_execution.md (100%) create mode 100644 devlog/_fin/260929_release_2_71_0/032_wp3_results.md rename devlog/{_plan => _fin}/260929_release_2_71_0/040_release.md (96%) create mode 100644 devlog/_fin/260929_release_2_71_0/041_wp4_execution.md create mode 100644 devlog/_fin/260929_release_2_71_0/090_outcome.md diff --git a/devlog/_plan/260929_release_2_71_0/000_plan.md b/devlog/_fin/260929_release_2_71_0/000_plan.md similarity index 100% rename from devlog/_plan/260929_release_2_71_0/000_plan.md rename to devlog/_fin/260929_release_2_71_0/000_plan.md diff --git a/devlog/_plan/260929_release_2_71_0/001_consultation.md b/devlog/_fin/260929_release_2_71_0/001_consultation.md similarity index 100% rename from devlog/_plan/260929_release_2_71_0/001_consultation.md rename to devlog/_fin/260929_release_2_71_0/001_consultation.md diff --git a/devlog/_plan/260929_release_2_71_0/010_integration.md b/devlog/_fin/260929_release_2_71_0/010_integration.md similarity index 100% rename from devlog/_plan/260929_release_2_71_0/010_integration.md rename to devlog/_fin/260929_release_2_71_0/010_integration.md diff --git a/devlog/_plan/260929_release_2_71_0/011_wp1_execution.md b/devlog/_fin/260929_release_2_71_0/011_wp1_execution.md similarity index 100% rename from devlog/_plan/260929_release_2_71_0/011_wp1_execution.md rename to devlog/_fin/260929_release_2_71_0/011_wp1_execution.md diff --git a/devlog/_plan/260929_release_2_71_0/020_verification.md b/devlog/_fin/260929_release_2_71_0/020_verification.md similarity index 100% rename from devlog/_plan/260929_release_2_71_0/020_verification.md rename to devlog/_fin/260929_release_2_71_0/020_verification.md diff --git a/devlog/_plan/260929_release_2_71_0/021_wp2_execution.md b/devlog/_fin/260929_release_2_71_0/021_wp2_execution.md similarity index 100% rename from devlog/_plan/260929_release_2_71_0/021_wp2_execution.md rename to devlog/_fin/260929_release_2_71_0/021_wp2_execution.md diff --git a/devlog/_plan/260929_release_2_71_0/022_wp2_results.md b/devlog/_fin/260929_release_2_71_0/022_wp2_results.md similarity index 100% rename from devlog/_plan/260929_release_2_71_0/022_wp2_results.md rename to devlog/_fin/260929_release_2_71_0/022_wp2_results.md diff --git a/devlog/_plan/260929_release_2_71_0/030_land.md b/devlog/_fin/260929_release_2_71_0/030_land.md similarity index 100% rename from devlog/_plan/260929_release_2_71_0/030_land.md rename to devlog/_fin/260929_release_2_71_0/030_land.md diff --git a/devlog/_plan/260929_release_2_71_0/031_wp3_execution.md b/devlog/_fin/260929_release_2_71_0/031_wp3_execution.md similarity index 100% rename from devlog/_plan/260929_release_2_71_0/031_wp3_execution.md rename to devlog/_fin/260929_release_2_71_0/031_wp3_execution.md diff --git a/devlog/_fin/260929_release_2_71_0/032_wp3_results.md b/devlog/_fin/260929_release_2_71_0/032_wp3_results.md new file mode 100644 index 0000000000..1affb33855 --- /dev/null +++ b/devlog/_fin/260929_release_2_71_0/032_wp3_results.md @@ -0,0 +1,18 @@ +# 032 wp3 results: landed on dev + +| Step | Evidence | +|---|---| +| Screenshots | pr-assets 947e869c8b (`260929-release-2-71-0/request-pacing.png`, `cursor-installer.png`), fast-forward from ea1749154d | +| PR | #6224 `chore(release): integrate six reviewed PRs for 2.71.0`, head dcfbb2f708 | +| PR-event CI | Cross-platform CI 36525839698 success; Service lifecycle 36525839767 success; React Doctor 36525839694, PR hygiene, Enforce PR target branch success | +| Full-lane CI | Cross-platform CI workflow_dispatch 36526719414 (lane all) success: 39 jobs success, 1 skipped; includes windows 1/9-9/9 (the pull_request event skips the Windows matrix, ci.yml:835-839) and macos control | +| Head totals | 80 check-runs: 74 success, 6 skipped by design, 0 failed | +| Policy | `scripts/ci/assert-mergeable-review.sh --maintainer-integration 6224`: OK; decision recorded in PR comment 5884552184 | +| Merge | `gh pr merge 6224 --admin --merge --match-head-commit dcfbb2f708` -> dev d161c0e83e; all six land commits and four fixes are ancestors of dev | +| Source PRs | #6206 #6201 #6209 #6094 #5905 #6198 closed with a comment naming the land commit; issue #6208 closed with the #6209 commit | + +Note: windows 8/9 passed in 36526719414, including the 32-profile transaction case that failed +the dev tip in 36499924172, now under BULK_DURABLE_IO_BUDGET_MS. + +Candidate for 040: dev d161c0e83e (version sources 2.71.0). + diff --git a/devlog/_plan/260929_release_2_71_0/040_release.md b/devlog/_fin/260929_release_2_71_0/040_release.md similarity index 96% rename from devlog/_plan/260929_release_2_71_0/040_release.md rename to devlog/_fin/260929_release_2_71_0/040_release.md index aa108ce81b..174aa21149 100644 --- a/devlog/_plan/260929_release_2_71_0/040_release.md +++ b/devlog/_fin/260929_release_2_71_0/040_release.md @@ -7,8 +7,8 @@ that merge commit is informative; the gating runs are on the promotion SHAs. 1. Pre-move dev: `gh workflow run dev-version-bump.yml --ref main -f intended-version=2.71.0 -f mode=pre-move`; the opened PR must change only the four version sources to 2.72.0 (package.json, - desktop/src-tauri/Cargo.toml, Cargo.lock, tauri.conf.json); merge it with --admin --merge - --match-head-commit. + desktop/src-tauri/Cargo.toml, Cargo.lock, tauri.conf.json); merge it with --admin --squash + --match-head-commit (as #6213). 2. Preview: branch `codex/promote-preview-2-71-0` from the candidate, `git merge -s ours origin/preview`, `bun scripts/release-version-sources.ts sync 2.71.0-preview.20260929`, commit, PR to preview, merge commit. Wait for push-event Cross-platform CI and Service lifecycle success on the @@ -28,4 +28,3 @@ Guards: verify preview tree differs from candidate only in the four version sour equals the candidate. A failing promotion run is fixed through dev and re-promoted. If a release run fails after npm acknowledged publication, re-dispatch with resume-after-npm-publish=true and the same expected-sha; never republish. - diff --git a/devlog/_fin/260929_release_2_71_0/041_wp4_execution.md b/devlog/_fin/260929_release_2_71_0/041_wp4_execution.md new file mode 100644 index 0000000000..f1db2d5025 --- /dev/null +++ b/devlog/_fin/260929_release_2_71_0/041_wp4_execution.md @@ -0,0 +1,59 @@ +# 041 wp4 execution: release 2.71.0 + +Previous D (wp3): #6224 merged, dev d161c0e83e; final CI green including windows 1-9. Direction +unchanged: release that candidate exactly as 040 (same shape as 2.70.0: #6211/#6212/#6213). + +Revalidated at wp4 P: dev d161c0e83e (package 2.71.0), preview aa3a8dda16 (2.70.0-preview.20260929), +main 53834ff47b (2.70.0); npm latest=2.70.0, preview=2.70.0-preview.20260929. Candidate C = +d161c0e83ea8a88027b2cef28c125ff3c1f29f8a. + +## Order and parallelism + +1. Pre-move dispatch (runs on GitHub, ~1 min): + `gh workflow run dev-version-bump.yml -R lidge-jun/opencodex --ref main -f intended-version=2.71.0 -f mode=pre-move` + -> PR `codex/dev-version-2.72.0`. Accept only if its files are exactly the four version sources and + each reads 2.72.0. Its PR CI runs while steps 2-3 proceed; merge it (`--admin --squash + --match-head-commit`, as #6213) once its Cross-platform CI and gates are green. It is merged + before any release dispatch; the stable dispatch strictly requires it (release-preflight.sh + assert-ahead), and doing it first for both keeps one order. +2. Preview branch (managed worktree, clean tree): + ```sh + git switch -c codex/promote-preview-2.71.0 d161c0e83ea8a88027b2cef28c125ff3c1f29f8a + git merge -s ours refs/remotes/canon/preview -m "Merge preview into 2.71.0-preview.20260929 promotion" + bun scripts/release-version-sources.ts sync 2.71.0-preview.20260929 + git commit -am "chore(release): 2.71.0-preview.20260929" + git diff --name-only d161c0e83e HEAD # must list exactly the four version sources + bun scripts/release-version-sources.ts # check mode: all sources 2.71.0-preview.20260929 + ``` + Push, PR to preview titled "release: 2.71.0-preview.20260929", body as #6211 (plus the two + pr-assets screenshots because the union touches gui/), merge with `--admin --merge + --match-head-commit`. +3. Main branch: + ```sh + git switch -c codex/promote-main-2.71.0 d161c0e83ea8a88027b2cef28c125ff3c1f29f8a + git merge -s ours refs/remotes/canon/main -m "Merge main into 2.71.0 promotion" + git diff --quiet d161c0e83e HEAD # tree equals the candidate + ``` + Push, PR to main titled "release: 2.71.0", body as #6212 plus screenshots, merge the same way. +4. Gate per promotion SHA (the merge commits on preview and main): push-event Cross-platform CI and + Service lifecycle both `success` (release.yml checks both). A red run is diagnosed; a flake with + evidence may be rerun once; a real defect is fixed on dev and re-promoted. +5. Dispatch, preview first: + `gh workflow run release.yml -R lidge-jun/opencodex --ref preview -f version=2.71.0-preview.20260929 -f tag=preview -f dry-run=false -f expected-sha=` + then after it succeeds: + `gh workflow run release.yml -R lidge-jun/opencodex --ref main -f version=2.71.0 -f tag=latest -f dry-run=false -f expected-sha=
` + On a failure after npm acknowledged publication: same inputs plus `-f resume-after-npm-publish=true`. +6. Verify: `npm view @bitkyc08/opencodex dist-tags --json` (latest=2.71.0, preview=2.71.0-preview.20260929), + `npm view @bitkyc08/opencodex@2.71.0 gitHead` = main merge sha, `gh release view v2.71.0` (not + prerelease, asset count as v2.70.0 = 25), `gh release view v2.71.0-preview.20260929` (prerelease), + latest.json from the v2.71.0 release reports 2.71.0 with signed platforms. +7. Outcome: 090_outcome.md, move the unit to devlog/_fin/260929_release_2_71_0, docs PR to dev from + a branch on the pre-moved dev tip; merge after its gates. + +Merging promotion PRs is owner-authorized in this session ("배포까지 완료"). enforce-target flags +promotion PRs as wrong base by design (2.70.0 precedent); that check is not required for preview/main. + +D11 (architect, wp4, ALIGNED): release.yml:847-867 accepts only a push-event ci.yml run on the exact +SHA (a PR run does not count); package.json is in both ci.yml push paths (:51) and +service-lifecycle.yml push paths (:19), so both gating runs fire on each promotion merge commit; +release-preflight.sh enforces dev strictly ahead, so the pre-move PR merges first. diff --git a/devlog/_fin/260929_release_2_71_0/090_outcome.md b/devlog/_fin/260929_release_2_71_0/090_outcome.md new file mode 100644 index 0000000000..f3bd2a59d2 --- /dev/null +++ b/devlog/_fin/260929_release_2_71_0/090_outcome.md @@ -0,0 +1,38 @@ +# 090 Outcome: 2.71.0 released + +Published 2026-09-29 (KST). GitHub releases `v2.71.0` and `v2.71.0-preview.20260929` exist with 25 +assets each; npm accepted both publications with signed provenance. + +Six reviewed PRs landed on dev with author credit, the Windows timeout that had turned the dev tip +red was fixed, and the whole set was released as 2.71.0 and 2.71.0-preview.20260929. + +| Step | Evidence | +|---|---| +| Integration | #6224 merged to dev as d161c0e83e (merge commit). Land commits: df951bc2ee #6206 (Ingwannu), 6d7af977a4 #6201 (luvs01), 21ccf35dbe #6209 (Jio Kim), e3fcf3e43c #6094 (Brad Hallett), 2b0ac07170 #5905 (halysondev), febc16d2a4 #6198 (luvs01); follow-ups 9155880950, 7b875559dd, 917db156de, 441aeb179e. Details in 032. | +| Local regression | 022: all gates exit 0; full-suite failures proven pre-existing at dev 37ad7e771b. | +| Final CI | #6224 head dcfbb2f708: Cross-platform CI pull_request 36525839698, workflow_dispatch lane=all 36526719414 (windows 1-9), Service lifecycle 36525839767, all success; 0 failed check-runs. | +| dev pre-move | Dev version bump run 36529404555 opened #6226; its PR checks were approval-gated (bot-authored, as #6213), so the version tests and typecheck ran locally on its head 316750815c (209 pass) before `--admin --squash`; dev 69ce312097 reads 2.72.0. | +| Preview | #6227 merged as fd346e2489 (tree = candidate + four version sources at 2.71.0-preview.20260929). Push Cross-platform CI 36529530341 and Service lifecycle 36529530333 success. Release run 36531705017 success; npm `preview=2.71.0-preview.20260929`, gitHead fd346e2489, provenance attestation present. | +| Stable | #6228 merged as 8a005dd98f (tree equal to the candidate). Push Cross-platform CI 36529537253 and Service lifecycle 36529537201 success. Release run 36533478070 success; npm publish acknowledged (`+ @bitkyc08/opencodex@2.71.0`, sigstore log 2995422874); GitHub release `v2.71.0` not prerelease, 25 assets; latest.json reports 2.71.0 with 5 signed platforms. | +| Source PRs | #6206 #6201 #6209 #6094 #5905 #6198 closed with landing comments; issue #6208 closed. | + +## Notes + +- The pull_request event skips the Windows matrix (ci.yml:835-839); a workflow_dispatch run on the + integration head covered windows 1-9 before merge. +- The promotion PRs' own pull_request CI runs (36529486815, 36529483354) were cancelled to free + runners after both promotions merged; the push runs on the same trees gate the release. +- The registry smoke step warned on both releases because npm took a few minutes to serve the new + versions; no republish was attempted. +- The installed proxy, service and desktop app on this machine were not updated. + +## What did not improve / open follow-ups + +- Local full `bun run test` on a loaded macOS host is not a clean signal: a claude-integration + server test can hang and cascade into `SpendLedgerOwnerError` for the rest of its worker, and + `tests/service/shutdown-launcher.test.ts` fails and leaves orphan proxies. Both reproduce at dev + 37ad7e771b; they deserve their own issue. +- Optional hardening noted by reviewers and not built here: a response-size cap and redirect policy + for the Cursor manifest fetch (#5905), strict single-[3] TBS parsing in picker-ca (#6201), extra + subprocess tests for #6209, localized copies of the cli.md priority note. + From f6cddd7d69c79f3be7e51ba7c329c6d3969416f4 Mon Sep 17 00:00:00 2001 From: hedging8563 <45883076+hedging8563@users.noreply.github.com> Date: Tue, 29 Sep 2026 20:51:58 +0800 Subject: [PATCH 3/5] feat(providers): add TokenLab API-key preset (#6221) Adds TokenLab as an ordinary API-key preset (openai-chat, https://api.tokenlab.sh/v1) in the dashboard, CLI initializer and `ocx provider add tokenlab`. Live discovery sends the configured Bearer key to /v1/models?category=chat and keeps only chat rows with tool-use; the authenticated catalog takes precedence over the seeded gpt-5.6-terra. Same-named custom destinations are preserved. The provider guide documents setup, discovery scope and service terms, and the localized preset counts are recounted to 100 (83 key-based).\n\nMerged by the maintainer under the MAINTAINERS.md dev integration policy on the repository owner's explicit instruction, at head fb565b7c48eb215d7806e7e1202a4d84ded2bf4d with 24/24 non-skipped checks green on that exact head (Cross-platform CI run 36567400620, React Doctor). The admission decision and credential-destination review are recorded on the PR. --- .../docs/fr/getting-started/quickstart.md | 2 +- .../src/content/docs/fr/guides/providers.md | 2 +- .../docs/getting-started/quickstart.md | 2 +- .../src/content/docs/guides/providers.md | 18 ++- .../docs/ja/getting-started/quickstart.md | 2 +- .../src/content/docs/ja/guides/providers.md | 2 +- .../docs/ko/getting-started/quickstart.md | 2 +- .../src/content/docs/ko/guides/providers.md | 2 +- .../docs/ru/getting-started/quickstart.md | 2 +- .../src/content/docs/ru/guides/providers.md | 2 +- .../docs/tr/getting-started/quickstart.md | 2 +- .../src/content/docs/tr/guides/providers.md | 2 +- .../docs/zh-cn/getting-started/quickstart.md | 2 +- .../content/docs/zh-cn/guides/providers.md | 2 +- .../docs/zh-tw/getting-started/quickstart.md | 2 +- .../content/docs/zh-tw/guides/providers.md | 2 +- scripts/test-layout/layout.json | 1 + src/providers/registry/entries-extended.ts | 28 +++++ structure/ops/docs-and-release.md | 2 +- structure/providers-and-adapters.md | 10 ++ tests/fixtures/test-layout-expected.json | 1 + .../provider-registry-parity.test.ts | 2 +- tests/providers/tokenlab-provider.test.ts | 113 ++++++++++++++++++ 23 files changed, 187 insertions(+), 18 deletions(-) create mode 100644 tests/providers/tokenlab-provider.test.ts 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 4d08caed17..3cf3a91b39 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 1f2090b4b8..641b9a9373 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 8f863b6f51..d49783ed4e 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 b8c6c7510c..821a5b8117 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 d2a3c3f4b8..3760250bc9 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 f54fa25dd0..282f7335c0 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 b0b22222a3..7ddac09c0d 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 8027e1ff1f..a3184d953c 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 945893e045..9552ed50b4 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 579005a20d..4bc043e2fa 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 2d45af871c..21056695bb 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 58c628b2dd..76201a9623 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 cb5abdcc45..b8561722c3 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 5b8c902e5b..0e8df8f42a 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 d7e601b025..f0b02c6ab8 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 5d68c93eb5..3b8c18a509 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 2cd657d98f..fd19e2040b 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 96defecfd9..7740782ea4 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 93536baaf5..168ad3c622 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 24d10b7204..b35fe601b4 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 784f61f688..459506dab5 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 d6b75c5565..9c3bd6e2aa 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 0000000000..b6ec09b387 --- /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); + } + }); +}); From 1cd9d25517aa5ffbcbd1aab40ce6ca97fbbad27f Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 30 Sep 2026 01:25:06 +0900 Subject: [PATCH 4/5] feat(sponsors): TokenLab Standard sponsor placement, README row and docs (#6240) * feat(sponsors): TokenLab Standard sponsor placement, README row and docs TokenLab is the third Standard sponsor under SPONSORS.md, after OrcaRouter and PackyCode, so it takes the third row of the Standard table and is pinned with the other sponsors in the picker (Main before Standard, then alphabetical by label). The tokenlab registry entry gains the sponsor field; routing, discovery and defaults never read it. The dashboard sponsor card now looks brands up in a table, so a sponsor preset without a row renders nothing instead of borrowing another sponsor's copy. Adds the sponsor-supplied logos (light and dark lockups via ), the translated README rows and manifest hashes, the picker mark for the GUI and desktop shell, localized card copy in every dashboard locale, a sponsor link in the provider guide, and fixes two stale SPONSORS.md sentences. * fix(sponsors): pin sponsor presets in ocx init and ocx provider presets SPONSORS.md promises sponsor presets near the top of the picker in the dashboard and CLI, but only the dashboard applied it: `ocx init` listed TokenLab 84th and `ocx provider presets` printed raw registry order. Both now use the dashboard's order (Main before Standard, alphabetical by label). In `ocx init` the sponsor rows move to the start of the first run of their own kind, so they stay under the right heading and every other row keeps its place; `ocx provider presets` lists sponsor rows first. The derivation and the /api/provider-presets payload are unchanged. * fix(sponsors): keep a sponsor-first run in place in the init menu pinSponsorsWithinKind recorded a kind only at its first non-sponsor row, so a run that opened with a sponsor moved that sponsor to a later run of the same kind (or to the end when its kind had no other row), changing the heading sequence. The kind is now recorded at its first row of any kind. Reported by CodeRabbit on #6240. * docs(sponsors): use TokenLab's final blurb and referral link TokenLab sent its final sponsor copy on 2026-09-29: an English blurb that also names Anthropic Messages, Gemini's native API, an MCP server and agent Skills, a Chinese blurb, and https://tokenlab.sh/r/OPENCODEX as the README and picker link in place of the homepage. The README row now carries both blurbs verbatim (the Chinese one as the second-language line, as for PackyCode), the translated rows follow, and the registry sponsor URL, providers guide and tests use the referral link. --- README.md | 4 ++ SPONSORS.md | 9 ++- assets/sponsors/tokenlab-dark.png | Bin 0 -> 6089 bytes assets/sponsors/tokenlab-light.png | Bin 0 -> 6234 bytes desktop/src-tauri/src/provider_icons.rs | 3 + .../260929_tokenlab_sponsor/000_roadmap.md | 13 ++++ .../260929_tokenlab_sponsor/010_merge_6221.md | 37 ++++++++++ .../260929_tokenlab_sponsor/020_sponsor_pr.md | 63 ++++++++++++++++++ .../src/content/docs/guides/providers.md | 7 +- gui/public/provider-icons/README.md | 9 +++ gui/public/provider-icons/tokenlab.svg | 8 +++ .../provider-workspace/ProviderSponsor.tsx | 34 +++++++--- gui/src/i18n/de.ts | 2 + gui/src/i18n/en.ts | 2 + gui/src/i18n/fr.ts | 2 + gui/src/i18n/ja.ts | 2 + gui/src/i18n/ko.ts | 2 + gui/src/i18n/ru.ts | 2 + gui/src/i18n/tr.ts | 2 + gui/src/i18n/vi.ts | 2 + gui/src/i18n/zh-TW.ts | 2 + gui/src/i18n/zh.ts | 2 + gui/src/provider-icons.ts | 3 + gui/src/styles/provider-workspace-shell.css | 7 +- .../provider-catalog-sponsor-pinning.test.ts | 3 +- gui/tests/provider-sponsor-overview.test.tsx | 21 ++++++ package.json | 2 + readme/README.fr.md | 4 ++ readme/README.ja.md | 4 ++ readme/README.ko.md | 4 ++ readme/README.ru.md | 4 ++ readme/README.tr.md | 4 ++ readme/README.zh-CN.md | 4 ++ readme/README.zh-TW.md | 4 ++ readme/i18n-manifest.json | 14 ++-- src/cli/init.ts | 3 +- src/cli/provider-runtime.ts | 11 ++- src/providers/registry/entries-extended.ts | 3 + src/providers/sponsor-order.ts | 56 ++++++++++++++++ structure/dashboard-and-usage.md | 6 +- tests/providers/sponsor-presets.test.ts | 42 ++++++++++++ tests/providers/tokenlab-provider.test.ts | 4 +- 42 files changed, 377 insertions(+), 33 deletions(-) create mode 100644 assets/sponsors/tokenlab-dark.png create mode 100644 assets/sponsors/tokenlab-light.png create mode 100644 devlog/_plan/260929_tokenlab_sponsor/000_roadmap.md create mode 100644 devlog/_plan/260929_tokenlab_sponsor/010_merge_6221.md create mode 100644 devlog/_plan/260929_tokenlab_sponsor/020_sponsor_pr.md create mode 100644 gui/public/provider-icons/tokenlab.svg create mode 100644 src/providers/sponsor-order.ts diff --git a/README.md b/README.md index 4fdc0868de..907a481db9 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,10 @@ See [SPONSORS.md](./SPONSORS.md). PackyCode Thanks to PackyCode for sponsoring this project! PackyCode is a stable, high-performance API relay provider, offering relay services for Claude Code, Codex, Gemini, and more. With automatic failover, smart routing, and unlimited concurrency, it turns AI into a real productivity tool. Register via this link and get started! Pick PackyCode in the Add provider picker or run ocx provider add packycode.
PackyCode 是一家稳定、高效的 API 中转服务商,提供 Claude Code、Codex、Gemini 等多种中转服务。具备自动故障转移、智能路由和无限并发等多种功能,让 AI 编程成为真正的生产力工具。点此链接注册,立即开始使用! + +TokenLab +Thanks to TokenLab for sponsoring this project! TokenLab gives coding agents one API key for leading models, supporting OpenAI Responses and Chat Completions, Anthropic Messages, and Gemini's native API formats, with streaming and tool calling. It also provides an MCP server and agent Skills for easy integration. Choose your delivery mode and pay as you go. Pick TokenLab in the Add provider picker or run ocx provider add tokenlab.
TokenLab 为编程智能体提供统一的多模型 API,一枚 API Key 即可接入主流模型,支持 OpenAI Responses、Chat Completions、Anthropic Messages 和 Gemini 原生 API 格式,以及流式输出和工具调用。同时提供 MCP 服务器和 Agent Skills,方便接入现有工作流;交付模式可选,按量付费。 + diff --git a/SPONSORS.md b/SPONSORS.md index 76a54c377b..97506d9ce9 100644 --- a/SPONSORS.md +++ b/SPONSORS.md @@ -47,8 +47,8 @@ sponsor receives: policy. A second-language blurb (for example Chinese) may run alongside the English one. - A built-in provider preset (`ocx provider add `) shipped in a public npm release, listed near the top of the provider picker in the dashboard and CLI and marked as a sponsor - there. (The registry field and picker ordering that back this land with the first sponsor - preset; today the picker follows registry order.) + there. The dashboard picker, `ocx init` and `ocx provider presets` list sponsor rows Main + before Standard, then alphabetically by label, an order no sponsor can buy. - A detailed entry on the [providers page](https://opencodex.me/guides/providers/) of the docs site. - Maintenance: if a release breaks the preset or its adapter, the maintainer fixes it; issues @@ -66,9 +66,8 @@ and the placements themselves: The README says nothing else about sponsorship; tiers, pricing, and contact channels live only on this page. -The translated READMEs under [`readme/`](./readme) carry one linking line right after their -own quick-start block instead of duplicating the section, so a sponsor change is one edit in -English. +The translated READMEs under [`readme/`](./readme) carry the same sponsor section, with each +row's thanks line and blurb translated, so a sponsor change updates every README together. ## Pricing diff --git a/assets/sponsors/tokenlab-dark.png b/assets/sponsors/tokenlab-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..7c1ef48dac96524bef0e09715f480fcc60c4c184 GIT binary patch literal 6089 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 Mp|)n#CU;Q(1ysqV1ONa4 literal 0 HcmV?d00001 diff --git a/assets/sponsors/tokenlab-light.png b/assets/sponsors/tokenlab-light.png new file mode 100644 index 0000000000000000000000000000000000000000..37a0f17ef694210cb746c1026b65da99b3312ad1 GIT binary patch literal 6234 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 &'static str { | "packycode.svg" | "siliconflow.svg" | "synthetic.svg" + | "tokenlab.svg" | "vercel-ai-gateway-color.svg" | "zenmux.svg" => "mask", "baseten.svg" | "kilo.svg" | "sambanova.svg" | "venice.svg" | "zai.svg" => "plate", @@ -200,6 +202,7 @@ fn svg(file: &str) -> Option<&'static str> { "stepfun-color.svg" => svg!("stepfun-color.svg"), "synthetic.svg" => svg!("synthetic.svg"), "together.svg" => svg!("together.svg"), + "tokenlab.svg" => svg!("tokenlab.svg"), "umans.svg" => svg!("umans.svg"), "venice.svg" => svg!("venice.svg"), "vercel-ai-gateway-color.svg" => svg!("vercel-ai-gateway-color.svg"), diff --git a/devlog/_plan/260929_tokenlab_sponsor/000_roadmap.md b/devlog/_plan/260929_tokenlab_sponsor/000_roadmap.md new file mode 100644 index 0000000000..3d14bac63c --- /dev/null +++ b/devlog/_plan/260929_tokenlab_sponsor/000_roadmap.md @@ -0,0 +1,13 @@ +# TokenLab — preset merge and third Standard sponsor + +TokenLab (TOKENLAB AI INC.) signed up as a Standard sponsor on 2026-09-29 (DocuSign envelope sent +21:08 KST; USD 1,200 / 3 months, term starts at the npm release that ships the preset). Two units: + +| Doc | Work phase | Outcome | +|-----|-----------|---------| +| [010](./010_merge_6221.md) | wp1 | #6221 (fork `hedging8563`, ordinary API-key preset) merged into `dev` on green exact-head CI | +| [020](./020_sponsor_pr.md) | wp2 | Separate maintainer PR: sponsor field, dashboard sponsor surface, README row #3, docs, assets | + +The sponsor PR stays unmerged for owner review. The Responses-first proposal in TokenLab's asset pack +(`integration-notes.md`) is out of scope for both units: it changes the upstream protocol and needs +its own evidence and PR. diff --git a/devlog/_plan/260929_tokenlab_sponsor/010_merge_6221.md b/devlog/_plan/260929_tokenlab_sponsor/010_merge_6221.md new file mode 100644 index 0000000000..789fa1aa08 --- /dev/null +++ b/devlog/_plan/260929_tokenlab_sponsor/010_merge_6221.md @@ -0,0 +1,37 @@ +# 010 — Merge #6221 (TokenLab API-key preset) + +## State at plan time + +- Head repo `hedging8563/opencodex`, branch `codex/tokenlab-provider`, draft, `maintainerCanModify`. +- Branch updated onto `dev` with `gh pr update-branch` → head `6a9e571202`. +- Fork runs `Cross-platform CI` (36566988054) and `React Doctor` (36566988033) approved by the maintainer. +- Admission gap recorded in the PR body: routing/resale authorization evidence. The owner's + sponsorship agreement with the operator (§2.2: sourcing and delivery mode are TokenLab's + responsibility, not audited or guaranteed by opencodex) is the maintainer decision that closes it; + no upstream endorsement claim is added. + +## Steps + +0. Head moved to `fb565b7c48`: the `dev` union failed `docs-provider-preset-counts` (16 assertions, + localized 99/82 counts). Recounted the anchored lines; the test passes locally 18/18. +1. Wait for every required check on `6a9e571202` to complete successfully. A red, skipped or + older-head result blocks merge; fix forward only if the failure is in this diff. +2. Mark the PR ready (`gh pr ready 6221`). +3. Post a maintainer-integration comment: owner instruction, exact head SHA, check list, and the + admission decision above. +4. Squash-merge with the admin bypass (`gh pr merge 6221 --squash --admin`); the squash keeps the + PR author as commit author. +5. Verify `dev` contains the squash commit and `src/providers/registry/entries-extended.ts` carries + the `tokenlab` entry. + +## Not in this unit + +Sponsor field, README, logos — see 020. + +## Outcome (2026-09-29) + +Merged as `f6cddd7d69` at 12:51 UTC from head `fb565b7c48`: 24 checks passed, 7 skipped by path +conditions (Cross-platform CI 36567400620, React Doctor). CodeRabbit's assertive review found nothing +actionable. The readiness gate re-drafted the PR on `ready`; the maintainer ticked the last two boxes +after that review and recorded it on the PR. A full local `bun run test` cannot run from a worktree +under `~/.codex`: the harness refuses to delete temporary directories inside the real Codex home. diff --git a/devlog/_plan/260929_tokenlab_sponsor/020_sponsor_pr.md b/devlog/_plan/260929_tokenlab_sponsor/020_sponsor_pr.md new file mode 100644 index 0000000000..0cbee8371c --- /dev/null +++ b/devlog/_plan/260929_tokenlab_sponsor/020_sponsor_pr.md @@ -0,0 +1,63 @@ +# 020 — TokenLab third Standard sponsor PR + +Branch `codex/tokenlab-sponsor`, started on the #6221 head and rebased onto `dev` after 010 lands. +Pattern: #3914 (OrcaRouter, mechanism) and #3915 (PackyCode, second sponsor). Assets come from +TokenLab's corrected pack (`TokenLab-OpenCodex-assets-corrected.zip`), artwork unchanged. + +## Diff + +- `src/providers/registry/entries-extended.ts` — the `tokenlab` entry gains + `sponsor: { tier: "standard", url: "https://tokenlab.sh/?utm_source=opencodex&utm_medium=readme" }` + with a comment naming SPONSORS.md and the signing date. No routing, default or discovery change. +- `assets/sponsors/tokenlab-light.png`, `assets/sponsors/tokenlab-dark.png` — the pack's 500×125 + lockups, unchanged (`-light` = for light page backgrounds, as in the pack). Listed in + `package.json` `files` beside the OrcaRouter/PackyCode logos. +- `README.md` — third row of the `sponsors:standard` table, in signing order after PackyCode. + Logo in `` with a `prefers-color-scheme: dark` source so the dark lockup shows in + GitHub dark mode; the `` fallback is the light lockup (npm). Text: "Thanks to TokenLab for + sponsoring this project!", the sponsor's English blurb verbatim, then the picker / + `ocx provider add tokenlab` line. +- `readme/README.{ko,ja,zh-CN,zh-TW,ru,fr,tr}.md` — the same row, translated like the existing + rows, image paths `../assets/...`. Keep the README drift gate green. +- `gui/public/provider-icons/tokenlab.svg` — the pack's symbol mark; `gui/src/provider-icons.ts` + maps `tokenlab` to it, sets the display name, and applies dark-mode treatment like other + monochrome marks. +- `gui/src/components/provider-workspace/ProviderSponsor.tsx` — replace the hardcoded + OrcaRouter/PackyCode ternaries with a small brand table so a sponsor is one row; add TokenLab. + i18n keys `pws.sponsor.tokenlabTitle` / `pws.sponsor.tokenlabDescription` in every + `gui/src/i18n/*.ts` locale (the closed catalog must stay exhaustive). +- `docs-site/src/content/docs/guides/providers.md` — sponsor paragraph after PackyCode, and the + base-URL table row if #6221 has not already added it. Translated locales must not contradict it. +- Tests — `tests/providers/sponsor-presets.test.ts` stays generic; the GUI sponsor overview test + covers the TokenLab card. Respect the file-size ratchet: move rather than grow capped files. + +## Verification + +## Audit folds (reviewer, NEAR-PASS) + +- `desktop/src-tauri/src/provider_icons.rs`: `("tokenlab", "tokenlab.svg")` in `ALIASES`, the same paint + arm as `MASKED_PROVIDER_ICONS`, and `svg!("tokenlab.svg")` (`gui/tests/provider-icons-native.test.ts`). +- `readme/i18n-manifest.json`: refresh all seven `sourceSha256` values to the new LF-normalized README hash + (`docs-readme-translation-parity.test.ts`). +- `` is gate-compatible: only `src=`/`href=` are asset-tracked, so each locale's `` must be + `../assets/sponsors/tokenlab-light.png`; include the dark `` too. +- Icon provenance row in `gui/public/provider-icons/README.md`; `structure/dashboard-and-usage.md` owns the + `ProviderSponsor` description — update it for the brand table. +- Docs: extend the existing TokenLab section in `guides/providers.md` with the sponsor link; do not add a + second paragraph. +- `SPONSORS.md`: fix the stale "picker follows registry order" and "translated READMEs carry one linking + line" sentences. +- Branch: after #6221 squashes, move only the sponsor commits onto `dev` + (`git rebase --onto origin/dev fb565b7c48`). + +## Verification (commands) + +Focused: sponsor-presets, provider-registry-parity, tokenlab-provider, README drift/translation +gates, GUI sponsor tests, `bun run typecheck`, `bun run lint:gui`, `bun run privacy:scan`, +`bun run structure:check`, `bun run build:gui`. Then `bun run test` or the documented focused +exception. Screenshot of the dashboard sponsor card and picker uploaded to the `pr-assets` branch +and linked by SHA in the PR description, never committed to the PR branch. + +## Out of scope + +Responses-first adapter and the `X-TokenLab-Delivery-Policy` header (pack `integration-notes.md`). diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 821a5b8117..5bbf4735c6 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -553,7 +553,9 @@ region-pinned EU routes, is at [opper.ai/models](https://opper.ai/models). Opper | 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. +**TokenLab** ([sponsor](https://github.com/lidge-jun/opencodex/blob/main/SPONSORS.md)) is an +OpenAI-compatible API gateway at [tokenlab.sh](https://tokenlab.sh/r/OPENCODEX), +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 @@ -566,7 +568,8 @@ policy. Use a valid key with a funded workspace for inference. `gpt-5.6-terra` i 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. +apply to requests sent to this service. The preset pins the row near the top of the Add provider +picker and marks it as a sponsor, and nothing else about routing or defaults changes. 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 diff --git a/gui/public/provider-icons/README.md b/gui/public/provider-icons/README.md index 7efbbbac91..e53cfda221 100644 --- a/gui/public/provider-icons/README.md +++ b/gui/public/provider-icons/README.md @@ -382,3 +382,12 @@ committing it. gradient is the brand, so it must never be masked. - `factory-droid.svg` — Factory Docs favicon, fetched 2026-09-28 from `https://docs.factory.ai/favicon.svg`; unmodified first-party asset for Factory Droid. + +## TokenLab (2026-09-29) + +- `tokenlab.svg` — TokenLab's "Specimen Split" symbol, `logos/tokenlab-symbol.svg` from the + sponsor asset pack TokenLab supplied for its Standard sponsorship (`SPONSORS.md`), committed + byte for byte (416 bytes, MD5 `fcb8c1325a34ed52b1dc89eabf08a0e6`), ``/`<desc>` included + since they do not affect rendering. `viewBox="0 0 48 48"`, one `#151714` ink, so it is + **masked** like `packycode.svg`: as an image it vanishes on the dark tile. The pack's reverse + symbol (`#F5F5F1`) is the same geometry and is not needed once the mark follows the theme. diff --git a/gui/public/provider-icons/tokenlab.svg b/gui/public/provider-icons/tokenlab.svg new file mode 100644 index 0000000000..6c193a9214 --- /dev/null +++ b/gui/public/provider-icons/tokenlab.svg @@ -0,0 +1,8 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" role="img" aria-labelledby="tokenlab-symbol-title tokenlab-symbol-desc"> + <title id="tokenlab-symbol-title">TokenLab + Specimen Split symbol, positive master for sizes from 32 to 96 pixels. + + + + + diff --git a/gui/src/components/provider-workspace/ProviderSponsor.tsx b/gui/src/components/provider-workspace/ProviderSponsor.tsx index 6b52252b91..621ac4a95b 100644 --- a/gui/src/components/provider-workspace/ProviderSponsor.tsx +++ b/gui/src/components/provider-workspace/ProviderSponsor.tsx @@ -1,8 +1,26 @@ -import { useT } from "../../i18n/shared"; +import { useT, type TKey } from "../../i18n/shared"; import { IconExternal } from "../../icons"; import type { WorkspaceItem } from "../../provider-workspace/catalog"; import { matchingWorkspacePreset, type CatalogPreset } from "../provider-catalog/provider-presets"; +type SponsorBrand = { name: string; title: TKey; description: TKey }; + +const ORCAROUTER: SponsorBrand = { + name: "OrcaRouter", title: "pws.sponsor.orcaTitle", description: "pws.sponsor.orcaDescription", +}; + +/** + * One row per sponsor preset id. The registry `sponsor` field decides whether a card shows; + * this table only supplies the brand name and localized copy, so a preset flagged as a sponsor + * without a row here still renders nothing rather than borrowing another sponsor's words. + */ +const SPONSOR_BRANDS: Readonly> = { + orcarouter: ORCAROUTER, + "orcarouter-oauth": ORCAROUTER, + packycode: { name: "PackyCode", title: "pws.sponsor.packyTitle", description: "pws.sponsor.packyDescription" }, + tokenlab: { name: "TokenLab", title: "pws.sponsor.tokenlabTitle", description: "pws.sponsor.tokenlabDescription" }, +}; + function webLink(value?: string): string | undefined { if (!value) return undefined; try { @@ -16,25 +34,23 @@ function webLink(value?: string): string | undefined { export default function ProviderSponsor({ item, preset }: { item: WorkspaceItem; preset?: CatalogPreset }) { const t = useT(); if (!preset?.sponsor || !matchingWorkspacePreset(item, [preset])) return null; - const brand = preset.id === "orcarouter" || preset.id === "orcarouter-oauth" - ? "OrcaRouter" : preset.id === "packycode" ? "PackyCode" : undefined; + const brand = Object.hasOwn(SPONSOR_BRANDS, preset.id) ? SPONSOR_BRANDS[preset.id] : undefined; if (!brand) return null; - const orca = brand === "OrcaRouter"; const visit = webLink(preset.sponsorUrl); const dashboard = webLink(preset.dashboardUrl); - return
+ return
- {brand} + {brand.name} {t("modal.badge.sponsor")}
-

{t(orca ? "pws.sponsor.orcaTitle" : "pws.sponsor.packyTitle")}

-

{t(orca ? "pws.sponsor.orcaDescription" : "pws.sponsor.packyDescription")}

+

{t(brand.title)}

+

{t(brand.description)}

{(visit || dashboard) &&
{visit && - {t("pws.sponsor.visit", { provider: brand })}} {dashboard && dashboard !== visit && {t("pws.sponsor.console")} { + const html = render(tokenlab, configured(tokenlab)); + expect(html).toContain("TokenLab"); + expect(html).toContain("One API key for leading models"); + expect(html).not.toContain("A model for every prompt"); + expect(html).toContain('href="https://tokenlab.sh/r/OPENCODEX"'); + expect(html).toContain('href="https://tokenlab.sh/dashboard/api?tab=keys"'); + expect(html.match(/ { + const unknown = { ...tokenlab, id: "future-sponsor" }; + expect(render(unknown, configured(unknown))).toBe(""); +}); + test("missing and non-sponsor presets render nothing; non-web links never become anchors", () => { expect(render()).toBe(""); expect(render({ ...orca, sponsor: undefined })).toBe(""); diff --git a/package.json b/package.json index f3f51f4cbc..ab92796acf 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "assets/codex-app-picker.png", "assets/sponsors/orcarouter.png", "assets/sponsors/packycode.png", + "assets/sponsors/tokenlab-light.png", + "assets/sponsors/tokenlab-dark.png", "README.md", "SPONSORS.md", "AGENTS_INSTALL.md", diff --git a/readme/README.fr.md b/readme/README.fr.md index 9329dfd2de..9051bef4d5 100644 --- a/readme/README.fr.md +++ b/readme/README.fr.md @@ -161,6 +161,10 @@ Voir [SPONSORS.md](../SPONSORS.md). PackyCode Merci à PackyCode pour son soutien à ce projet ! PackyCode est un fournisseur de relais API stable et performant, qui propose des services de relais pour Claude Code, Codex, Gemini et d'autres. Grâce au basculement automatique, au routage intelligent et à une concurrence illimitée, il fait de l'IA un véritable outil de productivité. Inscrivez-vous via ce lien et commencez ! Choisissez PackyCode dans le sélecteur Add provider ou exécutez ocx provider add packycode.
PackyCode 是一家稳定、高效的 API 中转服务商,提供 Claude Code、Codex、Gemini 等多种中转服务。具备自动故障转移、智能路由和无限并发等多种功能,让 AI 编程成为真正的生产力工具。点此链接注册,立即开始使用! + +TokenLab +Merci à TokenLab pour son soutien à ce projet ! TokenLab donne aux agents de code une seule clé API pour les principaux modèles, avec les formats OpenAI Responses et Chat Completions, Anthropic Messages et l'API native de Gemini, streaming et appel d'outils compris. Il fournit aussi un serveur MCP et des Skills pour agents afin de faciliter l'intégration. Choisissez votre mode de livraison et payez à l'usage. Choisissez TokenLab dans le sélecteur Add provider ou exécutez ocx provider add tokenlab.
TokenLab 为编程智能体提供统一的多模型 API,一枚 API Key 即可接入主流模型,支持 OpenAI Responses、Chat Completions、Anthropic Messages 和 Gemini 原生 API 格式,以及流式输出和工具调用。同时提供 MCP 服务器和 Agent Skills,方便接入现有工作流;交付模式可选,按量付费。 + diff --git a/readme/README.ja.md b/readme/README.ja.md index 43726a6564..fafe9da489 100644 --- a/readme/README.ja.md +++ b/readme/README.ja.md @@ -156,6 +156,10 @@ failover、アカウントの除外、affinity の失効、401/403 や 429 か PackyCode このプロジェクトを支援してくださる PackyCode に感謝します。PackyCode は安定した高性能の API リレープロバイダーで、Claude Code、Codex、Gemini などのリレーを提供しています。自動 failover、スマートルーティング、無制限の同時実行によって、AI を実際の生産性ツールに変えます。このリンクから登録してすぐに始めてください。Add provider ピッカーで PackyCode を選ぶか ocx provider add packycode を実行してください。
PackyCode 是一家稳定、高效的 API 中转服务商,提供 Claude Code、Codex、Gemini 等多种中转服务。具备自动故障转移、智能路由和无限并发等多种功能,让 AI 编程成为真正的生产力工具。点此链接注册,立即开始使用! + +TokenLab +このプロジェクトを支援してくださる TokenLab に感謝します。TokenLab はコーディングエージェントに主要モデル向けの API キーを 1 つだけ提供し、OpenAI Responses と Chat Completions、Anthropic Messages、Gemini ネイティブ API の各形式に、ストリーミングとツール呼び出し込みで対応しています。MCP サーバーとエージェント Skills も用意されており、簡単に組み込めます。配信モードを選び、使った分だけ支払えます。Add provider ピッカーで TokenLab を選ぶか ocx provider add tokenlab を実行してください。
TokenLab 为编程智能体提供统一的多模型 API,一枚 API Key 即可接入主流模型,支持 OpenAI Responses、Chat Completions、Anthropic Messages 和 Gemini 原生 API 格式,以及流式输出和工具调用。同时提供 MCP 服务器和 Agent Skills,方便接入现有工作流;交付模式可选,按量付费。 + diff --git a/readme/README.ko.md b/readme/README.ko.md index 4facdfd7de..26eee185dc 100644 --- a/readme/README.ko.md +++ b/readme/README.ko.md @@ -153,6 +153,10 @@ Codex Desktop 로그인처럼 다른 계정이 소진된 뒤에만 쓰고 싶은 PackyCode PackyCode의 후원에 감사합니다. PackyCode는 안정적인 고성능 API 릴레이 프로바이더로, Claude Code, Codex, Gemini 등의 릴레이를 제공합니다. 자동 failover, 스마트 라우팅, 무제한 동시성으로 AI를 실제 생산성 도구로 만듭니다. 이 링크로 등록하고 바로 시작하세요. Add provider 선택기에서 PackyCode를 고르거나 ocx provider add packycode를 실행하세요.
PackyCode 是一家稳定、高效的 API 中转服务商,提供 Claude Code、Codex、Gemini 等多种中转服务。具备自动故障转移、智能路由和无限并发等多种功能,让 AI 编程成为真正的生产力工具。点此链接注册,立即开始使用! + +TokenLab +TokenLab의 후원에 감사합니다. TokenLab은 코딩 에이전트에게 주요 모델용 API 키 하나를 제공하며, OpenAI Responses와 Chat Completions, Anthropic Messages, Gemini 네이티브 API 형식을 스트리밍과 도구 호출까지 지원합니다. MCP 서버와 에이전트 Skills도 제공해 쉽게 연동할 수 있습니다. 전달 모드를 고르고 쓴 만큼 결제하세요. Add provider 선택기에서 TokenLab을 고르거나 ocx provider add tokenlab을 실행하세요.
TokenLab 为编程智能体提供统一的多模型 API,一枚 API Key 即可接入主流模型,支持 OpenAI Responses、Chat Completions、Anthropic Messages 和 Gemini 原生 API 格式,以及流式输出和工具调用。同时提供 MCP 服务器和 Agent Skills,方便接入现有工作流;交付模式可选,按量付费。 + diff --git a/readme/README.ru.md b/readme/README.ru.md index c274d4f632..6732e0f312 100644 --- a/readme/README.ru.md +++ b/readme/README.ru.md @@ -164,6 +164,10 @@ opencodex также умеет управлять **пулом аккаунто PackyCode Благодарим PackyCode за спонсорскую поддержку проекта! PackyCode — стабильный высокопроизводительный API-релей, предоставляющий релей-сервисы для Claude Code, Codex, Gemini и других. Автоматический failover, умная маршрутизация и неограниченная конкурентность превращают AI в настоящий инструмент продуктивности. Зарегистрируйтесь по этой ссылке и начните работу! Выберите PackyCode в селекторе Add provider или выполните ocx provider add packycode.
PackyCode 是一家稳定、高效的 API 中转服务商,提供 Claude Code、Codex、Gemini 等多种中转服务。具备自动故障转移、智能路由和无限并发等多种功能,让 AI 编程成为真正的生产力工具。点此链接注册,立即开始使用! + +TokenLab +Благодарим TokenLab за спонсорскую поддержку проекта! TokenLab даёт агентам для программирования один API-ключ для ведущих моделей с поддержкой форматов OpenAI Responses и Chat Completions, Anthropic Messages и нативного API Gemini, включая стриминг и вызов инструментов. Также доступны MCP-сервер и Skills для агентов, чтобы упростить интеграцию. Выберите режим доставки и платите по мере использования. Выберите TokenLab в селекторе Add provider или выполните ocx provider add tokenlab.
TokenLab 为编程智能体提供统一的多模型 API,一枚 API Key 即可接入主流模型,支持 OpenAI Responses、Chat Completions、Anthropic Messages 和 Gemini 原生 API 格式,以及流式输出和工具调用。同时提供 MCP 服务器和 Agent Skills,方便接入现有工作流;交付模式可选,按量付费。 + diff --git a/readme/README.tr.md b/readme/README.tr.md index 1a5e35ae82..6e99c74da0 100644 --- a/readme/README.tr.md +++ b/readme/README.tr.md @@ -160,6 +160,10 @@ Her yukarı akış protokol değişiminde opencodex'in bakımını sürdürebilm PackyCode Bu projeye sponsor olduğu için PackyCode'a teşekkürler! PackyCode, Claude Code, Codex, Gemini ve daha fazlası için aktarma hizmeti sunan istikrarlı ve yüksek başarımlı bir API aktarma sağlayıcısıdır. Otomatik failover, akıllı yönlendirme ve sınırsız eşzamanlılıkla yapay zekâyı gerçek bir üretkenlik aracına dönüştürür. Bu bağlantıdan kaydolun ve hemen başlayın! Add provider seçicisinden PackyCode seçin ya da ocx provider add packycode çalıştırın.
PackyCode 是一家稳定、高效的 API 中转服务商,提供 Claude Code、Codex、Gemini 等多种中转服务。具备自动故障转移、智能路由和无限并发等多种功能,让 AI 编程成为真正的生产力工具。点此链接注册,立即开始使用! + +TokenLab +Bu projeye sponsor olduğu için TokenLab'a teşekkürler! TokenLab, kodlama ajanlarına önde gelen modeller için tek bir API anahtarı sunar; OpenAI Responses ve Chat Completions, Anthropic Messages ve Gemini'nin yerel API biçimleri, akış ve araç çağırma ile desteklenir. Kolay entegrasyon için bir MCP sunucusu ve ajan Skills'i de sağlar. Teslimat modunuzu seçin, kullandıkça ödeyin. Add provider seçicisinden TokenLab seçin ya da ocx provider add tokenlab çalıştırın.
TokenLab 为编程智能体提供统一的多模型 API,一枚 API Key 即可接入主流模型,支持 OpenAI Responses、Chat Completions、Anthropic Messages 和 Gemini 原生 API 格式,以及流式输出和工具调用。同时提供 MCP 服务器和 Agent Skills,方便接入现有工作流;交付模式可选,按量付费。 + diff --git a/readme/README.zh-CN.md b/readme/README.zh-CN.md index 2394f99ca9..f6e4131498 100644 --- a/readme/README.zh-CN.md +++ b/readme/README.zh-CN.md @@ -154,6 +154,10 @@ opencodex 还能为 Codex 认证管理一个 **ChatGPT 账户池**。添加多 PackyCode 感谢 PackyCode 赞助本项目!PackyCode 是一家稳定、高性能的 API 中转提供商,为 Claude Code、Codex、Gemini 等提供中转服务。凭借自动故障转移、智能路由和无限并发,它让 AI 成为真正的生产力工具。通过此链接注册并开始使用!在添加提供商选择器中选择 PackyCode,或运行 ocx provider add packycode。
PackyCode 是一家稳定、高效的 API 中转服务商,提供 Claude Code、Codex、Gemini 等多种中转服务。具备自动故障转移、智能路由和无限并发等多种功能,让 AI 编程成为真正的生产力工具。点此链接注册,立即开始使用! + +TokenLab +感谢 TokenLab 赞助本项目!TokenLab 为编程智能体提供统一的多模型 API,一枚 API Key 即可接入主流模型,支持 OpenAI Responses、Chat Completions、Anthropic Messages 和 Gemini 原生 API 格式,以及流式输出和工具调用。同时提供 MCP 服务器和 Agent Skills,方便接入现有工作流;交付模式可选,按量付费。在添加提供商选择器中选择 TokenLab,或运行 ocx provider add tokenlab。 + diff --git a/readme/README.zh-TW.md b/readme/README.zh-TW.md index 0728a1011d..b53a5d0ed1 100644 --- a/readme/README.zh-TW.md +++ b/readme/README.zh-TW.md @@ -152,6 +152,10 @@ opencodex 也能為 Codex 認證管理 **ChatGPT 帳號池**。新增多個 Chat PackyCode 感謝 PackyCode 贊助本專案!PackyCode 是穩定、高效能的 API 轉送供應商,提供 Claude Code、Codex、Gemini 等轉送服務。具備自動 failover、智慧路由與無限並行,讓 AI 成為真正的生產力工具。透過此連結註冊即可開始!在「新增供應商」選擇器選 PackyCode,或執行 ocx provider add packycode。
PackyCode 是一家稳定、高效的 API 中转服务商,提供 Claude Code、Codex、Gemini 等多种中转服务。具备自动故障转移、智能路由和无限并发等多种功能,让 AI 编程成为真正的生产力工具。点此链接注册,立即开始使用! + +TokenLab +感謝 TokenLab 贊助本專案!TokenLab 為程式設計智慧體提供統一的多模型 API,一把 API Key 即可接入主流模型,支援 OpenAI Responses、Chat Completions、Anthropic Messages 和 Gemini 原生 API 格式,以及串流輸出和工具呼叫。同時提供 MCP 伺服器和 Agent Skills,方便接入現有工作流程;交付模式可選,按量付費。在「新增供應商」選擇器選 TokenLab,或執行 ocx provider add tokenlab。 + diff --git a/readme/i18n-manifest.json b/readme/i18n-manifest.json index 90eb31c4f8..2a4b2803ae 100644 --- a/readme/i18n-manifest.json +++ b/readme/i18n-manifest.json @@ -6,43 +6,43 @@ "file": "readme/README.fr.md", "label": "Français", "docsPath": "fr", - "sourceSha256": "8ae1788ef70c66f4a7cef5e368805cedcdb96925752daf7feae5a0163a830c24" + "sourceSha256": "775eef12d4c0efec79cd4951e74f9e7ceaf244de815360fd3f78729f2420510d" }, "ko": { "file": "readme/README.ko.md", "label": "한국어", "docsPath": "ko", - "sourceSha256": "8ae1788ef70c66f4a7cef5e368805cedcdb96925752daf7feae5a0163a830c24" + "sourceSha256": "775eef12d4c0efec79cd4951e74f9e7ceaf244de815360fd3f78729f2420510d" }, "zh-CN": { "file": "readme/README.zh-CN.md", "label": "简体中文", "docsPath": "zh-cn", - "sourceSha256": "8ae1788ef70c66f4a7cef5e368805cedcdb96925752daf7feae5a0163a830c24" + "sourceSha256": "775eef12d4c0efec79cd4951e74f9e7ceaf244de815360fd3f78729f2420510d" }, "zh-TW": { "file": "readme/README.zh-TW.md", "label": "繁體中文", "docsPath": "zh-tw", - "sourceSha256": "8ae1788ef70c66f4a7cef5e368805cedcdb96925752daf7feae5a0163a830c24" + "sourceSha256": "775eef12d4c0efec79cd4951e74f9e7ceaf244de815360fd3f78729f2420510d" }, "ru": { "file": "readme/README.ru.md", "label": "Русский", "docsPath": "ru", - "sourceSha256": "8ae1788ef70c66f4a7cef5e368805cedcdb96925752daf7feae5a0163a830c24" + "sourceSha256": "775eef12d4c0efec79cd4951e74f9e7ceaf244de815360fd3f78729f2420510d" }, "ja": { "file": "readme/README.ja.md", "label": "日本語", "docsPath": "ja", - "sourceSha256": "8ae1788ef70c66f4a7cef5e368805cedcdb96925752daf7feae5a0163a830c24" + "sourceSha256": "775eef12d4c0efec79cd4951e74f9e7ceaf244de815360fd3f78729f2420510d" }, "tr": { "file": "readme/README.tr.md", "label": "Türkçe", "docsPath": "tr", - "sourceSha256": "8ae1788ef70c66f4a7cef5e368805cedcdb96925752daf7feae5a0163a830c24" + "sourceSha256": "775eef12d4c0efec79cd4951e74f9e7ceaf244de815360fd3f78729f2420510d" } } } diff --git a/src/cli/init.ts b/src/cli/init.ts index e53b203415..e3ae515535 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -8,6 +8,7 @@ import { InitialConfigPublicationError } from "../config/initialize"; import { redactUserPath } from "../lib/redact"; import { enrichProviderFromCatalog } from "../oauth/key-providers"; import { deriveInitProviders } from "../providers/derive"; +import { pinSponsorsWithinKind } from "../providers/sponsor-order"; import type { OcxConfig, OcxProviderConfig } from "../types"; class InitCancelledError extends Error { @@ -126,7 +127,7 @@ export async function runInit(): Promise { try { console.log("\n🔧 opencodex (ocx) setup\n"); - const providers = buildInitProviders(); + const providers = pinSponsorsWithinKind(buildInitProviders()); printMenu(providers); const choice = await prompt.ask("\nSelect default provider (number): "); diff --git a/src/cli/provider-runtime.ts b/src/cli/provider-runtime.ts index ff3d9f96d2..3b20703eb6 100644 --- a/src/cli/provider-runtime.ts +++ b/src/cli/provider-runtime.ts @@ -13,6 +13,7 @@ import { type RuntimeApiDeps, } from "./runtime-api"; import { providerQuotaLine } from "./account-extended"; +import { pinSponsorRows } from "../providers/sponsor-order"; import type { ProviderQuotaReportDto } from "./account-api"; interface ProviderQuotasDto { @@ -233,7 +234,15 @@ async function presets(argv: string[], deps: RuntimeApiDeps): Promise { rejectArgs(args, USAGE); const result = await runtimeRequest<{ providers?: unknown[] } | unknown[]>("/api/provider-presets", {}, deps); const rows = Array.isArray(result) ? result : result.providers ?? []; - printData(result, wantsJson, rows.map(row => { + const pinned = pinSponsorRows( + rows, + row => { + const tier = (row as Record)?.sponsor; + return tier === "main" || tier === "standard" ? tier : undefined; + }, + row => String((row as Record)?.label ?? (row as Record)?.id ?? ""), + ); + printData(result, wantsJson, pinned.map(row => { const record = row as Record; const sponsor = record.sponsor ? ` (sponsor: ${String(record.sponsor)})` : ""; return `${String(record.id ?? record.name ?? "?")} ${String(record.label ?? record.adapter ?? "")}${sponsor}`.trimEnd(); diff --git a/src/providers/registry/entries-extended.ts b/src/providers/registry/entries-extended.ts index 7740782ea4..b72eb462c3 100644 --- a/src/providers/registry/entries-extended.ts +++ b/src/providers/registry/entries-extended.ts @@ -1146,6 +1146,9 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [ baseUrl: "https://api.tokenlab.sh/v1", authKind: "key", dashboardUrl: "https://tokenlab.sh/dashboard/api?tab=keys", + // Standard sponsor under SPONSORS.md (agreement dated 2026-09-29). Pins the row in the + // picker and adds the chip; nothing about routing, discovery or defaults changes. + sponsor: { tier: "standard", url: "https://tokenlab.sh/r/OPENCODEX" }, liveModels: true, preserveCustomDestination: true, defaultModel: "gpt-5.6-terra", diff --git a/src/providers/sponsor-order.ts b/src/providers/sponsor-order.ts new file mode 100644 index 0000000000..7dff406fd4 --- /dev/null +++ b/src/providers/sponsor-order.ts @@ -0,0 +1,56 @@ +import { PROVIDER_REGISTRY } from "./registry"; + +export type SponsorTier = "main" | "standard"; + +const SPONSOR_RANK: Record = { main: 0, standard: 1 }; + +/** + * SPONSORS.md promises sponsor presets sit near the top of the picker "in the dashboard and CLI". + * The dashboard applies that in gui/src/components/provider-catalog/provider-presets.ts + * (`pinSponsors`); this is the same order for the CLI surfaces: sponsor rows first, Main before + * Standard, alphabetical by label within a tier, then every other row in the caller's order. + * Alphabetical among sponsors is deliberate: it is the one order no sponsor can buy. + */ +export function pinSponsorRows( + rows: readonly T[], + tierOf: (row: T) => SponsorTier | undefined, + labelOf: (row: T) => string, +): T[] { + const sponsors = rows.filter(row => tierOf(row) !== undefined); + if (sponsors.length === 0) return [...rows]; + sponsors.sort((a, b) => + SPONSOR_RANK[tierOf(a)!] - SPONSOR_RANK[tierOf(b)!] + || labelOf(a).localeCompare(labelOf(b), undefined, { sensitivity: "base" })); + return [...sponsors, ...rows.filter(row => tierOf(row) === undefined)]; +} + +/** Registry sponsor tier by preset id; the registry field is the only thing that marks a sponsor. */ +export function registrySponsorTier(id: string): SponsorTier | undefined { + return PROVIDER_REGISTRY.find(entry => entry.id === id)?.sponsor?.tier; +} + +/** + * The `ocx init` menu prints a heading whenever the provider kind changes, and registry order + * interleaves kinds, so the same heading appears more than once. Sponsor rows move, in pinned + * order, to the start of the first run of their own kind: they stay under the right heading, sit + * near the top of the menu, and no new heading is introduced. Every other row keeps its order. + */ +export function pinSponsorsWithinKind( + rows: readonly T[], + tierOf: (row: T) => SponsorTier | undefined = row => registrySponsorTier(row.id), +): T[] { + const sponsors = pinSponsorRows(rows.filter(row => tierOf(row) !== undefined), tierOf, row => row.label); + if (sponsors.length === 0) return [...rows]; + const out: T[] = []; + const placedKinds = new Set(); + for (const row of rows) { + // Record the kind at its first row, sponsor or not, so a run that opens with a sponsor + // keeps its place instead of the sponsor moving to a later run of the same kind. + if (!placedKinds.has(row.kind)) { + placedKinds.add(row.kind); + out.push(...sponsors.filter(sponsor => sponsor.kind === row.kind)); + } + if (tierOf(row) === undefined) out.push(row); + } + return out; +} diff --git a/structure/dashboard-and-usage.md b/structure/dashboard-and-usage.md index 3c0651f9f7..29ddb52d62 100644 --- a/structure/dashboard-and-usage.md +++ b/structure/dashboard-and-usage.md @@ -110,9 +110,9 @@ Provider Overview consumes the existing shared `add-provider-presets` resource f presentation. `matchingWorkspacePreset` requires the configured id, adapter and normalized endpoint to match; a custom endpoint or absent sponsor metadata suppresses the introduction. `ProviderSponsor` keeps localized promotional copy and outbound HTTP(S) links separate from -operator notes. Notes remain complete and editable once in the main column; stats and current -account quota remain in the side column. This presentation does not write provider configuration -or participate in routing. +operator notes; its brand table maps each sponsor preset id (OrcaRouter, PackyCode, TokenLab) to a +name and i18n copy, and a sponsor preset without a row renders nothing. Notes stay editable once in +the main column; stats and account quota stay in the side column. Nothing here writes config or routes. Provider marks remain a name-to-asset projection in `gui/src/provider-icons.ts`. The Crusoe preset maps to the self-hosted multicolor `gui/public/provider-icons/crusoe.svg`; the gradient is rendered diff --git a/tests/providers/sponsor-presets.test.ts b/tests/providers/sponsor-presets.test.ts index c7e28e9e16..740e52a36b 100644 --- a/tests/providers/sponsor-presets.test.ts +++ b/tests/providers/sponsor-presets.test.ts @@ -1,6 +1,8 @@ import { describe, expect, test } from "bun:test"; +import { buildInitProviders } from "../../src/cli/init"; import { deriveProviderPresets } from "../../src/providers/derive"; import { PROVIDER_REGISTRY } from "../../src/providers/registry"; +import { pinSponsorRows, pinSponsorsWithinKind } from "../../src/providers/sponsor-order"; /** * The registry `sponsor` field is the only thing that marks a paid sponsor, and SPONSORS.md @@ -36,4 +38,44 @@ describe("sponsor presets", () => { const deduped = registryOrder.filter(id => (seen.has(id) ? false : (seen.add(id), true))); expect(ids).toEqual(deduped); }); + + test("ocx init moves sponsors to the start of the first run of their kind, alphabetical by label", () => { + const rows = pinSponsorsWithinKind(buildInitProviders()); + const sponsorIds = PROVIDER_REGISTRY.filter(entry => entry.sponsor).map(entry => entry.id); + expect(sponsorIds.length).toBeGreaterThan(0); + const firstKey = rows.findIndex(row => row.kind === "key"); + const leading = rows.slice(firstKey, firstKey + sponsorIds.length); + expect(leading.map(row => row.id).sort()).toEqual([...sponsorIds].sort()); + const labels = leading.map(row => row.label); + expect(labels).toEqual([...labels].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }))); + // Same rows; non-sponsor rows keep their order, so the heading sequence is unchanged. + expect(rows.map(row => row.id).sort()).toEqual(buildInitProviders().map(row => row.id).sort()); + const rest = (list: { id: string }[]) => list.map(row => row.id).filter(id => !sponsorIds.includes(id)); + expect(rest(rows)).toEqual(rest(buildInitProviders())); + const kinds = (list: { kind: string }[]) => list.map(row => row.kind).filter((kind, i, all) => kind !== all[i - 1]); + expect(kinds(rows)).toEqual(kinds(buildInitProviders())); + }); + + test("pinSponsorRows orders Main before Standard and keeps the rest in caller order", () => { + const rows = [ + { id: "a", label: "Zed" }, { id: "b", label: "beta", tier: "standard" as const }, + { id: "c", label: "Mid" }, { id: "d", label: "Alpha", tier: "standard" as const }, + { id: "e", label: "Omega", tier: "main" as const }, + ]; + const pinned = pinSponsorRows(rows, row => row.tier, row => row.label).map(row => row.id); + expect(pinned).toEqual(["e", "d", "b", "a", "c"]); + }); + + test("a run that opens with a sponsor keeps its place in the kind sequence", () => { + type Row = { id: string; label: string; kind: string; tier?: "main" | "standard" }; + const rows: Row[] = [ + { id: "s1", label: "Sponsor B", kind: "key", tier: "standard" }, + { id: "o1", label: "OAuth", kind: "oauth" }, + { id: "k1", label: "Key", kind: "key" }, + { id: "s2", label: "Sponsor A", kind: "key", tier: "standard" }, + { id: "s3", label: "Only", kind: "local", tier: "main" }, + ]; + const pinned = pinSponsorsWithinKind(rows, row => row.tier).map(row => row.id); + expect(pinned).toEqual(["s2", "s1", "o1", "k1", "s3"]); + }); }); diff --git a/tests/providers/tokenlab-provider.test.ts b/tests/providers/tokenlab-provider.test.ts index b6ec09b387..1d6ef8b76e 100644 --- a/tests/providers/tokenlab-provider.test.ts +++ b/tests/providers/tokenlab-provider.test.ts @@ -35,7 +35,9 @@ describe("TokenLab provider", () => { }); 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(preset).toMatchObject({ + sponsor: "standard", sponsorUrl: "https://tokenlab.sh/r/OPENCODEX", + }); expect(KEY_LOGIN_PROVIDERS.tokenlab?.defaultModel).toBe("gpt-5.6-terra"); const provider = config().providers.tokenlab!; expect(provider).not.toHaveProperty("modelDiscovery"); From 14ccfe1a1ad1a1f45d5877385fbf89fffba4d017 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 30 Sep 2026 01:26:43 +0900 Subject: [PATCH 5/5] chore(release): 2.72.0-preview.20260930 --- desktop/src-tauri/Cargo.lock | 2 +- desktop/src-tauri/Cargo.toml | 2 +- desktop/src-tauri/tauri.conf.json | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 8d650cfc89..4f41c651d4 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -2645,7 +2645,7 @@ dependencies = [ [[package]] name = "opencodex-desktop" -version = "2.72.0" +version = "2.72.0-preview.20260930" dependencies = [ "base64 0.22.1", "dbus", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 0b995e022c..7c3f8d6de3 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "opencodex-desktop" -version = "2.72.0" +version = "2.72.0-preview.20260930" description = "OpenCodex desktop shell" authors = ["OpenCodex contributors"] license = "MIT" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index c3058739b7..32516fe8f7 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenCodex", - "version": "2.72.0", + "version": "2.72.0-preview.20260930", "identifier": "com.opencodex.desktop", "build": { "frontendDist": "../ui", diff --git a/package.json b/package.json index ab92796acf..76f4ecdc03 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.72.0", + "version": "2.72.0-preview.20260930", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs",