Merge remote-tracking branch 'origin/release/v3.8.45' into pr6231

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-05 17:32:26 -03:00
302 changed files with 11439 additions and 1650 deletions

View File

@@ -187,8 +187,13 @@ OMNIROUTE_USE_TURBOPACK=1
# Hostname/bind address for the Next.js server.
# Used by: scripts/dev/run-next.mjs (HOST), Playwright runner (HOSTNAME).
# Default: 0.0.0.0 (HOST) / 127.0.0.1 (HOSTNAME inside tests).
# NOTE: Do NOT use `HOSTNAME` — it is a POSIX shell variable automatically set to
# the machine name by bash/zsh. The .env loader cannot override it (first-wins
# semantics). Use OMNIROUTE_SERVER_HOST instead for `omniroute serve`.
# See: https://github.com/diegosouzapw/OmniRoute/issues/6194
#HOST=0.0.0.0
#HOSTNAME=127.0.0.1
#OMNIROUTE_SERVER_HOST=0.0.0.0
# Environment mode — affects Next.js behavior, logging verbosity, and caching.
# Values: production | development | Default: production
@@ -1516,6 +1521,11 @@ APP_LOG_TO_FILE=true
# PROXY_AUTO_REMOVE=false
# Consecutive failures before an auto-remove fires. Default: 3.
# PROXY_AUTO_REMOVE_AFTER=3
# Let automated reachability probes (the scheduler + the "Test All" button) WRITE
# a proxy's status. Default "false": probes are read-only and never deactivate a
# proxy — only the operator sets active/inactive (a flaky probe must not strand an
# assigned proxy; #6246). Set "true" to restore the legacy test-and-set behaviour.
# PROXY_HEALTH_AUTO_DEACTIVATE=false
# Allow OAuth and provider validation flows to bypass a pinned proxy and connect
# directly when proxy reachability pre-checks fail. Default: false.
@@ -1698,6 +1708,10 @@ APP_LOG_TO_FILE=true
# Routing-decision log verbosity: 0 silences, higher values log more bypass/route
# decisions (src/mitm/server.cjs, _internal/bypass.cjs).
# MITM_VERBOSE=1
# Strip the leading `sudo` from MITM cert-trust commands (src/mitm/systemCommands.ts) —
# for root-less / user-namespaced deployments (e.g. rootless Docker/Podman)
# where the operator trusts the CA manually (e.g. via Node's extra-CA-certs mechanism).
# OMNIROUTE_NO_SUDO=0
# ── 1Proxy egress pool ──
# Used by: src/lib/oneproxySync.ts — fetches proxy nodes from the OmniRoute

View File

@@ -506,7 +506,15 @@ jobs:
build:
name: Build
runs-on: ubuntu-latest
# Dynamic runner: when the release captain flips the USE_VPS_RUNNER repo var to
# 'true' (scripts/vps/release-runner-up.sh does it after the self-hosted VM is
# online), the heavy jobs run on the dedicated 32-core VPS runners (label
# omni-release) instead of queueing on the 20-concurrent-job hosted pool.
# Safety: fork PRs NEVER reach the self-hosted runner — the expression falls
# back to ubuntu-latest unless the PR head repo is this repository (push /
# dispatch events are own-origin by definition). Any failure path (VM down,
# var unset/false) also falls back to ubuntu-latest.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
needs: changes
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
steps:
@@ -519,14 +527,21 @@ jobs:
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- name: Cache Next.js build cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
with:
path: .build/next/cache
key: nextjs-${{ runner.os }}-node-${{ env.CI_NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-${{ hashFiles('src/**/*', 'open-sse/**/*', 'db/**/*', 'next.config.mjs', 'tsconfig*.json', 'postcss.config.*', 'tailwind.config.*') }}
restore-keys: |
nextjs-${{ runner.os }}-node-${{ env.CI_NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-
# NOTE: the webpack `.build/next/cache` actions/cache step was removed with the
# Turbopack switch below — Turbopack does not read/write the webpack cache dir
# (its persistent FS cache is still experimental and intentionally NOT enabled),
# so restoring the old ~0.5 GB webpack cache would only waste download time.
# Rolling back to webpack = revert this commit (cache step comes back with it).
#
# Turbopack production build (Next 16, stable): benchmarked 1.9× faster than the
# webpack pass (9min0s vs 17min15s on a 32-core box; multi-core Rust vs webpack's
# single-threaded compile). Standalone output smoke-validated (server boots,
# /api/monitoring/health 200). Downstream jobs (e2e ×9, package-artifact,
# electron-package-smoke) consume this artifact, so a green run here validates
# the Turbopack artifact end-to-end.
- run: npm run build
env:
OMNIROUTE_USE_TURBOPACK: "1"
- name: Archive Next.js build for downstream jobs
# Use tar so the archive preserves paths relative to CWD (.build/next/...).
# upload-artifact path-stripping is ambiguous when exclude patterns are used;
@@ -614,9 +629,16 @@ jobs:
test-unit:
name: Unit Tests (${{ matrix.shard }}/8)
runs-on: ubuntu-latest
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest).
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
timeout-minutes: 25
needs: build
# needs: changes (not build) — this job never downloads the next-build artifact;
# gating it on Build only serialized ~20min of wall-clock for nothing. Jobs that
# DO consume the artifact (e2e, package-artifact, electron-package-smoke) keep
# needs: build. The `if` mirrors Build's own skip condition so docs-only PRs
# still skip the suite.
needs: changes
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
strategy:
fail-fast: false
matrix:
@@ -662,9 +684,12 @@ jobs:
test-vitest:
name: Vitest (MCP / autoCombo / UI components)
runs-on: ubuntu-latest
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest).
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
timeout-minutes: 15
needs: build
# needs: changes (not build) — no artifact consumed; see test-unit note.
needs: changes
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
@@ -952,7 +977,9 @@ jobs:
name: Integration Tests (${{ matrix.shard }}/2)
runs-on: ubuntu-latest
timeout-minutes: 15
needs: build
# needs: changes (not build) — no artifact consumed; see test-unit note.
needs: changes
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
strategy:
fail-fast: false
matrix:
@@ -979,7 +1006,9 @@ jobs:
test-security:
name: Security Tests
runs-on: ubuntu-latest
needs: build
# needs: changes (not build) — no artifact consumed; see test-unit note.
needs: changes
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long

View File

@@ -60,22 +60,25 @@ RUN --mount=type=cache,target=/root/.npm \
&& npm rebuild better-sqlite3 \
&& node -e "require('better-sqlite3')(':memory:').close()"
# Build with webpack (stable). Turbopack hit a non-recoverable internal panic on this
# Next.js version during the v3.8.27 release build — TurbopackInternalError "entered
# unreachable code: there must be a path to a root" in ImportTracer::get_traces, on both
# linux/amd64 and linux/arm64. Webpack is the proven engine (build:release / VPS / CI Build
# all green). Re-enable Turbopack (=1) once the upstream tracer bug is fixed.
# Build with Turbopack (stable in Next 16, the repo default). The v3.8.27-era
# TurbopackInternalError panic ("entered unreachable code: there must be a path to a
# root" in ImportTracer::get_traces) no longer reproduces on Next 16.2.9 — validated
# 2026-07-05 with clean amd64 (12min14s, image smoke-tested: /api/monitoring/health
# 200) and arm64 (qemu, exit 0, zero panic strings) builds. Turbopack cut the bare
# build from 17min to 9min on the same 32-core box. Webpack stays available as the
# escape hatch: `--build-arg`/-e OMNIROUTE_USE_TURBOPACK=0.
# See docs/ops/QUALITY_GATE_PLAYBOOK.md Parte 6.
ENV OMNIROUTE_USE_TURBOPACK=0
ENV OMNIROUTE_USE_TURBOPACK=1
# Raise the V8 heap ceiling for the build. The webpack production optimization
# pass (forced above since Turbopack panics) needs more than V8's default ceiling
# (~2 GB) for a codebase this size; a memory-constrained Docker build otherwise
# dies with "FATAL ERROR: ... JavaScript heap out of memory" during the builder
# stage (#4076). NODE_OPTIONS propagates to the spawned `next build` child
# (build-next-isolated.mjs → resolveNextBuildEnv spreads process.env). Build-only;
# the runtime heap is set separately on the runner stage (OMNIROUTE_MEMORY_MB).
# Override for hosts with more/less RAM: `--build-arg OMNIROUTE_BUILD_MEMORY_MB=6144`.
# pass needs more than V8's default ceiling (~2 GB) for a codebase this size; a
# memory-constrained Docker build otherwise dies with "FATAL ERROR: ... JavaScript
# heap out of memory" during the builder stage (#4076). Turbopack's compile is
# native (Rust) and less V8-heap-bound, but the prerender/export phase still runs
# on V8, so keep the ceiling. NODE_OPTIONS propagates to the spawned `next build`
# child (build-next-isolated.mjs → resolveNextBuildEnv spreads process.env).
# Build-only; the runtime heap is set separately on the runner stage
# (OMNIROUTE_MEMORY_MB). Override: `--build-arg OMNIROUTE_BUILD_MEMORY_MB=6144`.
ARG OMNIROUTE_BUILD_MEMORY_MB=4096
ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_BUILD_MEMORY_MB}"

View File

@@ -380,18 +380,61 @@ function resolveLivenessUrl(options = {}) {
return `http://${formatHostForUrl(host || "127.0.0.1")}:${dashboardPort}/api/health/degradation`;
}
async function probeUrl(url) {
try {
const response = await fetchWithTimeout(url);
return { ok: response.ok, status: response.status };
} catch {
return { ok: false, status: 0 };
}
}
async function checkServerLiveness(options = {}) {
const url = resolveLivenessUrl(options);
try {
const response = await fetchWithTimeout(url);
if (!response.ok) {
return warn("Server liveness", `Server responded with HTTP ${response.status}`, { url });
}
return ok("Server liveness", "Server health endpoint is reachable", { url });
} catch {
return warn("Server liveness", "Server health endpoint is not reachable", { url });
// First attempt: configured health endpoint (may require auth token).
const primary = await probeUrl(url);
if (primary.ok) {
return ok("Server liveness", "Server health endpoint is reachable", { url, status: primary.status });
}
// #6162: /api/health and /api/health/degradation require a management token.
// When unauthenticated, fall back to probing a publicly served static asset
// (favicon.ico) to confirm the Next.js server is alive and reachable.
// Derive the fallback URL from the primary URL (preserving protocol/host/port)
// so custom liveness URL configurations are honored. Fall back to defaults
// only if the primary URL can't be parsed.
let fallbackUrl;
try {
const parsed = new URL(url);
parsed.pathname = "/favicon.ico";
parsed.search = "";
parsed.hash = "";
fallbackUrl = parsed.toString();
} catch {
const port = parsePort(process.env.PORT || "20128", 20128);
const dashboardPort = parsePort(process.env.DASHBOARD_PORT || String(port), port);
const host = String(options.livenessHost || process.env.OMNIROUTE_DOCTOR_HOST || "127.0.0.1")
.trim()
.replace(/^https?:\/\//, "")
.replace(/\/.*$/, "");
fallbackUrl = `http://${formatHostForUrl(host || "127.0.0.1")}:${dashboardPort}/favicon.ico`;
}
const fallback = await probeUrl(fallbackUrl);
if (fallback.ok) {
return ok(
"Server liveness",
`Server reachable (health endpoint returned ${primary.status}, likely requires MANAGEMENT_TOKEN)`,
{ primaryUrl: url, primaryStatus: primary.status, fallbackUrl, fallbackStatus: fallback.status }
);
}
return warn(
"Server liveness",
`Server health endpoint returned HTTP ${primary.status || "no-response"} and fallback probe failed`,
{ primaryUrl: url, primaryStatus: primary.status, fallbackUrl, fallbackStatus: fallback.status }
);
}
export async function collectDoctorChecks(context = {}, options = {}) {

View File

@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { platform, totalmem } from "node:os";
import { platform, totalmem, hostname as osHostname } from "node:os";
import { t } from "../i18n.mjs";
import { writePidFile, cleanupPidFile, waitForServer } from "../utils/pid.mjs";
import { ServerSupervisor, detectMitmCrash } from "../runtime/processSupervisor.mjs";
@@ -170,7 +170,16 @@ export async function runServe(opts = {}) {
PORT: String(dashboardPort),
DASHBOARD_PORT: String(dashboardPort),
API_PORT: String(apiPort),
HOSTNAME: process.env.HOSTNAME || "0.0.0.0",
// #6194: POSIX shells (bash/zsh) auto-set HOSTNAME to the machine name — the
// .env loader (first-wins) can never override it. Ignore HOSTNAME when it
// matches the OS-reported hostname (the auto-set signature). OMNIROUTE_SERVER_HOST
// takes precedence; legacy HOSTNAME values that don't match os.hostname() are
// still honoured for backward compatibility (e.g. Windows CMD/PowerShell users
// who set HOSTNAME in .env where it is NOT auto-set).
HOSTNAME:
process.env.OMNIROUTE_SERVER_HOST ||
(process.env.HOSTNAME !== osHostname() ? process.env.HOSTNAME : undefined) ||
"0.0.0.0",
NODE_ENV: "production",
// #5238: preserve a user-set NODE_OPTIONS (incl. their own
// `--max-old-space-size=…`) instead of clobbering it with the calibrated

View File

@@ -242,11 +242,6 @@
"count": 1
}
},
"src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx": {
"no-restricted-syntax": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/settings/components/ModelLockoutCard.tsx": {
"react-hooks/exhaustive-deps": {
"count": 1
@@ -1132,11 +1127,6 @@
"count": 4
}
},
"tests/unit/copilot-gemini-claude-route-no-responses.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 9
}
},
"tests/unit/cursor-usage-fetcher.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 1
@@ -1397,11 +1387,6 @@
"count": 1
}
},
"tests/unit/executor-kiro.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 1
}
},
"tests/unit/executor-nlpcloud.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 2
@@ -1572,11 +1557,6 @@
"count": 2
}
},
"tests/unit/messages-count-tokens-route.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 4
}
},
"tests/unit/mimocode-executor.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 58
@@ -2007,11 +1987,6 @@
"count": 9
}
},
"tests/unit/save-call-log-persistence.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 7
}
},
"tests/unit/schema-coercion.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 12
@@ -2357,11 +2332,6 @@
"count": 76
}
},
"tests/unit/web-cookie-providers-new.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 18
}
},
"tests/unit/web-runtime-env.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 1

View File

@@ -182,7 +182,7 @@
"open-sse/services/tokenRefresh.ts": 2181,
"open-sse/services/usage.ts": 3454,
"open-sse/translator/request/openai-to-gemini.ts": 906,
"open-sse/translator/request/openai-to-kiro.ts": 853,
"open-sse/translator/request/openai-to-kiro.ts": 890,
"open-sse/translator/response/openai-responses.ts": 1092,
"open-sse/utils/cursorAgentProtobuf.ts": 1521,
"open-sse/utils/stream.ts": 2727,
@@ -206,7 +206,7 @@
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 954,
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderModels.ts": 155,
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderSettings.ts": 264,
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1021,
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1034,
"src/app/(dashboard)/dashboard/providers/components/onboarding/ProviderOnboardingWizard.tsx": 906,
"src/app/(dashboard)/dashboard/providers/page.tsx": 1927,
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1201,
@@ -221,7 +221,7 @@
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1924,
"src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1016,
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148,
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1121,
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1127,
"src/app/api/oauth/[provider]/[action]/route.ts": 960,
"src/app/api/providers/[id]/models/route.ts": 2593,
"src/app/api/providers/[id]/test/route.ts": 940,
@@ -314,13 +314,13 @@
"tests/unit/translator-helper-branches.test.ts": 870,
"tests/unit/translator-openai-responses-req.test.ts": 1172,
"tests/unit/translator-openai-to-gemini.test.ts": 1579,
"tests/unit/translator-openai-to-kiro.test.ts": 1093,
"tests/unit/translator-openai-to-kiro.test.ts": 1234,
"tests/unit/translator-resp-gemini-to-openai.test.ts": 1234,
"tests/unit/usage-service-hardening.test.ts": 1633,
"tests/unit/vscode-token-routes.test.ts": 1212,
"tests/unit/combo-config.test.ts": 881,
"_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.",
"tests/unit/web-cookie-providers-new.test.ts": 850,
"tests/unit/web-cookie-providers-new.test.ts": 890,
"tests/unit/response-sanitizer.test.ts": 906
},
"_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.",
@@ -371,5 +371,9 @@
"_rebaseline_2026_06_22_phase4c_adaptive_context_budget": "Compression Phase 4 (C) adaptive context-budget wiring own growth: open-sse/services/compression/strategySelector.ts 818->848 (+30 at the existing selectCompressionPlan dispatch chokepoint). selectCompressionPlan gains an 8th optional `adaptiveOptions` param (modelContextLimit/requestMaxTokens/onAdaptive sink) and, after resolveBasePlan and before the caching-aware pass, runs the PURE resolveAdaptivePlan when config.contextBudget.mode is floor|replace-autotrigger; the new adaptiveEnabled(config) helper also gates the legacy shouldAutoTrigger branch inside resolveBasePlan off when adaptive owns automatic-by-size escalation (D-C4). The escalation ladder, target computation, and the resolver itself live in open-sse/services/compression/adaptiveCompression/{computeTarget,ladder,resolveAdaptivePlan,types}.ts (all <cap, fully unit-tested by Tasks 1-4). strategySelector.ts is cohesive dispatch wiring at the existing compression chokepoint, not extractable without hiding the dispatch boundary, mirroring the prior compression rebaselines (#4217/#4210/phase4b). Covered by tests/unit/compression/adaptive-select-plan-wiring.test.ts (4 tests) + adaptive-chatcore-source-guard.test.ts (2). chatCore.ts also grows ~30 lines at the same call site (threads getTokenLimit(provider,effectiveModel) + request max_tokens into adaptiveOptions and records the adaptive telemetry block onto compression.completed) but stays under its frozen cap. Structural shrink of this file tracked in #3501.",
"_rebaseline_2026_06_25_rc17_pr_batch": "rc17 PR batch own growth (cohesive, not extractable): responseSanitizer.ts 1103->1122 (+19 = SanitizeOpenAIResponseOptions interface + stripReasoning option, #4678); tokenRefresh.ts 2070->2090 (+20 = codex 401 defense-in-depth unrecoverable-refresh guard, #4686); token-refresh-service.test.ts 1322->1353 (+31 = 401-unfamiliar-payload regression case, #4686); translator-openai-responses-req.test.ts 1047->1050 (+3 = reasoning_effort non-Copilot assertion update, #4688). All are the merged PRs own surgical additions at existing chokepoints.",
"_rebaseline_2026_06_25_rc17b_leva2": "rc17 leva2 PR batch own growth (cohesive, not extractable): providerLimits.ts 950->955 (#4786 generalized accesstoken fallback); default.ts NEW frozen entry at 828 (#4729 anthropic-compatible Bearer + #4766 json_schema fallback + #4787 cline workos headers — three provider-specific header branches); openai-to-kiro.ts 807->814 (#4763 Claude-capability image gate); openai-responses.ts 923->937 (#4764 computeFinishReason guard); executor-default-base.test.ts 1339->1440 (#4766 json_schema fallback tests); translator-openai-to-kiro.test.ts 918->980 (#4763 non-Claude image gate tests).",
"_rebaseline_2026_06_27_v3838_filesize_drift": "Mid-cycle drift on release/v3.8.38 — feature/fix growth from already-merged PRs that the fast-path (PR->release skips check:file-size) let accumulate without a bump. src/shared/constants/sidebarVisibility.ts 1100->1198 (+98 = #3812 colored menu-icon support, per-item accent map across the sidebar entries; #5142 then dropped one orphan settings entry, net still above the frozen). src/sse/handlers/chat.ts 1560->1575 (+15 = #5064 self-inflicted-timeout cooldown skip + #5124 long OpenAI-compatible SSE hardening + #5110 embed-WS LIVE_WS_HOST honour / early empty-message reject). Localized feature/fix code next to existing branches, each covered by its own PR tests; not extractable without hiding the chokepoint. Structural shrink of chat.ts tracked in #3501."
"_rebaseline_2026_06_27_v3838_filesize_drift": "Mid-cycle drift on release/v3.8.38 — feature/fix growth from already-merged PRs that the fast-path (PR->release skips check:file-size) let accumulate without a bump. src/shared/constants/sidebarVisibility.ts 1100->1198 (+98 = #3812 colored menu-icon support, per-item accent map across the sidebar entries; #5142 then dropped one orphan settings entry, net still above the frozen). src/sse/handlers/chat.ts 1560->1575 (+15 = #5064 self-inflicted-timeout cooldown skip + #5124 long OpenAI-compatible SSE hardening + #5110 embed-WS LIVE_WS_HOST honour / early empty-message reject). Localized feature/fix code next to existing branches, each covered by its own PR tests; not extractable without hiding the chokepoint. Structural shrink of chat.ts tracked in #3501.",
"_rebaseline_2026_07_05_6211_providerlimits_fetch_timeout": "PR #6211 own growth: src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx 1121->1127 (+6 = data-timeout guard for the quota page's two first-paint fetches — a PROVIDER_LIMITS_FETCH_TIMEOUT_MS const + fetchWithTimeout at the /api/providers/client and /api/usage/provider-limits call sites — so a never-settling connection can no longer wedge initialLoading on the skeleton, same infinite-skeleton class the PR also fixes on the providers page). Cohesive fix code at the existing fetch chokepoints (the 5-line rationale comment explains why a timeout/abort is needed where a try/catch only rescues a rejection); not extractable. Covered by tests/unit/providers-page-data-timeout.test.ts. Fast-path PR->release skips check:file-size, so this bump lands with the PR. Structural shrink of this file tracked in #3501.",
"_rebaseline_2026_07_05_6154_copilot_catalog_helpers": "PR #6154 own growth: src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts 1021->1034 (+13 = GitHub Copilot catalog refresh — model-section helper wiring for the refreshed passthrough/compatible model lists). Cohesive UI-helper growth alongside the registry/modelSpecs catalog refresh; not extractable. Covered by the PR's provider-registry-github-copilot-* unit tests. Fast-path PR->release skips check:file-size, so this bump lands with the PR (contributor backryun).",
"_rebaseline_2026_07_05_6213_kiro_thinking_filesize": "PR #6213 own growth (kiro adaptive-thinking -> reasoning_content, +384): open-sse/translator/request/openai-to-kiro.ts 853->890 (+37 = additionalModelRequestFields builder for adaptive thinking: output_config.effort + thinking:{type:adaptive} + max_tokens, only when the request asked for thinking) and tests/unit/translator-openai-to-kiro.test.ts 1093->1234 (+141 = adaptive-thinking request/frame regression cases). The fast-path PR->release does NOT gate check:file-size on the merge, so this cohesive feature growth accumulated on the release tip (see the 2026-07-02 #5798 note for the same pattern). Superseded by the release captain's rebaseline-at-release.",
"_rebaseline_2026_07_05_6235_doubao_dola": "PR #6235 own growth: tests/unit/web-cookie-providers-new.test.ts 850->890 (+40 = doubao-web -> Dola global provider switch regression cases: new host/cookie-domain/token-source assertions for www.dola.com). Cohesive test growth alongside the provider switch; contributor backryun. Fast-path PR->release skips check:file-size, so this bump lands with the PR."
}

