diff --git a/.env.example b/.env.example index fdfe5d0b0f..0790badeae 100644 --- a/.env.example +++ b/.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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 988f844aaf..be1d3e4465 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/Dockerfile b/Dockerfile index 01f54c1bd1..92894f55c2 100644 --- a/Dockerfile +++ b/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}" diff --git a/bin/cli/commands/doctor.mjs b/bin/cli/commands/doctor.mjs index 0a2b78db74..087101b50d 100644 --- a/bin/cli/commands/doctor.mjs +++ b/bin/cli/commands/doctor.mjs @@ -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 = {}) { diff --git a/bin/cli/commands/serve.mjs b/bin/cli/commands/serve.mjs index 9ba6579c3f..ab9d793ca3 100644 --- a/bin/cli/commands/serve.mjs +++ b/bin/cli/commands/serve.mjs @@ -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 diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json index 0cab8df64d..c74f3b1b34 100644 --- a/config/quality/eslint-suppressions.json +++ b/config/quality/eslint-suppressions.json @@ -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 diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 090083aa63..65bcb345d9 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -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 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." } diff --git a/config/quality/test-masking-allowlist.json b/config/quality/test-masking-allowlist.json index 2529dd5267..d548c4a40c 100644 --- a/config/quality/test-masking-allowlist.json +++ b/config/quality/test-masking-allowlist.json @@ -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." } diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md index e91387e535..0dc4bbfc4d 100644 --- a/docs/architecture/ARCHITECTURE.md +++ b/docs/architecture/ARCHITECTURE.md @@ -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` diff --git a/docs/architecture/CODEBASE_DOCUMENTATION.md b/docs/architecture/CODEBASE_DOCUMENTATION.md index 599c5dc58b..0bd52c6599 100644 --- a/docs/architecture/CODEBASE_DOCUMENTATION.md +++ b/docs/architecture/CODEBASE_DOCUMENTATION.md @@ -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/` diff --git a/docs/architecture/REPOSITORY_MAP.md b/docs/architecture/REPOSITORY_MAP.md index 05b4488e14..2b1c5676d0 100644 --- a/docs/architecture/REPOSITORY_MAP.md +++ b/docs/architecture/REPOSITORY_MAP.md @@ -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 | diff --git a/docs/diagrams/db-schema-overview.mmd b/docs/diagrams/db-schema-overview.mmd index debf71f3b1..b5554eccf8 100644 --- a/docs/diagrams/db-schema-overview.mmd +++ b/docs/diagrams/db-schema-overview.mmd @@ -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 diff --git a/docs/i18n/ar/llm.txt b/docs/i18n/ar/llm.txt index 4fa9b21284..775cd2b66e 100644 --- a/docs/i18n/ar/llm.txt +++ b/docs/i18n/ar/llm.txt @@ -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. diff --git a/docs/i18n/az/llm.txt b/docs/i18n/az/llm.txt index 3950dcb984..45c98fe7db 100644 --- a/docs/i18n/az/llm.txt +++ b/docs/i18n/az/llm.txt @@ -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. diff --git a/docs/i18n/bg/llm.txt b/docs/i18n/bg/llm.txt index 3950dcb984..45c98fe7db 100644 --- a/docs/i18n/bg/llm.txt +++ b/docs/i18n/bg/llm.txt @@ -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. diff --git a/docs/i18n/bn/llm.txt b/docs/i18n/bn/llm.txt index 40552a5a11..fdb0178a80 100644 --- a/docs/i18n/bn/llm.txt +++ b/docs/i18n/bn/llm.txt @@ -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. diff --git a/docs/i18n/cs/llm.txt b/docs/i18n/cs/llm.txt index ce6de2f065..baf7b823d8 100644 --- a/docs/i18n/cs/llm.txt +++ b/docs/i18n/cs/llm.txt @@ -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. diff --git a/docs/i18n/da/llm.txt b/docs/i18n/da/llm.txt index d9b36886d6..819865a7ed 100644 --- a/docs/i18n/da/llm.txt +++ b/docs/i18n/da/llm.txt @@ -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. diff --git a/docs/i18n/de/llm.txt b/docs/i18n/de/llm.txt index 80f08179c9..fd8b2bb12c 100644 --- a/docs/i18n/de/llm.txt +++ b/docs/i18n/de/llm.txt @@ -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. diff --git a/docs/i18n/es/llm.txt b/docs/i18n/es/llm.txt index a1cb33aff2..dc3ca451f0 100644 --- a/docs/i18n/es/llm.txt +++ b/docs/i18n/es/llm.txt @@ -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. diff --git a/docs/i18n/fa/llm.txt b/docs/i18n/fa/llm.txt index 6ba4136624..6848cd28f9 100644 --- a/docs/i18n/fa/llm.txt +++ b/docs/i18n/fa/llm.txt @@ -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. diff --git a/docs/i18n/fi/llm.txt b/docs/i18n/fi/llm.txt index 7c5e76fec8..96224f8a4d 100644 --- a/docs/i18n/fi/llm.txt +++ b/docs/i18n/fi/llm.txt @@ -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. diff --git a/docs/i18n/fr/llm.txt b/docs/i18n/fr/llm.txt index ead67c7773..7de5c69aac 100644 --- a/docs/i18n/fr/llm.txt +++ b/docs/i18n/fr/llm.txt @@ -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. diff --git a/docs/i18n/gu/llm.txt b/docs/i18n/gu/llm.txt index 65877bf751..3123e0b0aa 100644 --- a/docs/i18n/gu/llm.txt +++ b/docs/i18n/gu/llm.txt @@ -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. diff --git a/docs/i18n/he/llm.txt b/docs/i18n/he/llm.txt index 8f0e22ab34..f73b2bb1cc 100644 --- a/docs/i18n/he/llm.txt +++ b/docs/i18n/he/llm.txt @@ -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. diff --git a/docs/i18n/hi/llm.txt b/docs/i18n/hi/llm.txt index ac3b0774ea..64c9880884 100644 --- a/docs/i18n/hi/llm.txt +++ b/docs/i18n/hi/llm.txt @@ -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. diff --git a/docs/i18n/hu/llm.txt b/docs/i18n/hu/llm.txt index 2f6ad261a3..9f574945c0 100644 --- a/docs/i18n/hu/llm.txt +++ b/docs/i18n/hu/llm.txt @@ -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. diff --git a/docs/i18n/id/llm.txt b/docs/i18n/id/llm.txt index 4a31546f56..e05008dedb 100644 --- a/docs/i18n/id/llm.txt +++ b/docs/i18n/id/llm.txt @@ -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. diff --git a/docs/i18n/in/llm.txt b/docs/i18n/in/llm.txt index 6eee7c1d9a..6e91c869fd 100644 --- a/docs/i18n/in/llm.txt +++ b/docs/i18n/in/llm.txt @@ -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. diff --git a/docs/i18n/it/llm.txt b/docs/i18n/it/llm.txt index 5ab3252731..6db7b5f06a 100644 --- a/docs/i18n/it/llm.txt +++ b/docs/i18n/it/llm.txt @@ -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. diff --git a/docs/i18n/ja/llm.txt b/docs/i18n/ja/llm.txt index 6ce95eddb2..ab529087fe 100644 --- a/docs/i18n/ja/llm.txt +++ b/docs/i18n/ja/llm.txt @@ -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. diff --git a/docs/i18n/ko/llm.txt b/docs/i18n/ko/llm.txt index 8a8b338b57..8c0aeb75c6 100644 --- a/docs/i18n/ko/llm.txt +++ b/docs/i18n/ko/llm.txt @@ -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. diff --git a/docs/i18n/mr/llm.txt b/docs/i18n/mr/llm.txt index 7536b91276..cde497ddc3 100644 --- a/docs/i18n/mr/llm.txt +++ b/docs/i18n/mr/llm.txt @@ -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. diff --git a/docs/i18n/ms/llm.txt b/docs/i18n/ms/llm.txt index c33b36f427..daf937c1f3 100644 --- a/docs/i18n/ms/llm.txt +++ b/docs/i18n/ms/llm.txt @@ -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. diff --git a/docs/i18n/nl/llm.txt b/docs/i18n/nl/llm.txt index dcad1b15b1..38c2c89661 100644 --- a/docs/i18n/nl/llm.txt +++ b/docs/i18n/nl/llm.txt @@ -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. diff --git a/docs/i18n/no/llm.txt b/docs/i18n/no/llm.txt index 79b1d8c5dd..1d19c59c96 100644 --- a/docs/i18n/no/llm.txt +++ b/docs/i18n/no/llm.txt @@ -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. diff --git a/docs/i18n/phi/llm.txt b/docs/i18n/phi/llm.txt index dbb53b1aca..783034fdfb 100644 --- a/docs/i18n/phi/llm.txt +++ b/docs/i18n/phi/llm.txt @@ -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. diff --git a/docs/i18n/pl/llm.txt b/docs/i18n/pl/llm.txt index 5504d9e4d4..1072ad9f24 100644 --- a/docs/i18n/pl/llm.txt +++ b/docs/i18n/pl/llm.txt @@ -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. diff --git a/docs/i18n/pt-BR/llm.txt b/docs/i18n/pt-BR/llm.txt index bedcfae6e2..c117a31086 100644 --- a/docs/i18n/pt-BR/llm.txt +++ b/docs/i18n/pt-BR/llm.txt @@ -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. diff --git a/docs/i18n/pt/llm.txt b/docs/i18n/pt/llm.txt index c9cdf06e64..0624b72f3b 100644 --- a/docs/i18n/pt/llm.txt +++ b/docs/i18n/pt/llm.txt @@ -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. diff --git a/docs/i18n/ro/llm.txt b/docs/i18n/ro/llm.txt index d34991f191..2810dae83d 100644 --- a/docs/i18n/ro/llm.txt +++ b/docs/i18n/ro/llm.txt @@ -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. diff --git a/docs/i18n/ru/llm.txt b/docs/i18n/ru/llm.txt index 6065228f78..923ed9e785 100644 --- a/docs/i18n/ru/llm.txt +++ b/docs/i18n/ru/llm.txt @@ -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. diff --git a/docs/i18n/sk/llm.txt b/docs/i18n/sk/llm.txt index 35d7392e29..bf2646ae46 100644 --- a/docs/i18n/sk/llm.txt +++ b/docs/i18n/sk/llm.txt @@ -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. diff --git a/docs/i18n/sv/llm.txt b/docs/i18n/sv/llm.txt index 885a484dfd..9415c9010d 100644 --- a/docs/i18n/sv/llm.txt +++ b/docs/i18n/sv/llm.txt @@ -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. diff --git a/docs/i18n/sw/llm.txt b/docs/i18n/sw/llm.txt index 106571d579..f9ebdc2a3d 100644 --- a/docs/i18n/sw/llm.txt +++ b/docs/i18n/sw/llm.txt @@ -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. diff --git a/docs/i18n/ta/llm.txt b/docs/i18n/ta/llm.txt index ef2f4fc981..91f2022328 100644 --- a/docs/i18n/ta/llm.txt +++ b/docs/i18n/ta/llm.txt @@ -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. diff --git a/docs/i18n/te/llm.txt b/docs/i18n/te/llm.txt index 96d1bab868..1de33d1956 100644 --- a/docs/i18n/te/llm.txt +++ b/docs/i18n/te/llm.txt @@ -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. diff --git a/docs/i18n/th/llm.txt b/docs/i18n/th/llm.txt index 89e84411e9..cd0a37688c 100644 --- a/docs/i18n/th/llm.txt +++ b/docs/i18n/th/llm.txt @@ -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. diff --git a/docs/i18n/tr/llm.txt b/docs/i18n/tr/llm.txt index 32b0be9837..634d4bc259 100644 --- a/docs/i18n/tr/llm.txt +++ b/docs/i18n/tr/llm.txt @@ -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. diff --git a/docs/i18n/uk-UA/llm.txt b/docs/i18n/uk-UA/llm.txt index e21defcb6a..14cf3b6600 100644 --- a/docs/i18n/uk-UA/llm.txt +++ b/docs/i18n/uk-UA/llm.txt @@ -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. diff --git a/docs/i18n/ur/llm.txt b/docs/i18n/ur/llm.txt index cac8e8c752..c402cff2a1 100644 --- a/docs/i18n/ur/llm.txt +++ b/docs/i18n/ur/llm.txt @@ -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. diff --git a/docs/i18n/vi/llm.txt b/docs/i18n/vi/llm.txt index efe5b2c9cd..e21816296c 100644 --- a/docs/i18n/vi/llm.txt +++ b/docs/i18n/vi/llm.txt @@ -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. diff --git a/docs/i18n/zh-CN/llm.txt b/docs/i18n/zh-CN/llm.txt index 4d3f47bfef..090969a4f0 100644 --- a/docs/i18n/zh-CN/llm.txt +++ b/docs/i18n/zh-CN/llm.txt @@ -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. diff --git a/docs/i18n/zh-TW/llm.txt b/docs/i18n/zh-TW/llm.txt index ece4e5b336..891f389de1 100644 --- a/docs/i18n/zh-TW/llm.txt +++ b/docs/i18n/zh-TW/llm.txt @@ -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. diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index efe32cff43..95bfa5bcf5 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -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. | diff --git a/llm.txt b/llm.txt index 8ba39f8e01..d2995c50a4 100644 --- a/llm.txt +++ b/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. diff --git a/open-sse/config/audioRegistry.ts b/open-sse/config/audioRegistry.ts index cbeaa789e5..144d0f563f 100644 --- a/open-sse/config/audioRegistry.ts +++ b/open-sse/config/audioRegistry.ts @@ -416,7 +416,6 @@ export const AUDIO_SPEECH_PROVIDERS: Record = { { 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); } diff --git a/open-sse/config/freeModelCatalog.data.ts b/open-sse/config/freeModelCatalog.data.ts index 7074fcb3dd..81cfb498a7 100644 --- a/open-sse/config/freeModelCatalog.data.ts +++ b/open-sse/config/freeModelCatalog.data.ts @@ -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" }, diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index aa733053e6..7627f79958 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -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 = { aimlapi: aimlapiProvider, @@ -314,6 +316,7 @@ export const REGISTRY: Record = { 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 = { sumopod: sumopodProvider, x5lab: x5labProvider, kenari: kenariProvider, + requesty: requestyProvider, }; diff --git a/open-sse/config/providers/registry/agentrouter/index.ts b/open-sse/config/providers/registry/agentrouter/index.ts index a4156d76ca..ebe9d4598f 100644 --- a/open-sse/config/providers/registry/agentrouter/index.ts +++ b/open-sse/config/providers/registry/agentrouter/index.ts @@ -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" }, diff --git a/open-sse/config/providers/registry/claude/web/index.ts b/open-sse/config/providers/registry/claude/web/index.ts index ae5612822e..952b4f39fe 100644 --- a/open-sse/config/providers/registry/claude/web/index.ts +++ b/open-sse/config/providers/registry/claude/web/index.ts @@ -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)" }, ], diff --git a/open-sse/config/providers/registry/cline/index.ts b/open-sse/config/providers/registry/cline/index.ts index d1467fdd9f..4913465d19 100644 --- a/open-sse/config/providers/registry/cline/index.ts +++ b/open-sse/config/providers/registry/cline/index.ts @@ -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", diff --git a/open-sse/config/providers/registry/clinepass/index.ts b/open-sse/config/providers/registry/clinepass/index.ts index 3ea7dbcb3a..e6d3697a03 100644 --- a/open-sse/config/providers/registry/clinepass/index.ts +++ b/open-sse/config/providers/registry/clinepass/index.ts @@ -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", diff --git a/open-sse/config/providers/registry/codex/index.ts b/open-sse/config/providers/registry/codex/index.ts index 2418a2bfb6..d739e09829 100644 --- a/open-sse/config/providers/registry/codex/index.ts +++ b/open-sse/config/providers/registry/codex/index.ts @@ -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, }, { diff --git a/open-sse/config/providers/registry/doubao/web/index.ts b/open-sse/config/providers/registry/doubao/web/index.ts index a51aaaaa4d..025d4b7e82 100644 --- a/open-sse/config/providers/registry/doubao/web/index.ts +++ b/open-sse/config/providers/registry/doubao/web/index.ts @@ -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" }, ], }; diff --git a/open-sse/config/providers/registry/github/index.ts b/open-sse/config/providers/registry/github/index.ts index f8c25d05a1..41673dcf7d 100644 --- a/open-sse/config/providers/registry/github/index.ts +++ b/open-sse/config/providers/registry/github/index.ts @@ -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, + }, ], }; diff --git a/open-sse/config/providers/registry/kiro/index.ts b/open-sse/config/providers/registry/kiro/index.ts index 6c4d41cf6e..71262a4f09 100644 --- a/open-sse/config/providers/registry/kiro/index.ts +++ b/open-sse/config/providers/registry/kiro/index.ts @@ -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, }, diff --git a/open-sse/config/providers/registry/nvidia/index.ts b/open-sse/config/providers/registry/nvidia/index.ts index 152c3279bf..c42f8da703 100644 --- a/open-sse/config/providers/registry/nvidia/index.ts +++ b/open-sse/config/providers/registry/nvidia/index.ts @@ -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" }, ], }; diff --git a/open-sse/config/providers/registry/opencode/go/index.ts b/open-sse/config/providers/registry/opencode/go/index.ts index b1c0b4f12e..c8e575b185 100644 --- a/open-sse/config/providers/registry/opencode/go/index.ts +++ b/open-sse/config/providers/registry/opencode/go/index.ts @@ -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", diff --git a/open-sse/config/providers/registry/requesty/index.ts b/open-sse/config/providers/registry/requesty/index.ts new file mode 100644 index 0000000000..6fc063bce2 --- /dev/null +++ b/open-sse/config/providers/registry/requesty/index.ts @@ -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, +}); diff --git a/open-sse/config/providers/registry/yuanbao-web/index.ts b/open-sse/config/providers/registry/yuanbao-web/index.ts new file mode 100644 index 0000000000..c3dc4b1565 --- /dev/null +++ b/open-sse/config/providers/registry/yuanbao-web/index.ts @@ -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, + }, + ], +}; diff --git a/open-sse/config/providers/registry/zenmux-free/index.ts b/open-sse/config/providers/registry/zenmux-free/index.ts index 126e2bf203..21ff0139bd 100644 --- a/open-sse/config/providers/registry/zenmux-free/index.ts +++ b/open-sse/config/providers/registry/zenmux-free/index.ts @@ -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" }, diff --git a/open-sse/config/providers/shared.ts b/open-sse/config/providers/shared.ts index ecc4fd088d..4059673697 100644 --- a/open-sse/config/providers/shared.ts +++ b/open-sse/config/providers/shared.ts @@ -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 = { "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 }, diff --git a/open-sse/executors/antigravity.ts b/open-sse/executors/antigravity.ts index a66f58df06..5489e9fbb9 100644 --- a/open-sse/executors/antigravity.ts +++ b/open-sse/executors/antigravity.ts @@ -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 +): Record { + 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") diff --git a/open-sse/executors/chatgpt-web.ts b/open-sse/executors/chatgpt-web.ts index 27085cf3c6..17125d4fcb 100644 --- a/open-sse/executors/chatgpt-web.ts +++ b/open-sse/executors/chatgpt-web.ts @@ -1579,6 +1579,21 @@ type ImageResolver = ( parentMessageId?: string | null ) => Promise; +/** + * 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, diff --git a/open-sse/executors/doubao-web.ts b/open-sse/executors/doubao-web.ts index 62c4a1ea80..10a9e395ea 100644 --- a/open-sse/executors/doubao-web.ts +++ b/open-sse/executors/doubao-web.ts @@ -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; + +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; - 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 = { + private createHeaders(cookieHeader: string): Record { + const headers: Record = { "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 { + 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; - 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, }; } } diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index 5984fde2ee..a3ab06cc6f 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -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"; diff --git a/open-sse/executors/kiro.ts b/open-sse/executors/kiro.ts index 26c3135257..7def296e39 100644 --- a/open-sse/executors/kiro.ts +++ b/open-sse/executors/kiro.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 `` 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 | 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 = diff --git a/open-sse/executors/yuanbao-web.ts b/open-sse/executors/yuanbao-web.ts new file mode 100644 index 0000000000..20ef5662e0 --- /dev/null +++ b/open-sse/executors/yuanbao-web.ts @@ -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 = { + "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; + 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>): 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; + 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; + transformedBody: unknown; + }> { + const { model, body, stream, credentials, signal, log, upstreamExtraHeaders } = input; + const messages = (body as Record).messages as + | Array> + | 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 = { + 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; + 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 = { + 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 = { + ...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 = { 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, + model: string, + id: string, + created: number, + signal: AbortSignal | null | undefined, + log?: ExecuteInput["log"] +): ReadableStream { + 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, + 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 }; +} diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index e6f7cb7391..3031483c0e 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -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, { diff --git a/open-sse/handlers/chatCore/attemptLogging.ts b/open-sse/handlers/chatCore/attemptLogging.ts index 8c7151e567..9db3efbc74 100644 --- a/open-sse/handlers/chatCore/attemptLogging.ts +++ b/open-sse/handlers/chatCore/attemptLogging.ts @@ -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(() => {}); } diff --git a/open-sse/handlers/chatCore/requestFormat.ts b/open-sse/handlers/chatCore/requestFormat.ts index 63ad12589b..fa9e9194fb 100644 --- a/open-sse/handlers/chatCore/requestFormat.ts +++ b/open-sse/handlers/chatCore/requestFormat.ts @@ -37,6 +37,33 @@ function isCopilotClient( return false; } +function isOpencodeClient( + headers: Headers | Record | 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, }; } diff --git a/open-sse/handlers/chatCore/upstreamBody.ts b/open-sse/handlers/chatCore/upstreamBody.ts index 1a302552d8..c426b240a7 100644 --- a/open-sse/handlers/chatCore/upstreamBody.ts +++ b/open-sse/handlers/chatCore/upstreamBody.ts @@ -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 { - 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); diff --git a/open-sse/handlers/imageGeneration/providers/chatgptWeb.ts b/open-sse/handlers/imageGeneration/providers/chatgptWeb.ts index 7faa531f5c..c6b9688b7a 100644 --- a/open-sse/handlers/imageGeneration/providers/chatgptWeb.ts +++ b/open-sse/handlers/imageGeneration/providers/chatgptWeb.ts @@ -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, }); } diff --git a/open-sse/mcp-server/server.ts b/open-sse/mcp-server/server.ts index cbf95b1956..77ee0706b8 100644 --- a/open-sse/mcp-server/server.ts +++ b/open-sse/mcp-server/server.ts @@ -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; + handler: (parsedArgs: unknown, extra?: unknown) => Promise; }) => { 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); diff --git a/open-sse/mcp-server/tools/githubSkillTools.ts b/open-sse/mcp-server/tools/githubSkillTools.ts new file mode 100644 index 0000000000..a67bd8ea5c --- /dev/null +++ b/open-sse/mcp-server/tools/githubSkillTools.ts @@ -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) { + 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) { + 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) { + 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, + }, +}; diff --git a/open-sse/services/ccWireImageBuiltins.ts b/open-sse/services/ccWireImageBuiltins.ts new file mode 100644 index 0000000000..870e4f78f1 --- /dev/null +++ b/open-sse/services/ccWireImageBuiltins.ts @@ -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 = 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); +} diff --git a/open-sse/services/claudeCodeCompatible.ts b/open-sse/services/claudeCodeCompatible.ts index 4fd5f22711..eff67c4203 100644 --- a/open-sse/services/claudeCodeCompatible.ts +++ b/open-sse/services/claudeCodeCompatible.ts @@ -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 { diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index a86bf5d90f..c78970360d 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -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 | null | undefined, + settings as Record | 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 | null | undefined, + settings as Record | 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, diff --git a/open-sse/services/combo/runtimeUnits.ts b/open-sse/services/combo/runtimeUnits.ts index 96951b81a2..d106ade336 100644 --- a/open-sse/services/combo/runtimeUnits.ts +++ b/open-sse/services/combo/runtimeUnits.ts @@ -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; diff --git a/open-sse/services/combo/sessionStickiness.ts b/open-sse/services/combo/sessionStickiness.ts index 1b81548106..5bb8779a94 100644 --- a/open-sse/services/combo/sessionStickiness.ts +++ b/open-sse/services/combo/sessionStickiness.ts @@ -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 | null | undefined, + settings: Record | 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 { diff --git a/open-sse/services/combo/types.ts b/open-sse/services/combo/types.ts index 766d6f71d9..290b0964d7 100644 --- a/open-sse/services/combo/types.ts +++ b/open-sse/services/combo/types.ts @@ -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 }; diff --git a/open-sse/services/combo/validateQuality.ts b/open-sse/services/combo/validateQuality.ts index fe7b00313e..2bab070a21 100644 --- a/open-sse/services/combo/validateQuality.ts +++ b/open-sse/services/combo/validateQuality.ts @@ -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(() => {}); +} diff --git a/open-sse/services/githubCopilotModels.ts b/open-sse/services/githubCopilotModels.ts index b24a57c3e8..5ccbcb623b 100644 --- a/open-sse/services/githubCopilotModels.ts +++ b/open-sse/services/githubCopilotModels.ts @@ -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(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)); diff --git a/open-sse/services/provider.ts b/open-sse/services/provider.ts index 9149c7e70b..9ba0a0acff 100644 --- a/open-sse/services/provider.ts +++ b/open-sse/services/provider.ts @@ -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 | 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 `?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) { diff --git a/open-sse/services/tokenExtractionConfig.ts b/open-sse/services/tokenExtractionConfig.ts index 7d141bacc1..8d7736f7e1 100644 --- a/open-sse/services/tokenExtractionConfig.ts +++ b/open-sse/services/tokenExtractionConfig.ts @@ -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", diff --git a/open-sse/services/toolLimitDetector.ts b/open-sse/services/toolLimitDetector.ts index 48969fcb12..b743aedb0b 100644 --- a/open-sse/services/toolLimitDetector.ts +++ b/open-sse/services/toolLimitDetector.ts @@ -6,6 +6,7 @@ const DEFAULT_LIMIT = MAX_TOOLS_LIMIT; const PROVIDER_TOOL_LIMITS: Record = { "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 { diff --git a/open-sse/translator/paramSupport.ts b/open-sse/translator/paramSupport.ts index 08e0e807a4..a0a3a70bab 100644 --- a/open-sse/translator/paramSupport.ts +++ b/open-sse/translator/paramSupport.ts @@ -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. diff --git a/open-sse/translator/request/openai-to-kiro.ts b/open-sse/translator/request/openai-to-kiro.ts index fc6d39abfd..8508fe54d0 100644 --- a/open-sse/translator/request/openai-to-kiro.ts +++ b/open-sse/translator/request/openai-to-kiro.ts @@ -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 { + let effort = typeof body.reasoning_effort === "string" ? body.reasoning_effort.toLowerCase() : ""; + + if (!effort) { + const outputConfig = body.output_config as Record | undefined; + if ( + outputConfig && + typeof outputConfig === "object" && + typeof outputConfig.effort === "string" + ) { + effort = outputConfig.effort.toLowerCase(); + } + } + + if (!effort) { + const thinking = body.thinking as Record | 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 `` 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 ``); 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 `enabledN` + // directive prepended to the current user message — makes Claude emit its + // reasoning INLINE as ``, 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) { + // `` / `` are Kiro/CodeWhisperer prompt + // conventions (NOT Anthropic API params); the length is a soft hint (the hard + // enable signal is ``), clamped to the model's thinking cap. + const thinkingLength = capThinkingBudget(normalizedModel, thinkingLengthForEffort(kiroEffort)); + const directive = + `enabled` + + `${thinkingLength}`; + 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; } diff --git a/open-sse/utils/earlyStreamKeepalive.ts b/open-sse/utils/earlyStreamKeepalive.ts index db2b98f194..c70180f927 100644 --- a/open-sse/utils/earlyStreamKeepalive.ts +++ b/open-sse/utils/earlyStreamKeepalive.ts @@ -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; }; 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, }, }); } diff --git a/open-sse/utils/finishReason.ts b/open-sse/utils/finishReason.ts index 79221ad07f..5d8ab33667 100644 --- a/open-sse/utils/finishReason.ts +++ b/open-sse/utils/finishReason.ts @@ -13,6 +13,7 @@ const SAFETY_FINISH_REASONS = new Set([ "prohibited_content", "content_filtered", "policy_violation", + "malformed_response", ]); export function normalizeOpenAICompatibleFinishReason(value: unknown): unknown { diff --git a/package-lock.json b/package-lock.json index a9c4af7c2d..22a61ed804 100644 --- a/package-lock.json +++ b/package-lock.json @@ -103,7 +103,7 @@ "@testing-library/react": "^16.3.2", "@types/better-sqlite3": "^7.6.13", "@types/bun": "latest", - "@types/node": "^26.0.0", + "@types/node": "^26.1.0", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", "@types/safe-regex": "^1.1.6", @@ -9500,9 +9500,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.0.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", - "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", "devOptional": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 4b5e40b5f3..3105eecbe5 100644 --- a/package.json +++ b/package.json @@ -309,7 +309,7 @@ "@testing-library/react": "^16.3.2", "@types/better-sqlite3": "^7.6.13", "@types/bun": "latest", - "@types/node": "^26.0.0", + "@types/node": "^26.1.0", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", "@types/safe-regex": "^1.1.6", diff --git a/scripts/build/build-next-isolated.mjs b/scripts/build/build-next-isolated.mjs index 28681bb299..09dc26bb89 100644 --- a/scripts/build/build-next-isolated.mjs +++ b/scripts/build/build-next-isolated.mjs @@ -108,7 +108,12 @@ function runNextBuild() { } export function resolveNextBuildBundlerFlag(baseEnv = process.env) { - return baseEnv.OMNIROUTE_USE_TURBOPACK === "1" ? "--turbopack" : "--webpack"; + // Turbopack is the default production bundler (Next 16 stable). Benchmarked on + // this codebase: 2-3x faster than the single-threaded webpack pass (17min -> 9min + // on a 32-core box; ~20min -> 7min on ubuntu-latest), artifact validated + // end-to-end (standalone smoke + e2e/package/electron CI jobs). Webpack stays as + // the explicit escape hatch (=0) for bundler-compat regressions. + return baseEnv.OMNIROUTE_USE_TURBOPACK === "0" ? "--webpack" : "--turbopack"; } export function resolveNextBuildEnv(baseEnv = process.env) { diff --git a/scripts/dev/run-next-playwright.mjs b/scripts/dev/run-next-playwright.mjs index e4e800f26f..37a7055e8c 100644 --- a/scripts/dev/run-next-playwright.mjs +++ b/scripts/dev/run-next-playwright.mjs @@ -187,7 +187,8 @@ const testServerEnv = { }; export function shouldUseWebpackForPlaywrightDev({ mode, env }) { - return mode === "dev" && env.OMNIROUTE_USE_TURBOPACK !== "1"; + // Webpack only on the explicit escape hatch (=0) — turbopack is the default. + return mode === "dev" && env.OMNIROUTE_USE_TURBOPACK === "0"; } function runChild(command, args, env) { diff --git a/scripts/dev/run-next.mjs b/scripts/dev/run-next.mjs index 5b8a9af0ce..38f914d2e3 100644 --- a/scripts/dev/run-next.mjs +++ b/scripts/dev/run-next.mjs @@ -68,7 +68,10 @@ process.env.NODE_ENV = dev ? "development" : "production"; const { dashboardPort } = runtimePorts; const hostname = process.env.HOST || "0.0.0.0"; -const useTurbopack = dev && mergedEnv.OMNIROUTE_USE_TURBOPACK === "1"; +// Turbopack by default in dev (matches the Next 16 CLI default and the production +// build default in build-next-isolated.mjs); OMNIROUTE_USE_TURBOPACK=0 is the +// webpack escape hatch. +const useTurbopack = dev && mergedEnv.OMNIROUTE_USE_TURBOPACK !== "0"; process.env.OMNIROUTE_WS_BRIDGE_SECRET ||= randomUUID(); // Per-process secret used to prove the trusted peer-IP stamp came from this // server (read by the authz middleware in the same process). See peer-stamp.mjs. diff --git a/scripts/quality/validate-release-green.mjs b/scripts/quality/validate-release-green.mjs index 78091ce8ab..3c1323b3d8 100644 --- a/scripts/quality/validate-release-green.mjs +++ b/scripts/quality/validate-release-green.mjs @@ -53,7 +53,9 @@ const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"; /** Read the committed ratchet baseline value for a metric (null if unknown). */ export function baselineValue(metric, root = ROOT) { try { - const raw = JSON.parse(readFileSync(join(root, "config/quality/quality-baseline.json"), "utf8")); + const raw = JSON.parse( + readFileSync(join(root, "config/quality/quality-baseline.json"), "utf8") + ); const metrics = raw.metrics || raw; const v = metrics?.[metric]?.value; return typeof v === "number" ? v : null; @@ -178,7 +180,13 @@ function main() { const hardCmd = (id, label, cmd, cmdArgs, opts) => { announce(label); const { code, out } = run(cmd, cmdArgs, opts); - record({ id, label, kind: "hard", ok: code === 0, detail: code === 0 ? "pass" : firstFailureLine(out) }); + record({ + id, + label, + kind: "hard", + ok: code === 0, + detail: code === 0 ? "pass" : firstFailureLine(out), + }); }; // A ratchet command (check:complexity, check:dead-code, …) exits 1 ONLY on a @@ -189,7 +197,13 @@ function main() { const driftCmd = (id, label, cmd, cmdArgs, okDetail = "within baseline", opts) => { announce(label); const { code, out } = run(cmd, cmdArgs, opts); - record({ id, label, kind: "drift", ok: code === 0, detail: code === 0 ? okDetail : firstFailureLine(out) }); + record({ + id, + label, + kind: "drift", + ok: code === 0, + detail: code === 0 ? okDetail : firstFailureLine(out), + }); }; process.stderr.write("🔎 Release-green validation (current working tree)\n\n"); @@ -198,14 +212,41 @@ function main() { // ESLint: ONE pass → errors (hard) + warnings (drift) { - announce("ESLint (errors + warnings — ~3-6min)"); - const { out } = run("npx", ["eslint", ".", "--format", "json"], { timeout: 15 * 60 * 1000 }); + announce("ESLint (errors + warnings — ~5-15min)"); + // Suppressions-aware, matching `npm run lint` (Pacote 4 no-new-warnings): the frozen + // pre-existing debt in config/quality/eslint-suppressions.json must not count as + // errors here — only NET-NEW violations are release reds. Timeout raised: a full + // repo pass takes ~14min alone and this pre-flight often runs alongside test suites. + const { out } = run( + "npx", + [ + "eslint", + ".", + "--format", + "json", + "--suppressions-location", + "config/quality/eslint-suppressions.json", + ], + { timeout: 30 * 60 * 1000 } + ); const parsed = parseEslintJson(out); if (!parsed) { - record({ id: "lint", label: "ESLint", kind: "hard", ok: false, detail: "could not parse eslint json" }); + record({ + id: "lint", + label: "ESLint", + kind: "hard", + ok: false, + detail: "could not parse eslint json", + }); } else { const { errors, warnings } = eslintCounts(parsed); - record({ id: "lint-errors", label: "ESLint errors", kind: "hard", ok: errors === 0, detail: `${errors} error(s)` }); + record({ + id: "lint-errors", + label: "ESLint errors", + kind: "hard", + ok: errors === 0, + detail: `${errors} error(s)`, + }); const base = baselineValue("eslintWarnings"); const over = isDrift(warnings, base); record({ @@ -283,9 +324,20 @@ function main() { driftCmd("complexity", "Cyclomatic complexity (ratchet)", npmCmd, ["run", "check:complexity"]); driftCmd("dead-code", "Dead-code (ratchet)", npmCmd, ["run", "check:dead-code"]); driftCmd("type-coverage", "Type coverage (ratchet)", npmCmd, ["run", "check:type-coverage"]); - driftCmd("compression-budget", "Compression budget (ratchet)", npmCmd, ["run", "check:compression-budget"]); - driftCmd("openapi-coverage", "OpenAPI route coverage (ratchet)", npmCmd, ["run", "check:openapi-coverage"]); - driftCmd("workflow-lint", "Workflow lint (zizmor ratchet)", npmCmd, ["run", "check:workflows", "--", "--ratchet"]); + driftCmd("compression-budget", "Compression budget (ratchet)", npmCmd, [ + "run", + "check:compression-budget", + ]); + driftCmd("openapi-coverage", "OpenAPI route coverage (ratchet)", npmCmd, [ + "run", + "check:openapi-coverage", + ]); + driftCmd("workflow-lint", "Workflow lint (zizmor ratchet)", npmCmd, [ + "run", + "check:workflows", + "--", + "--ratchet", + ]); driftCmd("codeql-ratchet", "CodeQL alerts (ratchet)", npmCmd, ["run", "check:codeql-ratchet"]); // Docs sync + fabricated-docs (strict) is a real-defect gate (invented env vars / @@ -298,15 +350,35 @@ function main() { // with 15 such reds). They run SILENTLY for many minutes; the announce line above + these // hard ceilings keep a long-but-healthy run from being mistaken for a hang (the ceiling also // converts a genuine DB-handle hang into a visible failure instead of an infinite block). - hardCmd("unit", "Unit tests (full suite, CI concurrency — runs ~20-35min silently)", npmCmd, ["run", "test:unit:ci"], { timeout: 45 * 60 * 1000 }); - hardCmd("vitest", "Vitest (MCP / autoCombo / cache — ~3-8min)", npmCmd, ["run", "test:vitest"], { timeout: 15 * 60 * 1000 }); + hardCmd( + "unit", + "Unit tests (full suite, CI concurrency — runs ~20-35min silently)", + npmCmd, + ["run", "test:unit:ci"], + { timeout: 45 * 60 * 1000 } + ); + hardCmd( + "vitest", + "Vitest (MCP / autoCombo / cache — ~3-8min)", + npmCmd, + ["run", "test:vitest"], + { timeout: 15 * 60 * 1000 } + ); // Integration tests run ONLY on the release PR full CI (PR→main), so an assertion // regression here (e.g. a contributor flipping a Codex fingerprint key order) is // invisible until release — run them in the pre-flight as a HARD gate. - hardCmd("integration", "Integration tests (~3-10min)", npmCmd, ["run", "test:integration"], { timeout: 20 * 60 * 1000 }); + hardCmd("integration", "Integration tests (~3-10min)", npmCmd, ["run", "test:integration"], { + timeout: 20 * 60 * 1000, + }); } if (WITH_BUILD) { - hardCmd("pack-artifact", "Package artifact (npm pack policy)", npmCmd, ["run", "check:pack-artifact"], { timeout: 20 * 60 * 1000 }); + hardCmd( + "pack-artifact", + "Package artifact (npm pack policy)", + npmCmd, + ["run", "check:pack-artifact"], + { timeout: 20 * 60 * 1000 } + ); } const { releaseGreen, hardFailures, drift } = computeVerdict(results); diff --git a/scripts/vps/release-runner-down.sh b/scripts/vps/release-runner-down.sh new file mode 100755 index 0000000000..dd27087cad --- /dev/null +++ b/scripts/vps/release-runner-down.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Desliga a VM self-hosted (VPS 113) ao fim do release e volta o CI para o GitHub-hosted. +# Idempotente: seguro chamar mesmo que a VM já esteja desligada. +# +# Uso: scripts/vps/release-runner-down.sh +set -uo pipefail + +PVE_HOST="${PVE_HOST:-192.168.0.100}" +VM_ID="${VM_ID:-113}" +REPO="${REPO:-diegosouzapw/OmniRoute}" +SSH="ssh -o BatchMode=yes -o ConnectTimeout=8" + +# 1) Volta o CI para ubuntu-latest ANTES de derrubar a VM (evita jobs presos). +echo "[release-runner] USE_VPS_RUNNER=false (CI volta ao GitHub-hosted)." +gh variable set USE_VPS_RUNNER --repo "$REPO" --body "false" >/dev/null 2>&1 || true + +# 2) Shutdown graceful da VM (libera os 32 cores / 24GB de volta ao host). +echo "[release-runner] desligando VM $VM_ID (graceful)..." +$SSH "root@$PVE_HOST" "qm shutdown $VM_ID --timeout 120" 2>/dev/null \ + || $SSH "root@$PVE_HOST" "qm stop $VM_ID" 2>/dev/null \ + || echo "[release-runner] ⚠️ não consegui desligar a VM $VM_ID — verifique manualmente." +echo "[release-runner] pronto." diff --git a/scripts/vps/release-runner-up.sh b/scripts/vps/release-runner-up.sh new file mode 100755 index 0000000000..8f35066d33 --- /dev/null +++ b/scripts/vps/release-runner-up.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Liga a VM self-hosted (VPS 113) e aguarda seus runners ficarem online, para a fase +# de release usar runners dedicados (anti-fila). Falha => o caller cai no GitHub-hosted. +# +# Uso: scripts/vps/release-runner-up.sh [timeout_seg] +# Saída: exit 0 + seta a repo-var USE_VPS_RUNNER=true quando >=1 runner omni-release online +# exit 1 + seta USE_VPS_RUNNER=false em qualquer falha/timeout (fallback) +# +# Pré-requisitos no host que roda o /generate-release: +# - chave SSH autorizada em root@$PVE_HOST (Proxmox) e root@$VPS_HOST (a VM) +# - gh autenticado com admin no repo (para ler runners + setar a variable) +set -uo pipefail + +PVE_HOST="${PVE_HOST:-192.168.0.100}" # Proxmox host +VPS_HOST="${VPS_HOST:-192.168.0.113}" # a VM dos runners +VM_ID="${VM_ID:-113}" +REPO="${REPO:-diegosouzapw/OmniRoute}" +LABEL="${RUNNER_LABEL:-omni-release}" +TIMEOUT="${1:-120}" +SSH="ssh -o BatchMode=yes -o ConnectTimeout=8" + +fallback() { + echo "[release-runner] ⚠️ $1 — usando GitHub-hosted (fallback)." + gh variable set USE_VPS_RUNNER --repo "$REPO" --body "false" >/dev/null 2>&1 || true + exit 1 +} + +echo "[release-runner] ligando VM $VM_ID no Proxmox $PVE_HOST..." +$SSH "root@$PVE_HOST" "qm start $VM_ID" 2>/dev/null || true # ok se já estiver rodando + +echo "[release-runner] aguardando runners '$LABEL' ficarem online (timeout ${TIMEOUT}s)..." +deadline=$(( $(date +%s) + TIMEOUT )) +while [ "$(date +%s)" -lt "$deadline" ]; do + online=$(gh api "repos/$REPO/actions/runners" \ + --jq "[.runners[] | select(.status==\"online\") | select(.labels[].name==\"$LABEL\")] | length" \ + 2>/dev/null || echo 0) + if [ "${online:-0}" -ge 1 ]; then + echo "[release-runner] ✅ $online runner(s) '$LABEL' online — usando a VPS." + gh variable set USE_VPS_RUNNER --repo "$REPO" --body "true" >/dev/null 2>&1 \ + || fallback "não consegui setar USE_VPS_RUNNER" + exit 0 + fi + sleep 6 +done +fallback "runners não ficaram online a tempo" diff --git a/skills/omni-github-skills/SKILL.md b/skills/omni-github-skills/SKILL.md new file mode 100644 index 0000000000..da835d38e1 --- /dev/null +++ b/skills/omni-github-skills/SKILL.md @@ -0,0 +1,22 @@ +--- +name: omni-github-skills +description: Search, score, scan, and import agent skills from GitHub repositories that contain SKILL.md, CLAUDE.md, .cursorrules, and similar agent skill files. Discover community skills across 160+ provider categories, evaluate relevance with heuristic scoring, check for malware or hardcoded secrets, and install into Hermes, Claude Code, Gemini CLI, or OpenCode agent directories. +--- + + + +## Overview + +Search, score, scan, and import agent skills from GitHub repositories that contain SKILL.md, CLAUDE.md, .cursorrules, and similar agent skill files. Discover community skills across 160+ provider categories, evaluate relevance with heuristic scoring, check for malware or hardcoded secrets, and install into Hermes, Claude Code, Gemini CLI, or OpenCode agent directories. + +## Authentication + +All requests require a valid Bearer token or session cookie. Obtain a token via `POST /api/auth/login` or configure `REQUIRE_API_KEY=false` for local development. + +## Endpoints + +_No endpoints mapped for this area yet._ + +## Payloads + +See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas. diff --git a/src/app/(dashboard)/dashboard/combos/WeightTotalBar.tsx b/src/app/(dashboard)/dashboard/combos/WeightTotalBar.tsx index 3d0a6cbe65..bacb890994 100644 --- a/src/app/(dashboard)/dashboard/combos/WeightTotalBar.tsx +++ b/src/app/(dashboard)/dashboard/combos/WeightTotalBar.tsx @@ -20,6 +20,20 @@ const WEIGHT_COLORS = [ "bg-indigo-500", ]; +/** + * #6147 — effective routing share of a weighted target. + * + * The per-target `weight` values do not have to sum to 100; at routing time each + * target is picked with probability `weight / Σweights`. So a raw weight of 30 + * with a total of 60 is an *effective* 50% share. This pure helper computes that + * share (0-100) and guards the `total === 0` case so the UI never renders NaN. + */ +export function effectiveSharePercent(weight: number, total: number): number { + if (!weight || weight <= 0) return 0; + if (!total || total <= 0) return 0; + return (weight / total) * 100; +} + export default function WeightTotalBar({ models }: WeightTotalBarProps) { const total = models.reduce((sum, m) => sum + (m.weight || 0), 0); const isValid = total === 100; @@ -49,6 +63,16 @@ export default function WeightTotalBar({ models }: WeightTotalBarProps) { className={`inline-block w-1.5 h-1.5 rounded-full ${WEIGHT_COLORS[i % WEIGHT_COLORS.length]}`} /> {m.weight}% + {/* #6147 — show the *effective* routing share when weights don't sum to 100 */} + {total > 0 && total !== 100 && ( + + {" → "} + {Math.round(effectiveSharePercent(m.weight, total))}% + + )} ) )} diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index 5f546db165..c5c068000c 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -3972,6 +3972,54 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo + {/* #6168: per-combo session-stickiness override (tri-state so it can + force ON or OFF regardless of the global default; blank = inherit). */} +
+ + +
{strategy === "context-relay" && (
diff --git a/src/app/(dashboard)/dashboard/free-provider-rankings/page.tsx b/src/app/(dashboard)/dashboard/free-provider-rankings/page.tsx index a112f299d5..a4689b1569 100644 --- a/src/app/(dashboard)/dashboard/free-provider-rankings/page.tsx +++ b/src/app/(dashboard)/dashboard/free-provider-rankings/page.tsx @@ -60,14 +60,21 @@ export default function FreeProviderRankingsPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [filter, setFilter] = useState(""); + const [configuredOnly, setConfiguredOnly] = useState(false); + const [availableOnly, setAvailableOnly] = useState(false); const fetchRankings = useCallback( - async (category?: string) => { + async (category?: string, opts?: { configuredOnly?: boolean; availableOnly?: boolean }) => { setLoading(true); setError(""); try { - const url = category - ? `/api/free-provider-rankings?category=${category}` + const params = new URLSearchParams(); + if (category) params.set("category", category); + if (opts?.configuredOnly) params.set("configuredOnly", "1"); + if (opts?.availableOnly) params.set("availableOnly", "1"); + const qs = params.toString(); + const url = qs + ? `/api/free-provider-rankings?${qs}` : "/api/free-provider-rankings"; const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); @@ -83,8 +90,8 @@ export default function FreeProviderRankingsPage() { ); useEffect(() => { - fetchRankings(filter || undefined); - }, [filter, fetchRankings]); + fetchRankings(filter || undefined, { configuredOnly, availableOnly }); + }, [filter, configuredOnly, availableOnly, fetchRankings]); return (
@@ -113,6 +120,33 @@ export default function FreeProviderRankingsPage() { ))}
+ {/* Availability toggles (default off → show all providers) */} +
+ + +
+ {error &&
{error}
} {loading ? ( diff --git a/src/app/(dashboard)/dashboard/providers/[id]/__tests__/phase1e.test.tsx b/src/app/(dashboard)/dashboard/providers/[id]/__tests__/phase1e.test.tsx index fb0562c8a8..a65bad6019 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/__tests__/phase1e.test.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/__tests__/phase1e.test.tsx @@ -15,6 +15,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { buildCompatMap, isModelHiddenFn, + getDisplayModelAlias, effectiveNormalizeForProtocol, effectivePreserveForProtocol, anyNormalizeCompatBadge, @@ -35,10 +36,7 @@ vi.mock("next/navigation", () => ({ vi.mock("next-intl", () => ({ useTranslations: () => (key: string, values?: Record) => { if (values) { - return Object.entries(values).reduce( - (acc, [k, v]) => acc.replace(`{${k}}`, String(v)), - key - ); + return Object.entries(values).reduce((acc, [k, v]) => acc.replace(`{${k}}`, String(v)), key); } return key; }, @@ -85,6 +83,22 @@ describe("providerPageHelpers — model-compat pure functions", () => { expect(isModelHiddenFn("unknown-model", customMap, overrideMap)).toBe(false); }); + it("isModelHiddenFn ignores deleted tombstones when reading visibility", () => { + const customMap = buildCompatMap([]); + const overrideMap = buildCompatMap([ + { id: "gpt-4o-2024-11-20", isHidden: true, isDeleted: true }, + { id: "gpt-5-mini", isHidden: true }, + ]); + + expect(isModelHiddenFn("gpt-4o-2024-11-20", customMap, overrideMap)).toBe(false); + expect(isModelHiddenFn("gpt-5-mini", customMap, overrideMap)).toBe(true); + }); + + it("getDisplayModelAlias ignores provider-scoped identity aliases", () => { + expect(getDisplayModelAlias("gpt-4o-2024-11-20", "gpt-4o-2024-11-20")).toBeNull(); + expect(getDisplayModelAlias("gpt-5-mini", "fast-mini")).toBe("fast-mini"); + }); + it("effectiveNormalizeForProtocol returns correct flag", () => { const customMap = buildCompatMap(customModels); const overrideMap = buildCompatMap(overrideModels); @@ -115,10 +129,10 @@ describe("providerPageHelpers — model-compat pure functions", () => { }); it("formatProviderModelsErrorResponse extracts error.message", async () => { - const mockRes = new Response( - JSON.stringify({ error: { message: "Model not found" } }), - { status: 422, statusText: "Unprocessable Entity" } - ); + const mockRes = new Response(JSON.stringify({ error: { message: "Model not found" } }), { + status: 422, + statusText: "Unprocessable Entity", + }); const detail = await formatProviderModelsErrorResponse(mockRes); expect(detail).toBe("Model not found"); }); @@ -185,7 +199,9 @@ describe("PassthroughModelRow — render smoke test", () => { }); afterEach(() => { - act(() => { root.unmount(); }); + act(() => { + root.unmount(); + }); container.remove(); }); @@ -222,7 +238,9 @@ describe("ModelVisibilityToolbar — render smoke test", () => { }); afterEach(() => { - act(() => { root.unmount(); }); + act(() => { + root.unmount(); + }); container.remove(); }); @@ -259,7 +277,9 @@ describe("useModelCompatState — hook unit test via component wrapper", () => { }); afterEach(() => { - act(() => { root.unmount(); }); + act(() => { + root.unmount(); + }); container.remove(); }); @@ -267,7 +287,12 @@ describe("useModelCompatState — hook unit test via component wrapper", () => { const { useModelCompatState } = await import("../hooks/useModelCompatState"); const customModels = [ - { id: "gpt-4o", normalizeToolCallId: true, preserveOpenAIDeveloperRole: false, isHidden: true }, + { + id: "gpt-4o", + normalizeToolCallId: true, + preserveOpenAIDeveloperRole: false, + isHidden: true, + }, ]; const modelCompatOverrides: any[] = []; @@ -281,7 +306,9 @@ describe("useModelCompatState — hook unit test via component wrapper", () => { compat.effectiveModelPreserveDeveloper("gpt-4o"), compat.anyNormalizeCompatBadge("gpt-4o"), compat.anyNoPreserveCompatBadge("gpt-4o"), - ].map(String).join(","); + ] + .map(String) + .join(","); return {results}; } @@ -291,8 +318,9 @@ describe("useModelCompatState — hook unit test via component wrapper", () => { const span = container.querySelector("[data-testid='results']"); expect(span).not.toBeNull(); - const [hidden, notHidden, normalize, preserve, anyNorm, anyNoPreserve] = - (span!.textContent ?? "").split(","); + const [hidden, notHidden, normalize, preserve, anyNorm, anyNoPreserve] = ( + span!.textContent ?? "" + ).split(","); expect(hidden).toBe("true"); expect(notHidden).toBe("false"); diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/CompatibleModelsSection.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/CompatibleModelsSection.tsx index 94c1307175..7a8542265b 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/CompatibleModelsSection.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/CompatibleModelsSection.tsx @@ -17,6 +17,7 @@ import { resolveManagedModelAlias } from "@/shared/utils/providerModelAliases"; import { useNotificationStore } from "@/store/notificationStore"; import { buildCompatMap, + getDisplayModelAlias, providerText, type CompatModelRow, } from "../providerPageHelpers"; @@ -57,10 +58,7 @@ export interface CompatibleModelsSectionProps { effectiveModelNormalize: (alias: string) => boolean; effectiveModelPreserveDeveloper: (alias: string) => boolean; getUpstreamHeadersRecord: (modelId: string, protocol: string) => Record; - saveModelCompatFlags: ( - modelId: string, - flags: CompatibleModelsSaveFlags - ) => Promise; + saveModelCompatFlags: (modelId: string, flags: CompatibleModelsSaveFlags) => Promise; compatSavingModelId?: string; onModelsChanged?: () => void; isModelHidden: (modelId: string) => boolean; @@ -155,7 +153,8 @@ export default function CompatibleModelsSection({ for (const [alias, fullModel] of providerAliases) { const fmStr = fullModel as string; const modelId = fmStr.startsWith(prefix) ? fmStr.slice(prefix.length) : fmStr; - aliasByModelId.set(modelId, alias as string); + const displayAlias = getDisplayModelAlias(modelId, alias as string); + if (displayAlias) aliasByModelId.set(modelId, displayAlias); } const addModel = (model: CompatModelRow, source: string) => { @@ -194,11 +193,13 @@ export default function CompatibleModelsSection({ const fmStr = fullModel as string; const modelId = fmStr.startsWith(prefix) ? fmStr.slice(prefix.length) : fmStr; if (!modelId || seenModelIds.has(modelId)) continue; + const displayAlias = getDisplayModelAlias(modelId, alias as string); + if (!displayAlias) continue; const customModel = customModelMap.get(modelId); rows.push({ modelId, - alias: alias as string, - displayName: alias as string, + alias: displayAlias, + displayName: displayAlias, source: customModel ? customModel.source || "custom" : "alias", isFree: modelId.endsWith(":free") || diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx index e3dda0a620..fb0d2b6e62 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx @@ -21,6 +21,7 @@ import { import { useNotificationStore } from "@/store/notificationStore"; import { buildCompatMap, + getDisplayModelAlias, providerText, testAllResultsText, evaluateTestAllEntry, @@ -228,7 +229,8 @@ export default function PassthroughModelsSection({ for (const [alias, fullModel] of providerAliases) { const fmStr = fullModel as string; const modelId = fmStr.startsWith(prefix) ? fmStr.slice(prefix.length) : fmStr; - aliasByModelId.set(modelId, alias as string); + const displayAlias = getDisplayModelAlias(modelId, alias as string); + if (displayAlias) aliasByModelId.set(modelId, displayAlias); fullModelByModelId.set(modelId, fmStr); } @@ -266,12 +268,14 @@ export default function PassthroughModelsSection({ const fmStr = fullModel as string; const modelId = fmStr.startsWith(prefix) ? fmStr.slice(prefix.length) : fmStr; if (!modelId || seenModelIds.has(modelId)) continue; + const displayAlias = getDisplayModelAlias(modelId, alias as string); + if (!displayAlias) continue; const customModel = customModelMap.get(modelId); rows.push({ modelId, fullModel: fmStr, - alias: alias as string, - displayName: alias as string, + alias: displayAlias, + displayName: displayAlias, source: customModel ? customModel.source || "custom" : "alias", isFree: modelId.endsWith(":free") || diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModelsSection.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModelsSection.tsx index c2edb4b633..1b252b209e 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModelsSection.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModelsSection.tsx @@ -15,7 +15,11 @@ import { useState } from "react"; import { Button } from "@/shared/components"; import { matchesModelCatalogQuery } from "@/shared/utils/modelCatalogSearch"; import { isFreeModel, sortModelsFreeFirst } from "@/shared/utils/freeModels"; -import { providerText, type ProviderMessageTranslator } from "../providerPageHelpers"; +import { + getDisplayModelAlias, + providerText, + type ProviderMessageTranslator, +} from "../providerPageHelpers"; import ModelRow, { ModelVisibilityToolbar } from "./ModelRow"; import PassthroughModelsSection from "./PassthroughModelsSection"; import CompatibleModelsSection from "./CompatibleModelsSection"; @@ -86,11 +90,7 @@ export interface ProviderModelsSectionProps { setAutoHideFailed: (v: boolean) => void; setVisibilityFilter: (v: "all" | "visible" | "hidden") => void; saveModelCompatFlags: (modelId: string, patch: ModelCompatSavePatch) => Promise; - handleToggleModelHidden: ( - providerKey: string, - modelId: string, - hidden: boolean - ) => Promise; + handleToggleModelHidden: (providerKey: string, modelId: string, hidden: boolean) => Promise; handleBulkToggleModelHidden: ( providerKey: string, modelIds: string[], @@ -187,8 +187,7 @@ export default function ProviderModelsSection({ ); - const clearAllButton = (modelMeta.customModels.length > 0 || - providerAliasEntries.length > 0) && ( + const clearAllButton = (modelMeta.customModels.length > 0 || providerAliasEntries.length > 0) && (