mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 10:43:43 +03:00
Merge remote-tracking branch 'origin/release/v3.8.45' into pr6231
This commit is contained in:
14
.env.example
14
.env.example
@@ -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
|
||||
|
||||
57
.github/workflows/ci.yml
vendored
57
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
29
Dockerfile
29
Dockerfile
@@ -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}"
|
||||
|
||||
|
||||
@@ -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 = {}) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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/`
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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. |
|
||||
|
||||
8
llm.txt
8
llm.txt
@@ -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.
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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)" },
|
||||
],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
11
open-sse/config/providers/registry/requesty/index.ts
Normal file
11
open-sse/config/providers/registry/requesty/index.ts
Normal 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,
|
||||
});
|
||||
37
open-sse/config/providers/registry/yuanbao-web/index.ts
Normal file
37
open-sse/config/providers/registry/yuanbao-web/index.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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" },
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 =
|
||||
|
||||
504
open-sse/executors/yuanbao-web.ts
Normal file
504
open-sse/executors/yuanbao-web.ts
Normal 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 };
|
||||
}
|
||||
@@ -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, {
|
||||
|
||||
@@ -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(() => {});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
140
open-sse/mcp-server/tools/githubSkillTools.ts
Normal file
140
open-sse/mcp-server/tools/githubSkillTools.ts
Normal 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.0–1.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,
|
||||
},
|
||||
};
|
||||
26
open-sse/services/ccWireImageBuiltins.ts
Normal file
26
open-sse/services/ccWireImageBuiltins.ts
Normal 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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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(() => {});
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user