View File

@@ -21,6 +21,18 @@
"open-sse/services/combo/__tests__/targetExhaustion.test.ts": {
"replacement": "tests/unit/combo/combo-target-exhaustion.test.ts",
"reason": "v3.8.44 #5976: os testes de exaustão eram flake-prone (delays Math.random, timeouts 30s, >3min no CI) e foram REESCRITOS como unit determinístico com MAIS cobertura (21 casos/52 asserts vs 13 casos/37 asserts). Documentado no commit 5fe225850. Revisão humana: apresentado ao operador no STOP #1 do release v3.8.44."
},
"src/shared/components/AutoRoutingBanner.test.tsx": {
"replacement": "tests/unit/home-no-autorouting-banner.test.ts",
"reason": "v3.8.45 #6164: fix(dashboard) remove the always-on Auto-Routing banner — o COMPONENTE foi deletado junto com o teste (feature removida pelo mantenedor, não mascaramento). O replacement guarda o novo contrato: a home NÃO renderiza o banner e o componente permanece deletado."
},
"tests/unit/free-provider-rankings-configured-filter.test.ts": {
"replacement": "tests/unit/freeProviderRankings-filters.test.ts",
"reason": "v3.8.45 #6251 supersede #6245: a página Free Provider Rankings migrou do toggle client-side 'Configured Only' (#6245, configuredProviderIds no cliente) para filtros server-side configuredOnly/availableOnly (#6251). O teste antigo pinava a implementação removida (7 asserts quebrados contra código que não existe); o replacement cobre o contrato novo com 11 casos (server-side, lib helper). Verificado legítimo — supersessão documentada no CHANGELOG do #6251."
}
}
},
"tests/unit/catalog-updates-v3x.test.ts": "v3.8.45 #6248: fix(providers) remove deprecated MiMo V2 entries — os 5 asserts removidos pinavam specs de modelos mimo-v2-* que deixaram de existir no catálogo (54→49). Asserts seguem a remoção dos modelos, não enfraquecimento. Verificado legítimo. Prune após v3.8.45 mergear para main.",
"tests/unit/xiaomi-mimo-provider.test.ts": "v3.8.45 #6248: fix(providers) remove deprecated MiMo V2 entries — 2 asserts sobre mimo-v2-pro/omni/flash removidos junto com os modelos (21→19). Verificado legítimo, não mascaramento. Prune após v3.8.45 mergear para main.",
"tests/unit/provider-models-route.test.ts": "v3.8.45 #6170: fix(providers) correct Kiro model catalog to real upstream ids — 3 assert.ok positivos de ids fabricados (claude-opus-4.7/sonnet-4.6) substituídos por 1 assert positivo de conjunto + 1 assert NEGATIVO garantindo que os ids fabricados sumiram (310→309). Asserts migrados ao catálogo real, não enfraquecidos. Verificado legítimo. Prune após v3.8.45 mergear para main.",
"tests/unit/copilot-gemini-claude-route-no-responses.test.ts": "v3.8.45 #6154: fix(providers) refresh GitHub Copilot catalog — 2 asserts combinados (registry-exists + par claude/gemini) reescritos como loop per-model com assert.ok individual por id do catálogo novo (7→6). Asserts reestruturados ao catálogo atualizado, não enfraquecidos. Verificado legítimo. Prune após v3.8.45 mergear para main."
}

View File

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

View File

@@ -482,7 +482,7 @@ open-sse/
### 4.2 `open-sse/executors/`
68 provider executors, each extending `BaseExecutor` (`base.ts`):
73 provider executors, each extending `BaseExecutor` (`base.ts`):
`antigravity`, `azure-openai`, `blackbox-web`, `chatgpt-web`, `cliproxyapi`,
`cloudflare-ai`, `codex`, `commandCode`, `cursor`, `default`, `devin-cli`,
@@ -522,21 +522,21 @@ Hub-and-spoke translation (OpenAI is the hub).
Highlights (full list under `open-sse/services/`):
| Concern | Files |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Combo routing | `combo.ts` (17 strategies), `comboConfig.ts`, `comboMetrics.ts`, `comboManifestMetrics.ts`, `comboAgentMiddleware.ts` |
| Auto Combo engine | `autoCombo/``engine.ts`, `scoring.ts`, `taskFitness.ts`, `virtualFactory.ts`, `modePacks.ts`, `autoPrefix.ts`, `persistence.ts`, `providerDiversity.ts`, `providerRegistryAccessor.ts`, `routerStrategy.ts`, `selfHealing.ts`, `index.ts` |
| Resilience | `accountFallback.ts` (cooldown + lockout), `errorClassifier.ts`, `emergencyFallback.ts`, `rateLimitManager.ts`, `rateLimitSemaphore.ts`, `accountSemaphore.ts`, `accountSelector.ts` |
| Quotas | `quotaMonitor.ts`, `quotaPreflight.ts`, `bailianQuotaFetcher.ts`, `codexQuotaFetcher.ts`, `deepseekQuotaFetcher.ts`, `crofUsageFetcher.ts`, `antigravityCredits.ts` |
| Caching | `reasoningCache.ts`, `searchCache.ts`, `signatureCache.ts`, `requestDedup.ts` |
| Routing intelligence | `intentClassifier.ts`, `taskAwareRouter.ts`, `backgroundTaskDetector.ts`, `volumeDetector.ts`, `wildcardRouter.ts`, `workflowFSM.ts`, `specificityDetector.ts`, `specificityRules.ts`, `specificityTypes.ts` |
| Model handling | `modelCapabilities.ts`, `modelDeprecation.ts`, `modelFamilyFallback.ts`, `modelStrip.ts`, `model.ts`, `provider.ts`, `providerRequestDefaults.ts`, `providerCostData.ts`, `payloadRules.ts` |
| Compression | `compression/` — full compression engine wiring |
| Token + session | `tokenRefresh.ts`, `sessionManager.ts`, `apiKeyRotator.ts`, `contextManager.ts`, `contextHandoff.ts`, `systemPrompt.ts`, `roleNormalizer.ts`, `responsesInputSanitizer.ts`, `toolSchemaSanitizer.ts`, `toolLimitDetector.ts`, `thinkingBudget.ts` |
| Tier / manifest | `tierResolver.ts`, `tierConfig.ts`, `tierDefaults.json`, `tierTypes.ts`, `manifestAdapter.ts` |
| IP / network | `ipFilter.ts`, `webSearchFallback.ts` |
| Batches | `batchProcessor.ts` |
| Usage | `usage.ts` |
| Concern | Files |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Combo routing | `combo.ts` (17 strategies), `comboConfig.ts`, `comboMetrics.ts`, `comboManifestMetrics.ts`, `comboAgentMiddleware.ts` |
| Auto Combo engine | `autoCombo/``engine.ts`, `scoring.ts`, `taskFitness.ts`, `virtualFactory.ts`, `modePacks.ts`, `autoPrefix.ts`, `persistence.ts`, `providerDiversity.ts`, `providerRegistryAccessor.ts`, `routerStrategy.ts`, `selfHealing.ts`, `index.ts` |
| Resilience | `accountFallback.ts` (cooldown + lockout), `errorClassifier.ts`, `emergencyFallback.ts`, `rateLimitManager.ts`, `rateLimitSemaphore.ts`, `accountSemaphore.ts`, `accountSelector.ts` |
| Quotas | `quotaMonitor.ts`, `quotaPreflight.ts`, `bailianQuotaFetcher.ts`, `codexQuotaFetcher.ts`, `deepseekQuotaFetcher.ts`, `crofUsageFetcher.ts`, `antigravityCredits.ts` |
| Caching | `reasoningCache.ts`, `searchCache.ts`, `signatureCache.ts`, `requestDedup.ts` |
| Routing intelligence | `intentClassifier.ts`, `taskAwareRouter.ts`, `backgroundTaskDetector.ts`, `volumeDetector.ts`, `wildcardRouter.ts`, `workflowFSM.ts`, `specificityDetector.ts`, `specificityRules.ts`, `specificityTypes.ts` |
| Model handling | `modelCapabilities.ts`, `modelDeprecation.ts`, `modelFamilyFallback.ts`, `modelStrip.ts`, `model.ts`, `provider.ts`, `providerRequestDefaults.ts`, `providerCostData.ts`, `payloadRules.ts` |
| Compression | `compression/` — full compression engine wiring |
| Token + session | `tokenRefresh.ts`, `sessionManager.ts`, `apiKeyRotator.ts`, `contextManager.ts`, `contextHandoff.ts`, `systemPrompt.ts`, `roleNormalizer.ts`, `responsesInputSanitizer.ts`, `toolSchemaSanitizer.ts`, `toolLimitDetector.ts`, `thinkingBudget.ts` |
| Tier / manifest | `tierResolver.ts`, `tierConfig.ts`, `tierDefaults.json`, `tierTypes.ts`, `manifestAdapter.ts` |
| IP / network | `ipFilter.ts`, `webSearchFallback.ts` |
| Batches | `batchProcessor.ts` |
| Usage | `usage.ts` |
### 4.6 `open-sse/mcp-server/`

View File

@@ -182,7 +182,7 @@ src/
| `compliance/` | Audit log + provider audit — see `docs/security/COMPLIANCE.md` |
| `compression/` | Compression engine glue (engines live in `open-sse/services/compression/`) |
| `config/` | Runtime config helpers |
| `db/` | 45+ domain DB modules + 55 migrations (always go through here for SQLite) |
| `db/` | 95+ domain DB modules + 110+ migrations (always go through here for SQLite) |
| `quota/` | Quota Sharing Engine: `dimensions.ts` (types/Zod), `types.ts` (QuotaStore interface), `sqliteQuotaStore.ts`, `redisQuotaStore.ts`, `storeFactory.ts`, `fairShare.ts`, `burnRate.ts`, `planResolver.ts`, `planRegistry.ts`, `saturationSignals.ts`, `enforce.ts`, `spendRecorder.ts` — see `docs/routing/QUOTA_SHARE.md` |
| `display/` | UI formatting helpers (cost, latency, etc.) |
| `embeddings/` | Embeddings service helpers |

View File

