mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 15:52:52 +03:00
* feat(providers): rebase ClinePass dual-auth (OAuth + BYOK) onto release/v3.8.47
ClinePass now offers both sign-in methods on its dashboard page: OAuth
(reusing the Cline WorkOS flow, primary "Connect" button) or a pasted
BYOK API key ("Manual API key"), instead of only the API-key-only
provider shipped in #5942.
- Registry: authType oauth + oauth urls, alias aligned to "cp" (matches
the OAUTH_PROVIDERS catalog alias so <alias>/<modelId> routing
resolves); keeps the #6165 forceStream:true fix (streaming-only API).
- Executor: new buildClinepassHeaders() (src/shared/utils/clineAuth.ts)
picks buildClineHeaders() for an OAuth accessToken or a plain Bearer +
Cline identification headers for a BYOK key — extracted to a leaf
module to avoid growing the frozen open-sse/executors/default.ts.
- Refresh: dispatch clinepass to the shared refreshClineToken() (was
falling through to the generic refresh and failing silently).
- Catalog: admit the BYOK path through a dedicated
DUAL_AUTH_APIKEY_PROVIDER_IDS gate (src/lib/providers/catalog.ts) so
POST /api/providers accepts an apikey connection without flipping
isOAuth off (which would break the primary Connect->OAuth routing).
- Dashboard: render both "Connect" + "Manual API key" buttons for
clinepass (ConnectionsHeaderToolbar.tsx, EmptyConnectionsPlaceholder.tsx).
- Dedup: removed the now-redundant API-key-only APIKEY_PROVIDERS_GATEWAYS
entry so ClinePass is listed once (OAuth-primary).
- oauth.ts: added the clinepass catalog entry (was reverted by staleness
during rebase); src/lib/oauth/providers/index.ts: clinepass -> cline.
This branch was ~167 commits / weeks behind release/v3.8.47; a real
merge surfaced 61 conflicting files, several of which are already-shipped
fixes (forceStream #6165, zed-hosted, requesty, agentrouter CC-wire-image,
NVIDIA/Mistral/kimi executor fixes, chatCore hardening) that a naive
resolution would have silently reverted. Reconstructed clean on top of
current release/v3.8.47, isolating and re-applying only the clinepass
dual-auth feature and preserving every already-shipped fix untouched.
tokenRefresh.ts's frozen-file cap raised by the irreducible 1-line
`case "clinepass":` switch label (config/quality/file-size-baseline.json,
justified inline); open-sse/executors/default.ts stays under its cap via
the buildClinepassHeaders() extraction.
Regression guard: tests/unit/clinepass-provider.test.ts (15/15, extended
with the dual-auth admission-gate and alias-consistency guards).
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* test(providers): update APIKEY_PROVIDERS spread-merge count 171->170
The ClinePass dual-auth rebase (this PR) removed the now-redundant
API-key-only APIKEY_PROVIDERS_GATEWAYS.clinepass entry (dedup — clinepass
is OAuth-primary now, with its BYOK path admitted through the
DUAL_AUTH_APIKEY_PROVIDER_IDS gate instead of a second catalog entry),
which drops the total APIKEY_PROVIDERS spread-merge count by one.
tests/unit/providers-constants-split.test.ts hardcoded the prior count
(171); updated to 170 to match, confirmed via CI (Unit Tests fast-path
1/2 and 2/2 both failed on the stale count).
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(oauth): register clinepass in PROVIDERS enum to fix Unknown provider error
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* chore(test): rebaseline oauth-providers-config.test.ts frozen size for clinepass entries
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(changelog): re-restore #6126 bullet after release sync
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: hajilok <hajilok@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
This commit is contained in:
committed by
GitHub
parent
9906dfc1ba
commit
d3331f8bca
@@ -14,6 +14,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral
|
||||
- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev)
|
||||
- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen)
|
||||
- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333)
|
||||
- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `<alias>/<modelId>` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
@@ -182,7 +182,8 @@
|
||||
"open-sse/services/compression/strategySelector.ts": 1043,
|
||||
"open-sse/services/rateLimitManager.ts": 1035,
|
||||
"_rebaseline_2026_06_29_4038_cas_guard": "PR (#4038) own growth: tokenRefresh.ts 2103->2181 (+78 = the compare-and-swap guard on the refresh persist — runWithCasGuard/getActiveCasGuard AsyncLocalStorage pair mirroring runWithOnPersist, casGuardShouldSkipPersist that rereads the row right before persisting and skips the write when a concurrent writer already rotated the refresh_token past the one presented, plus getCasGuardStats counters). Fixes the sibling-rotation-revert → token-family-revocation storm. Gated behind an active guard (opt-in; no guard => byte-identical). Wiring lives at the two persist chokepoints inside getAccessToken; the comparison reuses wasRefreshTokenRotated from refreshSerializer. Not extractable without splitting the refresh hot path.",
|
||||
"open-sse/services/tokenRefresh.ts": 2181,
|
||||
"_rebaseline_2026_07_09_6126_clinepass_dual_auth": "PR #6126 (@hajilok, dual-auth ClinePass) own growth: tokenRefresh.ts 2181->2182 (+1 = a single `case \"clinepass\":` fallthrough label added to the existing `case \"cline\":` in _getAccessTokenInternal's provider switch, so clinepass token refresh dispatches to the already-shared refreshClineToken() instead of silently falling through to the generic OAuth refresh). Irreducible 1-line switch-case wiring at the existing chokepoint; the header-building logic for the same feature was extracted to a new leaf src/shared/utils/clineAuth.ts::buildClinepassHeaders() (well under cap) to avoid growing open-sse/executors/default.ts. Covered by tests/unit/clinepass-provider.test.ts.",
|
||||
"open-sse/services/tokenRefresh.ts": 2182,
|
||||
"open-sse/services/usage.ts": 3454,
|
||||
"open-sse/translator/request/openai-to-gemini.ts": 906,
|
||||
"open-sse/translator/request/openai-to-kiro.ts": 890,
|
||||
@@ -276,6 +277,7 @@
|
||||
},
|
||||
"testCap": 800,
|
||||
"testFrozen": {
|
||||
"_rebaseline_2026_07_09_6126_clinepass_dualauth": "#6126 (ClinePass dual-auth) own test growth: oauth-providers-config.test.ts 842->845 (+3: clinepass key/config/required-fields entries reusing the Cline WorkOS flow config, needed after registering clinepass in the oauth.ts PROVIDERS enum).",
|
||||
"_rebaseline_2026_06_27_5193_antigravity_test": "#5193 own test growth: oauth-providers-config.test.ts 870->873 (+3: antigravity projectId assertion + 50ms tick for the now fire-and-forget onboarding, matching the no-PKCE/no-openid flow).",
|
||||
"_rebaseline_2026_07_02_5928_base_red": "web-cookie-providers-new.test.ts 845->850: #5928 (test(security) Kimi Web URL host parse, CodeQL #689) grew the file +5 lines and merged into release/v3.8.44 WITHOUT rebaselining, leaving a fast-gates base-red that blocked every subsequent PR->release. Test growth is legitimate (a security regression test); maintainer absorbs the drift here. Frozen at 850.",
|
||||
"_rebaseline_basered_codebuddy_cn": "Base-red fix (#4664 CodeBuddy CN): oauth-providers-config.test.ts 867->870 (+3) to align the EXPECTED provider list/config with the codebuddy-cn provider that #4664 added to the registry without updating this test (it asserts 'exactly once').",
|
||||
@@ -305,7 +307,7 @@
|
||||
"tests/unit/image-generation-handler.test.ts": 2019,
|
||||
"tests/unit/model-sync-route.test.ts": 1016,
|
||||
"tests/unit/models-catalog-route.test.ts": 1605,
|
||||
"tests/unit/oauth-providers-config.test.ts": 842,
|
||||
"tests/unit/oauth-providers-config.test.ts": 845,
|
||||
"tests/unit/perplexity-web.test.ts": 999,
|
||||
"tests/unit/provider-models-route.test.ts": 1752,
|
||||
"tests/unit/provider-validation-specialty.test.ts": 2856,
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
|
||||
// ClinePass — Cline's $9.99/mo BYOK API-key gateway (https://cline.bot). Distinct
|
||||
// from the OAuth `cline` provider: same host (api.cline.bot) but a plain Bearer
|
||||
// API key and the `cline-pass/*` model namespace. Responses are wrapped in a
|
||||
// {success, data} envelope — unwrapped by open-sse/utils/clinepassEnvelope.ts.
|
||||
// ClinePass — Cline's $9.99/mo gateway (https://cline.bot). Dual-auth: sign in
|
||||
// with a Cline account (OAuth, reusing the `cline` WorkOS flow) OR paste a direct
|
||||
// BYOK API key. Same host (api.cline.bot) as the OAuth `cline` provider; the
|
||||
// `cline-pass/*` model namespace. Responses are wrapped in a {success, data}
|
||||
// envelope — unwrapped by open-sse/utils/clinepassEnvelope.ts.
|
||||
export const clinepassProvider: RegistryEntry = {
|
||||
id: "clinepass",
|
||||
alias: "clinepass",
|
||||
// MUST match the OAUTH_PROVIDERS catalog alias (src/shared/constants/providers/oauth.ts).
|
||||
// The dashboard sends models as `<catalogAlias>/<modelId>` (e.g. "cp/cline-pass/glm-5.2"),
|
||||
// and routing resolves the prefix via ALIAS_TO_PROVIDER_ID (built from this field). If the
|
||||
// registry alias drifts from the catalog alias, the prefix won't resolve, the executor falls
|
||||
// back to PROVIDERS.openai, and requests hit api.openai.com with the ClinePass key → 401.
|
||||
alias: "cp",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
// ClinePass shares Cline's streaming-only API — a non-streaming request returns
|
||||
// "generateText is not implemented" / an empty body. Force upstream streaming;
|
||||
// chatCore accumulates the SSE and converts it back to JSON for stream:false
|
||||
// clients. (Same as the sibling `cline` provider.)
|
||||
// clients. (Same as the sibling `cline` provider. #6165.)
|
||||
forceStream: true,
|
||||
baseUrl: "https://api.cline.bot/api/v1/chat/completions",
|
||||
authType: "apikey",
|
||||
authType: "oauth",
|
||||
authHeader: "bearer",
|
||||
oauth: {
|
||||
tokenUrl: "https://api.cline.bot/api/v1/auth/token",
|
||||
refreshUrl: "https://api.cline.bot/api/v1/auth/refresh",
|
||||
authUrl: "https://api.cline.bot/api/v1/auth/authorize",
|
||||
},
|
||||
extraHeaders: {
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
|
||||
@@ -44,7 +44,7 @@ import { buildMaritalkChatUrl } from "../config/maritalk.ts";
|
||||
import { LOCAL_PROVIDERS } from "@/shared/constants/providers";
|
||||
import { isForbiddenCustomHeaderName } from "@/shared/constants/upstreamHeaders";
|
||||
import { getClaudeCodeCompatibleRequestDefaults } from "@/lib/providers/requestDefaults";
|
||||
import { buildClineHeaders } from "@/shared/utils/clineAuth";
|
||||
import { buildClineHeaders, buildClinepassHeaders } from "@/shared/utils/clineAuth";
|
||||
import {
|
||||
normalizeHerokuChatUrl,
|
||||
normalizeDatabricksChatUrl,
|
||||
@@ -369,6 +369,9 @@ export class DefaultExecutor extends BaseExecutor {
|
||||
case "glm-coding-apikey":
|
||||
headers["x-api-key"] = effectiveKey || credentials.accessToken;
|
||||
break;
|
||||
case "clinepass": // dual-auth (OAuth or BYOK) — see buildClinepassHeaders()
|
||||
Object.assign(headers, buildClinepassHeaders(credentials, effectiveKey));
|
||||
break;
|
||||
case "cline":
|
||||
// Cline's API requires the bearer token prefixed with `workos:` plus a
|
||||
// set of Cline client-identification headers; plain `Bearer <token>`
|
||||
|
||||
@@ -1602,6 +1602,7 @@ async function _getAccessTokenInternal(provider, credentials, log, proxyConfig:
|
||||
);
|
||||
|
||||
case "cline":
|
||||
case "clinepass": // reuses the Cline WorkOS refresh flow (clinepass: cline)
|
||||
return await refreshClineToken(credentials.refreshToken, log, proxyConfig);
|
||||
|
||||
case "kimi-coding":
|
||||
|
||||
@@ -247,17 +247,22 @@ export default function ConnectionsHeaderToolbar({
|
||||
)}
|
||||
{!isCompatible ? (
|
||||
<>
|
||||
{isCommandCode ? (
|
||||
{isCommandCode || providerId === "clinepass" ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
icon="open_in_new"
|
||||
loading={
|
||||
commandCodeAuthState.phase === "starting" ||
|
||||
commandCodeAuthState.phase === "polling" ||
|
||||
commandCodeAuthState.phase === "applying"
|
||||
isCommandCode &&
|
||||
(commandCodeAuthState.phase === "starting" ||
|
||||
commandCodeAuthState.phase === "polling" ||
|
||||
commandCodeAuthState.phase === "applying")
|
||||
}
|
||||
onClick={() =>
|
||||
gateConnectionFlow(
|
||||
isCommandCode ? handleOpenCommandCodeConnect : openPrimaryAddFlow
|
||||
)
|
||||
}
|
||||
onClick={() => gateConnectionFlow(handleOpenCommandCodeConnect)}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
|
||||
@@ -55,16 +55,21 @@ export default function EmptyConnectionsPlaceholder({
|
||||
<p className="text-sm text-text-muted mb-4">{t("addFirstConnectionHint")}</p>
|
||||
{!isCompatible && (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{isCommandCode ? (
|
||||
{isCommandCode || providerId === "clinepass" ? (
|
||||
<>
|
||||
<Button
|
||||
icon="open_in_new"
|
||||
loading={
|
||||
commandCodeAuthState.phase === "starting" ||
|
||||
commandCodeAuthState.phase === "polling" ||
|
||||
commandCodeAuthState.phase === "applying"
|
||||
isCommandCode &&
|
||||
(commandCodeAuthState.phase === "starting" ||
|
||||
commandCodeAuthState.phase === "polling" ||
|
||||
commandCodeAuthState.phase === "applying")
|
||||
}
|
||||
onClick={() =>
|
||||
gateConnectionFlow(
|
||||
isCommandCode ? handleOpenCommandCodeConnect : openPrimaryAddFlow
|
||||
)
|
||||
}
|
||||
onClick={() => gateConnectionFlow(handleOpenCommandCodeConnect)}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
|
||||
@@ -484,6 +484,7 @@ export const PROVIDERS = {
|
||||
CURSOR: "cursor",
|
||||
KILOCODE: "kilocode",
|
||||
CLINE: "cline",
|
||||
CLINEPASS: "clinepass",
|
||||
WINDSURF: "windsurf",
|
||||
DEVIN_CLI: "devin-cli",
|
||||
TRAE: "trae",
|
||||
|
||||
@@ -46,6 +46,10 @@ export const PROVIDERS = {
|
||||
trae,
|
||||
kilocode,
|
||||
cline,
|
||||
// clinepass reuses the Cline WorkOS OAuth flow 1:1 (same api.cline.bot host, same token
|
||||
// type) — it is a separate catalog entry advertising the cline-pass/* (ClinePass
|
||||
// subscription) models. See registry/clinepass/index.ts.
|
||||
clinepass: cline,
|
||||
windsurf,
|
||||
// devin-cli shares the same token format as windsurf (WINDSURF_API_KEY / devin auth login)
|
||||
"devin-cli": windsurf,
|
||||
|
||||
@@ -195,8 +195,22 @@ export function resolveStaticProviderCatalogEntry(
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth-primary providers that ALSO accept a direct BYOK API key (dual-auth),
|
||||
* admitted through the managed-connection API-key gate independent of the OAuth
|
||||
* catalog. These are deliberately kept OUT of `FREE_APIKEY_PROVIDER_IDS`: that
|
||||
* set flips `providerSupportsPat` true, which turns `isOAuth` false and would
|
||||
* make the dashboard's primary "Connect" button route to the API-key modal
|
||||
* instead of the OAuth flow. Admitting them here lets POST /api/providers
|
||||
* persist an `apikey` connection (the reliable BYOK path) while the provider
|
||||
* stays OAuth-primary (isOAuth=true). clinepass is the dual-auth case: sign in
|
||||
* with a Cline account OR paste a ClinePass API key.
|
||||
*/
|
||||
const DUAL_AUTH_APIKEY_PROVIDER_IDS = new Set<string>(["clinepass"]);
|
||||
|
||||
export function isManagedProviderConnectionId(providerId: string): boolean {
|
||||
if (supportsApiKeyOnFreeProvider(providerId)) return true;
|
||||
if (DUAL_AUTH_APIKEY_PROVIDER_IDS.has(providerId)) return true;
|
||||
|
||||
const entry = resolveStaticProviderCatalogEntry(providerId);
|
||||
return !!(entry && MANAGED_PROVIDER_CONNECTION_CATEGORIES.has(entry.category));
|
||||
|
||||
@@ -41,20 +41,6 @@ export const APIKEY_PROVIDERS_GATEWAYS = {
|
||||
"Use a Command Code API key. Requests are sent to Command Code's /alpha/generate endpoint.",
|
||||
apiHint: "Create or copy an API key from Command Code, then paste it here as a Bearer token.",
|
||||
},
|
||||
clinepass: {
|
||||
id: "clinepass",
|
||||
alias: "clinepass",
|
||||
name: "ClinePass",
|
||||
icon: "vpn_key",
|
||||
color: "#5B9BD5",
|
||||
textIcon: "CP",
|
||||
passthroughModels: true,
|
||||
website: "https://cline.bot",
|
||||
notice: {
|
||||
text: "ClinePass is Cline's paid BYOK gateway ($9.99/mo). Bring your own Cline API key; requests hit api.cline.bot with the cline-pass/* model namespace.",
|
||||
apiKeyUrl: "https://app.cline.bot/settings/api-keys",
|
||||
},
|
||||
},
|
||||
openrouter: {
|
||||
id: "openrouter",
|
||||
alias: "openrouter",
|
||||
|
||||
@@ -191,6 +191,19 @@ export const OAUTH_PROVIDERS = {
|
||||
subscriptionRisk: true,
|
||||
riskNoticeVariant: "oauth",
|
||||
},
|
||||
clinepass: {
|
||||
id: "clinepass",
|
||||
alias: "cp",
|
||||
name: "ClinePass",
|
||||
icon: "smart_toy",
|
||||
color: "#9D4EDD",
|
||||
textIcon: "CP",
|
||||
website: "https://cline.bot/clinepass",
|
||||
subscriptionRisk: true,
|
||||
riskNoticeVariant: "oauth",
|
||||
authHint:
|
||||
"ClinePass is Cline's $9.99/mo subscription bundling 10 open coding models. Sign in with your Cline account (same login as the Cline CLI/IDE), or paste a direct ClinePass API key (app.cline.bot → Settings → API Keys). A ClinePass subscription unlocks the cline-pass/* models. Reuses the Cline WorkOS OAuth flow.",
|
||||
},
|
||||
windsurf: {
|
||||
id: "windsurf",
|
||||
alias: "ws",
|
||||
|
||||
@@ -60,3 +60,26 @@ export function buildClineHeaders(
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build headers for a ClinePass request. ClinePass is dual-auth: an OAuth
|
||||
* connection (workos:-prefixed token in `accessToken`) needs the full Cline
|
||||
* client header set from `buildClineHeaders()`; a BYOK API-key connection
|
||||
* (`sk_...` key, #5942) sends the key as a plain Bearer token — no `workos:`
|
||||
* prefix — alongside the Cline identification headers.
|
||||
*/
|
||||
export function buildClinepassHeaders(
|
||||
credentials: { accessToken?: unknown; apiKey?: unknown } | null | undefined,
|
||||
effectiveKey?: string
|
||||
): Record<string, string> {
|
||||
if (credentials?.accessToken) {
|
||||
return buildClineHeaders(credentials.accessToken);
|
||||
}
|
||||
const headers: Record<string, string> = {
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
};
|
||||
const byokKey = effectiveKey || (credentials?.apiKey as string | undefined);
|
||||
if (byokKey) headers.Authorization = `Bearer ${byokKey}`;
|
||||
return headers;
|
||||
}
|
||||
|
||||
@@ -1,34 +1,39 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
|
||||
const { APIKEY_PROVIDERS, OAUTH_PROVIDERS, supportsApiKeyOnFreeProvider } =
|
||||
await import("../../src/shared/constants/providers.ts");
|
||||
const { isManagedProviderConnectionId } = await import("../../src/lib/providers/catalog.ts");
|
||||
const { PROVIDERS: oauthFlows } = await import("../../src/lib/oauth/providers/index.ts");
|
||||
const { REGISTRY: providerRegistry } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { unwrapClinepassEnvelope } = await import("../../open-sse/utils/clinepassEnvelope.ts");
|
||||
const { filterClinepassModels } = await import("../../open-sse/services/clinepassModels.ts");
|
||||
const { parseUpstreamError, buildErrorBody } = await import("../../open-sse/utils/error.ts");
|
||||
|
||||
// ── Provider metadata (Zod-validated APIKEY catalog) ─────────────────────────
|
||||
test("ClinePass is registered as an API-key provider with the canonical identity", () => {
|
||||
const cp = APIKEY_PROVIDERS.clinepass;
|
||||
assert.ok(cp, "APIKEY_PROVIDERS.clinepass must be defined");
|
||||
// ── Provider metadata (oauth-primary catalog; single provider) ──────────────
|
||||
test("ClinePass is registered as an OAuth-primary provider with the canonical identity", () => {
|
||||
const cp = OAUTH_PROVIDERS.clinepass;
|
||||
assert.ok(cp, "OAUTH_PROVIDERS.clinepass must be defined (oauth-primary catalog)");
|
||||
assert.equal(cp.id, "clinepass");
|
||||
assert.equal(cp.alias, "clinepass");
|
||||
assert.equal(cp.name, "ClinePass");
|
||||
assert.equal(cp.website, "https://cline.bot");
|
||||
assert.equal(
|
||||
(cp as { notice?: { apiKeyUrl?: string } }).notice?.apiKeyUrl,
|
||||
"https://app.cline.bot/settings/api-keys"
|
||||
// Single provider — NO duplicate APIKEY_PROVIDERS entry. Dual-auth (OAuth sign-in
|
||||
// + Manual API key) is rendered by the dashboard's isOAuth branch (same as
|
||||
// cline/claude), not via FREE_APIKEY_PROVIDER_IDS (which would flip isOAuth off).
|
||||
assert.ok(
|
||||
!APIKEY_PROVIDERS.clinepass,
|
||||
"clinepass must NOT be in APIKEY_PROVIDERS (single provider)"
|
||||
);
|
||||
});
|
||||
|
||||
test("ClinePass registry entry uses OpenAI format with bearer apikey auth + Cline headers", () => {
|
||||
test("ClinePass registry entry is oauth-primary (dual-auth) with Cline headers", () => {
|
||||
const entry = providerRegistry.clinepass;
|
||||
assert.ok(entry, "providerRegistry.clinepass must be defined");
|
||||
assert.equal(entry.id, "clinepass");
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authType, "oauth");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.ok(entry.oauth, "must carry the Cline OAuth urls (sign-in path)");
|
||||
assert.equal(entry.baseUrl, "https://api.cline.bot/api/v1/chat/completions");
|
||||
assert.equal(entry.extraHeaders?.["HTTP-Referer"], "https://cline.bot");
|
||||
assert.equal(entry.extraHeaders?.["X-Title"], "Cline");
|
||||
@@ -115,3 +120,61 @@ test("parseUpstreamError unwraps clinepass envelope error without leaking a stac
|
||||
const body = buildErrorBody(502, parsed.message) as { error: { message: string } };
|
||||
assert.ok(!body.error.message.includes("at /"), "sanitized error must not include a stack frame");
|
||||
});
|
||||
|
||||
// ── Dual-auth: clinepass accepts BOTH an API key (#5942) AND OAuth login ─────
|
||||
test("ClinePass is also in the OAuth catalog (dual-auth: API-key + OAuth login)", () => {
|
||||
const cp = OAUTH_PROVIDERS.clinepass;
|
||||
assert.ok(cp, "OAUTH_PROVIDERS.clinepass must be defined for the OAuth login path");
|
||||
assert.equal(cp.id, "clinepass");
|
||||
assert.equal(cp.name, "ClinePass");
|
||||
});
|
||||
|
||||
test("ClinePass reuses the Cline WorkOS OAuth flow (clinepass -> cline)", () => {
|
||||
assert.ok(oauthFlows.clinepass, "clinepass must map to an OAuth flow");
|
||||
assert.equal(
|
||||
oauthFlows.clinepass,
|
||||
oauthFlows.cline,
|
||||
"clinepass must reuse the cline OAuth flow 1:1 (same api.cline.bot host/token)"
|
||||
);
|
||||
});
|
||||
|
||||
test("ClinePass is a single OAuth-primary provider (no duplicate catalog entry)", () => {
|
||||
assert.ok(OAUTH_PROVIDERS.clinepass, "OAuth catalog entry");
|
||||
assert.ok(!APIKEY_PROVIDERS.clinepass, "no duplicate APIKEY_PROVIDERS entry");
|
||||
});
|
||||
|
||||
// ── Dual-auth API-key admission (POST /api/providers gate) ───────────────────
|
||||
// clinepass is OAuth-primary (isOAuth=true → "Connect" opens the OAuth flow) but
|
||||
// ALSO accepts a pasted BYOK API key. The API-key path must pass the managed-
|
||||
// connection gate (isManagedProviderConnectionId) WITHOUT flipping isOAuth off.
|
||||
// That means admitting it through the dedicated DUAL_AUTH set, NOT through
|
||||
// FREE_APIKEY_PROVIDER_IDS (which would set providerSupportsPat=true → isOAuth=false
|
||||
// and break the primary Connect→OAuth routing). Regression guard for the layout.
|
||||
test("ClinePass API-key connections pass the managed gate while staying OAuth-primary", () => {
|
||||
assert.ok(
|
||||
isManagedProviderConnectionId("clinepass"),
|
||||
"POST /api/providers must accept a clinepass apikey connection (dual-auth BYOK path)"
|
||||
);
|
||||
assert.ok(
|
||||
!supportsApiKeyOnFreeProvider("clinepass"),
|
||||
"clinepass must NOT be in FREE_APIKEY_PROVIDER_IDS — that would flip isOAuth false"
|
||||
);
|
||||
});
|
||||
|
||||
// ── Catalog ↔ registry alias consistency (routing prefix) ───────────────────
|
||||
// The dashboard sends models as `<catalogAlias>/<modelId>` (e.g. "cp/cline-pass/glm-5.2").
|
||||
// Routing resolves that prefix via ALIAS_TO_PROVIDER_ID, which is built from the REGISTRY
|
||||
// alias (generateAliasMap). If the registry alias drifts from the catalog alias, the prefix
|
||||
// won't resolve → executor falls back to PROVIDERS.openai → requests hit api.openai.com
|
||||
// with the ClinePass key → a misleading OpenAI 401. cline keeps these in sync (both "cl");
|
||||
// clinepass must too. Regression guard for the cp/cline-pass/* OpenAI-401 incident.
|
||||
test("ClinePass registry alias matches the OAUTH_PROVIDERS catalog alias (routing prefix)", () => {
|
||||
const cp = OAUTH_PROVIDERS.clinepass;
|
||||
assert.ok(cp?.alias, "catalog alias must be defined");
|
||||
assert.equal(
|
||||
providerRegistry.clinepass.alias,
|
||||
cp.alias,
|
||||
"registry alias must equal catalog alias so <alias>/<model> resolves to clinepass"
|
||||
);
|
||||
assert.equal(providerRegistry.clinepass.alias, "cp");
|
||||
});
|
||||
|
||||
@@ -64,6 +64,7 @@ const EXPECTED_PROVIDER_KEYS = [
|
||||
"trae",
|
||||
"kilocode",
|
||||
"cline",
|
||||
"clinepass",
|
||||
"windsurf",
|
||||
"devin-cli",
|
||||
"grok-cli",
|
||||
@@ -87,6 +88,7 @@ const EXPECTED_CONFIG_BY_PROVIDER = {
|
||||
cursor: CURSOR_CONFIG,
|
||||
kilocode: KILOCODE_CONFIG,
|
||||
cline: CLINE_CONFIG,
|
||||
clinepass: CLINE_CONFIG, // reuses the Cline WorkOS flow (clinepass: cline in providers/index.ts)
|
||||
windsurf: WINDSURF_CONFIG,
|
||||
"devin-cli": WINDSURF_CONFIG,
|
||||
trae: TRAE_CONFIG,
|
||||
@@ -138,6 +140,7 @@ const REQUIRED_FIELDS_BY_PROVIDER = {
|
||||
cursor: ["apiEndpoint", "api3Endpoint", "agentEndpoint", "agentNonPrivacyEndpoint", "dbKeys"],
|
||||
kilocode: ["apiBaseUrl", "initiateUrl", "pollUrlBase"],
|
||||
cline: ["appBaseUrl", "apiBaseUrl", "authorizeUrl", "tokenExchangeUrl", "refreshUrl"],
|
||||
clinepass: ["appBaseUrl", "apiBaseUrl", "authorizeUrl", "tokenExchangeUrl", "refreshUrl"],
|
||||
windsurf: ["authorizeUrl", "apiServerUrl", "exchangePath", "inferenceUrl"],
|
||||
"devin-cli": ["authorizeUrl", "apiServerUrl", "exchangePath", "inferenceUrl"],
|
||||
trae: ["apiEndpoint", "chatEndpoint", "webUrl"],
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// Characterization of the providers.ts catalog split (god-file decomposition): the host became a
|
||||
// barrel that re-exports 10 data catalogs now living under constants/providers/*, and APIKEY is
|
||||
// merged from 6 semantic family files (apikey/<family>.ts). Locks: the public surface (every catalog
|
||||
// + helpers still exported), the spread-merge integrity (167 APIKEY entries, no loss/dup), and that
|
||||
// + helpers still exported), the spread-merge integrity (166 APIKEY entries, no loss/dup), and that
|
||||
// load-time Zod validation still runs. Pure-data move → behavior must be identical.
|
||||
// Count was 171 before obsolete provider removals (PR #6675: glhf/kluster/cablyai/inclusionai etc.).
|
||||
// Count was 171 before obsolete provider removals (PR #6675: glhf/kluster/cablyai/inclusionai etc.,
|
||||
// 171->167) plus #6126 (ClinePass dual-auth): the API-key-only APIKEY_PROVIDERS_GATEWAYS entry was
|
||||
// removed as a duplicate now that clinepass is OAuth-primary (OAUTH_PROVIDERS.clinepass) with its
|
||||
// BYOK path admitted through the DUAL_AUTH_APIKEY_PROVIDER_IDS gate instead (167->166).
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
@@ -32,12 +35,12 @@ test("barrel still exports every catalog + key helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("APIKEY_PROVIDERS merges the 6 family files into 167 entries (no loss / no dup)", async () => {
|
||||
test("APIKEY_PROVIDERS merges the 6 family files into 166 entries (no loss / no dup)", async () => {
|
||||
const keys = Object.keys((P as Record<string, object>).APIKEY_PROVIDERS);
|
||||
assert.equal(keys.length, 167);
|
||||
assert.equal(new Set(keys).size, 167, "duplicate keys after spread-merge");
|
||||
assert.equal(keys.length, 166);
|
||||
assert.equal(new Set(keys).size, 166, "duplicate keys after spread-merge");
|
||||
// the merged object's entry-count equals the sum of the 6 semantic family files; families are a
|
||||
// strict partition (every provider in exactly one), so the sum must be exactly 167.
|
||||
// strict partition (every provider in exactly one), so the sum must be exactly 166.
|
||||
const families: [string, string][] = [
|
||||
["gateways", "APIKEY_PROVIDERS_GATEWAYS"],
|
||||
["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"],
|
||||
@@ -57,7 +60,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 167 entries (no loss / no
|
||||
seen.add(k);
|
||||
}
|
||||
}
|
||||
assert.equal(famTotal, 167, "families must partition all 167 providers");
|
||||
assert.equal(famTotal, 166, "families must partition all 166 providers");
|
||||
});
|
||||
|
||||
test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {
|
||||
|
||||
Reference in New Issue
Block a user