From 63301e50a7f47acb0472625544f39d7500848d12 Mon Sep 17 00:00:00 2001 From: theQuert Date: Mon, 3 Aug 2026 15:14:44 +0800 Subject: [PATCH 1/3] fix(providers): keep Apertis reference-only --- .../.openspec.yaml | 2 + .../apertis-reference-directory/README.md | 3 + .../apertis-reference-directory/design.md | 40 ++++++++++++ .../apertis-reference-directory/proposal.md | 31 +++++++++ .../provider-reference-directory/spec.md | 28 ++++++++ .../apertis-reference-directory/tasks.md | 14 ++++ openspec/config.yaml | 20 ++++++ src/providers/free-directory.ts | 6 ++ tests/apertis-provider.test.ts | 64 +++++++++++++++++++ 9 files changed, 208 insertions(+) create mode 100644 openspec/changes/apertis-reference-directory/.openspec.yaml create mode 100644 openspec/changes/apertis-reference-directory/README.md create mode 100644 openspec/changes/apertis-reference-directory/design.md create mode 100644 openspec/changes/apertis-reference-directory/proposal.md create mode 100644 openspec/changes/apertis-reference-directory/specs/provider-reference-directory/spec.md create mode 100644 openspec/changes/apertis-reference-directory/tasks.md create mode 100644 openspec/config.yaml create mode 100644 tests/apertis-provider.test.ts diff --git a/openspec/changes/apertis-reference-directory/.openspec.yaml b/openspec/changes/apertis-reference-directory/.openspec.yaml new file mode 100644 index 00000000000..e08b5f89a2a --- /dev/null +++ b/openspec/changes/apertis-reference-directory/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-03 diff --git a/openspec/changes/apertis-reference-directory/README.md b/openspec/changes/apertis-reference-directory/README.md new file mode 100644 index 00000000000..868c5435d7e --- /dev/null +++ b/openspec/changes/apertis-reference-directory/README.md @@ -0,0 +1,3 @@ +# apertis-reference-directory + +Move Apertis from an unverified canonical key-provider preset to an inert reference-only directory entry. diff --git a/openspec/changes/apertis-reference-directory/design.md b/openspec/changes/apertis-reference-directory/design.md new file mode 100644 index 00000000000..bf2232038d4 --- /dev/null +++ b/openspec/changes/apertis-reference-directory/design.md @@ -0,0 +1,40 @@ +## Context + +Canonical registry entries seed provider configuration, expose API-key setup, and can enrich a +same-named saved provider at route time. The available evidence does not authorize an Apertis +canonical integration, so retaining an adapter, base URL, models endpoint, or pricing category is +unsafe. The repository already has a directory isolation boundary for reference entries. + +## Goals / Non-Goals + +**Goals:** + +- Keep Apertis discoverable only as an explicitly unverified reference row. +- Guarantee that the row cannot create a preset, validate a key, discover models, or select an + upstream destination. +- Keep a manually configured `apertis` provider fully editable and routed to its own settings. + +**Non-Goals:** + +- Verify Apertis documentation, prices, entitlement terms, API compatibility, or routing/resale + authorization. +- Add an Apertis icon, GUI preset, key validator, model list, endpoint, or user setup guide. + +## Decisions + +- Add a dedicated `reference-only` access group instead of assigning an existing free/credit + category. This avoids an unsupported claim that the service has a free entitlement. +- Use the free-directory generator's default reference shape: empty base URL, unsupported + discovery, `liveModels: false`, `verification: "unverified"`, and no verification date. This + keeps the entry structurally inert without duplicating an integration definition. +- Do not add an Apertis entry to the canonical registry or GUI catalog-name map. Registry absence + preserves a same-named custom provider's adapter and destination. + +## Risks / Trade-offs + +- [The `free-directory` filename can be read too broadly] → the distinct `reference-only` group + and no-entitlement test make the classification explicit. +- [A future maintainer may promote the row without new evidence] → promotion requires a separate + change with routing/resale authorization and a canonical integration test suite. +- [Users cannot one-click configure Apertis] → this is intentional until the required evidence is + available; manual custom-provider configuration remains unchanged. diff --git a/openspec/changes/apertis-reference-directory/proposal.md b/openspec/changes/apertis-reference-directory/proposal.md new file mode 100644 index 00000000000..4c23dc43344 --- /dev/null +++ b/openspec/changes/apertis-reference-directory/proposal.md @@ -0,0 +1,31 @@ +## Why + +The prior Apertis proposal registered an API-key preset and fixed upstream destination without +maintainer-visible evidence of routing/resale authorization. That would make a user-supplied key +and request flow through an unverified canonical integration. + +## What Changes + +- Remove the canonical Apertis provider preset and all derived key-login, CLI, dashboard, and + model-discovery behavior. +- Add an inert `reference-only` directory row for Apertis that makes no pricing, entitlement, + endpoint, documentation, or verification claim. +- Preserve a manually configured provider named `apertis` as an ordinary custom provider. + +## Capabilities + +### New Capabilities + +- `provider-reference-directory`: represent unverified providers without registering a runtime + route or claiming an access entitlement. + +### Modified Capabilities + +- None. + +## Impact + +- `src/providers/free-directory.ts` gains the reference-only directory category. +- Provider registry derivation, GUI preset classification, and request routing must not recognize + Apertis as a catalog provider. +- Focused Bun tests replace the former canonical-provider behavior tests. diff --git a/openspec/changes/apertis-reference-directory/specs/provider-reference-directory/spec.md b/openspec/changes/apertis-reference-directory/specs/provider-reference-directory/spec.md new file mode 100644 index 00000000000..57f8dc0a3f0 --- /dev/null +++ b/openspec/changes/apertis-reference-directory/specs/provider-reference-directory/spec.md @@ -0,0 +1,28 @@ +## ADDED Requirements + +### Requirement: Unverified providers remain inert reference entries +The provider directory SHALL represent a provider lacking integration authorization in the +`reference-only` access group. Such an entry MUST use the reference support level and unverified +verification state, MUST NOT claim an access entitlement or verification date, and MUST NOT define +a usable upstream endpoint or live-model discovery. + +#### Scenario: Apertis is listed without an integration claim +- **WHEN** consumers inspect the Apertis directory row +- **THEN** it is `reference-only`, unverified, endpoint-free, and has unsupported discovery + +### Requirement: Reference entries do not become canonical providers +A `reference-only` directory entry SHALL NOT appear in the canonical provider registry or any +derived key-login, initialization, or dashboard preset list. It SHALL NOT be classified as a GUI +catalog provider. + +#### Scenario: Apertis is absent from canonical provider surfaces +- **WHEN** canonical provider surfaces are derived +- **THEN** no Apertis runtime preset, key validator, or catalog-provider classification exists + +### Requirement: Same-named custom providers retain their destination +Routing a manually configured provider whose id matches a `reference-only` directory row SHALL use +the configuration's own adapter and base URL without registry enrichment or redirection. + +#### Scenario: Custom Apertis configuration routes unchanged +- **WHEN** a user configures `apertis` with a custom adapter and base URL +- **THEN** a request for `apertis/` retains that adapter, base URL, and model id diff --git a/openspec/changes/apertis-reference-directory/tasks.md b/openspec/changes/apertis-reference-directory/tasks.md new file mode 100644 index 00000000000..3837e7f2b8b --- /dev/null +++ b/openspec/changes/apertis-reference-directory/tasks.md @@ -0,0 +1,14 @@ +## 1. Reference-only contract + +- [x] 1.1 Add the explicit no-entitlement access group and Apertis reference row. +- [x] 1.2 Ensure no canonical Apertis registry or GUI-preset metadata remains. + +## 2. Regression proof + +- [x] 2.1 Add focused tests for inert directory metadata and the absence of canonical derivations. +- [x] 2.2 Add a same-named custom-routing regression test. + +## 3. Verification + +- [x] 3.1 Run strict OpenSpec validation and focused provider-directory tests. +- [ ] 3.2 Run repository typecheck, full test suite, GUI lint/build, privacy scan, and diff checks. diff --git a/openspec/config.yaml b/openspec/config.yaml new file mode 100644 index 00000000000..392946c67c0 --- /dev/null +++ b/openspec/config.yaml @@ -0,0 +1,20 @@ +schema: spec-driven + +# Project context (optional) +# This is shown to AI when creating artifacts. +# Add your tech stack, conventions, style guides, domain knowledge, etc. +# Example: +# context: | +# Tech stack: TypeScript, React, Node.js +# We use conventional commits +# Domain: e-commerce platform + +# Per-artifact rules (optional) +# Add custom rules for specific artifacts. +# Example: +# rules: +# proposal: +# - Keep proposals under 500 words +# - Always include a "Non-goals" section +# tasks: +# - Break tasks into chunks of max 2 hours diff --git a/src/providers/free-directory.ts b/src/providers/free-directory.ts index 9d96063dd6f..49522a2df65 100644 --- a/src/providers/free-directory.ts +++ b/src/providers/free-directory.ts @@ -1,10 +1,15 @@ export type ProviderAccessGroup = + | "reference-only" | "recurring-or-keyless" | "recurring-uncapped" | "recurring-credit" | "signup-credit"; export const FREE_PROVIDER_ACCESS_GROUPS = { + // This group intentionally makes no free, credit, endpoint, or integration claim. It is for + // provider names that remain useful as reference entries but lack the evidence required for a + // canonical runtime preset. + "reference-only": ["apertis"], "recurring-or-keyless": [ "agy", "aihorde", "api-airforce", "arcee-ai", "bazaarlink", "blackbox", "bluesminds", "cerebras", "cloudflare-ai", "cohere", "coze", "duckduckgo-web", "felo-web", "friendliai", "gemini", "github-models", @@ -145,6 +150,7 @@ const CONNECTABLE: Record = { }; const LABELS: Record = { + apertis: "Apertis", agy: "AGY", aihorde: "AI Horde", "api-airforce": "API Airforce", "arcee-ai": "Arcee AI", bazaarlink: "BazaarLink", blackbox: "Blackbox", bluesminds: "Bluesminds", "cloudflare-ai": "Cloudflare AI", coze: "Coze", "duckduckgo-web": "DuckDuckGo Web", "felo-web": "Felo Web", friendliai: "FriendliAI", diff --git a/tests/apertis-provider.test.ts b/tests/apertis-provider.test.ts new file mode 100644 index 00000000000..eee54e861e0 --- /dev/null +++ b/tests/apertis-provider.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, test } from "bun:test"; +import { buildInitProviders } from "../src/cli/init"; +import { deriveProviderPresets } from "../src/providers/derive"; +import { FREE_PROVIDER_DIRECTORY } from "../src/providers/free-directory"; +import { PROVIDER_REGISTRY } from "../src/providers/registry"; +import { routeModel } from "../src/router"; +import type { OcxConfig } from "../src/types"; +import { formatProviderDisplayName, isCatalogProviderId } from "../gui/src/provider-icons"; +import type { TFn } from "../gui/src/i18n/shared"; + +const identityT: TFn = key => key; + +describe("Apertis reference directory entry", () => { + test("is inert and makes no integration or entitlement claim", () => { + const entry = FREE_PROVIDER_DIRECTORY.find(row => row.id === "apertis"); + + expect(entry).toMatchObject({ + id: "apertis", + label: "Apertis", + adapter: "openai-chat", + baseUrl: "", + authKind: "key", + accessGroups: ["reference-only"], + supportLevel: "reference", + verification: "unverified", + discovery: "unsupported", + liveModels: false, + }); + expect(entry).not.toHaveProperty("lastVerified"); + expect(entry).not.toHaveProperty("documentationUrl"); + expect(entry).not.toHaveProperty("dashboardUrl"); + }); + + test("does not derive a canonical provider or GUI preset", () => { + expect(PROVIDER_REGISTRY.some(entry => entry.id === "apertis")).toBe(false); + expect(buildInitProviders().some(entry => entry.id === "apertis")).toBe(false); + expect(deriveProviderPresets().some(entry => entry.id === "apertis")).toBe(false); + expect(isCatalogProviderId("apertis")).toBe(false); + expect(formatProviderDisplayName("apertis", identityT)).toBe("Apertis"); + }); + + test("keeps a same-named custom provider's adapter and destination", () => { + const config: OcxConfig = { + port: 10100, + defaultProvider: "apertis", + providers: { + apertis: { + adapter: "anthropic", + baseUrl: "https://custom.example.test/anthropic", + apiKey: "test-key", + liveModels: true, + }, + }, + }; + + const routed = routeModel(config, "apertis/custom-model"); + expect(routed.provider).toMatchObject({ + adapter: "anthropic", + baseUrl: "https://custom.example.test/anthropic", + liveModels: true, + }); + expect(routed.modelId).toBe("custom-model"); + }); +}); From 477609159660aabcf003542f2142e036a41c92e2 Mon Sep 17 00:00:00 2001 From: theQuert Date: Mon, 3 Aug 2026 15:21:00 +0800 Subject: [PATCH 2/3] fix(gui): run lint through Bun --- gui/package.json | 4 ++-- .../changes/apertis-reference-directory/design.md | 5 +++++ .../changes/apertis-reference-directory/proposal.md | 5 +++++ .../specs/gui-lint-runtime/spec.md | 13 +++++++++++++ .../changes/apertis-reference-directory/tasks.md | 3 ++- 5 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 openspec/changes/apertis-reference-directory/specs/gui-lint-runtime/spec.md diff --git a/gui/package.json b/gui/package.json index 1556402dc37..d24ec7b5d7e 100644 --- a/gui/package.json +++ b/gui/package.json @@ -6,9 +6,9 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "lint": "eslint .", + "lint": "bun --bun eslint .", "test": "bun test tests", - "lint:i18n": "eslint src/pages src/components src/App.tsx src/ui.tsx", + "lint:i18n": "bun --bun eslint src/pages src/components src/App.tsx src/ui.tsx", "doctor": "npx --yes react-doctor@0.9.3 --verbose --scope changed --base origin/main --no-telemetry", "doctor:full": "npx --yes react-doctor@0.9.3 --verbose --scope full --no-telemetry", "preview": "vite preview" diff --git a/openspec/changes/apertis-reference-directory/design.md b/openspec/changes/apertis-reference-directory/design.md index bf2232038d4..364d089690d 100644 --- a/openspec/changes/apertis-reference-directory/design.md +++ b/openspec/changes/apertis-reference-directory/design.md @@ -29,6 +29,9 @@ unsafe. The repository already has a directory isolation boundary for reference keeps the entry structurally inert without duplicating an integration definition. - Do not add an Apertis entry to the canonical registry or GUI catalog-name map. Registry absence preserves a same-named custom provider's adapter and destination. +- Invoke the ESLint package through `bun --bun` in the GUI scripts. The existing configuration + imports local `.ts` modules, which direct Node execution rejects before linting; Bun preserves + ESLint's package resolution while loading those modules. ## Risks / Trade-offs @@ -38,3 +41,5 @@ unsafe. The repository already has a directory isolation boundary for reference change with routing/resale authorization and a canonical integration test suite. - [Users cannot one-click configure Apertis] → this is intentional until the required evidence is available; manual custom-provider configuration remains unchanged. +- [The lint command could be run by a Node-only launcher] → the documented package scripts now + force Bun, while direct external Node invocations remain outside this repository contract. diff --git a/openspec/changes/apertis-reference-directory/proposal.md b/openspec/changes/apertis-reference-directory/proposal.md index 4c23dc43344..574d8fa1e9f 100644 --- a/openspec/changes/apertis-reference-directory/proposal.md +++ b/openspec/changes/apertis-reference-directory/proposal.md @@ -11,6 +11,8 @@ and request flow through an unverified canonical integration. - Add an inert `reference-only` directory row for Apertis that makes no pricing, entitlement, endpoint, documentation, or verification claim. - Preserve a manually configured provider named `apertis` as an ordinary custom provider. +- Run GUI ESLint commands under Bun so the repository's TypeScript local lint plugin loads during + the documented validation and pre-push paths. ## Capabilities @@ -18,6 +20,8 @@ and request flow through an unverified canonical integration. - `provider-reference-directory`: represent unverified providers without registering a runtime route or claiming an access entitlement. +- `gui-lint-runtime`: execute GUI lint commands with a runtime that supports the repository's + TypeScript ESLint configuration and local plugins. ### Modified Capabilities @@ -29,3 +33,4 @@ and request flow through an unverified canonical integration. - Provider registry derivation, GUI preset classification, and request routing must not recognize Apertis as a catalog provider. - Focused Bun tests replace the former canonical-provider behavior tests. +- `gui/package.json` lint commands use Bun's runtime bridge instead of a Node-only ESLint launch. diff --git a/openspec/changes/apertis-reference-directory/specs/gui-lint-runtime/spec.md b/openspec/changes/apertis-reference-directory/specs/gui-lint-runtime/spec.md new file mode 100644 index 00000000000..91be50f629c --- /dev/null +++ b/openspec/changes/apertis-reference-directory/specs/gui-lint-runtime/spec.md @@ -0,0 +1,13 @@ +## ADDED Requirements + +### Requirement: GUI lint scripts load the local TypeScript ESLint plugin +The GUI package's `lint` and `lint:i18n` scripts SHALL execute ESLint through Bun's runtime bridge +so that the checked-in ESLint configuration and its local TypeScript plugin load successfully. + +#### Scenario: Full GUI lint command +- **WHEN** a contributor runs the GUI `lint` script from the repository validation path +- **THEN** ESLint evaluates the configured GUI files without an unknown-TypeScript-extension error + +#### Scenario: Focused GUI i18n lint command +- **WHEN** a contributor runs the GUI `lint:i18n` script +- **THEN** ESLint evaluates the configured UI paths with the same local plugin available diff --git a/openspec/changes/apertis-reference-directory/tasks.md b/openspec/changes/apertis-reference-directory/tasks.md index 3837e7f2b8b..7b73dbafd15 100644 --- a/openspec/changes/apertis-reference-directory/tasks.md +++ b/openspec/changes/apertis-reference-directory/tasks.md @@ -2,6 +2,7 @@ - [x] 1.1 Add the explicit no-entitlement access group and Apertis reference row. - [x] 1.2 Ensure no canonical Apertis registry or GUI-preset metadata remains. +- [x] 1.3 Make GUI lint scripts load the local TypeScript ESLint plugin under Bun. ## 2. Regression proof @@ -11,4 +12,4 @@ ## 3. Verification - [x] 3.1 Run strict OpenSpec validation and focused provider-directory tests. -- [ ] 3.2 Run repository typecheck, full test suite, GUI lint/build, privacy scan, and diff checks. +- [x] 3.2 Run repository typecheck, full test suite, GUI lint/build, privacy scan, and diff checks. From a1dcde8cb70b3d23545900e4c93cd2987c98b1e5 Mon Sep 17 00:00:00 2001 From: theQuert Date: Tue, 4 Aug 2026 14:33:28 +0800 Subject: [PATCH 3/3] feat(providers): restore Apertis canonical provider --- .../src/content/docs/guides/providers.md | 9 +- .../src/content/docs/ja/guides/providers.md | 3 +- .../src/content/docs/ko/guides/providers.md | 3 +- .../src/content/docs/ru/guides/providers.md | 3 +- .../content/docs/zh-cn/guides/providers.md | 3 +- gui/package.json | 4 +- gui/src/provider-icons.ts | 1 + .../apertis-reference-directory/README.md | 3 +- .../apertis-reference-directory/design.md | 97 ++++--- .../apertis-reference-directory/proposal.md | 50 ++-- .../specs/gui-lint-runtime/spec.md | 15 ++ .../provider-canonical-integration/spec.md | 57 +++++ .../provider-reference-directory/spec.md | 28 -- .../apertis-reference-directory/tasks.md | 30 ++- src/providers/free-directory.ts | 6 - src/providers/registry.ts | 14 + tests/apertis-provider.test.ts | 239 +++++++++++++++--- tests/fixtures/apertis-models.json | 21 ++ tests/provider-registry-parity.test.ts | 2 +- 19 files changed, 445 insertions(+), 143 deletions(-) create mode 100644 openspec/changes/apertis-reference-directory/specs/provider-canonical-integration/spec.md delete mode 100644 openspec/changes/apertis-reference-directory/specs/provider-reference-directory/spec.md create mode 100644 tests/fixtures/apertis-models.json diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index b4a9573aec3..fd372abf865 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -216,7 +216,7 @@ selectors, then retry. Signing in from a machine with no existing `kiro-cli` ses ## 3. API-key catalog -opencodex ships 69 built-in presets: 58 key-based, seven OAuth, three local, and one default +opencodex ships 70 built-in presets: 59 key-based, seven 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, and Command Code's public catalog reports keys as unverifiable. Notable entries: @@ -249,6 +249,7 @@ free-experimentation model. | MiniMax · MiniMax (CN) | `https://api.minimax.io/v1` · `https://api.minimaxi.com/v1` | | DeepSeek | `https://api.deepseek.com` | | Cerebras | `https://api.cerebras.ai/v1` | +| Apertis | `https://api.apertis.ai/v1` | | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | | Baseten Model APIs | `https://inference.baseten.co/v1` | @@ -270,6 +271,12 @@ free-experimentation model. | 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 | +**Apertis discovery.** Apertis exposes an OpenAI-compatible API at +[`https://api.apertis.ai/v1`](https://docs.apertis.ai/api/), uses Bearer API keys, and returns a +key/plan-scoped live catalog from [`GET /v1/models`](https://docs.apertis.ai/api/utilities/models/). +Do not freeze a model id from the catalog; availability can vary by key type and plan. Apertis's +public site identifies the operator as STIMA AI LLC and publishes its [Terms of Service](https://apertis.ai/terms). + Most use the `openai-chat` adapter with a bearer key; a few that expose only an Anthropic-compatible endpoint (e.g. **Xiaomi MiMo**) use the `anthropic` adapter (`x-api-key`). Volcengine Agent Plan uses its native Responses endpoint through `openai-responses`. diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md index 48f04235c9f..4f792700164 100644 --- a/docs-site/src/content/docs/ja/guides/providers.md +++ b/docs-site/src/content/docs/ja/guides/providers.md @@ -143,7 +143,7 @@ Kiro のログインには Kiro CLI が必要です。Unix では `curl -fsSL ht ## 3. API キーカタログ -opencodex には組み込みプリセットが 69 個含まれています。キー方式 58、OAuth 7、ローカル 3、 +opencodex には組み込みプリセットが 70 個含まれています。キー方式 59、OAuth 7、ローカル 3、 デフォルト ChatGPT 転送プリセット 1 です。ダッシュボードの **Add provider** ピッカーはキー発行ページを開き、 入力したキーを検証した後保存します(検証はプロバイダー固有で、Command Code の公開カタログはキーを 検証不能として報告します)。主な項目は以下のとおりです: @@ -176,6 +176,7 @@ Cline IDE/CLI のみで API からは使えません。`minimax/minimax-m2.5` | MiniMax · MiniMax (CN) | `https://api.minimax.io/v1` · `https://api.minimaxi.com/v1` | | DeepSeek | `https://api.deepseek.com` | | Cerebras | `https://api.cerebras.ai/v1` | +| Apertis | `https://api.apertis.ai/v1` | | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | | Baseten Model APIs | `https://inference.baseten.co/v1` | diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md index ae29734994e..a8f0204bc36 100644 --- a/docs-site/src/content/docs/ko/guides/providers.md +++ b/docs-site/src/content/docs/ko/guides/providers.md @@ -142,7 +142,7 @@ Kiro 로그인에는 Kiro CLI가 필요합니다. Unix에서는 `curl -fsSL http ## 3. API 키 카탈로그 -opencodex에는 빌트인 프리셋이 69개 들어 있습니다. 키 방식 58개, OAuth 7개, 로컬 3개, +opencodex에는 빌트인 프리셋이 70개 들어 있습니다. 키 방식 59개, OAuth 7개, 로컬 3개, 기본 ChatGPT 포워드 프리셋 1개입니다. 대시보드의 **Add provider** 선택기는 키 발급 페이지를 열고, 입력한 키를 검증한 뒤 저장합니다(검증은 프로바이더별로 다르며, Command Code의 공개 카탈로그는 키를 검증 불가로 보고합니다). 주요 항목은 다음과 같습니다: @@ -176,6 +176,7 @@ Cline IDE/CLI에서만 제공되며 API로는 사용할 수 없습니다. `minim | MiniMax · MiniMax (CN) | `https://api.minimax.io/v1` · `https://api.minimaxi.com/v1` | | DeepSeek | `https://api.deepseek.com` | | Cerebras | `https://api.cerebras.ai/v1` | +| Apertis | `https://api.apertis.ai/v1` | | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | | Baseten Model APIs | `https://inference.baseten.co/v1` | diff --git a/docs-site/src/content/docs/ru/guides/providers.md b/docs-site/src/content/docs/ru/guides/providers.md index 4f912949a6e..ee923c3e83d 100644 --- a/docs-site/src/content/docs/ru/guides/providers.md +++ b/docs-site/src/content/docs/ru/guides/providers.md @@ -153,7 +153,7 @@ OAuth-провайдеры, чьи учётные данные содержат ## 3. Каталог API-ключей -opencodex поставляется с 69 встроенными пресетами: 58 на основе ключей, семь OAuth, три локальных и +opencodex поставляется с 70 встроенными пресетами: 59 на основе ключей, семь OAuth, три локальных и один пресет ChatGPT-форварда по умолчанию. Селектор **Add provider** в дашборде открывает страницу выдачи ключей провайдера, проверяет ключ и сохраняет его; проверка зависит от провайдера, а публичный каталог Command Code сообщает ключ как непроверенный. Наиболее заметные записи: @@ -186,6 +186,7 @@ opencodex поставляется с 69 встроенными пресетам | MiniMax · MiniMax (CN) | `https://api.minimax.io/v1` · `https://api.minimaxi.com/v1` | | DeepSeek | `https://api.deepseek.com` | | Cerebras | `https://api.cerebras.ai/v1` | +| Apertis | `https://api.apertis.ai/v1` | | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | | Baseten Model APIs | `https://inference.baseten.co/v1` | 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 e8ce7f5d105..1a5d6887513 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -131,7 +131,7 @@ Kiro 登录需要 Kiro CLI:Unix 使用 `curl -fsSL https://cli.kiro.dev/instal ## 3. API 密钥目录 -opencodex 内置 69 个预设:58 个密钥预设、7 个 OAuth 预设、3 个本地预设,以及 1 个默认的 +opencodex 内置 70 个预设:59 个密钥预设、7 个 OAuth 预设、3 个本地预设,以及 1 个默认的 ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提供商的控制台,验证并保存密钥。 验证因提供商而异,Command Code 的公开目录会将密钥报告为无法验证。主要条目包括: @@ -163,6 +163,7 @@ Cline IDE/CLI 中提供,不能通过 API 使用;`minimax/minimax-m2.5` 是 | MiniMax · MiniMax (CN) | `https://api.minimax.io/v1` · `https://api.minimaxi.com/v1` | | DeepSeek | `https://api.deepseek.com` | | Cerebras | `https://api.cerebras.ai/v1` | +| Apertis | `https://api.apertis.ai/v1` | | DeepInfra | `https://api.deepinfra.com/v1/openai` | | Hyperbolic | `https://api.hyperbolic.xyz/v1` | | Baseten Model APIs | `https://inference.baseten.co/v1` | diff --git a/gui/package.json b/gui/package.json index d24ec7b5d7e..0139de1563a 100644 --- a/gui/package.json +++ b/gui/package.json @@ -9,8 +9,8 @@ "lint": "bun --bun eslint .", "test": "bun test tests", "lint:i18n": "bun --bun eslint src/pages src/components src/App.tsx src/ui.tsx", - "doctor": "npx --yes react-doctor@0.9.3 --verbose --scope changed --base origin/main --no-telemetry", - "doctor:full": "npx --yes react-doctor@0.9.3 --verbose --scope full --no-telemetry", + "doctor": "npm exec --yes --package=typescript@6.0.2 --package=react-doctor@0.9.3 -- react-doctor --verbose --scope changed --base origin/main --no-telemetry", + "doctor:full": "npm exec --yes --package=typescript@6.0.2 --package=react-doctor@0.9.3 -- react-doctor --verbose --scope full --no-telemetry", "preview": "vite preview" }, "dependencies": { diff --git a/gui/src/provider-icons.ts b/gui/src/provider-icons.ts index c0e1d0677cf..3c33ff52fb8 100644 --- a/gui/src/provider-icons.ts +++ b/gui/src/provider-icons.ts @@ -71,6 +71,7 @@ const PROVIDER_DISPLAY_NAMES: Record = { xiaomi: "Xiaomi", cursor: "Cursor", deepseek: "DeepSeek", + apertis: "Apertis", github: "GitHub", "github-copilot": "GitHub Copilot", "gitlab-duo": "GitLab Duo", diff --git a/openspec/changes/apertis-reference-directory/README.md b/openspec/changes/apertis-reference-directory/README.md index 868c5435d7e..3d7dd8d9436 100644 --- a/openspec/changes/apertis-reference-directory/README.md +++ b/openspec/changes/apertis-reference-directory/README.md @@ -1,3 +1,4 @@ # apertis-reference-directory -Move Apertis from an unverified canonical key-provider preset to an inert reference-only directory entry. +Restore Apertis as a canonical OpenAI-compatible API-key provider with authenticated live model +discovery, while keeping the maintainer evidence and security-review boundary explicit. diff --git a/openspec/changes/apertis-reference-directory/design.md b/openspec/changes/apertis-reference-directory/design.md index 364d089690d..5bf7e5a39f5 100644 --- a/openspec/changes/apertis-reference-directory/design.md +++ b/openspec/changes/apertis-reference-directory/design.md @@ -1,45 +1,84 @@ ## Context -Canonical registry entries seed provider configuration, expose API-key setup, and can enrich a -same-named saved provider at route time. The available evidence does not authorize an Apertis -canonical integration, so retaining an adapter, base URL, models endpoint, or pricing category is -unsafe. The repository already has a directory isolation boundary for reference entries. +Canonical registry entries seed provider configuration, expose API-key setup, enrich saved +providers, and feed routing and the shared Codex catalog. Apertis's official API documentation +currently describes the OpenAI-compatible base URL `https://api.apertis.ai/v1`, Bearer +authentication, and an authenticated `/v1/models` endpoint whose result depends on the API key +type/plan. Apertis's public website identifies STIMA AI LLC as the operator and describes +multi-provider routing. + +The upstream repository requires additional private routing/resale authorization and maintainer +security review before a credential-destination preset can merge. This change therefore implements +the requested canonical behavior and records that external gate honestly; it does not infer private +authorization from public product or API documentation. ## Goals / Non-Goals **Goals:** -- Keep Apertis discoverable only as an explicitly unverified reference row. -- Guarantee that the row cannot create a preset, validate a key, discover models, or select an - upstream destination. -- Keep a manually configured `apertis` provider fully editable and routed to its own settings. +- Register Apertis as a canonical `authKind: "key"`, `adapter: "openai-chat"` provider. +- Seed the documented fixed base URL and dashboard URL into CLI/key-login/dashboard flows. +- Validate keys and discover model ids through the authenticated OpenAI-shaped `/models` path. +- Route chat-completion requests to the canonical host while preserving same-named custom + providers' own adapter, destination, and credentials. +- Keep model availability live and key-scoped rather than encoding a speculative static catalog. +- Keep public evidence links and the private maintainer authorization gate separate. **Non-Goals:** -- Verify Apertis documentation, prices, entitlement terms, API compatibility, or routing/resale - authorization. -- Add an Apertis icon, GUI preset, key validator, model list, endpoint, or user setup guide. +- Add a new adapter, Responses wire, image/audio integration, or provider-specific request + semantics beyond the existing OpenAI Chat adapter. +- Freeze model ids, context windows, prices, or a default model from the mutable live catalog. +- Claim that public endpoint/terms/legal-entity evidence proves private routing/resale authorization. +- Send credentials, post maintainer messages, push the branch, merge, or deploy as part of local + implementation. ## Decisions -- Add a dedicated `reference-only` access group instead of assigning an existing free/credit - category. This avoids an unsupported claim that the service has a free entitlement. -- Use the free-directory generator's default reference shape: empty base URL, unsupported - discovery, `liveModels: false`, `verification: "unverified"`, and no verification date. This - keeps the entry structurally inert without duplicating an integration definition. -- Do not add an Apertis entry to the canonical registry or GUI catalog-name map. Registry absence - preserves a same-named custom provider's adapter and destination. -- Invoke the ESLint package through `bun --bun` in the GUI scripts. The existing configuration - imports local `.ts` modules, which direct Node execution rejects before linting; Bun preserves - ESLint's package resolution while loading those modules. +- **Extend the existing registry-derived provider flow.** This keeps CLI init, key-login, + dashboard presets, routing, and catalog derivation on one source of truth instead of adding an + Apertis-specific path. +- **Use live discovery with the default `/models` resolution.** The configured base URL already + ends in `/v1`, so the existing discovery and key-validation flow produces `/v1/models`, sends a + Bearer key, and applies the repository's bounded response/row guards. A registry-specific + discovery policy is unnecessary for the documented endpoint. +- **Set `preserveCustomDestination: true`.** A saved provider named `apertis` with a custom + adapter/base URL must not be silently retargeted when the canonical preset is introduced. +- **Do not add static models or a default model.** Official documentation says availability varies + by key type and plan; live discovery is the faithful contract and avoids stale claims. +- **Use the existing GUI display-name classification without inventing an icon asset.** The provider + remains discoverable and correctly labeled while the default unknown-provider icon fallback stays + intact. +- **Keep the evidence gate outside runtime behavior.** Public API/terms/legal-entity links are + recorded in source/docs; private routing/resale authorization remains a maintainer review input. +- **Keep GUI validation runtimes explicit.** ESLint continues to use Bun's runtime bridge, while + React Doctor uses npm exec with pinned TypeScript and React Doctor packages so Bun cannot omit + React Doctor's TypeScript peer. ## Risks / Trade-offs -- [The `free-directory` filename can be read too broadly] → the distinct `reference-only` group - and no-entitlement test make the classification explicit. -- [A future maintainer may promote the row without new evidence] → promotion requires a separate - change with routing/resale authorization and a canonical integration test suite. -- [Users cannot one-click configure Apertis] → this is intentional until the required evidence is - available; manual custom-provider configuration remains unchanged. -- [The lint command could be run by a Node-only launcher] → the documented package scripts now - force Bun, while direct external Node invocations remain outside this repository contract. +- [A user key and traffic reach a multi-provider gateway] → fixed-host routing, explicit public + provenance, `preserveCustomDestination`, and maintainer/security review are required before merge. +- [The live catalog changes by key/plan] → no static model list/default is claimed; discovery is + bounded and fixture-tested. +- [A same-named custom provider could be disabled in GUI classification] → the canonical entry's + destination-preservation contract and regression test keep custom adapter/base URL controls intact. +- [Public docs may be mistaken for private authorization] → the change records public evidence and + the unresolved routing/resale gate separately. +- [A GUI validation command could lose its local TypeScript peer under Bun] → ESLint scripts use + Bun's runtime bridge and React Doctor scripts use npm exec with explicit TypeScript and React + Doctor packages; direct external invocations remain outside this repository contract. + +## Migration Plan + +Fresh Apertis setup derives the canonical fixed destination. Existing saved `apertis` providers +retain their configured adapter, base URL, and key when they do not match the canonical transport. +Rolling back removes the preset surfaces but does not rewrite or delete existing provider config; +explicit custom routes remain user-owned. + +## Open Questions + +- Which maintainer-designated private channel will record the routing/resale authorization, named + maintenance owner, and verification date required for merge? +- After the current head is reviewed, does the maintainer require any provider-specific capability + exclusions beyond the existing `openai-chat` contract? diff --git a/openspec/changes/apertis-reference-directory/proposal.md b/openspec/changes/apertis-reference-directory/proposal.md index 574d8fa1e9f..1ac53282b1a 100644 --- a/openspec/changes/apertis-reference-directory/proposal.md +++ b/openspec/changes/apertis-reference-directory/proposal.md @@ -1,36 +1,46 @@ ## Why -The prior Apertis proposal registered an API-key preset and fixed upstream destination without -maintainer-visible evidence of routing/resale authorization. That would make a user-supplied key -and request flow through an unverified canonical integration. +Apertis publishes an OpenAI-compatible API at `https://api.apertis.ai/v1`, uses Bearer API keys, +and exposes a key-scoped live `/v1/models` catalog. The current PR intentionally removed the +canonical entry and therefore no longer meets the requested outcome of adding Apertis as a model +provider. ## What Changes -- Remove the canonical Apertis provider preset and all derived key-login, CLI, dashboard, and - model-discovery behavior. -- Add an inert `reference-only` directory row for Apertis that makes no pricing, entitlement, - endpoint, documentation, or verification claim. -- Preserve a manually configured provider named `apertis` as an ordinary custom provider. -- Run GUI ESLint commands under Bun so the repository's TypeScript local lint plugin loads during - the documented validation and pre-push paths. +- Restore Apertis as a canonical API-key provider in the registry-derived CLI, dashboard, key-login, + routing, and model-discovery surfaces. +- Use the documented fixed base URL and authenticated live model discovery without freezing a + mutable model list or claiming a default model. +- Preserve same-named custom providers' adapter, destination, and key boundary. +- Add focused coverage for derivation, Bearer validation, live discovery, chat routing, and custom + provider preservation. +- Sync the provider catalog documentation and translated tables/counts. +- Retain the existing Bun-based GUI lint runtime fix and make React Doctor's TypeScript peer + resolution explicit for Bun-launched validation. +- Keep the maintainer-required routing/resale authorization and security review as an explicit + pre-merge gate; public endpoint/terms/legal-entity evidence must not be presented as proof of + private authorization. ## Capabilities ### New Capabilities -- `provider-reference-directory`: represent unverified providers without registering a runtime - route or claiming an access entitlement. -- `gui-lint-runtime`: execute GUI lint commands with a runtime that supports the repository's - TypeScript ESLint configuration and local plugins. +- `provider-canonical-integration`: expose Apertis as a canonical key provider with authenticated + live model discovery and fixed-host routing. +- `gui-lint-runtime`: execute GUI lint and React Doctor validation with runtimes that preserve + the repository's local TypeScript tooling. ### Modified Capabilities -- None. + ## Impact -- `src/providers/free-directory.ts` gains the reference-only directory category. -- Provider registry derivation, GUI preset classification, and request routing must not recognize - Apertis as a catalog provider. -- Focused Bun tests replace the former canonical-provider behavior tests. -- `gui/package.json` lint commands use Bun's runtime bridge instead of a Node-only ESLint launch. +- `src/providers/registry.ts`, registry derivation, key validation, GUI provider classification, + and provider parity gain the `apertis` canonical entry. +- `tests/apertis-provider.test.ts`, the discovery fixture, and registry parity tests prove the + provider contract without contacting the live service. +- English and translated `docs-site` provider guides list the new endpoint and updated counts. +- `gui/package.json` lint commands continue to use Bun's runtime bridge, while React Doctor + commands use npm's explicit package resolution instead of a Bun-translated `npx` launch. diff --git a/openspec/changes/apertis-reference-directory/specs/gui-lint-runtime/spec.md b/openspec/changes/apertis-reference-directory/specs/gui-lint-runtime/spec.md index 91be50f629c..b58dc2effe6 100644 --- a/openspec/changes/apertis-reference-directory/specs/gui-lint-runtime/spec.md +++ b/openspec/changes/apertis-reference-directory/specs/gui-lint-runtime/spec.md @@ -11,3 +11,18 @@ so that the checked-in ESLint configuration and its local TypeScript plugin load #### Scenario: Focused GUI i18n lint command - **WHEN** a contributor runs the GUI `lint:i18n` script - **THEN** ESLint evaluates the configured UI paths with the same local plugin available + +### Requirement: React Doctor scripts resolve their TypeScript peer explicitly +The GUI package's `doctor` and `doctor:full` scripts SHALL invoke React Doctor through npm +with the pinned TypeScript and React Doctor packages so that Bun's `npx` alias cannot omit the +TypeScript peer required by React Doctor. + +#### Scenario: Changed GUI React Doctor command +- **WHEN** a contributor runs the GUI `doctor` script from the repository validation path +- **THEN** React Doctor evaluates the changed GUI scope and resolves its TypeScript peer + successfully + +#### Scenario: Full GUI React Doctor command +- **WHEN** a contributor runs the GUI `doctor:full` script +- **THEN** React Doctor evaluates the full GUI scope with the same explicit TypeScript peer + resolution diff --git a/openspec/changes/apertis-reference-directory/specs/provider-canonical-integration/spec.md b/openspec/changes/apertis-reference-directory/specs/provider-canonical-integration/spec.md new file mode 100644 index 00000000000..9a8b0a8a4ba --- /dev/null +++ b/openspec/changes/apertis-reference-directory/specs/provider-canonical-integration/spec.md @@ -0,0 +1,57 @@ +## ADDED Requirements + +### Requirement: Apertis is a canonical API-key provider +The provider registry SHALL expose Apertis as a canonical key provider with label `Apertis`, the +`openai-chat` adapter, base URL `https://api.apertis.ai/v1`, the documented dashboard URL, and live +model discovery enabled. The registry SHALL NOT freeze a static model list or default model for +the mutable key-scoped catalog. + +#### Scenario: Canonical setup surfaces derive Apertis +- **WHEN** CLI init, key-login, dashboard preset, and provider catalog surfaces are derived +- **THEN** each surface contains `apertis` with key authentication, the fixed base URL, and live discovery + +#### Scenario: Live catalog remains the source of available models +- **WHEN** a fresh Apertis provider is created without an explicit model list +- **THEN** the saved provider enables live discovery and does not claim a static model list or default model + +### Requirement: Apertis key validation and discovery use the authenticated models endpoint +The key-login flow SHALL call `GET https://api.apertis.ai/v1/models` with `Authorization: Bearer ` +and SHALL treat an OpenAI-shaped model list as the live catalog. A 401 or 403 response SHALL reject +the key; other non-success or transport failures SHALL remain unknown rather than becoming a false +positive. Discovery SHALL use the repository's bounded response, row, and model-id guards. + +#### Scenario: Valid key returns an OpenAI-shaped catalog +- **WHEN** the models endpoint returns a bounded `{ "object": "list", "data": [{ "id": "..." }] }` response +- **THEN** key validation succeeds and discovery exposes the returned model ids under `apertis/` + +#### Scenario: Unauthorized key is rejected +- **WHEN** the models endpoint returns HTTP 401 or 403 +- **THEN** key validation returns false and does not persist the key as valid + +#### Scenario: Uncertain upstream failure is not a false positive +- **WHEN** the models endpoint times out, returns a transport error, or returns another non-success status +- **THEN** key validation returns unknown and discovery does not cache an untrusted catalog + +### Requirement: Apertis routing preserves the configured custom boundary +Requests for canonical Apertis models SHALL use the `openai-chat` adapter, Bearer authentication, +and the canonical `/chat/completions` destination. A manually configured provider named `apertis` +whose adapter or destination differs from the canonical preset SHALL retain its own adapter, base URL, +model id, and key boundary. + +#### Scenario: Canonical chat request uses the Apertis host +- **WHEN** a request selects `apertis/` from a canonical provider config +- **THEN** the request targets `https://api.apertis.ai/v1/chat/completions` with the configured Bearer key and model id + +#### Scenario: Same-named custom provider is not retargeted +- **WHEN** a saved `apertis` provider has a custom adapter or base URL +- **THEN** routing and model discovery use that saved adapter/destination instead of the canonical Apertis host + +### Requirement: Canonical-provider evidence remains explicit and review-gated +The change record and PR SHALL distinguish public endpoint, API, terms, and legal-entity evidence from +the maintainer-required private routing/resale authorization. The change SHALL NOT claim verified, +merge-ready, merged, or deployed status until a maintainer records the required authorization, +maintenance owner, verification date, security review, and required CI approval. + +#### Scenario: Public evidence does not silently satisfy private authorization +- **WHEN** public Apertis documentation is present but maintainer routing/resale authorization is not recorded +- **THEN** the implementation may remain locally verified, but the PR remains explicitly not merge-ready diff --git a/openspec/changes/apertis-reference-directory/specs/provider-reference-directory/spec.md b/openspec/changes/apertis-reference-directory/specs/provider-reference-directory/spec.md deleted file mode 100644 index 57f8dc0a3f0..00000000000 --- a/openspec/changes/apertis-reference-directory/specs/provider-reference-directory/spec.md +++ /dev/null @@ -1,28 +0,0 @@ -## ADDED Requirements - -### Requirement: Unverified providers remain inert reference entries -The provider directory SHALL represent a provider lacking integration authorization in the -`reference-only` access group. Such an entry MUST use the reference support level and unverified -verification state, MUST NOT claim an access entitlement or verification date, and MUST NOT define -a usable upstream endpoint or live-model discovery. - -#### Scenario: Apertis is listed without an integration claim -- **WHEN** consumers inspect the Apertis directory row -- **THEN** it is `reference-only`, unverified, endpoint-free, and has unsupported discovery - -### Requirement: Reference entries do not become canonical providers -A `reference-only` directory entry SHALL NOT appear in the canonical provider registry or any -derived key-login, initialization, or dashboard preset list. It SHALL NOT be classified as a GUI -catalog provider. - -#### Scenario: Apertis is absent from canonical provider surfaces -- **WHEN** canonical provider surfaces are derived -- **THEN** no Apertis runtime preset, key validator, or catalog-provider classification exists - -### Requirement: Same-named custom providers retain their destination -Routing a manually configured provider whose id matches a `reference-only` directory row SHALL use -the configuration's own adapter and base URL without registry enrichment or redirection. - -#### Scenario: Custom Apertis configuration routes unchanged -- **WHEN** a user configures `apertis` with a custom adapter and base URL -- **THEN** a request for `apertis/` retains that adapter, base URL, and model id diff --git a/openspec/changes/apertis-reference-directory/tasks.md b/openspec/changes/apertis-reference-directory/tasks.md index 7b73dbafd15..e82ffcb892a 100644 --- a/openspec/changes/apertis-reference-directory/tasks.md +++ b/openspec/changes/apertis-reference-directory/tasks.md @@ -1,15 +1,25 @@ -## 1. Reference-only contract +## 1. Spec and evidence lock -- [x] 1.1 Add the explicit no-entitlement access group and Apertis reference row. -- [x] 1.2 Ensure no canonical Apertis registry or GUI-preset metadata remains. -- [x] 1.3 Make GUI lint scripts load the local TypeScript ESLint plugin under Bun. +- [x] 1.1 Update the proposal/design/specs from reference-only to canonical provider behavior. +- [x] 1.2 Validate the change strictly and record the public evidence links plus unresolved private authorization gate. -## 2. Regression proof +## 2. Canonical provider surfaces -- [x] 2.1 Add focused tests for inert directory metadata and the absence of canonical derivations. -- [x] 2.2 Add a same-named custom-routing regression test. +- [x] 2.1 Register Apertis in the canonical registry with fixed host, key auth, live discovery, and custom-destination preservation. +- [x] 2.2 Derive CLI/key-login/dashboard/GUI provider surfaces and update registry parity expectations. +- [x] 2.3 Keep Bun GUI lint and explicit npm-based React Doctor validation green. -## 3. Verification +## 3. Regression proof -- [x] 3.1 Run strict OpenSpec validation and focused provider-directory tests. -- [x] 3.2 Run repository typecheck, full test suite, GUI lint/build, privacy scan, and diff checks. +- [x] 3.1 Add a fixture-backed authenticated `/models` validation and live-discovery test. +- [x] 3.2 Add canonical chat routing and same-named custom-provider preservation tests. + +## 4. User-facing documentation + +- [x] 4.1 Update the English and translated provider catalogs, endpoint table, counts, and Apertis discovery notes. +- [x] 4.2 Keep public evidence distinct from maintainer-only routing/resale authorization. + +## 5. Verification and handoff + +- [x] 5.1 Run focused tests, typecheck, full test suite, GUI lint/build, privacy scan, and diff checks. +- [x] 5.2 Inspect latest-base diff and prepare the PR-ready evidence/status handoff without pushing, messaging, merging, or deploying. diff --git a/src/providers/free-directory.ts b/src/providers/free-directory.ts index 49522a2df65..9d96063dd6f 100644 --- a/src/providers/free-directory.ts +++ b/src/providers/free-directory.ts @@ -1,15 +1,10 @@ export type ProviderAccessGroup = - | "reference-only" | "recurring-or-keyless" | "recurring-uncapped" | "recurring-credit" | "signup-credit"; export const FREE_PROVIDER_ACCESS_GROUPS = { - // This group intentionally makes no free, credit, endpoint, or integration claim. It is for - // provider names that remain useful as reference entries but lack the evidence required for a - // canonical runtime preset. - "reference-only": ["apertis"], "recurring-or-keyless": [ "agy", "aihorde", "api-airforce", "arcee-ai", "bazaarlink", "blackbox", "bluesminds", "cerebras", "cloudflare-ai", "cohere", "coze", "duckduckgo-web", "felo-web", "friendliai", "gemini", "github-models", @@ -150,7 +145,6 @@ const CONNECTABLE: Record = { }; const LABELS: Record = { - apertis: "Apertis", agy: "AGY", aihorde: "AI Horde", "api-airforce": "API Airforce", "arcee-ai": "Arcee AI", bazaarlink: "BazaarLink", blackbox: "Blackbox", bluesminds: "Bluesminds", "cloudflare-ai": "Cloudflare AI", coze: "Coze", "duckduckgo-web": "DuckDuckGo Web", "felo-web": "Felo Web", friendliai: "FriendliAI", diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 91d3ad07ddf..9fddff9aed2 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -1111,6 +1111,20 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ }, // llama-3.3-70b was deprecated by Cerebras on 2026-02-16. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md. { id: "cerebras", label: "Cerebras", baseUrl: "https://api.cerebras.ai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://cloud.cerebras.ai/platform/apikeys", defaultModel: "gpt-oss-120b" }, + // Public contract verified 2026-08-04 against https://docs.apertis.ai/api/, + // https://docs.apertis.ai/api/utilities/models/, https://apertis.ai/terms, and the Apertis + // product/legal pages. Private routing/resale authorization remains a maintainer review gate. + { + id: "apertis", + label: "Apertis", + baseUrl: "https://api.apertis.ai/v1", + adapter: "openai-chat", + authKind: "key", + dashboardUrl: "https://apertis.ai/setting?tab=keys", + liveModels: true, + preserveCustomDestination: true, + note: "OpenAI-compatible multi-provider API; live model access is scoped to the API key's plan.", + }, { id: "deepinfra", label: "DeepInfra", diff --git a/tests/apertis-provider.test.ts b/tests/apertis-provider.test.ts index eee54e861e0..253f869773b 100644 --- a/tests/apertis-provider.test.ts +++ b/tests/apertis-provider.test.ts @@ -1,64 +1,221 @@ -import { describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { createOpenAIChatAdapter } from "../src/adapters/openai-chat"; +import { gatherRoutedModels } from "../src/codex/catalog"; +import { clearModelCache } from "../src/codex/model-cache"; import { buildInitProviders } from "../src/cli/init"; -import { deriveProviderPresets } from "../src/providers/derive"; -import { FREE_PROVIDER_DIRECTORY } from "../src/providers/free-directory"; -import { PROVIDER_REGISTRY } from "../src/providers/registry"; +import { buildModelsRequest } from "../src/oauth"; +import { KEY_LOGIN_PROVIDERS, validateApiKey } from "../src/oauth/key-providers"; +import { + deriveInitProviders, + deriveProviderPresets, + providerConfigSeed, +} from "../src/providers/derive"; +import { PROVIDER_REGISTRY, type ProviderRegistryEntry } from "../src/providers/registry"; import { routeModel } from "../src/router"; -import type { OcxConfig } from "../src/types"; +import type { OcxConfig, OcxProviderConfig } from "../src/types"; import { formatProviderDisplayName, isCatalogProviderId } from "../gui/src/provider-icons"; import type { TFn } from "../gui/src/i18n/shared"; +import { withStubbedProviderFetch } from "./helpers/catalog-provider-fetch"; +const FIXTURE = readFileSync(join(import.meta.dir, "fixtures/apertis-models.json"), "utf8"); +const BASE_URL = "https://api.apertis.ai/v1"; +const API_KEY = "apertis-test-key"; const identityT: TFn = key => key; +const originalFetch = globalThis.fetch; -describe("Apertis reference directory entry", () => { - test("is inert and makes no integration or entitlement claim", () => { - const entry = FREE_PROVIDER_DIRECTORY.find(row => row.id === "apertis"); +afterEach(() => { + globalThis.fetch = originalFetch; + clearModelCache("apertis"); +}); + +function registryEntry(): ProviderRegistryEntry { + const entry = PROVIDER_REGISTRY.find(row => row.id === "apertis"); + if (!entry) throw new Error("missing apertis registry entry"); + return entry; +} + +function providerConfig(overrides: Partial = {}): OcxConfig { + return withStubbedProviderFetch({ + port: 10100, + defaultProvider: "apertis", + providers: { + apertis: { + adapter: "openai-chat", + baseUrl: BASE_URL, + authMode: "key", + apiKey: API_KEY, + liveModels: true, + // Discovery is fixture-only; this avoids platform-specific public-DNS classification. + allowPrivateNetwork: true, + ...overrides, + }, + }, + }); +} - expect(entry).toMatchObject({ +describe("Apertis provider", () => { + test("registers a fixed OpenAI transport with live discovery", () => { + expect(registryEntry()).toMatchObject({ id: "apertis", label: "Apertis", adapter: "openai-chat", - baseUrl: "", + baseUrl: BASE_URL, authKind: "key", - accessGroups: ["reference-only"], - supportLevel: "reference", - verification: "unverified", - discovery: "unsupported", - liveModels: false, + dashboardUrl: "https://apertis.ai/setting?tab=keys", + liveModels: true, + preserveCustomDestination: true, }); - expect(entry).not.toHaveProperty("lastVerified"); - expect(entry).not.toHaveProperty("documentationUrl"); - expect(entry).not.toHaveProperty("dashboardUrl"); + expect(registryEntry()).not.toHaveProperty("models"); + expect(registryEntry()).not.toHaveProperty("defaultModel"); + expect(registryEntry()).not.toHaveProperty("modelDiscovery"); }); - test("does not derive a canonical provider or GUI preset", () => { - expect(PROVIDER_REGISTRY.some(entry => entry.id === "apertis")).toBe(false); - expect(buildInitProviders().some(entry => entry.id === "apertis")).toBe(false); - expect(deriveProviderPresets().some(entry => entry.id === "apertis")).toBe(false); - expect(isCatalogProviderId("apertis")).toBe(false); + test("derives CLI and dashboard presets without persisting registry trust policy", () => { + const entry = registryEntry(); + expect(buildInitProviders()).toEqual(deriveInitProviders()); + expect(KEY_LOGIN_PROVIDERS.apertis).toMatchObject({ + adapter: "openai-chat", + baseUrl: BASE_URL, + dashboardUrl: entry.dashboardUrl, + liveModels: true, + }); + expect(buildInitProviders().find(row => row.id === "apertis")).toMatchObject({ + kind: "key", + adapter: "openai-chat", + baseUrl: BASE_URL, + }); + expect(deriveProviderPresets().find(row => row.id === "apertis")).toMatchObject({ + auth: "key", + dashboardUrl: entry.dashboardUrl, + }); + + const seed = providerConfigSeed(entry); + expect(seed).toMatchObject({ + adapter: "openai-chat", + baseUrl: BASE_URL, + authMode: "key", + liveModels: true, + }); + expect(seed).not.toHaveProperty("models"); + expect(seed).not.toHaveProperty("defaultModel"); + expect(seed).not.toHaveProperty("modelDiscovery"); + expect(seed).not.toHaveProperty("preserveCustomDestination"); + expect(KEY_LOGIN_PROVIDERS.apertis).not.toHaveProperty("modelDiscovery"); + expect(KEY_LOGIN_PROVIDERS.apertis).not.toHaveProperty("preserveCustomDestination"); expect(formatProviderDisplayName("apertis", identityT)).toBe("Apertis"); + expect(isCatalogProviderId("apertis")).toBe(true); }); - test("keeps a same-named custom provider's adapter and destination", () => { - const config: OcxConfig = { - port: 10100, - defaultProvider: "apertis", - providers: { - apertis: { - adapter: "anthropic", - baseUrl: "https://custom.example.test/anthropic", - apiKey: "test-key", - liveModels: true, - }, - }, - }; + test("lists and validates models through the documented Bearer-authenticated endpoint", async () => { + expect(buildModelsRequest(providerConfig().providers.apertis!, API_KEY, "apertis")).toEqual({ + url: `${BASE_URL}/models`, + headers: { Authorization: `Bearer ${API_KEY}` }, + }); + + globalThis.fetch = (async (input, init) => { + expect(String(input)).toBe(`${BASE_URL}/models`); + expect(new Headers(init?.headers).get("authorization")).toBe(`Bearer ${API_KEY}`); + expect(init?.redirect).toBe("error"); + return new Response(FIXTURE, { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + + expect(await validateApiKey("apertis", KEY_LOGIN_PROVIDERS.apertis!, API_KEY)).toBe(true); + }); + + test("rejects unauthorized keys without claiming validation", async () => { + globalThis.fetch = (async (input, init) => { + expect(String(input)).toBe(`${BASE_URL}/models`); + expect(new Headers(init?.headers).get("authorization")).toBe(`Bearer ${API_KEY}`); + return new Response("unauthorized", { status: 401 }); + }) as typeof fetch; - const routed = routeModel(config, "apertis/custom-model"); - expect(routed.provider).toMatchObject({ + expect(await validateApiKey("apertis", KEY_LOGIN_PROVIDERS.apertis!, API_KEY)).toBe(false); + }); + + test("rejects forbidden keys without claiming validation", async () => { + globalThis.fetch = (async (input, init) => { + expect(String(input)).toBe(`${BASE_URL}/models`); + expect(new Headers(init?.headers).get("authorization")).toBe(`Bearer ${API_KEY}`); + return new Response("forbidden", { status: 403 }); + }) as typeof fetch; + + expect(await validateApiKey("apertis", KEY_LOGIN_PROVIDERS.apertis!, API_KEY)).toBe(false); + }); + + test("keeps transient endpoint failures unknown", async () => { + globalThis.fetch = (async (input, init) => { + expect(String(input)).toBe(`${BASE_URL}/models`); + expect(new Headers(init?.headers).get("authorization")).toBe(`Bearer ${API_KEY}`); + return new Response("temporarily unavailable", { status: 503 }); + }) as typeof fetch; + + expect(await validateApiKey("apertis", KEY_LOGIN_PROVIDERS.apertis!, API_KEY)).toBe("unknown"); + + globalThis.fetch = (async () => { + throw new TypeError("network unavailable"); + }) as typeof fetch; + expect(await validateApiKey("apertis", KEY_LOGIN_PROVIDERS.apertis!, API_KEY)).toBe("unknown"); + }); + + test("discovers OpenAI-shaped model ids", async () => { + globalThis.fetch = (async (input, init) => { + expect(String(input)).toBe(`${BASE_URL}/models`); + expect(new Headers(init?.headers).get("authorization")).toBe(`Bearer ${API_KEY}`); + expect(init?.redirect).toBe("manual"); + return new Response(FIXTURE, { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + + const models = await gatherRoutedModels(providerConfig()); + expect(models.filter(row => row.provider === "apertis").map(row => row.id)).toEqual([ + "claude-sonnet-4.5", + "gpt-4.1", + ]); + }); + + test("routes chat completions to the fixed provider host", () => { + const route = routeModel(providerConfig(), "apertis/gpt-4.1"); + const request = createOpenAIChatAdapter(route.provider).buildRequest({ + modelId: route.modelId, + context: { messages: [{ role: "user", content: "ping", timestamp: 0 }] }, + stream: true, + options: {}, + }); + const body = JSON.parse(String(request.body)) as Record; + + expect(request.url).toBe(`${BASE_URL}/chat/completions`); + expect(request.headers.Authorization).toBe(`Bearer ${API_KEY}`); + expect(body.model).toBe("gpt-4.1"); + }); + + test("does not retarget same-named custom providers", () => { + const customConfig = providerConfig({ baseUrl: "https://custom.example/v1" }); + const route = routeModel(customConfig, "apertis/custom-model"); + expect(route.provider).toMatchObject({ + adapter: "openai-chat", + baseUrl: "https://custom.example/v1", + authMode: "key", + }); + expect(buildModelsRequest(customConfig.providers.apertis!, "custom-key", "apertis")).toEqual({ + url: "https://custom.example/v1/models", + headers: { Authorization: "Bearer custom-key" }, + }); + + const customAdapter = routeModel(providerConfig({ adapter: "anthropic", - baseUrl: "https://custom.example.test/anthropic", - liveModels: true, + baseUrl: "https://custom.example/anthropic", + }), "apertis/custom-model"); + expect(customAdapter.provider).toMatchObject({ + adapter: "anthropic", + baseUrl: "https://custom.example/anthropic", + authMode: "key", }); - expect(routed.modelId).toBe("custom-model"); }); }); diff --git a/tests/fixtures/apertis-models.json b/tests/fixtures/apertis-models.json new file mode 100644 index 00000000000..ca0fc60261e --- /dev/null +++ b/tests/fixtures/apertis-models.json @@ -0,0 +1,21 @@ +{ + "object": "list", + "data": [ + { + "id": "gpt-4.1", + "object": "model", + "created": 1626777600, + "owned_by": "OpenAI", + "root": "gpt-4.1", + "parent": null + }, + { + "id": "claude-sonnet-4.5", + "object": "model", + "created": 1626777600, + "owned_by": "Anthropic", + "root": "claude-sonnet-4.5", + "parent": null + } + ] +} diff --git a/tests/provider-registry-parity.test.ts b/tests/provider-registry-parity.test.ts index 11093a5cf6d..705da48a0c3 100644 --- a/tests/provider-registry-parity.test.ts +++ b/tests/provider-registry-parity.test.ts @@ -31,7 +31,7 @@ function nativeTemplate(): Record { const EXPECTED_KEY_PROVIDER_IDS = [ "anthropic-apikey", "openai-apikey", "umans", "opencode-go", "neuralwatt", "openrouter", "cline-pass", "cline", "orcarouter", "bizrouter", "groq", "google", "google-vertex", "azure-openai", - "deepseek", "cerebras", "deepinfra", "hyperbolic", "baseten", "commandcode", "together", "fireworks", "firepass", "moonshot", + "deepseek", "cerebras", "apertis", "deepinfra", "hyperbolic", "baseten", "commandcode", "together", "fireworks", "firepass", "moonshot", "huggingface", "nvidia", "venice", "zai", "zhipu-bigmodel", "nanogpt", "synthetic", "siliconflow", "qwen-cloud", "tencent-coding-plan", "volcengine", "volcengine-coding-plan", "volcengine-agent-plan", "qianfan", "alibaba", "alibaba-token-plan", "alibaba-token-plan-intl", "parallel", "zenmux", "litellm", "ollama-cloud", "mistral", "minimax", "minimax-cn", "kimi-code", "opencode-zen", "vercel-ai-gateway",