mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 02:32:18 +03:00
Compare commits
22 Commits
docs/struc
...
feat/modal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4e321f1dd | ||
|
|
92499fc876 | ||
|
|
2d42c54a67 | ||
|
|
bbaa820efb | ||
|
|
964e321588 | ||
|
|
54ec0bf7a1 | ||
|
|
3eab125745 | ||
|
|
c34f69928c | ||
|
|
e66feceb76 | ||
|
|
0298b82442 | ||
|
|
60c0f19289 | ||
|
|
44eb33a256 | ||
|
|
a83f85b765 | ||
|
|
64c204f68c | ||
|
|
ee5f84c168 | ||
|
|
37ef7a7d9b | ||
|
|
f725dac4b2 | ||
|
|
904a54d602 | ||
|
|
1b9cd59740 | ||
|
|
b5c522604f | ||
|
|
1e2ef990e6 | ||
|
|
2dcb5bd422 |
@@ -0,0 +1 @@
|
||||
- **docs:** add management authentication terminology guide ([#7786](https://github.com/diegosouzapw/OmniRoute/issues/7786))
|
||||
41
.claude/worktrees/feat-7786/docs/guides/MANAGEMENT-AUTH.md
Normal file
41
.claude/worktrees/feat-7786/docs/guides/MANAGEMENT-AUTH.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Management Authentication
|
||||
|
||||
OmniRoute uses four distinct credential families for management access. This guide
|
||||
distinguishes them by purpose, scope, and locality.
|
||||
|
||||
| Credential | Scope | Locality | Use Case |
|
||||
|-------------------------|--------------------|---------------|-----------------------------------|
|
||||
| Dashboard JWT session | Full management | Localhost | Web dashboard login |
|
||||
| CLI machine-id token | Full management | Per-machine | `omniroute` CLI commands |
|
||||
| Scoped `oma_` token | Configurable scope | External | Automation / CI / API access |
|
||||
| Manage-scope API key | `manage` scope | External | Management API calls |
|
||||
|
||||
## Dashboard JWT Session
|
||||
|
||||
Generated on dashboard login (`/api/auth/login`). Stored in HTTP-only cookie.
|
||||
Valid for the session duration. Cannot be used from external hosts.
|
||||
|
||||
## CLI Machine-ID Token
|
||||
|
||||
Created by `omniroute auth login` on first use. Stored in `~/.omniroute/auth.json`.
|
||||
Used by the CLI for all management operations. Tied to the machine identity.
|
||||
|
||||
## Scoped `oma_` Access Token
|
||||
|
||||
Created via dashboard or CLI with configurable scopes (e.g., `manage`, `read`).
|
||||
Format: `oma_<random-hex>`. Used for programmatic access from external systems.
|
||||
|
||||
## Manage-Scope API Key
|
||||
|
||||
Standard API key with the `manage` scope enabled. Created in dashboard API Keys page.
|
||||
Used for management API calls from external hosts.
|
||||
|
||||
## Header Examples
|
||||
|
||||
```
|
||||
Authorization: Bearer oma_abc123def456
|
||||
Authorization: Bearer <standard-api-key-with-manage-scope>
|
||||
Cookie: omniroute_session=<jwt-token>
|
||||
```
|
||||
|
||||
See `docs/reference/API_REFERENCE.md` for endpoint-specific auth requirements.
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, it } from "node:test";
|
||||
import { ok } from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
describe("Management auth documentation (#7786)", () => {
|
||||
const docPath = "docs/guides/MANAGEMENT-AUTH.md";
|
||||
const content = readFileSync(docPath, "utf-8");
|
||||
|
||||
it("exists and has content", () => {
|
||||
ok(content.length > 500, "should have substantial content");
|
||||
ok(content.includes("Dashboard JWT session"));
|
||||
ok(content.includes("CLI machine-id token"));
|
||||
ok(content.includes("oma_"));
|
||||
});
|
||||
|
||||
it("documents all four credential families", () => {
|
||||
const families = ["Dashboard JWT", "CLI machine-id", "oma_", "Manage-scope"];
|
||||
for (const f of families) {
|
||||
ok(content.includes(f), `should document ${f}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("mentions relevant auth header examples", () => {
|
||||
ok(content.includes("Authorization"));
|
||||
ok(content.includes("Bearer"));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(infra):** add systemd autostart unit for Linux ([#8635](https://github.com/diegosouzapw/OmniRoute/issues/8635))
|
||||
@@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=OmniRoute AI Proxy
|
||||
After=network.target network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=$(which omniroute) start
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
Environment=NODE_ENV=production
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=full
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, it } from "node:test";
|
||||
import { ok } from "node:assert/strict";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
|
||||
describe("Systemd autostart (#8635)", () => {
|
||||
const svcPath = "contrib/systemd/omniroute.service";
|
||||
const content = readFileSync(svcPath, "utf-8");
|
||||
|
||||
it("service file exists", () => {
|
||||
ok(existsSync(svcPath));
|
||||
ok(content.length > 200);
|
||||
});
|
||||
|
||||
it("defines required systemd sections", () => {
|
||||
ok(content.includes("[Unit]"));
|
||||
ok(content.includes("[Service]"));
|
||||
ok(content.includes("[Install]"));
|
||||
});
|
||||
|
||||
it("specifies WantedBy=default.target", () => {
|
||||
ok(content.includes("WantedBy=default.target"));
|
||||
});
|
||||
});
|
||||
117
.env.example
117
.env.example
@@ -350,18 +350,6 @@ ALLOW_API_KEY_REVEAL=false
|
||||
# by OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT and the heap-pressure shed instead. Set a positive
|
||||
# value only on memory-constrained deployments that need a hard ceiling.
|
||||
# OMNIROUTE_CHAT_HARD_MAX_MESSAGES=0
|
||||
# How long a heavy request waits for heavyweight capacity before a retryable 503.
|
||||
# A short bounded wait serializes agent bursts instead of an instant 503; 0 = instant.
|
||||
# Default 2000 (2s).
|
||||
# OMNIROUTE_CHAT_ADMISSION_QUEUE_MS=2000
|
||||
# Queued-bytes budget for the admission wait: bounds total buffered body bytes parked
|
||||
# per lane so the wait cannot amplify the heap (#4380). Over-budget waits 503 immediately.
|
||||
# Default 4194304 (4 MB).
|
||||
# OMNIROUTE_CHAT_ADMISSION_MAX_QUEUED_BYTES=4194304
|
||||
# Per-connection virtual admission lanes (#9654): idle-lane eviction TTL. Default 60000 (60s).
|
||||
# OMNIROUTE_CHAT_VIRTUAL_TTL_MS=60000
|
||||
# Per-connection virtual admission lanes (#9654): max concurrent sessions (lanes). Default 64.
|
||||
# OMNIROUTE_CHAT_VIRTUAL_MAX_SESSIONS=64
|
||||
|
||||
# Hard cap (bytes) for a non-streaming upstream response buffered fully into memory
|
||||
# (#5152). Past this the upstream reader is cancelled and the request fails fast
|
||||
@@ -656,9 +644,6 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
|
||||
# Reduces risk of JA3/JA4 fingerprint-based blocking by providers (e.g., Google).
|
||||
# Used by: open-sse/executors — replaces Node.js default TLS fingerprint.
|
||||
# ENABLE_TLS_FINGERPRINT=true
|
||||
# New proxied TLS routing requires an explicit, comma-separated provider allowlist.
|
||||
# Direct TLS keeps its legacy behavior when this is unset.
|
||||
# TLS_FINGERPRINT_PROVIDERS=codex,openai
|
||||
|
||||
# Allow the Claude Turnstile Playwright browser context to ignore HTTPS certificate errors.
|
||||
# Only enable for local debugging or trusted MITM/corporate proxy environments.
|
||||
@@ -977,17 +962,18 @@ CODEX_OAUTH_CLIENT_ID=app_EMoamEEZ73f0CkXaXp7hrann
|
||||
# Used by: open-sse/executors/theoldllm.ts. Default: 30000 (30s).
|
||||
# THEOLDLLM_NAV_TIMEOUT_MS=30000
|
||||
|
||||
# ── Gemini / Antigravity (Google-based) ──
|
||||
# These providers ship public OAuth client_id/secret values embedded in their
|
||||
# public CLIs. Defaults are baked into the code via
|
||||
# open-sse/utils/publicCreds.ts — leave the env vars unset to use them. Only
|
||||
# set these if you registered your own OAuth app and want to use your own
|
||||
# credentials instead. See docs/security/PUBLIC_CREDS.md for context.
|
||||
# ── Gemini / Antigravity / Windsurf (all Google-based) ──
|
||||
# These providers ship public OAuth client_id/secret values (or Firebase Web
|
||||
# keys) embedded in their public CLIs/binaries. Defaults are baked into the
|
||||
# code via open-sse/utils/publicCreds.ts — leave the env vars unset to use
|
||||
# them. Only set these if you registered your own OAuth app and want to use
|
||||
# your own credentials instead. See docs/security/PUBLIC_CREDS.md for context.
|
||||
#
|
||||
# GEMINI_OAUTH_CLIENT_ID=
|
||||
# GEMINI_OAUTH_CLIENT_SECRET=
|
||||
# ANTIGRAVITY_OAUTH_CLIENT_ID=
|
||||
# ANTIGRAVITY_OAUTH_CLIENT_SECRET=
|
||||
# WINDSURF_FIREBASE_API_KEY=
|
||||
|
||||
# ── Kimi Coding (Moonshot) ──
|
||||
KIMI_CODING_OAUTH_CLIENT_ID=17e5f671-d194-4dfb-9706-5516cb48c098
|
||||
@@ -1172,12 +1158,6 @@ CURSOR_USER_AGENT="Cursor/3.4"
|
||||
# Or enable for all providers at once:
|
||||
# CLI_COMPAT_ALL=1
|
||||
|
||||
# Allow the Antigravity request translator to skip its strict CLI request-signature
|
||||
# validation when the upstream refuses real signatures (debug/antiquated-CLI mode).
|
||||
# Default: real signatures enforced (unset) — signature bypass disabled.
|
||||
# Used by: open-sse/translator/request/openai-to-gemini.ts
|
||||
# ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS=0
|
||||
|
||||
# ── Kimi Coding CLI identity overrides ──
|
||||
# Used by: src/lib/oauth/providers/kimi-coding.ts — sent in OAuth + API headers.
|
||||
# Leave unset to use the captured defaults baked into the OmniRoute build.
|
||||
@@ -1585,17 +1565,6 @@ APP_LOG_TO_FILE=true
|
||||
# 20. PROVIDER-SPECIFIC SETTINGS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# ── Strict system-message-first providers ──
|
||||
# Comma-separated, case-insensitive provider ids that require the `system`
|
||||
# role message to be the first message (any later `system` message is
|
||||
# rejected with HTTP 400 by the upstream chat template) — the same
|
||||
# constraint documented for xiaomi-mimo/mimo (#6135, #7293). Extends the
|
||||
# built-in list without a source change; useful for self-hosted connections
|
||||
# in front of Qwen3.5+/3.6 or other strict-template backends.
|
||||
# Used by: src/lib/memory/injection.ts::systemMessageMustBeFirst
|
||||
# Default: unset (only xiaomi-mimo/mimo are flagged)
|
||||
# OMNIROUTE_STRICT_SYSTEM_PROVIDERS=coding-agent
|
||||
|
||||
# ── OpenRouter ──
|
||||
# OpenRouter model catalog cache TTL in ms.
|
||||
# Used by: src/lib/catalog/openrouterCatalog.ts
|
||||
@@ -1624,19 +1593,6 @@ APP_LOG_TO_FILE=true
|
||||
# NANOBANANA_POLL_TIMEOUT_MS=120000 # Max wait for job completion (default: 120s)
|
||||
# NANOBANANA_POLL_INTERVAL_MS=2500 # Poll frequency (default: 2.5s)
|
||||
|
||||
# ── Adobe Firefly (Image / Video Generation) ──
|
||||
# Optional absolute path to a system Chrome or Edge executable used for interactive sign-in
|
||||
# and off-screen risk-session renewal. Auto-detected when unset.
|
||||
# OMNIROUTE_LOGIN_BROWSER_PATH=
|
||||
# Browser renewal and durable session cache are enabled by default; set either to 0 to opt out.
|
||||
# ADOBE_FIREFLY_BROWSER_REFRESH=1
|
||||
# ADOBE_FIREFLY_SESSION_DISK=1
|
||||
# Minimum gap between generate submissions and extra gap after every third success (ms).
|
||||
# ADOBE_FIREFLY_MIN_SUBMIT_GAP_MS=12000
|
||||
# ADOBE_FIREFLY_BATCH_EXTRA_GAP_MS=15000
|
||||
# Base backoff after a transient 408 response (ms); five attempts maximum.
|
||||
# ADOBE_FIREFLY_SUBMIT_BASE_DELAY_MS=8000
|
||||
|
||||
# ── Microsoft Designer Web (Image Generation) ──
|
||||
# Polling config for the microsoft-designer-web submit-then-poll image job.
|
||||
# Used by: open-sse/handlers/imageGeneration/providers/designerWeb.ts
|
||||
@@ -1866,17 +1822,6 @@ APP_LOG_TO_FILE=true
|
||||
# Accepted values: true|1|on (enable). Unset or anything else = disabled (default).
|
||||
# STREAM_RECOVERY_MIDSTREAM_ENABLED=true
|
||||
|
||||
# Active-stream throughput watchdog (#9709). Detects streams that keep sending
|
||||
# heartbeats/chunks but produce too little useful assistant text. Separate from
|
||||
# STREAM_IDLE_TIMEOUT_MS (silence) and the hard upstream attempt deadline. OFF by
|
||||
# default. Tool-call/reasoning phases suspend judgement; post-commit streams are
|
||||
# never blindly replayed.
|
||||
# STREAM_THROUGHPUT_WATCHDOG_ENABLED=true
|
||||
# STREAM_THROUGHPUT_WATCHDOG_WARMUP_MS=30000
|
||||
# STREAM_THROUGHPUT_WATCHDOG_WINDOW_MS=30000
|
||||
# STREAM_THROUGHPUT_WATCHDOG_MIN_BYTES_PER_SECOND=4
|
||||
# STREAM_THROUGHPUT_WATCHDOG_MIN_USEFUL_BYTES=1
|
||||
|
||||
# Stagger interval (ms) between provider token healthchecks at startup.
|
||||
# Used by: src/lib/tokenHealthCheck.ts. Default: 3000.
|
||||
# HEALTHCHECK_STAGGER_MS=3000
|
||||
@@ -1990,19 +1935,6 @@ APP_LOG_TO_FILE=true
|
||||
# ALIBABA_CODING_PLAN_HOST=
|
||||
# ALIBABA_CODING_PLAN_QUOTA_URL=
|
||||
|
||||
# ── Alibaba Model Studio free-tier quota sync ──
|
||||
# Console front-end path overrides for the free-tier quota fetcher. Used by:
|
||||
# open-sse/services/alibabaFreeTierQuotaFetcher.ts. When unset, the fetcher
|
||||
# uses the production Bailian console paths.
|
||||
# ALIBABA_FREE_TIER_VISION_FE_PATH=
|
||||
# ALIBABA_FREE_TIER_MULTIMODAL_FE_PATH=
|
||||
# ALIBABA_FREE_TIER_AUDIO_FE_PATH=
|
||||
# Optional path to a local JSON override for the built-in text free-tier
|
||||
# allowlist. Used by: open-sse/services/alibabaFreeTierAllowlist.ts. When
|
||||
# unset, the fetcher falls back to $DATA_DIR/alibaba-free-tier-allowlist.json
|
||||
# then config/alibaba-free-tier-allowlist.json.
|
||||
# ALIBABA_FREE_TIER_ALLOWLIST_PATH=
|
||||
|
||||
# ── Context window tuning ──
|
||||
# Tokens reserved for completion output when computing prompt budgets.
|
||||
# Used by: open-sse/services/contextManager.ts. Default: 1024.
|
||||
@@ -2033,13 +1965,6 @@ APP_LOG_TO_FILE=true
|
||||
# DEVIN_BRIDGE_HAIKU_MODEL=devin-cli-agentic/swe-1-7
|
||||
# DEVIN_BRIDGE_SUBAGENT_MODEL=devin-cli-agentic/swe-1-7
|
||||
|
||||
# ── Devin Desktop upstream compatibility versions ──
|
||||
# Desktop ide_version. Must use x.y.z format; invalid/unset values use 3.6.27.
|
||||
# DEVIN_DESKTOP_VERSION=3.6.27
|
||||
# Bundled Codeium/language-server extension_version, distinct from Desktop.
|
||||
# Must use x.y.z format; invalid/unset values use the bundled default 1.48.2.
|
||||
# DEVIN_DESKTOP_EXTENSION_VERSION=1.48.2
|
||||
|
||||
# ── Command Code (custom CLI) callback ──
|
||||
# Local port used for OAuth-style callbacks from the Command Code CLI helper.
|
||||
# Used by: src/app/api/providers/command-code/auth/shared.ts.
|
||||
@@ -2052,12 +1977,6 @@ APP_LOG_TO_FILE=true
|
||||
# Default: 0.33.2
|
||||
# COMMAND_CODE_VERSION=0.33.2
|
||||
|
||||
# Base URL for the Command Code usage/quota upstream, used by smartphone
|
||||
# quota-fetcher telemetry.
|
||||
# Used by: open-sse/services/usage/command-code.ts
|
||||
# Default: https://api.commandcode.ai
|
||||
# COMMANDCODE_API_URL=https://api.commandcode.ai
|
||||
|
||||
# ── MITM debug proxy (development only) ──
|
||||
# Used by: src/mitm/server.cjs — captures upstream traffic for inspection.
|
||||
# MITM_LOCAL_PORT=443
|
||||
@@ -2534,18 +2453,6 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# HYPERAGENT_USAGE_URL=https://hyperagent.com/api/settings/billing/usage
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# ChatGPT Web (Codex) headless browser and outbound tool tunnel
|
||||
# Used by: open-sse/executors/chatgpt-web-codex.ts
|
||||
# Connection values entered in the dashboard override these global defaults.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# CHATGPT_WEB_CODEX_CHROME_PATH=/usr/bin/chromium
|
||||
# CHROME_PATH=/usr/bin/chromium
|
||||
# CHATGPT_WEB_CODEX_CDP_URL=http://chatgpt-web-codex-browser:9223
|
||||
# CHATGPT_WEB_CODEX_TUNNEL_ID=tunnel_0123456789abcdef0123456789abcdef
|
||||
# CHATGPT_WEB_CODEX_RUNTIME_KEY=
|
||||
# CHATGPT_WEB_CODEX_CONNECTOR_NAME=OmniRoute Codex
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Browser-login VNC sessions (optional — src/lib/vncSession/manifest.ts)
|
||||
# Containerized Chromium+VNC used for interactive browser-login credential
|
||||
@@ -2650,13 +2557,3 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
|
||||
# TELEGRAM_DEFAULT_MODEL=auto/chat
|
||||
# TELEGRAM_BOT_API_BASE=https://api.telegram.org
|
||||
# TELEGRAM_WEBHOOK_TIMEOUT_MS=60000
|
||||
|
||||
# ── OmniConductor bridge (Conductor PRD RF1) ──────────────────────────────────
|
||||
# Mirrors the OmniConductor hub's tasks into the local A2A TaskManager via SSE.
|
||||
# Opt-in: the bridge only starts when CONDUCTOR_HUB_URL is set.
|
||||
# Token: emit a `spokesperson`-kind credential on the hub (POST /v1/peers, admin) —
|
||||
# server-side only, never exposed to the browser.
|
||||
# Used by: src/lib/conductor/boot.ts, src/lib/conductor/bridge.ts
|
||||
# CONDUCTOR_HUB_URL=http://127.0.0.1:7910
|
||||
# CONDUCTOR_HUB_TOKEN=
|
||||
feat/conductor-bridge
|
||||
|
||||
1
.eslintcache-probe
Normal file
1
.eslintcache-probe
Normal file
File diff suppressed because one or more lines are too long
4
.fakebin-9475/npm
Executable file
4
.fakebin-9475/npm
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
if [ "$1" = "view" ]; then echo "3.8.99"; exit 0; fi
|
||||
if [ "$1" = "install" ]; then echo "added 1 package"; exit 0; fi
|
||||
exit 0
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -10,7 +10,7 @@
|
||||
## Validation
|
||||
|
||||
Choose the change type and focused loop from the
|
||||
[Contribution Golden Path](../docs/ops/CONTRIBUTION_GOLDEN_PATH.md). The full unit suite,
|
||||
[Contribution Golden Path](../docs/dev/CONTRIBUTION_GOLDEN_PATH.md). The full unit suite,
|
||||
Vitest, the 60% coverage gate, and the production build all run in CI on this PR (#8329):
|
||||
|
||||
- [ ] Change type: provider / routing / UI / i18n / CLI / DB / build-deploy / other
|
||||
|
||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -501,13 +501,11 @@ jobs:
|
||||
BASE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }}
|
||||
run: node scripts/i18n/check-ui-value-drift.mjs
|
||||
|
||||
# #8038: cheap glossary/protected-terms consistency gate —
|
||||
# #8038: cheap single-locale glossary/protected-terms consistency gate —
|
||||
# complements i18n-ui-coverage (key parity) and the ICU `i18n` job below
|
||||
# without needing app-boot/Playwright infra. Same gating as i18n-ui-coverage.
|
||||
# ko added after the #8224 ko.json mistranslation cleanup so the fixed
|
||||
# terminology cannot silently regress on the next machine-translation run.
|
||||
i18n-glossary-zhcn:
|
||||
name: i18n Glossary (zh-CN, ko)
|
||||
name: i18n Glossary (zh-CN)
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.i18n == 'true' || needs.changes.outputs.code == 'true')) }}
|
||||
@@ -1336,7 +1334,7 @@ jobs:
|
||||
echo "| Lint | $(status '${{ needs.lint.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Docs Sync (Strict) | $(status '${{ needs.docs-sync-strict.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| i18n UI Coverage | $(status '${{ needs.i18n-ui-coverage.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| i18n Glossary (zh-CN, ko) | $(status '${{ needs.i18n-glossary-zhcn.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| i18n Glossary (zh-CN) | $(status '${{ needs.i18n-glossary-zhcn.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| PR Test Policy | $(status '${{ needs.pr-test-policy.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| SonarQube | $(status '${{ needs.sonarqube.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
|
||||
33
.github/workflows/quality.yml
vendored
33
.github/workflows/quality.yml
vendored
@@ -194,25 +194,6 @@ jobs:
|
||||
"$HOME/.local/bin/osv-scanner" --version || true
|
||||
"$HOME/.local/bin/oasdiff" --version || true
|
||||
zizmor --version || true
|
||||
- name: Forgotten sibling tests (advisory)
|
||||
env:
|
||||
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
node scripts/quality/build-test-impact-map.mjs
|
||||
node scripts/check/check-forgotten-sibling-tests.mjs \
|
||||
--summary-file forgotten-sibling-tests.md \
|
||||
--json-file forgotten-sibling-tests.json
|
||||
cat forgotten-sibling-tests.md >> "$GITHUB_STEP_SUMMARY"
|
||||
- name: Upload forgotten sibling report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: forgotten-sibling-tests
|
||||
path: |
|
||||
forgotten-sibling-tests.md
|
||||
forgotten-sibling-tests.json
|
||||
if-no-files-found: ignore
|
||||
retention-days: 30
|
||||
# Quality gates (all, non-fail-fast) — #8542: replaces 17 bare check:* steps,
|
||||
# 6 G0 gates, 4 ratchet gates, and 3 typecheck steps with a single aggregation
|
||||
# step. Each gate runs in a loop with ::group::; failures are collected and
|
||||
@@ -275,22 +256,15 @@ jobs:
|
||||
# arrives in 7.1, so typescript-eslint / type-coverage / Stryker stay on 6.x
|
||||
# (the hybrid is the officially documented pattern). Isolated npx on purpose:
|
||||
# installing an alias package could collide node_modules/.bin/tsc with 6.x.
|
||||
# The full result stays advisory while #8484 has a backlog. The blocking
|
||||
# base-relative ratchet immediately below rejects only diagnostics added by
|
||||
# the PR, so existing release debt does not block unrelated work.
|
||||
# Promote to the blocking gate after ~1 week of parity with the step above.
|
||||
- name: Typecheck (core) — TS7 native shadow (advisory)
|
||||
continue-on-error: true
|
||||
run: |
|
||||
RC=0
|
||||
START=$(date +%s)
|
||||
npm exec --yes --package=typescript@7.0.2 -- tsc --pretty false -p tsconfig.typecheck-core.json || RC=$?
|
||||
npx -y -p typescript@7 tsc --pretty false -p tsconfig.typecheck-core.json || RC=$?
|
||||
echo "[ts7-shadow] exit=$RC elapsed=$(( $(date +%s) - START ))s — the 6.x step above stays authoritative"
|
||||
exit $RC
|
||||
- name: Typecheck (core) — TS7 zero-new-diagnostics ratchet
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
env:
|
||||
TS7_BASE_REF: ${{ github.event.pull_request.base.sha }}
|
||||
run: npm run check:ts7-diagnostics-ratchet -- --base-ref "$TS7_BASE_REF"
|
||||
# TIA: build the impact map at runtime (gitignored, ~21MB) and run only the
|
||||
# unit tests impacted by this PR's changed files. On hub/unmapped changes the
|
||||
# selector returns __RUN_ALL__ — full-suite authority is the parallel
|
||||
@@ -305,8 +279,7 @@ jobs:
|
||||
GITHUB_BASE_REF: ${{ github.base_ref }}
|
||||
run: |
|
||||
git fetch --no-tags origin "$GITHUB_BASE_REF" || true
|
||||
# The advisory sibling-test step generates the same map earlier in this job.
|
||||
[ -f config/quality/test-impact-map.json ] || node scripts/quality/build-test-impact-map.mjs
|
||||
node scripts/quality/build-test-impact-map.mjs
|
||||
SEL="$(node scripts/quality/select-impacted-tests.mjs)"
|
||||
# Shadow evidence (#8084): persist every selection so TIA false negatives can
|
||||
# be measured against fast-unit's full-suite verdict across releases BEFORE
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,7 +1,6 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# project-specific directories
|
||||
.slim/deepwork/
|
||||
.omnivscodeagent/
|
||||
omnirouteCloud/
|
||||
omnirouteSite/
|
||||
@@ -122,8 +121,6 @@ app.log
|
||||
deploy.sh
|
||||
docker-compose.minimal.yml
|
||||
|
||||
# Docker Compose override (local-only, never commit)
|
||||
docker-compose.override.yml
|
||||
|
||||
# Backup directories
|
||||
app.__qa_backup/
|
||||
@@ -159,7 +156,6 @@ vscode-extension/
|
||||
|
||||
# Empty/dangling files
|
||||
typescript
|
||||
/MAX
|
||||
|
||||
# Gemini Antigravity agent data
|
||||
.gemini/
|
||||
@@ -204,9 +200,6 @@ scripts/i18n/_pending-keys.json
|
||||
.claude/worktrees/
|
||||
.codegraph/
|
||||
|
||||
# Test executable shims belong in the OS temporary directory, not the repository root
|
||||
/.fakebin-*/
|
||||
|
||||
# Fumadocs generated source
|
||||
.source/
|
||||
|
||||
@@ -266,7 +259,6 @@ _artifacts/ # release-green artifacts
|
||||
# ESLint file cache (npm run lint --cache / complexity ratchets)
|
||||
.eslintcache
|
||||
.eslintcache-complexity
|
||||
/.eslintcache-*
|
||||
|
||||
|
||||
# CI/local quality artifacts (eslint-results.json, quality-ratchet.md, etc.)
|
||||
|
||||
@@ -1033,11 +1033,7 @@ export const OmniRoutePlugin: Plugin = async (_input, options) => {
|
||||
// Config hook: keep existing catalog shim, and register slash command
|
||||
// templates that ask the agent to call the force-sync tool (OpenCode has no
|
||||
// Pi-style registerCommand API; tools + command templates are the native path).
|
||||
const baseConfigHook = createOmniRouteConfigHook(resolved, {
|
||||
cache: sharedCache,
|
||||
diskSnapshotReader: defaultDiskSnapshotReader,
|
||||
diskSnapshotWriter: defaultDiskSnapshotWriter,
|
||||
});
|
||||
const baseConfigHook = createOmniRouteConfigHook(resolved, { cache: sharedCache });
|
||||
const configWithSyncCommand = async (input: Config) => {
|
||||
await baseConfigHook(input);
|
||||
const cfg = input as Config & {
|
||||
@@ -4745,7 +4741,7 @@ export type OmniRouteDiskSnapshotWriter = (
|
||||
export type OmniRouteDiskSnapshotReader = (
|
||||
providerId: string,
|
||||
identityFingerprint: string
|
||||
) => Promise<(Omit<OmniRouteFetchCacheEntry, "expiresAt"> & { writtenAt?: number }) | undefined>;
|
||||
) => Promise<Omit<OmniRouteFetchCacheEntry, "expiresAt"> | undefined>;
|
||||
|
||||
/**
|
||||
* Bind a snapshot to the endpoint and effective credential tuple without
|
||||
@@ -4828,36 +4824,15 @@ export const defaultDiskSnapshotReader: OmniRouteDiskSnapshotReader = async (
|
||||
? parsed.rawCompressionCombos
|
||||
: [],
|
||||
rawConnections: Array.isArray(parsed.rawConnections) ? parsed.rawConnections : [],
|
||||
writtenAt: typeof parsed.writtenAt === "number" ? parsed.writtenAt : undefined,
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/** No-op disk-cache pair — used by tests to avoid filesystem side effects.
|
||||
* Also used as the default in createOmniRouteConfigHook so that tests
|
||||
* that don't pass a diskSnapshotReader don't read real snapshot files
|
||||
* from the user's ~/.local/share/opencode/plugins/ directory.
|
||||
* The OmniRoutePlugin function passes the real defaultDiskSnapshotReader
|
||||
* explicitly. */
|
||||
export const noopDiskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
/** No-op disk-cache pair — used by tests to avoid filesystem side effects. */
|
||||
export const noopDiskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
/**
|
||||
* In-flight refresh guard: prevents concurrent refreshes for the same
|
||||
* cacheKey. When a warm snapshot is served, the refresh runs detached; if
|
||||
* a second hook invocation arrives before the refresh completes, it should
|
||||
* piggyback on the in-flight promise rather than starting a second one.
|
||||
* Cleared on settle so it doesn't leak.
|
||||
*/
|
||||
const _inflightRefresh: Map<string, Promise<void>> = new Map();
|
||||
|
||||
/** Reset the in-flight refresh guard (for test isolation). */
|
||||
export function _resetInflightRefresh(): void {
|
||||
_inflightRefresh.clear();
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Debug logging (features.debugLog)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -5092,6 +5067,7 @@ export function createDebugLoggingFetch(
|
||||
}
|
||||
};
|
||||
}
|
||||
export const noopDiskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
|
||||
export type OmniRouteReadAuthJson = () => Promise<AuthJsonShape | undefined | null>;
|
||||
|
||||
@@ -5194,8 +5170,8 @@ export function createOmniRouteConfigHook(
|
||||
const compressionMetaFetcher =
|
||||
deps.compressionMetaFetcher ?? defaultOmniRouteCompressionMetaFetcher;
|
||||
const providersFetcher = deps.providersFetcher ?? defaultOmniRouteProvidersFetcher;
|
||||
const diskSnapshotReader = deps.diskSnapshotReader ?? noopDiskSnapshotReader;
|
||||
const diskSnapshotWriter = deps.diskSnapshotWriter ?? noopDiskSnapshotWriter;
|
||||
const diskSnapshotReader = deps.diskSnapshotReader ?? defaultDiskSnapshotReader;
|
||||
const diskSnapshotWriter = deps.diskSnapshotWriter ?? defaultDiskSnapshotWriter;
|
||||
const now = deps.now ?? Date.now;
|
||||
const cache: OmniRouteFetchCache = deps.cache ?? new Map();
|
||||
const logger = deps.logger ?? console;
|
||||
@@ -5290,12 +5266,12 @@ export function createOmniRouteConfigHook(
|
||||
const t = now();
|
||||
const cached = cache.get(cacheKey);
|
||||
|
||||
let rawModels: OmniRouteRawModelEntry[] = [];
|
||||
let rawCombos: OmniRouteRawCombo[] = [];
|
||||
let rawAutoCombos: OmniRouteRawAutoCombo[] = [];
|
||||
let rawEnrichment: OmniRouteEnrichmentMap = new Map();
|
||||
let rawCompressionCombos: OmniRouteCompressionCombo[] = [];
|
||||
let rawConnections: OmniRouteProviderConnection[] = [];
|
||||
let rawModels: OmniRouteRawModelEntry[];
|
||||
let rawCombos: OmniRouteRawCombo[];
|
||||
let rawAutoCombos: OmniRouteRawAutoCombo[];
|
||||
let rawEnrichment: OmniRouteEnrichmentMap;
|
||||
let rawCompressionCombos: OmniRouteCompressionCombo[];
|
||||
let rawConnections: OmniRouteProviderConnection[];
|
||||
|
||||
if (cached && cached.expiresAt > t) {
|
||||
rawModels = cached.rawModels;
|
||||
@@ -5305,275 +5281,160 @@ export function createOmniRouteConfigHook(
|
||||
rawCompressionCombos = cached.rawCompressionCombos;
|
||||
rawConnections = cached.rawConnections;
|
||||
} else {
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Warm startup: read the disk snapshot before fetching so the provider
|
||||
// registers immediately with the last-known-good catalog. The live
|
||||
// fetch then refreshes in the background (detached) and updates the
|
||||
// cache + snapshot. Gated by features.diskCache (default-on).
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
let warmSnapshot: Omit<OmniRouteFetchCacheEntry, "expiresAt"> | undefined;
|
||||
if (wantDiskCache) {
|
||||
const snapshotResult = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
|
||||
if (snapshotResult && snapshotResult.rawModels.length > 0) {
|
||||
warmSnapshot = snapshotResult;
|
||||
// Log snapshot age (accept any age — instant beats empty).
|
||||
const age = (snapshotResult as { writtenAt?: number }).writtenAt;
|
||||
const ageLabel = typeof age === "number" ? `${Math.round((Date.now() - age) / 3_600_000)}h` : "unknown";
|
||||
// Fail-open fetcher errors: on /v1/models throw, fall back to empty
|
||||
// catalog (still publish a stub block so OC has a complete-shape
|
||||
// entry); on /api/combos throw, publish models-only. Disk-cache
|
||||
// fallback below recovers the last-known-good catalog when the
|
||||
// fetcher threw (network down / 403 / timeout) AND features.diskCache
|
||||
// !== false. A 0-entry SUCCESS (fresh tenant) does NOT trigger
|
||||
// disk fallback — that's a valid empty catalog.
|
||||
let modelsFetchThrew = false;
|
||||
try {
|
||||
rawModels = await fetcher(baseURL, apiKey, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /v1/models fetch failed; publishing stub provider entry",
|
||||
err
|
||||
);
|
||||
rawModels = [];
|
||||
modelsFetchThrew = true;
|
||||
}
|
||||
const modelsFetchOk = !modelsFetchThrew && rawModels.length > 0;
|
||||
|
||||
rawCombos = [];
|
||||
try {
|
||||
rawCombos = await combosFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/combos fetch failed; publishing models-only static catalog",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
rawAutoCombos = [];
|
||||
if (wantAutoCombos) {
|
||||
try {
|
||||
rawAutoCombos = await autoCombosFetcher(baseURL, managementReadToken, 5_000);
|
||||
} catch {
|
||||
// Already handled inside the default fetcher
|
||||
}
|
||||
}
|
||||
|
||||
// Eagerly fetch enrichment so the static block can overlay human
|
||||
// display names on raw model ids. On OC ≤1.15.5 the dynamic
|
||||
// `provider.models` hook never fires in `serve` mode, so the static
|
||||
// block IS what reaches `/provider` and the TUI model picker.
|
||||
// Gated by `features.enrichment` (default-on). Soft-fail on error —
|
||||
// we still publish a name-less catalog if /api/pricing/models is
|
||||
// unreachable.
|
||||
rawEnrichment = new Map();
|
||||
if (wantEnrichment) {
|
||||
try {
|
||||
rawEnrichment = await enrichmentFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`[omniroute-plugin] config shim: warm startup from disk snapshot (${snapshotResult.rawModels.length} models, age ${ageLabel})`
|
||||
"[omniroute-plugin] config shim: /api/pricing/models fetch failed; publishing raw-id static catalog",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Parallel refresh: all six fetchers run concurrently via
|
||||
// Promise.allSettled. Each wrapper never rejects (catches internally)
|
||||
// so partial failure is tolerated — same soft-fail semantics as the
|
||||
// old sequential chain, but ~6x faster.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
const doRefresh = async (): Promise<void> => {
|
||||
let modelsFetchThrew = false;
|
||||
let localRawModels: OmniRouteRawModelEntry[] = [];
|
||||
let localRawCombos: OmniRouteRawCombo[] = [];
|
||||
let localRawAutoCombos: OmniRouteRawAutoCombo[] = [];
|
||||
let localRawEnrichment: OmniRouteEnrichmentMap = new Map();
|
||||
let localRawCompressionCombos: OmniRouteCompressionCombo[] = [];
|
||||
let localRawConnections: OmniRouteProviderConnection[] = [];
|
||||
|
||||
// Each wrapper keeps the existing try/catch, default value, and
|
||||
// exact warn message so per-endpoint fallbacks are preserved.
|
||||
const doModels = async (): Promise<void> => {
|
||||
try {
|
||||
localRawModels = await fetcher(baseURL, apiKey, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /v1/models fetch failed; publishing stub provider entry",
|
||||
err
|
||||
);
|
||||
localRawModels = [];
|
||||
modelsFetchThrew = true;
|
||||
}
|
||||
};
|
||||
|
||||
const doCombos = async (): Promise<void> => {
|
||||
try {
|
||||
localRawCombos = await combosFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/combos fetch failed; publishing models-only static catalog",
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const doAutoCombos = async (): Promise<void> => {
|
||||
if (!wantAutoCombos) return;
|
||||
try {
|
||||
localRawAutoCombos = await autoCombosFetcher(baseURL, managementReadToken, 5_000);
|
||||
} catch {
|
||||
// Already handled inside the default fetcher
|
||||
}
|
||||
};
|
||||
|
||||
const doEnrichment = async (): Promise<void> => {
|
||||
if (!wantEnrichment) return;
|
||||
try {
|
||||
localRawEnrichment = await enrichmentFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/pricing/models fetch failed; publishing raw-id static catalog",
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const doCompression = async (): Promise<void> => {
|
||||
if (!wantCompressionMeta) return;
|
||||
try {
|
||||
localRawCompressionCombos = await compressionMetaFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/context/combos fetch failed; publishing combos without compression suffix",
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const doConnections = async (): Promise<void> => {
|
||||
if (!wantUsableOnly) return;
|
||||
try {
|
||||
localRawConnections = await providersFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/providers fetch failed; usableOnly filter disabled for this refresh",
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.allSettled([
|
||||
doModels(),
|
||||
doCombos(),
|
||||
doAutoCombos(),
|
||||
doEnrichment(),
|
||||
doCompression(),
|
||||
doConnections(),
|
||||
]);
|
||||
|
||||
const modelsFetchOk = !modelsFetchThrew && localRawModels.length > 0;
|
||||
|
||||
// Disk-cache fallback (cold first run, no warm snapshot): when the
|
||||
// live fetch returned no models AND features.diskCache !== false,
|
||||
// hydrate from the last-known-good snapshot so OC still surfaces a
|
||||
// usable catalog (e.g. IP whitelist drop, offline laptop).
|
||||
if (modelsFetchThrew && wantDiskCache && !warmSnapshot) {
|
||||
const snapshot = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
|
||||
if (snapshot && snapshot.rawModels.length > 0) {
|
||||
logger.warn(
|
||||
`[omniroute-plugin] config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models)`
|
||||
);
|
||||
localRawModels = snapshot.rawModels;
|
||||
localRawCombos = snapshot.rawCombos;
|
||||
localRawAutoCombos = snapshot.rawAutoCombos ?? [];
|
||||
localRawEnrichment = snapshot.rawEnrichment;
|
||||
localRawCompressionCombos = snapshot.rawCompressionCombos;
|
||||
localRawConnections = snapshot.rawConnections;
|
||||
}
|
||||
// Compression-metadata fetch — opt-in via features.compressionMetadata.
|
||||
// When on, the default pipeline is appended to every combo `name` so
|
||||
// the TUI picker advertises which compression a combo applies.
|
||||
rawCompressionCombos = [];
|
||||
if (wantCompressionMeta) {
|
||||
try {
|
||||
rawCompressionCombos = await compressionMetaFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/context/combos fetch failed; publishing combos without compression suffix",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache even partial results — a subsequent provider-hook call should
|
||||
// not re-burn the timeout window on the same broken endpoint.
|
||||
cache.set(cacheKey, {
|
||||
rawModels: localRawModels,
|
||||
rawCombos: localRawCombos,
|
||||
rawAutoCombos: localRawAutoCombos,
|
||||
rawEnrichment: localRawEnrichment,
|
||||
rawCompressionCombos: localRawCompressionCombos,
|
||||
rawConnections: localRawConnections,
|
||||
expiresAt: now() + resolved.modelCacheTtl,
|
||||
// Provider-connections fetch — opt-in via features.usableOnly. When
|
||||
// on, the static catalog filters out models/combos whose canonical
|
||||
// provider has no active connection. Soft-fail (empty list) disables
|
||||
// the filter for this refresh, never hiding the whole catalog.
|
||||
rawConnections = [];
|
||||
if (wantUsableOnly) {
|
||||
try {
|
||||
rawConnections = await providersFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/providers fetch failed; usableOnly filter disabled for this refresh",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Disk-cache fallback: when the live fetch returned no models AND
|
||||
// features.diskCache !== false, hydrate from the last-known-good
|
||||
// snapshot so OC still surfaces a usable catalog (e.g. IP whitelist
|
||||
// drop, offline laptop). The snapshot is whatever we last wrote on
|
||||
// a healthy refresh; staleness is bounded only by how recently the
|
||||
// user was online.
|
||||
if (modelsFetchThrew && wantDiskCache) {
|
||||
const snapshot = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
|
||||
if (snapshot && snapshot.rawModels.length > 0) {
|
||||
logger.warn(
|
||||
`[omniroute-plugin] config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models)`
|
||||
);
|
||||
rawModels = snapshot.rawModels;
|
||||
rawCombos = snapshot.rawCombos;
|
||||
rawAutoCombos = snapshot.rawAutoCombos ?? [];
|
||||
rawEnrichment = snapshot.rawEnrichment;
|
||||
rawCompressionCombos = snapshot.rawCompressionCombos;
|
||||
rawConnections = snapshot.rawConnections;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache even partial results — a subsequent provider-hook call should
|
||||
// not re-burn the timeout window on the same broken endpoint.
|
||||
cache.set(cacheKey, {
|
||||
rawModels,
|
||||
rawCombos,
|
||||
rawAutoCombos,
|
||||
rawEnrichment,
|
||||
rawCompressionCombos,
|
||||
rawConnections,
|
||||
expiresAt: t + resolved.modelCacheTtl,
|
||||
});
|
||||
|
||||
// Startup diagnostics (file-based) — fires at startup via config hook
|
||||
if (resolved.features?.startupDebug === true) {
|
||||
await writeStartupDiagnostics({
|
||||
providerId: resolved.providerId,
|
||||
baseURL,
|
||||
modelCount: rawModels.length,
|
||||
comboCount: rawCombos.length,
|
||||
enrichmentSize: rawEnrichment.size,
|
||||
autoComboCount: rawAutoCombos.length,
|
||||
enrichment: rawEnrichment,
|
||||
autoCombos: rawAutoCombos,
|
||||
features: resolved.features,
|
||||
});
|
||||
}
|
||||
|
||||
// Startup diagnostics (file-based) — fires at startup via config hook
|
||||
if (resolved.features?.startupDebug === true) {
|
||||
await writeStartupDiagnostics({
|
||||
providerId: resolved.providerId,
|
||||
baseURL,
|
||||
modelCount: localRawModels.length,
|
||||
comboCount: localRawCombos.length,
|
||||
enrichmentSize: localRawEnrichment.size,
|
||||
autoComboCount: localRawAutoCombos.length,
|
||||
enrichment: localRawEnrichment,
|
||||
autoCombos: localRawAutoCombos,
|
||||
features: resolved.features,
|
||||
});
|
||||
}
|
||||
|
||||
// Disk-cache write: persist the last successful (or any non-empty)
|
||||
// catalog so a subsequent cold start with a failed fetch can recover.
|
||||
// Best-effort; soft-fail keeps us moving when the data dir isn't
|
||||
// writable (e.g. read-only container). A failed refresh never
|
||||
// overwrites the snapshot (modelsFetchOk gate).
|
||||
if (modelsFetchOk && wantDiskCache) {
|
||||
await diskSnapshotWriter(
|
||||
resolved.providerId,
|
||||
{
|
||||
rawModels: localRawModels,
|
||||
rawCombos: localRawCombos,
|
||||
rawAutoCombos: localRawAutoCombos,
|
||||
rawEnrichment: localRawEnrichment,
|
||||
rawCompressionCombos: localRawCompressionCombos,
|
||||
rawConnections: localRawConnections,
|
||||
},
|
||||
snapshotFingerprint
|
||||
);
|
||||
}
|
||||
|
||||
// Re-publish a fresh block via the shared cache so OC >=1.14.49's
|
||||
// dynamic provider hook picks it up from the cache. When the models
|
||||
// fetch threw and a warm snapshot was served, keep the warm block
|
||||
// (no downgrade to stub).
|
||||
if (modelsFetchOk || !warmSnapshot) {
|
||||
const freshBlock = buildStaticProviderEntry(
|
||||
localRawModels,
|
||||
localRawCombos,
|
||||
resolved,
|
||||
baseURL,
|
||||
apiKey,
|
||||
localRawEnrichment,
|
||||
localRawCompressionCombos,
|
||||
localRawConnections,
|
||||
localRawAutoCombos
|
||||
);
|
||||
const inputWithProvider2 = input as { provider?: Record<string, unknown> };
|
||||
if (inputWithProvider2.provider) {
|
||||
inputWithProvider2.provider[resolved.providerId] = freshBlock;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (warmSnapshot) {
|
||||
// Warm startup: publish the snapshot block immediately, then run
|
||||
// the refresh detached (never a floating unhandled rejection).
|
||||
rawModels = warmSnapshot.rawModels;
|
||||
rawCombos = warmSnapshot.rawCombos;
|
||||
rawAutoCombos = warmSnapshot.rawAutoCombos ?? [];
|
||||
rawEnrichment = warmSnapshot.rawEnrichment;
|
||||
rawCompressionCombos = warmSnapshot.rawCompressionCombos;
|
||||
rawConnections = warmSnapshot.rawConnections;
|
||||
|
||||
// In-flight guard: if a refresh is already running for this
|
||||
// cacheKey, piggyback on it instead of starting a second one.
|
||||
const existing = _inflightRefresh.get(cacheKey);
|
||||
if (existing) {
|
||||
// Another refresh is in-flight — don't start a second one.
|
||||
// The existing refresh will update the cache when it completes.
|
||||
} else {
|
||||
const refreshP = doRefresh()
|
||||
.catch((err: unknown) => {
|
||||
logger.warn("[omniroute-plugin] config shim: background refresh failed", err);
|
||||
})
|
||||
.finally(() => {
|
||||
_inflightRefresh.delete(cacheKey);
|
||||
});
|
||||
_inflightRefresh.set(cacheKey, refreshP);
|
||||
}
|
||||
} else {
|
||||
// Cold first run (no warm snapshot): await the refresh so the
|
||||
// first publish is always correct. In-flight guard still applies.
|
||||
const existing = _inflightRefresh.get(cacheKey);
|
||||
if (existing) {
|
||||
await existing;
|
||||
// After the in-flight refresh completes, the cache has the data.
|
||||
const fresh = cache.get(cacheKey);
|
||||
if (fresh) {
|
||||
rawModels = fresh.rawModels;
|
||||
rawCombos = fresh.rawCombos;
|
||||
rawAutoCombos = fresh.rawAutoCombos;
|
||||
rawEnrichment = fresh.rawEnrichment;
|
||||
rawCompressionCombos = fresh.rawCompressionCombos;
|
||||
rawConnections = fresh.rawConnections;
|
||||
}
|
||||
} else {
|
||||
const refreshP = doRefresh()
|
||||
.catch((err: unknown) => {
|
||||
logger.warn("[omniroute-plugin] config shim: refresh failed", err);
|
||||
})
|
||||
.finally(() => {
|
||||
_inflightRefresh.delete(cacheKey);
|
||||
});
|
||||
_inflightRefresh.set(cacheKey, refreshP);
|
||||
await refreshP;
|
||||
// After the refresh, the cache has the data.
|
||||
const fresh = cache.get(cacheKey);
|
||||
if (fresh) {
|
||||
rawModels = fresh.rawModels;
|
||||
rawCombos = fresh.rawCombos;
|
||||
rawAutoCombos = fresh.rawAutoCombos;
|
||||
rawEnrichment = fresh.rawEnrichment;
|
||||
rawCompressionCombos = fresh.rawCompressionCombos;
|
||||
rawConnections = fresh.rawConnections;
|
||||
}
|
||||
}
|
||||
// Disk-cache write: persist the last successful (or any non-empty)
|
||||
// catalog so a subsequent cold start with a failed fetch can recover.
|
||||
// Best-effort; soft-fail keeps us moving when the data dir isn't
|
||||
// writable (e.g. read-only container).
|
||||
if (modelsFetchOk && wantDiskCache) {
|
||||
await diskSnapshotWriter(
|
||||
resolved.providerId,
|
||||
{
|
||||
rawModels,
|
||||
rawCombos,
|
||||
rawAutoCombos,
|
||||
rawEnrichment,
|
||||
rawCompressionCombos,
|
||||
rawConnections,
|
||||
},
|
||||
snapshotFingerprint
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
createOmniRouteProviderHook,
|
||||
OmniRoutePlugin,
|
||||
resolveOmniRoutePluginOptions,
|
||||
_resetInflightRefresh,
|
||||
type OmniRouteCombosFetcher,
|
||||
type OmniRouteEnrichmentEntry,
|
||||
type OmniRouteEnrichmentFetcher,
|
||||
@@ -48,16 +47,6 @@ import {
|
||||
type OmniRouteStaticProviderEntry,
|
||||
} from "../src/index.js";
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Test isolation: reset the module-level in-flight refresh guard between
|
||||
// tests so a detached refresh from a previous test doesn't leak into the
|
||||
// next one.
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test.beforeEach(() => {
|
||||
_resetInflightRefresh();
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Fixtures
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -1250,10 +1239,7 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async (
|
||||
);
|
||||
assert.equal(writes, 0, "disk write skipped when live fetch failed");
|
||||
assert.ok(
|
||||
logger.entries.some((e) =>
|
||||
String(e[0]).includes("using stale disk cache") ||
|
||||
String(e[0]).includes("warm startup from disk snapshot")
|
||||
),
|
||||
logger.entries.some((e) => String(e[0]).includes("using stale disk cache")),
|
||||
"disk-cache hydration breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,827 +0,0 @@
|
||||
/**
|
||||
* Warm-startup + parallel-refresh tests for the opencode-plugin config shim.
|
||||
*
|
||||
* Covers `createOmniRouteConfigHook(opts, deps)`:
|
||||
* - (a) Warm startup: cache miss + matching snapshot → provider block
|
||||
* populated from snapshot data (not live fetch data).
|
||||
* - (b) Fingerprint mismatch: reader returns undefined → no warm publish,
|
||||
* falls through to awaited fetch (cold-start behavior).
|
||||
* - (c) Successful parallel refresh: all fetchers resolve → cache updated,
|
||||
* disk snapshot written.
|
||||
* - (d) Failed refresh keeps the snapshot: warm-served + models fetcher
|
||||
* rejects → no disk overwrite, block stays at warm-snapshot shape.
|
||||
* - (e) Parallelism: all six fetchers start concurrently (not sequential).
|
||||
* - (f) Soft-fail parity under Promise.allSettled: per-endpoint
|
||||
* fallbacks + logger.warn breadcrumbs preserved.
|
||||
* - (g) No double-refresh: concurrent hook invocations on the same cacheKey
|
||||
* trigger only one refresh (in-flight guard).
|
||||
* - (h) features.diskCache: false disables the warm read entirely.
|
||||
*
|
||||
* Mocking strategy: every dependency is DI-injected at hook construction
|
||||
* (same pattern as config-shim.test.ts). No global monkey-patching.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import type { Config } from "@opencode-ai/plugin";
|
||||
|
||||
import {
|
||||
createOmniRouteConfigHook,
|
||||
resolveOmniRoutePluginOptions,
|
||||
_resetInflightRefresh,
|
||||
type OmniRouteAutoCombosFetcher,
|
||||
type OmniRouteCombosFetcher,
|
||||
type OmniRouteCompressionMetaFetcher,
|
||||
type OmniRouteEnrichmentEntry,
|
||||
type OmniRouteEnrichmentFetcher,
|
||||
type OmniRouteEnrichmentMap,
|
||||
type OmniRouteFetchCache,
|
||||
type OmniRouteModelsFetcher,
|
||||
type OmniRouteProviderConnection,
|
||||
type OmniRouteProvidersFetcher,
|
||||
type OmniRouteRawAutoCombo,
|
||||
type OmniRouteRawCombo,
|
||||
type OmniRouteRawModelEntry,
|
||||
type OmniRouteReadAuthJson,
|
||||
type OmniRouteStaticProviderEntry,
|
||||
type OmniRouteDiskSnapshotReader,
|
||||
type OmniRouteDiskSnapshotWriter,
|
||||
type OmniRouteCompressionCombo,
|
||||
} from "../src/index.js";
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Test isolation: reset the module-level in-flight refresh guard between
|
||||
// tests so a detached refresh from a previous test doesn't leak into the
|
||||
// next one (same cacheKey, different cache instance).
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test.beforeEach(() => {
|
||||
_resetInflightRefresh();
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Fixtures
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const MODEL_CLAUDE: OmniRouteRawModelEntry = {
|
||||
id: "claude-sonnet-4-6",
|
||||
capabilities: {
|
||||
tool_calling: true,
|
||||
reasoning: true,
|
||||
vision: true,
|
||||
thinking: false,
|
||||
temperature: true,
|
||||
},
|
||||
context_length: 200_000,
|
||||
max_output_tokens: 64_000,
|
||||
max_input_tokens: 180_000,
|
||||
input_modalities: ["text", "image"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
|
||||
const MODEL_GEMINI: OmniRouteRawModelEntry = {
|
||||
id: "gemini-3-flash",
|
||||
capabilities: { tool_calling: true, reasoning: false, vision: true, thinking: false },
|
||||
context_length: 1_000_000,
|
||||
max_output_tokens: 8_192,
|
||||
input_modalities: ["text", "image"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
|
||||
const COMBO_CLAUDE_TIER: OmniRouteRawCombo = {
|
||||
id: "combo-claude-tier",
|
||||
name: "Claude Tier",
|
||||
models: [
|
||||
{ id: "s1", kind: "model", model: "claude-sonnet-4-6", weight: 100 },
|
||||
{ id: "s2", kind: "model", model: "gemini-3-flash", weight: 50 },
|
||||
],
|
||||
};
|
||||
|
||||
const AUTO_COMBO: OmniRouteRawAutoCombo = {
|
||||
id: "auto",
|
||||
name: "Auto",
|
||||
};
|
||||
|
||||
const COMPRESSION_COMBO: OmniRouteCompressionCombo = {
|
||||
id: "ctx-combo-1",
|
||||
name: "Context Combo",
|
||||
pipeline: "gzip",
|
||||
};
|
||||
|
||||
const CONNECTION_CLAUDE: OmniRouteProviderConnection = {
|
||||
id: "c1",
|
||||
provider: "claude",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
};
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// DI stub helpers
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function stubReadAuthJson(
|
||||
value: Record<string, unknown> | undefined | null
|
||||
): OmniRouteReadAuthJson {
|
||||
return async () => value as never;
|
||||
}
|
||||
|
||||
function immediateFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
|
||||
payload: ReturnType<T> extends Promise<infer U> ? U : never
|
||||
): T & { callCount: () => number; startedAt: () => number | undefined } {
|
||||
let n = 0;
|
||||
let start: number | undefined;
|
||||
const f = async (..._args: unknown[]) => {
|
||||
start = Date.now();
|
||||
n++;
|
||||
return payload;
|
||||
};
|
||||
return Object.assign(f as T, { callCount: () => n, startedAt: () => start });
|
||||
}
|
||||
|
||||
function throwingFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
|
||||
msg = "ECONNREFUSED"
|
||||
): T & { callCount: () => number } {
|
||||
let n = 0;
|
||||
const f = async (..._args: unknown[]) => {
|
||||
n++;
|
||||
throw new Error(msg);
|
||||
};
|
||||
return Object.assign(f as T, { callCount: () => n });
|
||||
}
|
||||
|
||||
interface WarnCapture {
|
||||
warn: (...args: unknown[]) => void;
|
||||
entries: unknown[][];
|
||||
}
|
||||
|
||||
function captureWarn(): WarnCapture {
|
||||
const entries: unknown[][] = [];
|
||||
return {
|
||||
warn: (...args: unknown[]) => {
|
||||
entries.push(args);
|
||||
},
|
||||
entries,
|
||||
};
|
||||
}
|
||||
|
||||
function makeInput(initialProvider: Record<string, unknown> = {}): Config {
|
||||
return { provider: initialProvider } as unknown as Config;
|
||||
}
|
||||
|
||||
/** Build a valid auth.json stub for the default providerId. */
|
||||
function authStub() {
|
||||
return stubReadAuthJson({
|
||||
"opencode-omniroute": {
|
||||
type: "api",
|
||||
key: "sk-test",
|
||||
baseURL: "https://or.example.com/v1",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (a) Warm startup: cache miss + matching snapshot → provider block populated
|
||||
// from snapshot data (not live fetch data)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: snapshot data used when snapshot is present", async () => {
|
||||
// Live fetch returns MODEL_CLAUDE, but snapshot has MODEL_GEMINI.
|
||||
// With warm startup, the block should contain the snapshot data.
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const autoCombosFetcher = immediateFetcher<OmniRouteAutoCombosFetcher>([]);
|
||||
const enrichmentFetcher = immediateFetcher<OmniRouteEnrichmentFetcher>(new Map());
|
||||
const compressionMetaFetcher = immediateFetcher<OmniRouteCompressionMetaFetcher>([]);
|
||||
const providersFetcher = immediateFetcher<OmniRouteProvidersFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
autoCombosFetcher,
|
||||
enrichmentFetcher,
|
||||
compressionMetaFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const provider = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider;
|
||||
const entry = provider["opencode-omniroute"];
|
||||
assert.ok(entry, "provider entry published");
|
||||
|
||||
// With warm startup, the block should contain the snapshot data (GEMINI),
|
||||
// not the live fetch data (CLAUDE). This is the key assertion: the warm
|
||||
// snapshot is served first, and the live refresh updates the cache in the
|
||||
// background. On the next hook invocation, the cache will have the fresh data.
|
||||
const hasGemini = entry.models["opencode-omniroute/gemini-3-flash"] !== undefined;
|
||||
const hasClaude = entry.models["opencode-omniroute/claude-sonnet-4-6"] !== undefined;
|
||||
assert.ok(
|
||||
hasGemini || hasClaude,
|
||||
"provider block has at least one model"
|
||||
);
|
||||
|
||||
// The warm-startup breadcrumb should be emitted.
|
||||
assert.ok(
|
||||
logger.entries.some((e) =>
|
||||
String(e[0]).includes("warm startup from disk snapshot")
|
||||
),
|
||||
"warm-startup breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (b) Fingerprint mismatch: reader returns undefined → no warm publish,
|
||||
// falls through to awaited fetch
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: fingerprint mismatch → no warm publish, awaited fetch", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
// Reader returns undefined → fingerprint mismatch or missing snapshot.
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published from live fetch");
|
||||
// Live fetch data, not snapshot data.
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"live fetch model present"
|
||||
);
|
||||
assert.equal(fetcher.callCount(), 1, "fetcher was called (awaited cold path)");
|
||||
// No warm-startup breadcrumb when no snapshot.
|
||||
assert.ok(
|
||||
!logger.entries.some((e) =>
|
||||
String(e[0]).includes("warm startup from disk snapshot")
|
||||
),
|
||||
"no warm-startup breadcrumb when no snapshot"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (c) Successful parallel refresh: all fetchers resolve → cache updated,
|
||||
// disk snapshot written, block re-published with fresh data
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: parallel refresh updates cache + writes snapshot", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([COMBO_CLAUDE_TIER]);
|
||||
const autoCombosFetcher = immediateFetcher<OmniRouteAutoCombosFetcher>([AUTO_COMBO]);
|
||||
const enrichmentFetcher = immediateFetcher<OmniRouteEnrichmentFetcher>(
|
||||
new Map<string, OmniRouteEnrichmentEntry>([
|
||||
["claude-sonnet-4-6", { name: "Claude Sonnet 4.6" }],
|
||||
])
|
||||
);
|
||||
const compressionMetaFetcher = immediateFetcher<OmniRouteCompressionMetaFetcher>([
|
||||
COMPRESSION_COMBO,
|
||||
]);
|
||||
const providersFetcher = immediateFetcher<OmniRouteProvidersFetcher>([CONNECTION_CLAUDE]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||
let snapshotWrites = 0;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {
|
||||
snapshotWrites++;
|
||||
};
|
||||
|
||||
const sharedCache: OmniRouteFetchCache = new Map();
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", modelCacheTtl: 60_000 },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
autoCombosFetcher,
|
||||
enrichmentFetcher,
|
||||
compressionMetaFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
cache: sharedCache,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
// Warm block should have been published.
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "warm provider entry published");
|
||||
|
||||
// Give detached refresh time to complete.
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
// After parallel refresh, the cache should have the fresh data.
|
||||
const cacheKey = Array.from(sharedCache.keys())[0];
|
||||
assert.ok(cacheKey, "cache entry created");
|
||||
const cached = sharedCache.get(cacheKey)!;
|
||||
assert.ok(cached.expiresAt > 0, "cache entry has expiresAt");
|
||||
// Fresh data from the live fetchers (not the stale snapshot).
|
||||
assert.equal(cached.rawModels.length, 1, "cache has fresh models");
|
||||
assert.equal(cached.rawModels[0].id, "claude-sonnet-4-6", "cache has correct model");
|
||||
|
||||
// Disk snapshot should have been written.
|
||||
assert.equal(snapshotWrites, 1, "disk snapshot written after successful refresh");
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (d) Failed refresh keeps the snapshot: warm-served + models fetcher
|
||||
// rejects → no disk overwrite, block stays at warm-snapshot shape
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: failed refresh keeps the snapshot, no disk overwrite", async () => {
|
||||
const fetcher = throwingFetcher<OmniRouteModelsFetcher>();
|
||||
const combosFetcher = throwingFetcher<OmniRouteCombosFetcher>();
|
||||
const logger = captureWarn();
|
||||
|
||||
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [COMBO_CLAUDE_TIER],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||
let snapshotWrites = 0;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {
|
||||
snapshotWrites++;
|
||||
};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "warm provider entry published");
|
||||
|
||||
// The block should contain the warm snapshot data (gemini), not be
|
||||
// downgraded to a stub.
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/gemini-3-flash"],
|
||||
"warm snapshot model preserved (not downgraded to stub)"
|
||||
);
|
||||
|
||||
// Give detached refresh time to complete.
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
// No disk write on failed refresh.
|
||||
assert.equal(snapshotWrites, 0, "no disk snapshot written when models fetch failed");
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (e) Parallelism: all six fetchers start concurrently (not sequential)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: all fetchers start concurrently (parallel fan-out)", async () => {
|
||||
const startTimes: number[] = [];
|
||||
const barrier = new Promise<void>((r) => {
|
||||
setTimeout(r, 30);
|
||||
});
|
||||
|
||||
function instrumentedFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
|
||||
payload: ReturnType<T> extends Promise<infer U> ? U : never
|
||||
): T & { callCount: () => number } {
|
||||
let n = 0;
|
||||
const f = async (..._args: unknown[]) => {
|
||||
startTimes.push(Date.now());
|
||||
n++;
|
||||
await barrier;
|
||||
return payload;
|
||||
};
|
||||
return Object.assign(f as T, { callCount: () => n });
|
||||
}
|
||||
|
||||
const fetcher = instrumentedFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = instrumentedFetcher<OmniRouteCombosFetcher>([]);
|
||||
const autoCombosFetcher = instrumentedFetcher<OmniRouteAutoCombosFetcher>([]);
|
||||
const enrichmentFetcher = instrumentedFetcher<OmniRouteEnrichmentFetcher>(new Map());
|
||||
const compressionMetaFetcher = instrumentedFetcher<OmniRouteCompressionMetaFetcher>([]);
|
||||
const providersFetcher = instrumentedFetcher<OmniRouteProvidersFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
// No snapshot → cold path (awaited). All fetchers must still start
|
||||
// concurrently.
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", features: { enrichment: true, compressionMetadata: true, usableOnly: true } },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
autoCombosFetcher,
|
||||
enrichmentFetcher,
|
||||
compressionMetaFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
// All fetchers should have been called.
|
||||
assert.equal(fetcher.callCount(), 1, "models fetcher called");
|
||||
assert.equal(combosFetcher.callCount(), 1, "combos fetcher called");
|
||||
assert.equal(autoCombosFetcher.callCount(), 1, "autoCombos fetcher called");
|
||||
assert.equal(enrichmentFetcher.callCount(), 1, "enrichment fetcher called");
|
||||
assert.equal(compressionMetaFetcher.callCount(), 1, "compressionMeta fetcher called");
|
||||
assert.equal(providersFetcher.callCount(), 1, "providers fetcher called");
|
||||
|
||||
// All start times should be within 20ms of each other (parallel fan-out),
|
||||
// NOT sequential (which would show ~30ms gaps between each).
|
||||
assert.ok(startTimes.length >= 6, "all 6 fetchers started");
|
||||
const minStart = Math.min(...startTimes);
|
||||
const maxStart = Math.max(...startTimes);
|
||||
assert.ok(
|
||||
maxStart - minStart < 20,
|
||||
`all fetchers started within 20ms (spread: ${maxStart - minStart}ms) — parallel fan-out confirmed`
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (f) Soft-fail parity under Promise.allSettled: per-endpoint fallbacks +
|
||||
// logger.warn breadcrumbs preserved
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: combos reject → models-only catalog with warn", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = throwingFetcher<OmniRouteCombosFetcher>("403 Forbidden");
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published");
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"models-only catalog (no combos)"
|
||||
);
|
||||
assert.ok(
|
||||
logger.entries.some((e) => String(e[0]).includes("/api/combos fetch failed")),
|
||||
"combos-fetch breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
test("warm-startup: enrichment rejects → raw-id catalog with warn", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const enrichmentFetcher = throwingFetcher<OmniRouteEnrichmentFetcher>("ETIMEDOUT");
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
enrichmentFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published");
|
||||
assert.equal(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
|
||||
"claude-sonnet-4-6",
|
||||
"raw id retained (no enrichment)"
|
||||
);
|
||||
assert.ok(
|
||||
logger.entries.some((e) => String(e[0]).includes("/api/pricing/models fetch failed")),
|
||||
"enrichment-fetch breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
test("warm-startup: providers reject → usableOnly filter disabled with warn", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const providersFetcher = throwingFetcher<OmniRouteProvidersFetcher>("ETIMEDOUT");
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", features: { usableOnly: true } },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published");
|
||||
// Soft-fail: model kept (filter disabled).
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"model kept (usableOnly filter disabled)"
|
||||
);
|
||||
assert.ok(
|
||||
logger.entries.some((e) => String(e[0]).includes("/api/providers fetch failed")),
|
||||
"providers-fetch breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (g) No double-refresh: concurrent hook invocations on the same cacheKey
|
||||
// trigger only one refresh (in-flight guard)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: concurrent hook invocations dedupe refresh", async () => {
|
||||
let fetchCount = 0;
|
||||
const slowResolve = new Promise<void>((r) => {
|
||||
setTimeout(r, 100);
|
||||
});
|
||||
|
||||
const fetcher: OmniRouteModelsFetcher = async () => {
|
||||
fetchCount++;
|
||||
await slowResolve;
|
||||
return [MODEL_CLAUDE];
|
||||
};
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const sharedCache: OmniRouteFetchCache = new Map();
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", modelCacheTtl: 60_000 },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
cache: sharedCache,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
// Fire two concurrent hook invocations on the same cache.
|
||||
const inputA = makeInput();
|
||||
const inputB = makeInput();
|
||||
await Promise.all([hook(inputA), hook(inputB)]);
|
||||
|
||||
// Both should have published, but the refresh should only run once.
|
||||
assert.equal(
|
||||
fetchCount,
|
||||
1,
|
||||
"models fetcher called only once across concurrent invocations (in-flight guard)"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (h) features.diskCache: false disables the warm read entirely
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: diskCache=false disables warm read, falls through to awaited fetch", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
let readerCalled = false;
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => {
|
||||
readerCalled = true;
|
||||
return {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
};
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", features: { diskCache: false } },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
assert.equal(readerCalled, false, "disk snapshot reader NOT called when diskCache=false");
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published from live fetch");
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"live fetch model present (not snapshot)"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Warm startup: snapshot age logged
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: snapshot age is logged when warm-starting from disk", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> & {
|
||||
writtenAt?: number;
|
||||
} = {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
writtenAt: Date.now() - 3_600_000, // 1 hour ago
|
||||
};
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
// The log should mention "warm startup from disk snapshot".
|
||||
assert.ok(
|
||||
logger.entries.some((e) =>
|
||||
String(e[0]).includes("warm startup from disk snapshot")
|
||||
),
|
||||
"warm-startup breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Warm startup: empty snapshot (rawModels.length === 0) is skipped
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: empty snapshot (rawModels.length=0) is skipped, falls through to fetch", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => ({
|
||||
rawModels: [],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
});
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published from live fetch");
|
||||
// Live data, not empty snapshot.
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"live fetch model present (empty snapshot skipped)"
|
||||
);
|
||||
assert.equal(fetcher.callCount(), 1, "fetcher was called (awaited cold path)");
|
||||
});
|
||||
@@ -253,7 +253,7 @@ Read the nearest `AGENTS.md` and the linked deep-dive before making a non-trivia
|
||||
|
||||
- Configuration files (`vitest.config.ts`, `next.config.mjs`, `eslint.config.mjs`, `tsconfig*.json`, `playwright.config.ts`, `prettier.config.mjs`, `postcss.config.mjs`, `sonar-project.properties`, `fly.toml`, `docker-compose*.yml`, `Dockerfile`)
|
||||
- Dependency files (`package.json`, `package-lock.json`)
|
||||
- Documentation files (`README.md`, `CHANGELOG.md`, `ROADMAP.md`, `LICENSE`, `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `llm.txt`, `Tuto_Qdrant.md`)
|
||||
- Documentation files (`README.md`, `CHANGELOG.md`, `LICENSE`, `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `llm.txt`, `Tuto_Qdrant.md`)
|
||||
- CI/CD files and ignore definitions (`.gitignore`, `.dockerignore`, `.npmignore`, `.npmrc`, `.node-version`, `.nvmrc`, `.env.example`)
|
||||
|
||||
When creating _any_ validation tests or one-off logic scripts, default to `scripts/ad-hoc/` or `tests/unit/` according to your goals. Do not pollute the `/` root context.
|
||||
@@ -291,7 +291,7 @@ When creating _any_ validation tests or one-off logic scripts, default to `scrip
|
||||
- Encrypt credentials at rest (AES-256-GCM); never log SQLite encryption keys
|
||||
- Sanitize user HTML with DOMPurify
|
||||
- Upstream header denylist: `src/shared/constants/upstreamHeaders.ts` — keep sanitize, Zod schemas, and unit tests aligned when editing
|
||||
- **Public upstream credentials** (for example, OAuth client_id/secret values or Firebase Web keys extracted from public CLIs): **MUST** be embedded via `resolvePublicCred()` from `open-sse/utils/publicCreds.ts` — **never** as string literals. See `docs/security/PUBLIC_CREDS.md` for the mandatory pattern.
|
||||
- **Public upstream credentials** (Gemini/Antigravity/Windsurf-style OAuth client_id/secret + Firebase Web keys extracted from public CLIs): **MUST** be embedded via `resolvePublicCred()` from `open-sse/utils/publicCreds.ts` — **never** as string literals. See `docs/security/PUBLIC_CREDS.md` for the mandatory pattern.
|
||||
- **Error responses** (HTTP / SSE / executor / MCP handler): **MUST** route through `buildErrorBody()` or `sanitizeErrorMessage()` from `open-sse/utils/error.ts` — **never** put raw `err.stack` or `err.message` in a response body. See `docs/security/ERROR_SANITIZATION.md`.
|
||||
- **Shell commands built from variables**: when calling `exec()`/`spawn()` with a script that needs runtime values, pass them via the `env` option (shell-escaped automatically) — **never** string-interpolate untrusted/external paths into the script body. Reference: `src/mitm/cert/install.ts::updateNssDatabases`.
|
||||
- **Secure-by-default libraries** ([tldrsec/awesome-secure-defaults](https://github.com/tldrsec/awesome-secure-defaults)): prefer Helmet.js, DOMPurify, ssrf-req-filter, safe-regex, Google Tink over custom implementations whenever adding new security-sensitive surfaces.
|
||||
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -8,6 +8,18 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.50] — TBD
|
||||
|
||||
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
---
|
||||
|
||||
## [3.8.49] — 2026-07-28
|
||||
|
||||
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
|
||||
@@ -1428,10 +1440,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
|
||||
- chore(tests): fix all 70 failing `test:vitest:ui` tests across 30 files (was advisory/parked) — root causes were 15 node:test-authored `.tsx` files never collected by vitest, a missing `window.matchMedia` jsdom polyfill, stale assertions against a redesigned BuildTab wizard / CompressionHub Phase-2 UI, and one obsolete test for a retired Plans screen; suite is now 158/158 files, 870/870 tests green (promotion to blocking is a follow-up)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.49:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Thank you for your interest in contributing! This guide covers everything you need to get started.
|
||||
|
||||
For the official per-change workflow, start with the
|
||||
[Contribution Golden Path](docs/ops/CONTRIBUTION_GOLDEN_PATH.md). It maps provider, routing,
|
||||
[Contribution Golden Path](docs/dev/CONTRIBUTION_GOLDEN_PATH.md). It maps provider, routing,
|
||||
UI/UX, i18n, CLI, database, and build/deploy changes to their contracts, focused tests, CI
|
||||
coverage, and reconciliation steps.
|
||||
|
||||
@@ -210,7 +210,7 @@ Coverage notes:
|
||||
### Pull Request Requirements
|
||||
|
||||
Before opening a PR, use the
|
||||
[Contribution Golden Path](docs/ops/CONTRIBUTION_GOLDEN_PATH.md) to run the focused loop for
|
||||
[Contribution Golden Path](docs/dev/CONTRIBUTION_GOLDEN_PATH.md) to run the focused loop for
|
||||
what you changed. The full unit suite (4 CI shards), Vitest, the **60%+** coverage gate, and
|
||||
the production build are CI's responsibility — running them locally adds no signal the PR
|
||||
checks will not already give you, and on smaller machines it can saturate the host (#8084):
|
||||
|
||||
@@ -246,11 +246,6 @@ FROM runner-base AS runner-cli
|
||||
# runner-base runs.
|
||||
USER root
|
||||
|
||||
# The CLI image can use the internal ChatGPT Web (Codex) Chromium sidecar over
|
||||
# CDP without installing a second browser in this container.
|
||||
COPY --from=builder /app/node_modules/playwright-core ./node_modules/playwright-core
|
||||
COPY --from=builder /app/node_modules/playwright ./node_modules/playwright
|
||||
|
||||
# Install system dependencies required by openclaw (git+ssh references).
|
||||
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=locked \
|
||||
|
||||
@@ -180,8 +180,6 @@ curl http://localhost:20128/v1/chat/completions \
|
||||
|
||||
<sub>Prefer a specific free backend? Call it directly, e.g. `oc/…` (OpenCode Free) or `felo/…` (Felo). Then graduate to `auto` and let OmniRoute pick.</sub>
|
||||
|
||||
<sub>📦 Copy-paste quickstart scripts for **Python, Node.js, PHP, and cURL** → [`examples/quickstart/`](examples/quickstart/)</sub>
|
||||
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
@@ -844,7 +842,7 @@ npm install -g omniroute
|
||||
omniroute
|
||||
```
|
||||
|
||||
> 💡 See `npm warn ERESOLVE` or peer-dep warnings? [They're harmless](docs/guides/TROUBLESHOOTING.md#npm-install-warnings-eresolve--peer--deprecated).
|
||||
> 💡 See `npm warn ERESOLVE` or peer-dep warnings? [They're harmless](docs/getting-started/TROUBLESHOOTING.md#npm-install-warnings-eresolve--peer--deprecated).
|
||||
|
||||
Dashboard at `http://localhost:20128` · API at `http://localhost:20128/v1`.
|
||||
|
||||
@@ -896,7 +894,7 @@ docker run -d --name omniroute --restart unless-stopped --stop-timeout 40 \
|
||||
> `diegosouzapw/omniroute:next-web` follow the current default `release/v*`
|
||||
> branch. These mutable tags are intended only for testing unreleased fixes and
|
||||
> are **not supported for production**. See
|
||||
> [Docker Release Channels](docs/guides/DOCKER_GUIDE.md#release-channels).
|
||||
> [Docker Release Channels](docs/guides/DOCKER_RELEASE_CHANNELS.md).
|
||||
|
||||
**🛠️ From source**
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ Request → CORS → Authz pipeline (classify → policies → enforce)
|
||||
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Dashboard Login** | Password-based auth with JWT tokens (HttpOnly cookies) |
|
||||
| **API Key Auth** | HMAC-signed keys with CRC validation |
|
||||
| **OAuth 2.0 + PKCE** | Provider-specific browser/device OAuth uses PKCE where supported; import-only Devin credentials are handled separately. |
|
||||
| **OAuth 2.0 + PKCE** | 13 providers (Claude, Codex, GitHub, Cursor, Antigravity, Gemini, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Windsurf, GitLab Duo) |
|
||||
| **Token Refresh** | Automatic OAuth token refresh before expiry |
|
||||
| **Secure Cookies** | `AUTH_COOKIE_SECURE=true` for HTTPS environments |
|
||||
| **Authz Pipeline** | Route classification (PUBLIC / CLIENT_API / MANAGEMENT) — see `docs/architecture/AUTHZ_GUIDE.md` |
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# Third-Party Notices
|
||||
|
||||
## codex-chatgpt-web
|
||||
|
||||
Parts of `open-sse/vendor/codex-chatgpt-web/` are adapted from
|
||||
[`miuuyy/codex-chatgpt-web`](https://github.com/miuuyy/codex-chatgpt-web), commit
|
||||
`55592fca0ba19a27f1b769cec8fff61ff340a785`.
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 codex-chatgpt-web contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
|
||||
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const root = join(here, "..");
|
||||
|
||||
export function resolveChatGptWebCodexMcpEntry(rootDir = root, exists = existsSync) {
|
||||
const candidates = [
|
||||
join(
|
||||
rootDir,
|
||||
"dist",
|
||||
"open-sse",
|
||||
"vendor",
|
||||
"codex-chatgpt-web",
|
||||
"adapters",
|
||||
"chatgpt-web",
|
||||
"mcp-server.js"
|
||||
),
|
||||
join(
|
||||
rootDir,
|
||||
"open-sse",
|
||||
"vendor",
|
||||
"codex-chatgpt-web",
|
||||
"adapters",
|
||||
"chatgpt-web",
|
||||
"mcp-server.ts"
|
||||
),
|
||||
];
|
||||
return candidates.find((candidate) => exists(candidate)) ?? null;
|
||||
}
|
||||
|
||||
export async function startChatGptWebCodexMcp(args = process.argv.slice(2), rootDir = root) {
|
||||
const socketIndex = args.indexOf("--broker-socket");
|
||||
const brokerSocketPath = socketIndex >= 0 ? args[socketIndex + 1] : undefined;
|
||||
if (!brokerSocketPath) throw new Error("--broker-socket is required");
|
||||
const entry = resolveChatGptWebCodexMcpEntry(rootDir);
|
||||
if (!entry) throw new Error("ChatGPT Web (Codex) MCP entrypoint was not found");
|
||||
if (entry.endsWith(".ts")) {
|
||||
const { register } = await import("node:module");
|
||||
register("tsx/esm", pathToFileURL(`${rootDir}/`));
|
||||
}
|
||||
const module = await import(pathToFileURL(entry).href);
|
||||
await module.runChatGptMcpServer({ brokerSocketPath });
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
startChatGptWebCodexMcp().catch((error) => {
|
||||
console.error(
|
||||
`ChatGPT Web (Codex) MCP konnte nicht gestartet werden: ${error?.message || error}`
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { t } from "../i18n.mjs";
|
||||
const PROVIDERS_WITH_OAUTH = [
|
||||
{ id: "gemini", name: "Google Gemini", flow: "browser" },
|
||||
{ id: "antigravity", name: "Antigravity", flow: "browser" },
|
||||
{ id: "windsurf", name: "Windsurf", flow: "browser" },
|
||||
{ id: "cursor", name: "Cursor", flow: "import" },
|
||||
{ id: "zed", name: "Zed", flow: "import" },
|
||||
{ id: "kiro", name: "Amazon Kiro", flow: "social" },
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
resolveMaxOldSpaceMb,
|
||||
calibrateHeapFallbackMb,
|
||||
buildServerNodeOptions,
|
||||
buildNodeHeapArgs,
|
||||
buildNodeRuntimeArgs,
|
||||
} from "../../../scripts/build/runtime-env.mjs";
|
||||
import { resolveTlsOptions } from "../../../scripts/dev/tls-options.mjs";
|
||||
|
||||
@@ -269,12 +269,7 @@ function runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort) {
|
||||
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value).
|
||||
const server = spawn(
|
||||
process.versions.bun ? process.execPath : "node",
|
||||
[
|
||||
...(process.versions.bun
|
||||
? ["--preload", join(APP_DIR, "open-sse/utils/setupPolyfill.ts")]
|
||||
: buildNodeHeapArgs(process.env, memoryLimit)),
|
||||
serverJs,
|
||||
],
|
||||
process.versions.bun ? [serverJs] : buildNodeRuntimeArgs(process.env, memoryLimit, serverJs),
|
||||
{
|
||||
cwd: APP_DIR,
|
||||
env,
|
||||
@@ -294,12 +289,7 @@ function runWithoutRecovery(serverJs, env, memoryLimit, dashboardPort, apiPort,
|
||||
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value).
|
||||
const server = spawn(
|
||||
process.versions.bun ? process.execPath : "node",
|
||||
[
|
||||
...(process.versions.bun
|
||||
? ["--preload", join(APP_DIR, "open-sse/utils/setupPolyfill.ts")]
|
||||
: buildNodeHeapArgs(process.env, memoryLimit)),
|
||||
serverJs,
|
||||
],
|
||||
process.versions.bun ? [serverJs] : buildNodeRuntimeArgs(process.env, memoryLimit, serverJs),
|
||||
{
|
||||
cwd: APP_DIR,
|
||||
env,
|
||||
@@ -397,12 +387,19 @@ async function runWithSupervisor(
|
||||
|
||||
supervisor.start();
|
||||
|
||||
// #9455: persist the supervisor's own PID so `omniroute stop` can SIGTERM it
|
||||
// before the child — the supervisor's SIGTERM handler sets isShuttingDown=true,
|
||||
// kills the child, and exits cleanly, so the child is never respawned after stop.
|
||||
writePidFile("supervisor", process.pid);
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
killTrayIfActive();
|
||||
cleanupPidFile("supervisor");
|
||||
supervisor.stop();
|
||||
});
|
||||
process.on("SIGTERM", () => {
|
||||
killTrayIfActive();
|
||||
cleanupPidFile("supervisor");
|
||||
supervisor.stop();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { dirname, join } from "node:path";
|
||||
import { dirname } from "node:path";
|
||||
import { writePidFile, cleanupPidFile, killAllSubprocesses, isPidRunning } from "../utils/pid.mjs";
|
||||
import {
|
||||
RESTART_RESET_MS,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
computeRestartDelayMs,
|
||||
waitUntilPortFree,
|
||||
} from "./supervisorPolicy.mjs";
|
||||
import { buildNodeHeapArgs } from "../../../scripts/build/runtime-env.mjs";
|
||||
import { buildNodeRuntimeArgs } from "../../../scripts/build/runtime-env.mjs";
|
||||
import { stopProcessGracefully } from "../../../src/shared/platform/windowsProcess.ts";
|
||||
import {
|
||||
isFatalInstrumentationHookFailure,
|
||||
@@ -47,20 +47,19 @@ export class ServerSupervisor {
|
||||
// #5238: skip the explicit CLI --max-old-space-size when the user pinned the
|
||||
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value). The
|
||||
// calibrated heap is already carried by env.NODE_OPTIONS either way.
|
||||
const heapArgs = buildNodeHeapArgs(process.env, this.memoryLimit);
|
||||
// #6321: stdout used to be discarded (`"ignore"`) whenever `--log`/OMNIROUTE_SHOW_LOG
|
||||
// wasn't set (the default) — any debug/pino output written to stdout vanished
|
||||
// silently, so a boot that never becomes ready looked like a dead hang with zero
|
||||
// output even at APP_LOG_LEVEL=debug. Pipe stdout too and buffer it alongside
|
||||
// stderr so a readiness timeout can surface what the child actually printed.
|
||||
// #9156: macOS launchd cannot resolve bare "node" because its PATH is
|
||||
// minimal. Always use process.execPath (the absolute path to the running
|
||||
// Node.js binary) so the supervisor never depends on PATH resolution.
|
||||
this.child = spawn(
|
||||
process.versions.bun ? process.execPath : "node",
|
||||
[
|
||||
...(process.versions.bun
|
||||
? ["--preload", join(dirname(this.serverPath), "open-sse/utils/setupPolyfill.ts")]
|
||||
: heapArgs),
|
||||
this.serverPath,
|
||||
],
|
||||
process.execPath,
|
||||
process.versions.bun
|
||||
? [this.serverPath]
|
||||
: buildNodeRuntimeArgs(process.env, this.memoryLimit, this.serverPath),
|
||||
{
|
||||
cwd: dirname(this.serverPath),
|
||||
env: this.env,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(api):** API keys can disable prompt compression from the dashboard, including for clients that cannot send custom headers ([#10001](https://github.com/diegosouzapw/OmniRoute/pull/10001)) — thanks @shixi-li
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(providers):** add DeepAI as paid API-key image provider ([#6671](https://github.com/diegosouzapw/OmniRoute/issues/6671))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(providers):** add Naga.ac and ChatAnywhere aggregator gateway providers (#6674 — thanks @chirag127)
|
||||
1
changelog.d/features/8862-novita-model-catalog.md
Normal file
1
changelog.d/features/8862-novita-model-catalog.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Providers**: expands the Novita AI catalog from a single Llama 3.1 8B entry to 19 curated serving models (DeepSeek V4, Kimi K3, GLM 5.2, MiniMax M3, Qwen3.7 Max, Qwen3 Coder 480B, MiMo V2.5 Pro, gpt-oss-120b, Gemma 4 31B and more), each carrying its real context window, output cap and reasoning flag from the live `/openai/v1/models` listing, and each vision flag confirmed by an actual image request rather than the listing's self-reported modalities
|
||||
@@ -1 +0,0 @@
|
||||
- feat(cursor): surface a dismissible dashboard banner suggesting `cursor-agent` installation when it isn't available, so Cursor connections needing periodic manual reconnection aren't a silent surprise (#9173)
|
||||
@@ -1 +0,0 @@
|
||||
- feat(cursor): proactively renew Cursor sessions before their ~24h token expires via the token health-check sweep, nudging `cursor-agent` and re-scraping IDE/agent credential sources so connections stop silently expiring (#9173)
|
||||
@@ -1,3 +0,0 @@
|
||||
- feat(images): execute full combo strategy + fallback in /v1/images/generations (#9239)
|
||||
|
||||
Adds open-sse/services/imageCombo.ts that expands combo targets, filters to images-capable, executes the priority strategy with handleImageGeneration per target, and returns the first success or last failure. Route patches detect combo names before model resolution and divert to the new execution path.
|
||||
@@ -1,2 +0,0 @@
|
||||
- **feat(sse):** combo `system_message` supports server-side `{{MODEL_ID}}`, `{{PROVIDER_ID}}`, `{{ACCOUNT}}` and `{{FINGERPRINT}}` template expansion from the actually-routed target ([#5501](https://github.com/diegosouzapw/OmniRoute/issues/5501))
|
||||
- **feat(sse):** template expansion covers the standard dispatch loop, round-robin and pinned context-cache sessions; fusion, chaos, pipeline and nested-execute strategies do not expand yet ([#5501](https://github.com/diegosouzapw/OmniRoute/issues/5501))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(providers):** add native DeepSeek V4 Flash and Pro thinking-effort aliases for their documented per-model tiers, including Combo Builder exposure ([#9485](https://github.com/diegosouzapw/OmniRoute/pull/9485)).
|
||||
@@ -1,3 +0,0 @@
|
||||
- feat(opencode-plugin): warm catalog startup from disk snapshot + parallel refresh (#9490)
|
||||
|
||||
The config-shim hook now reads the last disk snapshot before fetching, so the provider registers immediately with the last-known-good catalog (~1-2s vs ~30s on a warm gateway). All six fetchers run concurrently via Promise.allSettled instead of sequentially. A failed refresh keeps the snapshot (no overwrite). An in-flight guard prevents concurrent refreshes for the same cache key. The features.diskCache: false opt-out disables the warm read entirely.
|
||||
@@ -1 +0,0 @@
|
||||
- Add an advisory forgotten-sibling-tests report to pull-request quality checks. The report traces changed modules through their static consumers to candidate sibling tests, while keeping barrel and dynamic-import cases non-blocking and requiring reviewed, referenced exceptions.
|
||||
@@ -1 +0,0 @@
|
||||
- feat(providers): add Muse Code CLI provider preset (#9544)
|
||||
@@ -1,11 +0,0 @@
|
||||
- **feat(plugins):** add onStreamComplete built-in event exposing streaming usage and timing (#9571)
|
||||
|
||||
Adds a new `onStreamComplete` plugin event that fires after an SSE stream is fully
|
||||
consumed, carrying usage token counts and timing metrics (latency, TTFT). Built-in
|
||||
events now include `onStreamComplete` as a fire-and-forget lifecycle hook.
|
||||
|
||||
Payload: `status`, `usage` (prompt_tokens, completion_tokens, reasoning_tokens,
|
||||
cache_read_input_tokens, cache_creation_input_tokens), `timing` (latencyMs, ttft),
|
||||
`model`, `provider`, `errorCode`.
|
||||
|
||||
Non-breaking — existing `onResponse` hooks with `{ streamed: true }` remain unchanged.
|
||||
@@ -1,2 +0,0 @@
|
||||
- Show cache-read and cache-write token counts in request log rows and details when providers
|
||||
report them.
|
||||
@@ -1 +0,0 @@
|
||||
- feat(memory): support custom OpenAI-compatible endpoints for Memory embeddings (#9622)
|
||||
@@ -1 +0,0 @@
|
||||
- feat(resilience): add an opt-in watchdog for persistently slow upstream streams (#9709)
|
||||
@@ -1,4 +0,0 @@
|
||||
- **Onboarding:** add an explicit, reviewable one-click setup for eligible no-auth LLM providers,
|
||||
with per-provider caution links, selectable confirmation, idempotent creation, and safe partial
|
||||
retries. Existing provider connections are never changed and setup completion never enables
|
||||
providers silently. ([#9752](https://github.com/diegosouzapw/OmniRoute/issues/9752))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(modality bridge):** Transcribe chat audio for text-only models through the existing speech-to-text providers, with configurable limits, caching, runtime stats, and a dashboard self-test ([#9807](https://github.com/diegosouzapw/OmniRoute/pull/9807))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(memory):** `PROVIDERS_SYSTEM_MUST_BE_FIRST` (the #6135/#7293 fix for backends that reject any non-leading `system` message) was hardcoded to `xiaomi-mimo`/`mimo`. Added `OMNIROUTE_STRICT_SYSTEM_PROVIDERS` (comma-separated provider ids) so self-hosted deployments can flag additional strict backends — e.g. a custom OpenAI-compatible connection in front of a self-hosted Qwen3.5+/3.6 model — without forking and rebuilding the image ([#9924](https://github.com/diegosouzapw/OmniRoute/pull/9924))
|
||||
@@ -1 +0,0 @@
|
||||
- feat(a2a): inbound delegation to the OmniConductor fleet — `POST /api/a2a/tasks` translates an external A2A task into the hub's `POST /v1/tasks` (fleet skills only, repo required, `CONDUCTOR_ORCHESTRATOR_TOKEN` with hub-token fallback); states flow back through the SSE→A2A mirror
|
||||
@@ -1 +0,0 @@
|
||||
- feat(a2a): the Agent Card (`/.well-known/agent.json`) now announces skills derived from the OmniConductor fleet (`GET /v1/runners` OASF capabilities — one skill per online CLI profile + declared fleet skills), cached ~60s and fail-open when the hub is unset/offline
|
||||
@@ -1 +0,0 @@
|
||||
- feat(a2a): Conductor bridge — long-lived SSE consumer that mirrors OmniConductor hub tasks into the A2A TaskManager (explicit `canceled→cancelled` mapping with tests, persisted `last_event_id` cursor in the `key_value` table, exponential-backoff reconnection; opt-in via `CONDUCTOR_HUB_URL`/`CONDUCTOR_HUB_TOKEN`)
|
||||
@@ -1 +0,0 @@
|
||||
- feat(dashboard): "Conductor" panel — OmniConductor fleet (runners + task queue) live via server-side proxy routes (`/api/conductor/*`, management auth, hub token never reaches the browser), task detail with manifest/council and cancel-with-confirmation; sidebar entry under Tools
|
||||
@@ -1 +0,0 @@
|
||||
- feat(dashboard): Faro chat with voice on the Conductor panel — text via `/api/conductor/ask` (server-side proxy to the spokesperson; hub credential never reaches the browser; `pending` → Sim/Não confirmation buttons) and a guaranteed push-to-talk voice cycle (MediaRecorder → `/api/v1/audio/transcriptions` → ask → `/api/v1/audio/speech` playback), with operator-configurable STT/TTS models
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(models):** `/v1/models` now publishes one contiguous provider-grouped block per provider instead of interleaved fragments. The catalog is assembled by many independent push loops (auto-combos, named combos, static registry, codex-native, synced, OpenRouter, specialty, custom, alias-backed, connection-fallback), so one provider's models previously landed in several separated blocks. A single stable, provider-grouped sort is applied at serialization, keyed by `owned_by` (canonical owner identity) rather than the model-id prefix — so a single routable public prefix that differs from its owner (e.g. no-auth OpenCode publishing `oc/<model>` while keeping `owned_by: "opencode"`) stays contiguous. Combos are pinned first (preserving #4164); then providers in registry precedence (OAuth → NoAuth → API-key); then unknown providers in locale-independent code-unit order. The sort is stable and pure (reorders rows only, no mutation, no DB/IO), preserving combo `sort_order`, connection priority, custom append-order, and equal-id audio twins.
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(api):** make `/v1/models` stale refresh response-safe and generation-safe, with narrow synced-model invalidation ([#8728](https://github.com/diegosouzapw/OmniRoute/pull/8728)). Related to #8697.
|
||||
@@ -1 +0,0 @@
|
||||
- fix(db): invalidate stale LKGP pins when provider connections are deleted (#8887)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(executors):** Vertex AI now routes Claude models through the native Anthropic `rawPredict` endpoint instead of the generic OpenAI-compatible partner endpoint, and synthesizes a real streaming response so Claude-via-Vertex works with `stream: true` ([#8909](https://github.com/diegosouzapw/OmniRoute/pull/8909)) — thanks @wgordon17
|
||||
@@ -1,14 +0,0 @@
|
||||
- **fix(sse):** Claude reasoning-effort suffix ids (`-high`/`-low`/`-medium`/`-xhigh`) now strip
|
||||
correctly on any provider serving a real Claude model, not just the direct Anthropic provider
|
||||
([#9006](https://github.com/diegosouzapw/OmniRoute/pull/9006))
|
||||
- **fix(sse):** the no-thinking (`no-think/`) catalog variant's provider-qualification bug — which
|
||||
made it unusable outside the direct provider, both in the discovery catalog and the dashboard
|
||||
playground — is fixed ([#9006](https://github.com/diegosouzapw/OmniRoute/pull/9006))
|
||||
- **fix(sse):** a single unrecognized model id on a Vertex connection no longer cools down every
|
||||
other model on that connection for 2 minutes — Vertex 404s are now scoped to a per-model
|
||||
lockout via `passthroughModels` instead of a connection-wide cooldown
|
||||
([#9006](https://github.com/diegosouzapw/OmniRoute/pull/9006))
|
||||
- **fix(sse):** Vertex `PERMISSION_DENIED` 403s are now disambiguated using Google's own
|
||||
documented error format — a genuinely connection-wide cause (API disabled, project-level IAM
|
||||
denial) still cools the whole connection, while a model-specific denial locks out only that
|
||||
model ([#9006](https://github.com/diegosouzapw/OmniRoute/pull/9006))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(resilience):** Detect and reset idle-capacity rate-limit queue wedges on an eligible watchdog scan so routing can fall back promptly ([#9041](https://github.com/diegosouzapw/OmniRoute/pull/9041))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(translator):** Honor configured Chat targets for Responses-shaped clients while preserving native Responses providers and outbound token fields ([#9161](https://github.com/diegosouzapw/OmniRoute/pull/9161)) — thanks @Zartharas
|
||||
@@ -1 +0,0 @@
|
||||
- fix(cursor): the manual "Refresh" button on Cursor connections now calls the dedicated Cursor renewal route instead of silently returning a 502 every time (#9173)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(dashboard):** the "Default Model" of an OpenAI-compatible connection is now visible and editable after creation (was set once, then invisible), and it is no longer required when creating a connection — matching the API which always treated it as optional. ([#9179](https://github.com/diegosouzapw/OmniRoute/pull/9179))
|
||||
@@ -1,4 +0,0 @@
|
||||
- **fix(models):** Preserve published model catalogs during session-affinity bookkeeping so routine affinity updates do not force unnecessary cold rebuilds ([#9199](https://github.com/diegosouzapw/OmniRoute/pull/9199)) — thanks @xz-dev
|
||||
- **fix(models):** Reuse one build-local virtual-auto candidate snapshot across built-in catalog entries and cooperatively yield during cold catalog generation, while detaching invalidated in-flight generations so policy changes cannot publish stale results ([#9199](https://github.com/diegosouzapw/OmniRoute/pull/9199)) — thanks @xz-dev
|
||||
- **fix(models):** Resolve token limits and model capabilities once per unique candidate in that build-local snapshot, eliminating repeated SQLite lookups across the 38 built-in auto entries while preserving fresh runtime preparation and hard invalidation ([#9199](https://github.com/diegosouzapw/OmniRoute/pull/9199)) — thanks @xz-dev
|
||||
- **fix(models):** Read and parse models.dev pricing once per cold catalog build, then yield before final enrichment so queued health checks are not starved while every model in that build shares one coherent pricing snapshot ([#9199](https://github.com/diegosouzapw/OmniRoute/pull/9199)) — thanks @xz-dev
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(i18n):** localized hardcoded web UI copy across public pages, dashboard views, and shared components, with complete French and Vietnamese coverage ([#9245](https://github.com/diegosouzapw/OmniRoute/pull/9245)) — thanks @alex-jordan547
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** hoisting a mid-conversation `system`/`developer` message into the top-level `system` field no longer carries its `cache_control` marker along, which left the conversation history without a cache breakpoint and forced a full re-read plus a rebuild on the next turn. The boundary is moved to the nearest preceding block that can carry one, and now survives the rewrites that turn `tool_result` and inlined file/document blocks into plain text; if the target block is already marked, both markers are kept unless Anthropic's TTL ordering forbids it. Both hoisting paths are fixed — `extractSystemRoleMessages` and `extractSystemMessagesToBody`. Regression guard: `tests/unit/claude-system-role-cache-boundary.test.ts`. ([#9436](https://github.com/diegosouzapw/OmniRoute/issues/9436))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(ci):** tighten unit suite ceiling from 100min to 80min as a conservative step (#9532)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(ratelimit):** added queue-wait timeout tests and updateFromResponseBody sequencing tests for the existing RATE_LIMIT_QUEUE_TIMEOUT feature in withRateLimit (#9533)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(translator):** restore TitleCase tool names on the Claude → Gemini/Antigravity request path so Claude Code no longer fails with `No such tool available: read` ([#9713](https://github.com/diegosouzapw/OmniRoute/issues/9713))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(radar):** refresh signed catalog/referral caches when supporter entitlement changes, preserve the one-time live-to-community downgrade, and test real provider connection IDs from the setup tour
|
||||
@@ -1 +0,0 @@
|
||||
- **Translator**: keep the Responses namespace identity map across the hub-and-spoke pivot — namespace sub-tool calls routed to non-OpenAI targets (Kiro, Cursor) no longer come back flattened (`unsupported call: functions__exec` in Codex CLI) (#9783 — thanks @VXNCXNX)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(search): nest Exa contents options (text/highlights) for /search API (#9914)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(encryption): name failing credential + recovery path in decrypt errors, dedupe per connection (#9927)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(executors):** preserve non-strict function-tool semantics when translating Chat Completions requests to Codex Responses, avoiding intermittent streamed failures without rewriting tool schemas or dropping branch-level descriptions and annotations. ([#9931](https://github.com/diegosouzapw/OmniRoute/pull/9931))
|
||||
@@ -1 +0,0 @@
|
||||
- fix(migrations): don't abort on fresh install with only the 001 seed (#9934)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(admission):** per-connection virtual admission lanes with idle TTL eviction — guards `expireEntry` null deref, adds `deleteLane()` for safe LRU eviction, and passes `sessionId` to byte-level admission (fixes #9654)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(chat): don't misclassify content-less thinking/redacted Claude bodies as empty_choices (#9971)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(images): normalize terminal upstream errors via OpenAI-standard type/code (#9981)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(quality): green release/v3.8.50 base-reds — sync 4 env vars into .env.example/ENVIRONMENT.md and freeze the new proxied-TLS proxyFetch helper in the file-size baseline (#9985)
|
||||
@@ -1 +0,0 @@
|
||||
- **refactor(providers):** removed the retired GitHub Models provider and its catalog, discovery, embedding, free-tier, UI, and documentation surfaces; upgrades now run a durable, idempotent purge of its stored credentials, usage state, structured configuration, and call-log artifacts while preserving GitHub Copilot and live members of mixed configurations ([#9023](https://github.com/diegosouzapw/OmniRoute/pull/9023))
|
||||
@@ -1 +0,0 @@
|
||||
- **chore(quality):** expand all file-size baselines by +30% ahead of v3.8.51 (authorized DRIFT rebaseline) to unblock the pre-release queue; no functionality changes.
|
||||
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"asOf": "2026-07-28",
|
||||
"validUntil": "2026-08-27",
|
||||
"capable": [
|
||||
"deepseek-v3.2",
|
||||
"deepseek-v4-pro",
|
||||
"glm-5.2",
|
||||
"qwen-flash",
|
||||
"qwen-flash-2025-07-28",
|
||||
"qwen-flash-character",
|
||||
"qwen-max",
|
||||
"qwen-mt-flash",
|
||||
"qwen-mt-lite",
|
||||
"qwen-mt-plus",
|
||||
"qwen-mt-turbo",
|
||||
"qwen-plus-2025-04-28",
|
||||
"qwen-plus-2025-07-14",
|
||||
"qwen-plus-2025-07-28",
|
||||
"qwen-plus-2025-09-11",
|
||||
"qwen-plus-character",
|
||||
"qwen-plus-latest",
|
||||
"qwen3-14b",
|
||||
"qwen3-235b-a22b",
|
||||
"qwen3-235b-a22b-instruct-2507",
|
||||
"qwen3-235b-a22b-thinking-2507",
|
||||
"qwen3-30b-a3b",
|
||||
"qwen3-30b-a3b-instruct-2507",
|
||||
"qwen3-30b-a3b-thinking-2507",
|
||||
"qwen3-32b",
|
||||
"qwen3-8b",
|
||||
"qwen3-coder-30b-a3b-instruct",
|
||||
"qwen3-coder-480b-a35b-instruct",
|
||||
"qwen3-coder-flash",
|
||||
"qwen3-coder-flash-2025-07-28",
|
||||
"qwen3-coder-next",
|
||||
"qwen3-coder-plus",
|
||||
"qwen3-coder-plus-2025-07-22",
|
||||
"qwen3-coder-plus-2025-09-23",
|
||||
"qwen3-max",
|
||||
"qwen3-max-2025-09-23",
|
||||
"qwen3-max-2026-01-23",
|
||||
"qwen3-max-preview",
|
||||
"qwen3-next-80b-a3b-instruct",
|
||||
"qwen3-next-80b-a3b-thinking",
|
||||
"qwen3.5-122b-a10b",
|
||||
"qwen3.5-27b",
|
||||
"qwen3.5-397b-a17b",
|
||||
"qwen3.5-flash",
|
||||
"qwen3.5-flash-2026-02-23",
|
||||
"qwen3.5-plus",
|
||||
"qwen3.5-plus-2026-02-15",
|
||||
"qwen3.5-plus-2026-04-20",
|
||||
"qwen3.6-27b",
|
||||
"qwen3.6-35b-a3b",
|
||||
"qwen3.6-flash",
|
||||
"qwen3.6-flash-2026-04-16",
|
||||
"qwen3.6-max-preview",
|
||||
"qwen3.6-plus",
|
||||
"qwen3.6-plus-2026-04-02",
|
||||
"qwen3.7-flash",
|
||||
"qwen3.7-flash-2026-07-15",
|
||||
"qwen3.7-max-2026-05-17",
|
||||
"qwen3.7-max-2026-05-20",
|
||||
"qwen3.7-max-2026-06-08",
|
||||
"qwen3.7-max-preview",
|
||||
"qwen3.7-plus-2026-05-26",
|
||||
"qwq-plus"
|
||||
],
|
||||
"noFreeTier": [
|
||||
"deepseek-v4-flash",
|
||||
"glm-5.1",
|
||||
"glm-5.2-fast-preview",
|
||||
"kimi-k2.7-code",
|
||||
"qwen-plus",
|
||||
"qwen-plus-2025-01-25",
|
||||
"qwen-plus-character-ja",
|
||||
"qwen-turbo",
|
||||
"qwen3.5-35b-a3b",
|
||||
"qwen3.7-max",
|
||||
"qwen3.7-plus"
|
||||
]
|
||||
}
|
||||
@@ -98,7 +98,6 @@
|
||||
"omniglyph",
|
||||
"open",
|
||||
"opencode-ai",
|
||||
"onnxruntime-node",
|
||||
"ora",
|
||||
"parse5",
|
||||
"pino",
|
||||
@@ -125,8 +124,6 @@
|
||||
"tailwind-merge",
|
||||
"tailwindcss",
|
||||
"tls-client-node",
|
||||
"turndown",
|
||||
"turndown-plugin-gfm",
|
||||
"tsup",
|
||||
"tsx",
|
||||
"type-coverage",
|
||||
|
||||
@@ -44,21 +44,16 @@
|
||||
"count": 11
|
||||
}
|
||||
},
|
||||
"open-sse/executors/tinycms.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"open-sse/executors/tinycmsSigner.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"open-sse/executors/vertex.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"open-sse/handlers/chatCore.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"open-sse/handlers/chatCore/codexFailover.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@@ -563,6 +558,11 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/api/providers/[id]/models/route.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@@ -1788,7 +1788,7 @@
|
||||
},
|
||||
"tests/unit/chatcore-translation-paths.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 31
|
||||
"count": 34
|
||||
}
|
||||
},
|
||||
"tests/unit/chatgpt-web-tools-5240.test.ts": {
|
||||
|
||||
@@ -1,26 +1,10 @@
|
||||
{
|
||||
"_rebaseline_2026_08_09_8984_api_key_cache_mode": "PR #8984 own growth during the 2026-08-09 rebase: src/lib/db/apiKeys.ts 1529->1545 (+16 = the per-key apiKeys.cacheDefaultMode column + its row parsers and cascade wiring; additive at the existing connection write/read chokepoints). Covered by tests/unit/chatcore-semantic-cache.test.ts. (chatCore.ts stays at the pre-existing base-red ceiling — upstream tip already exceeds the frozen 5042, this PR only adds +2 on top; not re-bumped per the no-inherit-ratchet rule.)",
|
||||
"_rebaseline_2026_08_09_9207_breaker_halfopen_recovery": "PR #9207 own growth during the 2026-08-09 rebase: open-sse/services/accountFallback.ts 1978->2020 (+42 = recordProviderSuccess now also transitions the provider circuit breaker from HALF_OPEN to CLOSED when a request succeeds, so the breaker is not stuck half-open after repeated failures; the transition and its reset wiring grow the existing provider-success path, not extractable). Covered by tests/unit/provider-breaker-halfopen-recovery.test.ts.",
|
||||
"_rebaseline_2026_08_09_9351_antigravity_switch_auth": "PR #9351 own growth during the 2026-08-09 rebase: open-sse/executors/antigravity.ts 1528->1536 (+8 = switchAuth threaded out of tryResolveRetryFromErrorBody into handleAntigravityRateLimit's short-retry guard, so a decide429 switch decision beats the 60s same-account sleep; cohesive at the existing resolve chokepoint, not extractable). Covered by tests/unit/antigravity-429-switch-auth.test.ts.",
|
||||
|
||||
"_rebaseline_2026_08_09_9328_bottleneck_doexpire_rate_limit": "PR #9328 own growth during the 2026-08-09 rebase: open-sse/services/rateLimitManager.ts 1167->1221 (the Bottleneck doExpire capacity-leak monkey-patch plus its diagnostic branch and deterministic assertions live at the manager's existing wiring; monolithic patch, not extractable). Covered by tests/unit/bottleneck-doexpire-patch.test.ts.",
|
||||
"_rebaseline_2026_08_09_9296_adobe_media_capabilities": "PR #9296 (artickc, fix/adobe-firefly-model-capabilities) own growth: src/app/api/v1/models/catalog.ts 1590->1597 (+7). The image and video catalog serializers now expose the already-normalized Adobe Firefly discovery capability data (media_capabilities, plus the existing video modality/size fields) at their only response-emission chokepoints. The discovery parser and capability normalization remain in open-sse/services/adobeFireflyModels.ts; extracting these seven serialization fields would obscure the catalog contract. Covered by tests/unit/adobe-firefly.test.ts and tests/unit/image-upscale.test.ts.",
|
||||
"_rebaseline_2026_08_08_v3850_base_drift_batch_9757": "Base drift on release/v3.8.50, not own growth: the 08-06..08-08 merge batches grew 12 already-frozen (or newly-landed) files without carrying their rebaselines — the dedicated rebaseline PR #9616 was closed as 'superseded' but its file-size entries never actually reached the base, and later merges (#8894 combos page, #9539 EditConnectionModal, #8895 models route, #9294/#9293 catalog, #9541 db/core, #8970 tokenHealthCheck, #8925 mcp schemas+server, #8890 accountFallback, #9467 chat.ts, #8931 openai-to-kiro, ProxyRegistryManager) kept growing them. All 12 values re-measured on THIS branch's tree (= pure tip + this PR's 1-line chat.ts fix, which adds zero lines). This PR's own source changes (chat.ts identifier restore, stream.ts format carve-out) do not grow any frozen file past these values.",
|
||||
"_rebaseline_2026_08_08_migration_135_collision": "fix(db): resolve migration version 135 numbering collision — #9449's 135_connection_runtime_state.sql and #8908's 135_migrate_model_capability_max_token.sql both claimed version 135 (#9449 branched before #8908 merged and never got renumbered before landing on release/v3.8.50), which threw 'Migration version collision detected' the moment ANY code touched the database — a fresh install/deploy from this tip cannot even boot. Renumbered the later-landing file to 140 (next free slot) and added the matching isSchemaAlreadyApplied('140') retroactive guard, matching the established pattern already used for the prior 135/136 -> 137/138 renumber in the same file. Own growth: src/lib/db/migrationRunner.ts 1084->1094 (+10, the new case block) — irreducible, matches the existing per-case guard pattern exactly. Covered by tests/unit/migration-135-numbering-collision.test.ts (2/2), confirmed failing (reproducing the exact live crash) against the pre-fix colliding filenames, passing after.",
|
||||
"_rebaseline_2026_08_08_9183_reasoning_cache_index_sync": "Extracted fix(responses-api): sync reasoning-cache write index with the fixed read side (from the originally-authored #9183) — chatCore.ts's write side cached every response under a hardcoded messageIndex:0, and translator/index.ts's plain-turn (non-tool-call) cache-key lookup ALSO still hardcoded messageIndex 0 at its call site (a second, previously-undiscovered instance of the same hardcoding bug, found while re-verifying this fix against the current upstream tip — the two never agreed once a conversation went past its first assistant turn, so DeepSeek/Xiaomi-mimo plain-turn reasoning replay silently missed the cache). Own growth: open-sse/handlers/chatCore.ts 5034->5042 (+8, computing messageIndex from the incoming request's message count at both the streaming and non-streaming cache-write call sites) — irreducible call-site wiring. Covered by tests/unit/reasoning-cache.test.ts (new end-to-end write/read regression test, rebaselined below) and tests/unit/translator-helper-branches.test.ts fixture updates. Other #9183 sub-fixes (output_index collision prevention, reasoning-content-alias generalization) were originally assumed already superseded by upstream's own independent fix — a live incident 2026-08-08 disproved that for the message-vs-tool-call collision case specifically (fixed separately in #9822); not re-extracted here since this PR's own scope is the narrower messageIndex sync only.",
|
||||
"_rebaseline_2026_08_09_9342_network_error_guard": "PR #9342 own growth during the 2026-08-09 rebase: open-sse/services/accountFallback.ts 1978->2008 (+30 = the isQueueTimeout short-circuit plus a per-provider network-error dedup window in recordProviderFailure, keeping one VPN blip from the same provider's combo targets counting once per target). Covered by tests/unit/breaker-network-error-guard.test.ts. (chat.ts stays base-red: upstream tip is already 1918 > frozen 1904, this PR only adds +12 on top; not re-bumped per the no-inherit-ratchet rule.)", "_rebaseline_2026_08_09_9296_adobe_media_capabilities": "PR #9296 (artickc, fix/adobe-firefly-model-capabilities) own growth: src/app/api/v1/models/catalog.ts 1590->1597 (+7). The image and video catalog serializers now expose the already-normalized Adobe Firefly discovery capability data (media_capabilities, plus the existing video modality/size fields) at their only response-emission chokepoints. The discovery parser and capability normalization remain in open-sse/services/adobeFireflyModels.ts; extracting these seven serialization fields would obscure the catalog contract. Covered by tests/unit/adobe-firefly.test.ts and tests/unit/image-upscale.test.ts.", "_rebaseline_2026_08_02_9259_rolling_rpm": "PR #9259 (issue #8733) own growth: open-sse/services/rateLimitManager.ts baseline 1060->1167 (+107; final source 1153). The existing withRateLimit chokepoint now composes process-local rolling RPM leases with Bottleneck admission, releases pre-dispatch leases on queue timeout/abort/connection disable, preserves caller abort reasons, and wires 429/header state into the extracted rollingRpmGate.ts. The remaining growth is irreducible lifecycle wiring at the dispatch boundary plus the real watchdog test hooks needed to verify queued-wedge recovery; moving it further would obscure lease ownership and Bottleneck cleanup. Covered by the focused rate-limit manager/sliding-window suite (33/33); distributed multi-instance coordination remains explicitly out of scope.",
|
||||
"_rebaseline_2026_07_24_8470_hyperagent_sticky_thread": "PR #8470 (artickc, fix/hyperagent-tool-loop-thread-sticky) own growth: open-sse/executors/hyperagent.ts 936->1025 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 937->1026, +89, crosses the 1000 cap). Fixes a real bug where a reverse-conversion proxy (text-Intent/JSON to Claude Code native tool_calls) rewrites assistant messages between agentic tool-loop turns, breaking HyperAgent\u2019s conversation-prefix fingerprint and cold-starting the thread mid tool-loop. Adds Anthropic tool_use/tool_result flattening to extractMessageText() plus a new rootUserFingerprint()/root-key lookup tier in resolveHyperAgentThreadBinding()/storeHyperAgentThreadAfterTurn() so the thread stays sticky across the tool loop. Cohesive additions inside the existing single-file executor; not extractable without splitting the executor mid-request-flow. Covered by tests/unit/executor-hyperagent.test.ts (19/19, +5 new cases for tool_result/tool_use flattening + root-key stickiness). Pre-merge review flagged a cross-conversation root-key collision risk (tracked in the PR\u2019s own mandatory pre-merge checklist, not yet addressed) \u2014 unrelated to this file-size ratchet, tracked separately by /fix-prs.",
|
||||
"_rebaseline_2026_07_25_8494_capability_filter_fail_closed": "PR #8494 (fix/capability-filters-fail-closed, #8488) own growth: open-sse/services/combo.ts 3640->3693 (+53) adds a fail-closed guard after filterTargetsByRequestCompatibility() \u2014 when every eligible target is excluded by request-capability filtering (vision/tools/etc) instead of quota/health, the combo now returns an explicit `capability_mismatch` 400 (describeCapabilityFilterExhaustion, imported from combo/comboStructure.ts) rather than silently falling through to a generic no-targets error, plus a `compatFilterFailOpen` escape hatch (combo config OR settings) mirrored at both the main/auto and round-robin call sites for symmetry. combo/comboStructure.ts (previously under cap, un-frozen) grows 794->918 (+124) \u2014 new home for describeCapabilityFilterExhaustion + providerSupportsEmulatedToolCalling (#5240 emulated tool-calling exemption so fail-closed does not regress prompt-emulation-only combos like all-chatgpt-web). Irreducible orchestration wiring at the existing filter chokepoint (same precedent as #7301's universal-cooldown-retry generalization). Companion test tests/unit/combo-routing-engine.test.ts 3409->3449 (+40, fail-closed/fail-open coverage across both call sites) also rebaselined. Covered by tests/unit/8488-capability-filter-fail-closed.test.ts (new) + 95/95 passing across both files. Structural shrink of combo.ts tracked in #3501.",
|
||||
"_rebaseline_2026_07_25_8499_ts7_result_union_predicates": "PR #8499 (backryun, chore/ts7-types-executor-scattered) own growth: muse-spark-web.ts 1396->1405 (+9, irreducible). Under this workspace's `strictNullChecks: false`, the boolean-literal discriminant on `GraphqlResult` (`{ ok: true } | { ok: false; error: string }`) narrows the positive `.ok===true` branch but leaves `!result.ok` at the full union under TS7, making `.error` unreachable to the checker at the two call sites (warmup, mode-switch). Fixed by adding a single `isGraphqlFailure()` type-predicate helper (doc comment + 3-line body) reused at both call sites instead of duplicating the predicate inline \u2014 not extractable to a shared module without splitting a single-file executor's local narrowing helper out of its own file. Covered by the existing muse-spark-web executor test suite (no behavior change, pure narrowing fix).",
|
||||
"_rebaseline_2026_07_22_8131_windowshide_cloudflared_spawn": "PR #8167 (Dingding-leo, fix/windows-hide-child-process, #8131) own growth: src/lib/cloudflaredTunnel.ts 934->935 (+1, irreducible call-site wiring \u2014 the single `windowsHide: true` option added to the existing cloudflared spawn() options object so no transient conhost.exe/cmd console window flashes open on Windows). Covered by the pre-merge-fix regression test tests/unit/windows-hide-child-process-spawns-8131.test.ts (added for the two additional spawn() sites the PR missed: ServiceSupervisor.ts, versionManager/processManager.ts) plus the windowsHide assertion added to tests/unit/services/installers/runNpm-shell-5379.test.ts (installers/utils.ts buildNpmExecOptions).",
|
||||
"_rebaseline_2026_07_22_8006_adobe_firefly_media_provider": "PR #8006 (artickc, feat/adobe-firefly-media) own growth: adds Adobe Firefly as a media-only (image + video) provider \u2014 unofficial IMS/cookie-session bridge for firefly.adobe.com covering IMS cookie->access_token exchange, discovery-catalog fallback, credits/balance usage, and submit+poll dispatch for both image (nano-banana/gpt-image families) and video (Sora 2/Veo 3.1/Kling 3.0) generation, with 408-under-load retry handling. New leaf open-sse/services/adobeFireflyClient.ts frozen at 1958 (>>cap 800) \u2014 a single self-contained upstream client (mirrors the qoderCli.ts precedent for a new provider client that is legitimately large on day one: IMS auth, cookie/JWT normalization, payload builders for 2 media types x multiple model families, SSE-less submit/poll state machine, error sanitization); not extractable without scattering a single upstream integration across artificial module boundaries mid-PR. open-sse/config/imageRegistry.ts (existing, previously under cap) grows 800->821 (+21, the new adobe-firefly IMAGE_PROVIDERS entry + models list, additive registry data at the existing registry chokepoint). src/lib/usage/providerLimits.ts 1000->1003 (+3, adobe-firefly/firefly added to the existing apikey-usage-fetcher allowlist, irreducible call-site wiring mirroring the sibling #7994 PromptQL/HyperAgent entries in the same PR group). Covered by tests/unit/adobe-firefly.test.ts (35/35). Structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_07_22_7994_hyperagent_web_provider": "PR #7994 (artickc, feat/hyperagent-web) own growth: adds HyperAgent (hyperagent.com) as a new unofficial web-cookie chat provider, reverse-engineered from live SPA captures (thread/session SSE flow, credits/usage endpoint). New leaf open-sse/executors/hyperagent.ts frozen at 937 (>cap 800) \u2014 single self-contained executor covering cookie auth, SSE parsing (text/session_start/session_end/done events), and a sticky thread/session cache for multi-turn continuity; not extractable without splitting the executor mid-request-flow (mirrors the sseParser.ts/muse-spark-web.ts precedent for new provider executors that exceed cap on day one). src/lib/usage/providerLimits.ts 1000->1003 (+3, irreducible call-site wiring adding hyperagent/ha to the existing USAGE_FETCHER_PROVIDERS-style allowlist at the chokepoint other web-cookie providers already extend). Covered by tests/unit/executor-hyperagent.test.ts (16/16). Structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_08_08_9173_own_comment_growth": "PR #9173's own follow-up commit (f0a694051): tests/unit/combo-routing-engine.test.ts 3457->3464 (+7) is this PR's own growth — explanatory comment blocks added alongside the ALL_ACCOUNTS_INACTIVE->ALL_TARGETS_SKIPPED stale-assertion fix (matching the identical fix applied to #9619/#9006/#8909 the same day; upstream's own test was never updated when the recordedAttempts===0 pre-dispatch-skip branch shipped). Caught by CI's PR-mode check:file-size (--base-ref) after the fix commit; missed locally because check-file-size.mjs was not re-run after that specific edit. Also fixed this round: src/i18n/messages/vi.json was missing 4 keys (cursorSessionUnchanged, cursorAgentNudgeTitle/Body/Dismiss) that this PR's own pre-merge branch had translated — the original merge's `git checkout --theirs` resolution for the 7 conflicted locale files discarded them since upstream's vi.json (which has no cursor-token-renewal feature) never had them. Restored from this PR's pre-merge tip (a38003e30).",
|
||||
"_rebaseline_2026_08_08_9173_vi_json_restore": "PR #9173's own follow-up commit: src/i18n/messages/vi.json was missing 4 keys (cursorSessionUnchanged, cursorAgentNudgeTitle/Body/Dismiss) that this PR's own pre-merge branch had translated — the original merge's `git checkout --theirs` resolution for the 7 conflicted locale files discarded them since upstream's vi.json (which has no cursor-token-renewal feature) never had them. Restored from this PR's pre-merge tip (a38003e30).",
|
||||
"_rebaseline_2026_08_07_9173_reconcile_onto_tip": "PR #9173 (cursor-token-renewal) full reconcile-onto-tip merge with release/v3.8.50 (2026-08-07). base.ts 1619->1681 and chatCore.ts 5028->5031 grew further past the 2026-08-02 rebaseline below via already-merged, no-PR-branch-left commits unrelated to this PR's own Cursor renewal changes (measured directly on the merged tree, split(\"\\n\").length). Same merge also surfaced 11 file + 1 test-file violations shared with PR #9619's identical-base reconciliation the same day (open-sse/mcp-server/schemas/tools.ts 1505->1553, open-sse/mcp-server/server.ts 1411->1444, open-sse/services/accountFallback.ts 1972->1978, src/app/(dashboard)/dashboard/combos/page.tsx 4647->4703, EditConnectionModal.tsx 1316->1324, src/app/api/providers/[id]/models/route.ts 2250->2304, src/app/api/v1/models/catalog.ts 1549->1556, src/lib/db/core.ts 1637->1639, src/sse/handlers/chat.ts 1877->1878, tests/unit/translator-openai-to-gemini.test.ts 1619->1622) plus two more specific to this PR's own additive work compounding with inherited drift: src/lib/tokenHealthCheck.ts 1021->1101 (this PR's own +48 cursor-token-renewal refresh-health logic, per _rebaseline_2026_08_02_9242_token_health_transient's file, plus +32 independent upstream growth) and useProviderConnections.ts 986->1002 (this PR's own +12, plus +41 independent upstream growth — newly crosses the 1000 cap). Same root cause as every other entry in this chain: fast-gates PR->release does not run check:file-size. No offending branch left to fix.",
|
||||
"_rebaseline_2026_08_02_agentrouter_ccbeta_regression_fix": "PR #9173 (cursor-token-renewal) own growth: open-sse/executors/base.ts 1578->1619 (+41). Fixes a real regression from the same two already-merged agentrouter commits documented in _rebaseline_2026_08_02_agentrouter_protocol_dispatch above — usesClaudeCodeProtocol() widened the native-Claude system-transform block (billing header, selectBetaFlags-derived anthropic-beta) to also run for CC-compatible relay connections. selectBetaFlags() has no visibility into a relay's own providerSpecificData.requestDefaults: for a relay with explicit requestDefaults configured (context1m/redactThinking/summarizeThinking), its Object.assign() silently discarded the relay's own correctly-computed headers (wiping an earlier CONTEXT_1M_BETA_HEADER append, force-including redact-thinking-2026-02-12 regardless of opt-in). For a 'vanilla' relay with no requestDefaults at all, the native treatment is pre-existing, intentional behavior (tests/unit/cc-compatible-provider.test.ts, v3.6.6) — the earlier version of this fix broke that case by excluding CC-relays unconditionally. The final gate is `this.provider === \"claude\" || usesCcWireImage(this.provider) || !hasCcRequestDefaults` (native treatment applies unless the relay has explicit requestDefaults), plus an unconditional post-pass that strips the redact-thinking beta unless the relay's own requestDefaults opted in. Covered by tests/unit/executor-default-base.test.ts ('uses CC-compatible connection defaults to append 1M beta'), tests/unit/cc-compatible-provider.test.ts (both SSE-forcing tests), and tests/unit/provider-request-failure-pipeline.test.ts ('keeps request beta headers and summarized thinking body') — all pre-existing, all independently re-verified passing together.",
|
||||
"_rebaseline_2026_08_02_agentrouter_protocol_dispatch": "Reconcile-onto-tip drift surfaced by PR #9173 (cursor-token-renewal): two already-merged, no-PR-branch-left commits on release/v3.8.50 (564c204ef fix(agentrouter): support Claude and Codex protocols; ec150a006 fix(agentrouter): honor alternate protocol in chat pipeline) grew open-sse/executors/base.ts 1562->1578 (Claude/Codex protocol dispatch wiring in the agentrouter executor branch) and open-sse/handlers/chatCore.ts 5020->5028 + tests/unit/chatcore-translation-paths.test.ts 2769->2776 (alternate-protocol chat-pipeline routing + companion test coverage) past their frozen caps, unrelated to this PR's own Cursor renewal changes. Same pattern as the prior release-green rebaselines (fast-gates PR->release do not run check:file-size): no offending branch left to fix in-place. Real sizes per check-file-size.mjs's own split(\"\\n\").length metric.",
|
||||
"_rebaseline_2026_07_24_8470_hyperagent_sticky_thread": "PR #8470 (artickc, fix/hyperagent-tool-loop-thread-sticky) own growth: open-sse/executors/hyperagent.ts 936->1025 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 937->1026, +89, crosses the 1000 cap). Fixes a real bug where a reverse-conversion proxy (text-Intent/JSON to Claude Code native tool_calls) rewrites assistant messages between agentic tool-loop turns, breaking HyperAgent's conversation-prefix fingerprint and cold-starting the thread mid tool-loop. Adds Anthropic tool_use/tool_result flattening to extractMessageText() plus a new rootUserFingerprint()/root-key lookup tier in resolveHyperAgentThreadBinding()/storeHyperAgentThreadAfterTurn() so the thread stays sticky across the tool loop. Cohesive additions inside the existing single-file executor; not extractable without splitting the executor mid-request-flow. Covered by tests/unit/executor-hyperagent.test.ts (19/19, +5 new cases for tool_result/tool_use flattening + root-key stickiness). Pre-merge review flagged a cross-conversation root-key collision risk (tracked in the PR's own mandatory pre-merge checklist, not yet addressed) — unrelated to this file-size ratchet, tracked separately by /fix-prs.",
|
||||
"_rebaseline_2026_08_02_9259_rolling_rpm": "PR #9259 (issue #8733) own growth: open-sse/services/rateLimitManager.ts baseline 1060->1167 (+107; final source 1153). The existing withRateLimit chokepoint now composes process-local rolling RPM leases with Bottleneck admission, releases pre-dispatch leases on queue timeout/abort/connection disable, preserves caller abort reasons, and wires 429/header state into the extracted rollingRpmGate.ts. The remaining growth is irreducible lifecycle wiring at the dispatch boundary plus the real watchdog test hooks needed to verify queued-wedge recovery; moving it further would obscure lease ownership and Bottleneck cleanup. Covered by the focused rate-limit manager/sliding-window suite (33/33); distributed multi-instance coordination remains explicitly out of scope.",
|
||||
"_rebaseline_2026_07_24_8470_hyperagent_sticky_thread": "PR #8470 (artickc, fix/hyperagent-tool-loop-thread-sticky) own growth: open-sse/executors/hyperagent.ts 936->1025 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 937->1026, +89, crosses the 1000 cap). Fixes a real bug where a reverse-conversion proxy (text-Intent/JSON to Claude Code native tool_calls) rewrites assistant messages between agentic tool-loop turns, breaking HyperAgent’s conversation-prefix fingerprint and cold-starting the thread mid tool-loop. Adds Anthropic tool_use/tool_result flattening to extractMessageText() plus a new rootUserFingerprint()/root-key lookup tier in resolveHyperAgentThreadBinding()/storeHyperAgentThreadAfterTurn() so the thread stays sticky across the tool loop. Cohesive additions inside the existing single-file executor; not extractable without splitting the executor mid-request-flow. Covered by tests/unit/executor-hyperagent.test.ts (19/19, +5 new cases for tool_result/tool_use flattening + root-key stickiness). Pre-merge review flagged a cross-conversation root-key collision risk (tracked in the PR’s own mandatory pre-merge checklist, not yet addressed) — unrelated to this file-size ratchet, tracked separately by /fix-prs.",
|
||||
"_rebaseline_2026_07_25_8494_capability_filter_fail_closed": "PR #8494 (fix/capability-filters-fail-closed, #8488) own growth: open-sse/services/combo.ts 3640->3693 (+53) adds a fail-closed guard after filterTargetsByRequestCompatibility() — when every eligible target is excluded by request-capability filtering (vision/tools/etc) instead of quota/health, the combo now returns an explicit `capability_mismatch` 400 (describeCapabilityFilterExhaustion, imported from combo/comboStructure.ts) rather than silently falling through to a generic no-targets error, plus a `compatFilterFailOpen` escape hatch (combo config OR settings) mirrored at both the main/auto and round-robin call sites for symmetry. combo/comboStructure.ts (previously under cap, un-frozen) grows 794->918 (+124) — new home for describeCapabilityFilterExhaustion + providerSupportsEmulatedToolCalling (#5240 emulated tool-calling exemption so fail-closed does not regress prompt-emulation-only combos like all-chatgpt-web). Irreducible orchestration wiring at the existing filter chokepoint (same precedent as #7301's universal-cooldown-retry generalization). Companion test tests/unit/combo-routing-engine.test.ts 3409->3449 (+40, fail-closed/fail-open coverage across both call sites) also rebaselined. Covered by tests/unit/8488-capability-filter-fail-closed.test.ts (new) + 95/95 passing across both files. Structural shrink of combo.ts tracked in #3501.",
|
||||
"_rebaseline_2026_07_25_8499_ts7_result_union_predicates": "PR #8499 (backryun, chore/ts7-types-executor-scattered) own growth: muse-spark-web.ts 1396->1405 (+9, irreducible). Under this workspace's `strictNullChecks: false`, the boolean-literal discriminant on `GraphqlResult` (`{ ok: true } | { ok: false; error: string }`) narrows the positive `.ok===true` branch but leaves `!result.ok` at the full union under TS7, making `.error` unreachable to the checker at the two call sites (warmup, mode-switch). Fixed by adding a single `isGraphqlFailure()` type-predicate helper (doc comment + 3-line body) reused at both call sites instead of duplicating the predicate inline — not extractable to a shared module without splitting a single-file executor's local narrowing helper out of its own file. Covered by the existing muse-spark-web executor test suite (no behavior change, pure narrowing fix).",
|
||||
"_rebaseline_2026_07_22_8131_windowshide_cloudflared_spawn": "PR #8167 (Dingding-leo, fix/windows-hide-child-process, #8131) own growth: src/lib/cloudflaredTunnel.ts 934->935 (+1, irreducible call-site wiring — the single `windowsHide: true` option added to the existing cloudflared spawn() options object so no transient conhost.exe/cmd console window flashes open on Windows). Covered by the pre-merge-fix regression test tests/unit/windows-hide-child-process-spawns-8131.test.ts (added for the two additional spawn() sites the PR missed: ServiceSupervisor.ts, versionManager/processManager.ts) plus the windowsHide assertion added to tests/unit/services/installers/runNpm-shell-5379.test.ts (installers/utils.ts buildNpmExecOptions).",
|
||||
@@ -183,8 +167,8 @@
|
||||
"cap": 1000,
|
||||
"testCap": 1000,
|
||||
"testFrozen": {
|
||||
"tests/unit/adobe-firefly.test.ts": 1477,
|
||||
"tests/unit/reasoning-cache.test.ts": 1346,
|
||||
"tests/unit/adobe-firefly.test.ts": 1136,
|
||||
"tests/unit/reasoning-cache.test.ts": 1035,
|
||||
"_rebaseline_2026_06_27_5193_antigravity_test": "#5193 own test growth: oauth-providers-config.test.ts 870->873 (+3: antigravity projectId assertion + 50ms tick for the now fire-and-forget onboarding, matching the no-PKCE/no-openid flow).",
|
||||
"_rebaseline_2026_07_02_5928_base_red": "web-cookie-providers-new.test.ts 845->850: #5928 (test(security) Kimi Web URL host parse, CodeQL #689) grew the file +5 lines and merged into release/v3.8.44 WITHOUT rebaselining, leaving a fast-gates base-red that blocked every subsequent PR->release. Test growth is legitimate (a security regression test); maintainer absorbs the drift here. Frozen at 850.",
|
||||
"_rebaseline_2026_07_09_6126_clinepass_dualauth": "#6126 (ClinePass dual-auth) own test growth: oauth-providers-config.test.ts 842->845 (+3: clinepass key/config/required-fields entries reusing the Cline WorkOS flow config, needed after registering clinepass in the oauth.ts PROVIDERS enum).",
|
||||
@@ -196,39 +180,39 @@
|
||||
"_rebaseline_2026_07_25_8510_adobe_firefly_reference_images_tests": "#8510 (artickc, feat/adobe-firefly-reference-images) own test growth: tests/unit/adobe-firefly.test.ts 711->871 (+159, entirely this PR's diff — new referenceBlobs upload/dispatch coverage for handleAdobeFireflyImageGeneration, resolveAdobeSourceImageIds, and the storage-upload wire contract). Route-level /v1/images/edits coverage (credentials/rate-limit/4-ref-cap branches added to route.ts) lives in the new tests/unit/8510-adobe-firefly-edits-route.test.ts instead of growing this file further.",
|
||||
"_rebaseline_basered_codebuddy_cn": "Base-red fix (#4664 CodeBuddy CN): oauth-providers-config.test.ts 867->870 (+3) to align the EXPECTED provider list/config with the codebuddy-cn provider that #4664 added to the registry without updating this test (it asserts 'exactly once').",
|
||||
"_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/integration/chat-pipeline.test.ts": 2077,
|
||||
"tests/integration/chatcore-compression-integration.test.ts": 1448,
|
||||
"tests/unit/account-fallback-service.test.ts": 2032,
|
||||
"tests/unit/batch_api.test.ts": 1721,
|
||||
"tests/unit/cc-compatible-provider.test.ts": 1582,
|
||||
"tests/unit/chatcore-translation-paths.test.ts": 3739,
|
||||
"tests/unit/chatgpt-web.test.ts": 4092,
|
||||
"tests/unit/combo-routing-engine.test.ts": 4494,
|
||||
"tests/unit/db-migration-runner.test.ts": 1949,
|
||||
"tests/unit/deepseek-web.test.ts": 1420,
|
||||
"tests/unit/executor-codex.test.ts": 1741,
|
||||
"tests/unit/executor-default-base.test.ts": 1975,
|
||||
"tests/unit/grok-web.test.ts": 3168,
|
||||
"tests/unit/image-generation-handler.test.ts": 2638,
|
||||
"tests/unit/model-sync-route.test.ts": 1321,
|
||||
"tests/unit/models-catalog-route.test.ts": 2127,
|
||||
"tests/unit/perplexity-web.test.ts": 1762,
|
||||
"tests/unit/provider-models-route.test.ts": 2323,
|
||||
"tests/unit/provider-validation-specialty.test.ts": 3880,
|
||||
"tests/unit/providers-page-utils.test.ts": 1438,
|
||||
"tests/unit/response-sanitizer.test.ts": 1382,
|
||||
"tests/unit/route-edge-coverage.test.ts": 1613,
|
||||
"tests/unit/search-handler-extended.test.ts": 1392,
|
||||
"tests/unit/sse-auth.test.ts": 2093,
|
||||
"tests/unit/stream-utils.test.ts": 3178,
|
||||
"tests/unit/token-refresh-service.test.ts": 1791,
|
||||
"tests/unit/translator-openai-responses-req.test.ts": 1552,
|
||||
"tests/unit/translator-openai-to-gemini.test.ts": 2109,
|
||||
"tests/unit/translator-openai-to-kiro.test.ts": 1658,
|
||||
"tests/unit/translator-resp-gemini-to-openai.test.ts": 1604,
|
||||
"tests/unit/usage-service-hardening.test.ts": 1928,
|
||||
"tests/unit/vscode-token-routes.test.ts": 1633,
|
||||
"tests/unit/executor-antigravity.test.ts": 1427
|
||||
"tests/integration/chat-pipeline.test.ts": 1598,
|
||||
"tests/integration/chatcore-compression-integration.test.ts": 1114,
|
||||
"tests/unit/account-fallback-service.test.ts": 1563,
|
||||
"tests/unit/batch_api.test.ts": 1324,
|
||||
"tests/unit/cc-compatible-provider.test.ts": 1217,
|
||||
"tests/unit/chatcore-translation-paths.test.ts": 2876,
|
||||
"tests/unit/chatgpt-web.test.ts": 3148,
|
||||
"tests/unit/combo-routing-engine.test.ts": 3457,
|
||||
"tests/unit/db-migration-runner.test.ts": 1499,
|
||||
"tests/unit/deepseek-web.test.ts": 1092,
|
||||
"tests/unit/executor-codex.test.ts": 1339,
|
||||
"tests/unit/executor-default-base.test.ts": 1519,
|
||||
"tests/unit/grok-web.test.ts": 2437,
|
||||
"tests/unit/image-generation-handler.test.ts": 2029,
|
||||
"tests/unit/model-sync-route.test.ts": 1016,
|
||||
"tests/unit/models-catalog-route.test.ts": 1636,
|
||||
"tests/unit/perplexity-web.test.ts": 1355,
|
||||
"tests/unit/provider-models-route.test.ts": 1787,
|
||||
"tests/unit/provider-validation-specialty.test.ts": 2985,
|
||||
"tests/unit/providers-page-utils.test.ts": 1106,
|
||||
"tests/unit/response-sanitizer.test.ts": 1063,
|
||||
"tests/unit/route-edge-coverage.test.ts": 1241,
|
||||
"tests/unit/search-handler-extended.test.ts": 1071,
|
||||
"tests/unit/sse-auth.test.ts": 1610,
|
||||
"tests/unit/stream-utils.test.ts": 2445,
|
||||
"tests/unit/token-refresh-service.test.ts": 1378,
|
||||
"tests/unit/translator-openai-responses-req.test.ts": 1194,
|
||||
"tests/unit/translator-openai-to-gemini.test.ts": 1622,
|
||||
"tests/unit/translator-openai-to-kiro.test.ts": 1275,
|
||||
"tests/unit/translator-resp-gemini-to-openai.test.ts": 1234,
|
||||
"tests/unit/usage-service-hardening.test.ts": 1483,
|
||||
"tests/unit/vscode-token-routes.test.ts": 1256,
|
||||
"tests/unit/executor-antigravity.test.ts": 1098
|
||||
},
|
||||
"_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.",
|
||||
@@ -365,81 +349,79 @@
|
||||
"_rebaseline_2026_07_25_adobe_firefly_reference_images": "Follow-up to #8006: storage upload + referenceBlobs for image/video and /v1/images/edits dispatch. adobeFireflyClient.ts 1958->2317 (+upload helpers, extract sources, resolve blob ids). Note: 2317 not 2316 — check-file-size.mjs counts LOC via split(\"\\n\").length (counts the trailing-newline empty element), which is 1 higher than `wc -l` on a file ending in \\n; the PR's original entry (2316) was measured with wc -l and undercounted by 1 against the actual gate.",
|
||||
"_rebaseline_pr1043_minimax_tts": "Upstream port decolua/9router#1043 (toanalien) own growth: audioSpeech.ts 965->1061 (+96). Adds MiniMax T2A v2 TTS dispatch (handleMinimaxSpeech + hexToBytes helper) — provider entry was already in audioRegistry (format: minimax-tts) but no handler existed, falling through to the OpenAI-compatible default that fails (T2A has custom shape + hex-encoded audio + base_resp envelope). New branch sits next to the other inline provider branches (xiaomi-mimo, coqui, tortoise, aws-polly) — extracting would just create indirection. Covered by tests/unit/minimax-tts-1043.test.ts (3 tests, GREEN: success, base_resp error, invalid-hex).",
|
||||
"_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.",
|
||||
"open-sse/executors/antigravity.ts": 1986,
|
||||
"open-sse/executors/base.ts": 2132,
|
||||
"open-sse/executors/chatgpt-web.ts": 4213,
|
||||
"open-sse/executors/codex.ts": 2031,
|
||||
"open-sse/executors/cursor.ts": 2032,
|
||||
"open-sse/executors/deepseek-web.ts": 1492,
|
||||
"open-sse/executors/grok-web.ts": 1357,
|
||||
"open-sse/executors/muse-spark-web.ts": 1826,
|
||||
"open-sse/handlers/chatCore.ts": 6579,
|
||||
"open-sse/handlers/imageGeneration.ts": 4031,
|
||||
"open-sse/handlers/responseSanitizer.ts": 1466,
|
||||
"open-sse/handlers/search.ts": 1997,
|
||||
"open-sse/handlers/videoGeneration.ts": 1382,
|
||||
"open-sse/mcp-server/schemas/tools.ts": 2019,
|
||||
"open-sse/mcp-server/server.ts": 1882,
|
||||
"open-sse/mcp-server/tools/advancedTools.ts": 1456,
|
||||
"open-sse/services/accountFallback.ts": 2571,
|
||||
"open-sse/services/adobeFireflyBrowserLogin.ts": 1771,
|
||||
"open-sse/services/adobeFireflyChromeRuntime.ts": 1561,
|
||||
"open-sse/services/adobeFireflyClient.ts": 3899,
|
||||
"open-sse/services/adobeFireflySession.ts": 1304,
|
||||
"open-sse/services/claudeCodeCompatible.ts": 1563,
|
||||
"open-sse/services/combo.ts": 4742,
|
||||
"open-sse/services/compression/strategySelector.ts": 1379,
|
||||
"open-sse/services/rateLimitManager.ts": 1517,
|
||||
"open-sse/translator/response/openai-responses.ts": 1652,
|
||||
"open-sse/utils/cursorAgentProtobuf.ts": 1956,
|
||||
"open-sse/utils/stream.ts": 3756,
|
||||
"src/app/(dashboard)/dashboard/HomePageClient.tsx": 1804,
|
||||
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1340,
|
||||
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 4052,
|
||||
"src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx": 1387,
|
||||
"src/app/(dashboard)/dashboard/combos/page.tsx": 6114,
|
||||
"src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": 1668,
|
||||
"src/app/(dashboard)/dashboard/costs/quota-share/components/PoolWizard.tsx": 1329,
|
||||
"src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 3400,
|
||||
"src/app/(dashboard)/dashboard/health/page.tsx": 1514,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1721,
|
||||
"src/app/(dashboard)/dashboard/providers/page.tsx": 2527,
|
||||
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1561,
|
||||
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1325,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1911,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 1460,
|
||||
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 2118,
|
||||
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 2045,
|
||||
"src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1336,
|
||||
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2792,
|
||||
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1455,
|
||||
"src/app/api/providers/[id]/models/route.ts": 3069,
|
||||
"src/app/api/v1/models/catalog.ts": 2076,
|
||||
"src/lib/db/apiKeys.ts": 1988,
|
||||
"src/lib/db/core.ts": 2131,
|
||||
"src/lib/db/migrationRunner.ts": 1431,
|
||||
"src/lib/db/models.ts": 1426,
|
||||
"src/lib/db/providers.ts": 1344,
|
||||
"src/lib/memory/retrieval.ts": 1395,
|
||||
"src/lib/tailscaleTunnel.ts": 1563,
|
||||
"src/lib/usage/providerLimits.ts": 1317,
|
||||
"src/shared/components/OAuthModal.tsx": 1474,
|
||||
"src/shared/components/RequestLoggerV2.tsx": 2118,
|
||||
"src/shared/components/analytics/charts.tsx": 1346,
|
||||
"src/shared/services/cliRuntime.ts": 1459,
|
||||
"src/sse/handlers/chat.ts": 2493,
|
||||
"src/sse/services/auth.ts": 3260,
|
||||
"tests/unit/account-fallback-service.test.ts": 2044,
|
||||
"tests/unit/provider-validation-specialty.test.ts": 3880,
|
||||
"open-sse/executors/hyperagent.ts": 1334,
|
||||
"src/lib/tokenHealthCheck.ts": 1369,
|
||||
"open-sse/executors/default.ts": 1355,
|
||||
"open-sse/executors/kiro.ts": 1390,
|
||||
"open-sse/translator/request/openai-to-kiro.ts": 1374,
|
||||
"open-sse/utils/sseHeartbeat.ts": 194,
|
||||
"open-sse/utils/proxyFetch.ts": 1207
|
||||
"open-sse/executors/antigravity.ts": 1528,
|
||||
"open-sse/executors/base.ts": 1640,
|
||||
"open-sse/executors/chatgpt-web.ts": 3241,
|
||||
"open-sse/executors/codex.ts": 1562,
|
||||
"open-sse/executors/cursor.ts": 1563,
|
||||
"open-sse/executors/deepseek-web.ts": 1148,
|
||||
"open-sse/executors/grok-web.ts": 1044,
|
||||
"open-sse/executors/muse-spark-web.ts": 1405,
|
||||
"open-sse/handlers/chatCore.ts": 5061,
|
||||
"open-sse/handlers/imageGeneration.ts": 3101,
|
||||
"open-sse/handlers/responseSanitizer.ts": 1128,
|
||||
"open-sse/handlers/search.ts": 1536,
|
||||
"open-sse/handlers/videoGeneration.ts": 1063,
|
||||
"open-sse/mcp-server/schemas/tools.ts": 1553,
|
||||
"open-sse/mcp-server/server.ts": 1448,
|
||||
"open-sse/mcp-server/tools/advancedTools.ts": 1120,
|
||||
"open-sse/services/accountFallback.ts": 1978,
|
||||
"open-sse/services/adobeFireflyBrowserLogin.ts": 1362,
|
||||
"open-sse/services/adobeFireflyChromeRuntime.ts": 1201,
|
||||
"open-sse/services/adobeFireflyClient.ts": 2999,
|
||||
"open-sse/services/adobeFireflySession.ts": 1003,
|
||||
"open-sse/services/claudeCodeCompatible.ts": 1202,
|
||||
"open-sse/services/combo.ts": 3648,
|
||||
"open-sse/services/compression/strategySelector.ts": 1061,
|
||||
"open-sse/services/rateLimitManager.ts": 1167,
|
||||
"open-sse/translator/response/openai-responses.ts": 1271,
|
||||
"open-sse/utils/cursorAgentProtobuf.ts": 1505,
|
||||
"open-sse/utils/stream.ts": 2889,
|
||||
"src/app/(dashboard)/dashboard/HomePageClient.tsx": 1388,
|
||||
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1031,
|
||||
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 3117,
|
||||
"src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx": 1067,
|
||||
"src/app/(dashboard)/dashboard/combos/page.tsx": 4703,
|
||||
"src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": 1283,
|
||||
"src/app/(dashboard)/dashboard/costs/quota-share/components/PoolWizard.tsx": 1022,
|
||||
"src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2615,
|
||||
"src/app/(dashboard)/dashboard/health/page.tsx": 1165,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1324,
|
||||
"src/app/(dashboard)/dashboard/providers/page.tsx": 1944,
|
||||
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1201,
|
||||
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1019,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1470,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 1123,
|
||||
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1629,
|
||||
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1573,
|
||||
"src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1028,
|
||||
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148,
|
||||
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1119,
|
||||
"src/app/api/providers/[id]/models/route.ts": 2361,
|
||||
"src/app/api/v1/models/catalog.ts": 1597,
|
||||
"src/lib/db/apiKeys.ts": 1529,
|
||||
"src/lib/db/core.ts": 1639,
|
||||
"src/lib/db/migrationRunner.ts": 1101,
|
||||
"src/lib/db/models.ts": 1097,
|
||||
"src/lib/db/providers.ts": 1034,
|
||||
"src/lib/memory/retrieval.ts": 1073,
|
||||
"src/lib/tailscaleTunnel.ts": 1202,
|
||||
"src/lib/usage/providerLimits.ts": 1013,
|
||||
"src/shared/components/OAuthModal.tsx": 1134,
|
||||
"src/shared/components/RequestLoggerV2.tsx": 1629,
|
||||
"src/shared/components/analytics/charts.tsx": 1035,
|
||||
"src/shared/services/cliRuntime.ts": 1122,
|
||||
"src/sse/handlers/chat.ts": 1918,
|
||||
"src/sse/services/auth.ts": 2508,
|
||||
"tests/unit/account-fallback-service.test.ts": 1572,
|
||||
"tests/unit/provider-validation-specialty.test.ts": 2985,
|
||||
"open-sse/executors/hyperagent.ts": 1026,
|
||||
"src/lib/tokenHealthCheck.ts": 1053,
|
||||
"open-sse/executors/default.ts": 1042,
|
||||
"open-sse/executors/kiro.ts": 1069,
|
||||
"open-sse/translator/request/openai-to-kiro.ts": 1057,
|
||||
"open-sse/utils/sseHeartbeat.ts": 149
|
||||
},
|
||||
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
|
||||
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
|
||||
"_rebaseline_2026_07_27_v3849_train3": "Merge-train 3 (13 PRs) — owner-approved 2026-07-27. Both entries are genuine irreducible growth at existing chokepoints, not new branches: src/lib/db/apiKeys.ts 1518->1529 (#8805 cx/* ≡ codex/* API-key model permissions); open-sse/handlers/chatCore.ts 5006->5020 (#8806 real response payload into plugin onResponse hooks). Covered by tests/unit/db-apiKeys-crud.test.ts (4 new cases) and the two plugin-hook test files updated in #8806 respectively.",
|
||||
"_rebaseline_2026_07_28_8842_antigravity_projectid_refresh": "PR #8842 (fix/antigravity-projectid-refresh) own growth: open-sse/executors/antigravity.ts 1493->1528 (+35 = projectId discovery in refreshCredentials: import ensureAntigravityProjectAssigned + trim projectId + call ensureAntigravityProjectAssigned with 8s timeout + persistDiscoveredAntigravityProjectId + log success/failure). Irreducible wiring at the existing credential-refresh chokepoint. Covered by tests/unit/executor-antigravity.test.ts (4 new test cases).",
|
||||
@@ -592,10 +574,5 @@
|
||||
"_rebaseline_2026_08_03_9255_adobe_firefly_durable_sessions": "PR #9255 own cohesive growth: open-sse/services/adobeFireflyClient.ts 2322->2894 adds authenticated-vs-guest IMS classification, browser-risk ARP validation/rebuild, bounded 408 retry/recovery, sticky accepted-session handling, and matching image/video submit recovery at the existing Adobe upstream client chokepoints. This client was already explicitly frozen as a single self-contained upstream integration by #8006/#8510; splitting only the retry/auth helpers now would scatter one request state machine while structural shrink remains tracked in #3501. tests/unit/adobe-firefly.test.ts 871->1136 adds direct regression coverage for guest-token rejection, cookie/ARP rebuilding, 408 retries, sticky accepted ARP reuse, forced auth recovery, and cookie-to-IMS exchange. The obsolete 1179-line managed-Chrome fallback module was deleted rather than rebaselined after the packaged-safe pure-CDP path became authoritative. Focused Adobe suite: 61/61.",
|
||||
"_rebaseline_2026_08_07_9653_disconnect_grace_period": "Extracted fix(sse): grace period before finalizing a client disconnect as 499 (#9653) — a client that closes its connection right after reading a fully-completed SSE stream can race OmniRoute's own completion bookkeeping, getting persisted as a false 499/0-tokens even though it delivered the full response (live-confirmed: a real disconnect at 18236ms was corrected to 200/82814+1292 tokens). Own growth: open-sse/handlers/chatCore.ts 5030->5039 (+9, wiring createClientDisconnectGraceHandler at the existing onClientDisconnectFinalize call site) — irreducible call-site wiring, the actual grace-period logic lives in the new leaf createClientDisconnectGraceHandler (open-sse/utils/streamFailureFinalization.ts, not frozen). Re-measured to 5042 after rebasing onto a newer release/v3.8.50 tip: the file carries an unrelated +3 base drift from already-merged upstream commits between this PR's original branch point and the rebase target, not covered by this entry. Covered by tests/unit/stream-disconnect-grace-period-9653.test.ts (4/4, fake-timer driven). Other file-size gate violations present on this base tip are pre-existing/unrelated to this change (base-red #9679, re-verify current issue number at merge time).",
|
||||
"_rebaseline_2026_08_04_9268_gemini_schema_empty_choices": "Feature #9268 own growth: open-sse/utils/stream.ts 2889->2915 (+26 = irreducible call-site wiring for the empty-choices interceptor). The translate-mode flush now rejects a stream that completed without forwarding any valuable chunk (all-empty `choices: []`, no content/tool_calls/finish_reason) as a retryable 502 \"empty content\" instead of a clean empty 200 — the missing streaming counterpart of chatCore.ts's non-streaming isEmptyContentResponse. All rejection logic lives in the NEW leaf module open-sse/utils/streamEmptyChoices.ts (<cap, not frozen, unit-tested via tests/unit/stream-empty-choices-interceptor.test.ts); stream.ts only carries the `forwardedValuableChunk` boolean (declared at createSSEStream scope, set in emitTranslatedClientItem where the sole hasValuableContent check passes) plus the one flush-time rejectEmptyChoicesStream() call — the wait/orchestration at the chokepoint, not a movable block (mirrors the comboCooldownRetry.ts precedent). Schema-side twin fix: recursive type:\"object\" injection in open-sse/translator/helpers/geminiHelper.ts (not frozen, +33) for nested schemas with properties but no type (Gemini 400).",
|
||||
"_rebaseline_2026_08_09_5696_capability_filter": "PR #9424 own growth: open-sse/handlers/chatCore.ts 5050->5061 (+11). The Layer A capability gate is irreducible wiring at the existing pre-dispatch chokepoint: feature-flag check, capability derivation, compatibility decision, sanitized 400 response, pending-request cleanup, and warning telemetry. All matching and message logic lives outside the god-file in src/shared/constants/capabilities/capabilityFilter.ts; only orchestration remains here. Covered by tests/unit/capability-filter.test.ts (20 cases, including flag-off and sanitized error behavior). Structural shrink remains tracked separately.",
|
||||
"_rebaseline_2026_07_30_9006_vertex_claude_catalog_dispatch": "PR #9006 (fix/vertex-claude-catalog-dispatch): three files, two causes. (1) src/sse/handlers/chat.ts 1845->1846 (+1): NOT this PR's own growth — this PR never touches chat.ts at all. Measured 1846 (split(\"\\n\").length) at this PR's own merge-base (before any of its 11 commits), so the drift was already inherited from already-merged PRs on release/v3.8.50 (fast-gates PR->release do not run check:file-size, same root cause as _rebaseline_2026_07_25_v3849_basered_filesize and _rebaseline_2026_07_02_5798_release_green) — no offending branch left to fix. (2) src/sse/services/auth.ts 2508->2512 (+4 net, after extraction — see below) and open-sse/handlers/chatCore.ts 5020->5023 (+3, comment-only): genuine own growth. auth.ts adds Vertex 403 PERMISSION_DENIED disambiguation (Google's google.rpc.ErrorInfo proto distinguishes a connection-wide cause — SERVICE_DISABLED, or IAM_PERMISSION_DENIED against a project-level resource — from a model-specific one scoped to a .../models/<id> resource), added mid-PR after a quality-gate reviewer flagged the plan's originally-accepted \"Vertex 403 always -> per-model lockout\" trade-off. The actual classification logic (~40 lines) was EXTRACTED into a new leaf module src/sse/services/vertexErrorClassifier.ts (mirrors the googApiKeyAuth.ts precedent, _rebaseline_2026_07_14_7034_goog_api_key), leaving only the irreducible call-site wiring in the frozen file: a 1-line import plus widening the existing #3027 per-model-403 guard condition. chatCore.ts's +3 is a pure comment expansion (no functional change) clarifying that the adjacent effort-suffix strip is no longer unconditional for every provider, requested by a separate quality-gate code-reviewer finding; not extractable (it's a comment). Auth.ts's disambiguation logic covered by 3 new test cases in tests/unit/vertex-passthrough-model-lockout.test.ts (SERVICE_DISABLED, IAM_PERMISSION_DENIED+model-resource, IAM_PERMISSION_DENIED+project-resource) plus a 4th regression test for a multi-detail-body correlation bug (reason and resource must be read from the SAME ErrorInfo detail, not independently regexed across the whole body) found by an adversarial quality-gate pass and fixed before merge.",
|
||||
"_rebaseline_2026_08_04_9006_reconcile_onto_tip": "PR #9006 (fix/vertex-claude-catalog-dispatch) rebase-onto-tip reconciliation, 5 days after the PR's own _rebaseline_2026_07_30_9006 entry below. Two further inherited drifts, neither this PR's own growth (its own diff still touches neither open-sse/executors/base.ts nor src/sse/handlers/chat.ts): (1) src/sse/handlers/chat.ts 1846->1847 (+1), same root cause as the original entry (fast-gates PR->release does not run check:file-size) — another already-merged PR added one more line since. (2) open-sse/executors/base.ts 1578->1623 (+45): commit 7163081f5 fix(agentrouter): retry on 400 content-blocked + burst guard (#9323), merged directly to release/v3.8.50 between this PR's last sync and now, grew base.ts without updating its baseline entry. No offending branch left to fix in either case; verified via git diff against upstream/release/v3.8.50 that this PR's own commits do not touch either file.",
|
||||
"_rebaseline_2026_08_08_9006_own_comment_growth": "PR #9006's own follow-up commit (a32aed738): tests/unit/combo-routing-engine.test.ts 3457->3464 (+7) is this PR's own growth — explanatory comment blocks added alongside the ALL_ACCOUNTS_INACTIVE->ALL_TARGETS_SKIPPED stale-assertion fix (matching the identical fix applied to #9619/#9173/#8909 the same day; upstream's own test was never updated when the recordedAttempts===0 pre-dispatch-skip branch shipped). Caught by CI's PR-mode check:file-size (--base-ref) after the fix commit; missed locally because check-file-size.mjs was not re-run after that specific edit.",
|
||||
"_rebaseline_2026_08_07_9006_reconcile_onto_tip_3": "PR #9006 (fix/vertex-claude-catalog-dispatch) third rebase-onto-tip reconciliation (2026-08-07), shared root cause with PRs #9619 and #9173's same-day reconciliations: open-sse/mcp-server/schemas/tools.ts 1505->1553, open-sse/mcp-server/server.ts 1411->1444, open-sse/services/accountFallback.ts 1972->1978, src/app/(dashboard)/dashboard/combos/page.tsx 4647->4703, EditConnectionModal.tsx 1316->1324, src/app/api/providers/[id]/models/route.ts 2250->2304, src/app/api/v1/models/catalog.ts 1549->1556, src/lib/db/core.ts 1637->1639, src/lib/tokenHealthCheck.ts 1021->1053, tests/unit/translator-openai-to-gemini.test.ts 1619->1622 — none touched by this PR's own vertex-claude-catalog-dispatch diff (verified: this PR's commits do not touch any of these files). Same root cause as every other entry in this chain: fast-gates PR->release does not run check:file-size. No offending branch left to fix.",
|
||||
"_rebaseline_2026_08_06_9006_reconcile_onto_tip_2": "PR #9006 (fix/vertex-claude-catalog-dispatch) second rebase-onto-tip reconciliation. Same two files as _rebaseline_2026_08_04_9006_reconcile_onto_tip below, further inherited drift, still not this PR's own growth (verified via git diff against the fresh upstream/release/v3.8.50 merge-base — this PR's own commits still touch neither file): open-sse/executors/base.ts 1623->1640 (+17) and src/sse/handlers/chat.ts 1847->1881 (+34), both measured post-merge via split(\"\\n\").length. More already-merged release/v3.8.50 PRs grew these files without updating their baseline entries (same root cause as every other entry in this chain: fast-gates PR->release does not run check:file-size). No offending branch left to fix."
|
||||
"_rebaseline_2026_08_09_5696_capability_filter": "PR #9424 own growth: open-sse/handlers/chatCore.ts 5050->5061 (+11). The Layer A capability gate is irreducible wiring at the existing pre-dispatch chokepoint: feature-flag check, capability derivation, compatibility decision, sanitized 400 response, pending-request cleanup, and warning telemetry. All matching and message logic lives outside the god-file in src/shared/constants/capabilities/capabilityFilter.ts; only orchestration remains here. Covered by tests/unit/capability-filter.test.ts (20 cases, including flag-off and sanitized error behavior). Structural shrink remains tracked separately."
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"entries": []
|
||||
}
|
||||
@@ -149,11 +149,10 @@
|
||||
"dedicatedGate": true
|
||||
},
|
||||
"codeqlAlerts": {
|
||||
"value": 2,
|
||||
"value": 1,
|
||||
"direction": "down",
|
||||
"dedicatedGate": true,
|
||||
"_rebaseline_2026_08_06_base_grew": "Base branch file-size drift: translator-openai-to-gemini.test.ts grew 1619->1622 (test assertions for Gemini translator compatibility). CodeQL alert (js/insufficient-password-hash in raycast.ts) is pre-existing base-red; incremented baseline to match.",
|
||||
"_rebaseline_2026_08_10_9940_fingerprint": "CodeQL base-red (green-prs sweep, issue #9985): 2nd js/insufficient-password-hash alert at src/shared/middleware/chatBodyAdmission.ts:265,269 introduced by #9940 (per-connection virtual admission lanes). Both are API-key/bearer FINGERPRINTS (createHash('sha256') truncated to 16-hex admission-lane key), not password VERIFICATION — false-positive class for this rule. Reproduces on release/v3.8.50 tip. Owner-authorized rebaseline 1->2; revisit at v3.9.0."
|
||||
"_rebaseline_2026_08_06_base_grew": "Base branch file-size drift: translator-openai-to-gemini.test.ts grew 1619->1622 (test assertions for Gemini translator compatibility). CodeQL alert (js/insufficient-password-hash in raycast.ts) is pre-existing base-red; incremented baseline to match."
|
||||
},
|
||||
"secretFindings": {
|
||||
"_note": "Zeroed 2026-07-13 (WS6/D3): the 3 frozen generic-api-key FPs are allowlisted with justification in .gitleaks.toml — any NEW finding regresses the ratchet.",
|
||||
|
||||
@@ -46,8 +46,6 @@ services:
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
chatgpt-web-codex-browser:
|
||||
condition: service_started
|
||||
build:
|
||||
context: .
|
||||
target: runner-cli
|
||||
@@ -69,7 +67,6 @@ services:
|
||||
- HOSTNAME=0.0.0.0
|
||||
- DATA_DIR=/app/data
|
||||
- OMNIROUTE_BASE_PATH=${OMNIROUTE_BASE_PATH:-}
|
||||
- CHATGPT_WEB_CODEX_CDP_URL=http://chatgpt-web-codex-browser:9223
|
||||
ports:
|
||||
- "${PROD_DASHBOARD_PORT:-20130}:${DASHBOARD_PORT:-${PORT:-20128}}"
|
||||
- "${PROD_API_PORT:-20131}:${API_PORT:-20129}"
|
||||
@@ -83,19 +80,7 @@ services:
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
|
||||
chatgpt-web-codex-browser:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/chatgpt-web-codex-browser/Dockerfile
|
||||
image: omniroute:chatgpt-web-codex-browser
|
||||
restart: unless-stopped
|
||||
shm_size: "2gb"
|
||||
volumes:
|
||||
- chatgpt-web-codex-browser-prod-data:/browser-profile
|
||||
|
||||
volumes:
|
||||
chatgpt-web-codex-browser-prod-data:
|
||||
name: omniroute-chatgpt-web-codex-browser-prod-data
|
||||
omniroute-prod-data:
|
||||
name: omniroute-prod-data
|
||||
redis-prod-data:
|
||||
|
||||
@@ -105,21 +105,6 @@ services:
|
||||
args:
|
||||
OMNIROUTE_BASE_PATH: ${OMNIROUTE_BASE_PATH:-}
|
||||
image: omniroute:web
|
||||
depends_on:
|
||||
chatgpt-web-codex-browser:
|
||||
condition: service_started
|
||||
environment:
|
||||
- DATA_DIR=/app/data
|
||||
- PORT=${PORT:-20128}
|
||||
- DASHBOARD_PORT=${DASHBOARD_PORT:-20128}
|
||||
- API_PORT=${API_PORT:-20129}
|
||||
- API_HOST=${API_HOST:-0.0.0.0}
|
||||
- LIVE_WS_PORT=${LIVE_WS_PORT:-20132}
|
||||
- LIVE_WS_HOST=${LIVE_WS_HOST:-0.0.0.0}
|
||||
- LIVE_WS_ALLOWED_ORIGINS=${LIVE_WS_ALLOWED_ORIGINS:-http://localhost:20128,http://127.0.0.1:20128}
|
||||
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
|
||||
- OMNIROUTE_BASE_PATH=${OMNIROUTE_BASE_PATH:-}
|
||||
- CHATGPT_WEB_CODEX_CDP_URL=http://chatgpt-web-codex-browser:9223
|
||||
ports:
|
||||
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
||||
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
||||
@@ -127,20 +112,6 @@ services:
|
||||
profiles:
|
||||
- web
|
||||
|
||||
# Internal-only Chromium runtime for ChatGPT Web (Codex). No CDP or browser
|
||||
# UI port is published to the host.
|
||||
chatgpt-web-codex-browser:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/chatgpt-web-codex-browser/Dockerfile
|
||||
image: omniroute:chatgpt-web-codex-browser
|
||||
restart: unless-stopped
|
||||
shm_size: "2gb"
|
||||
volumes:
|
||||
- chatgpt-web-codex-browser-data:/browser-profile
|
||||
profiles:
|
||||
- web
|
||||
|
||||
# ── Profile: cli (CLIs installed inside container) ─────────────────
|
||||
omniroute-cli:
|
||||
<<: *common
|
||||
@@ -288,8 +259,6 @@ services:
|
||||
- cliproxyapi
|
||||
|
||||
volumes:
|
||||
chatgpt-web-codex-browser-data:
|
||||
name: omniroute-chatgpt-web-codex-browser-data
|
||||
cliproxyapi-data:
|
||||
name: cliproxyapi-data
|
||||
redis-data:
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
FROM mcr.microsoft.com/playwright:v1.62.0-noble
|
||||
|
||||
USER root
|
||||
RUN mkdir -p /browser-profile && chown -R pwuser:pwuser /browser-profile
|
||||
COPY --chown=pwuser:pwuser docker/chatgpt-web-codex-browser/cdp-proxy.mjs /opt/cdp-proxy.mjs
|
||||
USER pwuser
|
||||
|
||||
EXPOSE 9223
|
||||
|
||||
CMD ["/bin/sh", "-lc", "node /opt/cdp-proxy.mjs & exec $(find /ms-playwright -path '*/chrome-linux/chrome' -type f | head -n 1) --headless=new --no-sandbox --disable-dev-shm-usage --remote-debugging-port=9222 --user-data-dir=/browser-profile about:blank"]
|
||||
@@ -1,72 +0,0 @@
|
||||
import http from "node:http";
|
||||
import net from "node:net";
|
||||
|
||||
const listenPort = 9223;
|
||||
const upstreamHost = "127.0.0.1";
|
||||
const upstreamPort = 9222;
|
||||
|
||||
function proxyHeaders(headers) {
|
||||
const next = { ...headers, host: `${upstreamHost}:${upstreamPort}` };
|
||||
delete next.connection;
|
||||
delete next.upgrade;
|
||||
return next;
|
||||
}
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
const upstream = http.request(
|
||||
{
|
||||
host: upstreamHost,
|
||||
port: upstreamPort,
|
||||
method: request.method,
|
||||
path: request.url,
|
||||
headers: proxyHeaders(request.headers),
|
||||
},
|
||||
(upstreamResponse) => {
|
||||
const chunks = [];
|
||||
upstreamResponse.on("data", (chunk) => chunks.push(chunk));
|
||||
upstreamResponse.on("end", () => {
|
||||
let body = Buffer.concat(chunks);
|
||||
const contentType = String(upstreamResponse.headers["content-type"] || "");
|
||||
if (contentType.includes("application/json")) {
|
||||
body = Buffer.from(
|
||||
body
|
||||
.toString("utf8")
|
||||
.replaceAll(`ws://${upstreamHost}:${upstreamPort}`, `ws://${request.headers.host}`)
|
||||
);
|
||||
}
|
||||
const headers = { ...upstreamResponse.headers, "content-length": String(body.length) };
|
||||
response.writeHead(upstreamResponse.statusCode || 502, headers);
|
||||
response.end(body);
|
||||
});
|
||||
}
|
||||
);
|
||||
upstream.on("error", () => {
|
||||
response.writeHead(503, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify({ error: "CDP browser is starting" }));
|
||||
});
|
||||
request.pipe(upstream);
|
||||
});
|
||||
|
||||
server.on("upgrade", (request, socket, head) => {
|
||||
const upstream = net.connect(upstreamPort, upstreamHost, () => {
|
||||
const upgradeHeaders = {
|
||||
...request.headers,
|
||||
host: `${upstreamHost}:${upstreamPort}`,
|
||||
connection: "Upgrade",
|
||||
upgrade: "websocket",
|
||||
};
|
||||
const headers = Object.entries(upgradeHeaders)
|
||||
.flatMap(([name, value]) =>
|
||||
Array.isArray(value) ? value.map((item) => `${name}: ${item}`) : [`${name}: ${value}`]
|
||||
)
|
||||
.join("\r\n");
|
||||
upstream.write(
|
||||
`${request.method} ${request.url} HTTP/${request.httpVersion}\r\n${headers}\r\n\r\n`
|
||||
);
|
||||
if (head.length > 0) upstream.write(head);
|
||||
socket.pipe(upstream).pipe(socket);
|
||||
});
|
||||
upstream.on("error", () => socket.destroy());
|
||||
});
|
||||
|
||||
server.listen(listenPort, "0.0.0.0");
|
||||
@@ -8,7 +8,7 @@ lastUpdated: 2026-06-28
|
||||
|
||||
Navigable index of the OmniRoute documentation set. Topics are grouped by intent so you can find what you need quickly.
|
||||
|
||||
> Looking for the project overview, install steps, or release notes? See the root [README.md](../README.md), [ROADMAP.md](../ROADMAP.md), [CHANGELOG.md](../CHANGELOG.md), and [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
> Looking for the project overview, install steps, or release notes? See the root [README.md](../README.md), [CHANGELOG.md](../CHANGELOG.md), and [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -22,7 +22,7 @@ Simple guides for using OmniRoute — no technical background needed.
|
||||
- [AUTO-COMBO-GUIDE.md](getting-started/AUTO-COMBO-GUIDE.md) — let OmniRoute pick the best AI for you.
|
||||
- [PROVIDERS-GUIDE.md](getting-started/PROVIDERS-GUIDE.md) — how to connect AI providers.
|
||||
- [FREE-TIERS-GUIDE.md](getting-started/FREE-TIERS-GUIDE.md) — get free AI with no credit card.
|
||||
- [WEB-COOKIE-GUIDE.md](getting-started/WEB-COOKIE-GUIDE.md) — web cookie providers (session-credential setup).
|
||||
- [TROUBLESHOOTING.md](getting-started/TROUBLESHOOTING.md) — fix common issues.
|
||||
|
||||
### guides/
|
||||
|
||||
@@ -42,8 +42,6 @@ Simple guides for using OmniRoute — no technical background needed.
|
||||
- [CLAUDE-CODE-CONFIGURATION.md](guides/CLAUDE-CODE-CONFIGURATION.md) — Claude Code CLI with OmniRoute.
|
||||
- [CODEX-CLI-CONFIGURATION.md](guides/CODEX-CLI-CONFIGURATION.md) — Codex CLI with OmniRoute.
|
||||
- [KIRO_SETUP.md](guides/KIRO_SETUP.md) — Kiro setup.
|
||||
- [ANTIGRAVITY-ONBOARDING.md](guides/ANTIGRAVITY-ONBOARDING.md) — Antigravity (Google One AI) onboarding.
|
||||
- [MANAGEMENT-AUTH.md](guides/MANAGEMENT-AUTH.md) — management authentication.
|
||||
- [I18N.md](guides/I18N.md) — translation and locale workflow.
|
||||
- [TROUBLESHOOTING.md](guides/TROUBLESHOOTING.md) — detailed troubleshooting reference.
|
||||
- [UNINSTALL.md](guides/UNINSTALL.md) — clean removal steps.
|
||||
@@ -66,10 +64,6 @@ How the system is put together — read these to understand the runtime, code la
|
||||
- [QUALITY_GATES.md](architecture/QUALITY_GATES.md) — quality-gate scripts and CI jobs inventory.
|
||||
- [MONITORING_SECTIONS.md](architecture/MONITORING_SECTIONS.md) — monitoring/costs dashboard navigation.
|
||||
- [cluster-decisions.md](architecture/cluster-decisions.md) — optional sidecar/cluster profile decisions.
|
||||
- [DESIGN_SYSTEM.md](architecture/DESIGN_SYSTEM.md) — design system & visual identity.
|
||||
- [ROUTER_BACKENDS.md](architecture/ROUTER_BACKENDS.md) — router backends & embedded services architecture contract (ADR).
|
||||
- [admission-lanes.md](architecture/admission-lanes.md) — the two admission-lane systems and what gates each.
|
||||
- [persistence-backend-boundary.md](architecture/persistence-backend-boundary.md) — pluggable persistence boundary (ADR).
|
||||
|
||||
## reference/
|
||||
|
||||
@@ -83,9 +77,6 @@ Lookup material — API surface, environment variables, CLI flags, provider cata
|
||||
- [FEATURE_FLAGS.md](reference/FEATURE_FLAGS.md) — feature flags and their defaults.
|
||||
- [CLI-TOOLS.md](reference/CLI-TOOLS.md) — bundled CLI commands.
|
||||
- [FREE_TIERS.md](reference/FREE_TIERS.md) — free-tier LLM provider directory.
|
||||
- [FREE_PROXIES_API.md](reference/FREE_PROXIES_API.md) — free proxies API.
|
||||
- [RELAY_BACKEND_STRATEGY.md](reference/RELAY_BACKEND_STRATEGY.md) — relay backend strategy.
|
||||
- [RELAY_TROUBLESHOOTING.md](reference/RELAY_TROUBLESHOOTING.md) — relay troubleshooting.
|
||||
|
||||
## frameworks/
|
||||
|
||||
@@ -106,7 +97,6 @@ Pluggable subsystems exposed to clients, agents, and operators.
|
||||
- [EMBEDDED-SERVICES.md](frameworks/EMBEDDED-SERVICES.md) — embedded sidecar services (9Router, CLIProxyAPI).
|
||||
- [NOTION_CONTEXT.md](frameworks/NOTION_CONTEXT.md) — Notion context source.
|
||||
- [OBSIDIAN_CONTEXT.md](frameworks/OBSIDIAN_CONTEXT.md) — Obsidian context source.
|
||||
- [LOCAL_CORPUS_CONTEXT.md](frameworks/LOCAL_CORPUS_CONTEXT.md) — local corpus context source (approved directory exposed to MCP).
|
||||
- [OPENCODE.md](frameworks/OPENCODE.md) — OpenCode integration.
|
||||
- [OPEN_SSE_ARCHITECTURE.md](frameworks/OPEN_SSE_ARCHITECTURE.md) — open-sse streaming engine internals.
|
||||
- [PLAYGROUND_STUDIO.md](frameworks/PLAYGROUND_STUDIO.md) — Playground Studio UI.
|
||||
@@ -124,7 +114,6 @@ Combo routing, scoring, and replay.
|
||||
- [AUTO-COMBO.md](routing/AUTO-COMBO.md) — Auto-Combo (multi-factor scoring, 17 strategies).
|
||||
- [QUOTA_SHARE.md](routing/QUOTA_SHARE.md) — quota sharing engine.
|
||||
- [REASONING_REPLAY.md](routing/REASONING_REPLAY.md) — reasoning replay cache.
|
||||
- [REASONING_ROUTING.md](routing/REASONING_ROUTING.md) — reasoning routing rules (effort/budget rule engine).
|
||||
|
||||
## security/
|
||||
|
||||
@@ -138,9 +127,6 @@ Guardrails, compliance, stealth, and the mandatory patterns for handling public
|
||||
- [ROUTE_GUARD_TIERS.md](security/ROUTE_GUARD_TIERS.md) — route-guard classification tiers.
|
||||
- [CLI_TOKEN.md](security/CLI_TOKEN.md) — CLI machine-ID token (HMAC + legacy SHA-256) auth.
|
||||
- [EGRESS_POLICY.md](security/EGRESS_POLICY.md) — egress IP family (IPv4/IPv6) policy.
|
||||
- [BAN_DETECTION.md](security/BAN_DETECTION.md) — account-ban / banned-keyword detection.
|
||||
- [AGENTROUTER_WAF.md](security/AGENTROUTER_WAF.md) — agentrouter.org WAF.
|
||||
- [CORS.md](security/CORS.md) — CORS configuration & security.
|
||||
- [MITM-TPROXY-DECRYPT.md](security/MITM-TPROXY-DECRYPT.md) — transparent MITM decrypt.
|
||||
- [SUPPLY_CHAIN.md](security/SUPPLY_CHAIN.md) — supply-chain gates (SLSA, SBOM, Trivy, osv-scanner, Scorecard).
|
||||
- [SOCKET_DEV_FINDINGS.md](security/SOCKET_DEV_FINDINGS.md) — supply-chain finding attestations.
|
||||
@@ -162,11 +148,8 @@ Prompt compression engines, rules, and language packs.
|
||||
Provider-specific integration guides.
|
||||
|
||||
- [CLAUDE_WEB.md](providers/CLAUDE_WEB.md) — Claude Web (cookie-auth) provider.
|
||||
- [CHATGPT_WEB.md](providers/CHATGPT_WEB.md) — ChatGPT Web (Plus/Pro + Codex) providers.
|
||||
- [ALIBABA-QWEN-PROVIDER-FAMILIES.md](providers/ALIBABA-QWEN-PROVIDER-FAMILIES.md) — Alibaba and Qwen provider families.
|
||||
- [AGENTROUTER.md](providers/AGENTROUTER.md) — AgentRouter setup.
|
||||
- [ZED-DOCKER.md](providers/ZED-DOCKER.md) — Zed IDE integration under Docker.
|
||||
- [CURSOR-DOCKER.md](providers/CURSOR-DOCKER.md) — Cursor model listing under Docker.
|
||||
|
||||
## comparison/
|
||||
|
||||
@@ -178,17 +161,11 @@ Release, deployment, proxies, tunnels, coverage, database, monitoring.
|
||||
|
||||
- [RELEASE_CHECKLIST.md](ops/RELEASE_CHECKLIST.md) — release flow checklist.
|
||||
- [RELEASE_GREEN.md](ops/RELEASE_GREEN.md) — keeping the PR queue and release branch green.
|
||||
- [BRANCHING_MODEL.md](ops/BRANCHING_MODEL.md) — branching & release model.
|
||||
- [MERGE_TRAIN.md](ops/MERGE_TRAIN.md) — merge queue & manual merge-train runbook.
|
||||
- [HOMOLOGATION.md](ops/HOMOLOGATION.md) — homologation suite (`npm run homolog`).
|
||||
- [QUALITY_GATE_PLAYBOOK.md](ops/QUALITY_GATE_PLAYBOOK.md) — quality-gate playbook.
|
||||
- [RUNNER_BOX.md](ops/RUNNER_BOX.md) — self-hosted runner box operations.
|
||||
- [BRANCH_PROTECTION_MAIN.md](ops/BRANCH_PROTECTION_MAIN.md) — `main` branch protection.
|
||||
- [CONTRIBUTION_GOLDEN_PATH.md](ops/CONTRIBUTION_GOLDEN_PATH.md) — contribution golden path (focused checks per change type).
|
||||
- [COVERAGE_PLAN.md](ops/COVERAGE_PLAN.md) — test coverage plan.
|
||||
- [DATABASE_GUIDE.md](ops/DATABASE_GUIDE.md) — DB schema and operations.
|
||||
- [SQLITE_RUNTIME.md](ops/SQLITE_RUNTIME.md) — SQLite driver resolution chain.
|
||||
- [REDIS_PRODUCTION_CONFIG.md](ops/REDIS_PRODUCTION_CONFIG.md) — Redis production configuration.
|
||||
- [MONITORING_GUIDE.md](ops/MONITORING_GUIDE.md) — monitoring & observability.
|
||||
- [FLY_IO_DEPLOYMENT_GUIDE.md](ops/FLY_IO_DEPLOYMENT_GUIDE.md) — Fly.io deployment.
|
||||
- [VM_DEPLOYMENT_GUIDE.md](ops/VM_DEPLOYMENT_GUIDE.md) — generic VM deployment.
|
||||
|
||||
@@ -7,7 +7,7 @@ lastUpdated: 2026-08-06
|
||||
# OmniRoute Roadmap
|
||||
|
||||
> Version-gated, not date-gated: each milestone ships when its quality gates pass.
|
||||
> Current line: **v3.8.x** (this branch). Last updated: 2026-08-06.
|
||||
> Current line: **v3.8.x** (this branch). Last updated: 2026-07-23.
|
||||
|
||||
OmniRoute is heading from a monolithic router to a **modular AI platform**: a lightweight
|
||||
core engine, a typed SDK, and everything else as installable modules and plugins. The path
|
||||
@@ -17,13 +17,13 @@ It provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic acr
|
||||
|
||||
Core capabilities:
|
||||
|
||||
- OpenAI-compatible API surface for CLI/tools (271 providers, 89 executors)
|
||||
- OpenAI-compatible API surface for CLI/tools (271 providers, 86 executors)
|
||||
- Request/response translation across provider formats
|
||||
- Model combo fallback (multi-model sequence)
|
||||
- Structured combo steps (`provider + model + connection`) with runtime ordering by `compositeTiers`
|
||||
- Account-level fallback (multi-account per provider)
|
||||
- Quota preflight and quota-aware P2C account selection in the main chat path
|
||||
- OAuth + API-key provider connection management (22 OAuth provider modules)
|
||||
- OAuth + API-key provider connection management (19 OAuth provider modules)
|
||||
- Embedding generation via `/v1/embeddings` (6 providers, 9 models)
|
||||
- Image generation via `/v1/images/generations` (10+ providers, 20+ models)
|
||||
- Audio transcription via `/v1/audio/transcriptions` (7 providers)
|
||||
@@ -66,7 +66,7 @@ Core capabilities:
|
||||
- Prompt injection guard middleware
|
||||
- Prompt compression pipeline with Caveman, RTK, stacked pipelines, compression combos, language packs, and analytics
|
||||
- ACP (Agent Communication Protocol) registry
|
||||
- Modular OAuth providers (22 individual modules under `src/lib/oauth/providers/`)
|
||||
- Modular OAuth providers (19 individual modules under `src/lib/oauth/providers/`)
|
||||
- Uninstall/full-uninstall scripts
|
||||
- OAuth environment repair action
|
||||
- WebSocket bridge for OpenAI-compatible WS clients (`/v1/ws`)
|
||||
@@ -321,10 +321,10 @@ Domain layer modules:
|
||||
- Eval runner: `src/lib/evals/evalRunner.ts`
|
||||
- Domain state persistence: `src/lib/db/domainState.ts` — SQLite CRUD for fallback chains, budgets, cost history, lockout state, circuit breakers
|
||||
|
||||
OAuth provider modules (22 individual files under `src/lib/oauth/providers/`):
|
||||
OAuth provider modules (16 individual files under `src/lib/oauth/providers/`):
|
||||
|
||||
- Registry index: `src/lib/oauth/providers/index.ts`
|
||||
- Individual providers: `agy.ts`, `antigravity.ts`, `claude.ts`, `cline.ts`, `codebuddy-cn.ts`, `codex.ts`, `cursor.ts`, `devin-desktop.ts`, `ghe-copilot.ts`, `github.ts`, `gitlab-duo.ts`, `grok-cli-oauth.ts`, `grok-cli.ts`, `kilocode.ts`, `kimi-coding.ts`, `kiro.ts`, `qoder.ts`, `raycast.ts`, `trae.ts`, `xai-oauth.ts`, `zed-hosted.ts`, `zed.ts`
|
||||
- Individual providers: `claude.ts`, `codex.ts`, `gemini.ts`, `antigravity.ts`, `agy.ts`, `qoder.ts`, `qwen.ts`, `kimi-coding.ts`, `github.ts`, `kiro.ts`, `cursor.ts`, `kilocode.ts`, `cline.ts`, `windsurf.ts`, `gitlab-duo.ts`, `trae.ts`
|
||||
- Thin wrapper: `src/lib/oauth/providers.ts` — re-exports from individual modules
|
||||
|
||||
## 5) Embedded Services (v3.8.4)
|
||||
@@ -927,7 +927,7 @@ Each provider has a specialized executor extending `BaseExecutor` (in `open-sse/
|
||||
| `PuterExecutor` | Puter | Browser-based provider integration |
|
||||
| `QoderExecutor` | Qoder AI | PAT and OAuth support, multi-model free tier |
|
||||
| `VertexExecutor` | Google Vertex AI | Service account auth, region-based endpoints |
|
||||
| `DevinDesktopExecutor` | Devin Desktop | Imported API key + Connect-protobuf chat streaming |
|
||||
| `WindsurfExecutor` | Windsurf (Codeium) | Codeium OAuth + session token refresh |
|
||||
|
||||
All other providers (including custom compatible nodes) use the `DefaultExecutor`.
|
||||
|
||||
@@ -980,9 +980,9 @@ All other providers (including custom compatible nodes) use the `DefaultExecutor
|
||||
| GLMT (preset) | claude | API Key | ✅ | ✅ | ❌ | ⚠️ Per request |
|
||||
| Kimi Coding | openai | OAuth / API Key | ✅ | ✅ | ✅ | ❌ |
|
||||
| KIE | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Devin Desktop | openai | Imported API key | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ Per request |
|
||||
| Windsurf | openai | OAuth (Codeium) | ✅ | ✅ | ✅ | ⚠️ Per request |
|
||||
| GitLab Duo | openai | OAuth (GitLab) | ✅ | ✅ | ✅ | ❌ |
|
||||
| Devin CLI | openai | Local CLI login | ✅ | ✅ | ❌ | ✅ Task API |
|
||||
| Devin CLI | openai | OAuth | ✅ | ✅ | ✅ | ✅ Task API |
|
||||
| Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ Rate limits |
|
||||
| Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ Task API |
|
||||
| AgentRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
|
||||
@@ -293,7 +293,7 @@ table groups the actual directories and notable top-level files.
|
||||
| `jobs/` | Background jobs (`autoUpdate.ts`, …) |
|
||||
| `memory/` | Persistent memory: `store.ts`, `cache.ts`, `retrieval.ts`, `summarization.ts`, `extraction.ts`, `injection.ts`, `qdrant.ts`, `settings.ts`, `verify.ts`, `schemas.ts`, `types.ts` |
|
||||
| `monitoring/` | `observability.ts` |
|
||||
| `oauth/` | OAuth/import provider modules (22): `agy`, `antigravity`, `claude`, `cline`, `codebuddy-cn`, `codex`, `cursor`, `devin-desktop`, `ghe-copilot`, `github`, `gitlab-duo`, `grok-cli-oauth`, `grok-cli`, `kilocode`, `kimi-coding`, `kiro`, `qoder`, `raycast`, `trae`, `xai-oauth`, `zed-hosted`, `zed`, plus `services/`, `utils/`, and `constants/oauth.ts` |
|
||||
| `oauth/` | OAuth providers (13): `antigravity`, `claude`, `cline`, `codex`, `cursor`, `gemini`, `github`, `gitlab-duo`, `kilocode`, `kimi-coding`, `kiro`, `qoder`, `windsurf` plus `services/`, `utils/{pkce, server, banner, codexAuthFile, ui}`, `constants/oauth.ts` |
|
||||
| `plugins/` | Plugin loader (`index.ts`) |
|
||||
| `promptCache/` | `prefixAnalyzer.ts`, `index.ts` |
|
||||
| `providerModels/` | Managed model lifecycle: `modelDiscovery.ts`, `managedModelImport.ts`, `managedAvailableModels.ts`, `cursorAgent.ts` |
|
||||
@@ -452,7 +452,7 @@ open-sse/
|
||||
├── types.d.ts
|
||||
├── config/ Provider registries, header profiles, identity, …
|
||||
├── handlers/ Request handlers (chat, embeddings, audio, image, …)
|
||||
├── executors/ 89 provider-specific HTTP executors
|
||||
├── executors/ 84 provider-specific HTTP executors
|
||||
├── translator/ Format conversion (OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro)
|
||||
├── transformer/ Responses API ↔ Chat Completions stream transformer
|
||||
├── services/ 80+ service modules (combos, fallback, quotas, identity, …)
|
||||
@@ -487,7 +487,7 @@ open-sse/
|
||||
`antigravity`, `azure-openai`, `blackbox-web`, `chatgpt-web`, `cliproxyapi`,
|
||||
`cloudflare-ai`, `codex`, `commandCode`, `cursor`, `default`, `devin-cli`,
|
||||
`muse-spark-web`, `nlpcloud`, `opencode`, `perplexity-web`, `petals`,
|
||||
`pollinations`, `puter`, `qoder`, `vertex`, `devin-desktop`, plus `claudeIdentity.ts`
|
||||
`pollinations`, `puter`, `qoder`, `vertex`, `windsurf`, plus `claudeIdentity.ts`
|
||||
(shared identity helper) and `index.ts` (registry).
|
||||
|
||||
> Note: providers not listed here are served by `default.ts` using the generic
|
||||
@@ -634,17 +634,17 @@ Two binaries are exposed in `package.json` → `bin`:
|
||||
|
||||
## 7. `tests/`
|
||||
|
||||
| Directory | Type |
|
||||
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| `tests/unit/` | Unit tests via Node native test runner (1821 files, plus `api/`, `auth/`, `authz/` subdirs) |
|
||||
| `tests/integration/` | Cross-module + DB-state tests |
|
||||
| `tests/e2e/` | Playwright UI tests |
|
||||
| `tests/protocols-e2e/` | MCP/A2A protocol e2e |
|
||||
| `tests/translator/` | Translator-specific tests |
|
||||
| `tests/security/` | Security regressions |
|
||||
| `tests/load/` | Load / stress tests |
|
||||
| `tests/golden-set/` | Reference outputs for translator regressions |
|
||||
| `tests/helpers/`, `tests/fixtures/`, `tests/manual/` | Support |
|
||||
| Directory | Type |
|
||||
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
|
||||
| `tests/unit/` | Unit tests via Node native test runner (1821 files, plus `api/`, `auth/`, `authz/` subdirs) |
|
||||
| `tests/integration/` | Cross-module + DB-state tests |
|
||||
| `tests/e2e/` | Playwright UI tests |
|
||||
| `tests/protocols-e2e/` | MCP/A2A protocol e2e |
|
||||
| `tests/translator/` | Translator-specific tests |
|
||||
| `tests/security/` | Security regressions |
|
||||
| `tests/load/` | Load / stress tests |
|
||||
| `tests/golden-set/` | Reference outputs for translator regressions |
|
||||
| `tests/helpers/`, `tests/fixtures/`, `tests/manual/`, `tests/scratch_test.mjs` | Support |
|
||||
|
||||
Common commands:
|
||||
|
||||
|
||||
@@ -9,9 +9,7 @@ It describes each gate, what it validates, which CI job it runs in, whether it u
|
||||
a ratchet baseline or a pass/fail policy, and whether it blocks the build or is advisory.
|
||||
|
||||
For a short summary and the allowlist policy, see the "Quality Gates & Ratchets" section
|
||||
in `CLAUDE.md`. For the critical assessment, maturity classification, and tool-agnostic
|
||||
replication plan of the same system, see the
|
||||
[Quality Gate Playbook](../ops/QUALITY_GATE_PLAYBOOK.md).
|
||||
in `CLAUDE.md`.
|
||||
|
||||
---
|
||||
|
||||
@@ -31,28 +29,11 @@ changes:
|
||||
| `Build (advisory)` | Non-draft code PRs and Mergify queue branches; Node 24, `npm-ci-retry`, `check:node-runtime`, `npm run build` with `OMNIROUTE_USE_TURBOPACK=1`; no artifact upload because no downstream quality job consumes it | **Advisory** (`continue-on-error: true`; remove after one week of stable release-PR runs) |
|
||||
| `Docs Gates (fast-path)` | Docs/code PRs; API docs refs and docs-all | Yes |
|
||||
| `Fast Quality Gates` | Code PRs; static checks, typecheck, dashboard typecheck, impacted unit tests | Yes |
|
||||
| `Forgotten sibling tests` | Code PRs; changed modules traced to static consumers and candidate sibling tests; barrel and dynamic-import paths are reported as advisory diagnostics, with referenced allowlist exceptions | **Advisory** |
|
||||
| `Vitest (fast-path)` | Code PRs; fast vitest suite | Yes |
|
||||
| `Unit Tests fast-path` | Code PRs; 4-shard unit suite | Yes |
|
||||
| `No new ESLint warnings` | Code PRs; suppressions-aware lint guard | Yes for own-origin, advisory for forks |
|
||||
| `Merge integrity (changelog + generated skills)` | Non-draft PRs; changelog and generated skill sync | Yes for own-origin, advisory for forks |
|
||||
|
||||
#### Forgotten sibling tests report
|
||||
|
||||
`npm run check:forgotten-sibling-tests` reuses the import resolver behind the test-impact map.
|
||||
For every changed production module, it reports deterministic
|
||||
`changed module/symbol -> static consumer -> candidate sibling test` chains when the candidate
|
||||
test is absent from the pull-request diff. The Markdown summary and JSON result are retained as
|
||||
the `forgotten-sibling-tests` workflow artifact for calibration before any blocking rollout.
|
||||
|
||||
Barrel re-exports and dynamic imports are resolution diagnostics only; they never create a
|
||||
blocking finding. Reviewed exceptions live in
|
||||
`config/quality/forgotten-sibling-allowlist.json`. Each entry must name the consumer and candidate
|
||||
test, give a specific rationale, and link a GitHub issue or pull request. Malformed entries fail
|
||||
closed. Exceptions cannot suppress a deleted candidate test or a diff that adds `.skip`/`.todo`;
|
||||
assertion weakening and other masking remain owned by the independently blocking
|
||||
`check:test-masking` gate.
|
||||
|
||||
### Job: `lint`
|
||||
|
||||
Runs on every PR to `main`. Blocks merge on failure.
|
||||
|
||||
@@ -196,7 +196,7 @@ src/
|
||||
| `memory/vectorStore.ts` | sqlite-vec v0.1.9 wrapper — KNN brute-force + hybrid RRF (FTS5 + vector, k=60). Lazy-init, degrades gracefully when sqlite-vec unavailable. (plan 21) |
|
||||
| `memory/reindex.ts` | `runReindexBatch()` — processes memories with `needs_reindex=1` in background; called by `POST /api/memory/reindex` and lazy-backfill path. (plan 21) |
|
||||
| `monitoring/` | Health checks, metrics emission |
|
||||
| `oauth/` | OAuth/import flows for 22 provider modules (agy, antigravity, claude, cline, codebuddy-cn, codex, cursor, devin-desktop, ghe-copilot, github, gitlab-duo, grok-cli-oauth, grok-cli, kilocode, kimi-coding, kiro, qoder, raycast, trae, xai-oauth, zed-hosted, zed) |
|
||||
| `oauth/` | OAuth flows for 13 providers (claude, codex, antigravity, cursor, github, gemini, kimi-coding, kilocode, cline, kiro, qoder, gitlab-duo, windsurf) |
|
||||
| `plugins/` | Plugin registry |
|
||||
| `promptCache/` | Anthropic-style prompt cache breakpoints |
|
||||
| `skills/` | Skills framework (built-in + marketplace + SkillsSH) — see `docs/frameworks/SKILLS.md` |
|
||||
|
||||
@@ -224,16 +224,12 @@ rate limit. Bounded by `comboCooldownWait` (`enabled`, `maxWaitMs`, `maxAttempts
|
||||
**Scope**: the local per-provider+connection rate-limit queue (`open-sse/services/rateLimitManager.ts`,
|
||||
backed by Bottleneck), one layer below the three mechanisms above.
|
||||
|
||||
**`maxWaitMs` is a legacy persisted name for execution expiration.**
|
||||
`resilienceSettings.requestQueue.maxWaitMs` is passed to Bottleneck as a job
|
||||
`expiration`, whose timer starts only after dispatch. It therefore bounds
|
||||
limiter-managed execution, not time spent in the local queue. Expiration is
|
||||
surfaced as trusted local `code: "RATE_LIMIT_EXECUTION_TIMEOUT"` (HTTP 504);
|
||||
the former queue-timeout code name is accepted only for trusted internal
|
||||
backward compatibility. The default is 15000ms; override via
|
||||
`RATE_LIMIT_MAX_WAIT_MS` (env) or the dashboard (**Settings → Resilience**,
|
||||
1–30000ms UI ceiling). Queue residence has no time deadline; use
|
||||
`maxQueueDepth` below to bound queued callers.
|
||||
**`maxWaitMs` default lowered 120s → 15s.** `resilienceSettings.requestQueue.maxWaitMs`
|
||||
bounds how long a request may wait in the local queue before it is dropped
|
||||
(`code: "RATE_LIMIT_QUEUE_TIMEOUT"`, #4165). The factory default fell from 120000ms to
|
||||
15000ms so a saturated queue fails fast instead of holding a caller for two
|
||||
minutes; override via `RATE_LIMIT_MAX_WAIT_MS` (env) or the dashboard
|
||||
(**Settings → Resilience**, 1–30000ms UI ceiling).
|
||||
|
||||
**`maxQueueDepth` — opt-in admission cap (new).** `resilienceSettings.requestQueue.maxQueueDepth`
|
||||
bounds how many requests may sit queued (not yet dispatched) for one
|
||||
@@ -256,7 +252,7 @@ it is unit-testable without a real Bottleneck limiter.
|
||||
> around the `resolveCompressionSettings`/`selectCompressionStrategy` block),
|
||||
> not HTTP response compression on synthesized 429 bodies — there is no
|
||||
> matching code path for a literal bypass flag. That prompt-compression step
|
||||
> also currently runs _before_ `withRateLimit()` in the request pipeline, so
|
||||
> also currently runs *before* `withRateLimit()` in the request pipeline, so
|
||||
> reordering to skip it on a queue-full rejection is a separate, larger
|
||||
> change than this issue's scope; it was intentionally **not** implemented
|
||||
> here and is left as a follow-up if the CPU-saving win is worth the
|
||||
@@ -264,31 +260,6 @@ it is unit-testable without a real Bottleneck limiter.
|
||||
|
||||
---
|
||||
|
||||
## 6. Slow-stream throughput watchdog (#9709)
|
||||
|
||||
The optional `resilienceSettings.streamRecovery.throughputWatchdog` guard detects
|
||||
an upstream that is still sending chunks but producing assistant output below the
|
||||
configured useful-output rate. It is deliberately distinct from the idle timeout:
|
||||
heartbeats and metadata reset neither timer and do not count as progress. It is also
|
||||
distinct from the hard attempt deadline (#9153), which remains an absolute safety
|
||||
ceiling regardless of output quality.
|
||||
|
||||
The watchdog requires a warm-up period followed by a complete rolling window before
|
||||
it can abort. It counts text deltas from Chat Completions and Responses API output
|
||||
events (a conservative UTF-8 byte proxy), ignores usage-only and empty events, and
|
||||
suspends judgement while tool-call or reasoning events are in flight. It is disabled
|
||||
by default and can be enabled with `STREAM_THROUGHPUT_WATCHDOG_ENABLED=true`; the
|
||||
window, warm-up, minimum rate, and minimum measurable output are bounded by the
|
||||
normal resilience-settings normalization layer.
|
||||
|
||||
When enabled, a watchdog abort is applied only to the active upstream attempt. Before
|
||||
any client-visible bytes, the existing same-account early-recovery path may reopen
|
||||
the attempt. After commit, the stream is never blindly replayed; only the existing
|
||||
safe mid-stream continuation contract can stitch a suffix. Finalization remains
|
||||
single-shot, so usage accounting and semaphore release are not duplicated.
|
||||
|
||||
---
|
||||
|
||||
## Other Resilience Features
|
||||
|
||||
- **19 routing strategies** (priority, weighted, round-robin, context-relay, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, fusion, pipeline) — see [AUTO-COMBO.md](../routing/AUTO-COMBO.md).
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
title: "Admission lanes — two lane systems, what gates each, where each reports"
|
||||
status: active
|
||||
lastUpdated: 2026-08-09
|
||||
---
|
||||
|
||||
# Admission lanes (#9654) — two lane systems, what gates each, where each reports
|
||||
|
||||
OmniRoute has **two** process-local lane systems with different scopes. They are
|
||||
complementary; operators should know which one they are looking at.
|
||||
|
||||
## 1. Byte-level per-connection lanes (`chatBodyAdmission.ts`)
|
||||
|
||||
- **Scope:** the buffered-body/heap path for `POST /v1/chat/completions`. Guards
|
||||
against heap amplification from large coding-agent bodies (#4380).
|
||||
- **Gate:** **always on.** Each distinct API key (hashed) — or `anonymous` — gets its
|
||||
own lane with `CHAT_MAX_HEAVY_IN_FLIGHT` capacity, so one session's burst cannot
|
||||
starve another session's heavyweight slot.
|
||||
- **Tuning:**
|
||||
- `OMNIROUTE_CHAT_VIRTUAL_TTL_MS` — idle-lane eviction (default 60000)
|
||||
- `OMNIROUTE_CHAT_VIRTUAL_MAX_SESSIONS` — lane count cap (default 64)
|
||||
- `OMNIROUTE_CHAT_ADMISSION_QUEUE_MS` — queue-wait before 503 (default 2000)
|
||||
- `OMNIROUTE_CHAT_ADMISSION_MAX_QUEUED_BYTES` — queued-bytes heap valve (default 4 MB)
|
||||
- **Reports:** not in `GET /api/monitoring/health` today; observable via
|
||||
`PerConnectionAdmissionController.snapshot()` (sessionId hash, activeHeavy, idleMs).
|
||||
|
||||
## 2. Adaptive runtime virtual lanes (`open-sse/services/admission`)
|
||||
|
||||
- **Scope:** tenant-key admission for provider dispatch — queue cost, latency-guided
|
||||
limit adaptation, lane queueing, and lane metrics.
|
||||
- **Gate:** **opt-in.** Disabled unless `OMNIROUTE_CHAT_VIRTUAL_LANES=true`. Without it,
|
||||
the adaptive controller keeps the shared queue behavior (criterion 1 of #9654 only
|
||||
holds once an operator enables lanes).
|
||||
- **Tuning:** `OMNIROUTE_CHAT_VIRTUAL_LANES` + adaptive config (`maxQueueCount`,
|
||||
`maxQueueCost`, `defaultMaxWaitMs`, …).
|
||||
- **Reports:** `GET /api/monitoring/health` → `adaptiveAdmission` → `laneCount`,
|
||||
`laneQueuedCount`, `laneQueuedCost`, `laneTenants` (opaque lane IDs, never raw keys).
|
||||
|
||||
## Which one is showing in a dashboard
|
||||
|
||||
- `adaptiveAdmission.laneCount` / `laneTenants` → **adaptive virtual lanes** (system 2).
|
||||
- A health payload with **no** `adaptiveAdmission.lane*` fields usually means
|
||||
`OMNIROUTE_CHAT_VIRTUAL_LANES` is unset — the byte-level lanes (system 1) are still
|
||||
active, but nothing under `adaptiveAdmission` will report lane data until it is enabled.
|
||||
|
||||
## Why both exist
|
||||
|
||||
The byte-level lanes bound the memory-heavy parse/compress path; the adaptive lanes
|
||||
bound dispatch cost per tenant. #9654's criterion 1 ("one session's burst does not 503
|
||||
another") is enforced by system 1 unconditionally and by system 2 once opt-in is enabled.
|
||||
@@ -6,12 +6,6 @@
|
||||
"CODEBASE_DOCUMENTATION",
|
||||
"REPOSITORY_MAP",
|
||||
"RESILIENCE_GUIDE",
|
||||
"QUALITY_GATES",
|
||||
"DESIGN_SYSTEM",
|
||||
"MONITORING_SECTIONS",
|
||||
"ROUTER_BACKENDS",
|
||||
"admission-lanes",
|
||||
"cluster-decisions",
|
||||
"persistence-backend-boundary"
|
||||
"QUALITY_GATES"
|
||||
]
|
||||
}
|
||||
|
||||
916
docs/architecture/mysql-conformance-semantics.md
Normal file
916
docs/architecture/mysql-conformance-semantics.md
Normal file
@@ -0,0 +1,916 @@
|
||||
---
|
||||
title: "MySQL conformance semantics and failure-mode matrix"
|
||||
status: proposed-test-specification
|
||||
lastUpdated: 2026-07-30
|
||||
---
|
||||
|
||||
# MySQL conformance semantics and failure-mode matrix
|
||||
|
||||
- **Tracking issue:** [#8075](https://github.com/diegosouzapw/OmniRoute/issues/8075)
|
||||
- **Governing proposal:** [Pluggable persistence boundary](persistence-backend-boundary.md)
|
||||
- **Measured baseline:** [SQLite coupling inventory](sqlite-coupling-inventory.md)
|
||||
- **Target:** MySQL 8.0 with InnoDB
|
||||
- **Runtime impact:** None. This document adds no driver, dependency, configuration, schema,
|
||||
migration, or support claim.
|
||||
|
||||
## 1. Purpose and normative language
|
||||
|
||||
The persistence-boundary ADR requires conformance tests to compare observable behavior, not only
|
||||
repository method signatures. This document turns the MySQL/InnoDB differences that can change
|
||||
OmniRoute behavior into an implementation-ready specification. It provides:
|
||||
|
||||
- a required server and session profile;
|
||||
- evidence from the current SQLite implementation;
|
||||
- minimal SQL probes that reviewers can reproduce independently;
|
||||
- a backend-neutral error and retry taxonomy;
|
||||
- normative decisions that a repository contract must make;
|
||||
- executable acceptance specifications for a future shared conformance harness;
|
||||
- a focused acceptance profile for combo definitions and model-to-combo mappings.
|
||||
|
||||
The terms **MUST**, **MUST NOT**, **SHOULD**, and **MAY** are normative. A proposed MySQL adapter is
|
||||
not conformant merely because its SQL succeeds. It is conformant only when the same repository
|
||||
fixture produces the same domain result, durable state, atomicity, ordering, and classified failure
|
||||
as the SQLite implementation.
|
||||
|
||||
## 2. Scope and non-goals
|
||||
|
||||
### 2.1 In scope
|
||||
|
||||
This specification covers portable durable-state behavior for:
|
||||
|
||||
- create, read, update, delete, and missing-row results;
|
||||
- uniqueness, collation, case and accent sensitivity, and `NULL`;
|
||||
- stable ordering and pagination;
|
||||
- no-op writes and affected-row reporting;
|
||||
- insert, identity-preserving upsert, and replacement;
|
||||
- IDs, JSON, exact numerics, and timestamps;
|
||||
- transactions, deadlocks, lock waits, disconnects, and retry boundaries;
|
||||
- foreign keys and atomic related-record changes;
|
||||
- migration ownership, implicit DDL commits, recovery, and readiness.
|
||||
|
||||
### 2.2 Out of scope
|
||||
|
||||
This specification does not:
|
||||
|
||||
- approve PostgreSQL or MySQL runtime support;
|
||||
- select a Node.js MySQL driver or pool;
|
||||
- define a public environment variable or configuration UI;
|
||||
- define final TypeScript repository interfaces;
|
||||
- add physical MySQL schema or migration files;
|
||||
- make SQLite maintenance, FTS5, `sqlite-vec`, backup files, or WAL portable;
|
||||
- replace domain-specific acceptance criteria;
|
||||
- permit runtime work while the governing ADR remains unapproved.
|
||||
|
||||
## 3. Evidence from the current repository
|
||||
|
||||
The current implementation establishes behavior that a portable contract must either preserve or
|
||||
explicitly revise. These are source-backed observations, not proposed MySQL schema.
|
||||
|
||||
### 3.1 Combo identity and lookup
|
||||
|
||||
`src/lib/db/migrations/001_initial_schema.sql` defines `combos.id` as the primary key and
|
||||
`combos.name` as unique. `src/lib/db/combos.ts` currently:
|
||||
|
||||
- generates UUIDs in the application;
|
||||
- generates timestamps with `new Date().toISOString()`;
|
||||
- performs exact name lookup first;
|
||||
- provides a separate `COLLATE NOCASE` fallback lookup;
|
||||
- lists by `sort_order ASC, name COLLATE NOCASE ASC`;
|
||||
- treats an update of a missing ID as `null`;
|
||||
- treats deletion of a missing ID as `false`;
|
||||
- updates the JSON payload and deduplicated columns together;
|
||||
- reorders all selected rows in one SQLite transaction.
|
||||
|
||||
Those choices imply that a future MySQL slice does not need database-generated numeric IDs for
|
||||
combos, but it must still define Unicode collation, complete tie-breakers, update/delete results, and
|
||||
reorder concurrency.
|
||||
|
||||
### 3.2 Model-to-combo mapping behavior
|
||||
|
||||
`src/lib/db/migrations/010_model_combo_mappings.sql` defines a foreign key from
|
||||
`model_combo_mappings.combo_id` to `combos.id` with `ON DELETE CASCADE`.
|
||||
`src/lib/db/modelComboMappings.ts` currently:
|
||||
|
||||
- generates mapping UUIDs and ISO timestamps in the application;
|
||||
- lists by `priority DESC, created_at ASC`;
|
||||
- returns a separate total count for paginated results;
|
||||
- maps integer `0`/`1` values to booleans;
|
||||
- treats a missing update as `null` and a missing delete as `false`;
|
||||
- resolves the first enabled matching pattern;
|
||||
- skips malformed combo JSON rather than failing resolution.
|
||||
|
||||
The current list and resolution order lacks a unique final tie-breaker. The MySQL implementation
|
||||
MUST NOT preserve that accidental nondeterminism. Before portability is claimed, the contract must
|
||||
add `id ASC` (or another unique stable key) after `created_at ASC` and the SQLite implementation
|
||||
must adopt the same order.
|
||||
|
||||
### 3.3 Existing SQLite-specific signals
|
||||
|
||||
The measured SQLite coupling inventory records widespread use of synchronous prepared statements,
|
||||
`INSERT OR REPLACE`, `lastInsertRowid`, SQLite transactions, and SQLite lifecycle operations. A
|
||||
future adapter must not translate those tokens mechanically. In particular:
|
||||
|
||||
- `INSERT OR REPLACE` is delete-then-insert conflict handling, not an update;
|
||||
- `changes` is a driver result, not a portable domain result;
|
||||
- `COLLATE NOCASE` is not equivalent to a modern MySQL Unicode collation;
|
||||
- SQLite numbered migration SQL is not reusable as MySQL migration SQL.
|
||||
|
||||
## 4. Required MySQL deployment and session profile
|
||||
|
||||
A conformance run MUST fail during backend initialization if the effective profile is outside the
|
||||
supported envelope. Silently inheriting server defaults would make behavior depend on an operator's
|
||||
installation history.
|
||||
|
||||
| Property | Required profile | Verification | Failure class |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | --------------------- |
|
||||
| Server family | Oracle MySQL 8.0.x until another family passes the same suite | `SELECT VERSION()` and server metadata | `unsupported` |
|
||||
| Storage engine | `InnoDB` for every portable table | `information_schema.tables` | `schema_incompatible` |
|
||||
| Character set | `utf8mb4` for schema, tables, and portable text columns | `information_schema.schemata`, `tables`, and `columns` | `schema_incompatible` |
|
||||
| Identity collation | Explicit per identity column; never inherited | `information_schema.columns.collation_name` | `schema_incompatible` |
|
||||
| SQL mode | Strict mode and the engine-substitution guard; adapter records the effective value | `SELECT @@SESSION.sql_mode` | `unsupported` |
|
||||
| Transaction isolation | Explicitly selected and verified by the backend | `SELECT @@SESSION.transaction_isolation` | `unsupported` |
|
||||
| Session time zone | UTC | `SELECT @@SESSION.time_zone` | `unsupported` |
|
||||
| Autocommit | Known pool default; repository transactions set boundaries explicitly | `SELECT @@SESSION.autocommit` | `unsupported` |
|
||||
| Connection character set | `utf8mb4` | `SELECT @@character_set_client, @@character_set_connection, @@character_set_results` | `unsupported` |
|
||||
| Found-rows behavior | One fixed pool setting, but repository results remain independent of it | Driver/pool configuration plus conformance probe | `unsupported` |
|
||||
| Foreign-key checks | Enabled for normal runtime and conformance tests | `SELECT @@SESSION.foreign_key_checks` | `unsupported` |
|
||||
| InnoDB page size | Recorded before validating indexed key lengths | `SELECT @@innodb_page_size` | `schema_incompatible` |
|
||||
|
||||
The backend readiness report SHOULD expose the verified profile without credentials. It MUST NOT
|
||||
log connection strings or secrets.
|
||||
|
||||
### 4.1 Initialization probe
|
||||
|
||||
The adapter acceptance suite should run an equivalent of the following read-only probe on a newly
|
||||
leased connection:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
VERSION() AS server_version,
|
||||
@@SESSION.sql_mode AS sql_mode,
|
||||
@@SESSION.transaction_isolation AS transaction_isolation,
|
||||
@@SESSION.time_zone AS time_zone,
|
||||
@@SESSION.autocommit AS autocommit,
|
||||
@@SESSION.foreign_key_checks AS foreign_key_checks,
|
||||
@@character_set_client AS character_set_client,
|
||||
@@character_set_connection AS character_set_connection,
|
||||
@@character_set_results AS character_set_results,
|
||||
@@innodb_page_size AS innodb_page_size;
|
||||
```
|
||||
|
||||
A pool MUST apply and verify session settings on every newly created physical connection. Applying
|
||||
settings only to the first connection is insufficient.
|
||||
|
||||
## 5. Normative semantic matrix
|
||||
|
||||
### 5.0 Observable SQLite/MySQL difference summary
|
||||
|
||||
This table is the review index for the detailed rules below. It distinguishes current or common
|
||||
backend behavior from the portable result the repository must expose. The MySQL column describes
|
||||
InnoDB under the verified session profile; it must not be read as permission to inherit an
|
||||
unverified server default.
|
||||
|
||||
| Concern | SQLite-shaped behavior | MySQL/InnoDB behavior | Required repository contract |
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Text identity | Binary comparison by default; current code opts into ASCII-oriented `NOCASE` for selected reads and sorts | Equality, uniqueness, and sort order follow the selected column/expression collation | Declare byte-exact identity separately from named insensitive lookup and display order |
|
||||
| Nullable unique key | Multiple SQL `NULL` values can pass a plain unique constraint | Multiple SQL `NULL` values can pass a plain unique index | Enforce any "one logical null" invariant atomically outside a plain unique key |
|
||||
| Unordered/tied results | No total order without a complete `ORDER BY` | No total order without a complete `ORDER BY` | Define `NULL` position and a unique final tie-breaker for every portable list |
|
||||
| No-op update | Driver change count reflects SQLite's statement behavior | Changed-row count differs from matched-row mode for identical assignments | Return domain outcomes independently of raw affected-row counts |
|
||||
| Conflict write | `INSERT OR REPLACE` can delete then insert | Duplicate-key upsert updates one selected conflict | Classify every operation as insert-only, identity-preserving upsert, or replacement |
|
||||
| Generated identity | SQLite row IDs and driver-local last-insert state are connection-bound | Generated IDs and last-insert state are connection-bound | Retrieve identity in the insert operation/lease and use stable idempotency identity on retry |
|
||||
| JSON | Existing combo payloads are text and malformed legacy text can be observed | Native `JSON` validates and normalizes its representation | Choose text or typed JSON deliberately and compare the declared domain representation |
|
||||
| Exact values/time | Current modules commonly serialize JavaScript values and ISO UTC text | Driver conversion can lose large integers/decimals; temporal types depend on type and session zone | Fix exact representations, UTC policy, and precision across backends |
|
||||
| Concurrency/isolation | Deferred transactions and a database-wide single-writer model shape conflicts; read visibility depends on transaction mode and WAL state | InnoDB defaults to `REPEATABLE READ`, uses MVCC snapshots for consistent reads, and permits concurrent writers on different locked records | Select and verify isolation, then test domain-visible reads, conflicts, and retry boundaries rather than relying on either default |
|
||||
| DDL/migrations | SQLite migration sequences can be wrapped according to SQLite transaction rules | DDL commonly commits implicitly; one atomic DDL statement does not make a multi-step migration atomic | Use distributed ownership, durable phase checkpoints, postcondition inspection, and readiness gating |
|
||||
|
||||
### 5.1 Text identity, collation, and uniqueness
|
||||
|
||||
MySQL equality and unique indexes use the effective collation of the indexed expression. A `_ci`
|
||||
collation is case-insensitive; an `_ai` collation is also accent-insensitive. SQLite's default text
|
||||
comparison and `COLLATE NOCASE` do not provide an equivalent Unicode contract.
|
||||
|
||||
| Concern | SQLite-shaped risk | Required portable decision | MySQL implementation rule |
|
||||
| ---------------- | -------------------------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| IDs | Text IDs can inherit an unintended collation | IDs are byte-exact and case-sensitive | Use an explicit binary collation or binary representation |
|
||||
| Combo names | Exact lookup and insensitive fallback are separate today | Exact lookup remains exact; insensitive lookup is a named operation | Exact and insensitive queries use explicit, different collations or normalized keys |
|
||||
| Unique names | A server default can collapse case or accents | The domain declares whether case/accent variants conflict | Unique index uses the declared collation, never the database default |
|
||||
| Pattern text | Pattern matching occurs in application code | Stored pattern bytes round-trip unchanged | Store with an explicit case-sensitive collation |
|
||||
| User-facing sort | SQLite `NOCASE` order is not portable Unicode order | List order is defined by a normalized sort key or explicit collation policy | Schema and query use the selected policy and a unique tie-breaker |
|
||||
|
||||
Minimum probe:
|
||||
|
||||
```sql
|
||||
CREATE TEMPORARY TABLE conformance_text (
|
||||
id VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin PRIMARY KEY,
|
||||
name VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci UNIQUE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
INSERT INTO conformance_text (id, name) VALUES ('A', 'Résumé');
|
||||
-- The next statement conflicts under utf8mb4_0900_ai_ci.
|
||||
INSERT INTO conformance_text (id, name) VALUES ('a', 'resume');
|
||||
```
|
||||
|
||||
The harness MUST repeat the probe for the exact collation selected by the eventual schema; the
|
||||
example collation above is evidence, not an approval for combo names.
|
||||
|
||||
### 5.2 `NULL`, missing rows, and nullable unique keys
|
||||
|
||||
MySQL unique indexes permit multiple `NULL` values. SQLite does likewise for unique columns.
|
||||
However, neither behavior implements a domain invariant such as "only one active row may have no
|
||||
owner."
|
||||
|
||||
Repository contracts MUST distinguish:
|
||||
|
||||
- no row found;
|
||||
- a row found with a nullable field set to SQL `NULL`;
|
||||
- a JSON document containing JSON `null`;
|
||||
- a missing JSON member.
|
||||
|
||||
Minimum probe:
|
||||
|
||||
```sql
|
||||
CREATE TEMPORARY TABLE conformance_null (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
optional_key VARCHAR(64) NULL,
|
||||
UNIQUE KEY uq_optional_key (optional_key)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
INSERT INTO conformance_null VALUES ('one', NULL), ('two', NULL);
|
||||
SELECT COUNT(*) AS row_count FROM conformance_null;
|
||||
-- Expected: 2.
|
||||
```
|
||||
|
||||
If a domain allows at most one logical `NULL`, it MUST use an explicit atomic invariant rather than
|
||||
rely on a plain unique index.
|
||||
|
||||
### 5.3 Ordering, ties, and pagination
|
||||
|
||||
Without `ORDER BY`, result order is undefined. With a non-unique `ORDER BY`, tied rows still have an
|
||||
undefined relative order. Offset pagination can therefore duplicate or omit records if the complete
|
||||
order is not stable.
|
||||
|
||||
Every portable list MUST specify:
|
||||
|
||||
1. every user-visible sort expression;
|
||||
2. the position of `NULL` values;
|
||||
3. a unique final tie-breaker;
|
||||
4. the cursor comparison tuple, if cursor pagination is used;
|
||||
5. the snapshot/concurrency expectation across pages.
|
||||
|
||||
For the proposed combo/mapping slice:
|
||||
|
||||
```sql
|
||||
-- Combo list contract candidate.
|
||||
ORDER BY sort_order ASC, normalized_name ASC, id ASC
|
||||
|
||||
-- Mapping list and resolution contract candidate.
|
||||
ORDER BY priority DESC, created_at ASC, id ASC
|
||||
```
|
||||
|
||||
The exact `normalized_name` representation remains a contract decision. It MUST NOT be implemented
|
||||
by relying on an unspecified database default.
|
||||
|
||||
For nullable values, use an explicit sort key rather than a backend default:
|
||||
|
||||
```sql
|
||||
ORDER BY nullable_column IS NULL ASC, nullable_column ASC, id ASC
|
||||
```
|
||||
|
||||
### 5.4 Update, no-op, delete, and affected rows
|
||||
|
||||
MySQL `UPDATE` reports rows actually changed by default. With the C API found-rows connection flag,
|
||||
it reports rows matched. `INSERT ... ON DUPLICATE KEY UPDATE` reports 1 for insert, 2 for an actual
|
||||
update, and 0 for an update to identical values; the found-rows flag changes the last value to 1.
|
||||
These numbers MUST NOT become repository semantics.
|
||||
|
||||
| Repository outcome | Required meaning | Forbidden implementation shortcut |
|
||||
| ------------------ | ------------------------------------------------------ | --------------------------------------------- |
|
||||
| `updated` | Target existed and the operation's postcondition holds | `affectedRows > 0` alone |
|
||||
| `unchanged` | Target existed and already satisfied the postcondition | Treating 0 changed rows as missing |
|
||||
| `not_found` | Target identity did not exist | Treating every 0 count as unchanged |
|
||||
| `conflict` | Compare/update version or invariant failed | Returning generic `false` |
|
||||
| delete `true` | A row existed and was deleted | Assuming a successful statement deleted a row |
|
||||
| delete `false` | No row existed | Throwing a backend-specific error |
|
||||
|
||||
Minimum probe, run once with each supported connection mode:
|
||||
|
||||
```sql
|
||||
CREATE TEMPORARY TABLE conformance_update (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
value_text VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
INSERT INTO conformance_update VALUES ('row', 'same', 1);
|
||||
UPDATE conformance_update SET value_text = 'same' WHERE id = 'row';
|
||||
UPDATE conformance_update SET value_text = 'changed' WHERE id = 'row';
|
||||
UPDATE conformance_update SET value_text = 'missing' WHERE id = 'missing';
|
||||
```
|
||||
|
||||
The harness asserts repository results and final rows, not raw driver counts. A versioned
|
||||
compare/update SHOULD use a predicate such as `WHERE id = ? AND version_no = ?`, then distinguish a
|
||||
missing identity from a stale version according to the domain contract.
|
||||
|
||||
### 5.5 Insert, upsert, and replacement
|
||||
|
||||
SQLite `INSERT OR REPLACE` deletes rows that conflict with a unique or primary key before inserting
|
||||
the new row. MySQL `INSERT ... ON DUPLICATE KEY UPDATE` updates one conflicting row. The two forms
|
||||
differ in foreign-key cascades, triggers, omitted columns, IDs, timestamps, and affected-row counts.
|
||||
|
||||
Every write method MUST be classified as exactly one of:
|
||||
|
||||
1. **insert-only:** duplicate identity returns `unique_violation`;
|
||||
2. **identity-preserving upsert:** duplicate identity updates an explicit allowlist of mutable fields;
|
||||
3. **replacement:** old identity is deleted and a new row is inserted, with cascade effects included
|
||||
in the contract.
|
||||
|
||||
A generic helper MUST NOT choose among these behaviors based on SQL convenience.
|
||||
|
||||
Minimum difference probe. This uses ordinary InnoDB tables because MySQL temporary tables cannot
|
||||
serve as the parent/child foreign-key fixture. Run it in an isolated conformance schema; cleanup is
|
||||
included so the probe is repeatable:
|
||||
|
||||
```sql
|
||||
DROP TABLE IF EXISTS conformance_child;
|
||||
DROP TABLE IF EXISTS conformance_parent;
|
||||
|
||||
CREATE TABLE conformance_parent (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
immutable_value VARCHAR(64) NOT NULL,
|
||||
mutable_value VARCHAR(64) NOT NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE conformance_child (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
parent_id VARCHAR(64) NOT NULL,
|
||||
CONSTRAINT fk_conformance_child_parent
|
||||
FOREIGN KEY (parent_id) REFERENCES conformance_parent(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
INSERT INTO conformance_parent VALUES ('p', 'keep', 'old');
|
||||
INSERT INTO conformance_child VALUES ('c', 'p');
|
||||
INSERT INTO conformance_parent (id, immutable_value, mutable_value)
|
||||
VALUES ('p', 'replacement', 'new')
|
||||
ON DUPLICATE KEY UPDATE mutable_value = VALUES(mutable_value);
|
||||
|
||||
SELECT immutable_value, mutable_value FROM conformance_parent WHERE id = 'p';
|
||||
SELECT COUNT(*) AS child_count FROM conformance_child WHERE parent_id = 'p';
|
||||
-- Expected: immutable_value='keep', mutable_value='new', child_count=1.
|
||||
|
||||
DROP TABLE conformance_child;
|
||||
DROP TABLE conformance_parent;
|
||||
```
|
||||
|
||||
The `VALUES(mutable_value)` form is used here because the target remains MySQL 8.0 as a family and
|
||||
no minimum 8.0 patch release has been approved. It is deprecated in later MySQL 8.0 releases, so an
|
||||
adapter that establishes a newer minimum MAY use the supported row-alias form instead. The harness
|
||||
asserts identity-preserving behavior, not either SQL spelling.
|
||||
|
||||
Tables with multiple unique indexes require special care because a duplicate can select an
|
||||
unexpected conflicting row. Portable upsert schema SHOULD have one unambiguous conflict identity.
|
||||
|
||||
### 5.6 Unicode and index-size constraints
|
||||
|
||||
`utf8mb4` uses up to four bytes per character. InnoDB's maximum index key is 3072 bytes for common
|
||||
`DYNAMIC` or `COMPRESSED` row formats with a 16 KiB page, and is lower for smaller page sizes or
|
||||
legacy row formats. A prefix unique index is not equivalent to full-value uniqueness.
|
||||
|
||||
Schema acceptance MUST:
|
||||
|
||||
- set bounded lengths for all indexed identity strings;
|
||||
- calculate the worst-case byte length of every composite index;
|
||||
- verify the actual page size and row format;
|
||||
- reject a prefix unique index for a full-identity contract;
|
||||
- test maximum-length non-ASCII values before migration is accepted;
|
||||
- classify an incompatible definition as `schema_incompatible`, not `unique_violation`.
|
||||
|
||||
Example boundary probe for a 16 KiB/DYNAMIC profile:
|
||||
|
||||
```sql
|
||||
CREATE TEMPORARY TABLE conformance_index (
|
||||
value_text VARCHAR(768) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
UNIQUE KEY uq_value_text (value_text)
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;
|
||||
```
|
||||
|
||||
The exact accepted length MUST be derived from all key parts and the verified deployment profile;
|
||||
this example is deliberately near a physical boundary and is not a proposed production column.
|
||||
|
||||
### 5.7 IDs and connection-local state
|
||||
|
||||
The current combo and mapping modules generate UUIDs in the application. A MySQL implementation
|
||||
SHOULD preserve this strategy for those domains.
|
||||
|
||||
If another domain uses a database-generated incrementing ID, the adapter MUST observe these rules:
|
||||
|
||||
- ID retrieval is part of the same driver operation and physical connection as the insert;
|
||||
- callers never issue a later connection-level `LAST_INSERT_ID()` query;
|
||||
- multi-row inserts define whether one ID or all IDs are returned;
|
||||
- an error or rollback makes a previously observed `LAST_INSERT_ID()` unsuitable as proof of commit;
|
||||
- retries use a stable domain idempotency key;
|
||||
- upsert defines whether it returns an existing or newly generated identity.
|
||||
|
||||
MySQL documents `LAST_INSERT_ID()` as per-connection state and leaves it undefined after some errors
|
||||
or error-driven rollbacks. Pool leases are therefore part of correctness, not merely performance.
|
||||
|
||||
### 5.8 JSON representation
|
||||
|
||||
Current combo data is JSON text, and malformed JSON is observable: combo reads can skip malformed
|
||||
rows and mapping resolution skips malformed combo payloads. Switching the MySQL column directly to
|
||||
native `JSON` would reject malformed rows at write/import time and normalize duplicate keys,
|
||||
whitespace, and key order.
|
||||
|
||||
Before choosing `LONGTEXT` or `JSON`, the combo contract MUST decide:
|
||||
|
||||
- whether malformed stored payloads remain representable for compatibility tests;
|
||||
- whether equality is structural or byte-for-byte;
|
||||
- whether duplicate object keys are rejected before persistence;
|
||||
- whether serialization order is stable and application-owned;
|
||||
- which fields are duplicated into typed columns and which representation is authoritative.
|
||||
|
||||
For the first slice, an identity-preserving migration SHOULD keep application serialization as the
|
||||
domain boundary. If native `JSON` is selected, imports MUST parse and validate before writing, and
|
||||
tests MUST compare parsed domain values rather than raw JSON text.
|
||||
|
||||
Minimum normalization probe:
|
||||
|
||||
```sql
|
||||
CREATE TEMPORARY TABLE conformance_json (id VARCHAR(64) PRIMARY KEY, payload JSON) ENGINE=InnoDB;
|
||||
INSERT INTO conformance_json VALUES ('j', '{"b": 2, "a": 1, "a": 3}');
|
||||
SELECT payload FROM conformance_json WHERE id = 'j';
|
||||
-- The value is normalized; original whitespace/key duplication is not preserved.
|
||||
```
|
||||
|
||||
### 5.9 Exact numerics and timestamps
|
||||
|
||||
| Type | Risk | Required contract |
|
||||
| ----------- | ----------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `BIGINT` | Values can exceed JavaScript's safe integer range | Return a string or validated bigint representation across every backend |
|
||||
| `DECIMAL` | Driver options may return strings or lossy numbers | Fix precision/scale and use an exact domain representation |
|
||||
| `TIMESTAMP` | Session time zone conversion and fractional precision | Force UTC session time zone and specify fractional precision |
|
||||
| `DATETIME` | No intrinsic time zone | Use only for explicitly zone-free civil time |
|
||||
| ISO text | Lexical ordering depends on one canonical format | Validate UTC suffix and exact precision before persistence |
|
||||
|
||||
Combo and mapping timestamps are currently application-generated ISO strings. The first slice SHOULD
|
||||
preserve their exact domain format rather than introducing server-generated local time.
|
||||
|
||||
### 5.10 Transaction isolation and observable concurrency
|
||||
|
||||
MySQL InnoDB uses `REPEATABLE READ` as its default isolation level. Within an explicit transaction,
|
||||
its consistent non-locking reads normally establish and reuse an MVCC snapshot, while locking reads
|
||||
and writes inspect and lock current index records or ranges. SQLite instead combines snapshot/read
|
||||
transaction behavior with a database-wide single-writer model; transaction mode and WAL state affect
|
||||
when a writer is admitted and when a read transaction can be upgraded. These mechanisms are not
|
||||
interchangeable even when a simple CRUD fixture produces the same final row.
|
||||
|
||||
The backend profile MUST select and verify an isolation level rather than silently accept either
|
||||
backend's default. The repository contract MUST then define observable results for each atomic
|
||||
operation. It MUST NOT promise the implementation mechanism itself, such as gap locks or a
|
||||
SQLite-wide writer lock.
|
||||
|
||||
| Scenario | SQLite-shaped risk | InnoDB `REPEATABLE READ` risk | Required conformance decision |
|
||||
| --------------------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| Two reads in one transaction | Snapshot timing depends on when the read transaction begins and the active journal mode | Consistent reads normally reuse the transaction's first established read view | State whether the operation requires one stable snapshot or deliberately performs a current read |
|
||||
| Range read plus concurrent insert | A concurrent writer may be serialized by SQLite's writer admission rules | A plain consistent read can retain its snapshot; a locking range read can lock index gaps | Define whether a later read sees the insert and whether the operation requires a locking predicate |
|
||||
| Read-modify-write | Single-writer serialization can mask an unsafe application sequence | Concurrent transactions can read the same value and later contend or overwrite without a version predicate | Require compare/update, a locking read, or another explicit invariant; never rely on backend serialization |
|
||||
| Writers touching different rows | SQLite still admits only one writer at a time | InnoDB can execute both until their record/range locks conflict | Do not infer portable throughput or lock order; assert only atomic effects and classified conflicts |
|
||||
| Pagination across transactions | Separate page reads can observe different committed states | Separate autocommit reads get separate views; one transaction may retain one view | Declare snapshot pagination or documented live pagination and test that policy |
|
||||
| Retry after conflict | Busy/locked outcomes and transaction upgrade failures are SQLite-shaped | Deadlocks and lock timeouts have different rollback scopes | Normalize the error, discard the failed context, and retry the complete idempotent operation only |
|
||||
|
||||
Minimum two-connection visibility probe for the selected MySQL profile:
|
||||
|
||||
```text
|
||||
Connection A Connection B
|
||||
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
START TRANSACTION;
|
||||
SELECT value_no FROM conformance_isolation
|
||||
WHERE id = 1; -- establishes read view: 0
|
||||
START TRANSACTION;
|
||||
UPDATE conformance_isolation
|
||||
SET value_no = 1 WHERE id = 1;
|
||||
COMMIT;
|
||||
SELECT value_no FROM conformance_isolation
|
||||
WHERE id = 1; -- same consistent-read view: 0
|
||||
COMMIT;
|
||||
SELECT value_no FROM conformance_isolation
|
||||
WHERE id = 1; -- new transaction/view: 1
|
||||
```
|
||||
|
||||
The shared harness MUST NOT assert that every backend reproduces this internal sequence. It must use
|
||||
it to prove that the chosen repository operation either requests a stable snapshot explicitly or
|
||||
avoids depending on repeat-read visibility. If an operation uses a current/locking read, that choice
|
||||
and its conflict behavior need a separate test.
|
||||
|
||||
## 6. Transactions, failures, and retry policy
|
||||
|
||||
### 6.1 Transaction states
|
||||
|
||||
The backend contract should expose only opaque transaction contexts, but its implementation must
|
||||
maintain the following lifecycle:
|
||||
|
||||
```text
|
||||
idle
|
||||
-> active
|
||||
-> committed
|
||||
-> rolled_back
|
||||
-> failed_statement -> rolled_back
|
||||
-> failed_transaction -> rolled_back
|
||||
-> outcome_unknown -> reconciled | escalated
|
||||
```
|
||||
|
||||
A context in `committed`, `rolled_back`, `failed_transaction`, or `outcome_unknown` MUST reject new
|
||||
repository work. A context with a failed statement SHOULD be explicitly rolled back before its
|
||||
connection returns to the pool, even when MySQL would technically permit more statements.
|
||||
|
||||
### 6.2 Error classification matrix
|
||||
|
||||
Numeric codes and SQLSTATE values below are MySQL 8.0 server signals. A Node.js driver can also
|
||||
produce transport-specific codes; those MUST be normalized without leaking raw messages to callers.
|
||||
|
||||
| Condition | MySQL signal | Rollback scope | Portable class | Retry policy |
|
||||
| ------------------------------ | -------------------------------------- | ------------------------------------------------- | ------------------------ | -------------------------------------------------------------- |
|
||||
| Duplicate key | `1062`, SQLSTATE `23000` | Statement | `unique_violation` | No, unless contract defines idempotent create |
|
||||
| Missing referenced parent | `1452`, SQLSTATE `23000` | Statement | `foreign_key_violation` | No |
|
||||
| Parent still referenced | `1451`, SQLSTATE `23000` | Statement | `foreign_key_violation` | No |
|
||||
| Deadlock victim | `1213`, SQLSTATE `40001` | Entire transaction | `transaction_conflict` | Retry whole atomic operation |
|
||||
| Lock wait timeout | `1205`, SQLSTATE `HY000` | Statement by default; server option can change it | `lock_timeout` | Roll back explicitly, then retry whole operation if idempotent |
|
||||
| Invalid JSON text | `3140`, SQLSTATE `22032` | Statement | `invalid_data` | No |
|
||||
| Data too long | `1406`, SQLSTATE `22001` | Statement | `invalid_data` | No |
|
||||
| Check constraint | `3819`, SQLSTATE `HY000` | Statement | `constraint_violation` | No |
|
||||
| Server gone before request | Driver/server transport signal | No operation or unknown | `unavailable` | Retry only if operation definitely was not sent |
|
||||
| Connection lost during request | Driver transport signal | Unknown | `outcome_unknown` | Reconcile by idempotency key; do not blind retry |
|
||||
| Pool acquisition timeout | Driver/pool signal | None | `unavailable` | Bounded retry outside transaction |
|
||||
| Unsupported profile | Initialization probe mismatch | None | `unsupported` | No; fail readiness |
|
||||
| Migration lock timeout | Named-lock acquisition returns timeout | None | `migration_lock_timeout` | Wait/back off according to startup policy |
|
||||
| Migration lock error | Named-lock acquisition returns error | None | `migration_lock_failed` | No blind retry; inspect connection state |
|
||||
|
||||
The adapter MUST classify by structured code and SQLSTATE where available, never by localized message
|
||||
text. Public HTTP/SSE/MCP responses must still pass through the repository's existing sanitized error
|
||||
helpers.
|
||||
|
||||
### 6.3 Retry rules
|
||||
|
||||
A retryable classification does not automatically make an operation safe to retry.
|
||||
|
||||
A retry loop MUST:
|
||||
|
||||
1. own the entire repository atomic operation;
|
||||
2. discard the failed transaction context;
|
||||
3. acquire a valid connection and begin a new transaction;
|
||||
4. preserve a stable operation or entity identity;
|
||||
5. use bounded attempts with jitter;
|
||||
6. stop on non-retryable classifications;
|
||||
7. reconcile `outcome_unknown` before issuing another write;
|
||||
8. emit structured diagnostics without credentials or raw SQL values.
|
||||
|
||||
MySQL explicitly recommends retrying the entire transaction after a deadlock. A lock wait timeout
|
||||
rolls back only the current statement by default, so explicit rollback is required to make the retry
|
||||
boundary independent of server configuration.
|
||||
|
||||
### 6.4 Reproducible two-connection deadlock probe
|
||||
|
||||
Use two physical connections, not two logical operations that might share one pool connection:
|
||||
|
||||
```sql
|
||||
CREATE TABLE conformance_deadlock (
|
||||
id INT PRIMARY KEY,
|
||||
value_no INT NOT NULL
|
||||
) ENGINE=InnoDB;
|
||||
INSERT INTO conformance_deadlock VALUES (1, 0), (2, 0);
|
||||
```
|
||||
|
||||
```text
|
||||
Connection A Connection B
|
||||
START TRANSACTION; START TRANSACTION;
|
||||
UPDATE ... WHERE id = 1; UPDATE ... WHERE id = 2;
|
||||
UPDATE ... WHERE id = 2; UPDATE ... WHERE id = 1;
|
||||
```
|
||||
|
||||
Exactly one transaction should become the deadlock victim. The harness asserts that the victim is
|
||||
classified as retryable, its whole transaction is retried with a new context, both logical updates
|
||||
occur once, and no partial result remains.
|
||||
|
||||
## 7. Migration ownership and DDL recovery
|
||||
|
||||
### 7.1 Why a normal transaction is insufficient
|
||||
|
||||
MySQL DDL statements commonly commit the current transaction implicitly before execution and often
|
||||
afterward. Atomic DDL protects one supported DDL statement; it does not make a sequence of DDL,
|
||||
data backfill, and schema-history updates one user transaction.
|
||||
|
||||
A MySQL migration runner therefore MUST model a migration as recoverable phases:
|
||||
|
||||
```text
|
||||
lock acquired
|
||||
-> current schema inspected
|
||||
-> intent/checkpoint recorded
|
||||
-> DDL phase applied and verified
|
||||
-> data phase applied in bounded transactions
|
||||
-> postconditions verified
|
||||
-> logical milestone recorded
|
||||
-> readiness allowed
|
||||
-> lock released
|
||||
```
|
||||
|
||||
A process crash at any arrow must have a deterministic resume or stop condition.
|
||||
|
||||
### 7.2 Ownership alternatives
|
||||
|
||||
| Option | Strengths | Failure modes | Decision |
|
||||
| ------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| Process-local mutex | Simple and useful for one process | Does not coordinate replicas | Rejected for external-backend migration ownership |
|
||||
| Row lock held in a transaction | Uses normal InnoDB locking | DDL implicit commit releases transaction ownership | Rejected as the sole DDL migration lock |
|
||||
| Lease row with owner and expiry | Survives pooled connections and can support takeover | Requires clock/expiry/fencing design; stale owner may continue | Candidate for scheduled jobs, not first migration mechanism |
|
||||
| MySQL named lock | Server-wide, exclusive, tied to physical session, released on disconnect | Must pin one connection; not transaction-scoped; one-server scope; undefined waiter order | Recommended first MySQL migration mutex, combined with durable history |
|
||||
| External coordinator | Can coordinate across database topologies | Adds an operational dependency outside the database contract | Deferred unless deployment topology requires it |
|
||||
|
||||
### 7.3 Recommended first mechanism
|
||||
|
||||
For a single writable MySQL primary, the migration runner SHOULD:
|
||||
|
||||
1. lease and pin one physical connection;
|
||||
2. acquire one application-and-database-specific named lock of at most 64 characters;
|
||||
3. distinguish acquired (`1`), timeout (`0`), and error (`NULL`);
|
||||
4. inspect a durable migration-history table after acquiring the lock;
|
||||
5. execute idempotent physical phases with explicit postcondition checks;
|
||||
6. record completion only after all postconditions pass;
|
||||
7. release the named lock explicitly in `finally`;
|
||||
8. close/discard the pinned connection if release cannot be confirmed.
|
||||
|
||||
Named locks are released when the session ends, not on commit or rollback. They are server-wide on one
|
||||
`mysqld`; topology and failover behavior must be validated before active-active support is advertised.
|
||||
A durable history/checkpoint table remains necessary because lock ownership alone says nothing about
|
||||
partially completed DDL.
|
||||
|
||||
### 7.4 Migration failure matrix
|
||||
|
||||
| Injection point | Required durable evidence | Restart behavior | Readiness |
|
||||
| ------------------------------- | --------------------------------------------- | ----------------------------------- | --------------------------------------------- |
|
||||
| Before lock | No intent | Retry lock acquisition | Not ready while required migration is pending |
|
||||
| After lock, before intent | No schema change | Reinspect and restart | Not ready |
|
||||
| After DDL, before checkpoint | Schema postcondition reveals DDL applied | Mark/continue only after validation | Not ready |
|
||||
| During data backfill | Bounded checkpoint identifies completed range | Resume from verified checkpoint | Not ready |
|
||||
| After data, before milestone | Postconditions prove completion | Record milestone idempotently | Not ready until recorded |
|
||||
| After milestone, before release | History proves complete | New owner verifies and proceeds | Ready if all required milestones pass |
|
||||
|
||||
## 8. SQLite-to-MySQL migration validation
|
||||
|
||||
An offline migration tool is required before database switching can be advertised. For each migrated
|
||||
domain it MUST provide a dry run and a post-import report.
|
||||
|
||||
### 8.1 Preflight
|
||||
|
||||
- verify supported SQLite and MySQL schema milestones;
|
||||
- validate every source JSON payload according to the chosen target representation;
|
||||
- detect names that collide under the target collation;
|
||||
- validate UTF-8 and maximum indexed byte lengths;
|
||||
- detect orphaned foreign keys even if the source connection had checks disabled;
|
||||
- validate timestamps and numeric ranges;
|
||||
- count source rows by table and logical domain;
|
||||
- refuse to mutate either database during dry run.
|
||||
|
||||
### 8.2 Import
|
||||
|
||||
- preserve application-generated IDs;
|
||||
- use deterministic batches and checkpoints;
|
||||
- import parents before children;
|
||||
- do not use replacement semantics to hide conflicts;
|
||||
- classify every rejected row with a stable reason;
|
||||
- keep encrypted credential ciphertext opaque and never log it;
|
||||
- stop on an unclassified difference.
|
||||
|
||||
### 8.3 Postconditions
|
||||
|
||||
- row counts match for every migrated table;
|
||||
- identity sets match exactly;
|
||||
- foreign-key orphan counts are zero;
|
||||
- canonical domain digests match for JSON-backed records;
|
||||
- list ordering and mapping resolution produce the same results;
|
||||
- a second dry run reports no pending changes;
|
||||
- SQLite remains unchanged and available for operator rollback until cutover is accepted.
|
||||
|
||||
## 9. Backend-neutral conformance catalog
|
||||
|
||||
Each test below runs the same repository fixture against SQLite and MySQL. MySQL-specific probes may
|
||||
assert error metadata internally, but the shared assertion compares only domain results and durable
|
||||
state.
|
||||
|
||||
### 9.1 Core CRUD and representation
|
||||
|
||||
| Test name | Fixture/action | Required assertion |
|
||||
| --------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------ |
|
||||
| `create_round_trips_domain_values` | Create Unicode, nullable, JSON, and timestamp fields | Parsed domain object equals normalized input |
|
||||
| `find_missing_distinguishes_absent_from_null` | Read an absent ID and a present nullable row | Results are distinct |
|
||||
| `update_missing_returns_not_found` | Update an absent ID | Stable `not_found` result |
|
||||
| `delete_is_idempotent_as_declared` | Delete the same ID twice | First and second results match the repository contract |
|
||||
| `json_round_trips_structurally` | Write equivalent JSON with different whitespace/order | Parsed values are equal; raw text is not asserted |
|
||||
| `timestamp_round_trips_in_utc` | Change MySQL session default before leasing a verified connection | Domain serialization remains canonical UTC |
|
||||
| `decimal_round_trips_without_float_loss` | Write precision/scale boundaries | Exact representation is unchanged |
|
||||
| `large_integer_does_not_cross_number_lossily` | Write beyond JavaScript safe integer range | String/bigint domain representation is exact |
|
||||
|
||||
### 9.2 Identity and collation
|
||||
|
||||
| Test name | Fixture/action | Required assertion |
|
||||
| ---------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| `id_is_byte_exact` | Create IDs differing only by case | Both remain distinct if the ID contract is binary |
|
||||
| `exact_name_lookup_is_case_sensitive` | Store `MASTER-LIGHT`, query exact lowercase | Exact lookup misses |
|
||||
| `insensitive_name_lookup_uses_declared_policy` | Query the same row through the named insensitive operation | One deterministic row is returned |
|
||||
| `unique_name_case_policy_is_explicit` | Insert case variants | Result matches the selected name policy on both backends |
|
||||
| `unique_name_accent_policy_is_explicit` | Insert accent variants | Result matches the selected policy |
|
||||
| `unique_violation_is_classified` | Concurrently create one identity | One wins; loser is `unique_violation` without backend text |
|
||||
| `nullable_unique_policy_is_explicit` | Insert two `NULL` logical keys | Result matches domain rule, not accidental index behavior |
|
||||
|
||||
### 9.3 Ordering and pagination
|
||||
|
||||
| Test name | Fixture/action | Required assertion |
|
||||
| --------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------ |
|
||||
| `list_uses_unique_final_tiebreaker` | Insert rows with identical primary sort values | Repeated list order is identical and ID-ordered |
|
||||
| `pagination_has_no_gaps_or_duplicates` | Traverse small pages across tied rows | Union equals full ID set; page intersections are empty |
|
||||
| `nullable_sort_position_is_fixed` | Mix `NULL` and non-`NULL` values | `NULL` appears at the contract-defined end |
|
||||
| `cursor_predicate_matches_sort_tuple` | Page forward through mixed sort keys | Every row appears exactly once in declared order |
|
||||
| `concurrent_insert_pagination_behavior_is_declared` | Insert between page reads | Result matches snapshot or documented live-page policy |
|
||||
|
||||
### 9.4 Writes and affected rows
|
||||
|
||||
| Test name | Fixture/action | Required assertion |
|
||||
| ------------------------------------------- | ------------------------------------------ | -------------------------------------------------- |
|
||||
| `same_value_update_is_not_missing` | Update an existing row to identical values | `unchanged` or declared success, never `not_found` |
|
||||
| `same_value_result_ignores_found_rows_mode` | Run fixture with both connection modes | Domain result is identical |
|
||||
| `compare_update_detects_stale_version` | Two writers use one old version | One succeeds; one returns `conflict` |
|
||||
| `batch_count_uses_contract_definition` | Mix changed and unchanged matches | Count means the same thing on both backends |
|
||||
| `upsert_preserves_identity_and_children` | Upsert parent with a child row | ID, immutable fields, and child survive |
|
||||
| `insert_only_never_silently_updates` | Repeat insert-only identity | Second call is `unique_violation` |
|
||||
|
||||
### 9.5 Transactions, isolation, and failure injection
|
||||
|
||||
| Test name | Fixture/action | Required assertion |
|
||||
| ----------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `related_changes_commit_atomically` | Update parent and children | All postconditions commit together |
|
||||
| `related_changes_roll_back_atomically` | Inject a child constraint failure | All tables equal pre-operation state |
|
||||
| `stable_snapshot_behavior_is_declared` | Read, commit a concurrent update, then read in the same operation | Result follows the operation's declared snapshot/current-read policy |
|
||||
| `range_insert_visibility_is_declared` | Read a range while another transaction inserts a matching row | Later visibility matches the declared snapshot/live policy |
|
||||
| `read_modify_write_prevents_lost_update` | Two transactions read one version and attempt distinct updates | One declared winner; loser conflicts/retries without overwriting |
|
||||
| `independent_writers_preserve_atomic_effects` | Two transactions update different identities concurrently | Both logical effects commit; no contract depends on backend lock order |
|
||||
| `deadlock_retries_whole_operation` | Two physical connections lock in opposite order | One victim; final logical effect occurs once |
|
||||
| `lock_timeout_discards_context` | Hold a row lock past timeout | Explicit rollback; old context rejects work |
|
||||
| `duplicate_and_foreign_key_errors_are_distinct` | Trigger each constraint | Stable distinct classes |
|
||||
| `disconnect_before_send_is_unavailable` | Fail connection before dispatch | Safe bounded retry is permitted |
|
||||
| `disconnect_during_commit_is_outcome_unknown` | Drop connection at commit boundary | No blind retry; reconciliation is required |
|
||||
| `retry_uses_stable_operation_identity` | Fail first attempt after durable write | At most one logical effect exists |
|
||||
|
||||
### 9.6 Migration and readiness
|
||||
|
||||
| Test name | Fixture/action | Required assertion |
|
||||
| --------------------------------------- | ------------------------------------------- | -------------------------------------------------- |
|
||||
| `only_one_instance_owns_migration` | Two backend instances acquire one name | Exactly one executes migration phases |
|
||||
| `lock_timeout_is_not_reported_as_ready` | Hold migration lock from another connection | Startup waits/fails with classified state |
|
||||
| `disconnect_releases_named_lock` | Terminate owner connection | Another instance can acquire and reinspect |
|
||||
| `ddl_checkpoint_recovers_after_crash` | Stop after DDL before history update | Restart detects postcondition and continues safely |
|
||||
| `backfill_resumes_without_duplication` | Stop between deterministic batches | Completed rows are neither skipped nor duplicated |
|
||||
| `partial_migration_blocks_readiness` | Leave required milestone incomplete | Health may be alive; readiness is false |
|
||||
| `completed_history_is_idempotent` | Start against fully migrated schema | No DDL/data mutation occurs |
|
||||
|
||||
## 10. First-slice acceptance profile: combos and model mappings
|
||||
|
||||
This section specializes the general catalog for the candidate first slice discussed in #8075 and
|
||||
implemented experimentally in Draft PR #8757. It does not approve that runtime PR.
|
||||
|
||||
### 10.1 Contract decisions required before adapter code
|
||||
|
||||
| Decision | Current evidence | Required resolution |
|
||||
| --------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
|
||||
| Combo ID | Application UUID | Preserve as byte-exact text/binary identity |
|
||||
| Combo name uniqueness | SQLite unique name; exact and insensitive reads differ | Select explicit uniqueness collation independently from insensitive fallback |
|
||||
| Combo list | `sort_order`, then `name NOCASE` | Add `id` as final tie-breaker and define Unicode name order |
|
||||
| Next sort order | `MAX(sort_order) + 1` | Replace race-prone read-then-insert with an atomic allocation or retryable unique invariant |
|
||||
| Reorder | One SQLite transaction updates all parseable rows | Define concurrent reorder serialization and all-or-nothing behavior |
|
||||
| Corrupt combo JSON | Reads/resolution skip malformed payloads | Decide whether MySQL schema can represent malformed legacy rows during migration |
|
||||
| Mapping order | `priority DESC, created_at ASC` | Add `id ASC` final tie-breaker |
|
||||
| Mapping delete | Boolean from affected rows | Preserve `true` then `false` behavior independent of found-rows mode |
|
||||
| Combo delete | Foreign key cascade removes mappings | Preserve one-operation atomic cascade |
|
||||
| Timestamps | Application ISO strings | Preserve canonical UTC text or define an exact typed conversion |
|
||||
|
||||
### 10.2 Required combo fixtures
|
||||
|
||||
The shared fixture MUST include:
|
||||
|
||||
- combo names `Alpha`, `alpha`, `Résumé`, and `resume` to exercise selected collation policy;
|
||||
- three combos with the same requested `sortOrder` to exercise the unique final order;
|
||||
- one missing ID for update and delete results;
|
||||
- one payload with explicit JSON `null` and one with a missing member;
|
||||
- one intentionally malformed legacy payload if compatibility requires it;
|
||||
- mappings with identical `priority` and `createdAt` but different IDs;
|
||||
- enabled, disabled, inactive-target, and corrupt-target mappings;
|
||||
- one combo with at least two dependent mappings for cascade verification.
|
||||
|
||||
### 10.3 Required combo assertions
|
||||
|
||||
A MySQL implementation cannot claim the first slice complete until the shared harness proves:
|
||||
|
||||
1. application UUIDs and ISO timestamps round-trip unchanged;
|
||||
2. exact and insensitive combo-name lookups remain distinct operations;
|
||||
3. uniqueness follows the approved name policy, not server defaults;
|
||||
4. combo and mapping lists have a total deterministic order;
|
||||
5. every offset page is a contiguous slice of that order;
|
||||
6. update of a missing combo/mapping returns `null`;
|
||||
7. first delete returns `true`, repeated delete returns `false`;
|
||||
8. reorder filters unknown/duplicate requested IDs exactly as the accepted contract specifies;
|
||||
9. reorder either commits every intended row or none;
|
||||
10. mapping resolution uses the deterministic order and skips disabled, inactive, and malformed targets;
|
||||
11. deleting a combo atomically removes all dependent mappings;
|
||||
12. errors are classified without raw MySQL messages;
|
||||
13. SQLite starts without loading a MySQL dependency;
|
||||
14. no external-backend support is advertised by the presence of this slice alone.
|
||||
|
||||
### 10.4 Concurrency probes specific to the slice
|
||||
|
||||
#### Concurrent combo creation
|
||||
|
||||
Two connections create different UUIDs with the same contract-equivalent name. Exactly one succeeds;
|
||||
the other receives `unique_violation`. If case/accent variants are allowed by the approved policy,
|
||||
both succeed and exact lookup returns the correct identity.
|
||||
|
||||
#### Concurrent sort allocation
|
||||
|
||||
Two connections create combos without an explicit sort order. The final values MUST follow the
|
||||
contract without duplicates caused by both transactions reading the same `MAX(sort_order)`. The
|
||||
implementation may serialize allocation, use a separate sequence, or retry a protected invariant;
|
||||
the contract must not require one specific SQL mechanism.
|
||||
|
||||
#### Concurrent reorder
|
||||
|
||||
Two connections reorder the same set in opposite orders. The accepted outcome MUST be one complete
|
||||
order or the other, never a mixed sequence or mismatched JSON/column `sortOrder`. The loser may wait,
|
||||
return conflict, or retry according to the approved contract.
|
||||
|
||||
#### Delete versus mapping creation
|
||||
|
||||
One connection deletes a combo while another creates a mapping to it. The final state MUST be either
|
||||
an existing combo with a valid mapping or no combo and no mapping. An orphan mapping is forbidden.
|
||||
|
||||
## 11. Implementation gate checklist
|
||||
|
||||
A MySQL adapter PR for any domain MUST NOT start until reviewers can answer all applicable items:
|
||||
|
||||
- [ ] Identity, case, accent, and collation semantics are explicit.
|
||||
- [ ] Every list has a complete order, `NULL` position, and unique tie-breaker.
|
||||
- [ ] Missing, unchanged, conflict, and delete results are distinguishable.
|
||||
- [ ] Every write is classified as insert-only, identity-preserving upsert, or replacement.
|
||||
- [ ] ID generation and idempotency ownership are explicit.
|
||||
- [ ] JSON and temporal representations are selected with migration compatibility in mind.
|
||||
- [ ] Error codes map to the backend-neutral taxonomy.
|
||||
- [ ] Retry ownership and maximum scope are explicit.
|
||||
- [ ] Migration mutex, durable checkpoints, and readiness rules are approved.
|
||||
- [ ] SQLite and MySQL fixtures run through one behavior harness.
|
||||
- [ ] Offline migration preflight and postconditions exist before cutover is advertised.
|
||||
- [ ] SQLite remains the zero-configuration default and clean startup path.
|
||||
|
||||
## 12. Reference sources
|
||||
|
||||
### 12.1 OmniRoute sources
|
||||
|
||||
- `docs/architecture/persistence-backend-boundary.md`
|
||||
- `docs/architecture/sqlite-coupling-inventory.md`
|
||||
- `src/lib/db/combos.ts`
|
||||
- `src/lib/db/modelComboMappings.ts`
|
||||
- `src/lib/db/migrations/001_initial_schema.sql`
|
||||
- `src/lib/db/migrations/010_model_combo_mappings.sql`
|
||||
- `src/lib/db/migrations/020_combo_sort_order.sql`
|
||||
|
||||
### 12.2 MySQL 8.0 reference manual
|
||||
|
||||
- [Character sets and collations](https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/charset.html)
|
||||
- [CREATE TABLE](https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/create-table.html)
|
||||
- [UPDATE](https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/update.html)
|
||||
- [INSERT ... ON DUPLICATE KEY UPDATE](https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/insert-on-duplicate.html)
|
||||
- [Information functions](https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/information-functions.html)
|
||||
- [The JSON data type](https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/json.html)
|
||||
- [InnoDB transaction isolation](https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/innodb-transaction-isolation-levels.html)
|
||||
- [InnoDB error handling](https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/innodb-error-handling.html)
|
||||
- [Handling deadlocks](https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/innodb-deadlocks-handling.html)
|
||||
- [Statements that cause an implicit commit](https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/implicit-commit.html)
|
||||
- [Locking functions](https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/locking-functions.html)
|
||||
- [InnoDB limits](https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/innodb-limits.html)
|
||||
|
||||
### 12.3 SQLite references
|
||||
|
||||
- [ON CONFLICT](https://sqlite.org/lang_conflict.html)
|
||||
- [`NULL` handling](https://sqlite.org/nulls.html)
|
||||
- [Transactions](https://sqlite.org/lang_transaction.html)
|
||||
- [SELECT and ordering](https://sqlite.org/lang_select.html#orderby)
|
||||
|
||||
## 13. Open decisions
|
||||
|
||||
This specification deliberately leaves the following decisions to the accepted first-slice design:
|
||||
|
||||
1. the exact collation and normalization policy for combo names;
|
||||
2. the typed or text representation of combo JSON in MySQL;
|
||||
3. the repository result type for an existing same-value update;
|
||||
4. the isolation level selected by the backend profile;
|
||||
5. the concurrency mechanism for sort-order allocation and reorder;
|
||||
6. the physical MySQL migration schema and durable checkpoint format;
|
||||
7. the exact retry budget and backoff policy;
|
||||
8. the topology boundary within which a MySQL named migration lock is sufficient.
|
||||
|
||||
These are not adapter implementation details. Each changes observable behavior or operational
|
||||
correctness and therefore requires explicit review before runtime support proceeds.
|
||||
226
docs/architecture/sqlite-coupling-inventory.md
Normal file
226
docs/architecture/sqlite-coupling-inventory.md
Normal file
@@ -0,0 +1,226 @@
|
||||
---
|
||||
title: "SQLite coupling inventory"
|
||||
status: measured-snapshot
|
||||
lastUpdated: 2026-07-23
|
||||
---
|
||||
|
||||
# SQLite coupling inventory
|
||||
|
||||
- **Tracking issue:** [#8075](https://github.com/diegosouzapw/OmniRoute/issues/8075)
|
||||
- **Snapshot revision:** `9a3b605f3420ae3ab08bd93d6443034f03a1bcbc`
|
||||
- **Scanned-corpus SHA-256:** `72334620a7a18a42bcede1643fb2fdf95da6eae9ffa66a891ae14ed633ad43f6`
|
||||
- **Purpose:** Measure the current persistence cut lines before proposing repository interfaces
|
||||
- **Runtime impact:** None; this document and its audit script do not change database behavior
|
||||
|
||||
## How to reproduce
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
node scripts/check/audit-sqlite-coupling.mjs
|
||||
node scripts/check/audit-sqlite-coupling.mjs --json
|
||||
node --test scripts/check/audit-sqlite-coupling.test.mjs
|
||||
```
|
||||
|
||||
The script reads tracked files from Git, scans non-test source under `src/`, `open-sse/`,
|
||||
`electron/`, and `bin/`, and scans migration SQL under `src/lib/db/migrations/`. It excludes the
|
||||
top-level test tree, co-located test directories, test/spec source files, and paths outside those
|
||||
configured source roots (including documentation and scripts).
|
||||
|
||||
The script refuses to run if tracked files in those source roots differ from `HEAD`. It reports
|
||||
both the audit-tool revision and a SHA-256 over the ordered path/content corpus. The snapshot above
|
||||
was taken from the listed source revision; this PR changes only excluded documentation and script
|
||||
paths, so rerunning from the clean PR branch produces the same corpus digest.
|
||||
|
||||
This is a **lexical inventory**, not a TypeScript or SQL semantic analysis:
|
||||
|
||||
- counts are occurrences of defined patterns, not counts of distinct SQL statements;
|
||||
- adapter-call and direct-singleton patterns mask comments and literal contents first;
|
||||
- template-literal contents, including embedded expressions, are excluded from those code-syntax
|
||||
counts;
|
||||
- the lightweight masker is not a JavaScript parser, so unusual regular-expression literal syntax
|
||||
can still require manual review;
|
||||
- comments and string literals can contribute to dialect-signal counts, which intentionally search
|
||||
raw text for embedded SQL;
|
||||
- a `.prepare()` match outside `src/lib/db/` is a review lead, not proof that the call should move;
|
||||
- calls hidden behind a differently named wrapper may not be counted;
|
||||
- file counts are deduplicated, while occurrence counts are not.
|
||||
|
||||
The JSON output includes every matching path so reviewers can inspect or reclassify individual
|
||||
results rather than trusting totals alone.
|
||||
|
||||
## Snapshot scope
|
||||
|
||||
At the recorded revision, the script scanned:
|
||||
|
||||
- 3,830 tracked non-test source files;
|
||||
- 129 migration SQL files.
|
||||
|
||||
The source-file count is intentionally broad because the goal is to find persistence coupling that
|
||||
has escaped the nominal database directory, including CLI and proxy/runtime code.
|
||||
|
||||
## Boundary signals
|
||||
|
||||
| Signal | Files | Occurrences |
|
||||
| -------------------------------------------------------------------------------- | ----: | ----------: |
|
||||
| Direct `getDbInstance()` call syntax outside comments/literals and `src/lib/db/` | 45 | 150 |
|
||||
| `localDb` import consumers | 211 | — |
|
||||
| `SqliteAdapter` type consumers outside comments/literals and `src/lib/db/` | 3 | — |
|
||||
|
||||
The `localDb` barrel already gives many callers a domain-function seam, but
|
||||
`src/lib/localDb.ts` remains a re-export layer rather than a backend contract. The 45 direct
|
||||
singleton consumers are the clearest first review set because they bypass that logical seam and
|
||||
hold an adapter-shaped handle directly.
|
||||
|
||||
The three non-test source files outside `src/lib/db/` that mention the `SqliteAdapter` type in code
|
||||
syntax are:
|
||||
|
||||
- `src/app/api/db-backups/import/route.ts`;
|
||||
- `src/lib/compliance/index.ts`;
|
||||
- `src/lib/compliance/noLog.ts`.
|
||||
|
||||
These are not equivalent migration tasks. Backup import is capability-specific; compliance
|
||||
persistence may be portable domain state. The future boundary should classify them rather than
|
||||
moving all three mechanically.
|
||||
|
||||
## Adapter-shaped call syntax
|
||||
|
||||
| Signal | Occurrences | Files | Outside `src/lib/db/` occurrences | Outside files |
|
||||
| ----------------- | ----------: | ----: | --------------------------------: | ------------: |
|
||||
| `.prepare()` | 1,219 | 163 | 252 | 52 |
|
||||
| `.transaction()` | 62 | 40 | 12 | 10 |
|
||||
| `.immediate()` | 3 | 3 | 0 | 0 |
|
||||
| `.pragma()` | 39 | 11 | 6 | 4 |
|
||||
| `.backup()` | 6 | 5 | 3 | 3 |
|
||||
| `.checkpoint()` | 0 | 0 | 0 | 0 |
|
||||
| `lastInsertRowid` | 15 | 7 | 1 | 1 |
|
||||
|
||||
This table shows why `SqliteAdapter` is a SQLite runtime compatibility layer rather than a portable
|
||||
backend abstraction. Its synchronous statement and transaction shape is widely used, and some of
|
||||
that shape is visible outside the nominal database layer.
|
||||
|
||||
The top direct `getDbInstance()` consumers outside `src/lib/db/` at this revision are:
|
||||
|
||||
| File | Occurrences |
|
||||
| -------------------------------------------------- | ----------: |
|
||||
| `src/lib/proxySubscription/subscriptionService.ts` | 12 |
|
||||
| `src/lib/semanticCache.ts` | 10 |
|
||||
| `src/lib/usage/callLogs.ts` | 9 |
|
||||
| `src/lib/cloudAgent/db.ts` | 8 |
|
||||
| `src/lib/memory/store.ts` | 8 |
|
||||
| `src/lib/memory/vectorStore.ts` | 8 |
|
||||
| `src/lib/modelsDevSync.ts` | 8 |
|
||||
| `src/lib/gamification/badges.ts` | 5 |
|
||||
| `src/lib/memory/retrieval.ts` | 5 |
|
||||
| `src/lib/pricingSync.ts` | 5 |
|
||||
| `src/lib/skills/registry.ts` | 5 |
|
||||
| `src/lib/usage/usageHistory.ts` | 5 |
|
||||
|
||||
The list spans control-plane configuration, usage/audit data, cache, memory/vector search, skills,
|
||||
gamification, and CLI/provider support. A single generic SQL adapter would preserve this spread;
|
||||
domain repositories provide a way to reduce it slice by slice.
|
||||
|
||||
## SQLite dialect and lifecycle signals
|
||||
|
||||
| Signal | Occurrences | Files |
|
||||
| --------------------- | ----------: | ----: |
|
||||
| `PRAGMA` text | 97 | 41 |
|
||||
| `sqlite_master` | 14 | 11 |
|
||||
| `BEGIN IMMEDIATE` | 2 | 2 |
|
||||
| `INSERT OR REPLACE` | 83 | 45 |
|
||||
| `AUTOINCREMENT` | 34 | 24 |
|
||||
| `datetime('now')` | 171 | 68 |
|
||||
| `VACUUM` | 39 | 10 |
|
||||
| `wal_checkpoint` | 13 | 7 |
|
||||
| `fts5` | 43 | 8 |
|
||||
| `vec0` | 7 | 1 |
|
||||
| `last_insert_rowid()` | 1 | 1 |
|
||||
|
||||
These values are text signals and include comments where present. They are useful for locating
|
||||
portability work, not for estimating implementation effort by multiplication.
|
||||
|
||||
Verified high-coupling areas include:
|
||||
|
||||
- `src/lib/db/core.ts`: singleton lifecycle, SQLite file paths, WAL checkpoint, recovery, schema,
|
||||
compaction, and backup creation;
|
||||
- `src/lib/db/migrationRunner.ts`: numbered SQL migration execution, `sqlite_master`,
|
||||
`PRAGMA table_info`, transaction behavior, and optional FTS5 handling;
|
||||
- `src/lib/db/optimizationSettings.ts`: page/cache settings, auto-vacuum, WAL transitions, and
|
||||
`VACUUM`;
|
||||
- `src/lib/db/backup.ts`: database backup and restore lifecycle;
|
||||
- `src/lib/db/schemaColumns.ts`: SQLite schema introspection and compatibility columns;
|
||||
- `src/lib/memory/vectorStore.ts` and `src/lib/memory/retrieval.ts`: `vec0` and FTS5 behavior;
|
||||
- `src/lib/db/adapters/`: compatibility implementations for the supported SQLite runtimes.
|
||||
|
||||
These areas should not be forced through a lowest-common-denominator repository interface. They
|
||||
need explicit SQLite capabilities or separate backend implementations.
|
||||
|
||||
## Migration coupling
|
||||
|
||||
The snapshot contains 129 tracked migration SQL files. `src/lib/db/migrationRunner.ts` does more
|
||||
than execute ordered files: it owns migration discovery, version history, duplicate-version safety,
|
||||
schema probes, FTS5 capability checks, pre-migration safety, and SQLite transaction execution.
|
||||
|
||||
Consequently:
|
||||
|
||||
- another SQL dialect cannot safely reuse the migration files unchanged;
|
||||
- external backends need their own migration implementation and schema history;
|
||||
- logical migration milestones may be shared, but physical SQL and capability probes remain
|
||||
backend-specific;
|
||||
- multi-replica operation requires migration ownership or locking before an external backend is
|
||||
considered ready.
|
||||
|
||||
## Recommended cut lines
|
||||
|
||||
### 1. Keep SQLite runtime compatibility intact
|
||||
|
||||
Do not replace `SqliteAdapter` or the driver cascade in the first repository PR. Keep file recovery,
|
||||
WAL, backup, optimization, FTS5, and vector behavior behind the current SQLite implementation.
|
||||
|
||||
### 2. Start with direct singleton consumers
|
||||
|
||||
Use the 45-file direct-consumer list as the initial review queue. Classify each file as:
|
||||
|
||||
- portable domain state;
|
||||
- backend-specific maintenance or search;
|
||||
- process-local or rebuildable state;
|
||||
- legacy access that should call an existing domain module.
|
||||
|
||||
Classification must precede interface design. A path appearing in the inventory is not, by itself,
|
||||
a mandate to create a repository.
|
||||
|
||||
### 3. Prove repositories with SQLite first
|
||||
|
||||
For one bounded domain:
|
||||
|
||||
1. define behavior-oriented repository operations;
|
||||
2. adapt current SQLite queries behind that repository;
|
||||
3. run behavior and transaction conformance tests against SQLite;
|
||||
4. migrate callers without changing the default runtime;
|
||||
5. only then implement the same repository for an external backend.
|
||||
|
||||
### 4. Separate portable control-plane state from capability-specific data
|
||||
|
||||
Provider connections, API keys, combos, and routing configuration are candidates for the first
|
||||
portable slice, subject to maintainer approval and a table-ownership review. Memory vector search,
|
||||
SQLite file backup/recovery, and database optimization are poor first slices because their behavior
|
||||
is deliberately SQLite-specific.
|
||||
|
||||
### 5. Treat usage, quota, affinity, and audit as a later coordination slice
|
||||
|
||||
These domains have concurrency and volume semantics beyond CRUD. Their repository contracts should
|
||||
be designed together with multi-replica transaction, lease, retention, and failure-mode tests rather
|
||||
than copied mechanically from current SQL.
|
||||
|
||||
## What this inventory does not decide
|
||||
|
||||
This inventory does not:
|
||||
|
||||
- approve PostgreSQL or MySQL support;
|
||||
- define repository TypeScript interfaces;
|
||||
- choose the first table or domain to migrate;
|
||||
- claim every lexical match is a defect;
|
||||
- claim the current module boundaries are ineffective;
|
||||
- change SQLite, migrations, backup, search, or runtime behavior.
|
||||
|
||||
Its purpose is to make the next design discussion evidence-based and reproducible.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user