fix: repair pre-existing red gates on the release/v3.8.49 tip (#8055)

* fix(dashboard): resolve Kimi banner casing collision + shrink frozen test file (release tip)

- Rename src/app/(dashboard)/dashboard/kimiSponsorBanner.ts to
  kimiSponsorBannerGate.ts so it no longer differs from
  KimiSponsorBanner.tsx only by the first letter's case (breaks next
  build on case-insensitive filesystems). Updates the sole importer
  (KimiSponsorBanner.tsx) and the two tests that reference it.
- Extract the 8 Kimi/Moonshot featured-ordering tests out of the
  frozen tests/unit/providers-page-utils.test.ts (grown 3 lines past
  its 1294 cap by #8039's rebrand-comment update) into a new sibling
  file tests/unit/providers-page-utils-kimi.test.ts. No assertions
  dropped; both files pass in full (24 + 8 = 32 tests).

* fix(sse): register PromptQlExecutor in the executor registry (release tip)

getExecutor("promptql") had no entry in open-sse/executors/index.ts, so it
silently fell through to DefaultExecutor's provider fallback, which issues a
raw fetch() and returns the bare upstream Response instead of the executor
wrapper shape {response, url, headers, transformedBody}. The real
PromptQlExecutor class (open-sse/executors/promptql.ts) already honors the
contract correctly — it was just never wired into the registry.

Fixes tests/unit/executor-web-cookie-sweep.test.ts "promptql executor
returns wrapper shape".

* fix(i18n): backfill 2220 missing pt-BR keys to restore en.json parity (release tip)

pt-BR.json fell behind after #7935 restored +2220 keys into en.json and
vi.json but left pt-BR.json unmodified. Translated all missing entries to
Brazilian Portuguese, preserving ICU/interpolation placeholders and existing
terminology, and merged them mirroring en.json's key order so the diff is
additions-only (the small comma-only deletions are pure JSON reformatting
from new sibling keys).

* fix(providers): repair 4 pre-existing catalog/registry reds on release tip

- providers-constants-split.test.ts: APIKEY_PROVIDERS grew 182->187 (PR #7887
  added 5 free-tier providers: ainative/aion/sealion/routeway/nara). Verified
  no dup/loss (6-family partition sums exactly to 187) and updated the stale
  expected count + comment trail to match.
- cline registry: added the missing minimax/minimax-m3 free OpenRouter entry
  (#3321) and fixed the neighbouring nemotron-3-ultra-550b-a55b entry, which
  carried a stray ":free" id suffix and an imprecise 1_000_000 contextLength
  instead of the 1_048_576 the test (and every sibling 1M-context entry in
  this catalog) expects.
- promptqlModels.ts / registry/promptql/index.ts: PROMPTQL_FALLBACK_MODELS's
  minimax-m3 entry was missing supportsVision, and the registry mapping
  dropped it entirely (only id/name were passed through) — it was the sole
  minimax-m3 entry across the whole registry not flagged multimodal, despite
  every other provider (minimax, minimax-cn, ollama-cloud, trae, bazaarlink,
  clinepass, codebuddy-cn, opencode-zen/go, synthetic, huggingchat, lmarena)
  agreeing MiniMax-M3 supports vision. Added the field to the PromptQlModel
  type and threaded it through.
- tests/snapshots/provider/translate-path.json: regenerated the golden via
  UPDATE_GOLDEN=1. Diffed old vs new — zero providers removed, 5 added
  (ainative/aion/nara/routeway/sealion, matching #7887), and the only
  changed entry (cline) reflects the already-merged #7914 ClinePass header
  protocol change (Cline/<version> User-Agent + X-Task-ID) that a prior
  narrow golden touch-up missed capturing.

* fix(docs): repair docs-sync/env-sync/repo-contract gates (release tip)

Six pre-existing reds on release/v3.8.49, all "repo drifted from its own
documented contract":

- check-docs-counts-sync: free-tier headline was stale (~1.4B/~2.0B) vs the
  live catalog (~1.53B steady / ~2.15B first month, 43 pools). Updated
  README.md and docs/reference/FREE_TIERS.md to the live numbers and added a
  v3.8.49 correction note explaining the pool-count delta (39->43, #7840).
  Also fixed a soft executors-count drift in ARCHITECTURE.md (84->86,
  268->271 providers) while touching that line.
- release-green-docs-drift-7253: docs/proxy-subscriptions.md referenced a
  fabricated migration filename (123_proxy_subscriptions.sql); the real file
  is 131_proxy_subscriptions.sql. Fixed all 3 occurrences.
- check-env-doc-sync + issue-7793-env-doc-sync-repro: OMNIROUTE_DATA_DIR
  (DATA_DIR fallback alias read by
  open-sse/executors/promptql/threadSticky.ts) was undocumented. Added to
  .env.example and docs/reference/ENVIRONMENT.md.
- check-db-rules: src/lib/db/proxySubscriptions.ts (#7299) is a db-internal
  split of proxies.ts (kept under the frozen file-size cap) whose one export
  is already re-exported via proxies.ts -> localDb.ts. Added it to
  INTENTIONALLY_INTERNAL with the same db-internal justification used for
  identical split modules (apiKeyColumnFallbacks, providerNodeSelect,
  webSessionDedup) rather than a redundant direct re-export from localDb.ts.
- mcp-server-hollow-dist-deps: the sanity test expected better-sqlite3 among
  the MCP bundle's static top-level external imports. That's been stale
  since the pre-#7878 migration to a cascading SqliteAdapter driver factory
  (createRequire()-based lazy require, not a static import); better-sqlite3
  already has its own native-asset copy guarantee in assembleStandalone.mjs,
  unrelated to this test's EXTRA_MODULE_ENTRIES concern. Updated the
  assertion to a still-genuinely-static external (zod) with a comment
  explaining the change.

No production runtime behavior changed — docs, .env.example, and a checker
allowlist/test-expectation only.

* fix(dashboard): repair stale UI component-shape test assertions (release tip)

Two pre-existing reds in the dashboard UI component-contract cluster were
caused by test assertions that had gone stale after intentional, correct
refactors — not by real defects in the components:

- quota-pool-wizard-multi.test.ts: the step-3 preview assertion required
  the literal single-line substring "connectionIds.map((cid)". Prettier
  (100-char width, project config) legitimately breaks the
  connectionIds.map(...).filter(...) chain across lines because of the
  multi-line callback body, so the literal never matches. PoolWizard.tsx
  still builds previewByProvider correctly by mapping over connectionIds;
  updated the assertion to a regex that tolerates the line break.

- v388-phase1-screen-fixes.test.ts: the shared Select placeholder-guard
  assertion required the literal "!children && placeholder". An earlier,
  intentional i18n commit changed the hardcoded "Select an option" default
  to a translated fallback (`placeholder ?? t("selectOption")`), which
  requires parens around the ?? expression for operator precedence. The
  guard behavior is unchanged (still gated on !children); updated the
  assertion to match the current, correct guard shape.

Both fixes are read-only test-file changes; no production behavior changed.

review-reviews-v3814-fixes.test.ts still has one pre-existing, unrelated
red (LEDGER-4: minimax-m3 registry entries missing supportsVision) that
requires editing the promptql provider registry/catalog — out of this
cluster's scope, left untouched and reported separately.

* fix(providers): reconcile cline catalog contradictions + deterministic golden (release tip)

The first tip-green pass introduced 3 regressions caught by CI on sibling guard tests:

- clinepass-provider + cline-catalog-models-3321 encoded OPPOSITE expectations of
  the same cline model list (minimax presence, nvidia :free suffix). Reference
  upstream (OpenRouter free lineup) confirms nvidia/nemotron-3-ultra-550b-a55b:free
  (with :free, 1M ctx) is correct, so restore that id and fix #3321's stale no-:free
  assertion; add minimax/minimax-m3 (the real #3321 gap) to clinepass-provider's list.
- check-db-rules-classification froze INTENTIONALLY_INTERNAL at 35; proxySubscriptions
  was the intentional 36th entry — add it + bump the count.
- provider-translate-path golden stored a LITERAL Cline/3.8.49: clineAuth resolves the
  version from APP_CONFIG.version (stable), but the golden sanitizer collapsed only
  process.env.npm_package_version (unset under `node`, set under `npm run`) — so the
  golden was shard-dependent. Resolve APP_VERSION from APP_CONFIG.version like clineAuth
  and regenerate; now Cline/<APP> normalizes identically in every shard.

* fix(services): type execFile signal/killed in classifyError + ratchet dashboard baseline (release tip)

Pre-existing base-red on the tip's Fast Quality Gates (dashboard-typecheck), missed
in the first inventory:

- src/lib/services/installers/utils.ts TS2339 — `err.signal` was read off a value typed
  as NodeJS.ErrnoException, which @types/node does not declare `signal`/`killed` on
  (those belong to execFile's ExecFileException). Widen classifyError's param to type
  both, and drop the now-redundant `(err as … { killed })` cast.
- Ratchet config/quality/dashboard-typecheck-baseline.json down: 5 baselined errors were
  fixed by already-merged PRs but never ratcheted (OAuthModal TS2769 4→3 / TS2345 4→3,
  CliproxyModelMappingEditor TS2339, CompressionPreviewAccordion TS4104, MonacoEditor
  TS2307). Baseline now 254, matching live — gate exits 0.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-21 21:25:00 -03:00
committed by GitHub
parent 577bbf3e47
commit 287802cf86
29 changed files with 3201 additions and 361 deletions

View File

@@ -40,6 +40,11 @@ INITIAL_PASSWORD=CHANGEME
# also if you want to share the same database as "npm run dev" use "./data"
# DATA_DIR=/var/lib/omniroute
# Fallback alias for DATA_DIR, checked only when DATA_DIR is unset.
# Used by: open-sse/executors/promptql/threadSticky.ts — locates the PromptQL
# executor's on-disk thread-sticky session cache. Leave unset to rely on DATA_DIR.
# OMNIROUTE_DATA_DIR=/var/lib/omniroute
# Encryption key for SQLite database encryption at rest.
# Used by: src/lib/db/encryption.ts — encrypts the entire SQLite database.
# Generate: openssl rand -hex 32 | Leave empty to disable DB encryption.

View File

@@ -6,19 +6,19 @@
# 🚀 OmniRoute — The Free AI Gateway
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 271 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 1595% tokens (~89% avg) — never hit limits. 271 AI providers · 90+ free tiers · ~1.4B free tokens/mo · 18 routing strategies · $0 to start."/>
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 271 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 1595% tokens (~89% avg) — never hit limits. 271 AI providers · 90+ free tiers · ~1.53B free tokens/mo · 18 routing strategies · $0 to start."/>
</div>
<div align="center">
# 💰 ~1.4B Free Tokens / Month
# 💰 ~1.53B Free Tokens / Month
</div>
> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute aggregates the **documented** free tiers of **39 provider pools / 460+ models** into one honest number and shows it live on the dashboard (`/dashboard/free-tiers`).
> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute aggregates the **documented** free tiers of **43 provider pools / 460+ models** into one honest number and shows it live on the dashboard (`/dashboard/free-tiers`).
<img src="./docs/diagrams/free-tier-budget.svg" width="100%" alt="OmniRoute free-tier budget card: ~1.4B free tokens per month steady, up to ~2.0B in the first month with signup credits, from the documented free tiers of 39 provider pools / 460+ models behind one endpoint. Honest pool-deduped math — each shared pool counted once (counting every rate limit 24/7 would read ~10B; not published), 15 providers ToS-flagged so you decide. Budget bar of the 19 countable free pools with per-model grid (Mistral Large 3 1B, GPT-4o mini 150M, Gemini 2.5 Flash 60M … Claude Sonnet 4.5 25K), ~626M one-time first-month signup credits (vertex 300M, agentrouter 200M, predibase 25M, together 25M, glm-cn 20M, doubao 15M, ai21 10M, longcat 10M, deepseek 5M, hyperbolic 5M, nscale 5M), plus permanently-free no-token-cap providers (SiliconFlow, Z.AI GLM-Flash, Kilo, OpenCode Zen, baidu …) and a $10 OpenRouter top-up unlocking +24M/mo — surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers."/>
<img src="./docs/diagrams/free-tier-budget.svg" width="100%" alt="OmniRoute free-tier budget card: ~1.53B free tokens per month steady, up to ~2.15B in the first month with signup credits, from the documented free tiers of 43 provider pools / 460+ models behind one endpoint. Honest pool-deduped math — each shared pool counted once (counting every rate limit 24/7 would read ~10B; not published), 15 providers ToS-flagged so you decide. Budget bar of the countable free pools with per-model grid (Mistral Large 3 1B, GPT-4o mini 150M, Gemini 2.5 Flash 60M … Claude Sonnet 4.5 25K), one-time first-month signup credits (vertex 300M, agentrouter 200M, predibase 25M, together 25M, glm-cn 20M, doubao 15M, ai21 10M, longcat 10M, deepseek 5M, hyperbolic 5M, nscale 5M), plus permanently-free no-token-cap providers (SiliconFlow, Z.AI GLM-Flash, Kilo, OpenCode Zen, baidu …) and a $10 OpenRouter top-up unlocking +24M/mo — surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers."/>
> Animated summary of the live `/dashboard/free-tiers` page. Full methodology (pool dedupe, credit tiers, provider terms): **[docs/reference/FREE_TIERS.md](docs/reference/FREE_TIERS.md)**.
>

View File

@@ -166,9 +166,6 @@
"src/app/(dashboard)/dashboard/providers/providerPageUtils.ts": {
"TS2345": 1
},
"src/app/(dashboard)/dashboard/providers/services/components/CliproxyModelMappingEditor.tsx": {
"TS2339": 1
},
"src/app/(dashboard)/dashboard/quota/page.tsx": {
"TS2339": 4
},
@@ -190,9 +187,6 @@
"src/app/(dashboard)/dashboard/translator/components/MonitorTab.tsx": {
"TS2345": 1
},
"src/app/(dashboard)/dashboard/translator/components/advanced/CompressionPreviewAccordion.tsx": {
"TS4104": 1
},
"src/app/(dashboard)/dashboard/translator/components/advanced/StreamTransformerAccordion.tsx": {
"TS2345": 1
},
@@ -218,12 +212,9 @@
"src/shared/components/Header.tsx": {
"TS2353": 1
},
"src/shared/components/MonacoEditor.tsx": {
"TS2307": 1
},
"src/shared/components/OAuthModal.tsx": {
"TS2769": 4,
"TS2345": 4
"TS2769": 3,
"TS2345": 3
},
"src/shared/components/SkillsConceptCard.tsx": {
"TS2503": 1

View File

@@ -17,7 +17,7 @@ It provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic acr
Core capabilities:
- OpenAI-compatible API surface for CLI/tools (268 providers, 84 executors)
- OpenAI-compatible API surface for CLI/tools (271 providers, 86 executors)
- Request/response translation across provider formats
- Model combo fallback (multi-model sequence)
- Structured combo steps (`provider + model + connection`) with runtime ordering by `compositeTiers`

View File

@@ -133,7 +133,7 @@ Added one column:
Existing rows on upgrade: `subscription_id = NULL`, behavior unchanged.
Migration: `ALTER TABLE proxy_registry ADD COLUMN subscription_id TEXT;`
(applied as `123_proxy_subscriptions.sql`, idempotent via the migration
(applied as `131_proxy_subscriptions.sql`, idempotent via the migration
runner's `ALTER` semantics).
### 5.3 Extended `proxy_subscriptions` test isolation
@@ -264,7 +264,7 @@ warning banner shows which protocols were skipped.
## 11. Migration & rollout
1. New migration `123_proxy_subscriptions.sql` runs on first DB open after
1. New migration `131_proxy_subscriptions.sql` runs on first DB open after
upgrade (auto-discovered by the existing migration runner).
2. The migration is **idempotent**: `ALTER TABLE … ADD COLUMN …` against an
already-migrated DB is a no-op in SQLite when wrapped in the runner's
@@ -349,7 +349,7 @@ node --import tsx/esm \
- `src/lib/proxySubscription/parse.ts`
- `src/lib/proxySubscription/subscriptionService.ts`
- `src/lib/proxySubscription/index.ts`
- `src/lib/db/migrations/123_proxy_subscriptions.sql`
- `src/lib/db/migrations/131_proxy_subscriptions.sql`
- `src/app/api/v1/management/proxy-subscriptions/route.ts`
- `src/app/api/v1/management/proxy-subscriptions/[id]/route.ts`
- `src/app/api/v1/management/proxy-subscriptions/[id]/refresh/route.ts`

View File

@@ -82,6 +82,7 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari
| Variable | Default | Source File | Description |
| -------------------------------------- | -------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DATA_DIR` | `~/.omniroute/` | `src/lib/db/core.ts` | Root directory for SQLite DB, backups, and data files. Override for Docker volumes or custom paths. |
| `OMNIROUTE_DATA_DIR` | _(unset)_ | `open-sse/executors/promptql/threadSticky.ts` | **Fallback alias** for `DATA_DIR`, checked only when `DATA_DIR` is unset. Used to locate the PromptQL executor's on-disk thread-sticky session cache (`<dir>/promptql-thread-sessions.json`); if neither var is set, the cache stays in-memory only (not persisted across restarts). |
| `STORAGE_ENCRYPTION_KEY` | _(empty = disabled)_ | `src/lib/db/encryption.ts` | AES key for full SQLite database encryption at rest. Generate with `openssl rand -hex 32`. |
| `STORAGE_ENCRYPTION_KEY_VERSION` | `v1` | `scripts/build/bootstrap-env.mjs`, `electron/main.js` | Version label for the encryption key. Increment when performing key rotation to support decryption of old backups. |
| `DISABLE_SQLITE_AUTO_BACKUP` | `false` | `src/lib/db/backup.ts` | When `true`, skips the automatic database backup that runs before migrations on every startup. |

View File

@@ -15,17 +15,19 @@ lastUpdated: 2026-06-28
| Metric | Tokens / month | Meaning |
| ------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Documented recurring grant (steady)** | **~1.37B** | Free-tier **pools** (per-model catalog), each shared pool counted **once**. The live source behind `/api/free-tier/summary` and the dashboard's Free-Tier Budget page. **Use this number.** |
| **+ first month with signup credits** | **~2.00B** | Steady + one-time signup credits (Together $25, Z.AI 20M, DeepSeek 5M, …), deduped per account. **First month only** — does not recur. |
| **Documented recurring grant (steady)** | **~1.53B** | Free-tier **pools** (per-model catalog), each shared pool counted **once**. The live source behind `/api/free-tier/summary` and the dashboard's Free-Tier Budget page. **Use this number.** |
| **+ first month with signup credits** | **~2.15B** | Steady + one-time signup credits (Together $25, Z.AI 20M, DeepSeek 5M, …), deduped per account. **First month only** — does not recur. |
| **+ permanently free, no published cap** | _un-quantifiable_ | `siliconflow`, `glm-cn` (GLM-4-Flash), `tencent`, `baidu`, `kilo-gateway`, `opencode-zen` — real recurring access, rate/concurrency-limited, **no token cap to count**. Listed, never summed (counting them at `RPM×24/7` is the inflation we reject). |
| **+ deposit-unlock boost** | **+~24M** | A one-time **$10** OpenRouter top-up raises its free pool from 50 → 1000 req/day. Reported separately so it never inflates the steady number. |
| Theoretical ceiling (all rate limits, 24/7) | ~10B | Sum of every provider rate limit extrapolated to non-stop use. **Not a guarantee** — do not headline this. |
**Honest headline:** _OmniRoute aggregates **~1.37B documented free tokens per month** (up to ~2.0B in your first month with signup credits) across 39 free-tier pools — plus a long tail of permanently-free, no-cap providers — and RTK + Caveman compression (1595% token savings) stretches that further._
**Honest headline:** _OmniRoute aggregates **~1.53B documented free tokens per month** (up to ~2.15B in your first month with signup credits) across 43 free-tier pools — plus a long tail of permanently-free, no-cap providers — and RTK + Caveman compression (1595% token savings) stretches that further._
> **Why this dropped from the previous ~1.94B.** The 2026-06-17 refresh is an honesty correction, not a loss: `gemini` is now pool-deduped (was inflated by counting each Flash variant separately, 462M → 60M), `cloudflare-ai` corrected to its real 10k-Neurons/day (122M → 30M), `doubao` reclassified as a one-time signup credit (not recurring), and shut-down tiers removed (`github-models` closed to new signups, `chutes`/`phind`/`kluster` discontinued). Partly offset by `llm7` (correct 5M/day → 150M) and new free providers (Kilo, OpenCode Zen, Z.AI GLM-Flash).
>
> **Further corrected to ~1.37B in v3.8.42:** `longcat` was reclassified from a 150M/mo recurring grant to a one-time 10M signup credit after its free preview ended. Same honesty rule — no provider was dropped by mistake.
>
> **Updated to ~1.53B in v3.8.49:** the pool count grew from 39 to 43 after mapping free tiers that were documented upstream but missing from the catalog (`requesty`, `ovhcloud`, `agnes`, `glm`) plus new providers `navy` and `aihorde` (#7840). This is the live, CI-gated number (`check:docs-counts` fails the build if this drifts from `computeFreeModelTotals()`).
Biggest **documented** contributors: `mistral` 1.00B, `llm7` 150M, `groq` 117M, `gemini` 60M, `cerebras` 30M, `cloudflare-ai` 30M, `sambanova` 30M. (`longcat` is excluded — its 10M LongCat-2.0 grant is a one-time, KYC-gated signup credit, not a recurring monthly budget.)

View File

@@ -140,6 +140,16 @@ export const clineProvider: RegistryEntry = {
maxInputTokens: 1000000,
maxOutputTokens: 65536,
},
{
id: "minimax/minimax-m3",
name: "MiniMax M3 (Free)",
toolCalling: true,
supportsReasoning: true,
supportsVision: true,
contextLength: 1048576,
maxInputTokens: 1048576,
maxOutputTokens: 65536,
},
],
passthroughModels: true,
};

View File

@@ -15,5 +15,6 @@ export const promptqlProvider: RegistryEntry = {
models: PROMPTQL_FALLBACK_MODELS.map((m) => ({
id: m.id,
name: m.name,
...(m.supportsVision ? { supportsVision: true } : {}),
})),
};

View File

@@ -64,6 +64,7 @@ import { GrokCliExecutor } from "./grok-cli.ts";
import { CodeBuddyCnExecutor } from "./codebuddy-cn.ts";
import { ZenmuxFreeExecutor } from "./zenmux-free.ts";
import { XaiExecutor } from "./xai.ts";
import { PromptQlExecutor } from "./promptql.ts";
const executors = {
antigravity: new AntigravityExecutor(),
@@ -151,6 +152,8 @@ const executors = {
ven: new VeniceWebExecutor(), // Alias
"notion-web": new NotionWebExecutor(),
nw: new NotionWebExecutor(), // Alias
promptql: new PromptQlExecutor(),
pql: new PromptQlExecutor(), // Alias
"v0-vercel-web": new V0VercelWebExecutor(),
v0: new V0VercelWebExecutor(), // Alias
"kimi-web": new KimiWebExecutor(),
@@ -269,3 +272,4 @@ export { CodeBuddyCnExecutor } from "./codebuddy-cn.ts";
export { ZenmuxFreeExecutor } from "./zenmux-free.ts";
export { XaiExecutor } from "./xai.ts";
export { MoonshotExecutor } from "./moonshot.ts";
export { PromptQlExecutor } from "./promptql.ts";

View File

@@ -14,6 +14,8 @@ export interface PromptQlModel {
configId?: string;
/** Upstream model id string from PromptQL. */
modelId?: string;
/** Whether the underlying model accepts image inputs (matches upstream capability). */
supportsVision?: boolean;
}
/** Offline seed when discovery fails (from live FetchLlmConfigs capture). */
@@ -107,6 +109,7 @@ export const PROMPTQL_FALLBACK_MODELS: PromptQlModel[] = [
name: "Minimax M3",
configId: "placeholder-minimax-m3",
modelId: "accounts/fireworks/models/minimax-m3",
supportsVision: true,
},
];

View File

@@ -65,6 +65,7 @@ export const INTENTIONALLY_INTERNAL = new Set([
"providerNodeSelect", // db-internal: importado só por db/providers.ts (selectProviderNodeForConnection — lógica pura de seleção de provider node split do providers.ts, #4421)
"providerStats", // intentionally-internal: src/app/api/provider-stats/route.ts
"proxyLatency", // intentionally-internal: imported directly by src/lib/db/proxies.ts (anti-barrel, #6798)
"proxySubscriptions", // db-internal: importado só por db/proxies.ts (addProxiesToScopePool — split do proxies.ts para ficar sob o cap de tamanho congelado, #7299); a função já é re-exportada por proxies.ts (que localDb.ts re-exporta)
"recovery", // intentionally-internal: bin/cli/runtime.mjs (import() dinâmico) + tests
"schemaColumns", // db-internal: importado só por db/core.ts (ensureProviderConnections/UsageHistory/CallLogsColumns + hasColumn/hasTable/getTableColumns — schema-column reconciliation split do core.ts, #4948)
"secrets", // intentionally-internal: src/instrumentation-node.ts (import() dinâmico na inicialização)

View File

@@ -4,7 +4,7 @@ import { useState } from "react";
import { useTranslations } from "next-intl";
import ProviderIcon from "@/shared/components/ProviderIcon";
import { APP_CONFIG } from "@/shared/constants/appConfig";
import { shouldShowKimiSponsorBanner } from "./kimiSponsorBanner";
import { shouldShowKimiSponsorBanner } from "./kimiSponsorBannerGate";
// Official Kimi partnership tracking link — keep in sync with README.md's
// Sponsors section and the aff links wired in the providers onboarding UI
@@ -28,7 +28,7 @@ function isNotDismissed(): boolean {
* Dismissable banner announcing the Kimi (Moonshot AI) official OmniRoute
* partnership on the dashboard home page. Self-contained: reads the app's own
* version (APP_CONFIG.version) to decide whether it is still inside the
* agreed display window (see kimiSponsorBanner.ts) and persists dismissal via
* agreed display window (see kimiSponsorBannerGate.ts) and persists dismissal via
* localStorage, mirroring RiskNoticeBanner's lazy-useState pattern. The
* logomark reuses <ProviderIcon providerId="moonshot" .../> so it stays
* theme-aware for free via the THEMED_SVGS wiring in ProviderIcon.tsx.

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,15 @@ export class InstallError extends Error {
/** Classify raw npm/OS errors into user-friendly messages. */
function classifyError(
err: NodeJS.ErrnoException & { stdout?: string; stderr?: string }
// execFile's callback error is an ExecFileException — it carries `signal`/`killed`
// (a timed-out/terminated child) on top of ErrnoException, which @types/node's
// ErrnoException itself does not declare. Widen the param so both are typed.
err: NodeJS.ErrnoException & {
stdout?: string;
stderr?: string;
signal?: NodeJS.Signals | null;
killed?: boolean;
}
): InstallError {
const raw = sanitizeErrorMessage(err.message);
const stderr = err.stderr ?? "";
@@ -50,11 +58,7 @@ function classifyError(
if (err.code === "ENOSPC" || stderr.includes("ENOSPC")) {
return new InstallError(raw, "Espaço em disco insuficiente.", 507);
}
if (
err.signal === "SIGTERM" ||
err.code === "ETIMEDOUT" ||
(err as Error & { killed?: boolean }).killed
) {
if (err.signal === "SIGTERM" || err.code === "ETIMEDOUT" || err.killed) {
return new InstallError(raw, "Instalação demorou demais. Tente novamente.", 504);
}
if (

View File

@@ -206,6 +206,52 @@
"stream": "https://api.aimlapi.com/v1/chat/completions"
}
},
"ainative": {
"format": "openai",
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"oauth": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
}
},
"url": {
"nonStream": "https://api.ainative.studio/api/v1/chat/completions",
"stream": "https://api.ainative.studio/api/v1/chat/completions"
}
},
"aion": {
"format": "openai",
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"oauth": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
}
},
"url": {
"nonStream": "https://api.aionlabs.ai/v1/chat/completions",
"stream": "https://api.aionlabs.ai/v1/chat/completions"
}
},
"alibaba": {
"format": "openai",
"headers": {
@@ -869,26 +915,28 @@
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json",
"HTTP-Referer": "https://cline.bot",
"User-Agent": "OmniRoute/<APP>",
"User-Agent": "Cline/<APP>",
"X-CLIENT-TYPE": "omniroute",
"X-CLIENT-VERSION": "<APP>",
"X-CORE-VERSION": "<APP>",
"X-IS-MULTIROOT": "false",
"X-PLATFORM": "<PLATFORM>",
"X-PLATFORM-VERSION": "<NODE>",
"X-Task-ID": "<UUID>",
"X-Title": "Cline"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json",
"HTTP-Referer": "https://cline.bot",
"User-Agent": "OmniRoute/<APP>",
"User-Agent": "Cline/<APP>",
"X-CLIENT-TYPE": "omniroute",
"X-CLIENT-VERSION": "<APP>",
"X-CORE-VERSION": "<APP>",
"X-IS-MULTIROOT": "false",
"X-PLATFORM": "<PLATFORM>",
"X-PLATFORM-VERSION": "<NODE>",
"X-Task-ID": "<UUID>",
"X-Title": "Cline"
},
"oauth": {
@@ -896,13 +944,14 @@
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json",
"HTTP-Referer": "https://cline.bot",
"User-Agent": "OmniRoute/<APP>",
"User-Agent": "Cline/<APP>",
"X-CLIENT-TYPE": "omniroute",
"X-CLIENT-VERSION": "<APP>",
"X-CORE-VERSION": "<APP>",
"X-IS-MULTIROOT": "false",
"X-PLATFORM": "<PLATFORM>",
"X-PLATFORM-VERSION": "<NODE>",
"X-Task-ID": "<UUID>",
"X-Title": "Cline"
}
},
@@ -3218,6 +3267,29 @@
"stream": "https://nano-gpt.com/api/v1/chat/completions"
}
},
"nara": {
"format": "openai",
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"oauth": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
}
},
"url": {
"nonStream": "https://router.bynara.id/v1/chat/completions",
"stream": "https://router.bynara.id/v1/chat/completions"
}
},
"navy": {
"format": "openai",
"headers": {
@@ -4041,6 +4113,32 @@
"stream": "https://router.requesty.ai/v1/chat/completions"
}
},
"routeway": {
"format": "openai",
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 OmniRoute/1.0"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 OmniRoute/1.0"
},
"oauth": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 OmniRoute/1.0"
}
},
"url": {
"nonStream": "https://api.routeway.ai/v1/chat/completions",
"stream": "https://api.routeway.ai/v1/chat/completions"
}
},
"sambanova": {
"format": "openai",
"headers": {
@@ -4087,6 +4185,29 @@
"stream": "https://api.scaleway.ai/v1/chat/completions"
}
},
"sealion": {
"format": "openai",
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"oauth": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
}
},
"url": {
"nonStream": "https://api.sea-lion.ai/v1/chat/completions",
"stream": "https://api.sea-lion.ai/v1/chat/completions"
}
},
"sensenova": {
"format": "openai",
"headers": {

View File

@@ -121,7 +121,7 @@ test("INTENTIONALLY_INTERNAL is exported from check-db-rules.mjs", () => {
assert.ok(INTENTIONALLY_INTERNAL.size > 0, "INTENTIONALLY_INTERNAL must not be empty");
});
test("INTENTIONALLY_INTERNAL contains the expected 35 audited modules", () => {
test("INTENTIONALLY_INTERNAL contains the expected 36 audited modules", () => {
const expected = [
"_rowTypes",
"accessTokens",
@@ -149,6 +149,7 @@ test("INTENTIONALLY_INTERNAL contains the expected 35 audited modules", () => {
"providerNodeSelect",
"providerStats",
"proxyLatency",
"proxySubscriptions",
"recovery",
"schemaColumns",
"secrets",

View File

@@ -16,7 +16,9 @@ test("#3321: Cline catalog exposes the verified OpenRouter free additions", () =
assert.ok(minimax, "cline must expose minimax/minimax-m3");
assert.equal(minimax.contextLength, 1048576);
const nemotron = byId.get("nvidia/nemotron-3-ultra-550b-a55b");
assert.ok(nemotron, "cline must expose nvidia/nemotron-3-ultra-550b-a55b");
assert.equal(nemotron.contextLength, 1048576);
// Upstream OpenRouter id carries the `:free` variant suffix (confirmed against the
// real OpenRouter free lineup) — 1M context, not 1048576.
const nemotron = byId.get("nvidia/nemotron-3-ultra-550b-a55b:free");
assert.ok(nemotron, "cline must expose nvidia/nemotron-3-ultra-550b-a55b:free");
assert.equal(nemotron.contextLength, 1000000);
});

View File

@@ -92,6 +92,7 @@ test("Cline fallback owns recommended/free models and excludes the ClinePass nam
"poolside/laguna-m.1:free",
"google/gemma-4-31b-it:free",
"nvidia/nemotron-3-ultra-550b-a55b:free",
"minimax/minimax-m3",
]);
assert.ok(ids.every((id: string) => !id.startsWith("cline-pass/")));
});

View File

@@ -1,10 +1,10 @@
// Kimi (Moonshot AI) sponsor banner — version-gate pure logic.
// See src/app/(dashboard)/dashboard/kimiSponsorBanner.ts.
// See src/app/(dashboard)/dashboard/kimiSponsorBannerGate.ts.
import test from "node:test";
import assert from "node:assert/strict";
const kimiSponsorBanner = await import(
"../../src/app/(dashboard)/dashboard/kimiSponsorBanner.ts"
"../../src/app/(dashboard)/dashboard/kimiSponsorBannerGate.ts"
);
test("KIMI_SPONSOR_BANNER_THROUGH_VERSION is the agreed sunset version", () => {

View File

@@ -99,7 +99,20 @@ function explicitlyGuaranteedPackages(): Set<string> {
test("sanity: MCP server bundle probe finds real external packages (parser didn't break)", () => {
const pkgs = mcpBundleStaticExternalImports();
assert.ok(pkgs.length > 5, `expected several external packages, got: ${pkgs.join(", ")}`);
assert.ok(pkgs.includes("better-sqlite3"), `missing better-sqlite3: ${pkgs.join(", ")}`);
// better-sqlite3 is intentionally NOT expected here (base-red audit, v3.8.49): since
// `feat(db): migrate core.ts to SqliteAdapter multi-driver factory` (71452e040, predates
// #7878's Bun-runtime driver) it is loaded lazily via `createRequire()(...)` in
// src/lib/db/adapters/driverFactory.ts (cascading better-sqlite3 -> node:sqlite ->
// sql.js, and now bun:sqlite first under Bun) instead of a static top-level `import`, so
// esbuild's `--packages=external` never emits a static `import "better-sqlite3"` line for
// it — this probe (which only parses static top-level imports) correctly stops seeing it.
// That's not a packaging regression: the native addon has its own, stronger copy
// guarantee in NATIVE_ASSET_ENTRIES (scripts/build/assembleStandalone.mjs — "better-sqlite3
// native binary", node_modules/better-sqlite3/build), independent of the
// EXTRA_MODULE_ENTRIES mechanism this file's #7701 regression guard protects for pure-JS
// static externals like `undici`. Assert on `zod` instead — a real, static, top-level
// external import of the MCP server's tool-schema layer that isn't going away.
assert.ok(pkgs.includes("zod"), `missing zod: ${pkgs.join(", ")}`);
});
test("undici (a static top-level external import of the real MCP server bundle) has an explicit dist/node_modules copy guarantee (#7701)", () => {

View File

@@ -5,6 +5,7 @@ import os from "node:os";
import path from "node:path";
import { PROVIDERS } from "../../open-sse/config/constants.ts";
import { APP_CONFIG } from "../../src/shared/constants/appConfig.ts";
import {
buildProviderHeaders,
buildProviderUrl,
@@ -33,13 +34,19 @@ const OAUTH_CRED = { accessToken: "tok-test-ACCESS", providerSpecificData: {} };
// (process.versions.node) forms are collapsed to <NODE>.
const NODE_VERSION = typeof process !== "undefined" ? process.version : "";
const NODE_VERSION_BARE = typeof process !== "undefined" ? (process.versions?.node ?? "") : "";
// The OmniRoute app version also leaks into headers (cline X-CLIENT-VERSION /
// X-CORE-VERSION = clineAuth APP_VERSION = process.env.npm_package_version ||
// "0.0.0"). It is "0.0.0" under a direct `node` run (Unit Tests shard) but the real
// package version under `npx`/`npm run` (Coverage shard), so it must be normalized
// too — mirror clineAuth's resolution and collapse it to <APP>.
// The OmniRoute app version leaks into headers (cline User-Agent `Cline/<ver>`,
// X-CLIENT-VERSION, X-CORE-VERSION — all clineAuth's APP_VERSION). clineAuth resolves
// it from APP_CONFIG.version (the package.json version, stable in every shard), NOT from
// process.env.npm_package_version (which is unset under a direct `node` run — Unit Tests
// shard — but the real version under `npx`/`npm run` — Coverage shard). Resolving it the
// SAME way clineAuth does keeps the golden runner-independent; the npm_package_version
// fallback below stays as a defensive second collapse. Both are normalized to <APP>.
const APP_VERSION =
(typeof process !== "undefined" ? process.env.npm_package_version : "") || "0.0.0";
APP_CONFIG.version ||
(typeof process !== "undefined" ? process.env.npm_package_version : "") ||
"0.0.0";
const APP_VERSION_ENV =
(typeof process !== "undefined" ? process.env.npm_package_version : "") || "";
function sanitize(headers: Record<string, unknown>): Record<string, unknown> {
const out: Record<string, unknown> = {};
@@ -76,6 +83,8 @@ function sanitize(headers: Record<string, unknown>): Record<string, unknown> {
if (NODE_VERSION) s = s.split(NODE_VERSION).join("<NODE>");
if (NODE_VERSION_BARE) s = s.split(NODE_VERSION_BARE).join("<NODE>");
if (APP_VERSION) s = s.split(APP_VERSION).join("<APP>");
if (APP_VERSION_ENV && APP_VERSION_ENV !== APP_VERSION)
s = s.split(APP_VERSION_ENV).join("<APP>");
out[k] = s;
}
return out;

View File

@@ -1,7 +1,7 @@
// 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 (180 APIKEY entries, no loss/dup), and that
// + helpers still exported), the spread-merge integrity (187 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.,
// 171->167) plus #6126 (ClinePass dual-auth): the API-key-only APIKEY_PROVIDERS_GATEWAYS entry was
@@ -10,8 +10,9 @@
// OpenVecta inference-gateway addition brought it back to 167, then #7246 (Chenzk API gateway)
// brought it to 168, then more additions brought it to 172, then #6650 (g4f.space no-key gateway:
// 5 new sub-path entries — g4f-groq/g4f-gemini/g4f-pollinations/g4f-ollama/g4f-nvidia) brought it
// to 177, then 2 more provider additions in the v3.8.49 cycle brought it to 179, and the free-catalog
// expansion (#7840, navy) to 180.
// to 177, then 2 more provider additions in the v3.8.49 cycle brought it to 179, the free-catalog
// expansion (#7840, navy) to 180, the Alibaba/Qwen Cloud regional additions (#7882) to 182, and
// #7887 (5 free-tier providers: ainative/aion/sealion/routeway/nara) to 187.
import { test } from "node:test";
import assert from "node:assert/strict";
@@ -40,12 +41,12 @@ test("barrel still exports every catalog + key helpers", () => {
}
});
test("APIKEY_PROVIDERS merges the 6 family files into 182 entries (no loss / no dup)", async () => {
test("APIKEY_PROVIDERS merges the 6 family files into 187 entries (no loss / no dup)", async () => {
const keys = Object.keys((P as Record<string, object>).APIKEY_PROVIDERS);
assert.equal(keys.length, 182);
assert.equal(new Set(keys).size, 182, "duplicate keys after spread-merge");
assert.equal(keys.length, 187);
assert.equal(new Set(keys).size, 187, "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 182.
// strict partition (every provider in exactly one), so the sum must be exactly 187.
const families: [string, string][] = [
["gateways", "APIKEY_PROVIDERS_GATEWAYS"],
["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"],
@@ -65,7 +66,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 182 entries (no loss / no
seen.add(k);
}
}
assert.equal(famTotal, 182, "families must partition all 182 providers");
assert.equal(famTotal, 187, "families must partition all 187 providers");
});
test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {

View File

@@ -0,0 +1,201 @@
// Kimi (Moonshot AI) featured-first ordering — split out of
// providers-page-utils.test.ts (2026-07-21) to keep that frozen file under
// its size cap; same providerPageUtils/featuredProviders module under test,
// no assertions dropped or weakened in the split.
import test from "node:test";
import assert from "node:assert/strict";
const providerPageUtils =
await import("../../src/app/(dashboard)/dashboard/providers/providerPageUtils.ts");
const providers = await import("../../src/shared/constants/providers.ts");
const featuredProviders =
await import("../../src/app/(dashboard)/dashboard/providers/featuredProviders.ts");
// ── Kimi (Moonshot AI) official-partnership featured-first ordering (2026-07) ──
// UI-only pin: Kimi-family providers must render first within whichever
// category/group they appear in on the providers dashboard. This must never
// touch routing/fallback order (open-sse/config/providerRegistry.ts) — only how
// filterConfiguredProviderEntries sorts a category's card grid.
test("featuredProviders identifies every Kimi/Moonshot dashboard provider id", () => {
const { isFeaturedProviderId, isKimiPartnerProviderId, KIMI_BRAND_COLOR } = featuredProviders;
for (const id of ["kimi", "kimi-coding", "kimi-coding-apikey", "kimi-web", "moonshot"]) {
assert.equal(isFeaturedProviderId(id), true, `${id} should be featured`);
assert.equal(isKimiPartnerProviderId(id), true, `${id} should be a Kimi partner id`);
}
// Unrelated providers must not be swept in.
for (const id of ["openai", "claude", "moonshot-labs", "kimichat", null, undefined, ""]) {
assert.equal(isFeaturedProviderId(id), false, `${id} should not be featured`);
assert.equal(isKimiPartnerProviderId(id), false, `${id} should not be a Kimi partner id`);
}
assert.equal(KIMI_BRAND_COLOR, "#1783FF");
});
test("sortProviderEntriesFeaturedFirst pins Kimi providers first, alphabetical otherwise", () => {
const entry = (providerId: string, name: string) => ({
providerId,
provider: { id: providerId, name },
stats: { total: 0 },
displayAuthType: "apikey",
toggleAuthType: "apikey",
});
// Deliberately alphabetically-earlier non-Kimi providers ("Acme", "Anthropic")
// so the assertion actually proves the pin overrides pure alphabetical order,
// not merely that Kimi happens to sort first on its own.
const entries = [
entry("zulu-provider", "Zulu Provider"),
entry("moonshot", "Kimi"),
entry("acme", "Acme"),
entry("kimi-web", "Kimi Web"),
entry("anthropic-clone", "Anthropic Clone"),
entry("kimi-coding", "Kimi Code CLI"),
];
const sorted = providerPageUtils.sortProviderEntriesFeaturedFirst(entries);
// "Kimi" (moonshot's rebranded display name) alphabetically precedes "Kimi
// Code CLI" and "Kimi Web" — a shorter string that is a prefix of a longer
// one sorts first — so moonshot leads the featured group.
assert.deepEqual(
sorted.map((e) => e.providerId),
["moonshot", "kimi-coding", "kimi-web", "acme", "anthropic-clone", "zulu-provider"],
"featured (Kimi) entries come first, each group alphabetical among itself"
);
});
test("filterConfiguredProviderEntries surfaces Kimi first within a mixed category (oauth section shape)", () => {
const entries = [
{ providerId: "claude", provider: { name: "Claude" }, stats: { total: 1 }, displayAuthType: "oauth", toggleAuthType: "oauth" },
{ providerId: "kimi-coding", provider: { name: "Kimi Code CLI" }, stats: { total: 0 }, displayAuthType: "oauth", toggleAuthType: "oauth" },
{ providerId: "amazon-q", provider: { name: "Amazon Q" }, stats: { total: 0 }, displayAuthType: "oauth", toggleAuthType: "oauth" },
];
// No filters applied (showConfiguredOnly=false) — pure ordering behavior.
const visible = providerPageUtils.filterConfiguredProviderEntries(entries, false);
assert.deepEqual(
visible.map((e) => e.providerId),
["kimi-coding", "amazon-q", "claude"],
"kimi-coding is pinned first even though 'Amazon Q' and 'Claude' sort earlier alphabetically"
);
});
test("sortProviderEntriesFeaturedFirst leaves a category with no featured providers alphabetical", () => {
const entry = (providerId: string, name: string) => ({
providerId,
provider: { name },
stats: { total: 0 },
displayAuthType: "apikey",
toggleAuthType: "apikey",
});
const entries = [entry("zulu", "Zulu"), entry("acme", "Acme"), entry("mid", "Mid")];
const sorted = providerPageUtils.sortProviderEntriesFeaturedFirst(entries);
assert.deepEqual(
sorted.map((e) => e.providerId),
["acme", "mid", "zulu"]
);
});
// ── Section-scoped proof against the REAL catalog (not synthetic mocks) ───────
// page.tsx builds each dashboard section as
// buildStaticProviderEntries(category) -> filterConfiguredProviderEntries(...).
// These tests replicate that exact call chain per real section to prove where
// each real Kimi/Moonshot card actually lands — the 3 sections that render a
// Kimi-family card today: OAuth (kimi-coding), Web Cookie (kimi-web), and the
// "LLM providers" subsection of API Key (moonshot). kimi-coding-apikey and kimi
// are both hiddenFromDashboard and never render their own card in ANY section
// (kimi-coding-apikey folds into the kimi-coding card's own connection flow —
// see KimiCodeAuthMethodModal.tsx; verified below).
test("real OAuth section pins kimi-coding first (page.tsx's oauthProviderEntries shape)", () => {
const getProviderStats = () => ({ total: 0 });
const oauthEntriesAll = providerPageUtils.buildStaticProviderEntries("oauth", getProviderStats);
const oauthEntries = providerPageUtils.filterConfiguredProviderEntries(oauthEntriesAll, false);
assert.ok(oauthEntries.length > 5, "sanity: the real OAuth section has many providers");
assert.equal(
oauthEntries[0].providerId,
"kimi-coding",
"kimi-coding (Kimi Code CLI) must be the first card in the real OAuth section"
);
});
test("real Web Cookie section pins kimi-web first (page.tsx's webCookieProviderEntries shape)", () => {
const getProviderStats = () => ({ total: 0 });
const webCookieEntriesAll = providerPageUtils.buildStaticProviderEntries(
"web-cookie",
getProviderStats
);
const webCookieEntries = providerPageUtils.filterConfiguredProviderEntries(
webCookieEntriesAll,
false
);
assert.ok(webCookieEntries.length > 5, "sanity: the real Web Cookie section has many providers");
assert.equal(
webCookieEntries[0].providerId,
"kimi-web",
"kimi-web (Kimi Web) must be the first card in the real Web Cookie section"
);
});
test("real API Key -> LLM subsection pins moonshot first (page.tsx's llmProviderEntries shape)", () => {
const getProviderStats = () => ({ total: 0 });
const apiKeyEntriesAll = providerPageUtils.buildStaticProviderEntries("apikey", getProviderStats);
// Mirrors page.tsx's llmProviderEntriesAll filter exactly (moonshot is not an
// image/aggregator/enterprise-cloud/video/embedding-rerank provider).
const llmEntriesAll = apiKeyEntriesAll.filter(
(entry) =>
!providers.IMAGE_ONLY_PROVIDER_IDS.has(entry.providerId) &&
!providers.AGGREGATOR_PROVIDER_IDS.has(entry.providerId) &&
!providers.ENTERPRISE_CLOUD_PROVIDER_IDS.has(entry.providerId) &&
!providers.VIDEO_PROVIDER_IDS.has(entry.providerId) &&
!providers.EMBEDDING_RERANK_PROVIDER_IDS.has(entry.providerId)
);
const llmEntries = providerPageUtils.filterConfiguredProviderEntries(llmEntriesAll, false);
assert.ok(llmEntries.length > 5, "sanity: the real API Key -> LLM subsection has many providers");
assert.equal(
llmEntries[0].providerId,
"moonshot",
"moonshot (displayed as 'Kimi', where kimi-k3 lives) must be the first card in the real API Key -> LLM subsection"
);
// kimi-coding-apikey and kimi (both hiddenFromDashboard) never surface as
// their own card here or in any other section — see the dedicated test below.
assert.equal(llmEntries.some((e) => e.providerId === "kimi-coding-apikey"), false);
assert.equal(llmEntries.some((e) => e.providerId === "kimi"), false);
});
test("kimi-coding-apikey and kimi never render as their own dashboard card in ANY section (hiddenFromDashboard)", () => {
const getProviderStats = () => ({ total: 0 });
const categories = [
"no-auth",
"oauth",
"web-cookie",
"local",
"search",
"audio",
"upstream-proxy",
"apikey",
"cloud-agent",
] as const;
for (const category of categories) {
const entries = providerPageUtils.buildStaticProviderEntries(category, getProviderStats);
assert.equal(
entries.some((e) => e.providerId === "kimi-coding-apikey"),
false,
`kimi-coding-apikey must not appear as its own card in the "${category}" category`
);
assert.equal(
entries.some((e) => e.providerId === "kimi"),
false,
`kimi (legacy alias) must not appear as its own card in the "${category}" category`
);
}
});

View File

@@ -7,8 +7,6 @@ const providerPageStorage =
await import("../../src/app/(dashboard)/dashboard/providers/providerPageStorage.ts");
const providers = await import("../../src/shared/constants/providers.ts");
const providerCatalog = await import("../../src/lib/providers/catalog.ts");
const featuredProviders =
await import("../../src/app/(dashboard)/dashboard/providers/featuredProviders.ts");
test("merged OAuth providers keep free-tier providers in the OAuth section", () => {
const statsCalls = [];
@@ -1105,192 +1103,3 @@ test("connectionMatchesProviderCard counts a dual-auth provider's PAT (apikey) c
assert.equal(connectionMatchesProviderCard(null, "qoder", "oauth"), false);
assert.equal(connectionMatchesProviderCard(undefined, "qoder", "oauth"), false);
});
// ── Kimi (Moonshot AI) official-partnership featured-first ordering (2026-07) ──
// UI-only pin: Kimi-family providers must render first within whichever
// category/group they appear in on the providers dashboard. This must never
// touch routing/fallback order (open-sse/config/providerRegistry.ts) — only how
// filterConfiguredProviderEntries sorts a category's card grid.
test("featuredProviders identifies every Kimi/Moonshot dashboard provider id", () => {
const { isFeaturedProviderId, isKimiPartnerProviderId, KIMI_BRAND_COLOR } = featuredProviders;
for (const id of ["kimi", "kimi-coding", "kimi-coding-apikey", "kimi-web", "moonshot"]) {
assert.equal(isFeaturedProviderId(id), true, `${id} should be featured`);
assert.equal(isKimiPartnerProviderId(id), true, `${id} should be a Kimi partner id`);
}
// Unrelated providers must not be swept in.
for (const id of ["openai", "claude", "moonshot-labs", "kimichat", null, undefined, ""]) {
assert.equal(isFeaturedProviderId(id), false, `${id} should not be featured`);
assert.equal(isKimiPartnerProviderId(id), false, `${id} should not be a Kimi partner id`);
}
assert.equal(KIMI_BRAND_COLOR, "#1783FF");
});
test("sortProviderEntriesFeaturedFirst pins Kimi providers first, alphabetical otherwise", () => {
const entry = (providerId: string, name: string) => ({
providerId,
provider: { id: providerId, name },
stats: { total: 0 },
displayAuthType: "apikey",
toggleAuthType: "apikey",
});
// Deliberately alphabetically-earlier non-Kimi providers ("Acme", "Anthropic")
// so the assertion actually proves the pin overrides pure alphabetical order,
// not merely that Kimi happens to sort first on its own.
const entries = [
entry("zulu-provider", "Zulu Provider"),
entry("moonshot", "Kimi"),
entry("acme", "Acme"),
entry("kimi-web", "Kimi Web"),
entry("anthropic-clone", "Anthropic Clone"),
entry("kimi-coding", "Kimi Code CLI"),
];
const sorted = providerPageUtils.sortProviderEntriesFeaturedFirst(entries);
// "Kimi" (moonshot's rebranded display name) alphabetically precedes "Kimi
// Code CLI" and "Kimi Web" — a shorter string that is a prefix of a longer
// one sorts first — so moonshot leads the featured group.
assert.deepEqual(
sorted.map((e) => e.providerId),
["moonshot", "kimi-coding", "kimi-web", "acme", "anthropic-clone", "zulu-provider"],
"featured (Kimi) entries come first, each group alphabetical among itself"
);
});
test("filterConfiguredProviderEntries surfaces Kimi first within a mixed category (oauth section shape)", () => {
const entries = [
{ providerId: "claude", provider: { name: "Claude" }, stats: { total: 1 }, displayAuthType: "oauth", toggleAuthType: "oauth" },
{ providerId: "kimi-coding", provider: { name: "Kimi Code CLI" }, stats: { total: 0 }, displayAuthType: "oauth", toggleAuthType: "oauth" },
{ providerId: "amazon-q", provider: { name: "Amazon Q" }, stats: { total: 0 }, displayAuthType: "oauth", toggleAuthType: "oauth" },
];
// No filters applied (showConfiguredOnly=false) — pure ordering behavior.
const visible = providerPageUtils.filterConfiguredProviderEntries(entries, false);
assert.deepEqual(
visible.map((e) => e.providerId),
["kimi-coding", "amazon-q", "claude"],
"kimi-coding is pinned first even though 'Amazon Q' and 'Claude' sort earlier alphabetically"
);
});
test("sortProviderEntriesFeaturedFirst leaves a category with no featured providers alphabetical", () => {
const entry = (providerId: string, name: string) => ({
providerId,
provider: { name },
stats: { total: 0 },
displayAuthType: "apikey",
toggleAuthType: "apikey",
});
const entries = [entry("zulu", "Zulu"), entry("acme", "Acme"), entry("mid", "Mid")];
const sorted = providerPageUtils.sortProviderEntriesFeaturedFirst(entries);
assert.deepEqual(
sorted.map((e) => e.providerId),
["acme", "mid", "zulu"]
);
});
// ── Section-scoped proof against the REAL catalog (not synthetic mocks) ───────
// page.tsx builds each dashboard section as
// buildStaticProviderEntries(category) -> filterConfiguredProviderEntries(...).
// These tests replicate that exact call chain per real section to prove where
// each real Kimi/Moonshot card actually lands — the 3 sections that render a
// Kimi-family card today: OAuth (kimi-coding), Web Cookie (kimi-web), and the
// "LLM providers" subsection of API Key (moonshot). kimi-coding-apikey and kimi
// are both hiddenFromDashboard and never render their own card in ANY section
// (kimi-coding-apikey folds into the kimi-coding card's own connection flow —
// see KimiCodeAuthMethodModal.tsx; verified below).
test("real OAuth section pins kimi-coding first (page.tsx's oauthProviderEntries shape)", () => {
const getProviderStats = () => ({ total: 0 });
const oauthEntriesAll = providerPageUtils.buildStaticProviderEntries("oauth", getProviderStats);
const oauthEntries = providerPageUtils.filterConfiguredProviderEntries(oauthEntriesAll, false);
assert.ok(oauthEntries.length > 5, "sanity: the real OAuth section has many providers");
assert.equal(
oauthEntries[0].providerId,
"kimi-coding",
"kimi-coding (Kimi Code CLI) must be the first card in the real OAuth section"
);
});
test("real Web Cookie section pins kimi-web first (page.tsx's webCookieProviderEntries shape)", () => {
const getProviderStats = () => ({ total: 0 });
const webCookieEntriesAll = providerPageUtils.buildStaticProviderEntries(
"web-cookie",
getProviderStats
);
const webCookieEntries = providerPageUtils.filterConfiguredProviderEntries(
webCookieEntriesAll,
false
);
assert.ok(webCookieEntries.length > 5, "sanity: the real Web Cookie section has many providers");
assert.equal(
webCookieEntries[0].providerId,
"kimi-web",
"kimi-web (Kimi Web) must be the first card in the real Web Cookie section"
);
});
test("real API Key -> LLM subsection pins moonshot first (page.tsx's llmProviderEntries shape)", () => {
const getProviderStats = () => ({ total: 0 });
const apiKeyEntriesAll = providerPageUtils.buildStaticProviderEntries("apikey", getProviderStats);
// Mirrors page.tsx's llmProviderEntriesAll filter exactly (moonshot is not an
// image/aggregator/enterprise-cloud/video/embedding-rerank provider).
const llmEntriesAll = apiKeyEntriesAll.filter(
(entry) =>
!providers.IMAGE_ONLY_PROVIDER_IDS.has(entry.providerId) &&
!providers.AGGREGATOR_PROVIDER_IDS.has(entry.providerId) &&
!providers.ENTERPRISE_CLOUD_PROVIDER_IDS.has(entry.providerId) &&
!providers.VIDEO_PROVIDER_IDS.has(entry.providerId) &&
!providers.EMBEDDING_RERANK_PROVIDER_IDS.has(entry.providerId)
);
const llmEntries = providerPageUtils.filterConfiguredProviderEntries(llmEntriesAll, false);
assert.ok(llmEntries.length > 5, "sanity: the real API Key -> LLM subsection has many providers");
assert.equal(
llmEntries[0].providerId,
"moonshot",
"moonshot (displayed as 'Kimi', where kimi-k3 lives) must be the first card in the real API Key -> LLM subsection"
);
// kimi-coding-apikey and kimi (both hiddenFromDashboard) never surface as
// their own card here or in any other section — see the dedicated test below.
assert.equal(llmEntries.some((e) => e.providerId === "kimi-coding-apikey"), false);
assert.equal(llmEntries.some((e) => e.providerId === "kimi"), false);
});
test("kimi-coding-apikey and kimi never render as their own dashboard card in ANY section (hiddenFromDashboard)", () => {
const getProviderStats = () => ({ total: 0 });
const categories = [
"no-auth",
"oauth",
"web-cookie",
"local",
"search",
"audio",
"upstream-proxy",
"apikey",
"cloud-agent",
] as const;
for (const category of categories) {
const entries = providerPageUtils.buildStaticProviderEntries(category, getProviderStats);
assert.equal(
entries.some((e) => e.providerId === "kimi-coding-apikey"),
false,
`kimi-coding-apikey must not appear as its own card in the "${category}" category`
);
assert.equal(
entries.some((e) => e.providerId === "kimi"),
false,
`kimi (legacy alias) must not appear as its own card in the "${category}" category`
);
}
});

View File

@@ -141,8 +141,13 @@ test("PoolWizard.tsx: step-3 preview maps over connectionIds (previewByProvider)
wizardSrc.includes("previewByProvider"),
"Expected previewByProvider useMemo in PoolWizard"
);
assert.ok(
wizardSrc.includes("connectionIds.map((cid)"),
// Prettier (100-char width, project config) breaks a `connectionIds.map(...).filter(...)`
// chain with a multi-line callback onto separate lines — `connectionIds` and `.map((cid)`
// land on different lines. Match across that line break instead of a rigid single-line
// substring so this assertion tracks the chain regardless of how Prettier wraps it.
assert.match(
wizardSrc,
/connectionIds\s*\.map\(\(cid\)/,
"Expected connectionIds.map to build per-provider preview"
);
});

View File

@@ -2,7 +2,7 @@
/**
* KimiSponsorBanner (2026-07 partnership) — render gate (version window +
* localStorage dismissal), CTA aff link, and discreet partner-link note.
* See src/app/(dashboard)/dashboard/kimiSponsorBanner.ts for the version-gate
* See src/app/(dashboard)/dashboard/kimiSponsorBannerGate.ts for the version-gate
* pure logic (covered separately by tests/unit/kimi-sponsor-banner-version-gate.test.ts).
*/
import React from "react";

View File

@@ -21,7 +21,14 @@ test("memory: tabs ordered memories -> engine -> playground", () => {
test("shared Select: renders children and guards placeholder/options when children present", () => {
const src = read("src/shared/components/Select.tsx");
assert.ok(src.includes("{children}"), "renders children passed by callers");
assert.ok(src.includes("!children && placeholder"), "placeholder guarded by !children");
// The i18n fallback (`placeholder ?? t("selectOption")`, added so an unset placeholder still
// shows a translated default) needs parens around the `??` expression for operator precedence,
// so the guard is now `!children && (placeholder ?? ...)` rather than the older bare
// `!children && placeholder`. The guard itself is unchanged — still gated on `!children`.
assert.ok(
src.includes("!children && (placeholder"),
"placeholder guarded by !children"
);
assert.ok(src.includes("!children &&\n options.map") || src.includes("!children &&"), "options guarded by !children");
});