@@ -1,5 +1,5 @@
%% Database schema overview (selected core tables)
%% Reflects: src/lib/db/* (45+ modules, 55 migrations)
%% Reflects: src/lib/db/* (95+ modules, 110+ migrations)
%% v3.8.0
erDiagram
api_keys ||--o{ api_key_usage : tracks

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -133,13 +133,14 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari
| `OMNIROUTE_DISABLE_LIVE_WS` | `false` | `scripts/start-ws-server.mjs` | CI/harness toggle that disables the standalone live WebSocket helper script. |
| `RELAY_IP_PER_MINUTE` | `30` | `src/app/api/v1/relay/chat/completions/route.ts` | Per-(token, IP) relay rate limit, requests/minute. In-memory, per instance. `0` or negative disables the IP-dimension gate (per-token DB limit still applies). |
| `NODE_ENV` | `production` | Next.js core | Controls logging verbosity, caching, error detail exposure, and Next.js optimizations. |
| `OMNIROUTE_USE_TURBOPACK` | `1` (default in `.env.example`) | `package.json` / Next.js 16 | Toggles the Next.js 16 Turbopack bundler in `npm run dev` and `npm run build`. Set to `0` on Windows or when running into native binding incompatibilities. |
| `OMNIROUTE_USE_TURBOPACK` | `1` (Turbopack — code default) | `package.json` / Next.js 16 | Turbopack is the default bundler for `npm run dev` and `npm run build` (2-3× faster builds, benchmarked). Set to `0` to fall back to webpack on Windows or when running into native binding / bundler-compat incompatibilities. |
| `OMNIROUTE_SKIP_DB_HEALTHCHECK` | _(unset)_ | `src/lib/db/core.ts` / `src/lib/db/healthCheck.ts` | Set to `1` to skip the SQLite integrity health check on startup. Useful for faster boot on large databases. |
| `CREDENTIAL_HEALTH_CHECK_INTERVAL` | `300000` | `open-sse/config/constants.ts` / `src/lib/credentialHealth/scheduler.ts` | Interval (ms) for the background credential health check scheduler. Minimum: 10000 (10s). |
| `CREDENTIAL_HEALTH_CACHE_TTL` | `300000` | `open-sse/config/constants.ts` / `src/lib/credentialHealth/cache.ts` | TTL (ms) for cached credential health status. |
| `OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK` | `false` | `src/lib/credentialHealth/scheduler.ts` | Set to `1` or `true` to disable background periodic testing of provider connections. |
| `HOST` | `0.0.0.0` | `scripts/dev/run-next.mjs` | Bind address for the Next.js dev/start server. Overrides the default `0.0.0.0` when set. |
| `HOSTNAME` | `127.0.0.1` | `scripts/dev/run-next-playwright.mjs` | Bind address used by the Playwright runner when launching Next.js. Defaults to `127.0.0.1` for hermetic tests. |
| `HOSTNAME` | `127.0.0.1` | `scripts/dev/run-next-playwright.mjs` | Bind address used by the Playwright runner when launching Next.js. Defaults to `127.0.0.1` for hermetic tests. **Do not use for `omniroute serve`** — use `OMNIROUTE_SERVER_HOST` instead (POSIX shells auto-set `HOSTNAME` to the machine name; `.env` cannot override it). |
| `OMNIROUTE_SERVER_HOST` | `0.0.0.0` | `bin/cli/commands/serve.mjs` | Bind address for `omniroute serve`. Avoids collision with the POSIX shell `HOSTNAME` variable (always set to the machine name by bash/zsh). Falls back to `0.0.0.0` when unset. (#6194) |
### Port Modes
@@ -835,6 +836,7 @@ Anthropic-compatible provider instead.
| `PROXY_HEALTH_ENABLED` | `true` | `src/lib/proxyHealth/scheduler.ts` | Set `false` to disable the background proxy health scheduler that periodically probes registered proxies. |
| `PROXY_HEALTH_INTERVAL_MS` | `600000` | `src/lib/proxyHealth/scheduler.ts` | Background health-scheduler sweep interval in ms (minimum `60000`). |
| `PROXY_HEALTH_TEST_URL` | `https://httpbin.org/ip` | `src/lib/proxyHealth/scheduler.ts` | Reachability probe target used by the scheduler and the `/api/settings/proxies/auto-test` endpoint. Point it at an internal/self-hosted URL to avoid the public default. |
| `PROXY_HEALTH_AUTO_DEACTIVATE` | `false` | `src/lib/proxyHealth/statusPolicy.ts` | When `false` (default), automated reachability probes (the scheduler + the `/api/settings/proxies/auto-test` "Test All" button) are **read-only** and never write a proxy's status — only the operator sets active/inactive, so a flaky probe can't strand an assigned proxy (#6246). Set `true` to restore the legacy test-and-set behaviour. |
| `PROXY_AUTO_REMOVE` | `false` | `src/lib/proxyHealth/scheduler.ts` | Set `true` to let the scheduler auto-remove proxies after repeated consecutive failures. |
| `PROXY_AUTO_REMOVE_AFTER` | `3` | `src/lib/proxyHealth/scheduler.ts` | Consecutive failures before the scheduler auto-removes a proxy (when `PROXY_AUTO_REMOVE=true`). |
| `OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK` | `false` | `src/shared/constants/featureFlagDefinitions.ts` | Allow OAuth and provider validation flows to bypass a pinned proxy and connect directly when proxy reachability pre-checks fail. Effective precedence is Feature Flags DB override > env var > default. |
@@ -1007,6 +1009,7 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy),
| `MITM_DISABLE_TLS_VERIFY` | `0` | `src/mitm/server.cjs` | Set `1` to disable upstream TLS verification (development only). |
| `MITM_IDLE_TIMEOUT_MS` | `60000` | `src/mitm/socketTimeouts.ts`, `src/mitm/server.cjs` | Idle socket timeout (ms) for proxied connections; idle sockets past this are torn down to avoid leaking half-open tunnels. |
| `MITM_VERBOSE` | `1` | `src/mitm/server.cjs`, `src/mitm/_internal/bypass.cjs` | Routing-decision log verbosity: `0` silences, higher values log more bypass/route decisions. |
| `OMNIROUTE_NO_SUDO` | `0` | `src/mitm/systemCommands.ts` | Set `1` (truthy) to strip the leading `sudo` from MITM cert-trust commands — for root-less / user-namespaced deployments where the operator trusts the CA manually (e.g. via Node's extra-CA-certs mechanism). |
| `ONEPROXY_ENABLED` | `true` | `src/lib/oneproxySync.ts` | Enable the 1Proxy egress pool sync. |
| `ONEPROXY_API_URL` | `https://1proxy-api.aitradepulse.com` | `src/lib/oneproxySync.ts` | 1Proxy service API URL override. |
| `ONEPROXY_MAX_PROXIES` | `500` | `src/lib/oneproxySync.ts` | Maximum proxies imported per sync. |

View File

@@ -14,7 +14,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 5.9
- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -102,7 +102,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ └── streaming.ts # SSE streaming for A2A
│ │ ├── acp/ # Agent Communication Protocol registry and manager
│ │ ├── compliance/ # Compliance policy engine
│ │ ├── db/ # SQLite database layer (21 modules + migrations)
│ │ ├── db/ # SQLite database layer (95+ modules + migrations)
│ │ │ ├── core.ts # Database initialization, connection, schema
│ │ │ ├── providers.ts # Provider connection CRUD
│ │ │ ├── models.ts # Model catalog management
@@ -124,7 +124,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 16 SQL migration files
│ │ │ └── migrations/ # 110+ versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -435,7 +435,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches.

View File

@@ -416,7 +416,6 @@ export const AUDIO_SPEECH_PROVIDERS: Record<string, AudioProvider> = {
{ id: "mimo-v2.5-tts", name: "MiMo V2.5 TTS" },
{ id: "mimo-v2.5-tts-voicedesign", name: "MiMo V2.5 Voice Design" },
{ id: "mimo-v2.5-tts-voiceclone", name: "MiMo V2.5 Voice Clone" },
{ id: "mimo-v2-tts", name: "MiMo V2 TTS" },
],
},
};
@@ -512,10 +511,7 @@ export function parseSpeechModel(modelStr: string | null, dynamicProviders?: Aud
return parseAudioModel(modelStr, AUDIO_SPEECH_PROVIDERS, dynamicProviders);
}
export function parseTranslationModel(
modelStr: string | null,
dynamicProviders?: AudioProvider[]
) {
export function parseTranslationModel(modelStr: string | null, dynamicProviders?: AudioProvider[]) {
return parseAudioModel(modelStr, AUDIO_TRANSLATION_PROVIDERS, dynamicProviders);
}

View File

@@ -245,11 +245,7 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
{ provider: "kilo-gateway", modelId: "nvidia/nemotron-3-ultra-550b-a55b:free", displayName: "NVIDIA Nemotron 3 Ultra (free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "kilo-gateway-free", tos: "caution" },
{ provider: "kilo-gateway", modelId: "nvidia/nemotron-3-super-120b-a12b:free", displayName: "NVIDIA Nemotron 3 Super (free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "kilo-gateway-free", tos: "caution" },
{ provider: "kilo-gateway", modelId: "nex-agi/nex-n2-pro:free", displayName: "Nex-N2-Pro (free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "kilo-gateway-free", tos: "caution" },
{ provider: "kiro", modelId: "auto-kiro", displayName: "Auto (Kiro picks best model)", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" },
{ provider: "kiro", modelId: "claude-opus-4.8", displayName: "Claude Opus 4.8", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" },
{ provider: "kiro", modelId: "claude-opus-4.7", displayName: "Claude Opus 4.7", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" },
{ provider: "kiro", modelId: "claude-opus-4.6", displayName: "Claude Opus 4.6", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" },
{ provider: "kiro", modelId: "claude-sonnet-4.6", displayName: "Claude Sonnet 4.6", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" },
{ provider: "kiro", modelId: "claude-sonnet-4.5", displayName: "Claude Sonnet 4.5", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" },
{ provider: "kiro", modelId: "claude-haiku-4.5", displayName: "Claude Haiku 4.5", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" },
{ provider: "kiro", modelId: "deepseek-3.2", displayName: "DeepSeek V3.2", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" },
{ provider: "kiro", modelId: "minimax-m2.5", displayName: "MiniMax M2.5", monthlyTokens: 25000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "kiro", tos: "avoid" },

View File

@@ -134,6 +134,7 @@ import { agentrouterProvider } from "./registry/agentrouter/index.ts";
import { zaiProvider } from "./registry/zai/index.ts";
import { waferProvider } from "./registry/wafer/index.ts";
import { huggingchatProvider } from "./registry/huggingchat/index.ts";
import { yuanbao_webProvider } from "./registry/yuanbao-web/index.ts";
import { galadrielProvider } from "./registry/galadriel/index.ts";
import { qianfanProvider } from "./registry/qianfan/index.ts";
import { meta_llamaProvider } from "./registry/meta-llama/index.ts";
@@ -181,6 +182,7 @@ import { zenmux_freeProvider } from "./registry/zenmux-free/index.ts";
import { sumopodProvider } from "./registry/sumopod/index.ts";
import { x5labProvider } from "./registry/x5lab/index.ts";
import { kenariProvider } from "./registry/kenari/index.ts";
import { requestyProvider } from "./registry/requesty/index.ts";
export const REGISTRY: Record<string, RegistryEntry> = {
aimlapi: aimlapiProvider,
@@ -314,6 +316,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
agentrouter: agentrouterProvider,
zai: zaiProvider,
huggingchat: huggingchatProvider,
"yuanbao-web": yuanbao_webProvider,
galadriel: galadrielProvider,
qianfan: qianfanProvider,
"meta-llama": meta_llamaProvider,
@@ -364,4 +367,5 @@ export const REGISTRY: Record<string, RegistryEntry> = {
sumopod: sumopodProvider,
x5lab: x5labProvider,
kenari: kenariProvider,
requesty: requestyProvider,
};

View File

@@ -1,14 +1,4 @@
import type { RegistryEntry } from "../../shared.ts";
import {
getClaudeCliHeaders,
mapStainlessOs,
mapStainlessArch,
ANTHROPIC_BETA_CLAUDE_OAUTH,
ANTHROPIC_VERSION_HEADER,
CLAUDE_CLI_STAINLESS_PACKAGE_VERSION,
CLAUDE_CLI_STAINLESS_RUNTIME_VERSION,
CLAUDE_CLI_USER_AGENT,
} from "../../shared.ts";
export const agentrouterProvider: RegistryEntry = {
id: "agentrouter",
@@ -19,7 +9,11 @@ export const agentrouterProvider: RegistryEntry = {
authType: "apikey",
authHeader: "x-api-key",
defaultContextLength: 128000,
headers: getClaudeCliHeaders(),
// No static `headers` here: agentrouter now adopts the DYNAMIC Claude-Code
// wire image via CC_WIRE_IMAGE_BUILTINS (#6056) — the fingerprint/headers are
// applied by buildProviderHeaders + applyFingerprint, keeping this entry's
// own baseUrl + x-api-key auth. A static fingerprint here would drift and
// trip AgentRouter's WAF ("unauthorized client detected").
models: [
{ id: "claude-opus-4-6", name: "Claude 4.6 Opus" },
{ id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" },

View File

@@ -9,6 +9,7 @@ export const claude_webProvider: RegistryEntry = {
authType: "apikey",
authHeader: "cookie",
models: [
{ id: "claude-sonnet-5", name: "Claude 5 Sonnet (web)" },
{ id: "claude-sonnet-4-6", name: "Claude 4.6 Sonnet (web)" },
{ id: "claude-haiku-4-5", name: "Claude 4.5 Haiku (web)" },
],

View File

@@ -5,6 +5,11 @@ export const clineProvider: RegistryEntry = {
alias: "cl",
format: "openai",
executor: "openai",
// Cline's API only implements streaming (streamText). A non-streaming request
// returns "generateText is not implemented" / an empty body, so force upstream
// streaming and let chatCore convert the SSE back to JSON for stream:false
// clients (e.g. the model-test button, non-streaming API callers).
forceStream: true,
baseUrl: "https://api.cline.bot/api/v1/chat/completions",
authType: "oauth",
authHeader: "Authorization",

View File

@@ -9,6 +9,11 @@ export const clinepassProvider: RegistryEntry = {
alias: "clinepass",
format: "openai",
executor: "default",
// ClinePass shares Cline's streaming-only API — a non-streaming request returns
// "generateText is not implemented" / an empty body. Force upstream streaming;
// chatCore accumulates the SSE and converts it back to JSON for stream:false
// clients. (Same as the sibling `cline` provider.)
forceStream: true,
baseUrl: "https://api.cline.bot/api/v1/chat/completions",
authType: "apikey",
authHeader: "bearer",

View File

@@ -28,11 +28,17 @@ export const codexProvider: RegistryEntry = {
// 1.05M). Public refs : openai/codex#19208, #19319, #19464 ;
// opencode#24171. max_output_tokens is stripped server-side
// (litellm#21193, codex#4138) so 128K is informational only.
// The usable INPUT budget is smaller than the 400K window (part is
// reserved for output), so max_input_tokens must be distinct from
// context_length or coding agents never auto-compact (#6191). OpenAI's
// own live catalog reports ~272K for gpt-5.5 in Codex.
{
id: "gpt-5.5",
name: "GPT 5.5",
...GPT_5_5_CODEX_CAPABILITIES,
contextLength: 400000,
// #6191: input cap per reporter; TODO confirm exact value
maxInputTokens: 272000,
maxOutputTokens: 128000,
},
{
@@ -40,6 +46,8 @@ export const codexProvider: RegistryEntry = {
name: "GPT 5.5 (xHigh)",
...GPT_5_5_CODEX_CAPABILITIES,
contextLength: 400000,
// #6191: input cap per reporter; TODO confirm exact value
maxInputTokens: 272000,
maxOutputTokens: 128000,
},
{
@@ -47,6 +55,8 @@ export const codexProvider: RegistryEntry = {
name: "GPT 5.5 (High)",
...GPT_5_5_CODEX_CAPABILITIES,
contextLength: 400000,
// #6191: input cap per reporter; TODO confirm exact value
maxInputTokens: 272000,
maxOutputTokens: 128000,
},
{
@@ -54,6 +64,8 @@ export const codexProvider: RegistryEntry = {
name: "GPT 5.5 (Medium)",
...GPT_5_5_CODEX_CAPABILITIES,
contextLength: 400000,
// #6191: input cap per reporter; TODO confirm exact value
maxInputTokens: 272000,
maxOutputTokens: 128000,
},
{
@@ -61,6 +73,8 @@ export const codexProvider: RegistryEntry = {
name: "GPT 5.5 (Low)",
...GPT_5_5_CODEX_CAPABILITIES,
contextLength: 400000,
// #6191: input cap per reporter; TODO confirm exact value
maxInputTokens: 272000,
maxOutputTokens: 128000,
},
{

View File

@@ -5,11 +5,11 @@ export const doubao_webProvider: RegistryEntry = {
alias: "db",
format: "openai",
executor: "doubao-web",
baseUrl: "https://www.doubao.com/api/chat",
baseUrl: "https://www.dola.com/chat/completion",
authType: "apikey",
authHeader: "cookie",
models: [
{ id: "doubao-default", name: "Doubao Default" },
{ id: "doubao-pro", name: "Doubao Pro" },
{ id: "dola-speed", name: "Dola Speed" },
{ id: "dola-pro", name: "Dola Pro" },
],
};

View File

@@ -25,69 +25,137 @@ export const githubProvider: RegistryEntry = {
defaultContextLength: 128000,
headers: getGitHubCopilotChatHeaders(),
models: [
// Copilot still serves the original GPT-4 via chat/completions; keep it
// alongside GPT-4o and the GPT-5.x family so apps that hard-code `gpt-4` resolve here.
{ id: "gpt-4", name: "GPT-4", contextLength: 128000 },
// 9router#98 — Copilot still serves GPT-4o via chat/completions; keep it
// alongside the GPT-5.x family so apps that hard-code `gpt-4o` resolve here.
{ id: "gpt-4o", name: "GPT-4o", contextLength: 128000 },
// Copilot also serves the cheaper GPT-4o mini via chat/completions; keep it
// alongside gpt-4o so apps that hard-code `gpt-4o-mini` resolve to the Copilot
// (`gh`) provider rather than only the github-models (`ghm`) marketplace entry.
{ id: "gpt-4o-mini", name: "GPT-4o mini", contextLength: 128000 },
{ id: "gpt-5-mini", name: "GPT-5 Mini", targetFormat: "openai-responses" },
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex", targetFormat: "openai-responses" },
{ id: "gpt-5.4-mini", name: "GPT-5.4 Mini", targetFormat: "openai-responses" },
{
id: "gpt-5.4",
name: "GPT-5.4",
targetFormat: "openai-responses",
supportsXHighEffort: true,
id: "claude-fable-5",
name: "Claude Fable 5",
contextLength: 1000000,
maxOutputTokens: 64000,
},
{ id: "gpt-5.5", name: "GPT-5.5", ...GPT_5_5_CODEX_CAPABILITIES },
{
id: "claude-haiku-4.5",
name: "Claude Haiku 4.5",
id: "claude-opus-4.8-fast",
name: "Claude Opus 4.8 (fast mode)",
contextLength: 1000000,
maxOutputTokens: 64000,
unsupportedParams: ["temperature", "top_p", "top_k"],
},
{
id: "claude-opus-4.8",
name: "Claude Opus 4.8",
contextLength: 1000000,
maxOutputTokens: 64000,
unsupportedParams: ["temperature", "top_p", "top_k"],
},
{
id: "claude-opus-4.7",
name: "Claude Opus 4.7",
contextLength: 1000000,
maxOutputTokens: 64000,
},
{
id: "claude-sonnet-4.6",
name: "Claude Sonnet 4.6",
contextLength: 1000000,
maxOutputTokens: 64000,
},
{
id: "claude-opus-4.5",
name: "Claude Opus 4.5",
contextLength: 200000,
maxOutputTokens: 32000,
},
{
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
contextLength: 1000000,
maxOutputTokens: 64000,
},
{
id: "claude-sonnet-4.5",
name: "Claude Sonnet 4.5",
contextLength: 200000,
maxOutputTokens: 64000,
maxOutputTokens: 32000,
},
{
id: "claude-sonnet-4.6",
name: "Claude Sonnet 4.6",
id: "claude-haiku-4.5",
name: "Claude Haiku 4.5",
contextLength: 200000,
maxOutputTokens: 64000,
},
{
// #2911: GitHub Copilot's Responses API does not serve Claude/Gemini —
// route them via chat/completions (provider default) like claude-opus-4.6.
id: "claude-opus-4-5-20251101",
name: "Claude Opus 4.5 (Full ID)",
contextLength: 200000,
maxOutputTokens: 64000,
},
{
id: "claude-opus-4.6",
name: "Claude Opus 4.6",
contextLength: 1000000,
maxOutputTokens: 128000,
},
{
// #2911: Claude on Copilot must use chat/completions, not the Responses API.
id: "claude-opus-4.7",
name: "Claude Opus 4.7",
contextLength: 1000000,
maxOutputTokens: 128000,
maxOutputTokens: 32000,
},
// #2911: Gemini on Copilot must use chat/completions, not the Responses API.
{ id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro" },
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash" },
{ id: "oswe-vscode-prime", name: "Raptor Mini", targetFormat: "openai-responses" },
//{ id: "?", name: "Goldeneye" },
{
id: "gemini-3.1-pro-preview",
name: "Gemini 3.1 Pro",
contextLength: 1000000,
maxOutputTokens: 64000,
},
{
id: "gemini-3.5-flash",
name: "Gemini 3.5 Flash",
contextLength: 1000000,
maxOutputTokens: 64000,
},
{ id: "gpt-5.5", name: "GPT-5.5", ...GPT_5_5_CODEX_CAPABILITIES, maxOutputTokens: 128000 },
{
id: "gpt-5.4",
name: "GPT-5.4",
targetFormat: "openai-responses",
supportsXHighEffort: true,
contextLength: 1050000,
maxOutputTokens: 128000,
},
{
id: "gpt-5.4-mini",
name: "GPT-5.4 mini",
targetFormat: "openai-responses",
contextLength: 400000,
maxOutputTokens: 128000,
},
{
id: "gpt-5.3-codex",
name: "GPT-5.3-Codex",
targetFormat: "openai-responses",
contextLength: 400000,
maxOutputTokens: 128000,
},
{
id: "gpt-5-mini",
name: "GPT-5 mini",
targetFormat: "openai-responses",
contextLength: 264000,
maxOutputTokens: 64000,
},
{
id: "gpt-4o-2024-11-20",
name: "GPT-4o",
contextLength: 128000,
maxOutputTokens: 16384,
},
{ id: "gpt-4o-mini", name: "GPT-4o mini", contextLength: 128000, maxOutputTokens: 4096 },
{
id: "gpt-4-0125-preview",
name: "GPT 4 Turbo",
contextLength: 128000,
maxOutputTokens: 4096,
},
{
id: "kimi-k2.7-code",
name: "Kimi K2.7 Code",
contextLength: 256000,
maxOutputTokens: 32000,
},
{
id: "mai-code-1-flash",
name: "MAI-Code-1-Flash",
targetFormat: "openai-responses",
contextLength: 256000,
maxOutputTokens: 128000,
},
{
id: "oswe-vscode-prime",
name: "Raptor mini",
targetFormat: "openai-responses",
contextLength: 264000,
maxOutputTokens: 64000,
},
],
};

View File

@@ -15,32 +15,14 @@ export const kiroProvider: RegistryEntry = {
tokenUrl: "https://prod.us-east-1.auth.desktop.kiro.dev/refreshToken",
authUrl: "https://prod.us-east-1.auth.desktop.kiro.dev",
},
// Model IDs must match Kiro's real upstream catalog exactly — an unknown id
// makes Kiro return `400 "Invalid model. Please select a different model"`.
// Fabricated ids (auto-kiro, claude-opus-4.x, claude-fable-5, claude-sonnet-4.6)
// were removed after live VPS validation: Kiro offers no Opus/Fable, its Sonnet
// is 4.5 (not 4.6), and there is no "auto" model id (it was sent verbatim and
// 400'd). claude-sonnet-5 is a real Kiro model but plan-gated per account —
// kept so entitled accounts can use it. See kiro cluster #6112/#6113/#6099.
models: [
{ id: "auto-kiro", name: "Auto (Kiro picks best model)" },
{
id: "claude-fable-5",
name: "Claude Fable 5",
contextLength: 1000000,
maxOutputTokens: 128000,
},
{
id: "claude-opus-4.8",
name: "Claude Opus 4.8",
contextLength: 1000000,
maxOutputTokens: 128000,
},
{
id: "claude-opus-4.7",
name: "Claude Opus 4.7",
contextLength: 1000000,
maxOutputTokens: 128000,
},
{
id: "claude-opus-4.6",
name: "Claude Opus 4.6",
contextLength: 1000000,
maxOutputTokens: 128000,
},
{
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
@@ -48,8 +30,8 @@ export const kiroProvider: RegistryEntry = {
maxOutputTokens: 128000,
},
{
id: "claude-sonnet-4.6",
name: "Claude Sonnet 4.6",
id: "claude-sonnet-4.5",
name: "Claude Sonnet 4.5",
contextLength: 200000,
maxOutputTokens: 64000,
},

View File

@@ -9,10 +9,12 @@ export const nvidiaProvider: RegistryEntry = {
authType: "apikey",
authHeader: "bearer",
models: [
{ id: "z-ai/glm-5.1", name: "GLM 5.1" },
// #3329: minimaxai/minimax-m3 removed — NVIDIA NIM does not host it yet
// (every request 404s), while minimax-m2.7 on the same provider works.
// Re-add only once NVIDIA actually serves it.
// #6108: z-ai/glm-5.1 EOL'd 2026-07-02 (direct probe returns 410) — dropped.
{ id: "z-ai/glm-5.2", name: "GLM 5.2" },
// #3329/#6108: minimaxai/minimax-m3 stays excluded from the nvidia tier — it
// still 404s here for most callers; the single 200 probe in #6108 was not
// reproducible enough to override the #3329 guard. Re-add only once NVIDIA
// reliably serves it (and flip nvidia-minimax-m3-removed-3329.test.ts then).
{ id: "minimaxai/minimax-m2.7", name: "MiniMax M2.7" },
{ id: "google/gemma-4-31b-it", name: "Gemma 4 31B" },
{ id: "mistralai/mistral-small-4-119b-2603", name: "Mistral Small 4 2603" },
@@ -25,11 +27,10 @@ export const nvidiaProvider: RegistryEntry = {
{ id: "deepseek-ai/deepseek-v4-pro", name: "DeepSeek V4 Pro", supportsReasoning: true },
{ id: "deepseek-ai/deepseek-v4-flash", name: "DeepSeek V4 Flash", supportsReasoning: true },
// Sweep 2026-06-19: verified present in the live NVIDIA NIM /v1/models catalog.
// minimaxai/minimax-m3 is now listed too, but left out per #3329 until inference
// (not just listing) is confirmed — re-add when a real request stops 404ing.
{ id: "moonshotai/kimi-k2.6", name: "Kimi K2.6" },
{ id: "openai/gpt-oss-120b", name: "GPT OSS 120B", toolCalling: false },
{ id: "openai/gpt-oss-20b", name: "GPT OSS 20B", toolCalling: false },
{ id: "nvidia/nemotron-3-super-120b-a12b", name: "Nemotron 3 Super 120B A12B" },
{ id: "nvidia/nemotron-3-ultra-550b-a55b", name: "Nemotron 3 Ultra 550B" },
],
};

View File

@@ -25,8 +25,6 @@ export const opencode_goProvider: RegistryEntry = {
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro" },
{ id: "mimo-v2.5", name: "MiMo-V2.5" },
{ id: "mimo-v2-pro", name: "MiMo-V2-Pro" },
{ id: "mimo-v2-omni", name: "MiMo-V2-Omni" },
// #3110: MiniMax M3 via OpenCode Go tier
{
id: "minimax-m3",

View File

@@ -0,0 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const requestyProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "requesty",
alias: "requesty",
baseUrl: "https://router.requesty.ai/v1/chat/completions",
modelsUrl: "https://router.requesty.ai/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -0,0 +1,37 @@
import type { RegistryEntry } from "../../shared.ts";
export const yuanbao_webProvider: RegistryEntry = {
id: "yuanbao-web",
alias: "ybw",
format: "openai",
executor: "yuanbao-web",
baseUrl: "https://yuanbao.tencent.com/api/chat",
authType: "apikey",
authHeader: "cookie",
models: [
{ id: "deepseek-v3", name: "DeepSeek V3 (via Yuanbao)", toolCalling: false },
{
id: "deepseek-r1",
name: "DeepSeek R1 (via Yuanbao)",
supportsReasoning: true,
},
{ id: "hunyuan", name: "Hunyuan (via Yuanbao)" },
{
id: "hunyuan-t1",
name: "Hunyuan T1 (via Yuanbao)",
supportsReasoning: true,
},
{ id: "deepseek-v3-search", name: "DeepSeek V3 + Web Search (via Yuanbao)" },
{
id: "deepseek-r1-search",
name: "DeepSeek R1 + Web Search (via Yuanbao)",
supportsReasoning: true,
},
{ id: "hunyuan-search", name: "Hunyuan + Web Search (via Yuanbao)" },
{
id: "hunyuan-t1-search",
name: "Hunyuan T1 + Web Search (via Yuanbao)",
supportsReasoning: true,
},
],
};

View File

@@ -9,7 +9,7 @@ import type { RegistryEntry } from "../../shared.ts";
* for all API requests as a query parameter.
*
* Models available on the free tier (5 Flows/5h, 38.64 Flows/week):
* DeepSeek V3.2, GLM 4.7 Flash Free, MiMo V2 Flash Free, and others.
* DeepSeek V3.2, GLM 4.7 Flash Free, and others.
*
* Short alias "zmf" is distinct from the paid "zenmux" (alias "zm") which
* uses API-key auth against the OpenAI-compatible endpoint.
@@ -27,7 +27,6 @@ export const zenmux_freeProvider: RegistryEntry = {
{ id: "deepseek/deepseek-reasoner", name: "DeepSeek V3.2 (Thinking)", supportsReasoning: true },
{ id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro", supportsReasoning: true },
{ id: "kuaishou/kat-coder-pro-v1-free", name: "KAT Coder Pro V1 Free" },
{ id: "xiaomi/mimo-v2-flash-free", name: "MiMo V2 Flash Free" },
{ id: "z-ai/glm-4.7-flash-free", name: "GLM 4.7 Flash Free" },
{ id: "stepfun/step-3.5-flash-free", name: "Step 3.5 Flash Free" },
{ id: "inclusionai/ling-1t", name: "Ling 1T" },

View File

@@ -56,6 +56,13 @@ export interface RegistryModel {
unsupportedParams?: readonly string[];
/** Maximum context window in tokens */
contextLength?: number;
/**
* Explicit maximum input-token budget, when it is smaller than the full
* context window (e.g. OAuth backends that reserve part of the window for
* output). When set, catalog/capability builders prefer this over deriving
* max_input_tokens from contextLength (#6191).
*/
maxInputTokens?: number;
/**
* Interleaved-reasoning signal, mirroring models.dev's `interleaved_field`.
* Set to "reasoning_content" for models whose upstream runs DeepSeek thinking
@@ -428,9 +435,6 @@ export const CHAT_OPENAI_COMPAT_MODELS: Record<string, RegistryModel[]> = {
"xiaomi-mimo": [
{ id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", contextLength: 1048576, maxOutputTokens: 131072 },
{ id: "mimo-v2.5", name: "MiMo-V2.5", contextLength: 1048576, maxOutputTokens: 131072 },
{ id: "mimo-v2-pro", name: "MiMo-V2-Pro", contextLength: 262144, maxOutputTokens: 131072 },
{ id: "mimo-v2-omni", name: "MiMo-V2-Omni", contextLength: 262144, maxOutputTokens: 131072 },
{ id: "mimo-v2-flash", name: "MiMo-V2-Flash", contextLength: 262144, maxOutputTokens: 65536 },
],
gitlawb: [
{ id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", contextLength: 1048576, maxOutputTokens: 131072 },

View File

@@ -480,6 +480,46 @@ function sanitizeAntigravityGeminiRequest(
return clean;
}
/**
* Ported from decolua/9router#2321 (anki1kr): Vertex AI (used by Antigravity for
* Claude-branded models) rejects a conversation ending on an assistant turn —
* "This model does not support assistant message prefill" — so the request must
* always end on a user turn. Upstream patched `openaiToClaudeRequestForAntigravity`
* (dead code here, zero callers — see `open-sse/translator/request/openai-to-claude.ts`);
* this relocates the same strip to the LIVE Antigravity dispatch path, where Claude
* requests are converted to Gemini `contents` (assistant role is `"model"`, not
* `"assistant"`). Mirrors the trailing-strip pop-loop already used for Mistral
* (#3396), Copilot (#5802), and the CC-bridge in `claudeCodeCompatible.ts`.
*
* Scoped strictly to the Claude path by the caller (`isClaude` branch only) — native
* Gemini models via Antigravity must be unaffected, since Vertex-Claude is the only
* documented rejection surface.
*
* Guard: never strip `contents` down to empty — an empty `contents` array is itself
* an invalid request, so at least one entry (even a lone trailing "model" turn) is
* always preserved.
*/
function stripTrailingAntigravityAssistantTurn(
request: Record<string, unknown>
): Record<string, unknown> {
const contents = request.contents;
if (!Array.isArray(contents) || contents.length === 0) {
return request;
}
while (
contents.length > 1 &&
(contents[contents.length - 1] as AntigravityContent)?.role === "model"
) {
contents.pop();
}
return request;
}
// Test-only export so the unit suite can exercise the strip logic directly.
export const __test_stripTrailingAntigravityAssistantTurn = stripTrailingAntigravityAssistantTurn;
export class AntigravityExecutor extends BaseExecutor {
constructor() {
super("antigravity", PROVIDERS.antigravity);
@@ -660,7 +700,7 @@ export class AntigravityExecutor extends BaseExecutor {
};
const transformedRequest = isClaude
? sanitizeAntigravityGeminiRequest(rawTransformedRequest)
? stripTrailingAntigravityAssistantTurn(sanitizeAntigravityGeminiRequest(rawTransformedRequest))
: rawTransformedRequest;
// Obfuscate sensitive client names in user content (e.g. "OpenCode", "Cursor")

View File

@@ -1579,6 +1579,21 @@ type ImageResolver = (
parentMessageId?: string | null
) => Promise<string | null>;
/**
* True when ChatGPT emitted an image asset pointer (the image WAS generated
* upstream) but none of the pointers could be resolved to a downloadable URL
* — so the assistant text carries no image markdown. Lets callers surface an
* accurate "generated but not retrievable" error instead of the misleading
* "no image was produced". Escalated mesh report: image visible in the ChatGPT
* chat but returned to OmniRoute as a bare "completed without image markdown".
*/
export function detectImageResolutionFailure(
pointerCount: number,
resolvedCount: number
): boolean {
return pointerCount > 0 && resolvedCount === 0;
}
/** Build the final markdown block for a list of resolved image URLs. */
function imageMarkdown(urls: string[]): string {
if (urls.length === 0) return "";
@@ -2017,6 +2032,23 @@ async function buildNonStreamingResponse(
log,
parentCandidateMessageId
);
// The image genuinely exists upstream but no pointer resolved to a URL
// (unknown asset scheme, download 403/expired, oversize). Flag it so the
// image-generation handler can report an accurate "generated but not
// retrievable" error instead of the misleading "no image markdown" 502.
const imageResolutionFailed = detectImageResolutionFailure(
imagePointers?.length ?? 0,
urls.length
);
if (imageResolutionFailed && log?.warn) {
const schemes = (imagePointers ?? [])
.map((p) => p.pointer.split("://")[0] || p.pointer.slice(0, 24))
.join(", ");
log.warn(
"CGPT-WEB",
`Image generated upstream but no asset pointer resolved (schemes: ${schemes}) — surfacing as unretrievable`
);
}
fullAnswer += imageMarkdown(urls);
const promptTokens = Math.ceil(currentMsg.length / 4);
const completionTokens = Math.ceil(fullAnswer.length / 4);
@@ -2028,6 +2060,7 @@ async function buildNonStreamingResponse(
created,
model,
system_fingerprint: null,
...(imageResolutionFailed ? { x_image_resolution_failed: true } : {}),
choices: [
{
index: 0,

View File

@@ -1,155 +1,650 @@
/**
* DoubaoWebExecutor — ByteDance AI Chat via doubao.com
* DoubaoWebExecutor — Dola Global web chat via dola.com.
*
* Routes requests through Doubao's consumer chat API.
* Chinese market provider with large model catalog.
* The provider id remains `doubao-web` for compatibility with existing saved
* provider connections, but the global consumer service now runs through Dola.
*
* Endpoint: POST https://www.doubao.com/api/chat
* Auth: Session cookie from doubao.com
* Endpoint: POST https://www.dola.com/chat/completion
* Auth: Session cookies from www.dola.com
*/
import { randomUUID } from "node:crypto";
import { BaseExecutor, type ExecuteInput } from "./base.ts";
import { makeExecutorErrorResult as makeErrorResult, normalizeCookie } from "../utils/error.ts";
const BASE_URL = "https://www.doubao.com";
const CHAT_URL = `${BASE_URL}/api/chat`;
const BASE_URL = "https://www.dola.com";
const CHAT_URL = `${BASE_URL}/chat/completion`;
const DEFAULT_MODEL = "dola-speed";
const DOLA_BOT_ID = "7339470689562525703";
const USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36";
type JsonRecord = Record<string, unknown>;
export interface DolaTextExtractionState {
deferUntilAnswer: boolean;
answerStarted: boolean;
bufferedDeltas: string[];
}
function asRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function toString(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
function toContentText(value: unknown): string {
return typeof value === "string" ? value : "";
}
function parseJsonRecord(raw: string): JsonRecord | null {
if (!raw.startsWith("{")) return null;
try {
return asRecord(JSON.parse(raw));
} catch {
return null;
}
}
function randomNumericId(length = 19): string {
let id = String(Math.floor(Math.random() * 9) + 1);
while (id.length < length) id += String(Math.floor(Math.random() * 10));
return id;
}
function contentToText(content: unknown): string {
if (typeof content === "string") return content;
if (Array.isArray(content)) {
return content
.map((part) => {
const item = asRecord(part);
if (item.type === "text") return toContentText(item.text);
if (typeof item.text === "string") return item.text;
return "";
})
.filter(Boolean)
.join("\n");
}
return "";
}
function isDolaReasoningModel(modelId: string): boolean {
return modelId === "dola-pro" || modelId === "dola-deep-think";
}
function createDolaTextExtractionState(modelId: string): DolaTextExtractionState {
const deferUntilAnswer = isDolaReasoningModel(modelId);
return {
deferUntilAnswer,
answerStarted: !deferUntilAnswer,
bufferedDeltas: [],
};
}
function isDolaAnswerBoundary(block: JsonRecord): boolean {
return block.block_type === 10040 && block.is_finish === true;
}
export function foldMessages(messages: unknown): string {
if (!Array.isArray(messages)) return "";
return messages
.map((message) => {
const item = asRecord(message);
const role = toString(item.role) || "user";
const text = contentToText(item.content);
return text ? `${role}: ${text}` : "";
})
.filter(Boolean)
.join("\n\n");
}
export function extractCookieValue(cookieHeader: string, name: string): string {
const pattern = new RegExp(`(?:^|;\\s*)${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=([^;]*)`);
const value = pattern.exec(cookieHeader)?.[1] ?? "";
try {
return decodeURIComponent(value).trim();
} catch {
return value.trim();
}
}
function extractQueryValue(raw: string, name: string): string {
if (!raw.includes("?") && !raw.includes("&")) return "";
try {
const url = raw.startsWith("http") ? new URL(raw) : new URL(`https://www.dola.com/?${raw}`);
return toString(url.searchParams.get(name));
} catch {
return "";
}
}
export function resolveDolaFingerprint(
cookieHeader: string,
providerSpecificData?: unknown,
rawCredential = ""
): string {
const data = asRecord(providerSpecificData);
return (
toString(data.s_v_web_id) ||
toString(data.sVWebId) ||
extractCookieValue(cookieHeader, "s_v_web_id") ||
toString(data.fp) ||
extractCookieValue(cookieHeader, "fp") ||
extractQueryValue(rawCredential, "fp")
);
}
export function buildDolaCookieHeader(
rawCredential: string,
providerSpecificData?: unknown
): string {
const providerData = asRecord(providerSpecificData);
const raw = normalizeCookie(rawCredential.trim());
const parsed = parseJsonRecord(raw);
const data = { ...providerData, ...(parsed ?? {}) };
const explicitCookie = normalizeCookie(toString(data.cookie));
const directCookie = raw && !parsed ? raw : "";
const cookieSource = explicitCookie || directCookie;
if (cookieSource.includes("=")) return cookieSource;
const cookieNames = [
"sessionid",
"ttwid",
"s_v_web_id",
"fp",
"sessionid_ss",
"sid_guard",
"sid_tt",
"uid_tt",
"uid_tt_ss",
"passport_auth_status",
"passport_auth_status_ss",
"odin_tt",
];
const parts = cookieNames
.map((name) => {
const value = toString(data[name]);
return value ? `${name}=${value}` : "";
})
.filter(Boolean);
if (parts.length > 0) return parts.join("; ");
return raw ? `sessionid=${raw}` : "";
}
export function buildDolaQueryParams(
cookieHeader: string,
providerSpecificData?: unknown,
rawCredential = ""
): URLSearchParams {
const data = asRecord(providerSpecificData);
const generatedId = randomNumericId();
const deviceId = toString(data.device_id) || toString(data.deviceId) || generatedId;
const fp = resolveDolaFingerprint(cookieHeader, providerSpecificData, rawCredential);
return new URLSearchParams({
aid: "495671",
real_aid: "495671",
device_platform: "web",
device_id: deviceId,
web_id: toString(data.web_id) || toString(data.webId) || deviceId,
tea_uuid: toString(data.tea_uuid) || toString(data.teaUuid) || deviceId,
web_tab_id: randomUUID(),
pc_version: toString(data.pc_version) || toString(data.pcVersion) || "3.25.3",
pkg_type: "release_version",
version_code: "20800",
samantha_web: "1",
web_platform: "browser",
"use-olympus-account": "1",
language: toString(data.language) || "en",
region: toString(data.region) || "US",
sys_region: toString(data.sys_region) || toString(data.sysRegion) || "US",
fp,
});
}
export function resolveDolaDeepThinkValue(modelId: string, providerSpecificData?: unknown): 0 | 3 {
const data = asRecord(providerSpecificData);
const configured = toString(data.use_deep_think) || toString(data.useDeepThink);
if (configured === "3") return 3;
if (configured === "0") return 0;
if (data.deepThink === true || modelId === "dola-pro" || modelId === "dola-deep-think") return 3;
return 0;
}
export function buildDolaPayload(
prompt: string,
modelId = DEFAULT_MODEL,
cookieHeader = "",
providerSpecificData?: unknown,
rawCredential = ""
): JsonRecord {
const data = asRecord(providerSpecificData);
const localConversationId =
toString(data.local_conversation_id) ||
toString(data.localConversationId) ||
`local_${randomNumericId(16)}`;
const blockId = randomUUID();
const messageId = randomUUID();
const uniqueKey = randomUUID();
const now = Date.now();
const deepThinkValue = resolveDolaDeepThinkValue(modelId, providerSpecificData);
const fp = resolveDolaFingerprint(cookieHeader, providerSpecificData, rawCredential);
return {
client_meta: {
local_conversation_id: localConversationId,
conversation_id: "",
bot_id: toString(data.bot_id) || toString(data.botId) || DOLA_BOT_ID,
last_section_id: "",
last_message_index: null,
},
messages: [
{
local_message_id: messageId,
content_block: [
{
block_type: 10000,
content: {
text_block: {
text: prompt,
icon_url: "",
icon_url_dark: "",
summary: "",
},
pc_event_block: "",
},
block_id: blockId,
parent_id: "",
meta_info: [],
append_fields: [],
},
],
message_status: 0,
},
],
option: {
send_message_scene: "",
create_time_ms: now,
collect_id: "",
is_audio: false,
answer_with_suggest: false,
tts_switch: false,
need_deep_think: deepThinkValue,
click_clear_context: false,
from_suggest: false,
is_regen: false,
is_replace: false,
is_from_click_option: false,
is_from_click_softlink: false,
disable_sse_cache: false,
select_text_action: "",
is_select_text: false,
resend_for_regen: false,
scene_type: 0,
unique_key: uniqueKey,
start_seq: 0,
need_create_conversation: true,
conversation_init_option: { need_ack_conversation: true },
regen_query_id: [],
edit_query_id: [],
regen_instruction: "",
no_replace_for_regen: false,
message_from: 0,
shared_app_name: "",
shared_app_id: "",
sse_recv_event_options: { support_chunk_delta: true },
is_ai_playground: false,
is_old_user: false,
recovery_option: {
is_recovery: false,
req_create_time_sec: Math.floor(now / 1000),
append_sse_event_scene: 0,
},
message_storage_type: 0,
},
user_context: [],
ext: {
use_deep_think: String(deepThinkValue),
fp,
sub_conv_firstmet_type: "1",
collection_id: "",
conversation_init_option: JSON.stringify({ need_ack_conversation: true }),
commerce_credit_config_enable: "0",
},
};
}
function parseSseBlock(block: string): { event: string; data: unknown } | null {
const lines = block.split(/\r?\n/);
const event = lines
.find((line) => line.startsWith("event:"))
?.slice(6)
.trim();
const dataLines = lines
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).trim());
if (dataLines.length === 0) return null;
const rawData = dataLines.join("\n");
if (rawData === "[DONE]") return { event: event || "done", data: "[DONE]" };
try {
return { event: event || "", data: JSON.parse(rawData) };
} catch {
return null;
}
}
function extractDolaBlockDeltas(blocks: unknown[], state?: DolaTextExtractionState): string[] {
const deltas: string[] = [];
for (const block of blocks) {
const blockRecord = asRecord(block);
if (state && isDolaAnswerBoundary(blockRecord)) {
state.answerStarted = true;
state.bufferedDeltas = [];
continue;
}
const text = toContentText(asRecord(asRecord(blockRecord.content).text_block).text);
if (!text) continue;
if (!state || state.answerStarted) {
deltas.push(text);
} else {
state.bufferedDeltas.push(text);
}
}
return deltas;
}
function flushDolaTextExtractionState(state: DolaTextExtractionState): string[] {
if (state.answerStarted) return [];
const fallback = state.bufferedDeltas;
state.bufferedDeltas = [];
state.answerStarted = true;
return fallback;
}
export function extractDolaTextDeltas(data: unknown, state?: DolaTextExtractionState): string[] {
const root = asRecord(data);
const payload = asRecord(root.data);
const content = asRecord(root.content);
const payloadContent = asRecord(payload.content);
const initialBlocks = Array.isArray(content.content_block)
? content.content_block
: Array.isArray(payloadContent.content_block)
? payloadContent.content_block
: [];
const patchOps = Array.isArray(root.patch_op)
? root.patch_op
: Array.isArray(payload.patch_op)
? payload.patch_op
: [];
const deltas = extractDolaBlockDeltas(initialBlocks, state);
for (const op of patchOps) {
const patchValue = asRecord(asRecord(op).patch_value);
const blocks = Array.isArray(patchValue.content_block) ? patchValue.content_block : [];
deltas.push(...extractDolaBlockDeltas(blocks, state));
}
return deltas;
}
function extractDolaError(data: unknown): string {
const root = asRecord(data);
const payload = asRecord(root.data);
return (
toString(root.message) ||
toString(payload.message) ||
toString(payload.error_msg) ||
toString(payload.errorMessage)
);
}
export function isDolaBusyMessage(content: string): boolean {
const normalized = content.trim().toLowerCase();
return (
normalized.includes("a lot of people are using the app right now") &&
normalized.includes("try again later")
);
}
function openAiChunk(modelId: string, content: string): JsonRecord {
return {
id: `chatcmpl-dola-${Date.now()}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: modelId,
choices: [{ index: 0, delta: { content }, finish_reason: null }],
};
}
function openAiCompletion(modelId: string, content: string): JsonRecord {
return {
id: `chatcmpl-dola-${Date.now()}`,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model: modelId,
choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: "stop" }],
};
}
export class DoubaoWebExecutor extends BaseExecutor {
constructor() {
super("doubao-web", { id: "doubao-web", baseUrl: "https://www.doubao.com" });
super("doubao-web", { id: "doubao-web", baseUrl: BASE_URL });
}
async execute(input: ExecuteInput) {
const { body, credentials, signal, stream: wantStream } = input;
const bodyObj = (body || {}) as Record<string, unknown>;
const rawCookie = normalizeCookie(String(credentials?.apiKey ?? "").trim());
const messages = (bodyObj.messages as Array<{ role: string; content: string }>) || [];
const modelId = (bodyObj.model as string) || "doubao-default";
const reqBody = {
messages: messages.map((m) => ({ role: m.role, content: m.content })),
model: modelId,
stream: wantStream,
max_tokens: (bodyObj.max_tokens as number) || 4096,
};
const reqHeaders: Record<string, string> = {
private createHeaders(cookieHeader: string): Record<string, string> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
Accept: wantStream ? "text/event-stream" : "application/json",
Referer: `${BASE_URL}/`,
Accept: "text/event-stream",
Referer: `${BASE_URL}/chat/`,
Origin: BASE_URL,
"Agw-Js-Conv": "str",
};
if (rawCookie) reqHeaders.Cookie = rawCookie;
if (cookieHeader) headers.Cookie = cookieHeader;
return headers;
}
let upstream: Response;
try {
upstream = await fetch(CHAT_URL, {
method: "POST",
headers: reqHeaders,
body: JSON.stringify(reqBody),
signal,
});
} catch (err) {
return makeErrorResult(
502,
`Doubao fetch failed: ${err instanceof Error ? err.message : "unknown"}`,
body,
CHAT_URL
);
private async collectText(upstream: Response, modelId: string): Promise<string> {
const raw = await upstream.text();
const state = createDolaTextExtractionState(modelId);
const deltas: string[] = [];
for (const block of raw.split(/\r?\n\r?\n/)) {
const event = parseSseBlock(block);
if (event) deltas.push(...extractDolaTextDeltas(event.data, state));
}
deltas.push(...flushDolaTextExtractionState(state));
if (!upstream.ok) {
const errText = await upstream.text().catch(() => "");
return makeErrorResult(upstream.status, `Doubao error: ${errText}`, body, CHAT_URL);
}
return deltas.join("");
}
if (!wantStream) {
const data = (await upstream.json()) as Record<string, unknown>;
const content =
(data?.choices as Array<{ message?: { content?: string } }>)?.[0]?.message?.content ||
(data?.content as string) ||
"";
return {
response: new Response(
JSON.stringify({
id: `chatcmpl-doubao-${Date.now()}`,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model: modelId,
choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: "stop" }],
}),
{ headers: { "Content-Type": "application/json" } }
),
url: CHAT_URL,
headers: reqHeaders,
transformedBody: reqBody,
};
}
// Streaming
private createStream(upstream: Response, modelId: string, signal?: AbortSignal | null) {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const stream = new ReadableStream({
const state = createDolaTextExtractionState(modelId);
let sentDone = false;
return new ReadableStream({
async start(controller) {
const reader = upstream.body?.getReader();
if (!reader) {
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
return;
}
let buffer = "";
let errored = false;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (!line.startsWith("data:")) continue;
const data = line.slice(5).trim();
if (data === "[DONE]") {
const blocks = buffer.split(/\r?\n\r?\n/);
buffer = blocks.pop() || "";
for (const block of blocks) {
const event = parseSseBlock(block);
if (!event) continue;
if (event.event === "STREAM_ERROR") {
const message = extractDolaError(event.data) || "Dola stream error";
errored = true;
controller.error(new Error(message));
return;
}
for (const text of extractDolaTextDeltas(event.data, state)) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(openAiChunk(modelId, text))}\n\n`)
);
}
if (event.event === "SSE_REPLY_END") {
sentDone = true;
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
continue;
}
try {
const parsed = JSON.parse(data);
const text = parsed.choices?.[0]?.delta?.content || "";
if (text) {
const chunk = {
id: `chatcmpl-doubao-${Date.now()}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: modelId,
choices: [{ index: 0, delta: { content: text }, finish_reason: null }],
};
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
}
} catch {}
}
}
} catch (err) {
if (!signal?.aborted) controller.error(err);
if (!signal?.aborted) {
errored = true;
controller.error(err);
}
return;
} finally {
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
if (errored) return;
for (const text of flushDolaTextExtractionState(state)) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(openAiChunk(modelId, text))}\n\n`)
);
}
if (!sentDone) controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
}
},
});
}
async execute(input: ExecuteInput) {
const { body, credentials, signal, stream: wantStream } = input;
const bodyObj = asRecord(body);
const providerSpecificData = credentials?.providerSpecificData;
const rawCredential = toString(credentials?.apiKey);
const cookieHeader = buildDolaCookieHeader(rawCredential, providerSpecificData);
const requestedModel = toString(bodyObj.model) || input.model || DEFAULT_MODEL;
const modelId = requestedModel.split("/").pop() || DEFAULT_MODEL;
const prompt = foldMessages(bodyObj.messages);
const fingerprint = resolveDolaFingerprint(cookieHeader, providerSpecificData, rawCredential);
const transformedBody = buildDolaPayload(
prompt,
modelId,
cookieHeader,
providerSpecificData,
rawCredential
);
const query = buildDolaQueryParams(cookieHeader, providerSpecificData, rawCredential);
const url = `${CHAT_URL}?${query.toString()}`;
const reqHeaders = this.createHeaders(cookieHeader);
if (!extractCookieValue(cookieHeader, "sessionid")) {
return {
...makeErrorResult(
401,
"Dola Web requires a www.dola.com Cookie header containing at least sessionid, ttwid, and s_v_web_id.",
body,
url
),
headers: reqHeaders,
transformedBody,
};
}
if (!fingerprint) {
return {
...makeErrorResult(
401,
"Dola Web requires the browser fingerprint value from www.dola.com. Add s_v_web_id=... from Cookies or fp=verify_... from a Network chat/completion request URL.",
body,
url
),
headers: reqHeaders,
transformedBody,
};
}
let upstream: Response;
try {
upstream = await fetch(url, {
method: "POST",
headers: reqHeaders,
body: JSON.stringify(transformedBody),
signal,
});
} catch (err) {
return {
...makeErrorResult(
502,
`Dola fetch failed: ${err instanceof Error ? err.message : "unknown"}`,
body,
url
),
headers: reqHeaders,
transformedBody,
};
}
if (!upstream.ok) {
const errText = await upstream.text().catch(() => "");
return {
...makeErrorResult(upstream.status, `Dola error: ${errText}`, body, url),
headers: reqHeaders,
transformedBody,
};
}
const contentType = upstream.headers.get("Content-Type") || "";
if (!contentType.toLowerCase().includes("text/event-stream")) {
const text = await upstream.text().catch(() => "");
return {
...makeErrorResult(502, `Dola returned non-SSE response: ${text}`, body, url),
headers: reqHeaders,
transformedBody,
};
}
if (!wantStream) {
const content = await this.collectText(upstream, modelId);
if (isDolaBusyMessage(content)) {
return {
...makeErrorResult(429, "Dola is temporarily busy. Please try again later.", body, url),
headers: reqHeaders,
transformedBody,
};
}
return {
response: new Response(JSON.stringify(openAiCompletion(modelId, content)), {
headers: { "Content-Type": "application/json" },
}),
url,
headers: reqHeaders,
transformedBody,
};
}
return {
response: new Response(stream, {
response: new Response(this.createStream(upstream, modelId, signal), {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
}),
url: CHAT_URL,
url,
headers: reqHeaders,
transformedBody: reqBody,
transformedBody,
};
}
}

View File

@@ -41,6 +41,7 @@ import { T3ChatWebExecutor } from "./t3-chat-web.ts";
import { ClaudeWebExecutor } from "./claude-web.ts";
import { InnerAiExecutor } from "./inner-ai.ts";
import { HuggingChatExecutor } from "./huggingchat.ts";
import { YuanbaoWebExecutor } from "./yuanbao-web.ts";
import { PoeWebExecutor } from "./poe-web.ts";
import { VeniceWebExecutor } from "./venice-web.ts";
import { V0VercelWebExecutor } from "./v0-vercel-web.ts";
@@ -129,6 +130,8 @@ const executors = {
"in-ai": new InnerAiExecutor(), // Alias
huggingchat: new HuggingChatExecutor(),
hc: new HuggingChatExecutor(), // Alias
"yuanbao-web": new YuanbaoWebExecutor(),
ybw: new YuanbaoWebExecutor(), // Alias
"poe-web": new PoeWebExecutor(),
poe: new PoeWebExecutor(), // Alias
"venice-web": new VeniceWebExecutor(),
@@ -212,6 +215,7 @@ export { ClaudeWebExecutor } from "./claude-web.ts";
export { DeepSeekWebExecutor } from "./deepseek-web.ts";
export { DeepSeekWebWithAutoRefreshExecutor } from "./deepseek-web-with-auto-refresh.ts";
export { AdaptaWebExecutor } from "./adapta-web.ts";
export { YuanbaoWebExecutor } from "./yuanbao-web.ts";
export { T3ChatWebExecutor } from "./t3-chat-web.ts";
export { InnerAiExecutor } from "./inner-ai.ts";
export { QwenWebExecutor } from "./qwen-web.ts";

View File

@@ -214,6 +214,12 @@ export class KiroExecutor extends BaseExecutor {
if (b.conversationState !== undefined) kiroPayload.conversationState = b.conversationState;
if (b.profileArn !== undefined) kiroPayload.profileArn = b.profileArn;
if (b.inferenceConfig !== undefined) kiroPayload.inferenceConfig = b.inferenceConfig;
// Thinking control: `additionalModelRequestFields` ({output_config.effort,
// thinking:{type:"adaptive"}, max_tokens}) is a recognized top-level field on
// GenerateAssistantResponse — it steers adaptive reasoning. Built by the
// openai-to-kiro translator only when the request asked for thinking.
if (b.additionalModelRequestFields !== undefined)
kiroPayload.additionalModelRequestFields = b.additionalModelRequestFields;
// Fallback: if somehow conversationState isn't there, return the rest without model
// (for backward compatibility if something else bypasses the translator)
@@ -382,6 +388,56 @@ export class KiroExecutor extends BaseExecutor {
if (!state.totalContentLength) state.totalContentLength = 0;
if (!state.contextUsagePercentage) state.contextUsagePercentage = 0;
// Native reasoning frames. Verified against the live CodeWhisperer
// stream (2026-07): with adaptive thinking enabled (via
// additionalModelRequestFields), Kiro streams reasoning as a dedicated
// `reasoningContentEvent` frame carrying `{ text, signature }` — NOT
// inline `<thinking>` tags and NOT `assistantResponseEvent`. Some
// models/variants instead use a `reasoningText` object or a flat
// `{ text }` (cf. javargasm/pi-kiro `src/event-parser.ts`). OmniRoute
// had no handler for this event, so the reasoning was silently dropped;
// route it to the OpenAI `reasoning_content` channel.
{
const rp = event.payload as Record<string, unknown> | undefined;
const rt = rp?.reasoningText;
if (eventType === "reasoningContentEvent" || rt !== undefined) {
let nativeReasoning = "";
if (rt && typeof rt === "object") {
const rto = rt as { text?: unknown; Text?: unknown };
nativeReasoning =
typeof rto.text === "string"
? rto.text
: typeof rto.Text === "string"
? rto.Text
: "";
} else if (typeof rt === "string") {
nativeReasoning = rt;
} else if (typeof rp?.text === "string") {
nativeReasoning = rp.text as string;
}
if (nativeReasoning) {
state.hasReasoningContent = true;
const reasoningDelta: JsonRecord =
(state.reasoningChunkCount ?? 0) === 0 && chunkIndex === 0
? { role: "assistant", reasoning_content: nativeReasoning }
: { reasoning_content: nativeReasoning };
const chunk: JsonRecord = {
id: responseId,
object: "chat.completion.chunk",
created,
model,
choices: [{ index: 0, delta: reasoningDelta, finish_reason: null }],
};
chunkIndex++;
state.reasoningChunkCount = (state.reasoningChunkCount ?? 0) + 1;
controller.enqueue(TEXT_ENCODER.encode(`data: ${JSON.stringify(chunk)}\n\n`));
}
// Consume the reasoning frame (incl. signature-only) so it never
// falls through to the content handlers below.
continue;
}
}
// Handle assistantResponseEvent
if (eventType === "assistantResponseEvent") {
const content =

View File

@@ -0,0 +1,504 @@
/**
* YuanbaoWebExecutor — Tencent Yuanbao (yuanbao.tencent.com) Web Provider
*
* Routes chat requests through the Tencent Yuanbao consumer web session.
* Requires the `hy_user` + `hy_token` cookies from a logged-in
* yuanbao.tencent.com browser session (paste the full Cookie header).
*
* API flow (verified against the reverse-engineered references below):
* 1. POST /api/user/agent/conversation/create { agentId } -> { id } (conversationId)
* 2. POST /api/chat/{conversationId} (JSON body) -> SSE stream
*
* Streaming format (SSE, `data: {json}` lines):
* - { type: "think", content: "..." } -- reasoning tokens (DeepSeek-R1 / Hunyuan-T1)
* - { type: "text", msg: "..." } -- answer tokens
* - { ..., stopReason: "..." } -- terminal marker
*
* References (endpoint/payload/session shape lifted + cross-checked):
* - juzeon/yuanbao-chat2api (Rust) — cookie-only auth: hy_user + hy_token + agentId
* - chenwr727/yuanbao-free-api (Python) — endpoints, body shape, model map
*/
import {
BaseExecutor,
mergeAbortSignals,
mergeUpstreamExtraHeaders,
type ExecuteInput,
} from "./base.ts";
import { FETCH_TIMEOUT_MS } from "../config/constants.ts";
import { buildErrorBody, sanitizeErrorMessage } from "../utils/error.ts";
import { extractCookieValue, stripCookieInputPrefix } from "@/lib/providers/webCookieAuth";
const YUANBAO_BASE = "https://yuanbao.tencent.com";
const CREATE_URL = `${YUANBAO_BASE}/api/user/agent/conversation/create`;
const CHAT_URL = `${YUANBAO_BASE}/api/chat`;
// Public default DeepSeek agent id used by the Yuanbao web app. Not a secret —
// it is the shared consumer agent every logged-in session addresses by default.
const DEFAULT_AGENT_ID = "naQivTmsDa";
const USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36";
const DEFAULT_MODEL = "deepseek-v3";
// OmniRoute model id -> Yuanbao internal chatModelId + optional supportFunctions.
const MODEL_MAP: Record<string, { chatModelId: string; supportFunctions?: string[] }> = {
"deepseek-v3": { chatModelId: "deep_seek_v3" },
"deepseek-r1": { chatModelId: "deep_seek" },
"deepseek-v3-search": {
chatModelId: "deep_seek_v3",
supportFunctions: ["supportInternetSearch"],
},
"deepseek-r1-search": {
chatModelId: "deep_seek",
supportFunctions: ["supportInternetSearch"],
},
hunyuan: { chatModelId: "hunyuan_gpt_175B_0404" },
"hunyuan-t1": { chatModelId: "hunyuan_t1" },
"hunyuan-search": {
chatModelId: "hunyuan_gpt_175B_0404",
supportFunctions: ["supportInternetSearch"],
},
"hunyuan-t1-search": {
chatModelId: "hunyuan_t1",
supportFunctions: ["supportInternetSearch"],
},
};
// ── Helpers ─────────────────────────────────────────────────────────────────
function isEncryptedCredentialBlob(value: unknown): boolean {
return typeof value === "string" && value.trim().startsWith("enc:v1:");
}
function extractText(content: unknown): string {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return String(content ?? "");
return content
.map((part: unknown) => {
if (!part || typeof part !== "object") return "";
const item = part as Record<string, unknown>;
if ((item.type === "text" || item.type === "input_text") && typeof item.text === "string") {
return item.text;
}
return "";
})
.filter((p: string) => p.length > 0)
.join("\n");
}
/** Flatten OpenAI messages into the single-prompt shape Yuanbao expects. */
function buildPrompt(messages: Array<Record<string, unknown>>): string {
const parts: Array<{ role: string; content: string }> = [];
for (const msg of messages) {
const role = String(msg.role || "user");
const text = extractText(msg.content).trim();
if (!text) continue;
parts.push({ role, content: text });
}
if (parts.length === 0) return "";
if (parts.length === 1) return parts[0].content;
// Multi-turn: label each turn (matches the reference chat2api formatting).
return parts.map((p) => `#[${p.role.trim()}]\n${p.content}`).join("\n\n");
}
/** Build the `hy_source=web; hy_user=...; hy_token=...` cookie from the pasted header. */
function buildYuanbaoCookie(rawApiKey: string): { cookie: string; hasToken: boolean } {
const raw = stripCookieInputPrefix(rawApiKey || "");
const hyUser = extractCookieValue(raw, "hy_user");
const hyToken = extractCookieValue(raw, "hy_token");
if (hyUser && hyToken) {
return { cookie: `hy_source=web; hy_user=${hyUser}; hy_token=${hyToken}`, hasToken: true };
}
// Fall back to forwarding whatever the user pasted (may already be a full
// Cookie header). Only usable if it plausibly carries the session token.
const hasToken = raw.includes("hy_token=");
return { cookie: raw, hasToken };
}
function estimateTokens(text: string): number {
return Math.max(1, Math.ceil((text || "").length / 4));
}
async function readUpstreamErrorDetails(response: Response): Promise<{
message: string | null;
details: unknown;
}> {
const contentType = response.headers.get("content-type") || "";
const text = await response.text().catch(() => "");
if (!text) return { message: null, details: null };
if (contentType.includes("json")) {
try {
const parsed = JSON.parse(text) as Record<string, unknown>;
const message =
typeof parsed.message === "string"
? parsed.message
: typeof parsed.error === "string"
? parsed.error
: null;
return { message: message ? sanitizeErrorMessage(message) : null, details: parsed };
} catch {
// fall through
}
}
return { message: sanitizeErrorMessage(text), details: { body: text } };
}
// ── Executor ────────────────────────────────────────────────────────────────
export class YuanbaoWebExecutor extends BaseExecutor {
constructor() {
super("yuanbao-web", { id: "yuanbao-web", baseUrl: CHAT_URL });
}
private errorResponse(status: number, message: string, url: string, details?: unknown) {
return {
response: new Response(JSON.stringify(buildErrorBody(status, message, details)), {
status,
headers: { "Content-Type": "application/json" },
}),
url,
headers: {},
transformedBody: undefined,
};
}
async execute(input: ExecuteInput): Promise<{
response: Response;
url: string;
headers: Record<string, string>;
transformedBody: unknown;
}> {
const { model, body, stream, credentials, signal, log, upstreamExtraHeaders } = input;
const messages = (body as Record<string, unknown>).messages as
| Array<Record<string, unknown>>
| undefined;
if (!messages || !Array.isArray(messages) || messages.length === 0) {
return this.errorResponse(400, "Missing or empty messages array", CHAT_URL);
}
if (isEncryptedCredentialBlob(credentials.apiKey)) {
return this.errorResponse(
401,
"Yuanbao credentials are encrypted but STORAGE_ENCRYPTION_KEY is not loaded. " +
"Restore the encryption key or re-save the Yuanbao cookie.",
CREATE_URL
);
}
const { cookie, hasToken } = buildYuanbaoCookie(credentials.apiKey || "");
if (!hasToken) {
return this.errorResponse(
401,
"Yuanbao requires a session cookie. Log in to yuanbao.tencent.com, open " +
"DevTools > Application > Cookies, and paste the full Cookie header " +
"(it must contain hy_user and hy_token).",
CREATE_URL
);
}
const resolvedModel = model && MODEL_MAP[model] ? model : DEFAULT_MODEL;
const modelSpec = MODEL_MAP[resolvedModel];
const prompt = buildPrompt(messages);
if (!prompt.trim()) {
return this.errorResponse(400, "Empty prompt after processing messages", CHAT_URL);
}
const baseHeaders: Record<string, string> = {
Cookie: cookie,
"User-Agent": USER_AGENT,
Origin: YUANBAO_BASE,
Referer: `${YUANBAO_BASE}/chat/${DEFAULT_AGENT_ID}`,
"X-Agentid": DEFAULT_AGENT_ID,
};
const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
const combinedSignal = signal ? mergeAbortSignals(signal, timeoutSignal) : timeoutSignal;
// ── Step 1: create conversation ─────────────────────────────────────────
let conversationId: string;
try {
const createRes = await fetch(CREATE_URL, {
method: "POST",
headers: { ...baseHeaders, "Content-Type": "application/json" },
body: JSON.stringify({ agentId: DEFAULT_AGENT_ID }),
signal: combinedSignal,
});
if (!createRes.ok) {
const status = createRes.status;
const upstreamError = await readUpstreamErrorDetails(createRes);
let message = `Yuanbao conversation creation failed (HTTP ${status})`;
if (status === 401 || status === 403) {
message =
"Yuanbao auth failed — your hy_user/hy_token cookies may be missing or expired. " +
"Log in to yuanbao.tencent.com and re-paste your Cookie header.";
} else if (status === 429) {
message = "Yuanbao rate limited. Wait a moment and retry.";
}
if (upstreamError.message) message = `${message}: ${upstreamError.message}`;
return this.errorResponse(status, message, CREATE_URL, upstreamError.details);
}
const createData = (await createRes.json()) as Record<string, unknown>;
conversationId = String(createData.id || "");
if (!conversationId) {
return this.errorResponse(
502,
"Yuanbao did not return a conversation id",
CREATE_URL
);
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
log?.error?.("YUANBAO-WEB", `Conversation creation failed: ${message}`);
return this.errorResponse(
502,
`Yuanbao connection failed: ${sanitizeErrorMessage(message)}`,
CREATE_URL
);
}
// ── Step 2: send message ────────────────────────────────────────────────
const messageUrl = `${CHAT_URL}/${conversationId}`;
const chatBody: Record<string, unknown> = {
model: "gpt_175B_0404",
prompt,
plugin: "Adaptive",
displayPrompt: prompt,
displayPromptType: 1,
options: {
imageIntention: {
needIntentionModel: true,
backendUpdateFlag: 2,
intentionStatus: true,
},
},
multimedia: [],
agentId: DEFAULT_AGENT_ID,
supportHint: 1,
version: "v2",
chatModelId: modelSpec.chatModelId,
};
if (modelSpec.supportFunctions) chatBody.supportFunctions = modelSpec.supportFunctions;
const chatHeaders: Record<string, string> = {
...baseHeaders,
"Content-Type": "application/json",
Accept: "text/event-stream",
};
mergeUpstreamExtraHeaders(chatHeaders, upstreamExtraHeaders);
let upstreamResponse: Response;
try {
upstreamResponse = await fetch(messageUrl, {
method: "POST",
headers: chatHeaders,
body: JSON.stringify(chatBody),
signal: combinedSignal,
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
log?.error?.("YUANBAO-WEB", `Message send failed: ${message}`);
return this.errorResponse(
502,
`Yuanbao connection failed: ${sanitizeErrorMessage(message)}`,
messageUrl
);
}
if (!upstreamResponse.ok) {
const status = upstreamResponse.status;
const upstreamError = await readUpstreamErrorDetails(upstreamResponse);
let message = `Yuanbao returned HTTP ${status}`;
if (status === 401 || status === 403) {
message = "Yuanbao auth failed — session cookie may be expired.";
} else if (status === 429) {
message = "Yuanbao rate limited. Wait a moment and retry.";
}
if (upstreamError.message) message = `${message}: ${upstreamError.message}`;
return this.errorResponse(status, message, messageUrl, upstreamError.details);
}
if (!upstreamResponse.body) {
return this.errorResponse(502, "Yuanbao returned empty response body", messageUrl);
}
// ── Step 3: translate SSE → OpenAI ──────────────────────────────────────
const id = `chatcmpl-yuanbao-${crypto.randomUUID().slice(0, 12)}`;
const created = Math.floor(Date.now() / 1000);
if (stream) {
return {
response: new Response(
transformYuanbaoStream(upstreamResponse.body, resolvedModel, id, created, signal, log),
{
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
},
}
),
url: messageUrl,
headers: chatHeaders,
transformedBody: chatBody,
};
}
const { content, reasoning } = await collectYuanbaoResponse(upstreamResponse.body, signal);
const completionTokens = estimateTokens(content + reasoning);
const messagePayload: Record<string, unknown> = { role: "assistant", content };
if (reasoning) messagePayload.reasoning_content = reasoning;
return {
response: new Response(
JSON.stringify({
id,
object: "chat.completion",
created,
model: resolvedModel,
choices: [{ index: 0, message: messagePayload, finish_reason: "stop" }],
usage: {
prompt_tokens: estimateTokens(prompt),
completion_tokens: completionTokens,
total_tokens: estimateTokens(prompt) + completionTokens,
},
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
),
url: messageUrl,
headers: chatHeaders,
transformedBody: chatBody,
};
}
}
// ── SSE translation helpers ───────────────────────────────────────────────────
interface YuanbaoEvent {
type?: string;
content?: string;
msg?: string;
stopReason?: string;
}
function parseYuanbaoDataLine(line: string): YuanbaoEvent | null {
if (!line.startsWith("data: ")) return null;
const payload = line.slice(6).trim();
if (!payload || payload === "[DONE]" || !payload.startsWith("{")) return null;
try {
return JSON.parse(payload) as YuanbaoEvent;
} catch {
return null;
}
}
function transformYuanbaoStream(
upstream: ReadableStream<Uint8Array>,
model: string,
id: string,
created: number,
signal: AbortSignal | null | undefined,
log?: ExecuteInput["log"]
): ReadableStream<Uint8Array> {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
let roleEmitted = false;
return new ReadableStream({
async start(controller) {
const reader = upstream.getReader();
let buffer = "";
const emit = (delta: object, finish?: string | null) => {
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify({
id,
object: "chat.completion.chunk",
created,
model,
choices: [{ index: 0, delta, finish_reason: finish ?? null }],
})}\n\n`
)
);
};
const ensureRole = () => {
if (!roleEmitted) {
roleEmitted = true;
emit({ role: "assistant", content: "" });
}
};
try {
while (true) {
if (signal?.aborted) break;
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
const event = parseYuanbaoDataLine(line);
if (!event) continue;
if (event.type === "think" && event.content) {
ensureRole();
emit({ reasoning_content: event.content });
} else if (event.type === "text" && typeof event.msg === "string" && event.msg) {
ensureRole();
emit({ content: event.msg });
}
}
}
} catch (err) {
log?.error?.("YUANBAO-WEB", `Stream error: ${err}`);
} finally {
ensureRole();
emit({}, "stop");
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
reader.releaseLock();
}
},
});
}
async function collectYuanbaoResponse(
upstream: ReadableStream<Uint8Array>,
signal: AbortSignal | null | undefined
): Promise<{ content: string; reasoning: string }> {
const decoder = new TextDecoder();
const reader = upstream.getReader();
let buffer = "";
let content = "";
let reasoning = "";
try {
while (true) {
if (signal?.aborted) break;
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
const event = parseYuanbaoDataLine(line);
if (!event) continue;
if (event.type === "think" && event.content) reasoning += event.content;
else if (event.type === "text" && typeof event.msg === "string") content += event.msg;
}
}
} finally {
reader.releaseLock();
}
return { content, reasoning };
}

View File

@@ -374,6 +374,7 @@ export async function handleChatCore({
skipUpstreamRetry = false,
createPiiTransform = null,
correlationId = null,
modelPinned = false,
}) {
let { provider, model, extendedContext } = modelInfo;
// ── Memory pressure guard ────────────────────────────────────────────
@@ -583,6 +584,7 @@ export async function handleChatCore({
isResponsesEndpoint,
nativeCodexPassthrough,
isDroidCLI,
isOpencodeClient,
copilotCompatibleReasoning,
clientResponseFormat,
} = resolveChatCoreRequestFormat({ clientRawRequest, body, provider, userAgent });
@@ -805,6 +807,7 @@ export async function handleChatCore({
apiKeyInfo,
noLogEnabled,
correlationId,
modelPinned,
});
// Primary path: merge client model id + alias target so config on either key applies; resolved
@@ -873,9 +876,16 @@ export async function handleChatCore({
// sourceFormat="claude" applies the Anthropic Messages spec default (stream=false
// when body omits stream), preventing STREAM_EARLY_EOF on /v1/messages when
// clients send Accept: */* without an explicit stream flag.
// providerRequiresStreaming: providers with forceStream:true reject stream:false
// upstream (HTTP 400); keep streaming so OmniRoute can convert the stream to JSON
// for the client via handleForcedSSEToJson. (#2081)
// providerRequiresStreaming: providers with forceStream:true (cline/clinepass)
// only implement upstream streaming — a non-streaming request returns
// "generateText is not implemented" / an empty body. This flag forces the
// UPSTREAM request to stream (see `upstreamStream` below), but it MUST NOT
// force the client-facing `stream` flag: a stream:false client (e.g. the
// model-test button, plain JSON API callers) still expects a JSON response.
// The client-side `if (!stream)` branch drains the forced upstream SSE and
// converts it back to JSON via readNonStreamingResponseBody. Passing this
// flag into resolveStreamFlag would force `stream=true` and skip that
// conversion, yielding STREAM_EARLY_EOF for JSON callers. (#2081, #6126)
const providerRequiresStreaming = REGISTRY[provider]?.forceStream === true;
const stream =
nativeCodexPassthrough && isCompactResponsesEndpoint(endpointPath)
@@ -883,7 +893,6 @@ export async function handleChatCore({
: resolveStreamFlag(body?.stream, acceptHeader, sourceFormat, {
userAgent: streamUserAgent,
streamDefaultMode: apiKeyInfo?.streamDefaultMode,
providerRequiresStreaming,
});
// `settings` is already consolidated once near the top of handleChatCore
@@ -1588,7 +1597,13 @@ export async function handleChatCore({
headers: clientRawRequest?.headers,
userAgent,
});
const upstreamStream = stream || isClaudeCodeCompatible;
// `forceStream` providers (e.g. Cline / ClinePass) only implement upstream
// streaming — a non-streaming request returns "generateText is not implemented"
// / an empty body. Force the upstream request to stream even when the client
// wants JSON; the non-streaming branch below accumulates the SSE and converts
// it back to JSON (same mechanism already used for Claude-Code-compatible
// providers via isClaudeCodeCompatible).
const upstreamStream = stream || isClaudeCodeCompatible || providerRequiresStreaming;
let ccSessionId: string | null = null;
const stripTypes = getStripTypesForProviderModel(provider || "", model || "");
@@ -2224,6 +2239,7 @@ export async function handleChatCore({
targetFormat,
credentials,
log,
bypassDefaultToolLimit: isOpencodeClient,
});
updatePendingScope(pendingScope, {

View File

@@ -51,6 +51,7 @@ export type PersistAttemptLogsContext = {
apiKeyInfo: { id?: string | null; name?: string | null } | null | undefined;
noLogEnabled: unknown;
correlationId?: string | null;
modelPinned?: boolean;
};
function toConnectionId(value: unknown): string | null {
@@ -108,6 +109,7 @@ export function persistAttemptLogs(args: PersistAttemptLogsArgs, ctx: PersistAtt
apiKeyInfo,
noLogEnabled,
correlationId,
modelPinned,
} = ctx;
const initialConnectionId = toConnectionId(connectionId);
const finalConnectionId = toConnectionId(credentials?.connectionId) || initialConnectionId;
@@ -203,5 +205,6 @@ export function persistAttemptLogs(args: PersistAttemptLogsArgs, ctx: PersistAtt
noLog: noLogEnabled,
pipelinePayloads,
correlationId,
modelPinned: modelPinned || false,
}).catch(() => {});
}

View File

@@ -37,6 +37,33 @@ function isCopilotClient(
return false;
}
function isOpencodeClient(
headers: Headers | Record<string, unknown> | null | undefined,
userAgent?: string | null
): boolean {
const matchesUserAgent = (value: unknown) =>
typeof value === "string" && value.toLowerCase().includes("opencode");
const matchesHeaderKey = (key: string) => key.toLowerCase().startsWith("x-opencode-");
if (matchesUserAgent(userAgent)) return true;
if (headers instanceof Headers) {
for (const [key, value] of headers as unknown as Iterable<[string, string]>) {
if (matchesHeaderKey(key) || (key.toLowerCase() === "user-agent" && matchesUserAgent(value))) {
return true;
}
}
} else if (headers && typeof headers === "object") {
for (const [key, value] of Object.entries(headers)) {
if (matchesHeaderKey(key) || (key.toLowerCase() === "user-agent" && matchesUserAgent(value))) {
return true;
}
}
}
return false;
}
/**
* Resolve the per-request endpoint/format facts at the top of handleChatCore. Pure: a function of
* the inbound endpoint, the (possibly already-mutated) body, the resolved provider, and the
@@ -64,6 +91,7 @@ export function resolveChatCoreRequestFormat(opts: {
const isDroidCLI =
userAgent?.toLowerCase().includes("droid") || userAgent?.toLowerCase().includes("codex-cli");
const copilotCompatibleReasoning = isCopilotClient(clientRawRequest?.headers, userAgent);
const isOpencodeClientRequest = isOpencodeClient(clientRawRequest?.headers, userAgent);
const clientResponseFormat =
sourceFormat === FORMATS.OPENAI_RESPONSES && !isResponsesEndpoint && !isDroidCLI
? FORMATS.OPENAI
@@ -75,6 +103,7 @@ export function resolveChatCoreRequestFormat(opts: {
nativeCodexPassthrough,
isDroidCLI,
copilotCompatibleReasoning,
isOpencodeClient: isOpencodeClientRequest,
clientResponseFormat,
};
}

View File

@@ -14,7 +14,7 @@ import {
applyConfiguredPayloadRules,
resolvePayloadRuleProtocols,
} from "../../services/payloadRules.ts";
import { getEffectiveToolLimit } from "../../services/toolLimitDetector.ts";
import { getEffectiveToolLimit, getKnownToolLimit } from "../../services/toolLimitDetector.ts";
import { providerSupportsCaching } from "../../utils/cacheControlPolicy.ts";
import { FORMATS } from "../../translator/formats.ts";
@@ -41,15 +41,35 @@ function buildAppliedRulesSummary(
function truncateToolList(
bodyToSend: Body,
provider: string | null | undefined,
bypassDefaultToolLimit: boolean,
log?: LoggerLike
): Body {
if (!Array.isArray(bodyToSend.tools)) return bodyToSend;
const knownLimit = getKnownToolLimit(provider);
if (knownLimit !== null) {
if (bodyToSend.tools.length > knownLimit) {
const originalCount = bodyToSend.tools.length;
const truncatedTools = bodyToSend.tools.slice(0, knownLimit);
bodyToSend = { ...bodyToSend, tools: truncatedTools };
log?.debug?.(
"TOOL_LIMIT",
`Truncated ${originalCount} tools to ${knownLimit} for ${provider}`
);
}
return bodyToSend;
}
if (bypassDefaultToolLimit === true) return bodyToSend;
const effectiveToolLimit = getEffectiveToolLimit(provider);
if (Array.isArray(bodyToSend.tools) && bodyToSend.tools.length > effectiveToolLimit) {
if (bodyToSend.tools.length > effectiveToolLimit) {
const originalCount = bodyToSend.tools.length;
const truncatedTools = bodyToSend.tools.slice(0, effectiveToolLimit);
bodyToSend = { ...bodyToSend, tools: truncatedTools };
log?.debug?.(
"TOOL_LIMIT",
`Truncated ${(bodyToSend.tools as unknown[]).length} tools to ${effectiveToolLimit} for ${provider}`
`Truncated ${originalCount} tools to ${effectiveToolLimit} for ${provider}`
);
}
return bodyToSend;
@@ -104,9 +124,18 @@ export async function prepareUpstreamBody(opts: {
provider: string | null | undefined;
targetFormat: string;
credentials: CredentialsLike;
bypassDefaultToolLimit?: boolean;
log?: LoggerLike;
}): Promise<Body> {
const { translatedBody, modelToCall, provider, targetFormat, credentials, log } = opts;
const {
translatedBody,
modelToCall,
provider,
targetFormat,
credentials,
bypassDefaultToolLimit = false,
log,
} = opts;
let bodyToSend: Body =
translatedBody.model === modelToCall
@@ -131,7 +160,7 @@ export async function prepareUpstreamBody(opts: {
);
}
bodyToSend = truncateToolList(bodyToSend, provider, log);
bodyToSend = truncateToolList(bodyToSend, provider, bypassDefaultToolLimit ?? false, log);
bodyToSend = backfillQwenOAuthUser(bodyToSend, provider, credentials, log);
bodyToSend = await injectPromptCacheKey(bodyToSend, provider, targetFormat);

View File

@@ -43,6 +43,9 @@ export async function handleChatGptWebImageGeneration({
log,
signal,
clientHeaders,
// Injectable so unit tests can drive the handler without a live ChatGPT
// session; production uses the real executor.
executorFactory = () => new ChatGptWebExecutor(),
}) {
const startTime = Date.now();
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
@@ -98,7 +101,7 @@ export async function handleChatGptWebImageGeneration({
};
for (let i = 0; i < requestedCount; i++) {
const executor = new ChatGptWebExecutor();
const executor = executorFactory();
const result = await executor.execute({
model,
body: {
@@ -124,21 +127,30 @@ export async function handleChatGptWebImageGeneration({
}
let content = "";
let imageResolutionFailed = false;
try {
const json = JSON.parse(responseText);
content = String(json?.choices?.[0]?.message?.content || "");
imageResolutionFailed = json?.x_image_resolution_failed === true;
} catch {
content = responseText;
}
const urls = extractMarkdownImageUrls(content);
if (urls.length === 0) {
// Distinguish "image was generated upstream but OmniRoute could not
// retrieve it" (executor flagged the unresolved asset pointer) from
// "no image was produced at all" — the former is our bug/limitation,
// not a failed prompt, so the message must not read as "no image made".
const error = imageResolutionFailed
? `ChatGPT Web generated an image but OmniRoute could not retrieve it (the image asset could not be downloaded — the URL may have expired or ChatGPT changed its image delivery format). Please retry; if it persists, report it. Assistant text: ${content.slice(0, 200)}`
: `ChatGPT Web completed without returning image markdown: ${content.slice(0, 300)}`;
return saveImageErrorResult({
provider,
model,
status: 502,
startTime,
error: `ChatGPT Web completed without returning image markdown: ${content.slice(0, 300)}`,
error,
requestBody,
});
}

View File

@@ -67,6 +67,7 @@ import { handlePickFastestModel } from "./tools/pickFastestModel.ts";
import { memoryTools } from "./tools/memoryTools.ts";
import { skillTools } from "./tools/skillTools.ts";
import { agentSkillTools } from "./tools/agentSkillTools.ts";
import { githubSkillTools } from "./tools/githubSkillTools.ts";
import { skillRegistry } from "../../src/lib/skills/registry.ts";
import { skillExecutor } from "../../src/lib/skills/executor.ts";
import { pluginTools } from "./tools/pluginTools.ts";
@@ -102,6 +103,7 @@ const TOTAL_MCP_TOOL_COUNT =
Object.keys(memoryTools).length +
Object.keys(skillTools).length +
Object.keys(agentSkillTools).length +
Object.keys(githubSkillTools).length +
Object.keys(poolTools).length +
gamificationTools.length +
pluginTools.length +
@@ -996,11 +998,11 @@ export function createMcpServer(): McpServer {
},
withScopeEnforcement(
toolDef.name,
async (args) => {
async (args, extra) => {
try {
const parsedArgs = toolDef.inputSchema.parse(args ?? {});
// @ts-ignore - handler type lost through dynamic Object.values() access
const result = await toolDef.handler(parsedArgs);
const result = await toolDef.handler(parsedArgs, extra);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
@@ -1023,11 +1025,11 @@ export function createMcpServer(): McpServer {
},
withScopeEnforcement(
toolDef.name,
async (args) => {
async (args, extra) => {
try {
const parsedArgs = toolDef.inputSchema.parse(args ?? {});
// @ts-ignore - handler type lost through dynamic Object.values() access
const result = await toolDef.handler(parsedArgs);
const result = await toolDef.handler(parsedArgs, extra);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
@@ -1041,6 +1043,29 @@ export function createMcpServer(): McpServer {
// ── Agent Skill Tools ─────────────────────────
Object.values(agentSkillTools).forEach((toolDef) => {
server.registerTool(
toolDef.name,
{
description: toolDef.description,
// @ts-ignore: dynamic zod access
inputSchema: toolDef.inputSchema,
},
withScopeEnforcement(toolDef.name, async (args, extra) => {
try {
const parsedArgs = toolDef.inputSchema.parse(args ?? {});
// @ts-expect-error - handler type lost through dynamic Object.values() access
const result = await toolDef.handler(parsedArgs, extra);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
})
);
});
// ── GitHub Skill Tools ──────────────────────────
Object.values(githubSkillTools).forEach((toolDef) => {
server.registerTool(
toolDef.name,
{
@@ -1058,7 +1083,7 @@ export function createMcpServer(): McpServer {
const msg = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
})
}, toolDef.scopes)
);
});
@@ -1073,11 +1098,11 @@ export function createMcpServer(): McpServer {
},
withScopeEnforcement(
toolDef.name,
async (args) => {
async (args, extra) => {
try {
const parsedArgs = toolDef.inputSchema.parse(args ?? {});
// @ts-ignore: handler expected specific object
const result = await toolDef.handler(parsedArgs);
const result = await toolDef.handler(parsedArgs, extra);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
@@ -1100,11 +1125,11 @@ export function createMcpServer(): McpServer {
},
withScopeEnforcement(
toolDef.name,
async (args) => {
async (args, extra) => {
try {
const parsedArgs = toolDef.inputSchema.parse(args ?? {});
// @ts-ignore - handler type lost through dynamic Object.values() access
const result = await toolDef.handler(parsedArgs);
const result = await toolDef.handler(parsedArgs, extra);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
@@ -1125,7 +1150,7 @@ export function createMcpServer(): McpServer {
description: string;
scopes: readonly string[];
inputSchema: { parse: (input: unknown) => unknown };
handler: (parsedArgs: unknown) => Promise<unknown>;
handler: (parsedArgs: unknown, extra?: unknown) => Promise<unknown>;
}) => {
server.registerTool(
toolDef.name,
@@ -1136,10 +1161,10 @@ export function createMcpServer(): McpServer {
},
withScopeEnforcement(
toolDef.name,
async (args) => {
async (args, extra) => {
try {
const parsedArgs = toolDef.inputSchema.parse(args ?? {});
const result = await toolDef.handler(parsedArgs);
const result = await toolDef.handler(parsedArgs, extra);
return {
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
};
@@ -1165,11 +1190,11 @@ export function createMcpServer(): McpServer {
},
withScopeEnforcement(
toolDef.name,
async (args) => {
async (args, extra) => {
try {
const parsedArgs = toolDef.inputSchema.parse(args ?? {});
// @ts-ignore: handler expected specific object
const result = await toolDef.handler(parsedArgs);
const result = await toolDef.handler(parsedArgs, extra);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
@@ -1192,11 +1217,11 @@ export function createMcpServer(): McpServer {
},
withScopeEnforcement(
toolDef.name,
async (args) => {
async (args, extra) => {
try {
const parsedArgs = toolDef.inputSchema.parse(args ?? {});
// @ts-ignore: handler expected specific object
const result = await toolDef.handler(parsedArgs);
const result = await toolDef.handler(parsedArgs, extra);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
@@ -1219,11 +1244,11 @@ export function createMcpServer(): McpServer {
},
withScopeEnforcement(
toolDef.name,
async (args) => {
async (args, extra) => {
try {
const parsedArgs = toolDef.inputSchema.parse(args ?? {});
// @ts-ignore: handler expected specific object
const result = await toolDef.handler(parsedArgs);
const result = await toolDef.handler(parsedArgs, extra);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);

View File

@@ -0,0 +1,140 @@
/**
* githubSkillTools.ts — MCP tools for GitHub agent skill discovery and import.
*
* Provides tools to:
* - Search GitHub for repos with SKILL.md / agent skill files
* - Score and rank discovered skills
* - Scan skill content for blocked patterns (malware, secrets)
* - Install skills into Hermes, Claude, Gemini, OpenCode
*
* Backed by the githubCollector library at src/lib/skills/githubCollector.ts.
*/
import { z } from "zod";
import {
searchGitHubSkills,
scanText,
resolveInstallPath,
GitHubSkillsSearchSchema,
GitHubSkillsScanSchema,
GitHubSkillsInstallSchema,
INSTALL_TARGETS,
type GitHubSkillRepo,
type SkillInstallResult,
} from "@/lib/skills/githubCollector";
// ── Handlers ─────────────────────────────────────────────────────────────────
async function handleSearch(args: z.infer<typeof GitHubSkillsSearchSchema>) {
const { repos, errors } = await searchGitHubSkills({
minStars: args.minStars,
maxResults: args.maxResults,
});
let filtered = repos;
if (args.minScore > 0) filtered = filtered.filter((r) => r.score >= args.minScore);
if (args.query) {
const q = args.query.toLowerCase();
filtered = filtered.filter(
(r) => r.fullName.toLowerCase().includes(q) || r.description.toLowerCase().includes(q)
);
}
return {
skills: filtered.map((r: GitHubSkillRepo) => ({
fullName: r.fullName,
stars: r.stars,
score: r.score,
description: r.description.slice(0, 200),
topics: r.topics,
htmlUrl: r.htmlUrl,
hasSkillFile: r.hasSkillFile,
license: r.license,
})),
total: filtered.length,
errors: errors.length > 0 ? errors : undefined,
};
}
async function handleScan(args: z.infer<typeof GitHubSkillsScanSchema>) {
const findings = scanText(args.content, args.repoName);
return {
repoName: args.repoName,
clean: findings.length === 0,
findings: findings.map((f) => ({
pattern: f.pattern,
context: f.context,
})),
};
}
async function handleInstall(args: z.infer<typeof GitHubSkillsInstallSchema>) {
const results: SkillInstallResult[] = [];
const skillName = args.repoName.split("/").pop() || args.repoName;
for (const target of args.targets) {
try {
const dest = resolveInstallPath(target, skillName, args.description);
// In a real implementation, this would clone the repo and copy files.
// For now, we return the planned install path as a dry-run result.
results.push({
target,
ok: true,
action: "installed",
destDir: dest,
});
} catch (err) {
results.push({
target,
ok: false,
action: "error",
error: (err as Error).message,
});
}
}
return {
repoName: args.repoName,
skillName,
results,
allOk: results.every((r) => r.ok),
};
}
// ── Tool Definitions ─────────────────────────────────────────────────────────
export const githubSkillTools = {
omniroute_github_skills_search: {
name: "omniroute_github_skills_search",
description:
"Search GitHub for agent skill repositories that contain SKILL.md, CLAUDE.md, .cursorrules, or similar agent configuration files. " +
"Returns scored results sorted by relevance. Scores are 0.01.0 based on stars, name signals, description keywords, and topic tags. " +
"Ideal for discovering community agent skills from GitHub.",
inputSchema: GitHubSkillsSearchSchema,
scopes: ["read:skills"],
handler: handleSearch,
},
omniroute_github_skills_scan: {
name: "omniroute_github_skills_scan",
description:
"Scan SKILL.md or README content from a GitHub repo for blocked patterns including eval(base64), " +
"hardcoded secrets (API keys, passwords, private keys), dangerous function calls (os.system, subprocess.Popen), " +
"and other malware indicators. Returns findings with context or 'clean' status.",
inputSchema: GitHubSkillsScanSchema,
scopes: ["read:skills"],
handler: handleScan,
},
omniroute_github_skills_install: {
name: "omniroute_github_skills_install",
description:
"Preview or plan the installation of a discovered GitHub skill into one or more agent directories " +
"(Hermes: ~/AppData/Local/hermes/skills/, Claude: ~/.claude/skills/, Gemini: ~/.gemini/skills/, " +
"OpenCode: ~/.opencode/skills/). Categorizes the skill based on its name and description. " +
"Returns the target paths where the skill would be installed.",
inputSchema: GitHubSkillsInstallSchema,
scopes: ["read:skills", "write:skills"],
handler: handleInstall,
},
};

View File

@@ -0,0 +1,26 @@
/**
* Built-in provider ids that must adopt the dynamic Claude-Code wire image
* (fingerprint headers/order + system transforms + `?beta=true` chat path)
* WITHOUT inheriting the Claude-Code-Compatible family's default anthropic
* baseUrl / Bearer auth.
*
* These providers keep their own registry `baseUrl` and auth scheme
* (e.g. `agentrouter` → `https://agentrouter.org/v1/messages` + `x-api-key`),
* while the two CC predicates (`isClaudeCodeCompatible` /
* `isClaudeCodeCompatibleProvider`) and `applyFingerprint` treat them as CC
* for the wire-image concerns only. The CC-baseUrl / CC-Bearer branches in
* `buildProviderUrl` / `buildProviderHeaders` are guarded so the registry
* baseUrl + auth are preserved.
*
* Single source of truth — imported by both predicates so they never diverge.
* See issue #6056.
*/
export const CC_WIRE_IMAGE_BUILTINS: ReadonlySet<string> = new Set(["agentrouter"]);
/**
* True when `provider` is a built-in that adopts the dynamic Claude-Code wire
* image while keeping its own registry baseUrl + auth.
*/
export function usesCcWireImage(provider: unknown): boolean {
return typeof provider === "string" && CC_WIRE_IMAGE_BUILTINS.has(provider);
}

View File

@@ -15,6 +15,7 @@ import {
import { applyClaudeCodeCompatibleThinkingDisplay } from "./claudeCodeCompatibleThinkingDisplay.ts";
import { obfuscateInBody } from "./claudeCodeObfuscation.ts";
import { applySystemTransformPipeline, PROVIDER_CC_BRIDGE } from "./systemTransforms.ts";
import { usesCcWireImage } from "./ccWireImageBuiltins.ts";
import {
fixToolPairs,
fixToolAdjacency,
@@ -95,7 +96,12 @@ function supportsClaudeXHighEffort(model: string | null | undefined): boolean {
}
export function isClaudeCodeCompatibleProvider(provider: string | null | undefined): boolean {
return typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX);
return (
(typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX)) ||
// Built-in providers (e.g. agentrouter) that adopt the dynamic CC wire image
// while keeping their own registry baseUrl + auth (#6056).
usesCcWireImage(provider)
);
}
export function stripAnthropicMessagesSuffix(baseUrl: string | null | undefined): string {

View File

@@ -12,6 +12,7 @@ import {
formatRetryAfter,
getModelLockoutInfo,
getRuntimeProviderProfile,
hasPerModelQuota,
isModelLocked,
recordModelLockoutFailure,
recordProviderFailure,
@@ -58,7 +59,11 @@ import { handlePipelineCombo, buildPipelineResponse } from "./autoCombo/pipeline
import { type ProviderCandidate } from "./autoCombo/scoring.ts";
import { estimateTokens } from "./contextManager.ts";
import { getSessionConnection } from "./sessionManager.ts";
import { applySessionStickiness, recordStickyBinding } from "./combo/sessionStickiness.ts";
import {
applySessionStickiness,
recordStickyBinding,
resolveDisableSessionStickiness,
} from "./combo/sessionStickiness.ts";
import { selectQuotaShareTarget } from "./combo/quotaShareStrategy.ts";
import { makeConnectionConcurrencyResolver, lookupPositiveCap } from "./combo/concurrencyCaps.ts";
import { acquireQuotaShareConcurrencySlot } from "./combo/quotaShareConcurrency.ts";
@@ -103,7 +108,11 @@ import {
getStickyWeightedExecutionKey,
recordStickyWeightedSuccess,
} from "./combo/rrState.ts";
import { validateResponseQuality, toRetryAfterDisplayValue } from "./combo/validateQuality.ts";
import {
validateResponseQuality,
releaseQualityClone,
toRetryAfterDisplayValue,
} from "./combo/validateQuality.ts";
import { resolveComboCooldownWaitDecision } from "./combo/comboCooldownRetry.ts";
import {
computeClosestRetryAfter,
@@ -701,7 +710,50 @@ export async function handleComboChat({
"COMBO",
`Bypassing strategy — routing directly to pinned context model: ${pinnedModel}`
);
return handleSingleModelWithTimeout(body, pinnedModel);
let pinnedResult: Response | null = null;
try {
pinnedResult = await handleSingleModelWithTimeout(body, pinnedModel, {
modelPinned: true,
} as SingleModelTarget);
} catch (pinErr) {
log.warn(
"COMBO",
`Pinned model ${pinnedModel} threw error: ${pinErr instanceof Error ? pinErr.message : String(pinErr)}, falling through to combo retry/fallback`
);
}
if (pinnedResult) {
if (pinnedResult.ok) {
let pinnedClone: Response;
try {
pinnedClone = pinnedResult.clone();
} catch {
pinnedClone = pinnedResult;
}
const pinnedQuality = await validateResponseQuality(
pinnedClone,
clientRequestedStream,
log,
config.responseValidation
);
releaseQualityClone(pinnedClone, pinnedResult, pinnedQuality);
if (pinnedQuality.valid) return pinnedResult;
log.warn(
"COMBO",
`Pinned model ${pinnedModel} returned 200 but failed quality check: ${pinnedQuality.reason}, falling through to combo retry/fallback`
);
} else {
const pinnedStatus = pinnedResult.status || 500;
if (![408, 429, 500, 502, 503, 504].includes(pinnedStatus)) {
return pinnedResult;
}
log.warn(
"COMBO",
`Pinned model ${pinnedModel} failed (${pinnedStatus}), falling through to combo retry/fallback`
);
}
}
// Fall through to the target iteration loop below — retries and sibling
// models will be tried via the normal combo machinery.
}
log.warn(
"COMBO",
@@ -709,7 +761,8 @@ export async function handleComboChat({
? `Context-cache pin "${pinnedModel}" provider durably unhealthy — dropping pin, using strategy`
: `Stale context-cache pin "${pinnedModel}" not in combo "${combo.name}" targets — dropping pin, using strategy`
);
return handleSingleModelWithTimeout(body, pinnedModel);
// Fall through to the normal target iteration loop below — the pin is
// dropped, so the combo strategy picks the best available target.
}
// Fusion strategy: parallel panel + judge synthesis. Handled in a separate module
@@ -1069,10 +1122,20 @@ export async function handleComboChat({
apiKeyAllowedConnections,
});
}
const _sticky = await applySessionStickiness(
orderedTargets,
body.messages as Array<{ role?: string; content?: unknown }>
// #6168: session stickiness opt-out. Per-combo `config.disableSessionStickiness`
// overrides the global `settings.disableSessionStickiness` fallback (default false,
// preserving the #3825 prompt-cache/504 fix). When disabled, skip the reorder and
// treat the result as a no-op so the recordStickyBinding write-back below is skipped.
const disableSessionStickiness = resolveDisableSessionStickiness(
config as Record<string, unknown> | null | undefined,
settings as Record<string, unknown> | null | undefined
);
const _sticky = disableSessionStickiness
? ({ targets: orderedTargets, messageHash: null, stuck: false } as const)
: await applySessionStickiness(
orderedTargets,
body.messages as Array<{ role?: string; content?: unknown }>
);
orderedTargets = _sticky.targets;
orderedTargets = orderTargetsByEvalScores(orderedTargets, config.evalRouting, log);
orderedTargets = filterTargetsByRequestCompatibility(orderedTargets, body, log);
@@ -1490,12 +1553,22 @@ export async function handleComboChat({
undefined;
const effectiveConnectionId = selectedConnectionId || target.connectionId || "";
// Clone BEFORE quality check — validateResponseQuality reads the body
// via getReader() which locks the stream. The clone's body is consumed
// by the quality check; the original stays unlocked for piping.
let qualityClone: Response;
try {
qualityClone = result.clone();
} catch {
qualityClone = result;
}
const quality = await validateResponseQuality(
result,
qualityClone,
clientRequestedStream,
log,
config.responseValidation
);
releaseQualityClone(qualityClone, result, quality);
if (!quality.valid) {
log.warn(
"COMBO",
@@ -1730,7 +1803,7 @@ export async function handleComboChat({
})();
}
return { ok: true, response: quality.clonedResponse ?? result };
return { ok: true, response: result };
}
// Extract error info from response
@@ -2009,7 +2082,16 @@ export async function handleComboChat({
}
log.warn("COMBO", `Model ${modelStr} failed, trying next`, { status: result.status });
if (resilienceSettings.providerCooldown.enabled && provider && provider !== "unknown") {
// #5976: per-model-quota providers (Gemini, GitHub, etc.) multiplex models
// behind one connection. A model-level 500 must NOT cool down the entire
// provider — sibling models may still succeed. Skip cooldown recording for
// these providers on 500 errors so the next target can try.
if (
resilienceSettings.providerCooldown.enabled &&
provider &&
provider !== "unknown" &&
!(result.status === 500 && hasPerModelQuota(provider, rawModel))
) {
recordProviderCooldown(
provider,
targetWithConnection.connectionId ?? undefined,
@@ -2389,10 +2471,19 @@ async function handleRoundRobinCombo({
// call — so sessionless RR combos rotated every turn, busting the upstream prompt-cache.
// Reuse the SAME mechanism: start the rotation at the conversation's sticky connection
// (the loop still falls through to the other targets on failure → failover preserved).
const _rrSessionSticky = await applySessionStickiness(
filteredTargets,
body?.messages as Array<{ role?: string; content?: unknown }>
// #6168: honor the session-stickiness opt-out here too, otherwise round-robin would
// still pin the conversation even when the flag is set. Per-combo `config` overrides
// the global `settings.disableSessionStickiness` fallback (default false).
const disableSessionStickiness = resolveDisableSessionStickiness(
config as Record<string, unknown> | null | undefined,
settings as Record<string, unknown> | null | undefined
);
const _rrSessionSticky = disableSessionStickiness
? ({ targets: filteredTargets, messageHash: null, stuck: false } as const)
: await applySessionStickiness(
filteredTargets,
body?.messages as Array<{ role?: string; content?: unknown }>
);
let rrStartIndex = startIndex;
if (_rrSessionSticky.stuck) {
const stickyIdx = filteredTargets.findIndex(
@@ -2549,12 +2640,19 @@ async function handleRoundRobinCombo({
// Success — validate response quality before returning
if (result.ok) {
let rrClone: Response;
try {
rrClone = result.clone();
} catch {
rrClone = result;
}
const quality = await validateResponseQuality(
result,
rrClone,
clientRequestedStream,
log,
config.responseValidation
);
releaseQualityClone(rrClone, result, quality);
if (!quality.valid) {
log.warn(
"COMBO-RR",
@@ -2642,12 +2740,8 @@ async function handleRoundRobinCombo({
}
})();
}
// validateResponseQuality peeks streaming bodies via getReader(),
// which locks `result.body`. It returns a clonedResponse that replays
// the buffered prefix and forwards the rest. Returning the original
// (now-locked) `result` makes Next.js throw "ReadableStream is locked"
// → 500. Mirror the priority strategy and return the replay response.
return quality.clonedResponse ?? result;
// Clone is consumed by quality check; original stays unlocked.
return result;
}
// Extract error info
@@ -2824,7 +2918,12 @@ async function handleRoundRobinCombo({
if (offset > 0) fallbackCount++;
log.warn("COMBO-RR", `${modelStr} failed, trying next model`, { status: result.status });
if (resilienceSettings.providerCooldown.enabled && provider && provider !== "unknown") {
if (
resilienceSettings.providerCooldown.enabled &&
provider &&
provider !== "unknown" &&
!(result.status === 500 && hasPerModelQuota(provider, parseModel(modelStr).model || modelStr))
) {
recordProviderCooldown(
provider,
targetWithConnection.connectionId ?? undefined,

View File

@@ -2,7 +2,7 @@
import { errorResponse } from "../../utils/error.ts";
import { recordComboRequest } from "../comboMetrics.ts";
import { resolveDelayMs } from "./comboPredicates.ts";
import { validateResponseQuality } from "./validateQuality.ts";
import { validateResponseQuality, releaseQualityClone } from "./validateQuality.ts";
import type { ResponseValidationConfig } from "./responseValidation.ts";
import type {
ComboCollectionLike,
@@ -228,12 +228,19 @@ export async function executeRuntimeUnitCombo(args: {
});
return { response, unit };
}
let unitClone: Response;
try {
unitClone = response.clone();
} catch {
unitClone = response;
}
const quality = await validateResponseQuality(
response,
unitClone,
clientRequestedStream,
args.log,
args.config.responseValidation as ResponseValidationConfig | undefined
);
releaseQualityClone(unitClone, response, quality);
if (quality.valid) {
recordComboRequest(args.combo.name, unit.modelStr, {
success: true,
@@ -242,7 +249,7 @@ export async function executeRuntimeUnitCombo(args: {
strategy: effectiveStrategy,
target: { executionKey: unit.executionKey, stepId: unit.stepId, label: unit.label },
});
return { response: quality.clonedResponse ?? response, unit };
return { response, unit };
}
}
if (![408, 429, 500, 502, 503, 504].includes(response.status)) break;

View File

@@ -191,6 +191,26 @@ export function clearAllStickyBindings(): void {
stickyMap.clear();
}
/**
* #6168: resolve the session-stickiness opt-out for a combo request.
*
* Precedence (mirrors the `stickyRoundRobinLimit` resolution in combo.ts):
* per-combo `config.disableSessionStickiness` (boolean) →
* global `settings.disableSessionStickiness` (boolean) →
* default `false`.
*
* Default `false` preserves the #3825 prompt-cache/504 fix — only an explicit
* `true` at either level disables stickiness.
*/
export function resolveDisableSessionStickiness(
config: Record<string, unknown> | null | undefined,
settings: Record<string, unknown> | null | undefined
): boolean {
const perCombo = config?.disableSessionStickiness;
if (typeof perCombo === "boolean") return perCombo;
return settings?.disableSessionStickiness === true;
}
// ─── Core: apply stickiness to an ordered target list ────────────────────────
export interface ApplyStickinessResult {

View File

@@ -46,6 +46,8 @@ export type SingleModelTarget =
allowRateLimitedConnection?: boolean;
effectiveComboStrategy?: string | null;
modelAbortSignal?: AbortSignal | null;
/** True when this target was selected via context-cache session pinning. */
modelPinned?: boolean;
})
| { modelAbortSignal: AbortSignal };

View File

@@ -10,10 +10,7 @@ import {
createSSEDataLineNormalizer,
isKnownNonClaudeStreamPayload,
} from "../../utils/streamHelpers.ts";
import {
evaluateResponseValidation,
type ResponseValidationConfig,
} from "./responseValidation.ts";
import { evaluateResponseValidation, type ResponseValidationConfig } from "./responseValidation.ts";
import { getReasoningTokens } from "../../../src/lib/usage/tokenAccounting.ts";
import type { ComboRetryAfter } from "./types.ts";
@@ -99,6 +96,8 @@ export async function validateResponseQuality(
let hasMessageStart = false;
let hasContentBlock = false;
let hasLifecycleEnd = false;
let anyContentFound = false;
let sawAnyBytes = false;
const sseLineNormalizer = createSSEDataLineNormalizer();
let pendingEventType = "";
@@ -236,6 +235,20 @@ export async function validateResponseQuality(
return { valid: false, reason: "streaming empty content block" };
}
// Stream ended with a truly EMPTY body (e.g. Gemini returning HTTP
// 200 with zero bytes) — mark as invalid for combo failover so the
// sibling model gets tried. Streams that carried ANY SSE activity
// (an explicit `data: [DONE]`, ping/metadata events, an incomplete
// Claude lifecycle) keep the pass-through contract (#3399/#3685):
// those are handled by the stream-readiness timeout, not failover.
if (!anyContentFound && !hasContentBlock && !sawAnyBytes) {
log.warn?.(
"COMBO",
"Streaming response ended with no recognized content — marking as invalid for combo failover"
);
return { valid: false, reason: "streaming no recognized content" };
}
// Incomplete lifecycle or non-Claude stream — replay all buffered
// bytes. The reader is exhausted so the forwarding reader will
// immediately signal done.
@@ -245,12 +258,14 @@ export async function validateResponseQuality(
// Accumulate raw bytes for potential replay.
bufferedChunks.push(value);
if (value && value.length > 0) sawAnyBytes = true;
// Decode incrementally (stream:true keeps multi-byte char state).
decodedSoFar += decoder.decode(value, { stream: true });
const foundContent = parseAccumulatedSse();
if (foundContent) {
anyContentFound = true;
// A content_block_* event was found — stop peeking. Return a
// clonedResponse that replays all buffered bytes (the current chunk
// is already in bufferedChunks) and then forwards the remainder of
@@ -259,9 +274,23 @@ export async function validateResponseQuality(
return { valid: true, clonedResponse };
}
}
} catch {
// If reading the stream fails, pass through — other mechanisms
// (stream readiness timeout) will catch truly broken streams.
} catch (streamErr) {
// If reading the stream fails due to a locked stream or pipe error,
// the content cannot be verified — mark as invalid for combo failover.
// A locked ReadableStream means the response body is already consumed
// or corrupted (e.g. "Invalid state: The ReadableStream is locked").
// Broad match: Chrome/V8 throws "body used already", Firefox throws
// "ReadableStream is locked", etc.
const errMsg = streamErr instanceof Error ? streamErr.message : String(streamErr);
if (
streamErr instanceof TypeError &&
(errMsg.includes("locked") ||
errMsg.includes("disturbed") ||
errMsg.includes("used already"))
) {
return { valid: false, reason: "stream locked or disturbed" };
}
// Other read errors — pass through (stream readiness timeout will catch truly broken streams)
return { valid: true };
}
}
@@ -308,7 +337,8 @@ export async function validateResponseQuality(
const choices = json?.choices;
if (json?.object === "response") {
if (!responsesApiOutputHasContent(json.output)) return { valid: false, reason: "empty_choices" };
if (!responsesApiOutputHasContent(json.output))
return { valid: false, reason: "empty_choices" };
const status = typeof json.status === "string" ? json.status : "";
if (status && !["completed", "done"].includes(status)) {
return { valid: false, reason: "no_terminal" };
@@ -389,3 +419,27 @@ export async function validateResponseQuality(
}),
};
}
/**
* Release the peek-and-abandon clone used by {@link validateResponseQuality}.
*
* The quality check clones the upstream response, reads the clone only until the
* first content block, then hands back a `clonedResponse` that callers on the
* streaming path DISCARD (they forward the original, untouched response). Because
* a `Response.clone()` tees the body, that abandoned branch would otherwise buffer
* the entire remaining body in memory until the original finishes streaming.
*
* Cancelling the abandoned branch releases that buffer. Per the ReadableStream tee
* contract, cancelling one branch does NOT cancel the shared source while the other
* branch (the original response being streamed to the client) is still active, so
* this is safe. No-op when the clone fell back to the original (clone unsupported)
* or when quality reading already exhausted the body (no `clonedResponse`).
*/
export function releaseQualityClone(
clone: Response,
original: Response,
quality: { clonedResponse?: Response }
): void {
if (clone === original) return;
void quality.clonedResponse?.body?.cancel().catch(() => {});
}

View File

@@ -20,6 +20,32 @@
import { getGitHubCopilotChatHeaders } from "../config/providerHeaderProfiles.ts";
export const GITHUB_COPILOT_MODELS_URL = "https://api.githubcopilot.com/models";
export const GITHUB_COPILOT_MODEL_ALLOWLIST = [
"claude-fable-5",
"claude-opus-4.8-fast",
"claude-opus-4.8",
"claude-opus-4.7",
"claude-sonnet-4.6",
"claude-opus-4.5",
"claude-sonnet-5",
"claude-sonnet-4.5",
"claude-haiku-4.5",
"gemini-3.1-pro-preview",
"gemini-3.5-flash",
"gpt-5.5",
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.3-codex",
"gpt-5-mini",
"gpt-4o-2024-11-20",
"gpt-4o-mini",
"gpt-4-0125-preview",
"kimi-k2.7-code",
"mai-code-1-flash",
"oswe-vscode-prime",
] as const;
const GITHUB_COPILOT_MODEL_ALLOWLIST_SET = new Set<string>(GITHUB_COPILOT_MODEL_ALLOWLIST);
export type GitHubCopilotModel = {
id: string;
@@ -59,6 +85,7 @@ export function parseGitHubCopilotModels(data: unknown): GitHubCopilotModel[] {
const item = asRecord(value);
const id = toNonEmptyString(item.id) || toNonEmptyString(item.model);
if (!id || seen.has(id)) continue;
if (!GITHUB_COPILOT_MODEL_ALLOWLIST_SET.has(id)) continue;
seen.add(id);
const name = toNonEmptyString(item.name) || toNonEmptyString(item.display_name) || id;
models.push({ id, name, owned_by: "github" });
@@ -89,6 +116,7 @@ function toFallbackResult(
.map((model) => {
const id = toNonEmptyString(model.id);
if (!id) return null;
if (!GITHUB_COPILOT_MODEL_ALLOWLIST_SET.has(id)) return null;
return { id, name: toNonEmptyString(model.name) || id, owned_by: "github" };
})
.filter((model): model is GitHubCopilotModel => Boolean(model));

View File

@@ -8,6 +8,7 @@ import {
} from "./claudeCodeCompatible.ts";
import { getClaudeCodeCompatibleRequestDefaults } from "@/lib/providers/requestDefaults";
import { buildClineHeaders } from "@/shared/utils/clineAuth";
import { usesCcWireImage } from "./ccWireImageBuiltins.ts";
const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-";
const OPENAI_COMPATIBLE_DEFAULTS = {
@@ -29,7 +30,12 @@ function isAnthropicCompatible(provider) {
}
export function isClaudeCodeCompatible(provider) {
return typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX);
return (
(typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX)) ||
// Built-in providers (e.g. agentrouter) that adopt the dynamic CC wire image
// while keeping their own registry baseUrl + auth (#6056).
usesCcWireImage(provider)
);
}
export function getOpenAICompatibleType(
@@ -256,6 +262,15 @@ export function buildProviderUrl(
providerSpecificData?: Record<string, unknown> | null;
} = {}
) {
// Built-in CC-wire-image providers (e.g. agentrouter): keep the registry's
// OWN baseUrl (NOT the CC family's anthropic default) but adopt the CC chat
// path so the request still targets `<registry-baseUrl>?beta=true` (#6056).
if (usesCcWireImage(provider)) {
const entry = getRegistryEntry(provider);
const config = getProviderConfig(provider);
const baseUrl = options?.baseUrl || entry?.baseUrl || config.baseUrl;
return joinClaudeCodeCompatibleUrl(baseUrl, CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH);
}
if (isOpenAICompatible(provider)) {
const providerSpecificData = options?.providerSpecificData || null;
const apiType = getOpenAICompatibleType(provider, providerSpecificData);
@@ -318,12 +333,27 @@ export function buildProviderHeaders(provider, credentials, stream = true, body
const ccRequestDefaults = getClaudeCodeCompatibleRequestDefaults(
credentials?.providerSpecificData
);
return buildClaudeCodeCompatibleHeaders(
const ccHeaders = buildClaudeCodeCompatibleHeaders(
token,
stream,
credentials?.providerSpecificData?.ccSessionId,
{ redactThinking: ccRequestDefaults.redactThinking === true }
);
// Built-in CC-wire-image providers (e.g. agentrouter): adopt the CC wire
// image headers but keep the registry's OWN auth scheme (e.g. x-api-key)
// instead of the CC family's Bearer auth (#6056).
if (usesCcWireImage(provider)) {
delete ccHeaders["Authorization"];
const authHeader = entry?.authHeader || "bearer";
if (authHeader === "x-api-key") {
if (token) ccHeaders["x-api-key"] = token;
} else if (authHeader === "key") {
if (token) ccHeaders["Authorization"] = `Key ${token}`;
} else {
ccHeaders["Authorization"] = `Bearer ${token}`;
}
}
return ccHeaders;
}
if (isAnthropicCompatible(provider)) {
if (credentials.apiKey) {

View File

@@ -110,9 +110,7 @@ const RAW_CONFIGS: TokenExtractionConfig[] = [
"ChatGPT Web",
"https://chatgpt.com/auth/login",
"https://chatgpt.com",
[
{ type: "cookie", name: "__Secure-next-auth.session-token", domain: ".chatgpt.com" },
],
[{ type: "cookie", name: "__Secure-next-auth.session-token", domain: ".chatgpt.com" }],
"Log in to ChatGPT. The __Secure-next-auth.session-token cookie will be extracted after login."
),
@@ -146,9 +144,7 @@ const RAW_CONFIGS: TokenExtractionConfig[] = [
"Perplexity Web",
"https://www.perplexity.ai/login",
"https://www.perplexity.ai",
[
{ type: "cookie", name: "__Secure-next-auth.session-token", domain: ".perplexity.ai" },
],
[{ type: "cookie", name: "__Secure-next-auth.session-token", domain: ".perplexity.ai" }],
"Log in to Perplexity. The __Secure-next-auth.session-token cookie will be extracted.",
{ cookieDomain: ".perplexity.ai" }
),
@@ -195,9 +191,7 @@ const RAW_CONFIGS: TokenExtractionConfig[] = [
"Kimi (Moonshot)",
"https://www.kimi.com/",
"https://www.kimi.com",
[
{ type: "cookie", name: "kimi-auth", domain: ".kimi.com" },
],
[{ type: "cookie", name: "kimi-auth", domain: ".kimi.com" }],
"Log in to Kimi at www.kimi.com (international). The kimi-auth JWT cookie will be extracted.",
{ cookieDomain: ".kimi.com" }
),
@@ -222,9 +216,7 @@ const RAW_CONFIGS: TokenExtractionConfig[] = [
"Poe (Quora)",
"https://poe.com/login",
"https://poe.com",
[
{ type: "cookie", name: "p-b", domain: ".poe.com" },
],
[{ type: "cookie", name: "p-b", domain: ".poe.com" }],
"Log in to Poe at poe.com. The session cookie will be extracted.",
{ cookieDomain: ".poe.com" }
),
@@ -235,9 +227,7 @@ const RAW_CONFIGS: TokenExtractionConfig[] = [
"Microsoft Copilot",
"https://copilot.microsoft.com/",
"https://copilot.microsoft.com",
[
{ type: "cookie", name: "RPSCAuth", domain: ".microsoft.com" },
],
[{ type: "cookie", name: "RPSCAuth", domain: ".microsoft.com" }],
"Log in with your Microsoft account at copilot.microsoft.com. The session auth cookie will be extracted.",
{ cookieDomain: ".microsoft.com" }
),
@@ -248,9 +238,7 @@ const RAW_CONFIGS: TokenExtractionConfig[] = [
"DuckDuckGo AI Chat",
"https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=1",
"https://duckduckgo.com",
[
{ type: "cookie", name: "duckai", domain: ".duckduckgo.com" },
],
[{ type: "cookie", name: "duckai", domain: ".duckduckgo.com" }],
"Open DuckDuckGo AI Chat. Some models may require a free account. The duckai cookie will be extracted.",
{
cookieDomain: ".duckduckgo.com",
@@ -258,17 +246,19 @@ const RAW_CONFIGS: TokenExtractionConfig[] = [
}
),
// ── DouBao Web ────────────────────────────────────────────
// ── Dola Web ──────────────────────────────────────────────
config(
"doubao-web",
"DouBao (ByteDance)",
"https://www.doubao.com/",
"https://www.doubao.com",
"Dola (ByteDance)",
"https://www.dola.com/",
"https://www.dola.com",
[
{ type: "cookie", name: "sessionid", domain: ".doubao.com" },
{ type: "cookie", name: "sessionid", domain: ".dola.com" },
{ type: "cookie", name: "ttwid", domain: ".dola.com" },
{ type: "cookie", name: "s_v_web_id", domain: ".dola.com" },
],
"Log in to DouBao at doubao.com with your ByteDance account. The sessionid will be extracted.",
{ cookieDomain: ".doubao.com" }
"Log in to Dola at www.dola.com with your ByteDance account. sessionid, ttwid, and s_v_web_id will be extracted.",
{ cookieDomain: ".dola.com" }
),
// ── T3 Chat Web ───────────────────────────────────────────
@@ -277,9 +267,7 @@ const RAW_CONFIGS: TokenExtractionConfig[] = [
"T3 Chat",
"https://t3.chat/login",
"https://t3.chat",
[
{ type: "localStorage", key: "token" },
],
[{ type: "localStorage", key: "token" }],
"Log in to T3 Chat at t3.chat using Google/GitHub. The token from localStorage will be extracted.",
{ pollingConfig: QUICK_POLLING }
),
@@ -304,9 +292,7 @@ const RAW_CONFIGS: TokenExtractionConfig[] = [
"v0 by Vercel",
"https://v0.dev/login",
"https://v0.dev",
[
{ type: "cookie", name: "__Secure-next-auth.session-token", domain: ".v0.dev" },
],
[{ type: "cookie", name: "__Secure-next-auth.session-token", domain: ".v0.dev" }],
"Log in to v0.dev with your Vercel/Google/GitHub account. The session cookie will be extracted.",
{ cookieDomain: ".v0.dev" }
),
@@ -317,9 +303,7 @@ const RAW_CONFIGS: TokenExtractionConfig[] = [
"Meta AI (Muse)",
"https://www.meta.ai/",
"https://www.meta.ai",
[
{ type: "cookie", name: "session", domain: ".meta.ai" },
],
[{ type: "cookie", name: "session", domain: ".meta.ai" }],
"Log in to Meta AI at meta.ai with your Facebook/Instagram account. The session cookie will be extracted.",
{ cookieDomain: ".meta.ai" }
),
@@ -330,9 +314,7 @@ const RAW_CONFIGS: TokenExtractionConfig[] = [
"Adapta AI",
"https://agent.adapta.one/login",
"https://agent.adapta.one",
[
{ type: "cookie", name: "__session", domain: ".adapta.one" },
],
[{ type: "cookie", name: "__session", domain: ".adapta.one" }],
"Log in to Adapta at agent.adapta.one. The session token will be extracted.",
{ cookieDomain: ".adapta.one" }
),
@@ -343,9 +325,7 @@ const RAW_CONFIGS: TokenExtractionConfig[] = [
"VeoAI Free",
"https://veoaifree.com/",
"https://veoaifree.com",
[
{ type: "cookie", name: "wordpress_logged_in", domain: ".veoaifree.com" },
],
[{ type: "cookie", name: "wordpress_logged_in", domain: ".veoaifree.com" }],
"Log in to VeoAI Free at veoaifree.com. The WordPress session cookie will be extracted.",
{
cookieDomain: ".veoaifree.com",

View File

@@ -6,6 +6,7 @@ const DEFAULT_LIMIT = MAX_TOOLS_LIMIT;
const PROVIDER_TOOL_LIMITS: Record<string, number> = {
"grok-cli": 200,
"nvidia": 1536,
};
const _detectedLimitsSweep = setInterval(() => {
@@ -18,7 +19,7 @@ if (typeof _detectedLimitsSweep === "object" && "unref" in _detectedLimitsSweep)
(_detectedLimitsSweep as { unref?: () => void }).unref?.();
}
export function getEffectiveToolLimit(provider: string): number {
export function getKnownToolLimit(provider: string | null | undefined): number | null {
const proactiveLimit = PROVIDER_TOOL_LIMITS[provider];
if (proactiveLimit !== undefined) {
return proactiveLimit;
@@ -27,7 +28,11 @@ export function getEffectiveToolLimit(provider: string): number {
if (cached && Date.now() - cached.timestamp < TTL_MS) {
return cached.limit;
}
return DEFAULT_LIMIT;
return null;
}
export function getEffectiveToolLimit(provider: string | null | undefined): number {
return getKnownToolLimit(provider) ?? DEFAULT_LIMIT;
}
export function setDetectedToolLimit(provider: string, limit: number): void {

View File

@@ -29,6 +29,9 @@ const STRIP_RULES: StripRule[] = [
/claude/i.test(m) && !/claude.*(opus|sonnet).*4\.6/i.test(m),
drop: ["thinking", "reasoning_effort"],
},
// NVIDIA NIM z-ai/glm-5.2: OpenAI-compatible wrapper rejects the `reasoning`
// body field → HTTP 400 "Unsupported parameter(s): `reasoning`". #6102 drop pattern.
{ provider: "nvidia", match: /z-ai\/glm-5\.2\b/i, drop: ["reasoning"] },
// NVIDIA NIM minimaxai/minimax-m2.7: NVIDIA's OpenAI-compatible wrapper
// (format:"openai") does not accept the Claude-style `thinking` body field
// and returns 400 "Unsupported parameter(s): thinking". Upstream #2268.

View File

@@ -5,6 +5,7 @@
import { register } from "../registry.ts";
import { FORMATS } from "../formats.ts";
import { v4 as uuidv4, v5 as uuidv5 } from "uuid";
import { capMaxOutputTokens, capThinkingBudget, supportsReasoning } from "@/lib/modelCapabilities";
import {
parseToolInput,
normalizeKiroToolSchema,
@@ -57,9 +58,9 @@ function convertMessages(messages, tools, model) {
let toolsAttached = false;
// Only Claude models support images in Kiro. Kiro also routes non-Claude
// models (deepseek, minimax, glm, qwen3-coder-next, auto-kiro) that do not
// accept image attachments — gate image extraction behind a Claude check so
// we never attach images those models would reject.
// models (deepseek, minimax, glm, qwen3-coder-next) that do not accept image
// attachments — gate image extraction behind a Claude check so we never
// attach images those models would reject.
const supportsImages = typeof model === "string" && model.toLowerCase().includes("claude");
const flushPending = () => {
@@ -575,6 +576,80 @@ function convertMessages(messages, tools, model) {
return { history: alternatingHistory, currentMessage, toolsAttached };
}
/** Kiro's accepted reasoning-effort levels (`output_config.effort`). */
const KIRO_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
/**
* Resolve the Kiro effort level for a request, or "" when no reasoning was asked
* for. Effort sources, in priority order:
* 1. OpenAI-style `reasoning_effort`
* 2. Anthropic adaptive-thinking `output_config.effort` (the canonical field)
* 3. Anthropic `thinking` block — `{type:"enabled", budget_tokens}` mapped to a
* level via {@link effortFromBudget}; `{type:"adaptive"}` (no explicit
* effort) defaults to `high`, matching Anthropic's documented default
* (omitting `effort` ≡ `high`).
* OpenAI's `minimal` collapses to `low` (Kiro has no `minimal`).
*/
function resolveKiroEffort(body: Record<string, unknown>): string {
let effort = typeof body.reasoning_effort === "string" ? body.reasoning_effort.toLowerCase() : "";
if (!effort) {
const outputConfig = body.output_config as Record<string, unknown> | undefined;
if (
outputConfig &&
typeof outputConfig === "object" &&
typeof outputConfig.effort === "string"
) {
effort = outputConfig.effort.toLowerCase();
}
}
if (!effort) {
const thinking = body.thinking as Record<string, unknown> | undefined;
if (thinking && typeof thinking === "object") {
if (thinking.type === "enabled") {
effort = effortFromBudget(Number(thinking.budget_tokens) || 0);
} else if (thinking.type === "adaptive") {
effort = "high";
}
}
}
if (effort === "minimal") effort = "low";
return KIRO_EFFORT_LEVELS.includes(effort) ? effort : "";
}
/** Map an Anthropic `thinking.budget_tokens` to a coarse Kiro effort level. */
function effortFromBudget(budget: number): string {
if (budget >= 32000) return "high";
if (budget >= 16000) return "medium";
if (budget > 0) return "low";
return "";
}
/**
* Soft `<max_thinking_length>` budget for the Kiro prompt directive, per effort
* level. Anthropic publishes no effort→token mapping (effort is "a behavioral
* signal, not a strict token budget"), so this is a heuristic tuned against the
* live CodeWhisperer stream, where a larger budget measurably deepens reasoning
* up to the model cap. It is a hint the model may honor, not a hard cap (the hard
* enable signal is `<thinking_mode>`); the caller clamps it to the model's cap.
*/
function thinkingLengthForEffort(effort: string): number {
switch (effort) {
case "max":
return 120000;
case "xhigh":
return 64000;
case "high":
return 32000;
case "medium":
return 16000;
default:
return 8000; // low
}
}
/**
* Build Kiro payload from OpenAI format
*/
@@ -683,6 +758,11 @@ export function buildKiroPayload(model, body, stream, credentials) {
temperature?: number;
topP?: number;
};
additionalModelRequestFields?: {
thinking?: { type: string; display?: string };
output_config?: { effort: string };
max_tokens?: number;
};
} = {
conversationState: {
chatTriggerType: "MANUAL",
@@ -754,6 +834,55 @@ export function buildKiroPayload(model, body, stream, credentials) {
if (topP !== undefined) payload.inferenceConfig.topP = topP;
}
// Thinking mode for Claude models on Kiro (ported from javargasm/pi-kiro).
// Two coordinated signals steer reasoning on the CodeWhisperer surface:
// 1. a `<thinking_mode>enabled</thinking_mode><max_thinking_length>N</...>`
// directive prepended to the current user message — makes Claude emit its
// reasoning INLINE as `<thinking>…</thinking>`, which the Kiro executor
// splits back into the OpenAI `reasoning_content` channel (kiroThinking.ts);
// 2. top-level `additionalModelRequestFields` (output_config.effort +
// thinking:{type:"adaptive"} + a clamped max_tokens), forwarded to AWS by
// the Kiro executor's transformRequest allowlist — this is the graded
// effort lever. Gated on models that advertise thinking support.
const kiroEffort = supportsReasoning(normalizedModel) ? resolveKiroEffort(body) : "";
if (kiroEffort) {
// `<thinking_mode>` / `<max_thinking_length>` are Kiro/CodeWhisperer prompt
// conventions (NOT Anthropic API params); the length is a soft hint (the hard
// enable signal is `<thinking_mode>`), clamped to the model's thinking cap.
const thinkingLength = capThinkingBudget(normalizedModel, thinkingLengthForEffort(kiroEffort));
const directive =
`<thinking_mode>enabled</thinking_mode>` +
`<max_thinking_length>${thinkingLength}</max_thinking_length>`;
payload.conversationState.currentMessage.userInputMessage.content = `${directive}\n\n${payload.conversationState.currentMessage.userInputMessage.content}`;
const fields: {
output_config: { effort: string };
thinking: { type: string; display: string };
max_tokens?: number;
} = {
output_config: { effort: kiroEffort },
thinking: { type: "adaptive", display: "summarized" },
};
// Forward max_tokens only when the client set one, clamped to the model's
// output window (floor 1024) — matches pi-kiro and avoids an over-budget reject.
if (maxTokens > 0) {
const capped = capMaxOutputTokens(normalizedModel, maxTokens) ?? maxTokens;
fields.max_tokens = Math.max(Math.floor(capped), 1024);
}
payload.additionalModelRequestFields = fields;
// Adaptive-only Claude models (Opus 4.7/4.8, Sonnet 5, Fable 5) reject a
// non-default temperature / top_p with a 400 while thinking is active, so
// strip both. Drop inferenceConfig entirely if nothing else remains.
if (payload.inferenceConfig) {
delete payload.inferenceConfig.temperature;
delete payload.inferenceConfig.topP;
if (Object.keys(payload.inferenceConfig).length === 0) {
delete payload.inferenceConfig;
}
}
}
return payload;
}

View File

@@ -54,6 +54,8 @@ export type EarlyStreamKeepaliveOptions = {
* for their stream watchdog and only a real `event: ping` keeps them from aborting.
*/
keepaliveFrame?: Uint8Array;
/** Extra headers to include in the keepalive response (e.g. X-Correlation-Id). */
extraHeaders?: Record<string, string>;
};
type SettledHandler = { ok: true; response: Response } | { ok: false; error: unknown };
@@ -66,6 +68,7 @@ export async function withEarlyStreamKeepalive(
const intervalMs = Math.max(250, options.intervalMs ?? 2_500);
const signal = options.signal ?? null;
const keepaliveFrame = options.keepaliveFrame ?? KEEPALIVE_FRAME;
const extraHeaders = options.extraHeaders ?? {};
// Settle into a tagged result so neither race branch leaves an unhandled
// rejection when the threshold timer wins.
@@ -154,10 +157,25 @@ export async function withEarlyStreamKeepalive(
if (response.body && isSse) {
// Real SSE stream — forward it verbatim.
upstreamReader = response.body.getReader();
while (true) {
const { done, value } = await upstreamReader.read();
if (done) break;
if (value) controller.enqueue(value);
let bytesForwarded = 0;
try {
while (true) {
const { done, value } = await upstreamReader.read();
if (done) break;
if (value) {
controller.enqueue(value);
bytesForwarded += value.byteLength;
}
}
} catch (readErr) {
// Upstream stream failed mid-flight. Only emit an error frame if
// NO content was forwarded yet — otherwise the client already
// received partial content and a late error frame would corrupt
// the SSE stream. Silently close instead; the client will see
// the stream end naturally.
if (bytesForwarded === 0) {
controller.enqueue(ERROR_FRAME);
}
}
} else {
// Non-SSE response (e.g. a JSON error) reached us after we already
@@ -204,6 +222,7 @@ export async function withEarlyStreamKeepalive(
"Content-Type": "text/event-stream; charset=utf-8",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
...extraHeaders,
},
});
}

Some files were not shown because too many files have changed in this diff Show More