mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
committed by
GitHub
parent
577bbf3e47
commit
287802cf86
@@ -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.
|
||||
|
||||
@@ -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 15–95% 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 15–95% 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)**.
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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 (15–95% 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 (15–95% 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.)
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -15,5 +15,6 @@ export const promptqlProvider: RegistryEntry = {
|
||||
models: PROMPTQL_FALLBACK_MODELS.map((m) => ({
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
...(m.supportsVision ? { supportsVision: true } : {}),
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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 (
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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/")));
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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)", () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
201
tests/unit/providers-page-utils-kimi.test.ts
Normal file
201
tests/unit/providers-page-utils-kimi.test.ts
Normal 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`
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -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`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user