Merge remote-tracking branch 'origin/release/v3.8.43' into fix/port-2267-kiro-claude-sonnet-5

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-01 20:46:43 -03:00
38 changed files with 1109 additions and 351 deletions

View File

@@ -624,7 +624,7 @@ jobs:
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
test-vitest:
name: Vitest (MCP / autoCombo / UI components)
@@ -676,7 +676,7 @@ jobs:
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
node-26-compat-build:
name: Node 26 Compatibility Build
@@ -729,7 +729,7 @@ jobs:
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
test-coverage-shard:
name: Coverage Shard (${{ matrix.shard }}/8)
@@ -770,7 +770,7 @@ jobs:
--exclude=tests/** \
--exclude=**/*.test.* \
node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 \
--test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
--test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
- name: Upload raw shard coverage
if: always()
uses: actions/upload-artifact@v7

View File

@@ -335,7 +335,7 @@ Result: 4 layers of fallback = zero downtime
- **🗜️ Compression hardening** — a default-on **inflation guard** (discard the stacked result and send the verbatim original whenever compression would _grow_ the prompt), completed **Caveman rule packs** for German / French / Japanese (dedup + ultra) plus a new **Chinese (文言 / wényán) input pack** with zh-vs-ja auto-detection, and **RTK filters for Gradle & .NET (`dotnet`)** build output. → [Compression](docs/compression/COMPRESSION_ENGINES.md)
- **💸 Honest flat-rate cost** — subscription / coding-plan providers (ChatGPT Web, grok-web, the Minimax / Kimi / GLM / Alibaba Coding plans, Xiaomi MiMo…) now read **$0** in cost analytics instead of an inflated per-token estimate, while budget / quota / routing keep estimating unchanged. → [API Reference](docs/reference/API_REFERENCE.md)
- **⚖️ Quota-Share routing** — a dedicated combo strategy that spreads load across accounts by _available quota_: Deficit-Round-Robin scheduling, per-connection `max_concurrent` with cooldown-wait queueing, multi-window usage buckets (5h / 7d / per-model), per-(key,model) caps, session stickiness for prompt-cache integrity, and proactive saturation from upstream token-usage headers. → [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md)
- **⚖️ Quota-Share routing** — a dedicated combo strategy that spreads load across accounts by _available quota_: Deficit-Round-Robin scheduling, per-connection `max_concurrent` with cooldown-wait queueing, multi-window usage buckets (5h / 7d / per-model), per-(key, model) caps, session stickiness for prompt-cache integrity, and proactive saturation from upstream token-usage headers. → [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md)
- **🤖 One-command CLI/agent setup** — a dedicated `setup-*` command configures each coding tool to route through OmniRoute (Claude Code, Codex, Cline, Continue, Cursor, Roo Code, Kilo Code, Crush, Goose, Qwen Code, Aider, OpenCode); `omniroute launch` / `omniroute launch-codex` are zero-config launchers. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
- **🛰️ Remote mode** — drive a remote OmniRoute from any machine with scoped access tokens (`omniroute connect` / `omniroute contexts` / `omniroute tokens`), plus an `omniroute login antigravity` helper that runs Google "native/desktop" OAuth on your own machine and pastes a credential blob into a remote/VPS install (where the loopback redirect is unreachable). → [Remote Mode](docs/guides/REMOTE-MODE.md)
- **🧭 Smarter auto-routing** — OpenRouter-style `auto/<category>:<tier>` combos (e.g. `auto/coding:fast`, `auto/reasoning:pro`), a **Fusion** strategy (fan out to a panel of models in parallel, then synthesize via a judge), **task-aware routing** (best-fit connection per task type), per-request `X-Route-Model` override, live Arena-ELO + models.dev model intelligence, per-step account allowlists, provider-wildcard combo steps, nested combo-ref execution, sticky weighted selection, and `web_search`-aware routing. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
@@ -344,7 +344,7 @@ Result: 4 layers of fallback = zero downtime
- **💸 Cost telemetry everywhere** — `X-OmniRoute-*` cost/usage headers on every endpoint (including media), a non-token cost engine, a cache-HIT `X-OmniRoute-Cost-Saved` header, and per-key USD spend quotas. → [API Reference](docs/reference/API_REFERENCE.md)
- **🧠 Memory you control** — opt-in int8 vector quantization (Qdrant + sqlite-vec), memory off by default, and a per-request `x-omniroute-no-memory` header. → [Memory](docs/frameworks/MEMORY.md)
- **🛡️ Security** — a prompt-injection guard across every LLM route (backed by a red-team suite), plus a free DuckDuckGo last-resort web search. → [Guardrails](docs/security/GUARDRAILS.md)
- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 237-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech / transcription / music / video), and one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`). → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 237-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech/transcription/music/video), and one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`). → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **⚡ Local performance & infra** — a one-click local Redis launcher (`omniroute redis up`, plus a dashboard Redis panel), one-click **Cloudflare Workers** and **Deno Deploy** relay deployers wired into the proxy pool, and an optional Bifrost Go sidecar that offloads the hottest relay path (`BIFROST_BASE_URL`, with automatic fallback to the TypeScript path on timeout) — now with a relay-backend selector (`OMNIROUTE_RELAY_BACKEND=ts|bifrost|auto`) so the `/v1/relay` endpoint stays the stable surface while choosing the fastest backend internally. → [Environment](docs/reference/ENVIRONMENT.md)
<br/>
@@ -555,7 +555,7 @@ claude mcp add-server omniroute --type http --url http://localhost:20128/api/mcp
</div>
> **Why use many token when few token do trick?** Every request passes through OmniRoute's compression pipeline **transparently** — no client changes. It's now a **stack of 10 composable engines** that run in order and mix & match per routing combo — building on ideas from [RTK](https://github.com/rtk-ai/rtk), [Caveman](https://github.com/JuliusBrussee/caveman) (⭐ 78K+), [LLMLingua-2](https://github.com/microsoft/LLMLingua), and [Troglodita](https://github.com/leninejunior/troglodita) (PT-BR).
> **Why use many tokens when few tokens do the trick?** Every request passes through OmniRoute's compression pipeline **transparently** — no client changes. It's now a **stack of 10 composable engines** that run in order and mix & match per routing combo — building on ideas from [RTK](https://github.com/rtk-ai/rtk), [Caveman](https://github.com/JuliusBrussee/caveman) (⭐ 78K+), [LLMLingua-2](https://github.com/microsoft/LLMLingua), and [Troglodita](https://github.com/leninejunior/troglodita) (PT-BR).
### 🧱 The 10-engine stack

View File

@@ -1,6 +1,7 @@
{
"_comment": "Catraca de complexidade (check-complexity.mjs, ESLint core rules complexity>=15 e max-lines-per-function>80 sobre src+open-sse+electron+bin via eslint.complexity.config.mjs). Conta total de violacoes; so pode cair. --update ratcheta.",
"count": 1981,
"count": 1982,
"_rebaseline_2026_07_01_v3843_release": "1981->1982 (+1). v3.8.43 cycle drift, surfaced after check:mutation-test-coverage was fixed (it was masked behind that earlier step in the Fast Quality Gates chain). 1982 = the value measured by check:complexity on BOTH fce85136c (release tip) and 6d7060e21 (release + the 5 CI fixes) — identical, so all +1 is inherited cycle drift; the fixes touch only test files + linkify.ts safeHttpHref (cyclomatic ~4, well under the >=15 threshold, 0 new violations) + config JSON. Tighten via --update next cycle.",
"_rebaseline_2026_06_28_v3840_5237_reconcile": "1980->1981 (+1). Inherited release/v3.8.40 drift surfaced while merging PR #5237 (impersonation-UA refresh) — the +1 is present on the pristine release tip (d8a392a47) WITHOUT #5237's changes, so it is #5222 (antigravity fallback-LRU retry) / #5221 (command-code) growth that merged via --admin without ratcheting complexity (the PR->release fast-gates do not run check:complexity). #5237 itself is complexity-net-zero: its only edits are a single UA constant, a regenerated golden snapshot, and baseline JSONs. Structural reduction tracked in #3501.",
"_rebaseline_2026_06_27_v3838_release": "1978->1980 (+2). v3.8.38 cycle-close drift surfaced by the release-green pre-flight (check:complexity does NOT run on PR->release fast-gates). +2 from late-cycle feature/fix merges (compression fidelity-gate steps #5143, SSE hardening). Release-finalize working tree touches ONLY CHANGELOG.md + i18n mirrors + the 2 baseline JSONs — 0 production-code change. Structural reduction tracked in #3501.",
"_rebaseline_2026_06_26_v3838_ownerprs_batch": "1972->1978 (+6). Drift do lote de merges de PRs do dono + contribuidores em release/v3.8.38 (sessao /review-prs): #4845 (antigravity convertGeminiToOpenAI), #5105 (executor zenmux-free), #5020 (executor grok-cli), #4940 (usage dedupe guard), #5093 (resilience: quota cutoff/gemini mime/model-lockout cooldown), #5015 (quota hydration + auto-combo scoping). Cada um e crescimento de feature/fix legitimo recem-TDD'd, nao regressao; o gate check:complexity NAO roda no fast-path PR->release, entao o ramo acumula sem rebaselinar (mesma familia dos rebaselines anteriores). #5121 cookie-dedup foi mantido complexity-NEUTRO via extracao do helper findExistingCookieConnection. Reducao estrutural fica como debt (#3501).",

View File

@@ -158,14 +158,14 @@
"open-sse/mcp-server/tools/advancedTools.ts": 1118,
"_rebaseline_2026_06_27_5193_antigravity_basered": "Base-red (pre-existing release drift, fast-gate PR->release skips check:file-size): accountFallback.ts 1773->1777 and src/app/api/providers/[id]/test/route.ts 924->940 were already over their frozen caps on release/v3.8.39 independent of any antigravity change. Owner chose to rebaseline (keep the documented issue-reference comments #1846/#1449/#347 etc.) rather than accept the contributor comment-stripping in #5200/#5198. Reverted #5200 to restore the comments; bumped these two frozen caps to the actual base sizes. No logic change.",
"open-sse/services/accountFallback.ts": 1783,
"open-sse/services/batchProcessor.ts": 828,
"open-sse/services/batchProcessor.ts": 915,
"open-sse/services/browserBackedChat.ts": 850,
"open-sse/services/claudeCodeCompatible.ts": 1202,
"_rebaseline_pr4592_exclude_exhausted_auto": "Reconcile #4592 already-merged growth: combo.ts 2991->3036 (+45, terminal-status quota-cutoff exclusion in buildAutoCandidates + opt-in gate). Fast-gate PR->release does not run check:file-size.",
"_rebaseline_2026_06_24_headroom_strategy": "Headroom-aware connection selection (dario technique): combo.ts 3168->3180 (+12 = a new `else if (strategy === \"headroom\")` dispatch branch in handleComboChat that delegates to orderTargetsByHeadroom + its log line, plus the import). The actual logic lives OUT of the god-file: the pure ranker rankByHeadroom/computeHeadroom is the new leaf open-sse/services/combo/headroomRanking.ts (91 LOC, <cap) and the async orderer orderTargetsByHeadroom is appended to the existing open-sse/services/combo/quotaStrategies.ts (<cap) next to its sibling reset-aware/reset-window orderers (reuses their connection-expansion machinery). headroom = 1 - max(util_5h, util_7d) from getSaturation (src/lib/quota/saturationSignals.ts), prefers the connection with the most free capacity. Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors the reset-aware/reset-window/context-optimized branches); not extractable without hiding the call site. fill-first stays default; all existing strategies untouched. Covered by tests/unit/combo-headroom-ranking.test.ts (pure helper) + tests/unit/combo-headroom-strategy.test.ts (orderer, saturation injected). Structural shrink of combo.ts tracked in #3501.",
"_rebaseline_2026_06_24_quota_share_strategy": "Dedicated quota-share strategy (Phase 3 #9): combo.ts 3180->3190 (+10 = one new `else if (strategy === \"quota-share\")` dispatch branch in handleComboChat that delegates 100% to selectQuotaShareTarget + its log line, plus the import). All the new logic lives OUT of the god-file in two new leaves under open-sse/services/combo/: quotaShareInflight.ts (in-flight counter with TTL/lease, ~150 LOC <cap) and quotaShareStrategy.ts (per-model bucket gating via isBucketSaturated + DRR proportional to weight + P2C over in-flight, ~240 LOC <cap). Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors the headroom/reset-aware/reset-window/context-optimized branches); not extractable without hiding the call site. ZERO existing strategy cases were modified — only this branch was added, and the qtSd/ combos switched from fill-first to quota-share in src/lib/quota/quotaCombos.ts. Covered by tests/unit/quota-share-strategy.test.ts (gating, DRR fairness, P2C in-flight, fail-open, activation). Structural shrink of combo.ts tracked in #3501.",
"_rebaseline_2026_06_24_task_aware_routing": "Task-aware routing strategy (port PR #2045, OmniRoute #4945): combo.ts 3190->3225 (+35) = one new `else if (strategy === \"task-aware\")` dispatch branch delegating 100% to selectTaskAwareTarget + its imports/log lines. All scoring/classification logic lives OUT of the god-file in the new leaf open-sse/services/taskAwareRouting.ts (553 LOC <cap). Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors quota-share/headroom/reset-aware branches). ZERO existing strategy cases modified. Covered by tests/unit/combo-task-aware.test.ts (35 tests). Structural shrink of combo.ts tracked in #3501.",
"open-sse/services/combo.ts": 3368,
"open-sse/services/combo.ts": 3387,
"_rebaseline_2026_06_26_fidelity_gate_extraction": "Milestone-B fidelity-gate wiring residual: bodyToText+gateAdvance extracted to fidelityGateStep.ts (889->854, -35), but the StackOptions.fidelityGate field, the `const fidelityGate` reads at the two stacked-loop dispatch chokepoints, and the import of FidelityGateConfig are irreducible wiring that cannot leave strategySelector without an architectural refactor of the pre-existing stacked pipeline. Net: 889->854 (+6 vs the pre-Milestone-B frozen 848). Covered by tests/unit/compression/*.test.ts (940 pass).",
"_rebaseline_2026_06_28_5243_risk_gate_prepass": "PR #5243 (compression risk-gate pre-pass) own growth: open-sse/services/compression/strategySelector.ts 854->899 (+45). The three exported entry points (applyCompression/applyStackedCompression/applyStackedCompressionAsync) become thin wrappers over pure-extracted private bodies (runCompression/runStackedCompression/runStackedCompressionAsync) so the risk-gate mask->run->restore wrapper sits strictly OUTSIDE the per-step loop — a single universal integration point. The wrapper logic itself (resolveRiskGate/withRiskGate) lives in the new riskGate/strategyWrap.ts (<cap); the residual growth is the duplicated thin-wrapper signatures + the extracted bodies' dispatch boundary, guarded by a byte-identical parity test (riskGateIntegration). Default off (DEFAULT_COMPRESSION_CONFIG unchanged). Not extractable without hiding the dispatch boundary, mirroring prior compression rebaselines. Structural shrink tracked in #3501.",
"_rebaseline_2026_06_29_5286_memoization": "PR #5286 own growth: strategySelector.ts 899->960 (+61 = the opt-in result-memoization branches in applyCompression/applyCompressionAsync — principal+determinism gate, makeMemoKey lookup/store with model+supportsVision folded into the key, recompute-with-memo-off). Default off (memoizeCompressionResults), so zero behavior change. The memo helpers live in the leaf resultMemo.ts (<cap); the chokepoint wiring here is not extractable. Structural shrink of this hot-path file tracked in #3501.",
@@ -181,12 +181,12 @@
"open-sse/utils/stream.ts": 2710,
"src/app/(dashboard)/dashboard/HomePageClient.tsx": 1385,
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1028,
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 2983,
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 3017,
"src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx": 1105,
"src/app/(dashboard)/dashboard/cache/page.tsx": 845,
"src/app/(dashboard)/dashboard/cli-code/components/CodexToolCard.tsx": 900,
"src/app/(dashboard)/dashboard/cloud-agents/page.tsx": 922,
"src/app/(dashboard)/dashboard/combos/page.tsx": 4594,
"src/app/(dashboard)/dashboard/combos/page.tsx": 4608,
"src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": 1495,
"src/app/(dashboard)/dashboard/costs/quota-share/components/PoolWizard.tsx": 1007,
"src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2612,
@@ -194,12 +194,12 @@
"src/app/(dashboard)/dashboard/playground/components/tabs/ApiTab.tsx": 847,
"src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": 784,
"src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx": 942,
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 868,
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 869,
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1204,
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 954,
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderModels.ts": 155,
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderSettings.ts": 264,
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 974,
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 996,
"src/app/(dashboard)/dashboard/providers/components/onboarding/ProviderOnboardingWizard.tsx": 906,
"src/app/(dashboard)/dashboard/providers/page.tsx": 1927,
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1201,
@@ -249,15 +249,17 @@
"src/shared/services/cliRuntime.ts": 1090,
"src/shared/validation/schemas.ts": 2523,
"_rebaseline_2026_06_28_5275_correlation_id_extract": "Extraction of the safe CorrelationId subset of #5275 (hartmark) — request correlation id stored in call_logs (migration 109) and returned via the X-Correlation-Id response header, WITHOUT the combo/resilience or build/lazy-loading changes (those stay in #5275). Own growth: callLogs.ts 975->985 (correlation_id column on CallLogSummaryRow + read/map), usageHistory.ts 983->988 (correlationId metadata normalize), chat.ts 1575->1632 (withCorrelationId response wiring + combo-failure log carrying correlationId), chatHelpers.ts new 811 (withCorrelationId helper + reqId threading; was 791<cap pre-feature). Cohesive request/logging chokepoint wiring; structural shrink of chat.ts tracked in #3501.",
"src/sse/handlers/chat.ts": 1635,
"src/sse/handlers/chat.ts": 1647,
"src/sse/handlers/chatHelpers.ts": 842,
"src/sse/services/auth.ts": 2401,
"src/sse/services/auth.ts": 2403,
"open-sse/executors/default.ts": 877,
"open-sse/translator/request/openai-responses.ts": 902,
"open-sse/executors/kiro.ts": 944,
"open-sse/translator/request/openai-to-claude.ts": 823,
"tests/unit/account-fallback-service.test.ts": 1572,
"tests/unit/provider-validation-specialty.test.ts": 2843
"tests/unit/provider-validation-specialty.test.ts": 2843,
"open-sse/executors/huggingchat.ts": 813,
"_rebaseline_2026_07_01_v3843_release_5609": "Rebaseline v3.8.43 (PR #5609 release reconciliation). DRIFT dos 109 commits do ciclo: 8 god-files existentes cresceram (ApiManagerPageClient 2983->3017, combos/page 4594->4608, AddApiKeyModal 868->869, providerPageHelpers 974->996, chat.ts 1635->1647, auth.ts 2401->2403, batchProcessor 828->915, combo.ts 3368->3387) + 2 novos acima do cap (huggingchat.ts 813, tests web-cookie-providers-new 827) + 4 test files cresceram. Modularizacao deferida (blast-radius mid-release); congelado no estado atual p/ o proximo ciclo ratchetar daqui."
},
"testCap": 800,
"testFrozen": {
@@ -266,11 +268,11 @@
"tests/integration/skills-pipeline.test.ts": 918,
"tests/unit/account-fallback-service.test.ts": 1572,
"tests/unit/arena-elo-sync.test.ts": 830,
"tests/unit/batch_api.test.ts": 1303,
"tests/unit/batch_api.test.ts": 1324,
"tests/unit/cc-compatible-provider.test.ts": 1179,
"tests/unit/chatcore-sanitization.test.ts": 829,
"tests/unit/chatcore-translation-paths.test.ts": 2810,
"tests/unit/chatgpt-web.test.ts": 3159,
"tests/unit/chatgpt-web.test.ts": 3170,
"tests/unit/combo-routing-engine.test.ts": 3213,
"tests/unit/combo-strategy-fallbacks.test.ts": 880,
"tests/unit/db-core-init.test.ts": 877,
@@ -289,8 +291,8 @@
"_rebaseline_2026_06_27_5193_antigravity_test": "#5193 own test growth: oauth-providers-config.test.ts 870->873 (+3: antigravity projectId assertion + 50ms tick for the now fire-and-forget onboarding, matching the no-PKCE/no-openid flow).",
"tests/unit/oauth-providers-config.test.ts": 873,
"tests/unit/perplexity-web.test.ts": 959,
"tests/unit/provider-models-route.test.ts": 1618,
"tests/unit/provider-validation-specialty.test.ts": 2843,
"tests/unit/provider-models-route.test.ts": 1628,
"tests/unit/provider-validation-specialty.test.ts": 2874,
"_rebaseline_pr4613_compatible_provider_groups": "Reconcile #4613 already-merged growth: providers-page-utils.test.ts 1004->1052 (+48, buildCompatibleProviderGroups partition unit test). Fast-gate PR->release does not run check:file-size, so this surfaced post-merge.",
"tests/unit/providers-page-utils.test.ts": 1052,
"tests/unit/reasoning-cache.test.ts": 980,
@@ -307,7 +309,8 @@
"tests/unit/translator-resp-gemini-to-openai.test.ts": 1234,
"tests/unit/usage-service-hardening.test.ts": 1633,
"tests/unit/vscode-token-routes.test.ts": 1212,
"tests/unit/combo-config.test.ts": 881
"tests/unit/combo-config.test.ts": 881,
"tests/unit/web-cookie-providers-new.test.ts": 827
},
"_rebaseline_2026_06_09": "Re-baseline consciente pre-release v3.8.19: 9 arquivos cresceram durante o ciclo (features mergeadas: RequestLoggerV2 +281 request-logger rework, stream +101, combo +73, chatCore +45, catalog +32 fable-5/catalog-flag, callLogs +4, accountFallback +2, usageHistory novo 840) + core.ts +7 (fix resetAllDbModuleState, PR 3536). A catraca segue valendo destes valores — proximo crescimento falha. Decisao: encolher (esp. RequestLoggerV2/chatCore) e a issue #3501 ficam para o ciclo seguinte.",
"_rebaseline_2026_06_11_phase1f": "Phase 1f (#3501): ProviderDetailPageClient.tsx 4948→4062 (-886 LOC); 3 novos hooks extraídos. useProviderConnections.ts=954 acima do cap=800 — justificado: extração direta do god-component (zero lógica nova), própria redução do cliente supera o custo. useProviderSettings.ts=263 e useProviderModels.ts=154 já abaixo do cap.",

View File

@@ -2,8 +2,9 @@
"_comment": "Catraca de qualidade. 'down' = nao pode aumentar; 'up' = nao pode cair. Atualize via 'npm run quality:ratchet -- --update' (somente quando melhora). Cada valor e um numero REAL medido, nunca um chute. Cobertura entra na Fase 4 a partir de um run de cobertura mergeada no CI.",
"metrics": {
"eslintWarnings": {
"value": 4121,
"value": 4158,
"direction": "down",
"_rebaseline_2026_07_01_v3843_release": "4121->4158 (+37). v3.8.43 cycle drift surfaced by the release-green pre-flight; the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle. 4158 = the value measured by the CI Quality Ratchet on the release tip fce85136c (release PR #5609). Trust-but-verify: the fix/release-v3843-ci-reds branch touches only test files (rtk-mcp-tools de-flake, compression-studio e2e anchor, oauth-error-linkify hardening test) + src/shared/utils/linkify.ts (eslint-clean, 0 warnings) + stryker.conf.json + this baseline -> 0 new warnings, so all +37 is inherited cycle drift (any warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
"_rebaseline_2026_06_30_v3842_release": "4116->4121 (+5). v3.8.42 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's 90 commits — chatgpt-web PoW sha3-512 BoringSSL fix #5540, provider baseUrl/i18n umbrella #5511, proxy union proxyUrlMap+acct.proxy #5521, dead-code + duplication waves #5468-#5495, tls-options packaging #5503, release-freeze + .npmrc fetch-retries #5506, dast-smoke spawn-prefix client-safe extraction #5546, plus ~30 SSE/translator/combo/dashboard fixes). Trust-but-verify: measured 4121 via `npm run check:release-green` on the working tree INCLUDING my reconciliation (CHANGELOG/i18n/golden snapshot + file-size baseline) — those touch only config JSON + a provider snapshot (eslint-ignored) and contribute 0 warnings; all +5 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
"_rebaseline_2026_06_29_v3841_release": "4103->4116 (+13). v3.8.41 cycle drift surfaced by the release-green collect (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's 52 commits — relay backend #5315, gemini catalog #5337, services dashboard #5299, empty-Claude-messages guard #5342, thinking-budget/redacted-replay + marker opt-out #5312/#5352/#5367, opencode proxy-pool + observability #5217/#5370/#5351, cors + HTTPS-serve #5242/#5360/#5361, grok cf_clearance #5350/#5358, oauth/chatgpt-web/routing/cli/dashboard/rerank #5326/#5240/#5239/#5238/#5264/#5332, partially offset by the dead-code sweep #5321-#5371). Trust-but-verify: measured 4116 via `npm run quality:collect` on the working tree INCLUDING my reconciliation (CHANGELOG/i18n/README/env docs + baselines) AND the lint-fix in useServiceLogs.ts — that fix REMOVES a setState-in-effect ERROR (eslintErrors stays 0) and adds an `open` listener with no `any`/unused, contributing 0 warnings; all +13 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
"_rebaseline_2026_06_29_v3840_release": "4090->4103 (+13). v3.8.40 cycle drift surfaced by the release-green pre-flight + the release PR Quality Ratchet (the ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's ~57 commits — compression roadmap relevance/hard-budget/memoization/transparency/saliency/splitter/tool_search/RTK/QuantumLock #5289/#5288/#5286/#5284/#5285/#5283/#5269/#5268/#5260, ~20 SSE/translator/combo fixes #5248/#5250/#5254/#5261/#5255/#5273/#5258, M365 Copilot provider #5302, public-origin centralization #5278). Trust-but-verify: measured 4103 locally via `npm run quality:collect` on the release tip INCLUDING my reconciliation commits (CHANGELOG + main merge + the 2 regression test fixes 165c823f5) — the test fixes add 0 `any`/warnings (health-autopilot added a NextRequest import + asserts; chat-pipeline changed one Accept string + a comment), so all +13 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
@@ -103,16 +104,18 @@
"_rebaseline_2026_06_28_v3839_release": "78.4 -> 77.5 (-0.9, beyond the 0.5 eps). v3.8.39 cycle drift surfaced ONLY on the release PR (i18n-ui-coverage does NOT run on PR->release fast-gates). The cycle added new UI strings (compression studio TOON A/B table, antigravity remote-login dashboard field, amber warning icon) to the en denominator faster than the 37 non-en locales were translated; those locales need `npm run i18n:run` with OMNIROUTE_TRANSLATION_API_KEY (unavailable locally) — same precedent as _rebaseline_2026_06_18_v3828_cycle_close + _quality_rebaseline_2026_06_20_ci_ratchet. Measured by CI collect-metrics (run 28317145160) = 77.5. My release-finalize tree changes no src/i18n/messages/*.json. Tightening is tracked as follow-up (run i18n:run with creds)."
},
"deadExports": {
"value": 225,
"value": 227,
"direction": "down",
"_rebaseline_2026_07_01_v3843_release": "225->227 (+2). v3.8.43 cycle drift, surfaced in the Quality Ratchet job after eslintWarnings was rebaselined (check:dead-code runs there). 227 = measured by check:dead-code (knip) on the release tip 4635076eb. The 5 CI fixes add 0 dead exports: safeHttpHref in linkify.ts is module-local AND used (called by linkifyText); no new exports; test files are not scanned. Tighten via --update next cycle.",
"dedicatedGate": true,
"_rebaseline_2026_06_30_v3842_deadcode_wave": "310 -> 225. Measured by `node scripts/check/check-dead-code.mjs` on the v3.8.42 tip after the JxnLexn dead-code (#5463/#5464/#5466) + duplication (#5471..#5500) wave landed: DEAD_EXPORTS=133 + DEAD_FILES=92 = 225. The stale 310 was the v3.8.38 release snapshot never ratcheted on PR->release fast-gates (check:dead-code runs only on ci.yml PR->main, not quality.yml). Tightening to the true measured value; release-time captain rebaselines up if parallel cycle merges add dead exports.",
"_rebaseline_2026_06_27_v3838_release": "345->346 (+1). v3.8.38 cycle drift surfaced by the release-green pre-flight (Quality Ratchet does NOT run on PR->release fast-gates). Net +1 inherited from this cycle's feature/fix merges (new executors/providers, compression fidelity-gate module) minus #5138's removal of dead legacy store modules. Release-finalize working tree touches ONLY CHANGELOG.md + i18n mirrors + README + baselines — 0 production-code change. Structural cleanup tracked as debt.",
"_rebaseline_2026_06_26_v3837_release": "343->345. v3.8.37 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings/complexity accrued unmeasured across this cycle's 76 commits — provider adds DGrid/Pioneer/xAI, headroom proxy lifecycle #4649, ~50 SSE/translator fixes, Engine Combos #5062). Trust-but-verify: this release-finalize working tree touches ONLY CHANGELOG.md, docs/i18n/*/CHANGELOG.md mirrors, and these baselines — 0 production-code change, so all drift is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle."
},
"cognitiveComplexity": {
"value": 842,
"value": 845,
"direction": "down",
"_rebaseline_2026_07_01_v3843_release": "842->845 (+3). v3.8.43 cycle drift, surfaced after check:mutation-test-coverage was fixed (masked behind it in the Fast Quality Gates chain). 845 = measured by check:cognitive-complexity on BOTH fce85136c and 6d7060e21 (identical) — all +3 is inherited cycle drift; the 5 CI fixes add 0 (safeHttpHref cognitive ~2, under the 15 threshold). Tighten via --update next cycle.",
"dedicatedGate": true,
"_rebaseline_2026_06_29_v3841_release": "841->842 (+1). v3.8.41 cycle drift surfaced by the release-PR Quality Ratchet (cognitive-complexity does NOT run on PR->release fast-gates). The Phase-0 pre-flight measured 840 on the pre-campaign tip; the campaign's +34 later commits (thinking-budget/marker #5312/#5352/#5367, opencode proxy-pool/observability #5217/#5370/#5351, cors/HTTPS #5242/#5360/#5361, grok #5350/#5358, oauth/routing/cli #5326/#5239/#5238) added +2 net. My release-finalize changes are docs/baselines + a lint-fix (useServiceLogs open-listener), a test alignment (encryption.spec) and a pack-allowlist entry — all complexity-neutral. Structural shrink tracked in #3501.",
"_rebaseline_2026_06_27_v3838_release": "833->841 (+8). v3.8.38 cycle drift surfaced by the release-green pre-flight (cognitive-complexity does NOT run on PR->release fast-gates). Inherited drift from this cycle's ~78 feature/fix merges (compression fidelity-gate #5143, SSE/streaming hardening #5124/#5108/#5085, resilience #5093, quota keepalive #5102, contributor provider/translator branches). god-file decomposition #3501 is complexity-neutral. Release-finalize working tree touches ONLY CHANGELOG.md + i18n mirrors + README + baselines — 0 production-code change. Structural shrink tracked in #3501.",

View File

@@ -7,5 +7,6 @@
"tests/unit/compression/session-dedup.test.ts": "v3.8.29 #4226: the vestigial SessionDedup round-trip helper was removed from source; its 2 asserts were removed accordingly (32→30). Helper no longer exists. Verified legitimate, not masking. Prune after v3.8.29 merges to main.",
"tests/unit/compression/ultra.test.ts": "v3.8.29 #4253: the vestigial SLM seam + dead deprecated alias were removed from the ultra compression engine; 6 asserts covering the removed seam were removed accordingly (49→43). Verified legitimate, not masking. Prune after v3.8.29 merges to main.",
"tests/unit/db-backup-extended.test.ts": "v3.8.29 #4132: db-backup de-flake — 1 timing-sensitive assertion on fire-and-forget backup completion was removed in favor of awaiting actual completion (44→43). Verified legitimate, not masking. Prune after v3.8.29 merges to main.",
"@omniroute/opencode-plugin/tests/combos.test.ts": "v3.8.31 #4384: the plugin now prefixes every catalog key with the `omniroute` provider id and drops the legacy `combo/` namespace; the test asserting raw-deletion + a `combo/<slug>` key (a namespace that no longer exists) was removed and the remaining asserts switched to `omniroute/<slug>` keys (82→81). Asserts updated to the new key contract, not weakened. Verified legitimate. Prune after v3.8.31 merges to main."
"@omniroute/opencode-plugin/tests/combos.test.ts": "v3.8.31 #4384: the plugin now prefixes every catalog key with the `omniroute` provider id and drops the legacy `combo/` namespace; the test asserting raw-deletion + a `combo/<slug>` key (a namespace that no longer exists) was removed and the remaining asserts switched to `omniroute/<slug>` keys (82→81). Asserts updated to the new key contract, not weakened. Verified legitimate. Prune after v3.8.31 merges to main.",
"tests/unit/chatgpt-web.test.ts": "v3.8.43 #5549: fix(chatgpt-web) restore dot-form Pro model ids — dois assert.equal separados (base Pro slug + pass-through slug) consolidados num único assert.equal(body.model, expectedSlugById[omniId], ...) orientado por tabela de mapeamento (281→280). Asserts consolidados, não enfraquecidos. Verificado legítimo. Prune após v3.8.43 mergear para main."
}

View File

@@ -60,20 +60,40 @@ const BUILT_IN_ALIASES: Record<string, string> = {
};
// ── Custom Aliases (persisted via Settings API) ─────────────────────────────
let _customAliases: Record<string, string> = {};
//
// Backed by globalThis so the singleton store is shared across the SEPARATE webpack
// module graphs Next.js builds for `instrumentation.ts` (boot-time hydration via
// applyRuntimeSettings → setCustomAliases) and the app-route `GET /api/settings/model-aliases`.
// A plain module-level `let` is DUPLICATED per graph, so startup hydration lands on the
// instrumentation graph's copy while the API route reads an empty copy — the exact
// symptom #5777 patched at the route layer. Migrating the store to globalThis fixes the
// root cause (both instances read/write one store), mirroring the #5312 pattern already
// applied to thinkingBudget.ts and backgroundTaskDetector.ts (and systemPrompt.ts #2470).
const CUSTOM_ALIASES_GLOBAL_KEY = "__omniroute_customAliases__";
const _aliasStore = globalThis as unknown as Record<
string,
Record<string, string> | undefined
>;
function customAliases(): Record<string, string> {
if (!_aliasStore[CUSTOM_ALIASES_GLOBAL_KEY]) {
_aliasStore[CUSTOM_ALIASES_GLOBAL_KEY] = {};
}
return _aliasStore[CUSTOM_ALIASES_GLOBAL_KEY]!;
}
/**
* Set custom aliases (called from settings API or startup).
*/
export function setCustomAliases(aliases: Record<string, string>): void {
_customAliases = { ...aliases };
_aliasStore[CUSTOM_ALIASES_GLOBAL_KEY] = { ...aliases };
}
/**
* Get current custom aliases.
*/
export function getCustomAliases(): Record<string, string> {
return { ..._customAliases };
return { ...customAliases() };
}
/**
@@ -81,7 +101,7 @@ export function getCustomAliases(): Record<string, string> {
* Custom aliases take precedence over built-in.
*/
export function getAllAliases(): Record<string, string> {
return { ...BUILT_IN_ALIASES, ..._customAliases };
return { ...BUILT_IN_ALIASES, ...customAliases() };
}
/**
@@ -95,7 +115,8 @@ export function resolveModelAlias(modelId: string): string {
if (!modelId) return modelId;
// Check custom aliases first (higher priority)
if (_customAliases[modelId]) return _customAliases[modelId];
const custom = customAliases();
if (custom[modelId]) return custom[modelId];
// Then check built-in
if (BUILT_IN_ALIASES[modelId]) return BUILT_IN_ALIASES[modelId];
@@ -129,15 +150,16 @@ export function isDeprecated(modelId: string): boolean {
* Add a custom alias.
*/
export function addCustomAlias(from: string, to: string): void {
_customAliases[from] = to;
customAliases()[from] = to;
}
/**
* Remove a custom alias.
*/
export function removeCustomAlias(from: string): boolean {
if (_customAliases[from]) {
delete _customAliases[from];
const custom = customAliases();
if (custom[from]) {
delete custom[from];
return true;
}
return false;

View File

@@ -24,6 +24,20 @@ import {
cleanJSONSchemaForAntigravity,
} from "../helpers/geminiHelper.ts";
import { buildGeminiTools, sanitizeGeminiToolName } from "../helpers/geminiToolsSanitizer.ts";
import {
type GeminiGenerationConfig,
isVertexGeminiProvider,
buildChangedToolNameMap,
extractClientThoughtSignature,
deepCleanUndefined,
applyAntigravityGenerationDefaults,
stringifyHistoricalToolArguments,
buildInertHistoricalToolCallText,
buildInertHistoricalToolResponseText,
escapeHistoricalContextAttribute,
escapeHistoricalContextContent,
buildHistoricalToolResultContext,
} from "./openai-to-gemini/helpers.ts";
// Observed Antigravity wrapper output cap, not an underlying model capability.
// Keep this bridge-local: Antigravity currently caps visible output around 16K.
@@ -43,20 +57,6 @@ const GEMINI_BUILTIN_TOOL_NAMES = new Set<string>([
type GeminiPart = Record<string, unknown>;
type GeminiContent = { role: string; parts: GeminiPart[] };
type GeminiGenerationConfig = {
temperature?: unknown;
topP?: unknown;
topK?: unknown;
maxOutputTokens?: unknown;
thinkingConfig?: {
thinkingBudget: number;
includeThoughts: boolean;
};
responseMimeType?: string;
responseSchema?: unknown;
stopSequences?: string[] | unknown[];
};
type GeminiFunctionDeclaration = {
name: string;
description: string;
@@ -118,126 +118,6 @@ type GeminiToolNameOptions = {
supportsSignatureBypass?: boolean;
};
// Vertex AI (and Vertex Partner models) reject the OpenAI-style `id` field inside
// function_call / function_response parts. Detect these by the routed provider id.
function isVertexGeminiProvider(provider: unknown): boolean {
return provider === "vertex" || provider === "vertex-partner";
}
type OpenAIToolCallLike = {
thoughtSignature?: unknown;
thought_signature?: unknown;
function?: {
thoughtSignature?: unknown;
thought_signature?: unknown;
};
};
function buildChangedToolNameMap(toolNameMap: Map<string, string>): Map<string, string> | null {
const changedEntries = [...toolNameMap.entries()].filter(
([sanitizedName, originalName]) => sanitizedName !== originalName
);
return changedEntries.length > 0 ? new Map(changedEntries) : null;
}
function extractClientThoughtSignature(toolCall: unknown): string | null {
if (!toolCall || typeof toolCall !== "object") return null;
const candidate = toolCall as OpenAIToolCallLike;
const signature =
candidate.thoughtSignature ||
candidate.thought_signature ||
candidate.function?.thoughtSignature ||
candidate.function?.thought_signature ||
null;
return typeof signature === "string" && signature.length > 0 ? signature : null;
}
function deepCleanUndefined(value: unknown, depth = 0): void {
if (depth > 10 || !value || typeof value !== "object") {
return;
}
if (Array.isArray(value)) {
for (const item of value) {
deepCleanUndefined(item, depth + 1);
}
} else {
const obj = value as Record<string, unknown>;
for (const key of Object.keys(obj)) {
const val = obj[key];
if (typeof val === "string" && val === "[undefined]") {
delete obj[key];
} else {
deepCleanUndefined(val, depth + 1);
}
}
}
}
function applyAntigravityGenerationDefaults(generationConfig: GeminiGenerationConfig) {
const config = { ...generationConfig };
if (config.topK === undefined) {
config.topK = 40;
}
if (config.topP === undefined) {
config.topP = 1;
}
const thinkingBudget = Number(config.thinkingConfig?.thinkingBudget);
const maxOutputTokens = Number(config.maxOutputTokens);
if (
Number.isFinite(thinkingBudget) &&
thinkingBudget > 0 &&
(!Number.isFinite(maxOutputTokens) || maxOutputTokens <= thinkingBudget)
) {
config.maxOutputTokens = Math.floor(thinkingBudget) + 1;
}
return config;
}
function stringifyHistoricalToolArguments(value: unknown): string {
if (typeof value === "string") return value;
try {
return JSON.stringify(value ?? {});
} catch {
return String(value ?? "{}");
}
}
function buildInertHistoricalToolCallText(name: string | undefined, args: unknown): string {
const toolName = name || "unknown";
return `[tool_history_call: ${toolName}] ${stringifyHistoricalToolArguments(args || "{}")}`;
}
function buildInertHistoricalToolResponseText(name: string, response: unknown): string {
return `[tool_history_result: ${name || "unknown"}] ${typeof response === "string" ? response : stringifyHistoricalToolArguments(response)}`;
}
function escapeHistoricalContextAttribute(value: string): string {
return value
.replaceAll("&", "&amp;")
.replaceAll('"', "&quot;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
}
function escapeHistoricalContextContent(value: string): string {
return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
}
function buildHistoricalToolResultContext(name: string, response: unknown): string {
const source = escapeHistoricalContextAttribute(name || "unknown");
const rawResult =
typeof response === "string" ? response : stringifyHistoricalToolArguments(response);
const result = escapeHistoricalContextContent(rawResult);
return [
`<previous_tool_result_context source="${source}">`,
result,
"</previous_tool_result_context>",
].join("\n");
}
// Gemini-family APIs (incl. Antigravity / Vertex) reject a `contents[]` array that
// has two adjacent entries with the same role:
// 400 INVALID_ARGUMENT "Request contains consecutive messages with the same role".

View File

@@ -0,0 +1,142 @@
// Pure, self-contained helpers extracted verbatim from ../openai-to-gemini.ts
// (god-file decomposition): historical-tool-context string builders, undefined-
// pruning, thought-signature extraction, tool-name remapping, and the Vertex
// provider check + Antigravity generation-config defaults. No I/O or module state;
// the host imports them back internally (these were module-private — no public API
// change). The GeminiGenerationConfig shape lives here with its only mutator.
export type GeminiGenerationConfig = {
temperature?: unknown;
topP?: unknown;
topK?: unknown;
maxOutputTokens?: unknown;
thinkingConfig?: {
thinkingBudget: number;
includeThoughts: boolean;
};
responseMimeType?: string;
responseSchema?: unknown;
stopSequences?: string[] | unknown[];
};
// Vertex AI (and Vertex Partner models) reject the OpenAI-style `id` field inside
// function_call / function_response parts. Detect these by the routed provider id.
export function isVertexGeminiProvider(provider: unknown): boolean {
return provider === "vertex" || provider === "vertex-partner";
}
type OpenAIToolCallLike = {
thoughtSignature?: unknown;
thought_signature?: unknown;
function?: {
thoughtSignature?: unknown;
thought_signature?: unknown;
};
};
export function buildChangedToolNameMap(
toolNameMap: Map<string, string>
): Map<string, string> | null {
const changedEntries = [...toolNameMap.entries()].filter(
([sanitizedName, originalName]) => sanitizedName !== originalName
);
return changedEntries.length > 0 ? new Map(changedEntries) : null;
}
export function extractClientThoughtSignature(toolCall: unknown): string | null {
if (!toolCall || typeof toolCall !== "object") return null;
const candidate = toolCall as OpenAIToolCallLike;
const signature =
candidate.thoughtSignature ||
candidate.thought_signature ||
candidate.function?.thoughtSignature ||
candidate.function?.thought_signature ||
null;
return typeof signature === "string" && signature.length > 0 ? signature : null;
}
export function deepCleanUndefined(value: unknown, depth = 0): void {
if (depth > 10 || !value || typeof value !== "object") {
return;
}
if (Array.isArray(value)) {
for (const item of value) {
deepCleanUndefined(item, depth + 1);
}
} else {
const obj = value as Record<string, unknown>;
for (const key of Object.keys(obj)) {
const val = obj[key];
if (typeof val === "string" && val === "[undefined]") {
delete obj[key];
} else {
deepCleanUndefined(val, depth + 1);
}
}
}
}
export function applyAntigravityGenerationDefaults(generationConfig: GeminiGenerationConfig) {
const config = { ...generationConfig };
if (config.topK === undefined) {
config.topK = 40;
}
if (config.topP === undefined) {
config.topP = 1;
}
const thinkingBudget = Number(config.thinkingConfig?.thinkingBudget);
const maxOutputTokens = Number(config.maxOutputTokens);
if (
Number.isFinite(thinkingBudget) &&
thinkingBudget > 0 &&
(!Number.isFinite(maxOutputTokens) || maxOutputTokens <= thinkingBudget)
) {
config.maxOutputTokens = Math.floor(thinkingBudget) + 1;
}
return config;
}
export function stringifyHistoricalToolArguments(value: unknown): string {
if (typeof value === "string") return value;
try {
return JSON.stringify(value ?? {});
} catch {
return String(value ?? "{}");
}
}
export function buildInertHistoricalToolCallText(name: string | undefined, args: unknown): string {
const toolName = name || "unknown";
return `[tool_history_call: ${toolName}] ${stringifyHistoricalToolArguments(args || "{}")}`;
}
export function buildInertHistoricalToolResponseText(name: string, response: unknown): string {
return `[tool_history_result: ${name || "unknown"}] ${typeof response === "string" ? response : stringifyHistoricalToolArguments(response)}`;
}
export function escapeHistoricalContextAttribute(value: string): string {
return value
.replaceAll("&", "&amp;")
.replaceAll('"', "&quot;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
}
export function escapeHistoricalContextContent(value: string): string {
return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
}
export function buildHistoricalToolResultContext(name: string, response: unknown): string {
const source = escapeHistoricalContextAttribute(name || "unknown");
const rawResult =
typeof response === "string" ? response : stringifyHistoricalToolArguments(response);
const result = escapeHistoricalContextContent(rawResult);
return [
`<previous_tool_result_context source="${source}">`,
result,
"</previous_tool_result_context>",
].join("\n");
}

View File

@@ -18,6 +18,25 @@
import zlib from "node:zlib";
import crypto from "node:crypto";
import {
WT_VARINT,
WT_LEN,
encodeVarint,
encodeTag,
encodeBytes,
encodeString,
encodeMessage,
encodeUInt32Field,
encodeBoolField,
encodeDoubleField,
decodeVarint,
checkedLen,
decodeFields,
findField,
decodeStringField,
decodeVarintField,
type Field,
} from "./cursorAgentProtobuf/wire.ts";
// ─── Field numbers (from agent.proto descriptor) ───────────────────────────
@@ -225,125 +244,6 @@ const LIST_VALUES = 1; // ListValue.values = repeated Value
const MAP_KEY = 1;
const MAP_VALUE = 2;
// ─── Wire-type constants ───────────────────────────────────────────────────
const WT_VARINT = 0;
const WT_LEN = 2;
// ─── Primitive encoders ────────────────────────────────────────────────────
function encodeVarint(value: number | bigint): Buffer {
let v = typeof value === "bigint" ? value : BigInt(value);
const bytes: number[] = [];
while (v > 0x7fn) {
bytes.push(Number(v & 0x7fn) | 0x80);
v >>= 7n;
}
bytes.push(Number(v));
return Buffer.from(bytes);
}
function encodeTag(fieldNumber: number, wireType: number): Buffer {
return encodeVarint((fieldNumber << 3) | wireType);
}
function encodeBytes(fieldNumber: number, value: Buffer | Uint8Array): Buffer {
const buf = Buffer.isBuffer(value) ? value : Buffer.from(value);
return Buffer.concat([encodeTag(fieldNumber, WT_LEN), encodeVarint(buf.length), buf]);
}
function encodeString(fieldNumber: number, value: string): Buffer {
return encodeBytes(fieldNumber, Buffer.from(value, "utf8"));
}
function encodeMessage(fieldNumber: number, parts: Buffer[]): Buffer {
const inner = Buffer.concat(parts);
return Buffer.concat([encodeTag(fieldNumber, WT_LEN), encodeVarint(inner.length), inner]);
}
function encodeUInt32Field(fieldNumber: number, value: number): Buffer {
return Buffer.concat([encodeTag(fieldNumber, WT_VARINT), encodeVarint(value)]);
}
function encodeBoolField(fieldNumber: number, value: boolean): Buffer {
return Buffer.concat([encodeTag(fieldNumber, WT_VARINT), encodeVarint(value ? 1 : 0)]);
}
function encodeDoubleField(fieldNumber: number, value: number): Buffer {
// wire type 1 = 64-bit fixed (double)
const buf = Buffer.alloc(8);
buf.writeDoubleLE(value, 0);
return Buffer.concat([encodeTag(fieldNumber, 1), buf]);
}
// ─── Primitive decoders ────────────────────────────────────────────────────
function decodeVarint(buf: Buffer, offset: number): [bigint, number] {
let result = 0n;
let shift = 0n;
let pos = offset;
while (pos < buf.length) {
const byte = buf[pos++];
result |= BigInt(byte & 0x7f) << shift;
if ((byte & 0x80) === 0) return [result, pos];
shift += 7n;
}
throw new Error("varint truncated");
}
type Field =
| { fieldNumber: number; wireType: 0; varint: bigint }
| { fieldNumber: number; wireType: 2; bytes: Buffer };
/**
* Validate a length-delimited field's declared length against the bytes that
* actually remain in the buffer. Cursor's frames are well-formed, but a
* corrupted or hostile upstream could declare a length that overruns the
* buffer; without this guard `Buffer.subarray` silently clamps to EOF and a
* truncated tool argument (or any nested message) is decoded as empty/partial
* data instead of being recognized as malformed. Throwing lets the caller —
* `processFrame`, wrapped in driveH2's per-frame try/catch — skip the bad
* frame rather than act on corrupted fields. Also rejects absurd lengths that
* would not fit a JS safe integer.
*/
function checkedLen(len: bigint, pos: number, buf: Buffer): number {
if (len < 0n || len > BigInt(buf.length - pos)) {
throw new Error(
`length-delimited field overruns buffer (len=${len}, remaining=${buf.length - pos})`
);
}
return Number(len);
}
function decodeFields(buf: Buffer): Field[] {
const fields: Field[] = [];
let pos = 0;
while (pos < buf.length) {
const [tag, np] = decodeVarint(buf, pos);
pos = np;
const fieldNumber = Number(tag >> 3n);
const wireType = Number(tag & 0x7n);
if (wireType === WT_VARINT) {
const [v, np2] = decodeVarint(buf, pos);
pos = np2;
fields.push({ fieldNumber, wireType: 0, varint: v });
} else if (wireType === WT_LEN) {
const [len, np2] = decodeVarint(buf, pos);
pos = np2;
const lenN = checkedLen(len, pos, buf);
fields.push({ fieldNumber, wireType: 2, bytes: buf.subarray(pos, pos + lenN) });
pos += lenN;
} else if (wireType === 5) {
pos += 4;
} else if (wireType === 1) {
pos += 8;
} else {
throw new Error(`unsupported wireType ${wireType}`);
}
}
return fields;
}
// ─── Connect-RPC framing ───────────────────────────────────────────────────
const FLAG_NONE = 0x00;
@@ -547,9 +447,7 @@ export function encodeAgentRunRequest(input: AgentRunInput): Buffer {
const selectedContextParts: Buffer[] = [];
if (input.images && input.images.length > 0) {
for (const img of input.images) {
selectedContextParts.push(
encodeMessage(SC_SELECTED_IMAGES, [encodeSelectedImageBody(img)])
);
selectedContextParts.push(encodeMessage(SC_SELECTED_IMAGES, [encodeSelectedImageBody(img)]));
}
}
// The empty selected_context placeholder and mode=1 match cursor-agent's
@@ -652,24 +550,6 @@ export type DecodedDelta =
| { kind: "kv_server_message" }
| { kind: "unknown"; field: number };
function findField(fields: Field[], fieldNumber: number): Field | undefined {
return fields.find((f) => f.fieldNumber === fieldNumber);
}
function decodeStringField(buf: Buffer, fieldNumber: number): string {
const fields = decodeFields(buf);
const f = findField(fields, fieldNumber);
if (f && f.wireType === 2) return f.bytes.toString("utf8");
return "";
}
function decodeVarintField(buf: Buffer, fieldNumber: number): number {
const fields = decodeFields(buf);
const f = findField(fields, fieldNumber);
if (f && f.wireType === 0) return Number(f.varint);
return 0;
}
export function decodeAgentServerMessage(payload: Buffer): DecodedDelta[] {
const out: DecodedDelta[] = [];
for (const top of decodeFields(payload)) {

View File

@@ -0,0 +1,143 @@
// Low-level protobuf wire-format primitives for the Cursor Agent codec, extracted
// verbatim from ../cursorAgentProtobuf.ts (god-file decomposition). Pure and
// dependency-free (Buffer only): varint/tag/length-delimited encode+decode and the
// generic field walker. Framing, the value codec, and the message encoders/decoders
// all build on this layer. Nothing here was part of the module's public API, so the
// host imports these back internally (no re-export).
// ─── Wire-type constants ───────────────────────────────────────────────────
export const WT_VARINT = 0;
export const WT_LEN = 2;
// ─── Primitive encoders ────────────────────────────────────────────────────
export function encodeVarint(value: number | bigint): Buffer {
let v = typeof value === "bigint" ? value : BigInt(value);
const bytes: number[] = [];
while (v > 0x7fn) {
bytes.push(Number(v & 0x7fn) | 0x80);
v >>= 7n;
}
bytes.push(Number(v));
return Buffer.from(bytes);
}
export function encodeTag(fieldNumber: number, wireType: number): Buffer {
return encodeVarint((fieldNumber << 3) | wireType);
}
export function encodeBytes(fieldNumber: number, value: Buffer | Uint8Array): Buffer {
const buf = Buffer.isBuffer(value) ? value : Buffer.from(value);
return Buffer.concat([encodeTag(fieldNumber, WT_LEN), encodeVarint(buf.length), buf]);
}
export function encodeString(fieldNumber: number, value: string): Buffer {
return encodeBytes(fieldNumber, Buffer.from(value, "utf8"));
}
export function encodeMessage(fieldNumber: number, parts: Buffer[]): Buffer {
const inner = Buffer.concat(parts);
return Buffer.concat([encodeTag(fieldNumber, WT_LEN), encodeVarint(inner.length), inner]);
}
export function encodeUInt32Field(fieldNumber: number, value: number): Buffer {
return Buffer.concat([encodeTag(fieldNumber, WT_VARINT), encodeVarint(value)]);
}
export function encodeBoolField(fieldNumber: number, value: boolean): Buffer {
return Buffer.concat([encodeTag(fieldNumber, WT_VARINT), encodeVarint(value ? 1 : 0)]);
}
export function encodeDoubleField(fieldNumber: number, value: number): Buffer {
// wire type 1 = 64-bit fixed (double)
const buf = Buffer.alloc(8);
buf.writeDoubleLE(value, 0);
return Buffer.concat([encodeTag(fieldNumber, 1), buf]);
}
// ─── Primitive decoders ────────────────────────────────────────────────────
export function decodeVarint(buf: Buffer, offset: number): [bigint, number] {
let result = 0n;
let shift = 0n;
let pos = offset;
while (pos < buf.length) {
const byte = buf[pos++];
result |= BigInt(byte & 0x7f) << shift;
if ((byte & 0x80) === 0) return [result, pos];
shift += 7n;
}
throw new Error("varint truncated");
}
export type Field =
| { fieldNumber: number; wireType: 0; varint: bigint }
| { fieldNumber: number; wireType: 2; bytes: Buffer };
/**
* Validate a length-delimited field's declared length against the bytes that
* actually remain in the buffer. Cursor's frames are well-formed, but a
* corrupted or hostile upstream could declare a length that overruns the
* buffer; without this guard `Buffer.subarray` silently clamps to EOF and a
* truncated tool argument (or any nested message) is decoded as empty/partial
* data instead of being recognized as malformed. Throwing lets the caller —
* `processFrame`, wrapped in driveH2's per-frame try/catch — skip the bad
* frame rather than act on corrupted fields. Also rejects absurd lengths that
* would not fit a JS safe integer.
*/
export function checkedLen(len: bigint, pos: number, buf: Buffer): number {
if (len < 0n || len > BigInt(buf.length - pos)) {
throw new Error(
`length-delimited field overruns buffer (len=${len}, remaining=${buf.length - pos})`
);
}
return Number(len);
}
export function decodeFields(buf: Buffer): Field[] {
const fields: Field[] = [];
let pos = 0;
while (pos < buf.length) {
const [tag, np] = decodeVarint(buf, pos);
pos = np;
const fieldNumber = Number(tag >> 3n);
const wireType = Number(tag & 0x7n);
if (wireType === WT_VARINT) {
const [v, np2] = decodeVarint(buf, pos);
pos = np2;
fields.push({ fieldNumber, wireType: 0, varint: v });
} else if (wireType === WT_LEN) {
const [len, np2] = decodeVarint(buf, pos);
pos = np2;
const lenN = checkedLen(len, pos, buf);
fields.push({ fieldNumber, wireType: 2, bytes: buf.subarray(pos, pos + lenN) });
pos += lenN;
} else if (wireType === 5) {
pos += 4;
} else if (wireType === 1) {
pos += 8;
} else {
throw new Error(`unsupported wireType ${wireType}`);
}
}
return fields;
}
export function findField(fields: Field[], fieldNumber: number): Field | undefined {
return fields.find((f) => f.fieldNumber === fieldNumber);
}
export function decodeStringField(buf: Buffer, fieldNumber: number): string {
const fields = decodeFields(buf);
const f = findField(fields, fieldNumber);
if (f && f.wireType === 2) return f.bytes.toString("utf8");
return "";
}
export function decodeVarintField(buf: Buffer, fieldNumber: number): number {
const fields = decodeFields(buf);
const f = findField(fields, fieldNumber);
if (f && f.wireType === 0) return Number(f.varint);
return 0;
}

View File

@@ -95,13 +95,13 @@
"electron:build:mac": "npm run build && cd electron && npm run build:mac",
"electron:build:linux": "npm run build && cd electron && npm run build:linux",
"electron:smoke:packaged": "node scripts/dev/smoke-electron-packaged.mjs",
"test": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-concurrency=20 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\"",
"test:unit": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=20 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\"",
"test:unit:ci": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\"",
"test:unit:fast": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-isolation=none tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\"",
"test": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-concurrency=20 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\"",
"test:unit": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=20 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\"",
"test:unit:ci": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\"",
"test:unit:fast": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-isolation=none tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\"",
"test:unit:shard": "concurrently --kill-others-on-fail -n s1,s2 \"npm:test:unit:shard:1\" \"npm:test:unit:shard:2\"",
"test:unit:shard:1": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=1/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\"",
"test:unit:shard:2": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=2/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\"",
"test:unit:shard:1": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=1/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\"",
"test:unit:shard:2": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=2/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\"",
"test:plan3": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/plan3-p0.test.ts",
"test:fixes": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/fixes-p1.test.ts",
"test:security": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/security-fase01.test.ts",
@@ -191,7 +191,7 @@
"test:mutation": "stryker run",
"test:ecosystem": "node scripts/dev/run-ecosystem-tests.mjs",
"test:system": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 tests/e2e/system-failover.test.ts",
"test:coverage": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true NODE_OPTIONS=--max-old-space-size=8192 c8 --merge-async --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 60 --lines 60 --functions 60 --branches 60 node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\"",
"test:coverage": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true NODE_OPTIONS=--max-old-space-size=8192 c8 --merge-async --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 60 --lines 60 --functions 60 --branches 60 node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\"",
"test:coverage:legacy": "c8 --output-dir coverage --exclude=open-sse --check-coverage --lines 50 --functions 50 --branches 50 node --import tsx --test tests/unit/*.test.ts",
"coverage:report": "cross-env NODE_OPTIONS=--max-old-space-size=8192 c8 report --merge-async --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov",
"coverage:summary": "node scripts/check/test-report-summary.mjs --input coverage/coverage-summary.json --output coverage/coverage-report.md",
@@ -217,6 +217,7 @@
"@monaco-editor/react": "^4.7.0",
"@ngrok/ngrok": "^1.7.0",
"@swc/helpers": "0.5.23",
"@toon-format/toon": "^2.3.0",
"@types/mdx": "^2.0.13",
"@xyflow/react": "^12.11.1",
"axios": "^1.16.1",
@@ -265,6 +266,7 @@
"react-markdown": "^10.1.0",
"react-reconciler": "^0.33.0",
"recharts": "^3.8.1",
"safe-regex": "^2.1.1",
"selfsigned": "^5.5.0",
"socks": "^2.8.7",
"sql.js": "^1.14.1",

View File

@@ -53,7 +53,7 @@ export const COLLECTORS = [
// "vitest" e explodem no node runner). Subdir novo: adicione aqui E nos scripts
// (o drift-check + o gate de órfãos forçam a manutenção em sincronia).
{
glob: "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts",
glob: "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts",
sources: ["package.json", ".github/workflows/ci.yml"],
},
// Node native runner — test:integration (top-level only; tests/integration/services/ NÃO roda)

View File

@@ -19,6 +19,7 @@ import {
normalizeAndValidateHttpBaseUrl,
extractCommandCodeCredentialInput,
providerText,
validationBadgeProps,
type CommandCodeAuthFlowState,
} from "../../providerPageHelpers";
import { getWebSessionCredentialRequirement } from "../../webSessionCredentials";
@@ -202,7 +203,7 @@ export default function AddApiKeyModal({
});
const data = await res.json();
const ok = !!data.valid;
setValidationResult(ok ? "success" : "failed");
setValidationResult(ok ? "success" : data.unsupported ? "unsupported" : "failed");
// #5088: surface the detailed reason the backend returns (e.g. a TLS/EACCES
// environment error for claude-web/chatgpt-web) instead of only a bare
// "invalid" badge — otherwise the real cause is hidden and users are stuck.
@@ -276,7 +277,7 @@ export default function AddApiKeyModal({
if (!isValid && data.error) {
validationError = data.error;
}
setValidationResult(isValid ? "success" : "failed");
setValidationResult(isValid ? "success" : isUnsupported ? "unsupported" : "failed");
} catch {
setValidationResult("failed");
} finally {
@@ -674,8 +675,8 @@ export default function AddApiKeyModal({
/>
)}
{validationResult && (
<Badge variant={validationResult === "success" ? "success" : "error"}>
{validationResult === "success" ? t("valid") : t("invalid")}
<Badge variant={validationBadgeProps(validationResult).variant}>
{t(validationBadgeProps(validationResult).labelKey)}
</Badge>
)}
{saveError && (

View File

@@ -118,6 +118,22 @@ export function providerText(
return fallback;
}
/**
* #5442 — Badge variant + i18n label key for an add-credential validation result.
* A provider with no live validator returns `unsupported` (Save still succeeds);
* previously the modal only had success/failed states, so it rendered a red
* "Invalid" badge for those providers even though saving worked (LMArena, PiAPI…).
* "unsupported" now maps to a neutral `info` badge ("N/A"), not "Invalid".
*/
export function validationBadgeProps(result: string): {
variant: "success" | "error" | "info";
labelKey: string;
} {
if (result === "success") return { variant: "success", labelKey: "valid" };
if (result === "unsupported") return { variant: "info", labelKey: "notApplicable" };
return { variant: "error", labelKey: "invalid" };
}
/** A single model's outcome from a `/api/models/test-all` response. */
export interface TestAllModelOutcome {
status: "ok" | "error";

View File

@@ -19,6 +19,10 @@ import {
import { NEWS_JSON_URL, parseActiveNewsPayload } from "@/shared/utils/releaseNotes";
import { isNewer, resolveLatestVersion } from "@/lib/system/versionCheck";
import { resolveGlobalOmniroutePath } from "@/lib/system/globalPackagePath";
// #5542 — On Windows npm is `npm.cmd`; Node ≥24 refuses to execFile a `.cmd` without
// a shell (nodejs/node#52554 → "spawn npm ENOENT"). buildNpmExecOptions enables the
// shell on win32 only; SERVICE_VERSION_PATTERN keeps the shell-joined version safe.
import { buildNpmExecOptions, SERVICE_VERSION_PATTERN } from "@/lib/services/installers/utils";
const execFileAsync = promisify(execFile);
@@ -208,10 +212,11 @@ export async function POST(req: NextRequest) {
status: "running",
message: "Installing dependencies...",
});
await execFileAsync("npm", ["install", "--legacy-peer-deps"], {
timeout: 300_000,
cwd: PROJECT_ROOT,
});
await execFileAsync(
"npm",
["install", "--legacy-peer-deps"],
buildNpmExecOptions(process.platform, { cwd: PROJECT_ROOT, timeoutMs: 300_000 })
);
send({ step: "rebuild", status: "done", message: "Dependencies installed" });
try {
@@ -228,10 +233,11 @@ export async function POST(req: NextRequest) {
status: "running",
message: "Building application...",
});
await execFileAsync("npm", ["run", "build"], {
timeout: 600_000,
cwd: PROJECT_ROOT,
});
await execFileAsync(
"npm",
["run", "build"],
buildNpmExecOptions(process.platform, { cwd: PROJECT_ROOT, timeoutMs: 600_000 })
);
send({ step: "rebuild", status: "done", message: "Build complete" });
send({ step: "restart", status: "running", message: "Restarting service..." });
@@ -286,14 +292,19 @@ export async function POST(req: NextRequest) {
try {
// Step 1: Install
// #5542 — buildNpmExecOptions enables the shell on win32 (npm.cmd), which
// shell-joins argv, so the version spec must be metacharacter-free before it
// reaches the command line (Hard Rule #13).
if (!SERVICE_VERSION_PATTERN.test(latest)) {
send({ step: "install", status: "error", message: "Invalid version format" });
controller.close();
return;
}
send({ step: "install", status: "running", message: `Installing omniroute@${latest}...` });
await execFileAsync(
"npm",
["install", "-g", `omniroute@${latest}`, "--ignore-scripts", "--legacy-peer-deps"],
{
timeout: 300000,
cwd: PROJECT_ROOT,
}
buildNpmExecOptions(process.platform, { cwd: PROJECT_ROOT, timeoutMs: 300_000 })
);
send({ step: "install", status: "done", message: `Installed omniroute@${latest}` });
@@ -307,10 +318,7 @@ export async function POST(req: NextRequest) {
await execFileAsync(
"npm",
["rebuild", "better-sqlite3"],
{
cwd: omniPath,
timeout: 120000,
}
buildNpmExecOptions(process.platform, { cwd: omniPath, timeoutMs: 120_000 })
);
send({ step: "rebuild", status: "done", message: "Native modules rebuilt" });

View File

@@ -93,6 +93,7 @@ export {
export * from "./db/compressionCacheStats";
export * from "./db/compressionCombos";
export * from "./db/compressionRunTelemetry";
export * from "./db/modelContextOverrides";
export {
// API Keys

View File

@@ -131,6 +131,13 @@ export const grokCli = {
expiresIn = exp - currentSec;
}
// #5775 follow-up: guard against an already-expired token yielding a negative
// expiresIn. A negative value is truthy downstream (import-token route) and maps
// to a PAST expiresAt, which AutoCombo reads as "already expired" and excludes the
// connection instead of refreshing it. Clamp to a tiny positive TTL so the token is
// treated as due-for-refresh.
expiresIn = Math.max(1, expiresIn);
return {
accessToken,
refreshToken,

View File

@@ -19,6 +19,7 @@
import { execFile } from "child_process";
import { promisify } from "util";
import { createLogger } from "@/shared/utils/logger";
import { buildNpmExecOptions } from "@/lib/services/installers/utils";
const execFileAsync = promisify(execFile);
const log = createLogger("system/versionCheck");
@@ -71,9 +72,13 @@ export function isNewer(latest: string | null | undefined, current: string): boo
/** Latest published version via the `npm` CLI (fast when npm is on PATH, e.g. source installs). */
export async function getLatestVersionFromNpmCli(): Promise<string | null> {
try {
const { stdout } = await execFileAsync("npm", ["info", "omniroute", "version", "--json"], {
timeout: LOOKUP_TIMEOUT_MS,
});
// #5542 — win32 npm is npm.cmd; execFile without a shell throws "spawn npm ENOENT"
// on Node ≥24 (nodejs/node#52554). buildNpmExecOptions enables the shell on win32.
const { stdout } = await execFileAsync(
"npm",
["info", "omniroute", "version", "--json"],
buildNpmExecOptions(process.platform, { timeoutMs: LOOKUP_TIMEOUT_MS })
);
const parsed = JSON.parse(String(stdout).trim());
return typeof parsed === "string" && parsed ? parsed : null;
} catch {

View File

@@ -0,0 +1,30 @@
import { Fragment } from "react";
import { linkifyText } from "@/shared/utils/linkify";
/**
* #5486 — Render a string with any embedded http(s) URLs as clickable links.
* Used by the OAuth error step so setup instructions (e.g. GitLab Duo's
* "register an OAuth application at https://gitlab.com/-/profile/applications …")
* are actionable instead of dead text. Links open in a new tab with a safe rel.
*/
export default function LinkifiedText({ text }: { text: string | null | undefined }) {
return (
<>
{linkifyText(text || "").map((seg, i) =>
seg.href ? (
<a
key={i}
href={seg.href}
target="_blank"
rel="noopener noreferrer"
className="underline break-all"
>
{seg.text}
</a>
) : (
<Fragment key={i}>{seg.text}</Fragment>
)
)}
</>
);
}

View File

@@ -5,6 +5,7 @@ import { useTranslations } from "next-intl";
import Modal from "./Modal";
import Button from "./Button";
import Input from "./Input";
import LinkifiedText from "./LinkifiedText";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { parseResponseBody, getErrorMessage } from "@/shared/utils/api";
import { isCredentialBlob, submitCredentialBlob } from "@/shared/components/oauthBlobSubmit";
@@ -948,7 +949,9 @@ export default function OAuthModal({
<span className="material-symbols-outlined text-3xl text-red-600">error</span>
</div>
<h3 className="text-lg font-semibold mb-2">{t("error")}</h3>
<p className="text-sm text-red-600 mb-4">{error}</p>
<p className="text-sm text-red-600 mb-4">
<LinkifiedText text={error} />
</p>
<div className="flex gap-2">
<Button onClick={startOAuthFlow} variant="secondary" fullWidth>
{t("tryAgain")}

View File

@@ -0,0 +1,54 @@
// #5486 — Split a plain string into text + URL segments so messages that embed a
// URL (e.g. the GitLab Duo OAuth setup instructions pointing at
// https://gitlab.com/-/profile/applications) can render clickable links instead of
// dead red text in the OAuth error step. Pure (no React) so it is unit-testable.
//
// The matcher is a single unbounded repetition over a NEGATED character class
// (`[^\s...]+`) — linear time, no catastrophic backtracking (ReDoS-safe per the
// bounded-regex rule). A trailing sentence punctuation char is peeled back out of
// the URL so "…restart." keeps its period as text and the link stays valid.
export type TextSegment = { text: string; href?: string };
const URL_RE = /https?:\/\/[^\s"'<>)]+/g;
// Defense-in-depth scheme allowlist. URL_RE already requires an http(s):// prefix, but
// validate the scheme EXPLICITLY before exposing `href` so the http(s)-only guarantee is
// enforced on the value itself — not merely implied by the regex. A non-parseable or
// non-http(s) match degrades to plain text (no `href`). This also makes the sink safe to
// static analysis: `href` provably can never carry a javascript:/data:/vbscript: scheme
// (CodeQL js/xss + js/client-side-unvalidated-url-redirection).
function safeHttpHref(url: string): string | undefined {
let protocol: string;
try {
protocol = new URL(url).protocol;
} catch {
return undefined;
}
return protocol === "http:" || protocol === "https:" ? url : undefined;
}
export function linkifyText(input: string): TextSegment[] {
if (!input) return [];
const segments: TextSegment[] = [];
let last = 0;
let match: RegExpExecArray | null;
URL_RE.lastIndex = 0;
while ((match = URL_RE.exec(input)) !== null) {
if (match.index > last) {
segments.push({ text: input.slice(last, match.index) });
}
let url = match[0];
let trailing = "";
while (url.length > 0 && /[.,;:!?]$/.test(url)) {
trailing = url.slice(-1) + trailing;
url = url.slice(0, -1);
}
const href = safeHttpHref(url);
segments.push(href ? { text: url, href } : { text: url });
if (trailing) segments.push({ text: trailing });
last = match.index + match[0].length;
}
if (last < input.length) segments.push({ text: input.slice(last) });
return segments;
}

View File

@@ -209,6 +209,7 @@
"tests/unit/headroom-proxy-lifecycle.test.ts",
"tests/unit/livews-forward-backoff-4604.test.ts",
"tests/unit/management-auth-hardening.test.ts",
"tests/unit/mark-account-unavailable-numeric-epoch-guard.test.ts",
"tests/unit/model-lockout-max-cooldown.test.ts",
"tests/unit/no-memory-header.test.ts",
"tests/unit/non-streaming-sse-terminal-typescan-4459.test.ts",

View File

@@ -29,8 +29,13 @@ test.describe("Compression Studio (Tela A)", () => {
const playTab = page.locator('[data-testid="tab-play"]');
await expect(playTab).toBeVisible({ timeout: 30_000 });
// Play view is the default tab → its lane proves the studio body mounted.
await expect(page.locator('[data-testid="play-lane"]').first()).toBeVisible({
// Play view is the default tab → its playground input proves the studio body mounted.
// NOTE: the per-lane `play-lane` buttons only render AFTER a preview-compression run
// populates `batch.lanes` (usePreviewCompression keeps `batch` null until `run()` is
// called — there is no mount auto-run). This smoke test intentionally does not drive a
// compression cascade (see the "Out of scope" note above), so asserting `play-lane`
// here can never become visible. Anchor on the always-present input panel instead.
await expect(page.locator('[data-testid="play-input"]')).toBeVisible({
timeout: 30_000,
});
});

View File

@@ -168,7 +168,20 @@ test("callLogs.ts wires no-log and PII sanitization before persistence", () => {
content.includes('from "../compliance"') || content.includes('from "../compliance/noLog"'),
"callLogs.ts should import compliance module"
);
assert.ok(content.includes('from "../piiSanitizer"'), "callLogs.ts should import piiSanitizer");
// PII sanitization for error strings was extracted to callLogs/format.ts by #5725
// (sanitizeErrorForLog); callLogs.ts still wires it in before persistence, and the
// extracted helper keeps the piiSanitizer dependency — so the "sanitize before
// persist" invariant holds post-refactor (verified on both the helper and the file).
assert.ok(
content.includes("sanitizeErrorForLog") && content.includes('from "./callLogs/format"'),
"callLogs.ts should wire the extracted PII-sanitizing error helper (sanitizeErrorForLog)"
);
const formatHelperContent = readIfExists("src/lib/usage/callLogs/format.ts");
assert.ok(formatHelperContent, "src/lib/usage/callLogs/format.ts should exist");
assert.ok(
formatHelperContent.includes('from "../../piiSanitizer"'),
"callLogs/format.ts should import piiSanitizer (PII sanitization still wired post-#5725)"
);
assert.ok(content.includes("isNoLog("), "callLogs.ts should check no-log policy");
const payloadHelperContent = readIfExists("src/lib/logPayloads.ts");

View File

@@ -0,0 +1,39 @@
/**
* #5757 guard fixture — NOT a test file (no `.test.` in the name, so the runner
* skips it). Exercised by `tests/unit/tsx-runtime-transform-5757.test.ts`.
*
* It concentrates the modern JS/TS syntax that the published CLI's runtime
* `tsx/esm` loader (`bin/omniroute.mjs` → `await import("tsx/esm")`) must
* transform through esbuild at startup: object/array destructuring + rest,
* object/array spread, class + private fields, optional chaining, nullish
* coalescing, logical assignment, async/await and top-level await.
*
* If a future esbuild (pulled transitively via `tsx`) cannot transform this on a
* supported Node runtime, running this file fails — which is the whole point.
*/
class Box {
value = 41; // public class field
#secret = 1; // private field
bump(): number {
this.value += this.#secret;
return this.value;
}
}
async function main() {
const { a, b, ...rest } = { a: 1, b: 2, c: 3, d: 4 }; // object destructuring + rest
const [first, ...tail] = [10, 20, 30]; // array destructuring + rest
const merged = { ...rest, first }; // object spread
const arr = [...tail, first]; // array spread
const bumped = new Box().bump(); // class + private field
const maybe: { x?: { y?: number } } = {};
const opt = maybe?.x?.y ?? 99; // optional chaining + nullish coalescing
let acc = 0;
acc ||= bumped; // logical assignment
const total = await Promise.resolve(a + b + first + opt + acc); // async/await
return { a, b, rest, first, tail, merged, arr, bumped, opt, total };
}
const result = await main(); // top-level await
console.log("TSX_TRANSFORM_OK " + JSON.stringify(result));

View File

@@ -61,13 +61,14 @@ test("permissions modal switch buttons declare button type", () => {
selfServiceBlock.match(/<button\s+type="button"\s+role="switch"/g) ?? []
).length;
// Self-service Visibility block has 4 switches: own-usage visibility,
// shared-account quota visibility, disable-non-public-models (#3041), and the
// per-key local usage command allowance (#4034).
// Self-service Visibility block has 5 switches: own-usage visibility,
// shared-account quota visibility, disable-non-public-models (#3041), the
// per-key local usage command allowance (#4034), and the API-key provider
// quota-policy bypass scope (#5731).
// The invariant is that every switch declares type="button"
// (typedSwitchButtonCount === switchButtonCount) to avoid implicit submit.
assert.equal(switchButtonCount, 4);
assert.equal(typedSwitchButtonCount, 4);
assert.equal(switchButtonCount, 5);
assert.equal(typedSwitchButtonCount, 5);
});
test("permissions modal exposes Claude Code default wildcard model", () => {

View File

@@ -0,0 +1,57 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
// #5542 — On Windows, npm is `npm.cmd`; Node ≥24 refuses to execFile a `.cmd`
// without a shell (nodejs/node#52554), so the in-app auto-update flow threw
// "spawn npm ENOENT" for the version lookup, dependency install, global install,
// and native rebuild. Those npm calls now go through buildNpmExecOptions (the
// same win32-shell helper the embedded-services installer uses, fix #5379).
const { buildNpmExecOptions, SERVICE_VERSION_PATTERN } = await import(
"../../src/lib/services/installers/utils.ts"
);
test("#5542 npm exec options enable the shell on win32 (resolves npm.cmd → no ENOENT)", () => {
const win = buildNpmExecOptions("win32", { cwd: "/x", timeoutMs: 1000 });
assert.equal(win.shell, true, "win32 must enable the shell so npm.cmd resolves");
assert.equal(win.timeout, 1000);
assert.equal(win.cwd, "/x");
const linux = buildNpmExecOptions("linux", { cwd: "/x", timeoutMs: 1000 });
assert.notEqual(linux.shell, true, "non-win32 must not enable the shell");
});
test("#5542 the update version spec is validated before it is shell-joined (Hard Rule #13)", () => {
assert.ok(SERVICE_VERSION_PATTERN.test("3.8.43"));
assert.ok(SERVICE_VERSION_PATTERN.test("3.8.43-beta.1"));
assert.ok(!SERVICE_VERSION_PATTERN.test("1.0.0; rm -rf /"));
assert.ok(!SERVICE_VERSION_PATTERN.test("$(whoami)"));
assert.ok(!SERVICE_VERSION_PATTERN.test("1.0 && curl evil"));
});
test("#5542 the auto-update npm call sites route through buildNpmExecOptions", () => {
const routeSrc = fs.readFileSync(
new URL("../../src/app/api/system/version/route.ts", import.meta.url),
"utf8"
);
const checkSrc = fs.readFileSync(
new URL("../../src/lib/system/versionCheck.ts", import.meta.url),
"utf8"
);
assert.ok(routeSrc.includes("buildNpmExecOptions"), "version route must use the win32-shell helper");
assert.ok(checkSrc.includes("buildNpmExecOptions"), "versionCheck must use the win32-shell helper");
// The global install spec must be guarded before it reaches the shell.
assert.ok(
routeSrc.includes("SERVICE_VERSION_PATTERN.test(latest)"),
"version route must validate the version spec before shell-joining it"
);
// Every npm invocation in the route must pass buildNpmExecOptions (not a bare
// inline options object that would lack the win32 shell).
const npmCalls = routeSrc.match(/execFileAsync\(\s*\n?\s*"npm",/g) || [];
const npmViaHelper = routeSrc.match(/"npm",[\s\S]{0,120}?buildNpmExecOptions\(/g) || [];
assert.equal(
npmViaHelper.length,
npmCalls.length,
`all ${npmCalls.length} npm calls must route through buildNpmExecOptions`
);
});

View File

@@ -27,8 +27,17 @@ const NOISE = [
function seedSamples() {
// retention "always" forces a capture even on a successful (non-failure) output.
// The two captures MUST differ in content: the raw-output filename is keyed on
// Date.now() (ms) + a hash of the content (see rawOutput.ts), so two BYTE-IDENTICAL
// captures that land in the same millisecond collapse to the same filename — the 2nd
// write overwrites the 1st, leaving only 1 sample. That made this test ~25% flaky on
// fast CI runners ("expected 2, got 1"). Distinct content = 2 files regardless of
// timing; two real build runs never emit byte-identical output anyway.
maybePersistRtkRawOutput(NOISE, { retention: "always", command: "gradle build" });
maybePersistRtkRawOutput(NOISE, { retention: "always", command: "gradle build" });
maybePersistRtkRawOutput(`${NOISE}\n> Task :app:test UP-TO-DATE`, {
retention: "always",
command: "gradle build",
});
}
beforeEach(() => {

View File

@@ -0,0 +1,83 @@
// Split-guard for the cursorAgentProtobuf wire-primitive extraction (god-file
// decomposition): the low-level protobuf wire codec (varint/tag/length-delimited
// encode+decode + the generic field walker) moved verbatim from cursorAgentProtobuf.ts
// into cursorAgentProtobuf/wire.ts. These primitives were module-private, so the host's
// public API is unchanged; the host imports them back internally. The locks pin the
// leaf's surface, the encode↔decode round-trip invariants, the overrun guard, and that
// the host now imports the wire leaf instead of defining the primitives inline.
import { test } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import * as wire from "../../open-sse/utils/cursorAgentProtobuf/wire.ts";
test("wire leaf exposes the primitives and wire-type constants", () => {
assert.equal(wire.WT_VARINT, 0);
assert.equal(wire.WT_LEN, 2);
for (const fn of [
"encodeVarint",
"encodeTag",
"encodeBytes",
"encodeString",
"encodeMessage",
"encodeUInt32Field",
"encodeBoolField",
"encodeDoubleField",
"decodeVarint",
"checkedLen",
"decodeFields",
"findField",
"decodeStringField",
"decodeVarintField",
]) {
assert.equal(
typeof (wire as Record<string, unknown>)[fn],
"function",
`${fn} must be exported`
);
}
});
test("varint round-trips across byte-boundaries and bigints", () => {
for (const n of [0, 1, 127, 128, 300, 16384, 2 ** 31]) {
const [decoded, next] = wire.decodeVarint(wire.encodeVarint(n), 0);
assert.equal(decoded, BigInt(n), `varint ${n}`);
assert.equal(next, wire.encodeVarint(n).length);
}
const big = 9007199254740993n; // > Number.MAX_SAFE_INTEGER
assert.equal(wire.decodeVarint(wire.encodeVarint(big), 0)[0], big);
});
test("string / uint32 / bool fields round-trip through decodeFields", () => {
assert.equal(wire.decodeStringField(wire.encodeString(1, "héllo"), 1), "héllo");
assert.equal(wire.decodeStringField(wire.encodeString(3, ""), 3), "");
assert.equal(wire.decodeVarintField(wire.encodeUInt32Field(2, 42), 2), 42);
assert.equal(wire.decodeVarintField(wire.encodeBoolField(4, true), 4), 1);
assert.equal(wire.decodeVarintField(wire.encodeBoolField(4, false), 4), 0);
});
test("decodeFields tags length-delimited vs varint fields and encodeMessage nests", () => {
const nested = wire.encodeMessage(5, [wire.encodeString(1, "x"), wire.encodeUInt32Field(2, 7)]);
const [outer] = wire.decodeFields(nested);
assert.equal(outer.fieldNumber, 5);
assert.equal(outer.wireType, 2);
if (outer.wireType === 2) {
const inner = wire.decodeFields(outer.bytes);
assert.equal(inner.length, 2);
assert.equal(wire.decodeStringField(outer.bytes, 1), "x");
assert.equal(wire.decodeVarintField(outer.bytes, 2), 7);
}
});
test("checkedLen rejects a length that overruns the buffer", () => {
assert.throws(() => wire.checkedLen(5n, 0, Buffer.alloc(3)), /overruns buffer/);
assert.equal(wire.checkedLen(3n, 0, Buffer.alloc(3)), 3);
});
test("host imports the wire leaf and no longer defines the primitives inline", () => {
const host = fs.readFileSync(path.join("open-sse", "utils", "cursorAgentProtobuf.ts"), "utf-8");
assert.match(host, /from "\.\/cursorAgentProtobuf\/wire\.ts"/);
assert.doesNotMatch(host, /^function encodeVarint\(/m, "encodeVarint must live in the wire leaf");
assert.doesNotMatch(host, /^function decodeFields\(/m, "decodeFields must live in the wire leaf");
});

View File

@@ -152,3 +152,32 @@ test("Grok Build OAuth Provider - mapTokens falls back to 21600 if no exp or exp
assert.equal(result.expiresIn, 21600);
});
// #5775 follow-up: an already-expired token must NOT produce a negative expiresIn.
// A negative value is truthy in the import-token route (route.ts), yielding a PAST
// expiresAt that AutoCombo (virtualFactory.ts) reads as "already expired" and excludes
// the connection immediately — instead of clamping to a tiny positive TTL so the token
// is treated as due-for-refresh. Clamp with Math.max(1, …).
test("Grok Build OAuth Provider - mapTokens clamps expired JWT exp to a positive expiresIn", () => {
const pastSec = Math.floor(Date.now() / 1000) - 3600; // expired 1h ago
const payload = { sub: "12345", email: "test@example.com", exp: pastSec };
const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString("base64url");
const mockJwt = `eyJhbGciOiJFUzI1NiJ9.${payloadBase64}.signature`;
const result = grokCli.mapTokens(mockJwt, null);
assert.ok(result.expiresIn >= 1, `expected expiresIn >= 1, got ${result.expiresIn}`);
});
test("Grok Build OAuth Provider - mapTokens clamps expired JSON expires_at to a positive expiresIn", () => {
const pastDateStr = new Date(Date.now() - 3600 * 1000).toISOString(); // expired 1h ago
const authJson = {
"https://auth.x.ai::clientId": {
key: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.signature",
refresh_token: "test-refresh-token",
expires_at: pastDateStr,
},
};
const result = grokCli.mapTokens(authJson, null);
assert.ok(result.expiresIn >= 1, `expected expiresIn >= 1, got ${result.expiresIn}`);
});

View File

@@ -122,7 +122,7 @@ describe("injectMemory — GLM providers use user role (#1701)", () => {
// ── normalizeSystemRole — GLM model names ──────────────────────────────────────
describe("normalizeSystemRole — GLM model names (#1701)", () => {
it("should convert system→user for glm-5.1", () => {
it("should preserve system role for glm-5.1 (GLM 5.1+ accepts system, #5610)", () => {
const messages = [
{ role: "system", content: "Memory context: test" },
{ role: "user", content: "Hello" },
@@ -130,7 +130,10 @@ describe("normalizeSystemRole — GLM model names (#1701)", () => {
const result = normalizeSystemRole(messages, "glm", "glm-5.1");
assert.ok(Array.isArray(result));
const roles = (result as { role: string }[]).map((m) => m.role);
assert.ok(!roles.includes("system"), "system role should be converted");
// GLM 5.1 / 5.2 (and newer) accept the `system` role per z.ai docs (#5610), so it
// must NOT be folded into the first user turn — unlike bare "glm" and the 4.x / 5.0
// families (covered below), which still reject system and get converted.
assert.ok(roles.includes("system"), "system role should be preserved for glm-5.1");
assert.ok(roles.includes("user"), "should have user role");
});

View File

@@ -0,0 +1,71 @@
/**
* #5777 follow-up — root-cause regression guard for the custom-alias store.
*
* The standalone production build makes webpack emit TWO copies of
* `open-sse/services/modelDeprecation.ts`: one hydrated at startup
* (`applyRuntimeSettings` → `setCustomAliases`) for request routing, and one used by
* `GET /api/settings/model-aliases`. When the store was a plain module-level
* `let _customAliases`, each copy had its own state, so the API route read an empty
* map after restart (#5777). The store is now backed by `globalThis` so BOTH module
* instances share one object — the same #5312 pattern used by thinkingBudget.ts /
* backgroundTaskDetector.ts.
*
* These tests fail on the old plain-`let` implementation: it never touches globalThis
* (test 1) and never reads a value another instance wrote to globalThis (test 2).
*/
import test from "node:test";
import assert from "node:assert/strict";
const GLOBAL_KEY = "__omniroute_customAliases__";
const g = globalThis as unknown as Record<string, Record<string, string> | undefined>;
const modelDeprecation = await import("../../open-sse/services/modelDeprecation.ts");
test.beforeEach(() => {
delete g[GLOBAL_KEY];
});
test.after(() => {
delete g[GLOBAL_KEY];
});
test("#5777: setCustomAliases writes through globalThis (store is not a per-module let)", () => {
modelDeprecation.setCustomAliases({ "old-model": "new-model" });
// A plain module-level `let` would never populate globalThis; the globalThis-backed
// store does. This is what lets a second webpack module instance see the write.
assert.deepEqual(
g[GLOBAL_KEY],
{ "old-model": "new-model" },
"custom aliases must live on globalThis so both webpack module graphs share them"
);
});
test("#5777: reads reflect a value written by another module instance via globalThis", () => {
// Simulate the OTHER webpack module instance (the startup/instrumentation graph)
// hydrating the shared store — this module instance did NOT call setCustomAliases.
g[GLOBAL_KEY] = { "claude-opus-4-8": "mimo/mimo-v2.5-pro" };
// With the plain-`let` store this module's own copy would still be empty (the #5777
// bug). With the globalThis backing the read reflects the other instance's write.
assert.deepEqual(modelDeprecation.getCustomAliases(), {
"claude-opus-4-8": "mimo/mimo-v2.5-pro",
});
assert.equal(modelDeprecation.resolveModelAlias("claude-opus-4-8"), "mimo/mimo-v2.5-pro");
assert.equal(
modelDeprecation.getAllAliases()["claude-opus-4-8"],
"mimo/mimo-v2.5-pro",
"getAllAliases must merge the globalThis-backed custom aliases over built-ins"
);
});
test("#5777: addCustomAlias / removeCustomAlias mutate the shared globalThis store", () => {
modelDeprecation.setCustomAliases({});
modelDeprecation.addCustomAlias("foo", "bar");
assert.equal(g[GLOBAL_KEY]?.foo, "bar", "addCustomAlias must mutate the globalThis store");
assert.equal(modelDeprecation.resolveModelAlias("foo"), "bar");
assert.equal(modelDeprecation.removeCustomAlias("foo"), true);
assert.equal(g[GLOBAL_KEY]?.foo, undefined, "removeCustomAlias must clear it from globalThis");
assert.equal(modelDeprecation.removeCustomAlias("missing"), false);
});

View File

@@ -0,0 +1,50 @@
import test from "node:test";
import assert from "node:assert/strict";
// #5486 — GitLab Duo's OAuth setup error embeds a registration URL
// (https://gitlab.com/-/profile/applications) but the OAuth error step rendered
// it as dead red text. linkifyText splits the message so the URL becomes a
// clickable <a> while the surrounding instructions stay plain text.
const { linkifyText } = await import("../../src/shared/utils/linkify.ts");
test("#5486 linkifies the GitLab Duo applications URL in the setup error", () => {
const msg =
"GitLab Duo OAuth is not configured. Register an OAuth application at " +
"https://gitlab.com/-/profile/applications with redirect URI " +
"http://localhost:20128/callback and scopes then restart.";
const segs = linkifyText(msg);
const links = segs.filter((s) => s.href);
assert.equal(links.length, 2, "both URLs must become links");
assert.equal(links[0].href, "https://gitlab.com/-/profile/applications");
assert.equal(links[1].href, "http://localhost:20128/callback");
// Reassembling the segment texts must reproduce the original message verbatim.
assert.equal(segs.map((s) => s.text).join(""), msg);
});
test("#5486 trailing sentence punctuation is peeled out of the URL", () => {
const segs = linkifyText("See https://gitlab.com/-/profile/applications.");
const link = segs.find((s) => s.href);
assert.equal(link?.href, "https://gitlab.com/-/profile/applications", "period not part of href");
assert.equal(segs[segs.length - 1].text, ".", "period kept as trailing text");
});
test("#5486 plain text with no URL is a single text segment", () => {
assert.deepEqual(linkifyText("no links here"), [{ text: "no links here" }]);
assert.deepEqual(linkifyText(""), []);
assert.deepEqual(linkifyText(null as unknown as string), []);
});
test("#5486 (hardening) only ever exposes http(s) hrefs — no javascript:/data: scheme", () => {
// The matcher requires an http(s):// prefix AND safeHttpHref validates the scheme, so a
// javascript:/data: "URL" embedded in text never becomes a clickable href — it stays
// plain text. Regression guard for the CodeQL js/xss + url-redirection sink on the <a href>.
const segs = linkifyText(
"run javascript:alert(document.cookie) or data:text/html,alert(1) " +
"but https://safe.example.com/ok is fine"
);
const hrefs = segs.filter((s) => s.href).map((s) => s.href as string);
assert.deepEqual(hrefs, ["https://safe.example.com/ok"], "only the http(s) URL is a link");
for (const h of hrefs) {
assert.ok(/^https?:\/\//.test(h), `href must be http(s): ${h}`);
}
});

View File

@@ -0,0 +1,88 @@
// Split-guard for the openai-to-gemini helpers extraction (god-file decomposition):
// the pure historical-tool-context builders, undefined-pruning, thought-signature
// extraction, tool-name remapping, the Vertex provider check, and the Antigravity
// generation-config defaults moved verbatim from openai-to-gemini.ts into
// openai-to-gemini/helpers.ts. These were module-private, so the translator's public
// API is unchanged; the host imports them back internally. The locks pin the leaf's
// pure behaviour and that the host now imports the leaf.
import { test } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import * as h from "../../open-sse/translator/request/openai-to-gemini/helpers.ts";
test("isVertexGeminiProvider matches only the vertex provider ids", () => {
assert.equal(h.isVertexGeminiProvider("vertex"), true);
assert.equal(h.isVertexGeminiProvider("vertex-partner"), true);
assert.equal(h.isVertexGeminiProvider("openai"), false);
assert.equal(h.isVertexGeminiProvider(undefined), false);
});
test("buildChangedToolNameMap keeps only renamed entries, else null", () => {
const changed = h.buildChangedToolNameMap(
new Map([
["a", "a"],
["b_sanitized", "b"],
])
);
assert.deepEqual([...(changed ?? new Map()).entries()], [["b_sanitized", "b"]]);
assert.equal(h.buildChangedToolNameMap(new Map([["a", "a"]])), null);
});
test("extractClientThoughtSignature reads the first non-empty signature field", () => {
assert.equal(h.extractClientThoughtSignature({ thoughtSignature: "sig" }), "sig");
assert.equal(h.extractClientThoughtSignature({ function: { thought_signature: "s2" } }), "s2");
assert.equal(h.extractClientThoughtSignature({ thoughtSignature: "" }), null);
assert.equal(h.extractClientThoughtSignature({}), null);
assert.equal(h.extractClientThoughtSignature(null), null);
});
test("deepCleanUndefined deletes '[undefined]' string values in place, recursively", () => {
const obj = { a: 1, b: "[undefined]", c: { d: "[undefined]", e: 2 }, f: ["[undefined]"] };
h.deepCleanUndefined(obj);
assert.deepEqual(obj, { a: 1, c: { e: 2 }, f: ["[undefined]"] });
});
test("applyAntigravityGenerationDefaults fills topK/topP and bumps maxOutputTokens past the budget", () => {
assert.deepEqual(h.applyAntigravityGenerationDefaults({}), { topK: 40, topP: 1 });
assert.deepEqual(h.applyAntigravityGenerationDefaults({ topK: 5 }), { topK: 5, topP: 1 });
const withBudget = h.applyAntigravityGenerationDefaults({
thinkingConfig: { thinkingBudget: 100, includeThoughts: true },
});
assert.equal(withBudget.maxOutputTokens, 101);
});
test("historical-tool-context builders stringify and escape as expected", () => {
assert.equal(h.stringifyHistoricalToolArguments("raw"), "raw");
assert.equal(h.stringifyHistoricalToolArguments({ a: 1 }), '{"a":1}');
assert.equal(h.stringifyHistoricalToolArguments(undefined), "{}");
assert.equal(
h.buildInertHistoricalToolCallText("foo", { a: 1 }),
'[tool_history_call: foo] {"a":1}'
);
assert.equal(
h.buildInertHistoricalToolResponseText("bar", "ok"),
"[tool_history_result: bar] ok"
);
// Attribute escaping includes quotes; content escaping does not.
assert.equal(h.escapeHistoricalContextAttribute('<t>"&'), "&lt;t&gt;&quot;&amp;");
assert.equal(h.escapeHistoricalContextContent('<t>"&'), '&lt;t&gt;"&amp;');
});
test("buildHistoricalToolResultContext wraps escaped source + result in the context tag", () => {
assert.equal(
h.buildHistoricalToolResultContext("myTool", { r: 1 }),
'<previous_tool_result_context source="myTool">\n{"r":1}\n</previous_tool_result_context>'
);
});
test("host imports the helpers leaf and no longer defines them inline", () => {
const host = fs.readFileSync(
path.join("open-sse", "translator", "request", "openai-to-gemini.ts"),
"utf-8"
);
assert.match(host, /from "\.\/openai-to-gemini\/helpers\.ts"/);
assert.doesNotMatch(host, /^function deepCleanUndefined\(/m);
assert.doesNotMatch(host, /^type GeminiGenerationConfig =/m);
});

View File

@@ -0,0 +1,80 @@
/**
* #5757 — regression guard for the runtime `tsx/esm` → esbuild transform path.
*
* Background: `tsx` is a runtime `dependency` (not dev), and the published CLI
* registers it at boot (`bin/omniroute.mjs` → `await import("tsx/esm")`) to load
* OmniRoute's own `.ts` sources. A fresh `npm install omniroute` therefore pulls
* `esbuild` transitively via `tsx`. #5757 worried a broken esbuild could make a
* fresh install "build-fragile", and proposed forcing `esbuild@0.27.4`.
*
* That override is unsafe: `tsx` declares `esbuild@~0.28.0` and `fumadocs-mdx`
* (also a runtime dep) declares `esbuild@^0.28.0`; forcing 0.27.x pushes esbuild
* below both. So instead of pinning a version, these two tests guard the actual
* invariants:
* 1. the runtime tsx/esm loader still transforms modern syntax correctly, and
* 2. the resolved esbuild stays inside tsx's declared range (so nobody
* reintroduces the out-of-range override this issue proposed).
*
* Run: node --import tsx/esm --test tests/unit/tsx-runtime-transform-5757.test.ts
*/
import test from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(__dirname, "..", "..");
const FIXTURE = join(__dirname, "_fixtures", "tsx-runtime-modern-syntax.ts");
test("#5757: runtime tsx/esm loader transforms modern syntax (esbuild functional guard)", () => {
// Exactly the mechanism bin/omniroute.mjs uses. cwd = package root so `tsx`
// resolves regardless of where the process is launched from (see #4055).
const res = spawnSync(process.execPath, ["--import", "tsx/esm", FIXTURE], {
cwd: REPO_ROOT,
encoding: "utf8",
timeout: 60_000,
});
assert.equal(res.status, 0, `tsx/esm failed to run the fixture:\n${res.stderr}`);
const line = (res.stdout || "").split("\n").find((l) => l.startsWith("TSX_TRANSFORM_OK"));
assert.ok(
line,
`missing TSX_TRANSFORM_OK marker.\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`
);
const payload = JSON.parse(line!.slice("TSX_TRANSFORM_OK ".length)) as {
rest: Record<string, number>;
arr: number[];
bumped: number;
opt: number;
total: number;
};
assert.equal(payload.bumped, 42); // class field + private field lowering
assert.equal(payload.opt, 99); // optional chaining + nullish coalescing
assert.equal(payload.total, 154); // destructuring/spread/async/logical-assign
assert.deepEqual(payload.rest, { c: 3, d: 4 });
assert.deepEqual(payload.arr, [20, 30, 10]);
});
test("#5757: resolved esbuild stays within tsx's declared range (blocks the out-of-range override)", () => {
const tsxPkg = JSON.parse(
readFileSync(join(REPO_ROOT, "node_modules", "tsx", "package.json"), "utf8")
) as { dependencies?: Record<string, string> };
const esbuildPkg = JSON.parse(
readFileSync(join(REPO_ROOT, "node_modules", "esbuild", "package.json"), "utf8")
) as { version: string };
const declared = tsxPkg.dependencies?.esbuild ?? "";
// e.g. "~0.28.0" → base minor "0.28". Self-maintaining: when tsx bumps its
// esbuild range, this guard follows it automatically.
const baseMinor = declared.replace(/^[~^]/, "").split(".").slice(0, 2).join(".");
assert.ok(baseMinor, `could not parse tsx's declared esbuild range: "${declared}"`);
assert.ok(
esbuildPkg.version.startsWith(baseMinor + "."),
`esbuild ${esbuildPkg.version} is outside tsx's declared range "${declared}". ` +
`A global esbuild override (such as the esbuild@0.27.4 workaround proposed in #5757) ` +
`breaks tsx and fumadocs-mdx (both require esbuild@^0.28) — do not add one.`
);
});

View File

@@ -0,0 +1,25 @@
import test from "node:test";
import assert from "node:assert/strict";
// #5442 — LMArena (and any provider with no live validator) returns
// `{ unsupported: true }` from /api/providers/validate; Save still succeeds.
// The Add-API-Key modal only had success/failed states, so it rendered a red
// "Invalid" badge for those providers even though the key was saved fine. The
// "unsupported" result now maps to a neutral info "N/A" badge, not "Invalid".
const { validationBadgeProps } = await import(
"../../src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts"
);
test("#5442 unsupported validation → neutral N/A badge, not red Invalid", () => {
assert.deepEqual(validationBadgeProps("unsupported"), {
variant: "info",
labelKey: "notApplicable",
});
});
test("#5442 success and failed badges are unchanged", () => {
assert.deepEqual(validationBadgeProps("success"), { variant: "success", labelKey: "valid" });
assert.deepEqual(validationBadgeProps("failed"), { variant: "error", labelKey: "invalid" });
// Any other/unknown result defaults to the error badge (fail-safe).
assert.deepEqual(validationBadgeProps("whatever"), { variant: "error", labelKey: "invalid" });
});

View File

@@ -69,6 +69,8 @@ test("web session credential metadata identifies cookie, token, and no-auth prov
placeholder: "convex-session-id=abc123...; Cookie: ...",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "convex-session-id", "convexSessionId"],
// #5465 — t3.chat ships a step-by-step DevTools copy hint (localStorage + Cookie header).
hintKey: "t3ChatWebCookieHint",
});
});