diff --git a/.env.example b/.env.example index e73f3203f1..1aa19cd898 100644 --- a/.env.example +++ b/.env.example @@ -3043,6 +3043,13 @@ QUOTA_STORE_DRIVER=sqlite # CHATGPT_WEB_CODEX_CHROME_PATH=/usr/bin/chromium # CHROME_PATH=/usr/bin/chromium # CHATGPT_WEB_CODEX_CDP_URL=http://chatgpt-web-codex-browser:9223 +# CDP_PROXY_TOKEN required by docker/chatgpt-web-codex-browser/cdp-proxy.mjs (#13679): +# when set, every request to the CDP proxy sidecar must present it as an +# `X-Omni-Cdp-Token` header. Left unset, the proxy keeps forwarding requests +# unauthenticated (network isolation via docker-compose.yml's dedicated +# `chatgpt-web-codex-net` is the default mitigation). Generate with: +# `openssl rand -hex 32` +# CDP_PROXY_TOKEN= # CHATGPT_WEB_CODEX_TUNNEL_ID=tunnel_0123456789abcdef0123456789abcdef # CHATGPT_WEB_CODEX_RUNTIME_KEY= # CHATGPT_WEB_CODEX_CONNECTOR_NAME=OmniRoute Codex v2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9317e3102..9937d3512d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -533,6 +533,15 @@ jobs: env: BASE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }} run: node scripts/i18n/check-new-key-coverage.mjs + # Absolute complement of the two gates above: every locale must carry exactly the key + # set of en.json, whatever the age of the key. A locale batch is generated from the + # en.json of the day the branch is cut and translates for days while the base keeps + # adding keys — the batch PR adds no key itself, so the new-key gate stays silent and + # 43 absent keys out of ~13,000 still read 99.7 % coverage. Incident 2026-09-15: + # batch 1 (#13044) landed 43 keys short in nine locales, batch 2 (#13660) 10 keys short + # in eight. Fix is `sync-ui-keys --locale= --translate-markers`. + - name: i18n key completeness (every locale carries every en.json key) + run: node scripts/i18n/check-key-completeness.mjs # #8038: cheap glossary/protected-terms consistency gate — # complements i18n-ui-coverage (key parity) and the ICU `i18n` job below diff --git a/.github/workflows/release-acceptance.yml b/.github/workflows/release-acceptance.yml new file mode 100644 index 0000000000..75527ad2dd --- /dev/null +++ b/.github/workflows/release-acceptance.yml @@ -0,0 +1,42 @@ +name: Release acceptance + +on: + push: + branches: ["release/v*"] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: release-acceptance-${{ github.ref }} + cancel-in-progress: false + +jobs: + acceptance: + name: Release acceptance + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: actions/setup-node@v5 + with: + node-version: "22" + cache: npm + - run: npm ci + - name: Emit shadow acceptance report + run: | + node scripts/quality/validate-release-acceptance.mjs \ + --plan tests/fixtures/release-acceptance/plan-lint.json \ + --manifests tests/fixtures/release-acceptance/shadow-manifests \ + --out release-acceptance-report.json + continue-on-error: true + - uses: actions/upload-artifact@v4 + if: always() + with: + name: release-acceptance-report + path: release-acceptance-report.json + if-no-files-found: ignore + retention-days: 30 diff --git a/@omniroute/opencode-plugin/package.json b/@omniroute/opencode-plugin/package.json index 4dc257e274..e36e20b984 100644 --- a/@omniroute/opencode-plugin/package.json +++ b/@omniroute/opencode-plugin/package.json @@ -23,7 +23,7 @@ "scripts": { "build": "tsup", "clean": "rm -rf dist", - "test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/telemetry.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/feature-defaults.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts tests/model-allowlist.test.ts tests/log-level.test.ts tests/effort-tier-variants.test.ts tests/naming.test.ts tests/free-budget-magnitude.test.ts tests/models-fetcher.test.ts", + "test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/telemetry.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/feature-defaults.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts tests/model-allowlist.test.ts tests/log-level.test.ts tests/effort-tier-variants.test.ts tests/naming.test.ts tests/free-budget-magnitude.test.ts tests/models-fetcher.test.ts tests/issue-13000-cold-start-combo-limit.test.ts", "prepublishOnly": "npm run clean && npm run build && npm test" }, "keywords": [ diff --git a/@omniroute/opencode-plugin/src/index.ts b/@omniroute/opencode-plugin/src/index.ts index 39e3c6f274..f00acf9416 100644 --- a/@omniroute/opencode-plugin/src/index.ts +++ b/@omniroute/opencode-plugin/src/index.ts @@ -4631,9 +4631,21 @@ export function buildStaticProviderEntry( .map((m) => m.max_output_tokens) .filter((v): v is number => typeof v === "number" && v > 0); - if (contextValues.length > 0 && outputValues.length > 0) { + // Prefer the server-computed aggregate (accounts for explicit + // context_length overrides and members outside memberEntries, e.g. + // not yet resolved in /v1/models) over the raw Math.min(member) + // lower bound. Mirrors mapComboToModelV2's limit.context logic + // (#13000) so the static catalog and the dynamic hook agree. + const preferredContext = + typeof combo.computed_context_length === "number" && combo.computed_context_length > 0 + ? combo.computed_context_length + : contextValues.length > 0 + ? Math.min(...contextValues) + : undefined; + + if (preferredContext !== undefined && outputValues.length > 0) { entry.limit = { - context: Math.min(...contextValues), + context: preferredContext, output: Math.min(...outputValues), }; } @@ -5511,6 +5523,32 @@ export function createOmniRouteConfigHook( const modelsFetchOk = !modelsFetchThrew && localRawModels.length > 0; + // Snapshot backfill for computed_context_length: a live /api/combos + // response can come back without this field (server hasn't finished + // recomputing it yet, e.g. just after a restart) even though the + // combo's members and identity are otherwise unchanged. When that + // happens, prefer the last-known-good value from the warm disk + // snapshot over the Math.min(member) fallback in + // mapComboToModelV2() — never overwrite any other combo field + // (models/name/etc.) with stale data, only this one derived number. + if (warmSnapshot) { + const snapshotComboById = new Map(warmSnapshot.rawCombos.map((c) => [c.id, c])); + for (const combo of localRawCombos) { + const hasLive = + typeof combo.computed_context_length === "number" && + combo.computed_context_length > 0; + if (hasLive) continue; + const stale = snapshotComboById.get(combo.id); + if ( + stale && + typeof stale.computed_context_length === "number" && + stale.computed_context_length > 0 + ) { + combo.computed_context_length = stale.computed_context_length; + } + } + } + // Disk-cache fallback (cold first run, no warm snapshot): when the // live fetch returned no models AND features.diskCache !== false, // hydrate from the last-known-good snapshot so OC still surfaces a diff --git a/@omniroute/opencode-plugin/tests/issue-13000-cold-start-combo-limit.test.ts b/@omniroute/opencode-plugin/tests/issue-13000-cold-start-combo-limit.test.ts new file mode 100644 index 0000000000..640c0678ca --- /dev/null +++ b/@omniroute/opencode-plugin/tests/issue-13000-cold-start-combo-limit.test.ts @@ -0,0 +1,221 @@ +/** + * Repro for #13000: combo context limits fall back to Math.min(member) + * instead of using computed_context_length after cold start — no disk + * snapshot fallback. + * + * Scenario (mirrors the report): a warm disk snapshot holds the combo with + * its correct server-computed `computed_context_length` (245000, from all 6 + * members). After a restart, the live refresh's combos fetch returns the + * SAME combo but without `computed_context_length` (e.g. the value hasn't + * propagated yet), and the live models fetch only resolves 2 of the 6 + * members (the rest not yet in /v1/models). The background refresh then + * republishes the provider block built from this degraded live data, + * downgrading a previously-known-good 245000 limit to Math.min(163840, + * 1_000_000) = 163840 — exactly the member-minimum described in the issue. + */ + +import test from "node:test"; +import assert from "node:assert/strict"; +import type { Config } from "@opencode-ai/plugin"; + +import { + createOmniRouteConfigHook, + _resetInflightRefresh, + type OmniRouteAutoCombosFetcher, + type OmniRouteCombosFetcher, + type OmniRouteCompressionMetaFetcher, + type OmniRouteEnrichmentFetcher, + type OmniRouteFetchCache, + type OmniRouteModelsFetcher, + type OmniRouteProvidersFetcher, + type OmniRouteRawCombo, + type OmniRouteRawModelEntry, + type OmniRouteReadAuthJson, + type OmniRouteStaticProviderEntry, + type OmniRouteDiskSnapshotReader, + type OmniRouteDiskSnapshotWriter, +} from "../src/index.js"; + +test.beforeEach(() => { + _resetInflightRefresh(); +}); + +function stubReadAuthJson(value: Record): OmniRouteReadAuthJson { + return async () => value as never; +} + +function authStub() { + return stubReadAuthJson({ + "opencode-omniroute": { + type: "api", + key: "sk-test", + baseURL: "https://or.example.com/v1", + }, + }); +} + +function makeInput(): Config { + return { provider: {} } as unknown as Config; +} + +// The two members resolvable in the degraded live /v1/models response. +const MEMBER_DEEPSEEK: OmniRouteRawModelEntry = { + id: "deepseek-v4-pro", + capabilities: { tool_calling: true, reasoning: true, vision: false, thinking: false }, + context_length: 163_840, + max_output_tokens: 64_000, + input_modalities: ["text"], + output_modalities: ["text"], +}; + +const MEMBER_GLM: OmniRouteRawModelEntry = { + id: "glm-5.2", + capabilities: { tool_calling: true, reasoning: true, vision: false, thinking: false }, + context_length: 1_000_000, + max_output_tokens: 16_384, + input_modalities: ["text"], + output_modalities: ["text"], +}; + +// The other member that IS present once the server is fully warm. +const MEMBER_GLM_53_HIGH: OmniRouteRawModelEntry = { + id: "GLM-5.3-high", + capabilities: { tool_calling: true, reasoning: true, vision: false, thinking: false }, + context_length: 245_000, + max_output_tokens: 128_000, + input_modalities: ["text"], + output_modalities: ["text"], +}; + +const COMBO_MODELS: OmniRouteRawCombo["models"] = [ + { kind: "model", model: "deepseek-v4-pro", weight: 25 }, + { kind: "model", model: "glm-5.2", weight: 25 }, + { kind: "model", model: "GLM-5.3-high", weight: 50 }, +]; + +test("issue #13000: warm combo limit (245000) survives a degraded post-restart refresh instead of downgrading to Math.min(member)", async () => { + const warmSnapshot: Omit = { + rawModels: [MEMBER_DEEPSEEK, MEMBER_GLM, MEMBER_GLM_53_HIGH], + rawCombos: [ + { + id: "orchestrator", + name: "orchestrator", + models: COMBO_MODELS, + computed_context_length: 245_000, + }, + ], + rawAutoCombos: [], + rawEnrichment: new Map(), + rawCompressionCombos: [], + rawConnections: [], + }; + + const fetcher: OmniRouteModelsFetcher = async () => [MEMBER_DEEPSEEK, MEMBER_GLM]; + const combosFetcher: OmniRouteCombosFetcher = async () => [ + { + id: "orchestrator", + name: "orchestrator", + models: COMBO_MODELS, + // computed_context_length intentionally omitted. + }, + ]; + const autoCombosFetcher: OmniRouteAutoCombosFetcher = async () => []; + const enrichmentFetcher: OmniRouteEnrichmentFetcher = async () => new Map(); + const compressionMetaFetcher: OmniRouteCompressionMetaFetcher = async () => []; + const providersFetcher: OmniRouteProvidersFetcher = async () => []; + + const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => warmSnapshot; + const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {}; + + const sharedCache: OmniRouteFetchCache = new Map(); + + const hook = createOmniRouteConfigHook( + { providerId: "omniroute", modelCacheTtl: 60_000 }, + { + readAuthJson: authStub(), + fetcher, + combosFetcher, + autoCombosFetcher, + enrichmentFetcher, + compressionMetaFetcher, + providersFetcher, + diskSnapshotReader, + diskSnapshotWriter, + cache: sharedCache, + } + ); + + const input = makeInput(); + await hook(input); + + // Let the detached background refresh (degraded live data) complete and + // republish the block. + await new Promise((r) => setTimeout(r, 100)); + + const entryAfter = (input as { provider: Record }).provider[ + "opencode-omniroute" + ]; + const comboModelAfter = entryAfter.models["orchestrator"]; + assert.ok(comboModelAfter, "combo model still published after refresh"); + + assert.equal( + comboModelAfter.limit.context, + 245_000, + `expected the combo limit to stay at the known-good 245000, but got ${comboModelAfter.limit.context} ` + + `(Math.min(member) fallback — the exact bug described in #13000)` + ); +}); + +test("issue #13000 (control): no warm snapshot exists — Math.min(member) fallback is still used (expected, documented behavior)", async () => { + const fetcher: OmniRouteModelsFetcher = async () => [MEMBER_DEEPSEEK, MEMBER_GLM]; + const combosFetcher: OmniRouteCombosFetcher = async () => [ + { + id: "orchestrator", + name: "orchestrator", + models: COMBO_MODELS, + // computed_context_length intentionally omitted. + }, + ]; + const autoCombosFetcher: OmniRouteAutoCombosFetcher = async () => []; + const enrichmentFetcher: OmniRouteEnrichmentFetcher = async () => new Map(); + const compressionMetaFetcher: OmniRouteCompressionMetaFetcher = async () => []; + const providersFetcher: OmniRouteProvidersFetcher = async () => []; + + // No prior snapshot on disk. + const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined; + const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {}; + + const sharedCache: OmniRouteFetchCache = new Map(); + + const hook = createOmniRouteConfigHook( + { providerId: "omniroute", modelCacheTtl: 60_000 }, + { + readAuthJson: authStub(), + fetcher, + combosFetcher, + autoCombosFetcher, + enrichmentFetcher, + compressionMetaFetcher, + providersFetcher, + diskSnapshotReader, + diskSnapshotWriter, + cache: sharedCache, + } + ); + + const input = makeInput(); + await hook(input); + + const entryAfter = (input as { provider: Record }).provider[ + "opencode-omniroute" + ]; + const comboModelAfter = entryAfter.models["orchestrator"]; + assert.ok(comboModelAfter, "combo model published on cold first run"); + + // No snapshot to backfill from — Math.min(163840, 1_000_000) = 163840. + assert.equal( + comboModelAfter.limit.context, + 163_840, + "pure cold start with no snapshot must keep using the Math.min(member) fallback" + ); +}); diff --git a/AGENTS.md b/AGENTS.md index 69e81b7ab3..89b2ab9adf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,7 @@ Repository map and Reference Documentation sections below. ## Project at a Glance -**OmniRoute** — unified AI proxy/router. One endpoint, 358 LLM providers, auto-fallback. +**OmniRoute** — unified AI proxy/router. One endpoint, 359 LLM providers, auto-fallback. | Layer | Location | Purpose | | ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -56,7 +56,7 @@ Repository map and Reference Documentation sections below. | Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) | | Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions | | Services | `open-sse/services/` | Combo routing, rate limits, caching, etc | -| Database | `src/lib/db/` | SQLite domain modules (176 migrations) | +| Database | `src/lib/db/` | SQLite domain modules (177 migrations) | | Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic | | MCP Server | `open-sse/mcp-server/` | 110 tools (45 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes | | A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol | diff --git a/README.md b/README.md index edca9ae592..8232d5128a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ # 🚀 OmniRoute — The Free AI Gateway -OmniRoute — Never stop coding. Every AI tool → 358 providers — 150+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 358 AI providers · 150+ free tiers · ~1.47B free tokens/mo · 19 routing strategies · $0 to start. +OmniRoute — Never stop coding. Every AI tool → 359 providers — 150+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 359 AI providers · 150+ free tiers · ~1.47B free tokens/mo · 19 routing strategies · $0 to start. @@ -17,9 +17,9 @@ -> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **443 free-tier entries across 34 recurring pool keys** and computes the token headline from the **16 pools with a published positive monthly budget plus five per-model Groq caps**, deduplicated by shared pool. Quotas that only open after a regional identity check (today: ModelScope) are shown apart, +~6M behind regional identity verification, and never summed into the headline. The result stays visible on the dashboard (`/dashboard/free-tiers`). +> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **446 free-tier entries across 34 recurring pool keys** and computes the token headline from the **16 pools with a published positive monthly budget plus five per-model Groq caps**, deduplicated by shared pool. Quotas that only open after a regional identity check (today: ModelScope) are shown apart, +~6M behind regional identity verification, and never summed into the headline. The result stays visible on the dashboard (`/dashboard/free-tiers`). -OmniRoute free-tier budget card: ~1.47B free tokens per month steady, up to ~2.07B in the first month with signup credits, from 34 documented recurring pool keys covering 443 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 16 recurring pools with a published positive monthly token budget plus five per-model Groq caps; 13 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, Nara 210M, LLM7 150M, Groq 30M (five per-model caps) and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers. +OmniRoute free-tier budget card: ~1.47B free tokens per month steady, up to ~2.07B in the first month with signup credits, from 34 documented recurring pool keys covering 446 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 16 recurring pools with a published positive monthly token budget plus five per-model Groq caps; 13 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, Nara 210M, LLM7 150M, Groq 30M (five per-model caps) and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers. > Animated summary of the live `/dashboard/free-tiers` page. Full methodology (pool dedupe, credit tiers, provider terms): **[docs/reference/FREE_TIERS.md](docs/reference/FREE_TIERS.md)**. > @@ -233,7 +233,7 @@ curl http://localhost:20128/v1/chat/completions \ -The Promise — One endpoint and 358 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 358 providers · up to 95% token savings on eligible workloads · $0 to start with 150+ free tiers and 52 recurring/keyless free-forever providers · 36 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files. +The Promise — One endpoint and 359 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 359 providers · up to 95% token savings on eligible workloads · $0 to start with 150+ free tiers and 53 recurring/keyless free-forever providers · 36 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files.

@@ -486,7 +486,7 @@ All **19** strategies — mix & match per combo step: -What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 358 providers, 150+ free tiers built in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA and 42 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology. +What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 359 providers, 150+ free tiers built in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA and 42 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology. 📊 Full methodology & per-feature detail vs 9router, OpenRouter, CLIProxyAPI & LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md) @@ -672,7 +672,7 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md) -> **352 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **152 carrying `hasFree: true` discovery metadata**. The chat model registry covers **229 providers / 2,554 distinct provider-model pairs / 1,283 raw model IDs**; the separate free-budget catalog has **443 per-model rows**, **34 recurring pools** and **52 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md). +> **352 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **152 carrying `hasFree: true` discovery metadata**. The chat model registry covers **229 providers / 2,554 distinct provider-model pairs / 1,283 raw model IDs**; the separate free-budget catalog has **443 per-model rows**, **34 recurring pools** and **53 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md).
@@ -1268,7 +1268,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi RuntimeNode.js 22.x / 24.x LTS — >=22.22.2 <23 || >=24.0.0 <27 LanguageTypeScript 6.0 — 100% TypeScript across src/ and open-sse/ (zero any in core since v2.0) FrameworkNext.js 16 + React 19 + Tailwind CSS 4 - Databasebetter-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 176 migrations + Databasebetter-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 177 migrations MemorySQLite FTS5 full-text + int8-quantized vector embeddings, typed decay SchemasZod 4 — MCP tool I/O validation + API contracts ProtocolsMCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE) @@ -1331,7 +1331,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi Resilience GuideCircuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing Auto-Combo Engine16-factor scoring, mode packs, self-healing Proxy Guide3-level proxy system, 1proxy marketplace, registry CRUD - Free TiersConsolidated directory: 34 documented recurring pools / 443 cataloged free-tier entries + Free TiersConsolidated directory: 34 documented recurring pools / 446 cataloged free-tier entries Features GalleryVisual dashboard tour with screenshots Codebase DocumentationBeginner-friendly codebase walkthrough diff --git a/bin/cli/commands/mcp.mjs b/bin/cli/commands/mcp.mjs index c4ce9fbda8..0e740c4ac2 100644 --- a/bin/cli/commands/mcp.mjs +++ b/bin/cli/commands/mcp.mjs @@ -9,6 +9,8 @@ function truncate(v, len = 60) { return s.length > len ? s.slice(0, len - 1) + "…" : s; } +const VALID_MCP_TRANSPORTS = ["stdio", "sse", "streamable-http"]; + const mcpToolSchema = [ { key: "name", header: "Tool", width: 36 }, { @@ -43,6 +45,25 @@ export function registerMcp(program) { if (exitCode !== 0) process.exit(exitCode); }); + mcp + .command("enable") + .description(t("mcp.enable.description")) + .option("--transport ", t("mcp.enable.transport")) + .action(async (opts, cmd) => { + const globalOpts = cmd.parent.optsWithGlobals(); + const exitCode = await runMcpEnableCommand({ ...opts, output: globalOpts.output }); + if (exitCode !== 0) process.exit(exitCode); + }); + + mcp + .command("disable") + .description(t("mcp.disable.description")) + .action(async (opts, cmd) => { + const globalOpts = cmd.parent.optsWithGlobals(); + const exitCode = await runMcpDisableCommand({ ...opts, output: globalOpts.output }); + if (exitCode !== 0) process.exit(exitCode); + }); + // 5.1 — mcp call + mcp scopes mcp .command("call [argsJson]") @@ -61,10 +82,15 @@ export function registerMcp(program) { ? JSON.parse(argsPositional) : {}; - const exitCode = await runMcpCallCommand(tool, args, { - ...opts, - stream: opts.stream, - }, globalOpts); + const exitCode = await runMcpCallCommand( + tool, + args, + { + ...opts, + stream: opts.stream, + }, + globalOpts + ); if (exitCode !== 0) process.exit(exitCode); }); @@ -127,7 +153,9 @@ async function mcpJsonRpcCall(tool, args, { stream = false, globalOpts = {} } = if (!initRes.ok) { const text = await initRes.text().catch(() => ""); - process.stderr.write(`MCP initialize failed: HTTP ${initRes.status}${text ? ` — ${text}` : ""}\n`); + process.stderr.write( + `MCP initialize failed: HTTP ${initRes.status}${text ? ` — ${text}` : ""}\n` + ); return 1; } @@ -227,6 +255,7 @@ export async function runMcpStatusCommand(opts = {}) { }); if (!res.ok) { console.log(t("mcp.stopped")); + console.log(t("mcp.stoppedHint")); return 0; } @@ -240,6 +269,9 @@ export async function runMcpStatusCommand(opts = {}) { const transport = status.transport || "stdio"; const online = status.online ?? status.running; console.log(online ? t("mcp.running", { transport }) : t("mcp.stopped")); + if (!online && status.enabled === false) { + console.log(t("mcp.stoppedHint")); + } if (status.toolsCount !== undefined) console.log(` Tools: ${status.toolsCount}`); if (status.scopes?.length) { console.log(" Scopes:"); @@ -270,10 +302,76 @@ export async function runMcpRestartCommand(opts = {}) { console.log(t("mcp.restarted")); return 0; } - console.error(t("common.error", { message: `HTTP ${res.status}` })); + const body = await res.json().catch(() => null); + const message = body?.error || `HTTP ${res.status}`; + console.error(t("common.error", { message })); return 1; } catch (err) { console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) })); return 1; } } + +export async function runMcpEnableCommand(opts = {}) { + const serverUp = await isServerUp(); + if (!serverUp) { + console.error(t("common.serverOffline")); + return 1; + } + + if (opts.transport && !VALID_MCP_TRANSPORTS.includes(opts.transport)) { + console.error( + t("common.error", { + message: `Invalid transport '${opts.transport}'. Valid: ${VALID_MCP_TRANSPORTS.join(", ")}`, + }) + ); + return 1; + } + + try { + const body = { mcpEnabled: true }; + if (opts.transport) body.mcpTransport = opts.transport; + + const res = await apiFetch("/api/settings", { + method: "PATCH", + body, + retry: false, + acceptNotOk: true, + }); + if (!res.ok) { + console.error(t("common.error", { message: `HTTP ${res.status}` })); + return 1; + } + console.log(t("mcp.enabled")); + return 0; + } catch (err) { + console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) })); + return 1; + } +} + +export async function runMcpDisableCommand(opts = {}) { + const serverUp = await isServerUp(); + if (!serverUp) { + console.error(t("common.serverOffline")); + return 1; + } + + try { + const res = await apiFetch("/api/settings", { + method: "PATCH", + body: { mcpEnabled: false }, + retry: false, + acceptNotOk: true, + }); + if (!res.ok) { + console.error(t("common.error", { message: `HTTP ${res.status}` })); + return 1; + } + console.log(t("mcp.disabled")); + return 0; + } catch (err) { + console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) })); + return 1; + } +} diff --git a/bin/cli/locales/en.json b/bin/cli/locales/en.json index 4b37832ba3..b66e4876dd 100644 --- a/bin/cli/locales/en.json +++ b/bin/cli/locales/en.json @@ -348,6 +348,16 @@ "running": "MCP server running ({transport})", "stopped": "MCP server stopped.", "restarted": "MCP server restarted.", + "stoppedHint": "Run `omniroute mcp enable` to turn it on.", + "enabled": "MCP server enabled.", + "disabled": "MCP server disabled.", + "enable": { + "description": "Enable the MCP server", + "transport": "Transport to use: stdio|sse|streamable-http" + }, + "disable": { + "description": "Disable the MCP server" + }, "call": { "description": "Invoke an MCP tool directly", "args": "JSON arguments object (inline)", diff --git a/bin/cli/locales/zh-CN.json b/bin/cli/locales/zh-CN.json index d3f1ab718b..78012ac950 100644 --- a/bin/cli/locales/zh-CN.json +++ b/bin/cli/locales/zh-CN.json @@ -346,6 +346,16 @@ "running": "MCP 服务器正在运行({transport})", "stopped": "MCP 服务器已停止。", "restarted": "MCP 服务器已重启。", + "stoppedHint": "运行 `omniroute mcp enable` 以启用它。", + "enabled": "MCP 服务器已启用。", + "disabled": "MCP 服务器已禁用。", + "enable": { + "description": "启用 MCP 服务器", + "transport": "要使用的传输方式:stdio|sse|streamable-http" + }, + "disable": { + "description": "禁用 MCP 服务器" + }, "call": { "description": "直接调用 MCP 工具", "args": "JSON 参数对象(内联)", diff --git a/bin/cli/locales/zh-TW.json b/bin/cli/locales/zh-TW.json index f4c9c39e10..79128cf49f 100644 --- a/bin/cli/locales/zh-TW.json +++ b/bin/cli/locales/zh-TW.json @@ -346,6 +346,16 @@ "running": "MCP 伺服器正在執行({transport})", "stopped": "MCP 伺服器已停止。", "restarted": "MCP 伺服器已重啟。", + "stoppedHint": "執行 `omniroute mcp enable` 以啟用它。", + "enabled": "MCP 伺服器已啟用。", + "disabled": "MCP 伺服器已停用。", + "enable": { + "description": "啟用 MCP 伺服器", + "transport": "要使用的傳輸方式:stdio|sse|streamable-http" + }, + "disable": { + "description": "停用 MCP 伺服器" + }, "call": { "description": "直接呼叫 MCP 工具", "args": "JSON 引數物件(內聯)", diff --git a/bin/cli/runtime/processSupervisor.mjs b/bin/cli/runtime/processSupervisor.mjs index 3d7bf39742..3bef5fa08d 100644 --- a/bin/cli/runtime/processSupervisor.mjs +++ b/bin/cli/runtime/processSupervisor.mjs @@ -14,6 +14,7 @@ import { stopProcessGracefully } from "../../../src/shared/platform/windowsProce import { isFatalInstrumentationHookFailure, formatAndroidInstrumentationFailureHint, + isFatalStartupDiagnostic, } from "../utils/ensureAndroidCacheDir.mjs"; const CRASH_LOG_LINES = 50; @@ -55,12 +56,14 @@ export class ServerSupervisor { this.child = null; this.isShuttingDown = false; this.instrumentationFailureHintPrinted = false; + this.fatalStartupDiagnosticPrinted = false; } start() { this.startedAt = Date.now(); this.crashLog = []; this.instrumentationFailureHintPrinted = false; + this.fatalStartupDiagnosticPrinted = false; const showLog = process.env.OMNIROUTE_SHOW_LOG === "1"; // #6321: stdout used to be discarded (`"ignore"`) whenever `--log`/OMNIROUTE_SHOW_LOG @@ -99,6 +102,15 @@ export class ServerSupervisor { ) ); } + // #13314: surface any `[STARTUP] Fatal:`-guarded boot diagnostic + // immediately, even without --log — otherwise it is only buffered and + // reaches the operator on exit/crash, which never happens when the + // HTTP listener still comes up after the fatal failure (every route + // then 500s with zero visible diagnostic anywhere). + if (!this.fatalStartupDiagnosticPrinted && isFatalStartupDiagnostic(text)) { + this.fatalStartupDiagnosticPrinted = true; + process.stderr.write(text.endsWith("\n") ? text : `${text}\n`); + } }; if (this.child.stdout) { diff --git a/bin/cli/tray/icon.ico b/bin/cli/tray/icon.ico new file mode 100644 index 0000000000..f0891cfc79 Binary files /dev/null and b/bin/cli/tray/icon.ico differ diff --git a/bin/cli/tray/icon.png b/bin/cli/tray/icon.png index 4e4abe2b78..508f949e16 100644 Binary files a/bin/cli/tray/icon.png and b/bin/cli/tray/icon.png differ diff --git a/bin/cli/utils/ensureAndroidCacheDir.mjs b/bin/cli/utils/ensureAndroidCacheDir.mjs index 0e3f2d20ec..88fe314dff 100644 --- a/bin/cli/utils/ensureAndroidCacheDir.mjs +++ b/bin/cli/utils/ensureAndroidCacheDir.mjs @@ -105,6 +105,27 @@ export function isFatalInstrumentationHookFailure(text) { return /Unsupported platform:\s*android/i.test(text); } +/** + * Detect any fatal boot-time diagnostic guarded by the `[STARTUP] Fatal:` + * prefix (`src/instrumentation-node.ts::ensureDbReadyForBoot()`, + * `src/instrumentation.ts::register()`, and any future guard using the same + * marker). #13314: in the default `omniroute serve` mode (no `--log`), + * `ServerSupervisor` only buffers stdout/stderr and flushes it to the real + * console on exit/crash/readiness-timeout — so if the HTTP listener still + * comes up after a fatal boot diagnostic was already printed (e.g. the + * better-sqlite3 / node:sqlite driver cascade failing hard), the operator + * sees "OmniRoute is running!" with zero visible diagnostic anywhere, and + * every route 500s. This generalizes the #10028 Android/Termux carve-out to + * every `[STARTUP] Fatal:` guard, not just that one platform-specific string. + * + * @param {string} text + * @returns {boolean} + */ +export function isFatalStartupDiagnostic(text) { + if (!text) return false; + return /^\[STARTUP\] Fatal:/m.test(text); +} + /** * Operator-facing hint when that instrumentation failure shows up in child * output — defense in depth if prep was skipped or a future Next.js probe diff --git a/changelog.d/features/13827-i18n-key-completeness-gate.md b/changelog.d/features/13827-i18n-key-completeness-gate.md new file mode 100644 index 0000000000..f7a405faac --- /dev/null +++ b/changelog.d/features/13827-i18n-key-completeness-gate.md @@ -0,0 +1 @@ +- **feat(i18n):** new blocking gate `i18n:check-keys` (`scripts/i18n/check-key-completeness.mjs`) — every locale catalog must carry exactly the key set of `en.json`, whatever the age of the key; the percentage and new-key gates let batch 1 (#13044) ship 43 keys short and batch 2 (#13660) 10 keys short. The i18n guide now documents the post-merge re-sync and the retranslation flow. (#13827) diff --git a/changelog.d/features/agnes-cn-provider.md b/changelog.d/features/agnes-cn-provider.md new file mode 100644 index 0000000000..56b2e0abe8 --- /dev/null +++ b/changelog.d/features/agnes-cn-provider.md @@ -0,0 +1 @@ +- feat(providers): **Added Agnes AI (China) as `agnes-cn` pointed at `https://api.agnes-ai.cn/v1`. Keys issued for `apihub.agnes-ai.com` stay on the existing `agnes` card. Live `/v1/models` on that host lists `agnes-3.0-flash` (same id as intl); the CN seed matches 2.0/2.5/3.0 and not retired 1.5.** diff --git a/changelog.d/features/xai-oauth-live-models.md b/changelog.d/features/xai-oauth-live-models.md new file mode 100644 index 0000000000..494325183e --- /dev/null +++ b/changelog.d/features/xai-oauth-live-models.md @@ -0,0 +1 @@ +- **feat(providers):** share the existing `api.x.ai/v1/models` discovery config with `xai-oauth` so SuperGrok OAuth connections pick up new Grok ids without a registry seed edit. diff --git a/changelog.d/fixes/12370-responses-function-call-name.md b/changelog.d/fixes/12370-responses-function-call-name.md new file mode 100644 index 0000000000..231a2a78af --- /dev/null +++ b/changelog.d/fixes/12370-responses-function-call-name.md @@ -0,0 +1 @@ +- fix(api): restore the `name` field on non-streaming `/v1/responses` `function_call` output items — a plain (non-namespace) tool call's identity restore was blindly applying the `_toolNameMap` alias-table fallback as a `{namespace, name}` object, silently blanking `name` to `undefined` (dropped entirely by JSON.stringify) and leaving Codex unable to dispatch the call, so it re-narrated its intent in a loop instead (#12370) diff --git a/changelog.d/fixes/12453-conversation-turn-nodes-retention.md b/changelog.d/fixes/12453-conversation-turn-nodes-retention.md new file mode 100644 index 0000000000..be8cc99dfb --- /dev/null +++ b/changelog.d/fixes/12453-conversation-turn-nodes-retention.md @@ -0,0 +1 @@ +- **fix(db):** give `conversation_turn_nodes` its own independent retention knob (`retention.conversationTurnNodes`, default 30 days — matching `callLogs` so upgrading changes nothing until an operator overrides it) instead of sharing `callLogs`, and sweep orphaned `agentic_conversations` after the nodes expire (#12453). diff --git a/changelog.d/fixes/12491-codex-wreq-standalone-runtime.md b/changelog.d/fixes/12491-codex-wreq-standalone-runtime.md new file mode 100644 index 0000000000..461aff2789 --- /dev/null +++ b/changelog.d/fixes/12491-codex-wreq-standalone-runtime.md @@ -0,0 +1 @@ +- **fix(sse):** Codex WebSocket transport (including the app-server) no longer fails to load in the Next.js standalone Docker runtime — the wreq-js loader now resolves its module name dynamically instead of a literal Turbopack could rewrite to an unreachable build-time symlink (#12491) — thanks @marshalfevzi diff --git a/changelog.d/fixes/12692-xai-legacy-function-call.md b/changelog.d/fixes/12692-xai-legacy-function-call.md new file mode 100644 index 0000000000..4ba198f73a --- /dev/null +++ b/changelog.d/fixes/12692-xai-legacy-function-call.md @@ -0,0 +1 @@ +- **fix(providers):** xAI requests no longer silently drop an assistant tool call sent in the legacy OpenAI `function_call` shape (instead of `tool_calls[]`) — the call is now translated into the xAI request the same way modern tool calls are (#12692) — thanks @soroush5 diff --git a/changelog.d/fixes/12700-xai-usage-total.md b/changelog.d/fixes/12700-xai-usage-total.md new file mode 100644 index 0000000000..c7bc861702 --- /dev/null +++ b/changelog.d/fixes/12700-xai-usage-total.md @@ -0,0 +1 @@ +- **fix(providers):** xAI responses no longer report `total_tokens`/`totalTokenCount` as `0` when upstream usage uses the legacy `prompt_tokens`/`completion_tokens` names instead of `input_tokens`/`output_tokens` (#12700) — thanks @soroush5 diff --git a/changelog.d/fixes/12861-direct-fetch-timeout-unhandled-rejection.md b/changelog.d/fixes/12861-direct-fetch-timeout-unhandled-rejection.md new file mode 100644 index 0000000000..3821c75945 --- /dev/null +++ b/changelog.d/fixes/12861-direct-fetch-timeout-unhandled-rejection.md @@ -0,0 +1 @@ +- **fix(resilience):** a recoverable direct-fetch response-start timeout (`DIRECT_RESPONSE_START_TIMEOUT`) could, in a narrow timer/promise-settlement race, escape as an `unhandledRejection` → `uncaughtException` and kill the server process — even though `proxyFetch` already retries this exact condition on a fresh socket. Guarded the timer callback so it can no longer fire against an already-settled attempt, and extended the process-level crash guard (already used by the WS/API-bridge servers) to recognize and swallow this code if it ever escapes anyway. Also installs that same guard in the production server entrypoint (`dist/server-ws.mjs`), which never had it even though the dev server already did ([#12861](https://github.com/diegosouzapw/OmniRoute/issues/12861)) — thanks @insoln diff --git a/changelog.d/fixes/12927-magnific-key-validation-false-negative.md b/changelog.d/fixes/12927-magnific-key-validation-false-negative.md new file mode 100644 index 0000000000..a694de7a66 --- /dev/null +++ b/changelog.d/fixes/12927-magnific-key-validation-false-negative.md @@ -0,0 +1 @@ +- **fix(providers):** correct Magnific API key validation, which reported every valid key as invalid due to a GET probe against a POST-only endpoint (#12927) — thanks @hubo1989 diff --git a/changelog.d/fixes/12958-gitlab-duo-403-entitlement-fallback.md b/changelog.d/fixes/12958-gitlab-duo-403-entitlement-fallback.md new file mode 100644 index 0000000000..5c512c556b --- /dev/null +++ b/changelog.d/fixes/12958-gitlab-duo-403-entitlement-fallback.md @@ -0,0 +1 @@ +- **fix(providers):** GitLab Duo Retest and chat requests now fall back to the public Code Suggestions endpoint for ANY `direct_access` 403 (not only the "direct connections are disabled" tenant-config message), and surface the real upstream error body instead of a generic "Access denied" when both endpoints reject the token (#12958) — thanks @Rahulsharma0810 diff --git a/changelog.d/fixes/12968-anthropic-shim-tiny-probe-empty.md b/changelog.d/fixes/12968-anthropic-shim-tiny-probe-empty.md new file mode 100644 index 0000000000..2f576c8e3d --- /dev/null +++ b/changelog.d/fixes/12968-anthropic-shim-tiny-probe-empty.md @@ -0,0 +1 @@ +- **fix(sse):** stop misclassifying a truncated Anthropic-compatible `max_tokens` probe response (`content:[{type:"text",text:""}]`) as an empty upstream response (#12968) — thanks @pranay-gpt diff --git a/changelog.d/fixes/13000-combo-context-limit-cold-start.md b/changelog.d/fixes/13000-combo-context-limit-cold-start.md new file mode 100644 index 0000000000..c475d32218 --- /dev/null +++ b/changelog.d/fixes/13000-combo-context-limit-cold-start.md @@ -0,0 +1 @@ +- fix(providers): stop `@omniroute/opencode-plugin` combo context limits from downgrading to the raw `Math.min(member)` lower bound after a restart — the static catalog now honors the server-computed `computed_context_length` (mirroring the dynamic hook), and a background refresh with a degraded `/api/combos` response backfills the field from the last-known-good disk snapshot instead of overwriting it (#13000) — thanks @morpheus9393 diff --git a/changelog.d/fixes/13012-cli-mcp-restart-and-enable.md b/changelog.d/fixes/13012-cli-mcp-restart-and-enable.md new file mode 100644 index 0000000000..eb5c5f4a90 --- /dev/null +++ b/changelog.d/fixes/13012-cli-mcp-restart-and-enable.md @@ -0,0 +1 @@ +- **fix(cli):** `omniroute mcp restart` no longer 404s — the missing `POST /api/mcp/restart` route now exists — and new `omniroute mcp enable`/`mcp disable [--transport]` subcommands give the CLI a way to turn the MCP server on without the dashboard ([#13012](https://github.com/diegosouzapw/OmniRoute/issues/13012)) — thanks @ricardusx diff --git a/changelog.d/fixes/13022-desktop-exe-malformed-skill-tool-schemas.md b/changelog.d/fixes/13022-desktop-exe-malformed-skill-tool-schemas.md new file mode 100644 index 0000000000..8765865032 --- /dev/null +++ b/changelog.d/fixes/13022-desktop-exe-malformed-skill-tool-schemas.md @@ -0,0 +1 @@ +- **fix(skills):** repair nested malformed skill-tool schemas (bare property maps, boolean `required: true`) for OpenAI-compatible providers, not just the schema root (#13022) — thanks @ftevxk diff --git a/changelog.d/fixes/13089-combo-live-roundrobin-missing-events.md b/changelog.d/fixes/13089-combo-live-roundrobin-missing-events.md new file mode 100644 index 0000000000..a873d3b894 --- /dev/null +++ b/changelog.d/fixes/13089-combo-live-roundrobin-missing-events.md @@ -0,0 +1 @@ +- **fix(routing):** round-robin combos now show up in Combo Studio's Live dashboard — they were completing successfully but never publishing the attempt/success/failure events the dashboard listens for (#13089) — thanks @adityadwi21 diff --git a/changelog.d/fixes/13122-responses-custom-tool-choice.md b/changelog.d/fixes/13122-responses-custom-tool-choice.md new file mode 100644 index 0000000000..41463f0b0f --- /dev/null +++ b/changelog.d/fixes/13122-responses-custom-tool-choice.md @@ -0,0 +1 @@ +- **fix(sse):** stop rejecting a Responses API `tool_choice.type: "custom"` (e.g. Codex CLI forcing `functions__exec`) with a 400 `unsupported_feature` error (#13122) — thanks @phamtienduceng-eng diff --git a/changelog.d/fixes/13232-zai-web-missing-browser-executable.md b/changelog.d/fixes/13232-zai-web-missing-browser-executable.md new file mode 100644 index 0000000000..52794607eb --- /dev/null +++ b/changelog.d/fixes/13232-zai-web-missing-browser-executable.md @@ -0,0 +1 @@ +- **fix(sse):** classify a missing Playwright Chromium install on the Z.ai web transport as an actionable 503 host/config cooldown instead of a generic 502 that trips the provider circuit breaker (#13232) — thanks @oleksandr1811 diff --git a/changelog.d/fixes/13306-windows-libuv-abort-sqljs-exit.md b/changelog.d/fixes/13306-windows-libuv-abort-sqljs-exit.md new file mode 100644 index 0000000000..9ab3f86b17 --- /dev/null +++ b/changelog.d/fixes/13306-windows-libuv-abort-sqljs-exit.md @@ -0,0 +1 @@ +- **fix(db):** defer `process.exit(0)` on graceful shutdown by one macrotask, avoiding a Windows-only libuv abort when the sql.js fallback driver has a statement in flight (#13306) — thanks @anhtahaylove diff --git a/changelog.d/fixes/13314-supervisor-surfaces-fatal-startup-diagnostic.md b/changelog.d/fixes/13314-supervisor-surfaces-fatal-startup-diagnostic.md new file mode 100644 index 0000000000..b2aa05be52 --- /dev/null +++ b/changelog.d/fixes/13314-supervisor-surfaces-fatal-startup-diagnostic.md @@ -0,0 +1 @@ +- **fix(cli):** `omniroute serve` now surfaces a fatal `[STARTUP] Fatal: ...` boot diagnostic (e.g. a DB driver init failure) to the console immediately, even without `--log`, instead of only when the process later crashes or restarts (#13314) — thanks @Orion1943 diff --git a/changelog.d/fixes/13326-memory-fts-skip-access-updates.md b/changelog.d/fixes/13326-memory-fts-skip-access-updates.md new file mode 100644 index 0000000000..d1564a7c1c --- /dev/null +++ b/changelog.d/fixes/13326-memory-fts-skip-access-updates.md @@ -0,0 +1 @@ +- **fix(memory):** stop FTS5 rewrite on access-count updates; rebuild the index on cleanup so leftover tombstones shrink (#13326). diff --git a/changelog.d/fixes/13364-zed-hosted-haiku-thinking-inflation.md b/changelog.d/fixes/13364-zed-hosted-haiku-thinking-inflation.md new file mode 100644 index 0000000000..9b6ec3a649 --- /dev/null +++ b/changelog.d/fixes/13364-zed-hosted-haiku-thinking-inflation.md @@ -0,0 +1 @@ +- **fix(providers):** stop zed-hosted `claude-haiku-4-5` extended-thinking requests from inflating `max_tokens` past the model's real 64000 output cap (#13364) — thanks @ThiagoMafra-Integrare diff --git a/changelog.d/fixes/13380-gemini-web-system-and-tool-prompt.md b/changelog.d/fixes/13380-gemini-web-system-and-tool-prompt.md new file mode 100644 index 0000000000..8c1db0a08b --- /dev/null +++ b/changelog.d/fixes/13380-gemini-web-system-and-tool-prompt.md @@ -0,0 +1 @@ +- **fix(providers):** gemini-web no longer drops the system instruction on single-turn requests or the tool contract when a client system message is present, and switches to an atomic composer insert so embedded newlines can't submit the message early (#13380) — thanks @formilw diff --git a/changelog.d/fixes/13389-catalog-cache-backoff-reset.md b/changelog.d/fixes/13389-catalog-cache-backoff-reset.md new file mode 100644 index 0000000000..943befe238 --- /dev/null +++ b/changelog.d/fixes/13389-catalog-cache-backoff-reset.md @@ -0,0 +1 @@ +- **fix(db):** stop routine connection-backoff auto-recovery from busting the entire `/v1/models` response cache, which was causing intermittent 75-120s/502 responses on deployments routing many providers (#13389) — thanks @RaviTharuma diff --git a/changelog.d/fixes/13429-lite-redundant-remove-tool-call-id.md b/changelog.d/fixes/13429-lite-redundant-remove-tool-call-id.md new file mode 100644 index 0000000000..78df114e20 --- /dev/null +++ b/changelog.d/fixes/13429-lite-redundant-remove-tool-call-id.md @@ -0,0 +1 @@ +- **fix(compression):** stop lite compression from dropping a `role:"tool"` message when it is byte-identical to the previous message, which orphaned a `tool_call_id` and triggered upstream 400 errors on parallel tool calls (#13429) — thanks @tolgaaksoy diff --git a/changelog.d/fixes/13431-responses-post-keepalive-error-type.md b/changelog.d/fixes/13431-responses-post-keepalive-error-type.md new file mode 100644 index 0000000000..6bf0440598 --- /dev/null +++ b/changelog.d/fixes/13431-responses-post-keepalive-error-type.md @@ -0,0 +1 @@ +- **fix(sse):** frame post-keepalive `/v1/responses` stream errors with a top-level `type` field so Responses clients (Codex) surface the real upstream error instead of reporting "stream disconnected before completion" (#13431) — thanks @andrea-kingautomation diff --git a/changelog.d/fixes/13432-incremental-auto-vacuum-drift.md b/changelog.d/fixes/13432-incremental-auto-vacuum-drift.md new file mode 100644 index 0000000000..15af1af534 --- /dev/null +++ b/changelog.d/fixes/13432-incremental-auto-vacuum-drift.md @@ -0,0 +1 @@ +- **fix(db):** reconcile `auto_vacuum` drift between the configured INCREMENTAL mode and the live SQLite pragma — detected at startup and reconciled out-of-request by the vacuum scheduler, which now also runs a bounded `PRAGMA incremental_vacuum` reclaim instead of an unconditional full `VACUUM` once INCREMENTAL is actually in effect (#13432) — thanks @tolgaaksoy diff --git a/changelog.d/fixes/13452-provider-node-baseurl-ignored.md b/changelog.d/fixes/13452-provider-node-baseurl-ignored.md new file mode 100644 index 0000000000..67209917d1 --- /dev/null +++ b/changelog.d/fixes/13452-provider-node-baseurl-ignored.md @@ -0,0 +1 @@ +- **fix(sse):** stop an unhydrated `openai-compatible-*`/`anthropic-compatible-*` connection from silently routing chat requests (and its stored credential) to the real OpenAI/Anthropic API instead of the operator's configured provider-node endpoint (#13452) — thanks @DenXio101 diff --git a/changelog.d/fixes/13470-background-oauth-refresh-proxy-guard.md b/changelog.d/fixes/13470-background-oauth-refresh-proxy-guard.md new file mode 100644 index 0000000000..cd9821beec --- /dev/null +++ b/changelog.d/fixes/13470-background-oauth-refresh-proxy-guard.md @@ -0,0 +1 @@ +- **fix(resilience):** background OAuth token refresh (proactive health-check sweep and the shared refresh helper behind `refreshAccessToken`/`refreshClaudeOAuthToken`/etc.) now fails closed like the interactive chat path when a connection's assigned proxy pool is entirely dead, instead of silently sending the refresh-token exchange out direct or via a stray `HTTPS_PROXY` (#13470) — thanks @elielsousa-pathbit diff --git a/changelog.d/fixes/13472-responses-cache-creation-tokens.md b/changelog.d/fixes/13472-responses-cache-creation-tokens.md new file mode 100644 index 0000000000..b0f0498bf4 --- /dev/null +++ b/changelog.d/fixes/13472-responses-cache-creation-tokens.md @@ -0,0 +1 @@ +- **fix(sse):** forward Anthropic prompt-cache-creation tokens through the `/v1/responses` usage hop so cache-write counts stop logging as zero (#13472) — thanks @fidelix diff --git a/changelog.d/fixes/13488-pii-sanitizer-splices-openrouter-metadata.md b/changelog.d/fixes/13488-pii-sanitizer-splices-openrouter-metadata.md new file mode 100644 index 0000000000..2b270f4adf --- /dev/null +++ b/changelog.d/fixes/13488-pii-sanitizer-splices-openrouter-metadata.md @@ -0,0 +1 @@ +- **fix(sse):** stop the streaming PII sanitizer from splicing OpenRouter metadata (`provider`, `native_finish_reason`, `reasoning_details[].format`) into the answer text buffer (#13488) — thanks @Xore diff --git a/changelog.d/fixes/13535-windows-tray-icon-contrast.md b/changelog.d/fixes/13535-windows-tray-icon-contrast.md new file mode 100644 index 0000000000..8c4aa21333 --- /dev/null +++ b/changelog.d/fixes/13535-windows-tray-icon-contrast.md @@ -0,0 +1 @@ +- **fix(cli):** redraw the CLI/Electron system tray icon with a dark outline and ship a native multi-res `icon.ico` so it is no longer a pure-white, nearly invisible glyph on the Windows light-theme taskbar and hidden-icons flyout (#13535) — thanks @ProphetOfDoom-PoD diff --git a/changelog.d/fixes/13544-audio-transcription-call-log.md b/changelog.d/fixes/13544-audio-transcription-call-log.md new file mode 100644 index 0000000000..9930a28937 --- /dev/null +++ b/changelog.d/fixes/13544-audio-transcription-call-log.md @@ -0,0 +1 @@ +- **fix(api):** `/v1/audio/transcriptions`, `/v1/audio/translations` and `/v1/audio/speech` requests now show up in Dashboard → Request Logs — the three routes never called the shared call-log pipeline, so every successful (and failed) transcription/translation/speech request was silently dropped from `call_logs` ([#13544](https://github.com/diegosouzapw/OmniRoute/issues/13544)) — thanks @delafu diff --git a/changelog.d/fixes/13558-minimax-m3-reasoning-leak.md b/changelog.d/fixes/13558-minimax-m3-reasoning-leak.md new file mode 100644 index 0000000000..ad254b2256 --- /dev/null +++ b/changelog.d/fixes/13558-minimax-m3-reasoning-leak.md @@ -0,0 +1 @@ +- **fix(providers):** MiniMax-M3's inline `...` reasoning no longer leaks into `message.content`/`delta.content` on the `minimax`/`minimax-cn` routes — it is now stripped and surfaced as `reasoning_content`, in both streaming and non-streaming responses (#13558) — thanks @pan17 diff --git a/changelog.d/fixes/13562-hide-auto-nothink-settings-schema.md b/changelog.d/fixes/13562-hide-auto-nothink-settings-schema.md new file mode 100644 index 0000000000..263184ad5f --- /dev/null +++ b/changelog.d/fixes/13562-hide-auto-nothink-settings-schema.md @@ -0,0 +1 @@ +- **fix(api):** `PATCH /api/settings` now persists `hideAutoCombos` and `hideNoThinkVariants` instead of silently dropping them (#13562) — thanks @texastoland diff --git a/changelog.d/fixes/13591-antigravity-opaque-400-error-surfaced.md b/changelog.d/fixes/13591-antigravity-opaque-400-error-surfaced.md new file mode 100644 index 0000000000..37c11a42fe --- /dev/null +++ b/changelog.d/fixes/13591-antigravity-opaque-400-error-surfaced.md @@ -0,0 +1,3 @@ +- **fix(providers):** Antigravity error responses and logs now surface the real upstream + message (e.g. Gemini field-path rejections) instead of the generic "Antigravity upstream + error (400)" placeholder (#13591) — thanks @afonsoft diff --git a/changelog.d/fixes/13597-calllogs-worker-error-detail.md b/changelog.d/fixes/13597-calllogs-worker-error-detail.md new file mode 100644 index 0000000000..19c50b3118 --- /dev/null +++ b/changelog.d/fixes/13597-calllogs-worker-error-detail.md @@ -0,0 +1 @@ +- **fix(usage):** the call-logs artifact worker's failure warning now includes the underlying error's message/code instead of the generic "detail omitted" — a crashed or non-zero-exit worker was previously undiagnosable in the logs (#13597) — thanks @afonsoft diff --git a/changelog.d/fixes/13599-deepseek-bai-reasoning-content-echo.md b/changelog.d/fixes/13599-deepseek-bai-reasoning-content-echo.md new file mode 100644 index 0000000000..9f7e923912 --- /dev/null +++ b/changelog.d/fixes/13599-deepseek-bai-reasoning-content-echo.md @@ -0,0 +1 @@ +- **fix(providers):** echo back `reasoning_content` on `bai` DeepSeek thinking-mode follow-up turns, fixing the upstream 400 "reasoning_content must be passed back" (#13599) — thanks @afonsoft diff --git a/changelog.d/fixes/13620-combo-reasoning-only-sse-burst.md b/changelog.d/fixes/13620-combo-reasoning-only-sse-burst.md new file mode 100644 index 0000000000..fa54e1cbec --- /dev/null +++ b/changelog.d/fixes/13620-combo-reasoning-only-sse-burst.md @@ -0,0 +1 @@ +- **fix(sse):** stream reasoning deltas from combo targets incrementally instead of buffering them into a single burst, and stop rejecting reasoning-only streams as an empty completion (#13620) — thanks @NaNomicon diff --git a/changelog.d/fixes/13628-grok46-default-effort.md b/changelog.d/fixes/13628-grok46-default-effort.md new file mode 100644 index 0000000000..1ee9f2f810 --- /dev/null +++ b/changelog.d/fixes/13628-grok46-default-effort.md @@ -0,0 +1,2 @@ +- fix(providers): restore grok-4.6/4.5 default reasoning effort so requests without an explicit effort keep reasoning enabled (#13628) +- fix(registry): declare supportedThinkingEfforts on claude-opus-5 and claude-fable-5 across the anthropic/claude/claude-web/ghe-copilot/github registries (#13628) diff --git a/changelog.d/fixes/13652-kiro-tooldocs-repeat-every-turn.md b/changelog.d/fixes/13652-kiro-tooldocs-repeat-every-turn.md new file mode 100644 index 0000000000..9e2ad49c79 --- /dev/null +++ b/changelog.d/fixes/13652-kiro-tooldocs-repeat-every-turn.md @@ -0,0 +1 @@ +- **fix(sse):** Kiro translator no longer re-prepends the full relocated tool-documentation block onto every subsequent turn of a multi-turn conversation; it now stays anchored to the turn that originally carried it. (#13652) — thanks @KelvinKSPS diff --git a/changelog.d/fixes/13679-cdp-proxy-auth-network-isolation.md b/changelog.d/fixes/13679-cdp-proxy-auth-network-isolation.md new file mode 100644 index 0000000000..1612b0e558 --- /dev/null +++ b/changelog.d/fixes/13679-cdp-proxy-auth-network-isolation.md @@ -0,0 +1 @@ +- **fix(docker):** isolate the ChatGPT Web (Codex) CDP proxy sidecar onto its own Compose network, add an opt-in `CDP_PROXY_TOKEN` auth gate to `cdp-proxy.mjs`, and stop the VNC browser-login CDP bridge from starting when no token is configured (#13679) diff --git a/changelog.d/fixes/13679-cloudsync-hmac-fail-open.md b/changelog.d/fixes/13679-cloudsync-hmac-fail-open.md new file mode 100644 index 0000000000..c4b72aae69 --- /dev/null +++ b/changelog.d/fixes/13679-cloudsync-hmac-fail-open.md @@ -0,0 +1 @@ +- **fix(auth):** `verifyCloudSignature()` no longer accepts an unverifiable `X-Cloud-Sig` when `OMNIROUTE_CLOUD_SYNC_SECRET` is unset — a forged/garbage signature is rejected outright, and the new opt-in `OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE=true` flag rejects unsigned Cloud-sync payloads too (default stays legacy pass-through for v3.8.x; the default flips in v3.9) ([#13679](https://github.com/diegosouzapw/OmniRoute/issues/13679)) diff --git a/changelog.d/fixes/13679-deploy-manifest-literal-secrets.md b/changelog.d/fixes/13679-deploy-manifest-literal-secrets.md new file mode 100644 index 0000000000..fdb470929e --- /dev/null +++ b/changelog.d/fixes/13679-deploy-manifest-literal-secrets.md @@ -0,0 +1 @@ +- **fix(security):** removed the copy-pasteable placeholder `JWT_SECRET`/`API_KEY_SECRET`/`INITIAL_PASSWORD` values from the Podman Quadlet deploy manifest, and blocked remote dashboard logins with the well-known default `INITIAL_PASSWORD=CHANGEME` (#13679) diff --git a/changelog.d/fixes/13679-selfloop-bearer-random.md b/changelog.d/fixes/13679-selfloop-bearer-random.md new file mode 100644 index 0000000000..3b024838fd --- /dev/null +++ b/changelog.d/fixes/13679-selfloop-bearer-random.md @@ -0,0 +1 @@ +- **fix(security):** the internal self-loop admission-bypass bearer is now a random per-process secret instead of the checked-in literal `"sk_omniroute"` when no `OMNIROUTE_API_KEY`/`ROUTER_API_KEY` is configured (#13679) diff --git a/changelog.d/fixes/13680-13681-batches-sweep-cap-shared-file.md b/changelog.d/fixes/13680-13681-batches-sweep-cap-shared-file.md new file mode 100644 index 0000000000..79fb1524e9 --- /dev/null +++ b/changelog.d/fixes/13680-13681-batches-sweep-cap-shared-file.md @@ -0,0 +1 @@ +- **fix(db):** `DELETE /v1/batches/delete-completed` now caps the work it does per request and reports `hasMore` so a caller can resume, and the sweep no longer deletes a file that another batch still references (#13680, #13681) diff --git a/changelog.d/fixes/agnes-cn-thinking-effort-tiers.md b/changelog.d/fixes/agnes-cn-thinking-effort-tiers.md new file mode 100644 index 0000000000..7fe9b5ce6d --- /dev/null +++ b/changelog.d/fixes/agnes-cn-thinking-effort-tiers.md @@ -0,0 +1 @@ +- fix(providers): **declare Agnes CN chat models' live `reasoning_effort` vocabulary** so the catalog and sanitizer stop inventing tiers the CN API rejects. Probes on api.agnes-ai.cn (2026-09-14) match the international endpoint: 2.0/2.5 accept `none/low/medium/high/max`, 3.0 also accepts `minimal` and `xhigh`; `off`/`ultra` clamp off the wire and Hermes' default `xhigh` clamps to `max` on 2.x. diff --git a/changelog.d/fixes/gemini-38-think-level.md b/changelog.d/fixes/gemini-38-think-level.md new file mode 100644 index 0000000000..7dcef7435a --- /dev/null +++ b/changelog.d/fixes/gemini-38-think-level.md @@ -0,0 +1 @@ +- **fix(gemini):** send Gemini 3.8 `thinkingLevel` instead of a numeric `thinkingBudget`, and omit `includeThoughts` unless the client asked, so hidden thoughts stop eating `maxOutputTokens` diff --git a/changelog.d/fixes/release-acceptance-shadow.md b/changelog.d/fixes/release-acceptance-shadow.md new file mode 100644 index 0000000000..aeae72889c --- /dev/null +++ b/changelog.d/fixes/release-acceptance-shadow.md @@ -0,0 +1 @@ +- Add a shadow release-acceptance report next to release-green.json. It does not close #12732 and is not a Mergify required check. diff --git a/changelog.d/maintenance/12732-agent-skills-sync-omni-settings.md b/changelog.d/maintenance/12732-agent-skills-sync-omni-settings.md new file mode 100644 index 0000000000..48df1a2aa8 --- /dev/null +++ b/changelog.d/maintenance/12732-agent-skills-sync-omni-settings.md @@ -0,0 +1 @@ +- **chore(skills):** regenerate the `omni-settings` agent skill after the pool egress-observation route landed (#13581), clearing the `check:agent-skills-sync` base-red (#12732) diff --git a/changelog.d/maintenance/12732-pack-artifact-abort-guard.md b/changelog.d/maintenance/12732-pack-artifact-abort-guard.md new file mode 100644 index 0000000000..ac9d1718cb --- /dev/null +++ b/changelog.d/maintenance/12732-pack-artifact-abort-guard.md @@ -0,0 +1 @@ +- **chore(build):** ship `httpClientAbortGuard.mjs` in the published tarball — the #13636 crash guard was a new `server-ws.mjs` import missing from both pack-artifact allowlists (#12732) diff --git a/changelog.d/maintenance/12732-stryker-provider-401-ambiguous.md b/changelog.d/maintenance/12732-stryker-provider-401-ambiguous.md new file mode 100644 index 0000000000..0b8fe56757 --- /dev/null +++ b/changelog.d/maintenance/12732-stryker-provider-401-ambiguous.md @@ -0,0 +1 @@ +- **chore(ci):** register the #13609 ambiguous-401 regression test in the mutation-coverage config, clearing the second `check:agent-skills-sync`/`mutation-test-coverage` base-red (#12732) diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json index 5ffac46924..f1005c77c1 100644 --- a/config/quality/eslint-suppressions.json +++ b/config/quality/eslint-suppressions.json @@ -21,7 +21,7 @@ }, "open-sse/config/providers/registry/claude/index.ts": { "@typescript-eslint/no-unused-vars": { - "count": 7 + "count": 6 } }, "open-sse/config/providers/registry/vertex/index.ts": { diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 78a27ebb4c..782319b6ec 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -2,6 +2,7 @@ "_rebaseline_2026_09_15_13572_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): open-sse/executors/base.ts->1754. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.", "_rebaseline_2026_09_15_13445_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): open-sse/utils/proxyFetch.ts->1296. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.", "_rebaseline_2026_09_15_13643_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): open-sse/executors/codex.ts->1528. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.", + "_rebaseline_2026_09_15_13344_conversation_turn_nodes_retention_field": "PR #13344 rework: src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx 1596->1597 (+1 checker count on the pure tip, matching the existing frozen 1597; this rework's own edit only changes the literal default shown in the retentionFields row from 1 to 30, no line added/removed). The one added line is the PR's own retentionFields row (conversationTurnNodes) exposing the new independent retention.conversationTurnNodes knob (src/types/databaseSettings.ts) added by the same PR. Irreducible: one row per existing retention setting in this table. Covered by tests/unit/db-cleanup-conversation-nodes-12453.test.ts.", "_rebaseline_2026_09_15_13609_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): open-sse/services/accountFallback.ts->2507. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.", "_rebaseline_2026_09_15_13602_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): src/sse/handlers/chatHelpers.ts->1214. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.", "_rebaseline_2026_09_15_13580_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): src/sse/handlers/chatHelpers.ts->1202. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.", @@ -482,7 +483,7 @@ "src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1477, "src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 1271, "src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1607, - "src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1597, + "src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1598, "src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2152, "src/app/api/providers/[id]/models/route.ts": 2432, "src/app/api/providers/[id]/test/route.ts": 1252, @@ -503,6 +504,7 @@ "open-sse/services/autoCombo/virtualFactory.ts": 1230, "open-sse/services/combo/roundRobinCombo.ts": 1213 }, + "_rebaseline_2026_09_15_roundrobin_dashboard_events": "Fix #13089 (Combo Studio Live dashboard shows an empty backlog for round-robin combos): open-sse/services/combo/roundRobinCombo.ts 1205->1213. Round-robin is the only combo strategy that bypasses handleComboChat/executeTargetAttempt.ts, the path that publishes the combo.target.attempt/succeeded/failed EventBus events the Live dashboard listens for — so round-robin completions never showed up. The new call-site wiring (createRRDashboardEvents(...) instantiated once per target, one-line .attempt()/.succeeded()/.failed() calls at the 6 existing dispatch/outcome points) is the emitter logic actually extracted into a new module, open-sse/services/combo/rrDashboardEvents.ts — this is the minimum irreducible footprint for wiring 6 required call sites into 6 fixed control-flow points of the frozen file. Covered by tests/unit/issue-13089-roundrobin-live-ws-events.test.ts (2 tests: success + failure paths).", "_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.", "_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).", "_rebaseline_2026_07_27_v3849_train3": "Merge-train 3 (13 PRs) — owner-approved 2026-07-27. Both entries are genuine irreducible growth at existing chokepoints, not new branches: src/lib/db/apiKeys.ts 1518->1529 (#8805 cx/* ≡ codex/* API-key model permissions); open-sse/handlers/chatCore.ts 5006->5020 (#8806 real response payload into plugin onResponse hooks). Covered by tests/unit/db-apiKeys-crud.test.ts (4 new cases) and the two plugin-hook test files updated in #8806 respectively.", diff --git a/config/quality/release-acceptance.schema.json b/config/quality/release-acceptance.schema.json new file mode 100644 index 0000000000..28c024ca5d --- /dev/null +++ b/config/quality/release-acceptance.schema.json @@ -0,0 +1,183 @@ +{ + "$id": "https://omniroute.local/quality/release-acceptance.schema.json", + "title": "Release acceptance report", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "identity", + "required_gates", + "gates", + "evidence_errors", + "verdict", + "artifact" + ], + "properties": { + "schema_version": { "type": "integer", "const": 1 }, + "identity": { "$ref": "#/$defs/identity" }, + "required_gates": { + "type": "array", + "uniqueItems": true, + "items": { "$ref": "#/$defs/gateInstanceKey" } + }, + "gates": { + "type": "array", + "items": { "$ref": "#/$defs/gateResult" } + }, + "evidence_errors": { + "type": "array", + "items": { "$ref": "#/$defs/evidenceError" } + }, + "verdict": { "enum": ["VERIFIED", "FAILED", "UNVERIFIED"] }, + "artifact": { + "anyOf": [ + { "type": "null" }, + { "$ref": "#/$defs/artifact" } + ] + } + }, + "allOf": [ + { + "if": { "properties": { "verdict": { "const": "VERIFIED" } }, "required": ["verdict"] }, + "then": { "properties": { "required_gates": { "minItems": 1 } } } + } + ], + "$defs": { + "sha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "gateInstanceKey": { + "type": "object", + "additionalProperties": false, + "required": ["gate_id", "suite_id", "shard_index", "shard_total"], + "properties": { + "gate_id": { "type": "string", "minLength": 1 }, + "suite_id": { "type": ["string", "null"] }, + "shard_index": { "type": ["integer", "null"], "minimum": 0 }, + "shard_total": { "type": ["integer", "null"], "minimum": 1 } + } + }, + "identity": { + "type": "object", + "additionalProperties": false, + "required": [ + "repository", + "run_id", + "run_attempt", + "workflow", + "trigger", + "scope", + "requested_ref", + "base_sha", + "candidate_sha", + "tested_sha" + ], + "properties": { + "repository": { "type": "string", "minLength": 1 }, + "run_id": { "type": "string", "minLength": 1 }, + "run_attempt": { "type": "integer", "minimum": 1 }, + "workflow": { "type": "string", "minLength": 1 }, + "trigger": { "type": "string", "minLength": 1 }, + "scope": { "enum": ["pr", "release", "scheduled"] }, + "requested_ref": { "type": "string", "minLength": 1 }, + "base_sha": { "$ref": "#/$defs/sha" }, + "candidate_sha": { "$ref": "#/$defs/sha" }, + "tested_sha": { "$ref": "#/$defs/sha" } + } + }, + "evidenceRef": { + "type": "object", + "additionalProperties": false, + "required": ["artifact_id", "member", "algorithm", "digest"], + "properties": { + "artifact_id": { "type": "string", "minLength": 1 }, + "member": { + "type": "string", + "minLength": 1, + "pattern": "^(?!/)(?!.*(?:^|[/\\\\])\\.\\.(?:[/\\\\]|$))[^\\s]+$" + }, + "algorithm": { "const": "sha256" }, + "digest": { "$ref": "#/$defs/digest" } + } + }, + "artifact": { + "type": "object", + "additionalProperties": false, + "required": ["algorithm", "digest", "identity"], + "properties": { + "algorithm": { "const": "sha256" }, + "digest": { "$ref": "#/$defs/digest" }, + "identity": { "type": "string", "minLength": 1 } + } + }, + "evidenceError": { + "type": "object", + "additionalProperties": false, + "required": ["code", "gate", "detail"], + "properties": { + "code": { "type": "string", "minLength": 1 }, + "gate": { "$ref": "#/$defs/gateInstanceKey" }, + "detail": { "type": "string" } + } + }, + "gateResult": { + "type": "object", + "additionalProperties": false, + "required": [ + "gate_id", + "suite_id", + "shard_index", + "shard_total", + "tested_sha", + "run_id", + "run_attempt", + "command_id", + "gate_type", + "status", + "cause", + "exit_code", + "duration_ms", + "evidence" + ], + "properties": { + "gate_id": { "type": "string", "minLength": 1 }, + "suite_id": { "type": ["string", "null"] }, + "shard_index": { "type": ["integer", "null"], "minimum": 0 }, + "shard_total": { "type": ["integer", "null"], "minimum": 1 }, + "tested_sha": { "$ref": "#/$defs/sha" }, + "run_id": { "type": "string", "minLength": 1 }, + "run_attempt": { "type": "integer", "minimum": 1 }, + "command_id": { "type": "string", "minLength": 1 }, + "gate_type": { "enum": ["static", "test", "artifact"] }, + "status": { "enum": ["PASS", "FAIL", "INFRA_ERROR", "SKIPPED"] }, + "reason": { "type": "string", "minLength": 1 }, + "cause": { + "anyOf": [ + { "type": "null" }, + { "$ref": "#/$defs/gateInstanceKey" } + ] + }, + "exit_code": { "type": ["integer", "null"] }, + "duration_ms": { "type": "integer", "minimum": 0 }, + "evidence": { + "type": "array", + "items": { "$ref": "#/$defs/evidenceRef" } + } + }, + "allOf": [ + { + "if": { + "properties": { "status": { "const": "SKIPPED" } }, + "required": ["status"] + }, + "then": { "required": ["reason"] } + } + ] + } + } +} diff --git a/contrib/podman/README.md b/contrib/podman/README.md index 41ee3651d7..c008077a87 100644 --- a/contrib/podman/README.md +++ b/contrib/podman/README.md @@ -40,7 +40,28 @@ cp contrib/podman/*.network ~/.config/containers/systemd/omniroute/ cp contrib/podman/*.volume ~/.config/containers/systemd/omniroute/ ``` -### 3. Mount the project .env for secrets +### 3. Generate secrets before first start + +`omniroute.container` no longer ships `JWT_SECRET` / `API_KEY_SECRET` / +`INITIAL_PASSWORD` values — earlier versions shipped copy-pasteable +placeholders (`change-me-to-a-random-base64-string`, +`change-me-to-a-random-hex-string`) that an operator could forget to +rotate, leaving the deployment with a public, guessable secret/password +(#13679). Generate real ones and put them in your project `.env`: + +```bash +echo "JWT_SECRET=$(openssl rand -base64 48)" >> .env +echo "API_KEY_SECRET=$(openssl rand -hex 32)" >> .env +echo "INITIAL_PASSWORD=$(openssl rand -hex 24)" >> .env +``` + +If you skip this: `JWT_SECRET`/`API_KEY_SECRET` are auto-generated and +persisted on first boot, and the dashboard requires setup from `localhost` +before it accepts any password — safer than a literal default, but a real +`INITIAL_PASSWORD` is still recommended so a non-interactive first boot has +a known credential to log in with. + +### 4. Mount the project .env for secrets Edit `~/.config/containers/systemd/omniroute/omniroute.container` and uncomment/replace the `EnvironmentFile` line with the absolute path to @@ -54,7 +75,7 @@ Make sure `CONTAINER_HOST=podman` is set in that `.env`. Alternatively, edit the env vars directly in the `.container` file. -### 4. Reload systemd and start +### 5. Reload systemd and start ```bash systemctl --user daemon-reload @@ -62,7 +83,7 @@ systemctl --user start omniroute-redis systemctl --user start omniroute ``` -### 5. Verify +### 6. Verify ```bash systemctl --user status omniroute diff --git a/contrib/podman/omniroute.container b/contrib/podman/omniroute.container index 31d85257e1..9e2f33d982 100644 --- a/contrib/podman/omniroute.container +++ b/contrib/podman/omniroute.container @@ -28,14 +28,23 @@ Environment=DASHBOARD_PORT=20128 Environment=API_PORT=20129 Environment=API_HOST=0.0.0.0 Environment=REDIS_URL=redis://redis:6379 -Environment=JWT_SECRET=change-me-to-a-random-base64-string -Environment=API_KEY_SECRET=change-me-to-a-random-base64-string -Environment=INITIAL_PASSWORD=change-me-to-a-random-hex-string Environment=NODE_ENV=production Environment=REQUIRE_API_KEY=true -# Load additional secrets (API keys, OAuth creds) from the project .env: +# JWT_SECRET, API_KEY_SECRET and INITIAL_PASSWORD are deliberately NOT set here. +# This unit used to ship copy-pasteable "replace-me" placeholder literals — an +# operator who forgot to replace them ran production with a public, guessable +# secret and dashboard password (#13679). Generate real values and load them from +# your project .env before the FIRST start — see "Generate secrets before first +# start" in contrib/podman/README.md — by uncommenting and pointing this at your +# project .env: # EnvironmentFile=%h/code/docker/OmniRoute/.env +# +# If left unset: JWT_SECRET and API_KEY_SECRET are auto-generated and persisted +# on first boot, and the dashboard requires setup from localhost before it +# accepts any password (see managementPassword.ts / apiAuth.ts) — safer than a +# known-literal default either way, but a real INITIAL_PASSWORD is still +# recommended for non-interactive first boots. HealthCmd=node /app/healthcheck.mjs HealthInterval=30s diff --git a/docker-compose.yml b/docker-compose.yml index 831f4ea175..0bf20c4785 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -142,11 +142,24 @@ services: - "${APP_BIND_HOST:-127.0.0.1}:${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}" - "${APP_BIND_HOST:-127.0.0.1}:${API_PORT:-20129}:${API_PORT:-20129}" - "${APP_BIND_HOST:-127.0.0.1}:${LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}" + # SECURITY (#13679): joins BOTH `default` (to keep reaching redis and the + # other sidecars) AND the dedicated `chatgpt-web-codex-net` (the one + # legitimate consumer of the CDP proxy below). + networks: + - default + - chatgpt-web-codex-net profiles: - web # Internal-only Chromium runtime for ChatGPT Web (Codex). No CDP or browser # UI port is published to the host. + # + # SECURITY (#13679): isolated onto its own `chatgpt-web-codex-net` network + # instead of the shared implicit default bridge — its cdp-proxy.mjs + # sidecar republishes Chromium's CDP on 0.0.0.0:9223, and CDP grants full + # control over a live browser session. Without this isolation, any + # compromised sibling container (redis, qdrant, bifrost, cliproxyapi, + # codex-app-server, ...) on the default network could reach it. chatgpt-web-codex-browser: build: context: . @@ -154,8 +167,12 @@ services: image: omniroute:chatgpt-web-codex-browser restart: unless-stopped shm_size: "2gb" + environment: + - CDP_PROXY_TOKEN=${CDP_PROXY_TOKEN:-} volumes: - chatgpt-web-codex-browser-data:/browser-profile + networks: + - chatgpt-web-codex-net profiles: - web @@ -370,7 +387,12 @@ services: # compose network by the omniroute app. healthcheck: test: - ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:1456/readyz',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"] + [ + "CMD", + "node", + "-e", + "require('http').get('http://127.0.0.1:1456/readyz',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))", + ] interval: 30s timeout: 5s retries: 3 @@ -378,6 +400,13 @@ services: profiles: - codex-app-server +networks: + # SECURITY (#13679): dedicated network for the unauthenticated-by-default + # CDP proxy sidecar (docker/chatgpt-web-codex-browser/cdp-proxy.mjs) — + # shared only with omniroute-web, not with redis/qdrant/bifrost/cliproxyapi/ + # codex-app-server or any other sibling on the implicit default network. + chatgpt-web-codex-net: {} + volumes: chatgpt-web-codex-browser-data: name: omniroute-chatgpt-web-codex-browser-data diff --git a/docker/chatgpt-web-codex-browser/cdp-proxy.mjs b/docker/chatgpt-web-codex-browser/cdp-proxy.mjs index a340348803..8615a22678 100644 --- a/docker/chatgpt-web-codex-browser/cdp-proxy.mjs +++ b/docker/chatgpt-web-codex-browser/cdp-proxy.mjs @@ -5,6 +5,31 @@ const listenPort = 9223; const upstreamHost = "127.0.0.1"; const upstreamPort = 9222; +// SECURITY (#13679): this proxy republishes Chromium's loopback CDP onto +// 0.0.0.0:9223 with no auth of its own — CDP grants full control over a +// live browser session (Runtime.evaluate, cookie theft, etc). When the +// operator sets CDP_PROXY_TOKEN, every request/WS-upgrade MUST present it as +// an `X-Omni-Cdp-Token: ` header before a single byte is forwarded +// upstream, mirroring the gate docker/vnc-browser/chromium/cdp-bridge.py +// already has (#12571). Left unset, the proxy keeps its historical +// zero-config behavior — the primary mitigation for the shared-bridge risk +// is docker-compose.yml isolating this service onto its own network so no +// unrelated sibling container can reach it at all. +const TOKEN = process.env.CDP_PROXY_TOKEN || ""; +const TOKEN_HEADER = "x-omni-cdp-token"; + +if (!TOKEN) { + console.error( + "[cdp-proxy] WARNING: running without CDP_PROXY_TOKEN — every request is forwarded " + + "unauthenticated. Set CDP_PROXY_TOKEN to require an X-Omni-Cdp-Token header (#13679)." + ); +} + +function hasValidToken(headers) { + if (!TOKEN) return true; + return headers[TOKEN_HEADER] === TOKEN; +} + function proxyHeaders(headers) { const next = { ...headers, host: `${upstreamHost}:${upstreamPort}` }; delete next.connection; @@ -13,6 +38,11 @@ function proxyHeaders(headers) { } const server = http.createServer((request, response) => { + if (!hasValidToken(request.headers)) { + response.writeHead(403, { "content-type": "application/json" }); + response.end(JSON.stringify({ error: "missing or invalid X-Omni-Cdp-Token" })); + return; + } const upstream = http.request( { host: upstreamHost, @@ -48,6 +78,10 @@ const server = http.createServer((request, response) => { }); server.on("upgrade", (request, socket, head) => { + if (!hasValidToken(request.headers)) { + socket.destroy(); + return; + } const upstream = net.connect(upstreamPort, upstreamHost, () => { const upgradeHeaders = { ...request.headers, @@ -69,4 +103,6 @@ server.on("upgrade", (request, socket, head) => { upstream.on("error", () => socket.destroy()); }); -server.listen(listenPort, "0.0.0.0"); +server.listen(listenPort, "0.0.0.0", () => { + console.error(`[cdp-proxy] listening on 0.0.0.0:${listenPort}`); +}); diff --git a/docker/vnc-browser/chromium/svc-de-run b/docker/vnc-browser/chromium/svc-de-run index 3d2dee7f49..7cc6e40a7e 100644 --- a/docker/vnc-browser/chromium/svc-de-run +++ b/docker/vnc-browser/chromium/svc-de-run @@ -10,7 +10,13 @@ if [[ "${PIXELFLUX_WAYLAND,,}" == "true" ]]; then echo "[svc-de] ${SOCKET_PATH} found launching de" cd $HOME # OmniRoute: bridge Chromium DevTools (127.0.0.1:9222) to 0.0.0.0:9223. - ( sleep 8; python3 /usr/local/bin/cdp-bridge.py >/proc/1/fd/2 2>&1 ) & + # SECURITY (#13679): only start the bridge when CDP_BRIDGE_TOKEN is + # configured — cdp-bridge.py already fails closed for every caller when + # it is unset (#12571), so an unconfigured container gains nothing by + # running an always-listening 0.0.0.0:9223 process anyway. + if [ -n "${CDP_BRIDGE_TOKEN:-}" ]; then + ( sleep 8; python3 /usr/local/bin/cdp-bridge.py >/proc/1/fd/2 2>&1 ) & + fi exec s6-setuidgid abc \ /bin/bash /defaults/startwm_wayland.sh & PID=$! @@ -57,7 +63,13 @@ chmod 777 /tmp/selkies* # run cd $HOME # OmniRoute: bridge Chromium DevTools (127.0.0.1:9222) to 0.0.0.0:9223. -( sleep 8; python3 /usr/local/bin/cdp-bridge.py >/proc/1/fd/2 2>&1 ) & +# SECURITY (#13679): only start the bridge when CDP_BRIDGE_TOKEN is +# configured — cdp-bridge.py already fails closed for every caller when it +# is unset (#12571), so an unconfigured container gains nothing by running +# an always-listening 0.0.0.0:9223 process anyway. +if [ -n "${CDP_BRIDGE_TOKEN:-}" ]; then + ( sleep 8; python3 /usr/local/bin/cdp-bridge.py >/proc/1/fd/2 2>&1 ) & +fi exec s6-setuidgid abc \ /bin/bash /defaults/startwm.sh & PID=$! diff --git a/docs/architecture/QUALITY_GATES.md b/docs/architecture/QUALITY_GATES.md index 2038c437a7..d3f053e28d 100644 --- a/docs/architecture/QUALITY_GATES.md +++ b/docs/architecture/QUALITY_GATES.md @@ -145,6 +145,7 @@ Runs on every PR to `main`. Blocks merge on failure. | `check-ui-keys-coverage` (inline) | UI i18n key coverage is ≥ 65% | Yes | | `check-ui-value-drift` (inline) | A rewritten English **value** leaves no stale translation behind | Yes | | `check-new-key-coverage` (inline) | A **new** English key reaches every locale | Yes | +| `check-key-completeness` (inline) | Every locale carries exactly the key set of `en.json` (absent key = defect, whatever its age; `__MISSING__` counts as present) | Yes | | `check-translation-ratio` | Real-translation ratio per locale (identical-to-English / placeholder / missing leaves outside the allowlist) must not exceed `config/quality/i18n-translation-baseline.json` + slack | **Advisory** | Needs `fetch-depth: 0` — the value-drift gate diffs `en.json` against the merge base. @@ -536,6 +537,21 @@ several "obvious" merges turned out to hide debt and are **not** clean drop-ins. - Supply-chain (provenance, SBOM, Trivy, Scorecard): [`docs/security/SUPPLY_CHAIN.md`](../security/SUPPLY_CHAIN.md) +#### `check-key-completeness` — key-set parity gate + +`scripts/i18n/check-key-completeness.mjs` (`npm run i18n:check-keys`, job `i18n-ui-coverage`). +Compares the leaf key set of every `src/i18n/messages/.json` with `en.json` and fails +on any absent or extra leaf, regardless of when the key was added. `__MISSING__:` placeholders +count as present (their content is the ratio gate's business). It is the absolute complement +of the two diff-based/percentage gates: `check-ui-keys-coverage` enforces an 80 % floor per +locale (43 absent keys out of ~13,000 still read 99.7 %) and `check-new-key-coverage` judges +only the keys a PR adds to `en.json`. A locale batch is generated from the `en.json` of the day +its branch is cut and translates for days while the base keeps adding keys; the batch PR adds no +key itself, so both siblings stayed silent when batch 1 (#13044) landed 43 keys short in nine +locales and batch 2 (#13660) 10 keys short in eight (2026-09-15). Fix a red with +`node scripts/i18n/sync-ui-keys.mjs --locale= --translate-markers`; an `extra` leaf +means the source dropped it — delete it from the locale. `--warn` reports without failing. + #### `check-new-key-coverage` — new-key i18n gate Sibling of `check-ui-value-drift`. That one catches an English value that was **rewritten** diff --git a/docs/diagrams/cli-terminal.svg b/docs/diagrams/cli-terminal.svg index 17f1c784e3..0c32513a80 100644 --- a/docs/diagrams/cli-terminal.svg +++ b/docs/diagrams/cli-terminal.svg @@ -1,4 +1,4 @@ - + Compact animated terminal cycling three real OmniRoute CLI commands with a typewriter effect and a scrolling subcommand ticker; the first frame shows the completed providers-list screen. diff --git a/docs/diagrams/comparison-table.svg b/docs/diagrams/comparison-table.svg index 02c0f45c32..b3c00a917a 100644 --- a/docs/diagrams/comparison-table.svg +++ b/docs/diagrams/comparison-table.svg @@ -1,4 +1,4 @@ - + Static-header comparison table where each capability row fades in top to bottom; the OmniRoute column is highlighted and shows a check or a leading value in every row, while competitors show a mix of checks, partials and crosses. diff --git a/docs/diagrams/free-tier-budget.svg b/docs/diagrams/free-tier-budget.svg index dd0d3d392a..35f92c74e3 100644 --- a/docs/diagrams/free-tier-budget.svg +++ b/docs/diagrams/free-tier-budget.svg @@ -64,7 +64,7 @@ ~1.47B FREE TOKENS / MONTH · STEADY up to ~2.07B in your first month — signup credits - documented free tiers · 34 recurring pools · 443 catalog entries · one endpoint + documented free tiers · 34 recurring pools · 446 catalog entries · one endpoint diff --git a/docs/diagrams/promise-pillars.svg b/docs/diagrams/promise-pillars.svg index 160f13a4b5..2bc7b538df 100644 --- a/docs/diagrams/promise-pillars.svg +++ b/docs/diagrams/promise-pillars.svg @@ -1,4 +1,4 @@ - + Animated promise card: six pillar tiles fade in in reading order, then a soft colored border highlight sweeps from tile to tile in a continuous cycle. @@ -21,7 +21,7 @@ - One endpoint. 358 providers. Never stop building — OmniRoute picks the cheapest one that works. + One endpoint. 359 providers. Never stop building — OmniRoute picks the cheapest one that works. @@ -38,7 +38,7 @@ Never hit limits - Auto-fallback across 358 providers in + Auto-fallback across 359 providers in milliseconds. Quota out? The next provider takes over while a healthy target remains. diff --git a/docs/diagrams/readme-hero.svg b/docs/diagrams/readme-hero.svg index 69cd5a65e0..6002a9485d 100644 --- a/docs/diagrams/readme-hero.svg +++ b/docs/diagrams/readme-hero.svg @@ -1,4 +1,4 @@ - + Animated hero card: a pulse travels the divider line and a compression bar demo repeatedly shrinks a prompt by up to 95 percent; all headline content is static and readable on the first frame. @@ -28,7 +28,7 @@ Never stop coding. - Every AI tool → 358 providers150+ free — through one endpoint. + Every AI tool → 359 providers150+ free — through one endpoint. Claude Code · Codex · Cursor · Cline · Copilot · Antigravity  →  FREE Claude / GPT / Gemini · auto-fallback diff --git a/docs/frameworks/MCP-SERVER.md b/docs/frameworks/MCP-SERVER.md index b0413dfa26..b823e6add4 100644 --- a/docs/frameworks/MCP-SERVER.md +++ b/docs/frameworks/MCP-SERVER.md @@ -25,6 +25,23 @@ Or via the open-sse transport: omniroute --dev # MCP auto-starts on /mcp endpoint ``` +The HTTP transports (`sse` / `streamable-http`, served in-process by the dashboard server) are +off by default and were previously toggleable only from the `/dashboard/mcp` page. As of v3.8.51 +the CLI has parity: + +```bash +omniroute mcp status # enabled/online, transport, tool count +omniroute mcp enable [--transport stdio|sse|streamable-http] +omniroute mcp disable +omniroute mcp restart # resets active sse/streamable-http sessions +``` + +`mcp enable`/`mcp disable` PATCH the same `mcpEnabled` (and optionally `mcpTransport`) setting +the dashboard toggles via `/api/settings`. `mcp restart` calls `POST /api/mcp/restart`: it tears +down active `sse`/`streamable-http` sessions so the next request re-initializes cleanly, returns +`409` if MCP is disabled, and `501` for the `stdio` transport (stdio clients own their own +subprocess — there is no in-process handle to restart). + ## Transports The MCP server exposes three transports, all backed by the same `createMcpServer()` factory: diff --git a/docs/guides/I18N.md b/docs/guides/I18N.md index d518238449..627ff57162 100644 --- a/docs/guides/I18N.md +++ b/docs/guides/I18N.md @@ -211,6 +211,38 @@ npm run i18n:check-ui-coverage && npm run i18n:check-ratio && npm run check:docs adapter and must not be edited by hand. The Google-Translate generator (`generate-multilang.mjs`) is deprecated and is not part of this flow. +## Keeping catalogs complete and retranslating English copies + +Three gates guard the catalogs, and they see different things: + +| Gate | Sees | +| -------------------------------- | ----------------------------------------------------------------------- | +| `npm run i18n:check-ui-coverage` | ≥ 80 % of leaves translated per locale | +| `npm run i18n:check-new-keys` | a key the PR adds to `en.json` reached every locale | +| `npm run i18n:check-keys` | every locale carries exactly the key set of `en.json`, whatever the age | +| `npm run i18n:check-ratio` | share of leaves still identical to English may only fall (ratchet) | + +**After every merge of the base into a locale branch**, re-sync the locales the branch owns — +the base keeps adding keys while a batch translates: + +```bash +node scripts/i18n/sync-ui-keys.mjs --locale=km,kn,ml --translate-markers --batch-size=40 +npm run i18n:check-keys +``` + +**Retranslating verbatim-English leaves** (`--retranslate-identical`) turns every leaf that is +still byte-identical to `en.json` — outside `scripts/i18n/untranslatable-keys.json` — into a +`__MISSING__:` placeholder and translates it in the same run. Before a bulk run, put every key +that a test pins to its English value (product, engine and flag names — e.g. the Vietnamese +sidebar engines in `dashboard-localization-contract.test.ts`, the pt-BR label in +`server-owned-tool-loop-flag.test.ts`) into the allowlist first, then: + +```bash +node scripts/i18n/sync-ui-keys.mjs --locale=es --retranslate-identical --translate-markers --batch-size=40 +npm run i18n:check-ratio:update # tighten the baseline once the locale improved +npm run i18n:check-glossary # zh-CN / zh-TW / ko protected terms +``` + ## Auto-Translation Pipeline ### generate-multilang.mjs (Google Translate) diff --git a/docs/i18n/am/llm.txt b/docs/i18n/am/llm.txt index 39a61f6de3..9bdab5bdcd 100644 --- a/docs/i18n/am/llm.txt +++ b/docs/i18n/am/llm.txt @@ -5,7 +5,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -19,7 +21,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -129,7 +131,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -282,7 +284,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -394,7 +396,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -438,7 +440,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ar/llm.txt b/docs/i18n/ar/llm.txt index 9134543a98..6e6759f480 100644 --- a/docs/i18n/ar/llm.txt +++ b/docs/i18n/ar/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/az/llm.txt b/docs/i18n/az/llm.txt index 0ed9ba4afe..bf65d9d51b 100644 --- a/docs/i18n/az/llm.txt +++ b/docs/i18n/az/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/bg/llm.txt b/docs/i18n/bg/llm.txt index 6ec72e9b07..4c1e9ecfc8 100644 --- a/docs/i18n/bg/llm.txt +++ b/docs/i18n/bg/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/bn/llm.txt b/docs/i18n/bn/llm.txt index 3f682bf3b7..e2059d6d8f 100644 --- a/docs/i18n/bn/llm.txt +++ b/docs/i18n/bn/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/cs/llm.txt b/docs/i18n/cs/llm.txt index 0846e63050..60d7aba92e 100644 --- a/docs/i18n/cs/llm.txt +++ b/docs/i18n/cs/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/da/llm.txt b/docs/i18n/da/llm.txt index 28c6c9004b..48a746343a 100644 --- a/docs/i18n/da/llm.txt +++ b/docs/i18n/da/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/de/llm.txt b/docs/i18n/de/llm.txt index b7c8f543cb..c2cd1923fa 100644 --- a/docs/i18n/de/llm.txt +++ b/docs/i18n/de/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/el/llm.txt b/docs/i18n/el/llm.txt index bb42596d24..cd376de2e9 100644 --- a/docs/i18n/el/llm.txt +++ b/docs/i18n/el/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/es/llm.txt b/docs/i18n/es/llm.txt index 35277266c1..4ba0200685 100644 --- a/docs/i18n/es/llm.txt +++ b/docs/i18n/es/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/et/llm.txt b/docs/i18n/et/llm.txt index 05ea61696e..ada503b32c 100644 --- a/docs/i18n/et/llm.txt +++ b/docs/i18n/et/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/fa/llm.txt b/docs/i18n/fa/llm.txt index 10c736125b..384c16f706 100644 --- a/docs/i18n/fa/llm.txt +++ b/docs/i18n/fa/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/fi/llm.txt b/docs/i18n/fi/llm.txt index b90155fe90..2d453cdac2 100644 --- a/docs/i18n/fi/llm.txt +++ b/docs/i18n/fi/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/fr/llm.txt b/docs/i18n/fr/llm.txt index b4575e8b23..f456f352c4 100644 --- a/docs/i18n/fr/llm.txt +++ b/docs/i18n/fr/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ga/llm.txt b/docs/i18n/ga/llm.txt index 000ec04984..e3ebb9a392 100644 --- a/docs/i18n/ga/llm.txt +++ b/docs/i18n/ga/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/gu/llm.txt b/docs/i18n/gu/llm.txt index 625e9d85e3..7a506dff91 100644 --- a/docs/i18n/gu/llm.txt +++ b/docs/i18n/gu/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ha/llm.txt b/docs/i18n/ha/llm.txt index c1c8f69cb1..c60aae0e24 100644 --- a/docs/i18n/ha/llm.txt +++ b/docs/i18n/ha/llm.txt @@ -5,7 +5,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -19,7 +21,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -129,7 +131,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -282,7 +284,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -394,7 +396,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -438,7 +440,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/he/llm.txt b/docs/i18n/he/llm.txt index e3bbdcd90d..cd38dbf94b 100644 --- a/docs/i18n/he/llm.txt +++ b/docs/i18n/he/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/hi/llm.txt b/docs/i18n/hi/llm.txt index e38bc37c27..4c7839a1a3 100644 --- a/docs/i18n/hi/llm.txt +++ b/docs/i18n/hi/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/hr/llm.txt b/docs/i18n/hr/llm.txt index 8ef00fa5c7..66236d7ba0 100644 --- a/docs/i18n/hr/llm.txt +++ b/docs/i18n/hr/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/hu/llm.txt b/docs/i18n/hu/llm.txt index 106c2b25a7..4800392223 100644 --- a/docs/i18n/hu/llm.txt +++ b/docs/i18n/hu/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/hy/llm.txt b/docs/i18n/hy/llm.txt index 4de463ecd4..13f7677d37 100644 --- a/docs/i18n/hy/llm.txt +++ b/docs/i18n/hy/llm.txt @@ -5,7 +5,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -19,7 +21,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -129,7 +131,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -282,7 +284,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -394,7 +396,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -438,7 +440,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/id/llm.txt b/docs/i18n/id/llm.txt index ccfb8f192d..be6ad7c93f 100644 --- a/docs/i18n/id/llm.txt +++ b/docs/i18n/id/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ig/llm.txt b/docs/i18n/ig/llm.txt index 27c0f08db8..8f64916e86 100644 --- a/docs/i18n/ig/llm.txt +++ b/docs/i18n/ig/llm.txt @@ -5,7 +5,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -19,7 +21,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -129,7 +131,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -282,7 +284,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -394,7 +396,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -438,7 +440,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/it/llm.txt b/docs/i18n/it/llm.txt index 586c70bf9e..510cd00f18 100644 --- a/docs/i18n/it/llm.txt +++ b/docs/i18n/it/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ja/llm.txt b/docs/i18n/ja/llm.txt index 42aa3211aa..32b32b25bd 100644 --- a/docs/i18n/ja/llm.txt +++ b/docs/i18n/ja/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ka/llm.txt b/docs/i18n/ka/llm.txt index b87709baa1..f105d5907a 100644 --- a/docs/i18n/ka/llm.txt +++ b/docs/i18n/ka/llm.txt @@ -5,7 +5,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -19,7 +21,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -129,7 +131,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -282,7 +284,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -394,7 +396,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -438,7 +440,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/km/llm.txt b/docs/i18n/km/llm.txt index c559c1c069..0f879e2ffd 100644 --- a/docs/i18n/km/llm.txt +++ b/docs/i18n/km/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/kn/llm.txt b/docs/i18n/kn/llm.txt index 8b90b5bd75..1ac8537899 100644 --- a/docs/i18n/kn/llm.txt +++ b/docs/i18n/kn/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ko/llm.txt b/docs/i18n/ko/llm.txt index e2c88a3bc8..409df6e5cc 100644 --- a/docs/i18n/ko/llm.txt +++ b/docs/i18n/ko/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/lt/llm.txt b/docs/i18n/lt/llm.txt index 30c84480ef..104ae3dd6c 100644 --- a/docs/i18n/lt/llm.txt +++ b/docs/i18n/lt/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/lv/llm.txt b/docs/i18n/lv/llm.txt index a10e5cfb44..d8268b6266 100644 --- a/docs/i18n/lv/llm.txt +++ b/docs/i18n/lv/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ml/llm.txt b/docs/i18n/ml/llm.txt index c35b9e0c3f..a35d2ca2f9 100644 --- a/docs/i18n/ml/llm.txt +++ b/docs/i18n/ml/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/mr/llm.txt b/docs/i18n/mr/llm.txt index 267d20e389..efa36e5303 100644 --- a/docs/i18n/mr/llm.txt +++ b/docs/i18n/mr/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ms/llm.txt b/docs/i18n/ms/llm.txt index 1cecd2685f..fe6bb92d17 100644 --- a/docs/i18n/ms/llm.txt +++ b/docs/i18n/ms/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/mt/llm.txt b/docs/i18n/mt/llm.txt index 06b08636c9..cd0ed499df 100644 --- a/docs/i18n/mt/llm.txt +++ b/docs/i18n/mt/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/my/llm.txt b/docs/i18n/my/llm.txt index 7f5052c484..d2d02ba74f 100644 --- a/docs/i18n/my/llm.txt +++ b/docs/i18n/my/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ne/llm.txt b/docs/i18n/ne/llm.txt index 7e3c7b9afe..261b3ef46f 100644 --- a/docs/i18n/ne/llm.txt +++ b/docs/i18n/ne/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/nl/llm.txt b/docs/i18n/nl/llm.txt index 0f60572235..f6fe485fe7 100644 --- a/docs/i18n/nl/llm.txt +++ b/docs/i18n/nl/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/no/llm.txt b/docs/i18n/no/llm.txt index d800a7dddc..ca9a17535f 100644 --- a/docs/i18n/no/llm.txt +++ b/docs/i18n/no/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/or/llm.txt b/docs/i18n/or/llm.txt index ec6ac7fce7..675ea3082e 100644 --- a/docs/i18n/or/llm.txt +++ b/docs/i18n/or/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/pa/llm.txt b/docs/i18n/pa/llm.txt index b55f4ae03d..2bcfc6059f 100644 --- a/docs/i18n/pa/llm.txt +++ b/docs/i18n/pa/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/phi/llm.txt b/docs/i18n/phi/llm.txt index 9fc54131b0..a6925d7798 100644 --- a/docs/i18n/phi/llm.txt +++ b/docs/i18n/phi/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/pl/llm.txt b/docs/i18n/pl/llm.txt index e78a340602..e3f8deccda 100644 --- a/docs/i18n/pl/llm.txt +++ b/docs/i18n/pl/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/pt-BR/llm.txt b/docs/i18n/pt-BR/llm.txt index 1bec9af6e6..372785c4d3 100644 --- a/docs/i18n/pt-BR/llm.txt +++ b/docs/i18n/pt-BR/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/pt/llm.txt b/docs/i18n/pt/llm.txt index 31a8ae5a12..273375e8ee 100644 --- a/docs/i18n/pt/llm.txt +++ b/docs/i18n/pt/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ro/llm.txt b/docs/i18n/ro/llm.txt index d723831165..56e8974d73 100644 --- a/docs/i18n/ro/llm.txt +++ b/docs/i18n/ro/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ru/llm.txt b/docs/i18n/ru/llm.txt index ada7cab271..48dba1f1fa 100644 --- a/docs/i18n/ru/llm.txt +++ b/docs/i18n/ru/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/si/llm.txt b/docs/i18n/si/llm.txt index 663f79b27f..7a04f60796 100644 --- a/docs/i18n/si/llm.txt +++ b/docs/i18n/si/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/sk/llm.txt b/docs/i18n/sk/llm.txt index d6c5098f0a..73ec50d28b 100644 --- a/docs/i18n/sk/llm.txt +++ b/docs/i18n/sk/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/sl/llm.txt b/docs/i18n/sl/llm.txt index 7a6e4b2a6d..029ac7d2ba 100644 --- a/docs/i18n/sl/llm.txt +++ b/docs/i18n/sl/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/sr/llm.txt b/docs/i18n/sr/llm.txt index 56bc46640c..07c3ba3d7f 100644 --- a/docs/i18n/sr/llm.txt +++ b/docs/i18n/sr/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/sv/llm.txt b/docs/i18n/sv/llm.txt index 7d4698e370..3845189f5e 100644 --- a/docs/i18n/sv/llm.txt +++ b/docs/i18n/sv/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/sw/llm.txt b/docs/i18n/sw/llm.txt index 7d041ae8e9..73c2638549 100644 --- a/docs/i18n/sw/llm.txt +++ b/docs/i18n/sw/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ta/llm.txt b/docs/i18n/ta/llm.txt index a6c2c2852d..287d0fab90 100644 --- a/docs/i18n/ta/llm.txt +++ b/docs/i18n/ta/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/te/llm.txt b/docs/i18n/te/llm.txt index 4b7315bc31..125ee1999b 100644 --- a/docs/i18n/te/llm.txt +++ b/docs/i18n/te/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/th/llm.txt b/docs/i18n/th/llm.txt index c363c854bb..bed8dd5839 100644 --- a/docs/i18n/th/llm.txt +++ b/docs/i18n/th/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/tr/llm.txt b/docs/i18n/tr/llm.txt index 2cf5ea45dc..b9c22fc034 100644 --- a/docs/i18n/tr/llm.txt +++ b/docs/i18n/tr/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/uk-UA/llm.txt b/docs/i18n/uk-UA/llm.txt index 253b94e9f5..7889af95d5 100644 --- a/docs/i18n/uk-UA/llm.txt +++ b/docs/i18n/uk-UA/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ur/llm.txt b/docs/i18n/ur/llm.txt index 304f6d87fa..7f4ba7fb8f 100644 --- a/docs/i18n/ur/llm.txt +++ b/docs/i18n/ur/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/uz/llm.txt b/docs/i18n/uz/llm.txt index e9217f5d03..02823ff482 100644 --- a/docs/i18n/uz/llm.txt +++ b/docs/i18n/uz/llm.txt @@ -5,7 +5,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -19,7 +21,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -129,7 +131,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -282,7 +284,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -394,7 +396,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -438,7 +440,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/vi/llm.txt b/docs/i18n/vi/llm.txt index 6a7c3c25c7..807c951dee 100644 --- a/docs/i18n/vi/llm.txt +++ b/docs/i18n/vi/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/yo/llm.txt b/docs/i18n/yo/llm.txt index 9956519f16..1c67b47341 100644 --- a/docs/i18n/yo/llm.txt +++ b/docs/i18n/yo/llm.txt @@ -5,7 +5,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -19,7 +21,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -129,7 +131,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -282,7 +284,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -394,7 +396,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -438,7 +440,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/zh-CN/llm.txt b/docs/i18n/zh-CN/llm.txt index 9f130ca59b..37ae904bb9 100644 --- a/docs/i18n/zh-CN/llm.txt +++ b/docs/i18n/zh-CN/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/zh-TW/llm.txt b/docs/i18n/zh-TW/llm.txt index be78d6b4a5..0760bc3386 100644 --- a/docs/i18n/zh-TW/llm.txt +++ b/docs/i18n/zh-TW/llm.txt @@ -4,7 +4,9 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. + + +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +20,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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -128,7 +130,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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -281,7 +283,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -393,7 +395,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +439,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/reference/FEATURE_FLAGS.md b/docs/reference/FEATURE_FLAGS.md index 7c203eae7d..acdd7cdf8f 100644 --- a/docs/reference/FEATURE_FLAGS.md +++ b/docs/reference/FEATURE_FLAGS.md @@ -46,7 +46,7 @@ A boolean flag is considered **enabled** when its effective value is `"true"`, ## Flag Catalog -69 flags across 6 categories. **Default** is the definition default — the value +70 flags across 6 categories. **Default** is the definition default — the value used when neither a DB override nor an environment variable is present. ### Security (10) @@ -94,7 +94,7 @@ used when neither a DB override nor an environment variable is present. | `CAPABILITY_FILTER_ENABLED` | boolean | `false` | Reject requests before dispatch when the target model lacks required capabilities (vision, tools, structured output, context window). Protects direct single-provider requests that bypass the combo-layer compatibility filter. | | `RADAR_ENABLED` | boolean | `false` | Enable the OmniRoute Radar module (catalog feed screens and sync). Off by default; enabling only unlocks the UI — data sync remains a separate opt-in. | -### Runtime (30) +### Runtime (31) | Key | Type | Default | Restart | Description | | ------------------------------------------- | ------- | ------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -128,6 +128,7 @@ used when neither a DB override nor an environment variable is present. | `RETRY_AFTER_PROVENANCE_ENABLED` | boolean | `false` | | On aggregated 429/503 unavailable responses, omit `Retry-After` when no concrete future retry time is known (instead of a synthetic 1s), add `error.retry_after_provenance` (`signal` \| `none`), and let combo drain paths read prose retry hints from JSON and plain-text upstream bodies. The field only appears on responses built by `unavailableResponse()`; other 429/503 bodies are unchanged. | | `PROTECTED_PRIORITY_INFRA_502_ENABLED` | boolean | `false` | | When a `priority` combo target marked fallback-only-on-quota-exhaustion stops the combo for a cause that is provably not quota (provider circuit breaker open, predictive latency skip), answer 502 instead of the quota-looking 503. Lockout, cooldown, unavailable, exhaustion and concurrency-cap stops keep 503. | | `MISTRAL_AMBIGUOUS_401_SOFT_LOCKOUT` | boolean | `false` | | A bare Mistral 401 (`{"detail":"Unauthorized"}`, no explicit auth signal) is identical for a revoked key and for exhausted quota. When on, it cools the connection down instead of parking it as `expired`, at most 3 times per hour per connection; the next one parks it, so a revoked key still converges. Off by default: every bare Mistral 401 parks the connection as before. | +| `XAI_OAUTH_LIVE_MODEL_DISCOVERY` | boolean | `false` | | Fetch the live xAI model catalog for `xai-oauth` connections from `https://api.x.ai/v1/models` using the OAuth bearer token, instead of the frozen static seed. Off by default: `xai-oauth` keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed (unverified whether x.ai accepts an OAuth bearer at this endpoint). | ### CLI (5) @@ -209,7 +210,7 @@ Returns every flag with its effective value, source, and a summary. "requiresRestart": false, "warningLevel": "caution", }, - // ... all 69 flags + // ... all 70 flags ], "summary": { "total": 56, diff --git a/docs/reference/PROVIDER_REFERENCE.md b/docs/reference/PROVIDER_REFERENCE.md index 4820ac3703..2477f0900d 100644 --- a/docs/reference/PROVIDER_REFERENCE.md +++ b/docs/reference/PROVIDER_REFERENCE.md @@ -1,16 +1,16 @@ --- title: "Provider Reference" version: 3.8.51 -lastUpdated: 2026-09-14 +lastUpdated: 2026-09-16 --- # Provider Reference > **Auto-generated** from `src/shared/constants/providers.ts` — do not edit by hand. > Regenerate with: `npm run gen:provider-reference` -> **Last generated:** 2026-09-14 +> **Last generated:** 2026-09-16 -Total providers: **358**. See category breakdown below. +Total providers: **359**. See category breakdown below. ## Categories @@ -118,13 +118,14 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each | `zai-web` | `zw` | Z.ai Web | Web cookie | [link](https://chat.z.ai) | Copy the "token" value from chat.z.ai → DevTools → Application → Local Storage. Do not copy cookies; OmniRoute handles the per-request CAPTCHA through its browser transport. | — | | `zenmux-free` | `zmf` | ZenMux Free (Web) | Web cookie | [link](https://zenmux.ai) | Login at zenmux.ai, then export all cookies using EditThisCookie or Cookie-Editor and paste the full Cookie header string here. Refresh every ~30 days. | — | -## API Key Providers (paid / paid-with-free-credits) (240) +## API Key Providers (paid / paid-with-free-credits) (241) | ID | Alias | Name | Tags | Website | Notes | |----|-------|------|------|---------|-------| | `360ai` | `360ai` | 360 AI | API key | [link](https://ai.360.cn) | Get API key at ai.360.cn | | `agentrouter` | `agentrouter` | AgentRouter | API key, aggregator | [link](https://agentrouter.org) | $200 free credits on signup - multi-model routing gateway | | `agnes` | `agnes` | Agnes AI | API key, video | [link](https://agnes-ai.com) | Get API key at agnes-ai.com | +| `agnes-cn` | `agnescn` | Agnes AI (China) | API key | [link](https://api.agnes-ai.cn) | Get API key from the Agnes CN site. | | `ai21` | `ai21` | AI21 Labs | API key | [link](https://www.ai21.com) | $10 trial credits on signup (valid 3 months), no credit card required | | `aimlapi` | `aiml` | AI/ML API | API key, aggregator | [link](https://aimlapi.com) | Free tier paused (2026) — AI/ML API is now pay-as-you-go only (min $20 top-up); no recurring free credits. | | `ainative` | `ainative` | AINative Studio | API key | [link](https://ainative.studio) | Create a free API key at ainative.studio (no card), then paste it here as a Bearer token. | @@ -446,7 +447,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each - Catalog: [`src/shared/constants/providers.ts`](../../src/shared/constants/providers.ts) - Registry (per-model details): [`open-sse/config/providerRegistry.ts`](../../open-sse/config/providerRegistry.ts) -- Executors: [`open-sse/executors/`](../../open-sse/executors/) (111 implementations) +- Executors: [`open-sse/executors/`](../../open-sse/executors/) (114 implementations) - Translators: [`open-sse/translator/`](../../open-sse/translator/) ## See Also diff --git a/electron/assets/tray-icon.png b/electron/assets/tray-icon.png index 4e4abe2b78..508f949e16 100644 Binary files a/electron/assets/tray-icon.png and b/electron/assets/tray-icon.png differ diff --git a/llm.txt b/llm.txt index 4d7f1d1f63..402579e8cd 100644 --- a/llm.txt +++ b/llm.txt @@ -1,6 +1,6 @@ # OmniRoute -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 358 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 359 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 176 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 177 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 @@ -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/ # 176 versioned SQL migration files +│ │ │ └── migrations/ # 177 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -277,7 +277,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **358 AI providers** with automatic format translation +- **359 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -389,7 +389,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 176 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 177 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -433,7 +433,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 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 (122 domain-specific files, 176 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 (122 domain-specific files, 177 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: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/open-sse/config/freeModelCatalog.data.ts b/open-sse/config/freeModelCatalog.data.ts index 3d7ebed595..1a51519f63 100644 --- a/open-sse/config/freeModelCatalog.data.ts +++ b/open-sse/config/freeModelCatalog.data.ts @@ -22,7 +22,7 @@ import type { FreeModelBudget } from "./freeModelCatalog.ts"; * rewrites file timestamps on every deploy, which would report a months-old * catalog as "updated today". Bump this whenever the entries below change. */ -export const FREE_CATALOG_CURATED_AT = "2026-09-09"; +export const FREE_CATALOG_CURATED_AT = "2026-09-12"; export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [ { provider: "agentrouter", modelId: "claude-opus-4-8", displayName: "Claude Opus 4.8", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" }, @@ -462,6 +462,9 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [ { provider: "agnes", modelId: "agnes-2.0-flash", displayName: "Agnes 2.0 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "agnes-free", tos: "ok" }, { provider: "agnes", modelId: "agnes-2.5-flash", displayName: "Agnes 2.5 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "agnes-free", tos: "ok" }, { provider: "agnes", modelId: "agnes-3.0-flash", displayName: "Agnes 3.0 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "agnes-free", tos: "ok" }, + { provider: "agnes-cn", modelId: "agnes-2.0-flash", displayName: "Agnes 2.0 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "agnes-cn-free", tos: "ok" }, + { provider: "agnes-cn", modelId: "agnes-2.5-flash", displayName: "Agnes 2.5 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "agnes-cn-free", tos: "ok" }, + { provider: "agnes-cn", modelId: "agnes-3.0-flash", displayName: "Agnes 3.0 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "agnes-cn-free", tos: "ok" }, { provider: "glm", modelId: "glm-4.7-flash", displayName: "GLM-4.7-Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "zhipu-flash-free", tos: "ok" }, { provider: "glm", modelId: "glm-4.5-flash", displayName: "GLM-4.5-Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "zhipu-flash-free", tos: "ok" }, { provider: "navy", modelId: "shared-pool", displayName: "NavyAI free pool (150K tokens/day, shared)", monthlyTokens: 4500000, creditTokens: 0, freeType: "recurring-daily", poolKey: "navy-free", tos: "ok" }, diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index df443a6f7b..64c6416b24 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -319,3 +319,24 @@ export function getClaudeCodeDefaultModels(): { haiku: find(/haiku/i), }; } + +/** + * #13452: shared guard for `*-compatible-*` executors' `buildUrl()`. + * `BaseExecutor`/`DefaultExecutor` used to default an `openai-compatible-*` + * / `anthropic-compatible-*` connection to the real OpenAI/Anthropic API + * when `credentials.providerSpecificData.baseUrl` was absent — silently + * shipping the connection's own stored "API key" as a Bearer/x-api-key + * token to a public third party instead of the operator's intended + * local/self-hosted endpoint. `provider` is embedded in the thrown error + * only for operator debuggability — it is never sent upstream. + */ +export function requireCompatibleBaseUrl( + provider: string | null | undefined, + providerSpecificData: { baseUrl?: unknown } | null | undefined +): string { + const baseUrl = providerSpecificData?.baseUrl; + if (typeof baseUrl === "string" && baseUrl) return baseUrl; + throw new Error( + `provider node "${provider}" has no baseUrl — node missing or connection not hydrated` + ); +} diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index fa9ba3b069..1f5fe8898d 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -23,6 +23,7 @@ import { llamagateProvider } from "./registry/llamagate/index.ts"; import { glmProvider } from "./registry/glm/index.ts"; import { glmtProvider } from "./registry/glm/t/index.ts"; import { glm_cnProvider } from "./registry/glm/cn/index.ts"; +import { agnes_cnProvider } from "./registry/agnes/cn/index.ts"; import { traeProvider } from "./registry/trae/index.ts"; import { muse_spark_webProvider } from "./registry/muse-spark-web/index.ts"; import { lmarenaProvider } from "./registry/lmarena/index.ts"; @@ -414,6 +415,7 @@ export const REGISTRY: Record = { deepinfra: deepinfraProvider, agy: agyProvider, agnes: agnesProvider, + "agnes-cn": agnes_cnProvider, aihorde: aihordeProvider, ainative: ainativeProvider, aion: aionProvider, diff --git a/open-sse/config/providers/registry/agnes/cn/index.ts b/open-sse/config/providers/registry/agnes/cn/index.ts new file mode 100644 index 0000000000..eceeed6135 --- /dev/null +++ b/open-sse/config/providers/registry/agnes/cn/index.ts @@ -0,0 +1,63 @@ +import type { RegistryEntry } from "../../../shared.ts"; + +// Official Agnes chat effort vocabulary from live /v1/chat/completions probes +// (2026-09-14, api.agnes-ai.cn and apihub.agnes-ai.com behave identically). +// 2.0/2.5 accept none/low/medium/high/max and 400 on xhigh/minimal/off/ultra. +// 3.0 additionally accepts minimal and xhigh. +export const AGNES_CN_FLASH_THINKING_EFFORTS = ["none", "low", "medium", "high", "max"] as const; +export const AGNES_CN_30_THINKING_EFFORTS = [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max", +] as const; + +export const agnes_cnProvider: RegistryEntry = { + id: "agnes-cn", + alias: "agnescn", + format: "openai", + executor: "default", + baseUrl: "https://api.agnes-ai.cn/v1/chat/completions", + modelsUrl: "https://api.agnes-ai.cn/v1/models", + authType: "apikey", + authHeader: "bearer", + passthroughModels: true, + liveCatalogAuthoritative: true, + models: [ + { + id: "agnes-2.0-flash", + name: "Agnes 2.0 Flash", + contextLength: 262144, + maxOutputTokens: 65536, + supportsReasoning: true, + supportedThinkingEfforts: [...AGNES_CN_FLASH_THINKING_EFFORTS], + supportsVision: true, + toolCalling: true, + }, + { + id: "agnes-2.5-flash", + name: "Agnes 2.5 Flash", + contextLength: 524288, + maxOutputTokens: 65536, + supportsReasoning: true, + supportedThinkingEfforts: [...AGNES_CN_FLASH_THINKING_EFFORTS], + supportsVision: true, + toolCalling: true, + interleavedField: "reasoning_content", + }, + { + id: "agnes-3.0-flash", + name: "Agnes 3.0 Flash", + contextLength: 524288, + maxOutputTokens: 65536, + supportsReasoning: true, + supportedThinkingEfforts: [...AGNES_CN_30_THINKING_EFFORTS], + supportsVision: true, + toolCalling: true, + interleavedField: "reasoning_content", + }, + ], +}; diff --git a/open-sse/config/providers/registry/bai/index.ts b/open-sse/config/providers/registry/bai/index.ts index 574362693c..6423f1c2ec 100644 --- a/open-sse/config/providers/registry/bai/index.ts +++ b/open-sse/config/providers/registry/bai/index.ts @@ -11,4 +11,7 @@ export const baiProvider: RegistryEntry = { modelsUrl: "https://api.b.ai/v1/models", models: [], passthroughModels: true, + // #13599: bai resells DeepSeek's `deepseek-reasoner` thinking-mode models, which 400 + // when a prior assistant turn is missing `reasoning_content` on a follow-up request. + requiresReasoningContentEcho: true, }; diff --git a/open-sse/config/providers/registry/grok-cli/index.ts b/open-sse/config/providers/registry/grok-cli/index.ts index e65ced0e76..ffdcc7cd62 100644 --- a/open-sse/config/providers/registry/grok-cli/index.ts +++ b/open-sse/config/providers/registry/grok-cli/index.ts @@ -26,6 +26,7 @@ export const grok_cliProvider: RegistryEntry = { name: "Grok 4.6", contextLength: 500000, supportsReasoning: true, + supportedThinkingEfforts: ["low", "medium", "high"], toolCalling: true, targetFormat: "openai-responses", unsupportedParams: ["presencePenalty", "frequencyPenalty", "logprobs", "topLogprobs"], @@ -35,6 +36,7 @@ export const grok_cliProvider: RegistryEntry = { name: "Grok 4.5", contextLength: 500000, supportsReasoning: true, + supportedThinkingEfforts: ["low", "medium", "high"], toolCalling: true, targetFormat: "openai-responses", unsupportedParams: ["presencePenalty", "frequencyPenalty", "logprobs", "topLogprobs"], diff --git a/open-sse/config/providers/shared.ts b/open-sse/config/providers/shared.ts index 250d631775..8007b12627 100644 --- a/open-sse/config/providers/shared.ts +++ b/open-sse/config/providers/shared.ts @@ -144,6 +144,16 @@ export interface RegistryEntry { responsesBaseUrl?: string; /** Provider-bound replay format; omitted providers accept portable plaintext reasoning. */ reasoningTransport?: ReasoningTransport; + /** + * Thinking-mode upstreams proxied by this provider require the assistant's + * prior-turn `reasoning_content` to be echoed back on every follow-up request + * (e.g. DeepSeek-reselling gateways such as `bai`). Standard OpenAI-shaped + * clients do not preserve that field when replaying history, so when this is + * `true`, DefaultExecutor injects a placeholder via + * `open-sse/utils/reasoningContentInjector.ts` for model ids matching + * `isThinkingMessageModel()`. See issue #13599. + */ + requiresReasoningContentEcho?: boolean; /** Anthropic-native /v1/messages endpoint (e.g. GitHub Copilot's shim) used * for models tagged `targetFormat: "claude"` on an otherwise openai-format * provider — see registry/github/index.ts. */ diff --git a/open-sse/executors/antigravityUpstreamError.ts b/open-sse/executors/antigravityUpstreamError.ts index 074824ef17..2807baca9f 100644 --- a/open-sse/executors/antigravityUpstreamError.ts +++ b/open-sse/executors/antigravityUpstreamError.ts @@ -21,6 +21,20 @@ const GEO_BLOCKED_HINT = "call the model API. Route antigravity/agy egress through a proxy in a " + "supported region (e.g. US/EU) or use a different provider."; +/** + * Extract the real upstream error message (e.g. Google's Gemini-dialect field-path + * rejection) from a parsed Antigravity `upstream_details`-shaped body, so callers can + * surface it directly in `error.message` instead of only nesting it under + * `upstream_details` — the generic `parseUpstreamError()` re-parser used by the shared + * chatCore failure path only reads the outer `error.message` (#13591). + */ +function extractUpstreamMessage(details: unknown): string | null { + if (!details || typeof details !== "object") return null; + const err = (details as { error?: { message?: unknown } }).error; + const msg = err && typeof err.message === "string" ? err.message : null; + return msg && msg.trim() ? msg.trim() : null; +} + export function buildAntigravityUpstreamError(status: number, statusText: string, rawBody: string) { let upstreamDetails: unknown; try { @@ -36,5 +50,9 @@ export function buildAntigravityUpstreamError(status: number, statusText: string upstreamDetails ); } - return buildErrorBody(status, `Antigravity upstream error (${status})${suffix}`, upstreamDetails); + const upstreamMessage = extractUpstreamMessage(upstreamDetails); + const message = upstreamMessage + ? `Antigravity upstream error (${status}): ${upstreamMessage}` + : `Antigravity upstream error (${status})${suffix}`; + return buildErrorBody(status, message, upstreamDetails); } diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index 4b559d396e..9514610b2a 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -1,5 +1,5 @@ import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.ts"; -import { getRegistryEntry } from "../config/providerRegistry.ts"; +import { getRegistryEntry, requireCompatibleBaseUrl } from "../config/providerRegistry.ts"; import { resolveFetchStartTimeout } from "../utils/fetchStartTimeoutPolicy.ts"; import { resolveAlternateFormat, @@ -30,7 +30,7 @@ import { addParamToBlocklist, isAutoLearnGloballyEnabled, } from "@/lib/db/paramFilters"; -import { applyFingerprint, isCliCompatEnabled, stripInternalBodyFields } from "../config/cliFingerprints.ts"; +import { applyFingerprint, isCliCompatEnabled, stripInternalBodyFields } from "../config/cliFingerprints.ts"; // prettier-ignore import { supportsClaudeMaxEffort, supportsXHighEffort } from "../config/providerModels.ts"; import { getThinkingBudgetConfig, ThinkingMode } from "../services/thinkingBudget.ts"; import { @@ -380,7 +380,7 @@ export class BaseExecutor { void stream; if (this.provider?.startsWith?.("openai-compatible-")) { const psd = credentials?.providerSpecificData; - const baseUrl = typeof psd?.baseUrl === "string" ? psd.baseUrl : "https://api.openai.com/v1"; + const baseUrl = requireCompatibleBaseUrl(this.provider, psd); // #13452 const normalized = baseUrl.replace(/\/$/, ""); // Sanitize custom path: must start with '/', no path traversal, no null bytes const rawPath = typeof psd?.chatPath === "string" && psd.chatPath ? psd.chatPath : null; diff --git a/open-sse/executors/browserExecutableCheck.ts b/open-sse/executors/browserExecutableCheck.ts new file mode 100644 index 0000000000..fadd5b5a0d --- /dev/null +++ b/open-sse/executors/browserExecutableCheck.ts @@ -0,0 +1,18 @@ +/** + * Shared classification for browser-backed executors: distinguishes a missing Playwright + * Chromium binary (`chromium.launch: Executable doesn't exist at ...`) from a transient upstream + * fault. This is a host/config problem, not something a retry loop can fix, so executors must + * NOT surface it as a plain retryable 5xx (which marks the account unavailable / trips the + * provider circuit breaker). Originally added for `gemini-web.ts` (#3516); extracted here so + * every browser-backed executor (Gemini Web, Z.ai Web, ...) can share the same detection. + */ +export function isMissingBrowserExecutable(message: string): boolean { + if (!message) return false; + const lower = message.toLowerCase(); + return ( + lower.includes("executable doesn't exist") || + lower.includes("executablenotfound") || + lower.includes("playwright install") || + (lower.includes("chromium") && lower.includes("download")) + ); +} diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index 8e478cb833..c74cc00bff 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -43,6 +43,7 @@ import { errorResponse } from "../utils/error.ts"; import { normalizeCodexResponsesInput } from "../utils/responsesInputNormalization.ts"; import * as prl from "../utils/providerRequestLogging.ts"; import { createRequire } from "module"; +import { loadDynamicModule } from "./codex/wreqLoader.ts"; // Quota parsing/scheduling extracted to a pure leaf; re-exported for the // Codex account module and tests. export { @@ -90,7 +91,7 @@ function getCodexWebSocketTransport(): WebsocketFn | null { if (_wreqChecked) return _websocketFn; _wreqChecked = true; try { - const mod = _wreqRequire("wreq-js") as { websocket?: WebsocketFn }; + const mod = loadDynamicModule(_wreqRequire, "wreq-js") as { websocket?: WebsocketFn }; _websocketFn = typeof mod.websocket === "function" ? mod.websocket : null; } catch { console.warn("[codex] wreq-js import failed, websocket disabled"); diff --git a/open-sse/executors/codex/wreqLoader.ts b/open-sse/executors/codex/wreqLoader.ts new file mode 100644 index 0000000000..61cbc0b518 --- /dev/null +++ b/open-sse/executors/codex/wreqLoader.ts @@ -0,0 +1,9 @@ +// #12491 — keep the module-name argument dynamic (never a literal string) +// when calling a createRequire()-returned function. Turbopack statically +// detects a literal specifier and rewrites the call to a hashed require() +// target that resolves only via a `.next`-relative symlink generated at +// build time, which is absent from the standalone Docker runtime. Mirrors +// open-sse/utils/tlsClient.ts's loadRuntimeModule(). +export function loadDynamicModule(requireFn: NodeRequire, moduleName: string): unknown { + return Reflect.apply(requireFn, undefined, [moduleName]); +} diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index c267904d9f..39c92cc5e4 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -11,7 +11,7 @@ import { joinClaudeCodeCompatibleUrl, } from "../services/claudeCodeCompatible.ts"; import { getGigachatAccessToken } from "../services/gigachatAuth.ts"; -import { getRegistryEntry } from "../config/providerRegistry.ts"; +import { getRegistryEntry, requireCompatibleBaseUrl } from "../config/providerRegistry.ts"; import { getModelTargetFormat } from "../config/providerModels.ts"; import { mergeClientAnthropicBeta, @@ -231,7 +231,7 @@ export class DefaultExecutor extends BaseExecutor { void urlIndex; if (this.provider?.startsWith?.("openai-compatible-")) { const psd = credentials?.providerSpecificData; - const baseUrl = psd?.baseUrl || "https://api.openai.com/v1"; + const baseUrl = requireCompatibleBaseUrl(this.provider, psd); // #13452 const normalized = baseUrl.replace(/\/$/, ""); const customPath = typeof psd?.chatPath === "string" && psd.chatPath ? psd.chatPath : null; if (customPath) return `${normalized}${customPath}`; @@ -244,7 +244,7 @@ export class DefaultExecutor extends BaseExecutor { } if (this.provider?.startsWith?.("anthropic-compatible-")) { const psd = credentials?.providerSpecificData; - const baseUrl = psd?.baseUrl || "https://api.anthropic.com/v1"; + const baseUrl = requireCompatibleBaseUrl(this.provider, psd); // #13452 const customPath = typeof psd?.chatPath === "string" && psd.chatPath ? psd.chatPath : null; if (isClaudeCodeCompatible(this.provider)) { return joinClaudeCodeCompatibleUrl( @@ -1013,18 +1013,19 @@ export class DefaultExecutor extends BaseExecutor { this.ensureThinkingBudget(withDefaults as Record, model); } - // 9router#1480: native Moonshot providers 400 when a prior assistant turn - // lacks reasoning_content. OpencodeExecutor - // already injects a placeholder for OpenCode-routed thinking models; the - // direct connections hit neither injection path. Scope to Moonshot ids so - // gateway-served models that merely match the thinking-model name pattern - // (and may reject an extra field) are unaffected. - if (this.provider === "kimi" || this.provider === "moonshot") { + // 9router#1480: native Moonshot providers 400 when a prior assistant turn lacks + // reasoning_content. Scope to Moonshot ids, or a registry entry opting in via + // `requiresReasoningContentEcho` (e.g. `bai`'s DeepSeek resale, #13599). + const reasoningEcho = + this.provider === "kimi" || + this.provider === "moonshot" || + !!getRegistryEntry(this.provider)?.requiresReasoningContentEcho; + if (reasoningEcho) { const outboundModel = typeof (withDefaults as Record)?.model === "string" ? ((withDefaults as Record).model as string) : model; - if (shouldInjectReasoningContentPlaceholder(this.provider, outboundModel)) { + if (shouldInjectReasoningContentPlaceholder(reasoningEcho, this.provider, outboundModel)) { withDefaults = injectReasoningContentForThinkingModel(withDefaults); } } diff --git a/open-sse/executors/gemini-web.ts b/open-sse/executors/gemini-web.ts index b08f84bbee..a55d8bb331 100644 --- a/open-sse/executors/gemini-web.ts +++ b/open-sse/executors/gemini-web.ts @@ -15,6 +15,7 @@ import { BaseExecutor, type ExecuteInput } from "./base.ts"; import { buildErrorBody, sanitizeErrorMessage } from "../utils/error.ts"; +import { isMissingBrowserExecutable } from "./browserExecutableCheck.ts"; import { normalizeGeminiCookieInput } from "../utils/geminiCookies.ts"; import { prepareToolMessages } from "../translator/webTools.ts"; import { buildToolModeResponse } from "./chatgptWebTools.ts"; @@ -27,22 +28,12 @@ import { const GEMINI_URL = "https://gemini.google.com/app"; -/** - * Whether an error came from Playwright failing to launch because the browser binary is not - * installed (`chromium.launch: Executable doesn't exist at ...`). This is a host/config - * problem, not a transient upstream fault, so the executor must NOT surface it as a retryable - * 500 (which marks the account unavailable and loops / trips the provider breaker). See #3516. - */ -export function isMissingBrowserExecutable(message: string): boolean { - if (!message) return false; - const lower = message.toLowerCase(); - return ( - lower.includes("executable doesn't exist") || - lower.includes("executablenotfound") || - lower.includes("playwright install") || - (lower.includes("chromium") && lower.includes("download")) - ); -} +// Re-exported for backward compatibility: some tests/callers import this classification helper +// from gemini-web.ts, its original home (#3516). The implementation now lives in +// browserExecutableCheck.ts so other browser-backed executors (e.g. zai-web.ts, #13232) can +// share it without importing this whole executor module. +export { isMissingBrowserExecutable } from "./browserExecutableCheck.ts"; + const GEMINI_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"; @@ -96,9 +87,13 @@ function formatStreamChunk(content: string, model: string, finishReason: string * flatten the full history into one prompt so the web UI still sees the * conversation. * - * Single-turn requests are preserved byte-for-byte (only the final user message - * is returned) — the regression guard for the pre-existing no-tools path. - * Multi-turn requests emit a labeled transcript: + * Single-turn requests with NO system message are preserved byte-for-byte + * (only the final user message is returned) — the regression guard for the + * pre-existing no-tools path. A single-turn request that DOES carry a system + * message prepends it the same way the multi-turn branch does (#13380 — the + * old fast path silently dropped the system instruction whenever there was + * no prior user/assistant turn, e.g. title generation or a one-shot chat + * completion). Multi-turn requests emit a labeled transcript: * * System: * @@ -125,16 +120,19 @@ export function buildGeminiPrompt(messages: Array<{ role: string; content: unkno (m, i) => i < lastUserIdx && (m.role === "user" || m.role === "assistant") ); - // Single-turn (no earlier user/assistant turns): byte-for-byte the original - // single-message derivation. Do NOT prepend system text here — the old - // no-tools path ignored a system-only prefix on the first turn. - if (priorTurns.length === 0) return lastUserContent; - const systemText = textMessages .filter((m) => m.role === "system") .map((m) => m.content) .join("\n\n"); + // Single-turn (no earlier user/assistant turns) with no system message: + // byte-for-byte the original single-message derivation. + if (priorTurns.length === 0 && !systemText) return lastUserContent; + + // Single-turn with a system message (#13380): prepend it instead of + // silently dropping it. + if (priorTurns.length === 0) return `System:\n${systemText}\n\n${lastUserContent}`; + const historyLines = priorTurns.map( (m) => `${m.role === "assistant" ? "Assistant" : "User"}: ${m.content}` ); @@ -148,18 +146,29 @@ export function buildGeminiPrompt(messages: Array<{ role: string; content: unkno /** * Build the plain-text prompt typed into the Gemini web UI when a tool - * contract is active — the synthetic system message injected by - * `prepareToolMessages()` prepended to the last user message. gemini-web - * only ever sends a single flat string (no native message array), so the - * tool contract and the user's ask are concatenated (#7286). + * contract is active — every system message (the client's own instruction(s) + * plus the synthetic tool contract that `prepareToolMessages()` appends last) + * prepended, in order, to the last user message. gemini-web only ever sends + * a single flat string (no native message array), so the tool contract and + * the user's ask are concatenated (#7286). + * + * `prepareToolMessages()` (open-sse/translator/webTools.ts) pushes the + * synthetic tool contract as the LAST system message so it never buries a + * long client system prompt. Picking only the FIRST system message + * (`.find()`) therefore dropped the tool contract whenever the client + * already sent its own system message — #13380. Joining ALL system messages + * in order keeps the client instruction(s) first and the tool contract last, + * matching that dual-placement design intent. */ export function buildGeminiToolPrompt( effectiveMessages: Array<{ role: string; content: unknown }> ): string { - const toolSystemMsg = effectiveMessages.find((m) => m.role === "system"); + const toolPrompt = effectiveMessages + .filter((m) => m.role === "system" && typeof m.content === "string") + .map((m) => m.content as string) + .join("\n\n"); const lastUserMsg = [...effectiveMessages].reverse().find((m) => m.role === "user"); const userText = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : ""; - const toolPrompt = typeof toolSystemMsg?.content === "string" ? toolSystemMsg.content : ""; return toolPrompt ? `${toolPrompt}\n\n${userText}` : userText; } @@ -456,13 +465,23 @@ export class GeminiWebExecutor extends BaseExecutor { // hasTools === false: flatten the full multi-turn history into the single // prompt so gemini-web (a stateless web-cookie provider that captures only // the first StreamGenerate response) preserves prior context across turns - // (#8371). Single-turn requests stay byte-for-byte identical to the original - // derivation, keeping the #7286 no-tools regression guard intact. + // (#8371). Single-turn requests with no system message stay byte-for-byte + // identical to the original derivation; a single-turn system message is + // now prepended instead of silently dropped (#13380). const prompt = hasTools ? buildGeminiToolPrompt(effectiveMessages) : buildGeminiPrompt(messages); - if (!prompt) { + // A system-only request (no user message at all) must still 400 — since + // #13380 prepends the system text, `prompt` alone is no longer a + // reliable "no user message" signal for the no-tools path (it used to be + // empty for a system-only request; now it carries the system text). + const hasUserMessage = messages.some( + (m: { role: string; content: unknown }) => + m.role === "user" && typeof m.content === "string" && m.content.trim().length > 0 + ); + + if (!prompt || (!hasTools && !hasUserMessage)) { return { response: new Response(JSON.stringify({ error: "No user message found" }), { status: 400, @@ -535,7 +554,14 @@ export class GeminiWebExecutor extends BaseExecutor { timeout: 10000, }); await inputEl.click(); - await page.keyboard.type(prompt, { delay: 10 }); + // insertText() dispatches a DOM `input` event atomically instead of a + // per-character keydown/keypress/keyup sequence (#13380) — an embedded + // `\n` in `prompt` (produced by the multi-turn transcript format above, + // or by any multiline system/user text) no longer fires the + // composer's Enter-submits-the-message handler before this function's + // own explicit Enter below. It also removes the fixed 10ms/char typing + // cost that made long prompts race the 30s response-wait timeout. + await page.keyboard.insertText(prompt); await page.waitForTimeout(300); await page.keyboard.press("Enter"); diff --git a/open-sse/executors/gitlab.ts b/open-sse/executors/gitlab.ts index 1d985a16da..a7840323f2 100644 --- a/open-sse/executors/gitlab.ts +++ b/open-sse/executors/gitlab.ts @@ -599,12 +599,17 @@ export class GitlabExecutor extends BaseExecutor { }; } - if (response.status === 403 && !isGitLabDirectAccessDisabled(response.status, bodyText)) { - return { - target: null, - credentials, - errorResponse: toOpenAIError(403, "GitLab Duo direct access scope is unavailable"), - }; + // #12958: any direct_access 403 (not only GitLab's exact "direct connections + // are disabled" tenant-config message) is recoverable via the public + // completions fallback — mirrors the 401 branch above and the connection-test + // path's shouldFallbackToPublicCodeSuggestions() contract. + if (response.status === 403 && input.log) { + input.log.warn( + "GITLAB-DUO", + isGitLabDirectAccessDisabled(response.status, bodyText) + ? "direct_access exchange rejected (403, direct connections disabled); falling back to public completions endpoint" + : `direct_access exchange rejected (403); falling back to public completions endpoint. Body: ${bodyText.slice(0, 500)}` + ); } return { diff --git a/open-sse/executors/grok-cli.ts b/open-sse/executors/grok-cli.ts index fc37b23ce2..b9e004a538 100644 --- a/open-sse/executors/grok-cli.ts +++ b/open-sse/executors/grok-cli.ts @@ -127,13 +127,18 @@ function normalizeGrokBuildReasoning( model: string ): Record | null { const reasoning = asRequestRecord(value); + // Capture BEFORE stripping: an explicit (but unsupported/invalid, e.g. "none"/"off"/ + // "xhigh") effort must still count as an explicit off-switch below — only the true + // ABSENCE of an effort key gets the model default. Restores the #7358 behavior the + // 4.6 default accidentally regressed: without this, every explicit "none"/"off" from + // grok-cli got silently promoted to "high", leaving no way to disable reasoning. const hasExplicitEffort = Object.prototype.hasOwnProperty.call(reasoning, "effort"); if (!GROK_BUILD_REASONING_EFFORT_SET.has(String(reasoning.effort))) { delete reasoning.effort; } if (model === "grok-composer-2.5-fast") { delete reasoning.effort; - } else if (model === "grok-4.5" && !hasExplicitEffort) { + } else if ((model === "grok-4.5" || model === "grok-4.6") && !hasExplicitEffort) { reasoning.effort = GROK_BUILD_DEFAULT_REASONING_EFFORT; } return Object.keys(reasoning).length > 0 ? reasoning : null; diff --git a/open-sse/executors/zai-web.ts b/open-sse/executors/zai-web.ts index fb09e4ea82..dafed25c63 100644 --- a/open-sse/executors/zai-web.ts +++ b/open-sse/executors/zai-web.ts @@ -51,6 +51,7 @@ import { makeZaiChunkEmitter, } from "./zai-web/stream.ts"; import { browserBackedChat } from "../services/browserBackedChat.ts"; +import { isMissingBrowserExecutable } from "./browserExecutableCheck.ts"; import { CursorImageError, resolveCursorImages } from "../utils/cursorImages.ts"; import { makeExecutorErrorResult as makeErrorResult, @@ -424,9 +425,26 @@ export class ZaiWebExecutor extends BaseExecutor { try { result = await browserBackedChat(buildZaiBrowserChatOptions({ ...input, attachments })); } catch (error) { - const message = sanitizeErrorMessage( - error instanceof Error ? error.message : "browser transport unavailable" - ); + const rawMessage = error instanceof Error ? error.message : "browser transport unavailable"; + // #13232: a missing Playwright browser binary is a host/config problem, not a transient + // upstream fault (same class as #3516 in gemini-web.ts). Surface an actionable message and + // tag it with the connection-cooldown hint so accountFallback skips the whole-provider + // circuit breaker (502/500 would trip it) and applies a short, non-exponential cooldown + // instead. + if (isMissingBrowserExecutable(rawMessage)) { + return { + errorResult: makeErrorResult( + 503, + "Z.ai requires the Playwright Chromium browser, which is not installed. " + + "Run `npx playwright install chromium` on the host (or rebuild the Docker image " + + "with browsers).", + input.body, + ZAI_CHAT_URL, + { "X-Omni-Fallback-Hint": "connection_cooldown" } + ), + }; + } + const message = sanitizeErrorMessage(rawMessage); return { errorResult: makeErrorResult( 502, diff --git a/open-sse/handlers/chatCore/nonStreamingClientTranslate.ts b/open-sse/handlers/chatCore/nonStreamingClientTranslate.ts index 79821278f0..d328934140 100644 --- a/open-sse/handlers/chatCore/nonStreamingClientTranslate.ts +++ b/open-sse/handlers/chatCore/nonStreamingClientTranslate.ts @@ -130,7 +130,17 @@ export function translateNonStreamingClientResponse( for (const item of responseOutput) { if (item?.type !== "function_call") continue; const identity = requestToolIdentityMap.get(item.name); - if (identity) { + // `requestToolIdentityMap` is typed as Map, but + // extractRequestToolIdentityMap() (chatCore/requestToolIdentity.ts) falls + // back to `_toolNameMap` when no namespace tools were present — and that + // side channel is a plain Map alias table published by the + // openai->gemini/claude pivot (#9780), not {namespace, name} identities. + // Applying that fallback here unconditionally overwrote a perfectly valid + // `item.name` (e.g. "shell") with `("shell").name === undefined`, which + // JSON.stringify then drops the key entirely (#12370) — Codex receives a + // function_call with no name and cannot dispatch it. Only apply the + // restore when `identity` actually has the {namespace, name} shape. + if (identity && typeof identity === "object" && typeof identity.name === "string") { item.namespace = identity.namespace; item.name = identity.name; } diff --git a/open-sse/handlers/responseSanitizer/reasoning.ts b/open-sse/handlers/responseSanitizer/reasoning.ts index d69abb054e..d56388cf7e 100644 --- a/open-sse/handlers/responseSanitizer/reasoning.ts +++ b/open-sse/handlers/responseSanitizer/reasoning.ts @@ -131,11 +131,15 @@ export function isTextualReasoningTagNativeRoute(providerId: string, modelId: st /r1[-_/]?distill\b/.test(routeId) || /(?:^|[/:_-])qwq(?:[/._:-]|$)/.test(routeId) || /(?:^|[/_-])k3(?:[/._:-]|$)/.test(modelId) || - // 9router#2231: MiniMax M3 leaks raw ... into `content` on its - // OpenAI-format provider tiers (trae, huggingchat, bazaarlink, ollama-cloud, - // opencode, cline, opencode-zen, codebuddy-cn). The direct minimax/minimax-cn - // tiers stay on Anthropic's Messages format (targetFormat: "claude") and - // already surface reasoning natively, so they are excluded here. + // 9router#2231, #13558: MiniMax M3 leaks raw ... into `content` + // on BOTH its OpenAI-format provider tiers (trae, huggingchat, bazaarlink, + // ollama-cloud, opencode, cline, opencode-zen, codebuddy-cn) AND its direct + // Anthropic Messages tiers (minimax, minimax-cn) — the latter were previously + // excluded here on the false assumption that speaking Claude's wire format + // meant reasoning already arrived as a structured `thinking` block. Only + // non-M3 minimax models still surface reasoning natively that way. + ((providerId === "minimax" || providerId === "minimax-cn") && + /minimax[-_]?m3\b/.test(modelId)) || (providerId !== "minimax" && providerId !== "minimax-cn" && /minimax[-_]?m3\b/.test(routeId)) ); } diff --git a/open-sse/handlers/responseTranslator.ts b/open-sse/handlers/responseTranslator.ts index 05195b8011..07c55e964f 100644 --- a/open-sse/handlers/responseTranslator.ts +++ b/open-sse/handlers/responseTranslator.ts @@ -15,6 +15,10 @@ import { restoreClaudeToolName } from "../services/claudeCodeToolRemapper.ts"; import { extractReplayableResponsesReasoningText } from "../services/reasoningInputPolicy.ts"; import { sanitizeToolId } from "../translator/helpers/schemaCoercion.ts"; import { stripEmptyOptionalToolArgs } from "../translator/response/openai-responses/pureHelpers.ts"; +import { + extractThinkingFromContent, + shouldParseTextualReasoningTags, +} from "./responseSanitizer/reasoning.ts"; type JsonRecord = Record; @@ -571,6 +575,22 @@ export function translateNonStreamingResponse( } } + // #13558: MiniMax-M3's Anthropic-compatible endpoint puts its reasoning + // inline as ... inside an ordinary "text" content block + // instead of a structured "thinking" block, so it never hit the + // thinkingContent accumulation above. Strip any such markup out of the + // accumulated text and merge it into thinkingContent, gated the same + // way the streaming/passthrough paths already are. + if (textContent && shouldParseTextualReasoningTags(undefined, root.model)) { + const extracted = extractThinkingFromContent(textContent); + textContent = extracted.content; + if (extracted.thinking) { + thinkingContent = thinkingContent + ? `${thinkingContent}\n\n${extracted.thinking}` + : extracted.thinking; + } + } + // #9971: a content-less-but-valid Claude body (thinking / redacted_thinking // / tool_use-only, or a truncated extended-thinking-only stream) has blocks // but no final text. Surfacing it here helps correlate a live VPS capture diff --git a/open-sse/services/batchProcessor.ts b/open-sse/services/batchProcessor.ts index 7b242e4851..832dd46441 100644 --- a/open-sse/services/batchProcessor.ts +++ b/open-sse/services/batchProcessor.ts @@ -7,6 +7,7 @@ import { getBatch, getPendingBatches, getTerminalBatches, + isFileReferencedByOtherBatch, listBatchItemCheckpoints, markBatchItemError, markBatchItemProcessing, @@ -253,13 +254,32 @@ async function cleanupExpiredBatches(): Promise { : null; const outputExpiresAt = getBatchOutputExpiresAt(batch); - if (batch.inputFileId && inputExpiresAt && now > inputExpiresAt) { + // #13681: skip the soft-delete when some OTHER batch still references + // the same file id (e.g. one input file reused across batches) — a + // terminal batch's own expiry must not null a file a sibling still + // needs. + if ( + batch.inputFileId && + inputExpiresAt && + now > inputExpiresAt && + !isFileReferencedByOtherBatch(batch.inputFileId, [batch.id]) + ) { deleteFile(batch.inputFileId); } - if (batch.outputFileId && outputExpiresAt && now > outputExpiresAt) { + if ( + batch.outputFileId && + outputExpiresAt && + now > outputExpiresAt && + !isFileReferencedByOtherBatch(batch.outputFileId, [batch.id]) + ) { deleteFile(batch.outputFileId); } - if (batch.errorFileId && outputExpiresAt && now > outputExpiresAt) { + if ( + batch.errorFileId && + outputExpiresAt && + now > outputExpiresAt && + !isFileReferencedByOtherBatch(batch.errorFileId, [batch.id]) + ) { deleteFile(batch.errorFileId); } } diff --git a/open-sse/services/combo/roundRobinCombo.ts b/open-sse/services/combo/roundRobinCombo.ts index be973e57d0..3b728188a9 100644 --- a/open-sse/services/combo/roundRobinCombo.ts +++ b/open-sse/services/combo/roundRobinCombo.ts @@ -100,6 +100,7 @@ import { import { applyComboTargetExhaustion } from "./targetExhaustion.ts"; import { isRetryAfterEligibleStatus } from "./unavailableRetryGate.ts"; import { isRecord } from "./comboData.ts"; +import { createRRDashboardEvents } from "./rrDashboardEvents.ts"; import { attemptCompatRejectedFallback } from "./comboCompatFallback.ts"; import { applyRequestTagRouting } from "./autoStrategy.ts"; import { @@ -485,6 +486,7 @@ export async function handleRoundRobinCombo({ const target = filteredTargets[modelIndex]; const modelStr = target.modelStr; const provider = target.provider; + const rrEvents = createRRDashboardEvents(combo.name, modelIndex, provider, modelStr); const profile = await getRuntimeProviderProfile(provider); const semaphoreKey = `combo:${combo.name}:${target.executionKey}`; const allowRateLimitedConnection = @@ -646,6 +648,7 @@ export async function handleRoundRobinCombo({ fingerprint: resolveTargetFingerprint(target) ?? "", }); + rrEvents.attempt(); const result = await Promise.race([ handleSingleModel(attemptBody, modelStr, { ...targetForAttempt, @@ -709,6 +712,7 @@ export async function handleRoundRobinCombo({ rrSelectedConnectionId || target.connectionId ); } + rrEvents.failed(`Quality: ${quality.reason}`, Date.now() - startTime); recordComboRequest(combo.name, modelStr, { success: false, latencyMs: Date.now() - startTime, @@ -735,6 +739,7 @@ export async function handleRoundRobinCombo({ "COMBO-RR", `${modelStr} succeeded (${latencyMs}ms, ${fallbackCount} fallbacks)` ); + rrEvents.succeeded(latencyMs); recordComboRequest(combo.name, modelStr, { success: true, latencyMs, @@ -844,6 +849,7 @@ export async function handleRoundRobinCombo({ "COMBO-RR", `Client disconnected (499) during ${modelStr} — stopping combo loop` ); + rrEvents.failed("Client disconnected", Date.now() - startTime); recordComboRequest(combo.name, modelStr, { success: false, latencyMs: Date.now() - startTime, @@ -884,6 +890,7 @@ export async function handleRoundRobinCombo({ "COMBO-RR", `Local rate-limit queue capacity reached for ${modelStr} — returning without upstream fallback` ); + rrEvents.failed(errorText || "Local queue full", Date.now() - startTime); recordComboRequest(combo.name, modelStr, { success: false, latencyMs: Date.now() - startTime, @@ -1023,6 +1030,7 @@ export async function handleRoundRobinCombo({ } // Done with this model + rrEvents.failed(errorText || `HTTP ${result.status}`, Date.now() - startTime); recordComboRequest(combo.name, modelStr, { success: false, latencyMs: Date.now() - startTime, diff --git a/open-sse/services/combo/rrDashboardEvents.ts b/open-sse/services/combo/rrDashboardEvents.ts new file mode 100644 index 0000000000..55bf0b703e --- /dev/null +++ b/open-sse/services/combo/rrDashboardEvents.ts @@ -0,0 +1,43 @@ +/** + * Dashboard EventBus emitters for the round-robin combo loop (#13089). + * + * `roundRobinCombo.ts` is frozen at its file-size cap (#12884), so the + * `combo.target.attempt` / `combo.target.succeeded` / `combo.target.failed` + * publishing logic lives here — a factory bound to one target's identity so + * each call site in the frozen file is a single line. + * + * @internal — not part of the public combo.ts barrel. + */ +import { emit } from "../../../src/lib/events/eventBus"; + +export interface RRDashboardEvents { + attempt(): void; + succeeded(latencyMs: number): void; + failed(error: string, latencyMs: number): void; +} + +export function createRRDashboardEvents( + comboName: string, + targetIndex: number, + provider: string, + model: string +): RRDashboardEvents { + return { + attempt() { + emit("combo.target.attempt", { + comboName, + targetIndex, + provider, + model, + timestamp: Date.now(), + strategy: "round-robin", + }); + }, + succeeded(latencyMs) { + emit("combo.target.succeeded", { comboName, targetIndex, provider, model, latencyMs }); + }, + failed(error, latencyMs) { + emit("combo.target.failed", { comboName, targetIndex, provider, model, error, latencyMs }); + }, + }; +} diff --git a/open-sse/services/compression/lite.ts b/open-sse/services/compression/lite.ts index ade5858352..644fd300a8 100644 --- a/open-sse/services/compression/lite.ts +++ b/open-sse/services/compression/lite.ts @@ -157,6 +157,7 @@ export function removeRedundantContent( const contentStr = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content); if ( i > 0 && + msg.role !== "tool" && body.messages[i - 1].role === msg.role && typeof body.messages[i - 1].content === "string" && body.messages[i - 1].content === contentStr diff --git a/open-sse/services/model.ts b/open-sse/services/model.ts index 1d36623205..0570227d91 100644 --- a/open-sse/services/model.ts +++ b/open-sse/services/model.ts @@ -73,6 +73,14 @@ const PROVIDER_MODEL_ALIASES: ProviderModelAliasMap = { "claude-sonnet-4-5": "claude-sonnet-4.5", "claude-haiku-4-5": "claude-haiku-4.5", }, + // #13364: zed-hosted's passthrough catalog exposes short hyphenated Claude ids + // that don't match modelSpecs' dotted canonical alias, so capMaxOutputTokens() + // resolves no cap and thinking+tools requests inflate max_tokens unbounded. + // Scoped to claude-haiku-4-5 (the reported/reproduced model) — add Sonnet/Opus + // entries only once confirmed against the live Zed catalog. + "zed-hosted": { + "claude-haiku-4-5": "claude-haiku-4.5", + }, }; const CROSS_PROXY_MODEL_ALIASES: Record = { diff --git a/open-sse/services/thinkingBudget.ts b/open-sse/services/thinkingBudget.ts index d9926cbb71..003dd82e19 100644 --- a/open-sse/services/thinkingBudget.ts +++ b/open-sse/services/thinkingBudget.ts @@ -62,6 +62,62 @@ export const THINKING_LEVEL_MAP: Record = { xhigh: 131072, // T11: explicit xhigh alias }; +export type Gemini38ThinkingLevel = "low" | "medium" | "high"; + +/** Gemini 3.8 Flash (and prefixed ids like agy/gemini-3.8-flash-high). */ +export function isGemini38Model(model: string): boolean { + return /(?:^|[\/])gemini-3\.8(?:$|-)/i.test(model); +} + +export function gemini38ThinkingLevelFromBudget( + model: string, + budget: number +): Gemini38ThinkingLevel { + const resolved = getResolvedModelCapabilities(model); + const cap = resolved.thinkingBudgetCap ?? 24576; + const medium = resolved.defaultThinkingBudget || 8192; + if (budget <= 0) { + throw new RangeError( + "gemini38ThinkingLevelFromBudget: budget must be > 0; use gemini38ThinkingConfig for the off-switch" + ); + } + if (budget >= cap) return "high"; + if (budget <= 1024) return "low"; + if (budget <= medium) return "medium"; + return "high"; +} + +function clientAskedForThoughts(body: Record): boolean { + return body.includeThoughts === true || body.include_thoughts === true; +} + +/** + * Gemini 3.8 honors thinkingLevel, not a 3.7 numeric thinkingBudget. + * Omit includeThoughts unless the client asked - thoughts share maxOutputTokens. + */ +export function gemini38ThinkingConfig( + model: string, + budget: number, + body: Record +): + | { thinkingLevel: Gemini38ThinkingLevel; includeThoughts?: boolean } + | { thinkingBudget: number; includeThoughts: boolean } { + if (budget <= 0) { + return { thinkingBudget: 0, includeThoughts: false }; + } + const thinkingConfig: { + thinkingLevel: Gemini38ThinkingLevel; + includeThoughts?: boolean; + } = { + thinkingLevel: gemini38ThinkingLevelFromBudget(model, budget), + }; + if (clientAskedForThoughts(body)) { + thinkingConfig.includeThoughts = true; + } + return thinkingConfig; +} + + // Default config (passthrough = backward compatible) export const DEFAULT_THINKING_CONFIG = { mode: ThinkingMode.PASSTHROUGH, diff --git a/open-sse/translator/request/claude-to-gemini.ts b/open-sse/translator/request/claude-to-gemini.ts index 9137aec0f0..b41fc9d88c 100644 --- a/open-sse/translator/request/claude-to-gemini.ts +++ b/open-sse/translator/request/claude-to-gemini.ts @@ -11,6 +11,8 @@ import { } from "../../services/geminiThoughtSignatureStore.ts"; import { capMaxOutputTokens, capThinkingBudget } from "../../../src/lib/modelCapabilities.ts"; import { getModelSpec } from "../../../src/shared/constants/modelSpecs.ts"; +import { gemini38ThinkingConfig, isGemini38Model } from "../../services/thinkingBudget.ts"; + import { buildChangedToolNameMap, buildHistoricalToolResultContext, @@ -259,14 +261,16 @@ export function claudeToGeminiRequest(model, body, stream, credentials = null) { // but thinkingBudgetCap:24576, meaning it supports thinking via budget). // Models not in MODEL_SPECS (thinkingBudgetCap=undefined) default to allowed. if (cappedBudget > 0 || getModelSpec(model)?.thinkingBudgetCap !== 0) { - result.generationConfig.thinkingConfig = { - thinkingBudget: cappedBudget, - // #6813: `budget_tokens: 0` on this explicit path is the client's dynamic-thinking - // sentinel, not an off-switch — includeThoughts stays true regardless of the - // (possibly cap-clamped) budget value. Only the reasoning_effort/output_config.effort - // paths below treat a resulting budget of 0 as "thinking disabled". - includeThoughts: true, - }; + result.generationConfig.thinkingConfig = isGemini38Model(model) + ? gemini38ThinkingConfig(model, cappedBudget, body) + : { + thinkingBudget: cappedBudget, + // #6813: `budget_tokens: 0` is the explicit path's client's dynamic-thinking + // sentinel, not an off-switch — includeThoughts stays true regardless of the + // (possibly cap-clamped) budget value. Only the reasoning_effort/output_config.effort + // paths below treat a resulting budget of 0 as "thinking disabled". + includeThoughts: true, + }; } } else if (typeof body.output_config?.effort === "string") { const effort = body.output_config.effort.toLowerCase(); @@ -290,10 +294,12 @@ export function claudeToGeminiRequest(model, body, stream, credentials = null) { // Models with thinkingBudgetCap:0 (e.g. gemini-3-flash) reject // thinkingConfig even for effort-based paths. if (getModelSpec(model)?.thinkingBudgetCap !== 0) { - result.generationConfig.thinkingConfig = { - thinkingBudget: budget, - includeThoughts: true, - }; + result.generationConfig.thinkingConfig = isGemini38Model(model) + ? gemini38ThinkingConfig(model, budget, body) + : { + thinkingBudget: budget, + includeThoughts: true, + }; } } } diff --git a/open-sse/translator/request/openai-responses.ts b/open-sse/translator/request/openai-responses.ts index 18e8f41940..0e4e4f5d71 100644 --- a/open-sse/translator/request/openai-responses.ts +++ b/open-sse/translator/request/openai-responses.ts @@ -716,6 +716,12 @@ export function openaiResponsesToOpenAIRequest( result.tool_choice = { type: "function", function: { name: tc.name } }; } else if (tcType === "local_shell") { result.tool_choice = { type: "function", function: { name: "shell" } }; + } else if (tcType === "custom" && tc.name !== undefined) { + // #13122: forced custom/freeform tool_choice (Codex CLI's wire_api="responses" + // sends this to force functions__exec-style tools). Custom tools are already + // normalized into a Chat { input: string } function schema above, so forcing that + // same declared name via Chat's tool_choice selects it correctly. + result.tool_choice = { type: "function", function: { name: tc.name } }; } else if (tcType === "allowed_tools") { const mode = toString(tc.mode); if (mode !== "auto" && mode !== "required") { diff --git a/open-sse/translator/request/openai-to-gemini.ts b/open-sse/translator/request/openai-to-gemini.ts index d68946369b..23a0a767ba 100644 --- a/open-sse/translator/request/openai-to-gemini.ts +++ b/open-sse/translator/request/openai-to-gemini.ts @@ -17,6 +17,7 @@ import { getDefaultThinkingBudget, } from "../../../src/lib/modelCapabilities.ts"; import { getModelSpec } from "../../../src/shared/constants/modelSpecs.ts"; +import { gemini38ThinkingConfig, isGemini38Model } from "../../services/thinkingBudget.ts"; import { DEFAULT_SAFETY_SETTINGS, @@ -241,10 +242,12 @@ function openaiToGeminiBase( // the pre-#6943 native-defaults contract (thinkingBudget 0 / includeThoughts // false must still be present) and crashed callers that read // .thinkingConfig.thinkingBudget unconditionally. - result.generationConfig.thinkingConfig = { - thinkingBudget: budget, - includeThoughts: budget !== 0, - }; + result.generationConfig.thinkingConfig = isGemini38Model(model) + ? gemini38ThinkingConfig(model, budget, body) + : { + thinkingBudget: budget, + includeThoughts: budget !== 0, + }; } // 2. Claude format: thinking (type: enabled, budget_tokens) // Use an explicit numeric check (not truthy) so an explicit `budget_tokens: 0` — the @@ -264,10 +267,12 @@ function openaiToGeminiBase( // but thinkingBudgetCap:24576, meaning it supports thinking via budget). // Models not in MODEL_SPECS (thinkingBudgetCap=undefined) default to allowed. if (cappedBudget > 0 || getModelSpec(model)?.thinkingBudgetCap !== 0) { - result.generationConfig.thinkingConfig = { - thinkingBudget: cappedBudget, - includeThoughts: cappedBudget !== 0, - }; + result.generationConfig.thinkingConfig = isGemini38Model(model) + ? gemini38ThinkingConfig(model, cappedBudget, body) + : { + thinkingBudget: cappedBudget, + includeThoughts: cappedBudget !== 0, + }; } } } @@ -294,10 +299,14 @@ function openaiToGeminiBase( // Models not in MODEL_SPECS (thinkingBudgetCap=undefined) default to allowed. getModelSpec(model)?.thinkingBudgetCap !== 0 ) { - result.generationConfig.thinkingConfig = { - thinkingBudget: getDefaultThinkingBudget(model) || capThinkingBudget(model, 24576), - includeThoughts: true, - }; + const defaultBudget = + getDefaultThinkingBudget(model) || capThinkingBudget(model, 24576); + result.generationConfig.thinkingConfig = isGemini38Model(model) + ? gemini38ThinkingConfig(model, defaultBudget, body) + : { + thinkingBudget: defaultBudget, + includeThoughts: true, + }; } } diff --git a/open-sse/translator/request/openai-to-gemini/helpers.ts b/open-sse/translator/request/openai-to-gemini/helpers.ts index 092a857a2c..23c9cd03d4 100644 --- a/open-sse/translator/request/openai-to-gemini/helpers.ts +++ b/open-sse/translator/request/openai-to-gemini/helpers.ts @@ -11,8 +11,9 @@ export type GeminiGenerationConfig = { topK?: unknown; maxOutputTokens?: unknown; thinkingConfig?: { - thinkingBudget: number; - includeThoughts: boolean; + thinkingBudget?: number; + thinkingLevel?: "low" | "medium" | "high"; + includeThoughts?: boolean; }; responseMimeType?: string; responseSchema?: unknown; diff --git a/open-sse/translator/request/openai-to-kiro.ts b/open-sse/translator/request/openai-to-kiro.ts index 03bae61bb1..1c6238d5d2 100644 --- a/open-sse/translator/request/openai-to-kiro.ts +++ b/open-sse/translator/request/openai-to-kiro.ts @@ -183,6 +183,13 @@ function convertMessages(messages, tools, model) { let currentRole = null; let toolsAttached = false; let toolDocs = ""; + // The actual turn object that ends up carrying `toolDocs` (issue #13652). + // `buildKiroPayload()` only prepends the doc block onto `currentMessage`, so + // once this turn is demoted into `history` (any turn after the first, on a + // resent multi-turn request) we need to know it was NOT promoted, and embed + // the doc text directly onto it instead of letting it get re-glued onto + // whatever the newest turn happens to be. + let toolDocsCarrier = null; // Only Claude models support images in Kiro. Kiro also routes non-Claude // models (deepseek, minimax, glm, qwen3-coder-next) that do not accept image @@ -242,7 +249,10 @@ function convertMessages(messages, tools, model) { } const built = buildKiroToolSpecs(tools); userMsg.userInputMessage.userInputMessageContext.tools = built.specs; - if (built.docs) toolDocs = built.docs; + if (built.docs) { + toolDocs = built.docs; + toolDocsCarrier = userMsg; + } toolsAttached = true; } @@ -530,10 +540,30 @@ function convertMessages(messages, tools, model) { } const built = buildKiroToolSpecs(tools); currentMessage.userInputMessage.userInputMessageContext.tools = built.specs; - if (built.docs) toolDocs = built.docs; + if (built.docs) { + toolDocs = built.docs; + toolDocsCarrier = currentMessage; + } toolsAttached = true; } + // The relocated doc text is only safe to leave in `toolDocs` (which + // `buildKiroPayload()` unconditionally prepends onto `currentMessage`) when + // the turn that originally carried it IS `currentMessage` — true for a + // single-turn conversation and the "no user turn" fallback above. On any + // later turn of a resent multi-turn request, the tool-bearing turn has been + // demoted into `history` instead, so re-prepending `toolDocs` here would + // glue the *already delivered* doc block onto the newest turn every time + // (issue #13652). Embed it directly onto the carrier turn's own content — + // still in `history` at this point — and clear `toolDocs` so + // `buildKiroPayload()` does not also inject it. + if (toolDocs && toolDocsCarrier && toolDocsCarrier !== currentMessage) { + const carrierMessage = toolDocsCarrier.userInputMessage; + const existingContent = carrierMessage.content || ""; + carrierMessage.content = `# Tool Documentation\n\n${toolDocs}\n\n---\n\n${existingContent}`; + toolDocs = ""; + } + // Clean up history for Kiro API compatibility history.forEach((item) => { if (item.userInputMessage?.userInputMessageContext?.tools) { diff --git a/open-sse/translator/response/claude-to-openai.ts b/open-sse/translator/response/claude-to-openai.ts index 2d20661e7b..f0bc818721 100644 --- a/open-sse/translator/response/claude-to-openai.ts +++ b/open-sse/translator/response/claude-to-openai.ts @@ -1,5 +1,6 @@ import { register } from "../registry.ts"; import { FORMATS } from "../formats.ts"; +import { initThinkState, applyThinkTag, flushThinkBuffer } from "../../utils/thinkTagParser.ts"; type OpenAIUsage = { prompt_tokens: number; @@ -44,6 +45,13 @@ export function claudeToOpenAIResponse(chunk, state) { state.messageId = chunk.message?.id || `msg_${Date.now()}`; state.model = chunk.message?.model; state.toolCallIndex = 0; + // #13558: MiniMax-M3's Anthropic-compatible endpoint puts its reasoning + // inline as ... inside ordinary text/text_delta blocks + // instead of a structured thinking/thinking_delta block. Reuse the + // passthrough-mode think-tag parser here (gated the same way, by + // shouldParseTextualReasoningTags) so it strips markup out of + // delta.content and re-emits it as delta.reasoning_content. + state.thinkState = initThinkState(true, state.provider, state.model); const startUsage = chunk.message?.usage; if (startUsage && typeof startUsage === "object") { const inputTokens = @@ -126,7 +134,16 @@ export function claudeToOpenAIResponse(chunk, state) { } state.pendingThinkClose = false; } - results.push(createChunk(state, { content: delta.text })); + const textDelta: { content: unknown; reasoning_content?: string } = { + content: delta.text, + }; + if (state.thinkState) applyThinkTag(state.thinkState, textDelta); + if (textDelta.reasoning_content) { + results.push(createChunk(state, { reasoning_content: textDelta.reasoning_content })); + } + if (textDelta.content) { + results.push(createChunk(state, { content: textDelta.content })); + } } else if (delta?.type === "thinking_delta" && delta.thinking) { // Map Claude thinking_delta → OpenAI reasoning_content // Clients (Claude Code, Cursor, etc.) display reasoning_content as the thinking panel @@ -151,6 +168,19 @@ export function claudeToOpenAIResponse(chunk, state) { } case "content_block_stop": { + // #13558: flush any /reasoning text still buffered by the + // textual think-tag parser (e.g. a block that ends mid-tag, or a + // reasoning tail with no trailing visible content) before the block + // closes, so it is never silently dropped. + if (state.thinkState?.active) { + const flushed = flushThinkBuffer(state.thinkState); + if (flushed.reasoningDelta) { + results.push(createChunk(state, { reasoning_content: flushed.reasoningDelta })); + } + if (flushed.contentDelta) { + results.push(createChunk(state, { content: flushed.contentDelta })); + } + } if (state.inThinkingBlock && chunk.index === state.currentBlockIndex) { // Defer the close marker instead of emitting immediately. // If the next block is tool_use there will be no text_delta, so the diff --git a/open-sse/translator/response/openai-responses.ts b/open-sse/translator/response/openai-responses.ts index 43453c0b28..29b9a937d7 100644 --- a/open-sse/translator/response/openai-responses.ts +++ b/open-sse/translator/response/openai-responses.ts @@ -9,6 +9,7 @@ import { projectCompletedStreamError } from "../../utils/streamErrorFormat.ts"; import { fallbackToolCallId } from "../helpers/toolCallHelper.ts"; import { shouldParseTextualReasoningTags } from "../../handlers/responseSanitizer.ts"; import { getReadableReasoningValue } from "../../utils/reasoningFields.ts"; +import { resolveResponsesCacheUsageDetails } from "../../utils/resolveResponsesCacheUsageDetails.ts"; import { isInternalReasoningPlaceholder, stripInternalReasoningPlaceholder, @@ -132,10 +133,9 @@ export function openaiToOpenAIResponsesResponse(chunk, state) { output_tokens, total_tokens: u.total_tokens ?? input_tokens + output_tokens, }; - const cachedTokens = - u.input_tokens_details?.cached_tokens ?? u.prompt_tokens_details?.cached_tokens; - if (cachedTokens) { - state.usage.input_tokens_details = { cached_tokens: cachedTokens }; + const cacheDetails = resolveResponsesCacheUsageDetails(u); + if (cacheDetails) { + state.usage.input_tokens_details = cacheDetails; } const reasoningTokens = u.output_tokens_details?.reasoning_tokens ?? u.completion_tokens_details?.reasoning_tokens; diff --git a/open-sse/utils/diagnostics.ts b/open-sse/utils/diagnostics.ts index bb84d02461..c4dde7a8a2 100644 --- a/open-sse/utils/diagnostics.ts +++ b/open-sse/utils/diagnostics.ts @@ -255,7 +255,9 @@ export function detectMalformedNonStream(resp: unknown): MalformedReason | null // 1) A block IS present but invalid (e.g. text:"", a lone "(empty response)" // sentinel, or only null entries) — the model genuinely produced no // usable output. That is a MALFORMED-200 empty_choices regardless of - // stop_reason (parity with the OpenAI content:"" path). + // stop_reason (parity with the OpenAI content:"" path) — UNLESS the + // terminal stop_reason is one of the legitimate truncated-completion + // exemptions below (#12968). // 2) `content: []` — no block at all. #9971: a truncated / non-terminal // body (no stop_reason) must not become empty_choices. A terminal // stop_reason with no output usually is empty_choices — except the @@ -265,12 +267,16 @@ export function detectMalformedNonStream(resp: unknown): MalformedReason | null // return content:[] + stop_reason max_tokens. Treating that as // empty_choices turns a valid 200 into MALFORMED-200 → 502 even // though errorClassifier would have let it through. - if (content.length === 0) { - const stopReason = typeof body.stop_reason === "string" ? body.stop_reason : ""; - if (stopReason.length === 0) return null; - if (stopReason === "max_tokens" || stopReason === "tool_use") return null; - return "empty_choices"; - } + const stopReason = typeof body.stop_reason === "string" ? body.stop_reason : ""; + // #12968: the #9971 exemption above only fired when `content` was a + // completely empty array. A tiny `max_tokens` probe against an + // Anthropic-compatible shim can instead return content:[{type:"text", + // text:""}] — one block, just with no visible text — which is the exact + // same legitimate truncated-completion shape, so the exemption must apply + // whenever there is no visible output, not only when content is []. + if (stopReason === "max_tokens" || stopReason === "tool_use") return null; + // content:[] with no stop_reason at all is non-terminal, not empty (#9971). + if (content.length === 0 && stopReason.length === 0) return null; return "empty_choices"; } diff --git a/open-sse/utils/directResponseStartTimeout.ts b/open-sse/utils/directResponseStartTimeout.ts index 90e7b6a04a..2476413805 100644 --- a/open-sse/utils/directResponseStartTimeout.ts +++ b/open-sse/utils/directResponseStartTimeout.ts @@ -61,16 +61,29 @@ export async function directFetchWithBoundedResponseStart( ): Promise { if (!timeoutMs || timeoutMs <= 0) return fetchImpl(input, options); const attemptController = new AbortController(); - const timer = setTimeout( - () => attemptController.abort(createDirectResponseStartTimeout(timeoutMs)), - timeoutMs - ); + // #12861: guards a narrow but real race between the timer macrotask and the + // fetch promise settling. If `fetchImpl` has already resolved/rejected by + // the time this timer fires, aborting now delivers the abort reason to a + // promise nobody is awaiting anymore — Node promotes that to an + // unhandledRejection -> uncaughtException and kills the process. Once the + // attempt has settled, the timer becomes a no-op instead: the caller + // already has its answer, and there's nothing left to abort for. + let settled = false; + const timer = setTimeout(() => { + if (settled) return; + attemptController.abort(createDirectResponseStartTimeout(timeoutMs)); + }, timeoutMs); timer.unref?.(); try { - return await fetchImpl(input, { + const response = await fetchImpl(input, { ...options, signal: mergeAbortSignals(options.signal, attemptController.signal), }); + settled = true; + return response; + } catch (err) { + settled = true; + throw err; } finally { clearTimeout(timer); } diff --git a/open-sse/utils/earlyStreamKeepalive.ts b/open-sse/utils/earlyStreamKeepalive.ts index 0b7cdaab36..67b81e6f63 100644 --- a/open-sse/utils/earlyStreamKeepalive.ts +++ b/open-sse/utils/earlyStreamKeepalive.ts @@ -89,6 +89,44 @@ export const OPENAI_RESPONSES_ERROR_FRAME = ENCODER.encode( })}\n\n` ); +/** + * Reshapes an already-sanitized upstream error body into the Responses API + * convention (`{"type":"error",...}`) for the dynamic real-upstream-body branch + * of the slow path (#13431). The body reaching here is Chat-Completions-shaped + * (`{"error":{message,type,code}}`, the combo/handler failure convention) most of + * the time, but may also be a bare `{message}` or unparseable text — every shape + * must still produce a non-empty `message` so the client never sees an opaque + * frame (never crash the stream on a malformed body). + */ +function buildResponsesErrorDataLine(text: string): string { + const trimmed = text.trim(); + let parsed: Record | null = null; + if (trimmed) { + try { + const candidate = JSON.parse(trimmed); + if (candidate && typeof candidate === "object") parsed = candidate as Record; + } catch { + parsed = null; + } + } + const errorObj = + parsed && typeof parsed.error === "object" && parsed.error !== null + ? (parsed.error as Record) + : null; + const message = + (typeof errorObj?.message === "string" && errorObj.message) || + (typeof parsed?.message === "string" && parsed.message) || + trimmed || + "Upstream stream failed before completion."; + const code = (typeof errorObj?.code === "string" && errorObj.code) || null; + const param = (typeof errorObj?.param === "string" && errorObj.param) || null; + const extras = + parsed && typeof parsed.diagnostics === "object" && parsed.diagnostics !== null + ? { diagnostics: parsed.diagnostics } + : {}; + return JSON.stringify({ type: "error", code, message, param, ...extras }); +} + export type EarlyStreamKeepaliveOptions = { /** Wait this long for the handler before committing to a keepalive stream. */ thresholdMs?: number; @@ -168,11 +206,29 @@ export async function withEarlyStreamKeepalive( : null; const extraHeaders = options.extraHeaders ?? {}; const errorFrame = options.errorFrame ?? ERROR_FRAME; - // Single source of truth for whether THIS route's error framing uses a named SSE - // `event: error` line (Anthropic) or a plain `data:` line (OpenAI Chat Completions / - // Responses) — derived from errorFrame itself so the dynamic real-upstream-body case - // below stays consistent with the static default-message case without a second option. - const errorFrameUsesNamedEvent = new TextDecoder().decode(errorFrame).startsWith("event:"); + // Single source of truth for THIS route's error-framing convention, derived from + // errorFrame itself so the dynamic real-upstream-body case below stays consistent + // with the static default-message case without a second option. Three shapes exist: + // - "anthropic": named SSE `event: error` line (Anthropic /v1/messages). + // - "responses": plain `data:` line, discriminated by a top-level `type` field + // inside the JSON payload (OpenAI Responses API convention). + // - "chat": plain `data:` line, discriminated by a top-level `error` key + // (OpenAI Chat Completions convention) — the default/fallback. + const decodedErrorFrame = new TextDecoder().decode(errorFrame); + const errorFrameFormat: "anthropic" | "responses" | "chat" = decodedErrorFrame.startsWith( + "event:" + ) + ? "anthropic" + : (() => { + const dataLine = decodedErrorFrame.match(/^data: (.+)\n\n$/); + if (!dataLine) return "chat"; + try { + const parsed = JSON.parse(dataLine[1]); + return parsed && typeof parsed === "object" && "type" in parsed ? "responses" : "chat"; + } catch { + return "chat"; + } + })(); const correlationId = options.correlationId; const frameDecoder = correlationId ? new TextDecoder() : null; // Records every direct-to-client write EXCEPT the forwarded real response @@ -321,11 +377,14 @@ export async function withEarlyStreamKeepalive( // instead of forwarding raw JSON, which would be malformed SSE. const text = response.body ? await response.text().catch(() => "") : ""; const dataLine = - text.trim() || - JSON.stringify({ error: { message: "stream_error", type: "stream_error" } }); - const framed = errorFrameUsesNamedEvent - ? `event: error\ndata: ${dataLine}\n\n` - : `data: ${dataLine}\n\n`; + errorFrameFormat === "responses" + ? buildResponsesErrorDataLine(text) + : text.trim() || + JSON.stringify({ error: { message: "stream_error", type: "stream_error" } }); + const framed = + errorFrameFormat === "anthropic" + ? `event: error\ndata: ${dataLine}\n\n` + : `data: ${dataLine}\n\n`; const framedBytes = ENCODER.encode(framed); controller.enqueue(framedBytes); recordClientBytes(framedBytes); diff --git a/open-sse/utils/error.ts b/open-sse/utils/error.ts index b8b8785c89..e77d513259 100644 --- a/open-sse/utils/error.ts +++ b/open-sse/utils/error.ts @@ -1134,7 +1134,8 @@ export function makeExecutorErrorResult( status: number, message: string, body: unknown, - url: string + url: string, + extraResponseHeaders?: Record ) { return { response: new Response( @@ -1145,7 +1146,10 @@ export function makeExecutorErrorResult( code: `HTTP_${status}`, }, }), - { status, headers: { "Content-Type": "application/json" } } + { + status, + headers: { "Content-Type": "application/json", ...extraResponseHeaders }, + } ), url, headers: {} as Record, diff --git a/open-sse/utils/reasoningContentInjector.ts b/open-sse/utils/reasoningContentInjector.ts index a1d69ebf6d..7d3f48a9e7 100644 --- a/open-sse/utils/reasoningContentInjector.ts +++ b/open-sse/utils/reasoningContentInjector.ts @@ -55,16 +55,20 @@ export function isThinkingMessageModel(model: string | undefined | null): boolea return THINKING_MODEL_PATTERNS.some((re) => re.test(model)); } +/** + * `providerRequiresEcho` is resolved by the caller (Moonshot/Kimi legacy check, + * or a registry entry's `requiresReasoningContentEcho` capability flag — see + * `open-sse/config/providers/shared.ts`) so the provider allowlist lives in one + * place instead of being duplicated here. See issue #13599. + */ export function shouldInjectReasoningContentPlaceholder( + providerRequiresEcho: boolean, provider: unknown, model: string | undefined | null ): boolean { - const normalizedProvider = String(provider ?? "") - .trim() - .toLowerCase(); return ( - (normalizedProvider === "moonshot" || normalizedProvider === "kimi") && - !requiresAuthenticReasoningContent(normalizedProvider, model) && + providerRequiresEcho && + !requiresAuthenticReasoningContent(provider, model) && isThinkingMessageModel(model) ); } diff --git a/open-sse/utils/resolveResponsesCacheUsageDetails.ts b/open-sse/utils/resolveResponsesCacheUsageDetails.ts new file mode 100644 index 0000000000..ea977b487e --- /dev/null +++ b/open-sse/utils/resolveResponsesCacheUsageDetails.ts @@ -0,0 +1,24 @@ +import { pickCacheCreationTokens } from "./pickCacheCreationTokens.ts"; + +type CacheUsageSource = { + cache_creation_input_tokens?: number; + cache_write_tokens?: number; + input_tokens_details?: { cached_tokens?: number; cache_creation_tokens?: number }; + prompt_tokens_details?: { cached_tokens?: number; cache_creation_tokens?: number }; +}; + +/** + * Resolve the `input_tokens_details` object for a Responses API usage payload, + * merging cache READ (`cached_tokens`) and cache CREATION (`cache_creation_tokens`) + * tokens from any upstream shape instead of dropping the creation leg (#13472). + */ +export function resolveResponsesCacheUsageDetails(usage: CacheUsageSource) { + const cachedTokens = + usage.input_tokens_details?.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens; + const cacheCreationTokens = pickCacheCreationTokens(usage); + if (!cachedTokens && !cacheCreationTokens) return undefined; + return { + ...(cachedTokens ? { cached_tokens: cachedTokens } : {}), + ...(cacheCreationTokens ? { cache_creation_tokens: cacheCreationTokens } : {}), + }; +} diff --git a/open-sse/utils/streamHelpers.ts b/open-sse/utils/streamHelpers.ts index 39aafbacf9..13a57db6af 100644 --- a/open-sse/utils/streamHelpers.ts +++ b/open-sse/utils/streamHelpers.ts @@ -202,11 +202,7 @@ export function createSSEDataLineNormalizer(): SSEDataLineNormalizer { const normalizedLine = line.replace(CR_STRIP_RE, ""); const trimmed = normalizedLine.trim(); - if ( - trimmed && - SSE_FIELD_RE.test(trimmed) && - hasSelfDescribingPendingDataPayload() - ) { + if (trimmed && SSE_FIELD_RE.test(trimmed) && hasSelfDescribingPendingDataPayload()) { flush(output); } @@ -220,7 +216,9 @@ export function createSSEDataLineNormalizer(): SSEDataLineNormalizer { }; } -export function createSSEEventPrefixBuffer(options?: { forwardEvent?: boolean }): SSEEventPrefixBuffer { +export function createSSEEventPrefixBuffer(options?: { + forwardEvent?: boolean; +}): SSEEventPrefixBuffer { let lines: string[] = []; let emitted = false; // The `event:` line is only part of the SSE framing for protocols that define @@ -277,12 +275,7 @@ function hasOpenAICompatibleStreamValue(parsed: Record): boolea const delta = isRecord(choice.delta) ? choice.delta : null; if (!delta) return false; if (typeof delta.content === "string" && delta.content.length > 0) return true; - if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) { - return true; - } - if (typeof delta.reasoning_text === "string" && delta.reasoning_text.length > 0) { - return true; - } + if (hasAnyReasoningSignal(delta)) return true; return Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0; }); } diff --git a/open-sse/utils/tlsClient.ts b/open-sse/utils/tlsClient.ts index e0767d1b64..7106d3b5aa 100644 --- a/open-sse/utils/tlsClient.ts +++ b/open-sse/utils/tlsClient.ts @@ -5,7 +5,13 @@ import { getTlsClientTimeoutConfig } from "@/shared/utils/runtimeTimeouts"; // import the first-byte watchdog alongside TlsClient without adding a line. export { guardTlsFirstByte } from "./tlsFirstByteWatchdog.ts"; -const runtimeRequire = nodeModule.createRequire(import.meta.url); +// #12491 — anchor on process.argv[1]||cwd() rather than import.meta.url: the +// standalone Docker runtime re-lays-out files at a different relative depth +// than the build, so an import.meta.url-relative resolution can miss even +// though this loader already keeps the specifier itself dynamic (see +// loadRuntimeModule() below). Matches src/lib/machineToken.ts and +// src/lib/db/adapters/runtimeRequire.ts's established anchor pattern. +const runtimeRequire = nodeModule.createRequire(process.argv[1] || process.cwd()); function loadRuntimeModule(moduleName: string): unknown { // Keep the specifier dynamic. Turbopack rewrites a literal createRequire call diff --git a/package.json b/package.json index a42bf3a0cf..0964ce8be7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "omniroute", "version": "3.8.51", - "description": "Unified AI router with 358 providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.", + "description": "Unified AI router with 359 providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.", "type": "module", "bin": { "omniroute": "bin/omniroute.mjs", @@ -286,7 +286,8 @@ "test:unit:serial": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 \"tests/unit/serial/**/*.test.ts\"", "alibaba:sync-allowlist": "node --import tsx/esm scripts/ops/sync-alibaba-allowlist.mjs", "check:vitest-exclusions": "node scripts/check/check-vitest-exclusions.mjs", - "i18n:check-new-keys": "node scripts/i18n/check-new-key-coverage.mjs" + "i18n:check-new-keys": "node scripts/i18n/check-new-key-coverage.mjs", + "i18n:check-keys": "node scripts/i18n/check-key-completeness.mjs" }, "dependencies": { "@aws-sdk/client-bedrock-runtime": "^3.1120.0", diff --git a/public/images/tier-flow-dark.svg b/public/images/tier-flow-dark.svg index b8016c15c5..1aede4f7a8 100644 --- a/public/images/tier-flow-dark.svg +++ b/public/images/tier-flow-dark.svg @@ -1,6 +1,6 @@ - + OmniRoute 4-tier fallback - OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 358 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free. + OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 359 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free. @@ -15,7 +15,7 @@ OmniRoute 4-tier fallback - Never stop building — automatic zero-config failover across 358 providers + Never stop building — automatic zero-config failover across 359 providers diff --git a/public/images/tier-flow-light.svg b/public/images/tier-flow-light.svg index 2cff72d93d..6425db4f56 100644 --- a/public/images/tier-flow-light.svg +++ b/public/images/tier-flow-light.svg @@ -1,6 +1,6 @@ - + OmniRoute 4-tier fallback - OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 358 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free. + OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 359 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free. @@ -15,7 +15,7 @@ OmniRoute 4-tier fallback - Never stop building — automatic zero-config failover across 358 providers + Never stop building — automatic zero-config failover across 359 providers diff --git a/scripts/build/assembleStandalone.mjs b/scripts/build/assembleStandalone.mjs index d6e2010ef2..16ee2426c2 100644 --- a/scripts/build/assembleStandalone.mjs +++ b/scripts/build/assembleStandalone.mjs @@ -229,6 +229,15 @@ const EXTRA_MODULE_ENTRIES = [ src: ["scripts", "dev", "responses-ws-proxy.mjs"], dest: ["responses-ws-proxy.mjs"], }, + { + // server-ws.mjs imports ./httpClientAbortGuard.mjs. In the repo that path is + // the scripts/dev shim re-exporting the shared implementation, but the + // assembled bundle has no src/ tree, so ship the real self-contained + // implementation (no relative imports of its own) under the same file name. + label: "http client abort guard (server-ws.mjs dependency)", + src: ["src", "shared", "utils", "httpClientAbortGuard.mjs"], + dest: ["httpClientAbortGuard.mjs"], + }, { label: "ChatGPT Web Codex MCP tunnel entrypoint", src: ["bin", "chatgpt-web-codex-mcp.mjs"], diff --git a/scripts/build/pack-artifact-policy.ts b/scripts/build/pack-artifact-policy.ts index 12d3896a73..bdaebaffc5 100644 --- a/scripts/build/pack-artifact-policy.ts +++ b/scripts/build/pack-artifact-policy.ts @@ -53,6 +53,8 @@ export const APP_STAGING_ALLOWED_EXACT_PATHS: string[] = [ // server-ws.mjs import (sd_notify helper) — enforced by the closure test // tests/unit/pack-artifact-server-ws-closure.test.ts. "systemd-notify.mjs", + // server-ws.mjs import (process crash guard, #13636) — same closure test. + "httpClientAbortGuard.mjs", "responses-ws-proxy.mjs", "bin/chatgpt-web-codex-mcp.mjs", "scripts/dev/sync-env.mjs", @@ -199,6 +201,7 @@ export const PACK_ARTIFACT_REQUIRED_PATHS: string[] = [ "dist/main-server-timeouts.mjs", // server-ws.mjs import (sd_notify helper) — enforced by the closure test. "dist/systemd-notify.mjs", + "dist/httpClientAbortGuard.mjs", "dist/http-method-guard.cjs", // #5452: regression guard — make check:pack-artifact fail loudly if the TLS // opt-in sidecar (imported by dist/server-ws.mjs) ever vanishes from the tarball. diff --git a/scripts/dev/httpClientAbortGuard.mjs b/scripts/dev/httpClientAbortGuard.mjs index 9fdabf7a61..8c032372d9 100644 --- a/scripts/dev/httpClientAbortGuard.mjs +++ b/scripts/dev/httpClientAbortGuard.mjs @@ -11,6 +11,7 @@ export { isClientAbortError, + isRecoverableUpstreamTimeoutError, shouldSwallowUncaught, attachRequestStreamGuards, installProcessCrashGuard, diff --git a/scripts/dev/standalone-server-ws.mjs b/scripts/dev/standalone-server-ws.mjs index 65fb3ab65a..ec4508f295 100644 --- a/scripts/dev/standalone-server-ws.mjs +++ b/scripts/dev/standalone-server-ws.mjs @@ -9,6 +9,17 @@ import headResponseGuard from "./head-response-guard.cjs"; import { resolveTlsOptions, createServerListener } from "./tls-options.mjs"; import { getMainServerTimeoutConfig } from "./main-server-timeouts.mjs"; import { createSystemdNotifier } from "./systemd-notify.mjs"; +import { installProcessCrashGuard } from "./httpClientAbortGuard.mjs"; + +// Safety net (#12861): this is the actual production entry point (see the +// keepAliveTimeout comment below for why `run-next.mjs`-only fixes don't +// reach real installs). Without this, a client abort OR a recoverable +// upstream-fetch timeout that a retry path already handles (see +// open-sse/utils/directResponseStartTimeout.ts) can surface as an +// unhandledRejection -> uncaughtException and take the whole server down — +// exactly the asymmetry `run-next.mjs` already closed for dev. Benign errors +// are swallowed and logged; genuine bugs still crash loudly. +installProcessCrashGuard(); // systemd sd_notify (Type=notify / WatchdogSec=): this process is the one // whose event loop can freeze (cold /v1/models rebuild), so it must own the diff --git a/scripts/i18n/check-key-completeness.mjs b/scripts/i18n/check-key-completeness.mjs new file mode 100644 index 0000000000..bd9e006c3e --- /dev/null +++ b/scripts/i18n/check-key-completeness.mjs @@ -0,0 +1,132 @@ +#!/usr/bin/env node +/** + * OmniRoute — i18n key COMPLETENESS gate (CI gate, blocking). + * + * Every `src/i18n/messages/.json` must carry exactly the key set of `en.json`: + * no leaf absent, no leaf the source no longer has. A `__MISSING__:` placeholder counts as + * present (the ratio gate judges its content); an ABSENT key is the defect this gate names. + * + * Why the two sibling gates cannot see it (the incident it encodes, 2026-09-15): + * - `check-ui-keys-coverage.mjs` enforces an 80 % floor per locale — 43 absent keys out of + * ~13,000 still reads 99.7 %. + * - `check-new-key-coverage.mjs` judges only the keys a PR ADDS to en.json. A locale batch + * is generated from the en.json of the moment the branch is cut; while its translation + * runs for days the base keeps adding keys, and the batch PR adds none itself — so the + * nine batch-1 catalogs (#13044) landed 43 keys short and the eight batch-2 catalogs + * (#13660) 10 keys short. The home widget test was the first thing that noticed. + * + * This gate is absolute, not diff-based: it compares the tree as it is. + * + * Usage: + * node scripts/i18n/check-key-completeness.mjs # blocking + * node scripts/i18n/check-key-completeness.mjs --warn # report only, exit 0 + * npm run i18n:check-keys + */ + +import { promises as fs } from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(SCRIPT_DIR, "..", ".."); +const MESSAGES_DIR = path.join(ROOT, "src", "i18n", "messages"); +const SOURCE_LOCALE = "en"; + +function isPlainObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +/** Dotted leaf paths of a catalog tree (objects recurse, everything else is a leaf). */ +export function leafPaths(node, prefix = "", out = new Set()) { + if (!isPlainObject(node)) return out; + for (const [key, value] of Object.entries(node)) { + const dotted = prefix ? `${prefix}.${key}` : key; + if (isPlainObject(value)) leafPaths(value, dotted, out); + else out.add(dotted); + } + return out; +} + +/** + * Pure core. `en` is the source catalog, `locales` maps locale code → catalog. Returns one + * entry per locale whose key set differs from the source, sorted by locale, with sorted + * `missing` (in en, absent in the locale) and `extra` (in the locale, gone from en) lists. + * Locales with an identical key set are not listed. + */ +export function findIncompleteLocales({ en, locales }) { + const source = leafPaths(en); + const gaps = []; + for (const locale of Object.keys(locales).sort()) { + const target = leafPaths(locales[locale]); + const missing = [...source].filter((k) => !target.has(k)).sort(); + const extra = [...target].filter((k) => !source.has(k)).sort(); + if (missing.length || extra.length) gaps.push({ locale, missing, extra }); + } + return gaps; +} + +async function readCatalogs() { + const files = (await fs.readdir(MESSAGES_DIR)).filter((f) => f.endsWith(".json")).sort(); + const locales = {}; + let en = null; + for (const file of files) { + const code = file.slice(0, -".json".length); + const parsed = JSON.parse(await fs.readFile(path.join(MESSAGES_DIR, file), "utf8")); + if (code === SOURCE_LOCALE) en = parsed; + else locales[code] = parsed; + } + if (!en) throw new Error(`[i18n-keys] ${SOURCE_LOCALE}.json not found in ${MESSAGES_DIR}`); + return { en, locales }; +} + +function formatReport(gaps, sample = 5) { + const lines = []; + for (const { locale, missing, extra } of gaps) { + const parts = []; + if (missing.length) { + parts.push( + `${missing.length} missing (${missing.slice(0, sample).join(", ")}${missing.length > sample ? ", …" : ""})` + ); + } + if (extra.length) { + parts.push( + `${extra.length} extra (${extra.slice(0, sample).join(", ")}${extra.length > sample ? ", …" : ""})` + ); + } + lines.push(` - ${locale}: ${parts.join("; ")}`); + } + return lines.join("\n"); +} + +async function main() { + const warnOnly = process.argv.includes("--warn"); + const { en, locales } = await readCatalogs(); + const gaps = findIncompleteLocales({ en, locales }); + const total = leafPaths(en).size; + const count = Object.keys(locales).length; + if (gaps.length === 0) { + console.log( + `[i18n-keys] OK — ${count} locales carry all ${total} keys of en.json, none extra.` + ); + return; + } + const missingTotal = gaps.reduce((s, g) => s + g.missing.length, 0); + const extraTotal = gaps.reduce((s, g) => s + g.extra.length, 0); + console.error( + `[i18n-keys] ${warnOnly ? "WARN" : "FAIL"} — ${gaps.length}/${count} locales differ from en.json (${missingTotal} missing, ${extraTotal} extra leaves):` + ); + console.error(formatReport(gaps)); + console.error( + "[i18n-keys] Fix: node scripts/i18n/sync-ui-keys.mjs --locale= --translate-markers (adds the missing keys and translates them); extra keys mean the source dropped them — remove them from the locale." + ); + if (!warnOnly) process.exitCode = 1; +} + +const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href; +if (isDirectRun) { + main().catch((err) => { + console.error(`[i18n-keys] ${err.message}`); + process.exitCode = 1; + }); +} diff --git a/scripts/quality/release-acceptance/closeOracle.mjs b/scripts/quality/release-acceptance/closeOracle.mjs new file mode 100644 index 0000000000..0417c955aa --- /dev/null +++ b/scripts/quality/release-acceptance/closeOracle.mjs @@ -0,0 +1,30 @@ +const CLOSE_RE = + /gh issue close\b|issues\.update\b|state=closed/g; + +const KEYWORD_RE = new RegExp( + String.raw`\b(?:fix(?:es|ed)?|close[sd]?|resolve[sd]?)\s+#(\d+)\b`, + "i" +); + +export function findTrackerCloses(workflowText) { + const hits = []; + const lines = String(workflowText ?? "").split(/\n/); + for (let i = 0; i < lines.length; i++) { + CLOSE_RE.lastIndex = 0; + if (CLOSE_RE.test(lines[i])) { + hits.push({ line: i + 1, text: lines[i].trim() }); + } + CLOSE_RE.lastIndex = 0; + } + return hits; +} + +export function closingKeywordInBody(body, tracker = 12732) { + const re = new RegExp(KEYWORD_RE.source, KEYWORD_RE.flags.includes("g") ? KEYWORD_RE.flags : `${KEYWORD_RE.flags}g`); + const text = String(body ?? ""); + let m; + while ((m = re.exec(text)) !== null) { + if (Number(m[1]) === Number(tracker)) return true; + } + return false; +} diff --git a/scripts/quality/release-acceptance/inventory.mjs b/scripts/quality/release-acceptance/inventory.mjs new file mode 100644 index 0000000000..6145b32164 --- /dev/null +++ b/scripts/quality/release-acceptance/inventory.mjs @@ -0,0 +1,102 @@ +import fs from "node:fs"; +import path from "node:path"; +import { + COLLECTORS, + globToRegExp, +} from "../../check/check-test-discovery.mjs"; + +const UNIT_CI_GLOBS = new Set([ + "tests/unit/*.test.ts", + "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,translator,ui,usage}/**/*.test.ts", + "tests/unit/dashboard/**/*.test.ts", + "tests/unit/serial/**/*.test.ts", + "tests/unit/**/*.test.mjs", +]); + +const INTEGRATION_GLOBS = new Set([ + "tests/integration/*.test.ts", + "tests/integration/combo-matrix/*.test.ts", +]); + +function inScope(collector, scopeSuites) { + const suites = new Set(scopeSuites); + if (suites.has("test:unit:ci") && UNIT_CI_GLOBS.has(collector.glob)) return true; + if (suites.has("test:integration") && INTEGRATION_GLOBS.has(collector.glob)) return true; + if (suites.has("test:vitest") && collector.sources?.includes("vitest.mcp.config.ts")) { + return true; + } + return false; +} + +function walkTestFiles(root = process.cwd()) { + const out = []; + function walk(dir) { + if (!fs.existsSync(dir)) return; + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + if (e.name === "node_modules" || e.name === ".git") continue; + const p = path.join(dir, e.name); + if (e.isDirectory()) walk(p); + else if (/\.(test|spec)\.(ts|tsx|mjs|js)$/.test(e.name)) { + out.push(path.relative(root, p).split(path.sep).join("/")); + } + } + } + walk(path.join(root, "tests")); + walk(path.join(root, "open-sse")); + walk(path.join(root, "src")); + return out; +} + +export function canonicalSet(scopeSuites, collectors = COLLECTORS, files) { + const scoped = collectors.filter((c) => inScope(c, scopeSuites)); + const regexes = scoped.map((c) => globToRegExp(c.glob)); + const discovered = files ?? walkTestFiles(); + return discovered.filter((f) => regexes.some((re) => re.test(f))); +} + +export function knownUnexecuted(scopeSuites, collectors = COLLECTORS, baseline, files) { + const discovered = files ?? walkTestFiles(); + const orphans = baseline?.orphans ?? []; + const outOfScope = collectors.filter((c) => !inScope(c, scopeSuites)); + const collectorsOut = outOfScope.map((c) => { + const re = globToRegExp(c.glob); + const count = discovered.filter((f) => re.test(f)).length; + return { + glob: c.glob, + count, + reason: "collector runner is not a suite of this scope", + }; + }); + return { + orphans: { count: orphans.length, paths: orphans }, + collectors: collectorsOut, + }; +} + +export function inventoryErrors(scopeSuites, collectors, baseline, discoveredFiles) { + const errors = []; + const full = COLLECTORS; + const givenGlobs = new Set(collectors.map((c) => c.glob)); + for (const c of full) { + if (!givenGlobs.has(c.glob)) { + errors.push({ + code: "collector_omitted", + glob: c.glob, + detail: `collector ${c.glob} omitted without known_unexecuted listing`, + }); + } + } + const ku = knownUnexecuted(scopeSuites, collectors, baseline, discoveredFiles); + const knownGlobs = new Set(ku.collectors.map((c) => c.glob)); + const knownOrphans = new Set(ku.orphans.paths); + const scoped = collectors.filter((c) => inScope(c, scopeSuites)); + const regexes = scoped.map((c) => globToRegExp(c.glob)); + for (const f of discoveredFiles ?? []) { + const inCanonical = regexes.some((re) => re.test(f)); + const inKnown = knownOrphans.has(f) || [...knownGlobs].some((g) => globToRegExp(g).test(f)); + if (!inCanonical && !inKnown) { + errors.push({ code: "unmapped_file", path: f, detail: "discovered file belongs to no set" }); + } + } + return errors; +} diff --git a/scripts/quality/release-acceptance/nodeReporter.mjs b/scripts/quality/release-acceptance/nodeReporter.mjs new file mode 100644 index 0000000000..105f16ba38 --- /dev/null +++ b/scripts/quality/release-acceptance/nodeReporter.mjs @@ -0,0 +1,41 @@ +const SUBTEST = /^# Subtest:\s+(\S+)/; +const RESULT = /^(ok|not ok)\s+\d+\s+-\s+(\S+)/; + +export function fromNodeTestTap(tapText, argvFiles) { + const completed = []; + const failed = []; + const seen = new Set(); + const lines = String(tapText ?? "").split(/\r?\n/); + let pending = null; + for (const line of lines) { + const sub = line.match(SUBTEST); + if (sub) { + pending = sub[1]; + continue; + } + const res = line.match(RESULT); + if (res) { + const file = pending; + const ok = res[1] === "ok"; + if (file) { + seen.add(file); + if (!ok) { + if (!failed.includes(file)) failed.push(file); + const i = completed.indexOf(file); + if (i >= 0) completed.splice(i, 1); + } else if (!failed.includes(file) && !completed.includes(file)) { + completed.push(file); + } + } + } + } + const attempted = [...argvFiles]; + const missing = attempted.filter((f) => !seen.has(f)); + return { + completed, + attempted, + missing, + failed, + pass: completed.length > 0 && missing.length === 0 && failed.length === 0, + }; +} diff --git a/scripts/quality/release-acceptance/reduce.mjs b/scripts/quality/release-acceptance/reduce.mjs new file mode 100644 index 0000000000..6e3ddd6157 --- /dev/null +++ b/scripts/quality/release-acceptance/reduce.mjs @@ -0,0 +1,243 @@ +import { gateKey, sameKey } from "./types.mjs"; + +export function classifyDependent(prereqStatus, dependentKey, prereqKey) { + if (prereqStatus === "FAIL") { + return { status: "FAIL", cause: prereqKey }; + } + if (prereqStatus === "INFRA_ERROR") { + return { status: "INFRA_ERROR", cause: prereqKey }; + } + if (prereqStatus == null) { + return { + status: "INFRA_ERROR", + cause: prereqKey, + evidence_error: { + code: "prerequisite_missing", + gate: dependentKey, + detail: `missing prerequisite ${prereqKey.gate_id}`, + }, + }; + } + if (prereqStatus === "SKIPPED") { + return { status: "SKIPPED", cause: prereqKey }; + } + return { status: "RUN", cause: null }; +} + +function requiredSet(plan) { + return plan.required_gates ?? []; +} + +function optionalSet(plan) { + return plan.optional_gates ?? []; +} + +function isRequired(plan, k) { + return requiredSet(plan).some((r) => sameKey(r, k)); +} + +function copies(gates, k) { + return gates.filter((g) => sameKey(gateKey(g), k)); +} + +function copiesByGateId(gates, gateId) { + return gates.filter((g) => g.gate_id === gateId); +} + +function uniqueKeys(keys) { + const out = []; + for (const k of keys) { + if (!out.some((existing) => sameKey(existing, k))) out.push(k); + } + return out; +} + +function keysForGateId(plan, gates, gateId) { + return uniqueKeys([ + ...copiesByGateId(gates, gateId).map((g) => gateKey(g)), + ...requiredSet(plan).filter((k) => k.gate_id === gateId), + ...optionalSet(plan).filter((k) => k.gate_id === gateId), + ]); +} + +function statusOf(gates, k) { + const list = copies(gates, k); + if (list.length === 0) return null; + if (list.some((g) => g.status === "INFRA_ERROR")) return "INFRA_ERROR"; + if (list.some((g) => g.status === "FAIL")) return "FAIL"; + if (list.some((g) => g.status === "SKIPPED")) return "SKIPPED"; + return list[0].status; +} + +function statusOfGateId(gates, gateId) { + const list = copiesByGateId(gates, gateId); + if (list.length === 0) return null; + if (list.some((g) => g.status === "INFRA_ERROR")) return "INFRA_ERROR"; + if (list.some((g) => g.status === "FAIL")) return "FAIL"; + if (list.some((g) => g.status === "SKIPPED")) return "SKIPPED"; + return list[0].status; +} + +function pushEvidenceError(evidence_errors, err) { + if (!err) return; + const already = evidence_errors.some( + (e) => + e.code === err.code && + e.detail === err.detail && + e.gate?.gate_id === err.gate?.gate_id + ); + if (!already) evidence_errors.push(err); +} + +function patchDependent(gates, depKey, classified, prereqKey, evidence_errors, identity) { + const matches = copies(gates, depKey); + const reason = + classified.status === "SKIPPED" ? `classified from ${prereqKey.gate_id}` : undefined; + const exit_code = classified.status === "FAIL" ? 1 : 2; + if (matches.length === 0) { + gates.push({ + gate_id: depKey.gate_id, + suite_id: depKey.suite_id, + shard_index: depKey.shard_index, + shard_total: depKey.shard_total, + tested_sha: identity.tested_sha || "0".repeat(40), + run_id: identity.run_id ?? "0", + run_attempt: identity.run_attempt ?? 1, + command_id: depKey.gate_id, + gate_type: "artifact", + status: classified.status, + cause: classified.cause, + reason, + exit_code, + duration_ms: 0, + evidence: [], + }); + if (classified.evidence_error) pushEvidenceError(evidence_errors, classified.evidence_error); + return true; + } + let changed = false; + for (const existing of matches) { + if ( + existing.status === classified.status && + ((existing.cause == null && classified.cause == null) || + (existing.cause && classified.cause && sameKey(existing.cause, classified.cause))) + ) { + continue; + } + existing.status = classified.status; + existing.cause = classified.cause; + existing.exit_code = exit_code; + if (classified.status === "SKIPPED" && !existing.reason) existing.reason = reason; + changed = true; + } + if (changed && classified.evidence_error) { + pushEvidenceError(evidence_errors, classified.evidence_error); + } + return changed; +} + +function assertAcyclic(deps) { + const visiting = new Set(); + const done = new Set(); + function walk(id) { + if (done.has(id)) return; + if (visiting.has(id)) throw new Error("cyclic prerequisite"); + visiting.add(id); + if (Object.hasOwn(deps, id)) walk(deps[id]); + visiting.delete(id); + done.add(id); + } + for (const id of Object.keys(deps)) walk(id); +} + +export function reduce(plan, records) { + const deps = plan.dependencies ?? {}; + assertAcyclic(deps); + for (const [depId, prereqId] of Object.entries(deps)) { + const requiredDep = requiredSet(plan).some((k) => k.gate_id === depId); + const optionalPrereq = optionalSet(plan).some((k) => k.gate_id === prereqId); + if (requiredDep && optionalPrereq) { + throw new Error("optional prerequisite"); + } + } + + const gates = []; + const evidence_errors = []; + + for (const rec of records) { + const k = gateKey(rec); + const copy = { ...rec, cause: rec.cause ?? null }; + if (copy.status === "SKIPPED" && isRequired(plan, k) && !copy.reason) { + copy.reason = "required skipped"; + } + gates.push(copy); + } + + const identity = plan.identity ?? {}; + const edges = Object.entries(deps); + let changed = true; + let guard = edges.length + 1; + while (changed && guard-- > 0) { + changed = false; + for (const [depId, prereqId] of edges) { + const prereqKey = { gate_id: prereqId, suite_id: null, shard_index: null, shard_total: null }; + let depKeys = keysForGateId(plan, gates, depId); + if (depKeys.length === 0) { + depKeys = [{ gate_id: depId, suite_id: null, shard_index: null, shard_total: null }]; + } + const prereqStatus = statusOfGateId(gates, prereqId); + for (const depKey of depKeys) { + const classified = classifyDependent(prereqStatus, depKey, prereqKey); + if (classified.status === "RUN") continue; + if (patchDependent(gates, depKey, classified, prereqKey, evidence_errors, identity)) { + changed = true; + } + } + } + } + + for (const k of requiredSet(plan)) { + const rec = gates.find((g) => sameKey(gateKey(g), k)); + if (!rec) { + evidence_errors.push({ + code: "missing_record", + gate: k, + detail: `required gate ${k.gate_id} has no record`, + }); + } else if (rec.status === "SKIPPED") { + evidence_errors.push({ + code: "required_skipped", + gate: k, + detail: rec.reason ?? "required gate SKIPPED", + }); + } + } + + const required = requiredSet(plan); + if (required.length === 0) { + evidence_errors.push({ + code: "empty_required_set", + gate: { gate_id: "schema", suite_id: null, shard_index: null, shard_total: null }, + detail: "required_gates is empty", + }); + } + + let verdict = "VERIFIED"; + const hasFail = gates.some( + (g) => g.status === "FAIL" && isRequired(plan, gateKey(g)) && statusOf(gates, gateKey(g)) === "FAIL" + ); + const hasUnverified = + evidence_errors.length > 0 || + gates.some( + (g) => + isRequired(plan, gateKey(g)) && + (g.status === "SKIPPED" || g.status === "INFRA_ERROR") + ); + if (hasFail) verdict = "FAILED"; + else if (hasUnverified) verdict = "UNVERIFIED"; + else if (required.some((k) => !gates.some((g) => sameKey(gateKey(g), k)))) { + verdict = "UNVERIFIED"; + } + + return { verdict, evidence_errors, gates }; +} diff --git a/scripts/quality/release-acceptance/staticAdapter.mjs b/scripts/quality/release-acceptance/staticAdapter.mjs new file mode 100644 index 0000000000..f00a55f44d --- /dev/null +++ b/scripts/quality/release-acceptance/staticAdapter.mjs @@ -0,0 +1,50 @@ +export function adaptCompiler({ commandId, inputDigest, exitCode, diagnostics }) { + const diags = Array.isArray(diagnostics) ? diagnostics : []; + const digest = typeof inputDigest === "string" ? inputDigest : ""; + if (exitCode === 0 && digest.length > 0) { + return { + command_id: commandId, + input_digest: digest, + exit_code: 0, + diagnostics: diags, + status: "PASS", + }; + } + if (exitCode === 0 && digest.length === 0) { + return { + command_id: commandId, + input_digest: digest, + exit_code: 0, + diagnostics: diags, + status: "INFRA_ERROR", + }; + } + return { + command_id: commandId, + input_digest: digest, + exit_code: exitCode, + diagnostics: diags, + status: "FAIL", + }; +} + +export function adaptScript({ commandId, inputDigest, exitCode, stdout }) { + const digest = typeof inputDigest === "string" ? inputDigest : ""; + let parsed = null; + if (typeof stdout === "string" && stdout.trim()) { + try { + parsed = JSON.parse(stdout); + } catch { + parsed = null; + } + } + const diagnostics = parsed ?? { input_digest: digest, exit_code: exitCode, diagnostics: stdout ?? "" }; + const status = exitCode === 0 ? (digest ? "PASS" : "INFRA_ERROR") : "FAIL"; + return { + command_id: commandId, + input_digest: digest, + exit_code: exitCode, + diagnostics, + status, + }; +} diff --git a/scripts/quality/release-acceptance/types.mjs b/scripts/quality/release-acceptance/types.mjs new file mode 100644 index 0000000000..8090da5424 --- /dev/null +++ b/scripts/quality/release-acceptance/types.mjs @@ -0,0 +1,19 @@ +export const STATUSES = Object.freeze(["PASS", "FAIL", "INFRA_ERROR", "SKIPPED"]); +export const VERDICTS = Object.freeze(["VERIFIED", "FAILED", "UNVERIFIED"]); + +export function gateKey(rec) { + return { + gate_id: rec.gate_id, + suite_id: rec.suite_id ?? null, + shard_index: rec.shard_index ?? null, + shard_total: rec.shard_total ?? null, + }; +} + +export function keyId(k) { + return `${k.gate_id}\0${k.suite_id ?? ""}\0${k.shard_index ?? ""}\0${k.shard_total ?? ""}`; +} + +export function sameKey(a, b) { + return keyId(a) === keyId(b); +} diff --git a/scripts/quality/validate-release-acceptance.mjs b/scripts/quality/validate-release-acceptance.mjs new file mode 100644 index 0000000000..9a45ac95e5 --- /dev/null +++ b/scripts/quality/validate-release-acceptance.mjs @@ -0,0 +1,81 @@ +#!/usr/bin/env node +import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import Ajv from "ajv"; +import { reduce } from "./release-acceptance/reduce.mjs"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); + +function loadJson(p) { + return JSON.parse(readFileSync(p, "utf8")); +} + +export function exitFor(verdict) { + if (verdict === "VERIFIED") return 0; + if (verdict === "FAILED") return 1; + return 2; +} + +export function reduceManifests(plan, manifests) { + const records = []; + for (const m of manifests) { + if (Array.isArray(m.gates)) records.push(...m.gates); + else records.push(m); + } + return reduce(plan, records); +} + +export function validateReport(report, schema) { + const ajv = new Ajv({ allErrors: true, strict: false }); + const validate = ajv.compile(schema); + return { ok: validate(report), errors: validate.errors }; +} + +function parseArgs(argv) { + const out = { plan: null, manifests: null, out: join(ROOT, "release-acceptance-report.json") }; + for (let i = 2; i < argv.length; i++) { + if (argv[i] === "--plan") out.plan = argv[++i]; + else if (argv[i] === "--manifests") out.manifests = argv[++i]; + else if (argv[i] === "--out") out.out = argv[++i]; + } + return out; +} + +export async function main(argv = process.argv) { + const args = parseArgs(argv); + const plan = loadJson(args.plan); + const schema = loadJson(join(ROOT, "config/quality/release-acceptance.schema.json")); + const files = readdirSync(args.manifests) + .filter((f) => f.endsWith(".json")) + .map((f) => loadJson(join(args.manifests, f))); + const reduced = reduceManifests(plan, files); + const report = { + schema_version: 1, + identity: plan.identity, + required_gates: plan.required_gates ?? [], + gates: reduced.gates, + evidence_errors: reduced.evidence_errors, + verdict: reduced.verdict, + artifact: plan.artifact ?? null, + }; + const { ok, errors } = validateReport(report, schema); + if (!ok) { + if (report.verdict !== "FAILED") report.verdict = "UNVERIFIED"; + const gate = + Array.isArray(plan.required_gates) && plan.required_gates.length > 0 + ? plan.required_gates[0] + : { gate_id: "schema", suite_id: null, shard_index: null, shard_total: null }; + report.evidence_errors = [ + ...(report.evidence_errors ?? []), + { code: "schema_invalid", gate, detail: JSON.stringify(errors) }, + ]; + } + mkdirSync(dirname(args.out), { recursive: true }); + writeFileSync(args.out, JSON.stringify(report, null, 2) + "\n"); + return exitFor(report.verdict); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + main().then((code) => process.exit(code)); +} diff --git a/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx b/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx index c82170e256..2bd3a7ba5d 100644 --- a/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx @@ -934,6 +934,7 @@ export default function SystemStorageTab() { ["mcpAudit", t("retentionMcpAudit"), 30], ["a2aEvents", t("retentionA2aEvents"), 30], ["callLogs", t("retentionCallLogs"), 30], + ["conversationTurnNodes", t("retentionConversationTurnNodes"), 30], ["usageHistory", t("retentionUsageHistory"), 30], ["memoryEntries", t("retentionMemoryEntries"), 30], ["xpAuditLog", t("retentionXpAuditLog"), 30], diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index c4142a4768..558e3c64f6 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -8,6 +8,7 @@ import { cookies } from "next/headers"; import { ensurePersistentManagementPasswordHash, getStoredManagementPassword, + isKnownInsecureManagementPassword, verifyManagementPassword, } from "@/lib/auth/managementPassword"; import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags"; @@ -153,6 +154,41 @@ export async function POST(request: NextRequest) { const isValid = await verifyManagementPassword(password, storedHash); + // #8336: tag the origin scope so the audit view can distinguish a mistyped + // password from the host itself / the LAN (loopback / private) from a + // genuinely external attempt, instead of every failure reading as intrusion. + // Computed once and reused below for the #13679 insecure-default gate. + const sourceScope = classifyIpScope(auditContext.ipAddress); + + // #13679 (PR D, item #5): the well-known INITIAL_PASSWORD placeholder shipped + // in .env.example / contrib/podman/omniroute.container / docker deploy + // manifests is a public, guessable credential. Anyone who knows it (i.e. + // everyone) can otherwise sign in from anywhere the dashboard is reachable. + // `ensurePersistentManagementPasswordHash()` already warns loudly on boot, + // but that is a log line, not a control — refuse the login here instead + // whenever it matches AND the request is not loopback, forcing the operator + // to rotate the password from a trusted local console first. + if (isValid && isKnownInsecureManagementPassword(password) && sourceScope !== "loopback") { + logAuditEvent({ + action: "auth.login.insecure_default_blocked", + actor: "anonymous", + target: "dashboard-auth", + resourceType: "auth_session", + status: "failed", + ipAddress: auditContext.ipAddress || undefined, + requestId: auditContext.requestId, + metadata: { reason: "well_known_default_password_non_loopback", sourceScope }, + }); + return NextResponse.json( + { + error: + "The management password is still set to the well-known default. " + + "Log in from localhost and change it before signing in remotely.", + }, + { status: 403 } + ); + } + if (isValid) { const forceSecureCookie = process.env.AUTH_COOKIE_SECURE === "true"; const forwardedProtoHeader = request.headers.get("x-forwarded-proto") || ""; @@ -197,11 +233,6 @@ export async function POST(request: NextRequest) { const failureDecision = recordLoginFailure(clientIp, { enabled: bruteForceEnabled }); - // #8336: tag the origin scope so the audit view can distinguish a mistyped - // password from the host itself / the LAN (loopback / private) from a - // genuinely external attempt, instead of every failure reading as intrusion. - const sourceScope = classifyIpScope(auditContext.ipAddress); - logAuditEvent({ action: "auth.login.failed", actor: "anonymous", diff --git a/src/app/api/mcp/restart/route.ts b/src/app/api/mcp/restart/route.ts new file mode 100644 index 0000000000..b675f04752 --- /dev/null +++ b/src/app/api/mcp/restart/route.ts @@ -0,0 +1,53 @@ +import { NextResponse } from "next/server"; +import { getMcpHttpStatus, shutdownMcpHttp } from "@omniroute/open-sse/mcp-server/httpTransport"; +import { getCachedSettings } from "@/lib/db/settings"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; + +/** + * POST /api/mcp/restart — resets the in-process MCP HTTP/SSE transport so the + * next MCP request re-initializes cleanly (mirrors the lazy-start design + * documented at the top of open-sse/mcp-server/httpTransport.ts). There is no + * external MCP process to restart the way `/api/restart` self-restarts the + * whole server — this only tears down active SSE/Streamable HTTP sessions. + * + * Fixes #13012: the CLI's `omniroute mcp restart` POSTs here but the route + * never existed, so every call 404d. + */ +export async function POST(request: Request) { + const authError = await requireManagementAuth(request, { acceptMcpConnectScope: true }); + if (authError) return authError; + + const settings = await getCachedSettings(); + const mcpEnabled = !!settings.mcpEnabled; + const mcpTransport = (settings.mcpTransport as string) || "stdio"; + + if (!mcpEnabled) { + return NextResponse.json( + { + error: "MCP is disabled; enable it first (`omniroute mcp enable`).", + }, + { status: 409 } + ); + } + + if (mcpTransport === "stdio") { + return NextResponse.json( + { + error: + "MCP restart is not supported for the stdio transport — stdio clients spawn their " + + "own subprocess with no in-process handle to restart. Switch to sse/streamable-http " + + "(`omniroute mcp enable --transport sse`) or restart the client instead.", + }, + { status: 501 } + ); + } + + shutdownMcpHttp(); + + return NextResponse.json({ + status: "restarted", + enabled: mcpEnabled, + transport: mcpTransport, + httpTransport: getMcpHttpStatus(), + }); +} diff --git a/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts b/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts index 72946e44c5..c7dff93bd9 100644 --- a/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts +++ b/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts @@ -26,6 +26,7 @@ import { filterAlibabaFreeEligibleModels } from "@omniroute/open-sse/services/al import { shouldUseLiveAlibabaFreeModelDiscovery } from "@omniroute/open-sse/services/alibabaFreeTier.ts"; import { isDashscopeTextModelId } from "@omniroute/open-sse/services/dashscopeTextModels.ts"; import { extractZaiToken } from "@omniroute/open-sse/services/zaiWebCredentials.ts"; +import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags"; import { normalizeOpenAiLikeModelsResponse } from "./normalizers"; const QWEN_CLOUD_TEXT_MODEL_IDS = new Set(QWEN_CLOUD_TEXT_MODELS.map((model) => model.id)); @@ -130,11 +131,9 @@ export type ProviderModelsConfigEntry = { export function assembleProviderModelsHeaders( config: ProviderModelsConfigEntry, token: string, - context?: ProviderModelsHeaderContext, + context?: ProviderModelsHeaderContext ): Record { - const headers = config.buildHeaders - ? config.buildHeaders(token, context) - : { ...config.headers }; + const headers = config.buildHeaders ? config.buildHeaders(token, context) : { ...config.headers }; if (!config.buildHeaders && config.authHeader && !config.authQuery) { headers[config.authHeader] = (config.authPrefix || "") + token; } @@ -396,6 +395,35 @@ const KIMI_CODING_MODELS_CONFIG: ProviderModelsConfigEntry = { parseResponse: parseKimiCodingModels, }; +// Also used, behind the XAI_OAUTH_LIVE_MODEL_DISCOVERY flag, to fetch a live +// catalog for xai-oauth (see getXaiOauthLiveModelsConfig below). Whether x.ai +// accepts an OAuth bearer at this endpoint is unverified — that is why +// xai-oauth is not registered in PROVIDER_MODELS_CONFIG below and stays on +// its frozen static seed (open-sse/config/providers/registry/xai/index.ts) +// unless the flag is explicitly turned on. +export const XAI_MODELS_CONFIG: ProviderModelsConfigEntry = { + url: "https://api.x.ai/v1/models", + method: "GET", + headers: { "Content-Type": "application/json" }, + authHeader: "Authorization", + authPrefix: "Bearer ", + parseResponse: (data) => data.data || data.models || [], +}; + +/** + * Resolve the live-discovery config for xai-oauth when the + * XAI_OAUTH_LIVE_MODEL_DISCOVERY flag is on, or `undefined` when it is off + * (or its resolution throws) so the caller falls back to the frozen static + * seed — the flag defaults to "false" and fails closed on any error. + */ +export function getXaiOauthLiveModelsConfig(): ProviderModelsConfigEntry | undefined { + try { + return isFeatureFlagEnabled("XAI_OAUTH_LIVE_MODEL_DISCOVERY") ? XAI_MODELS_CONFIG : undefined; + } catch { + return undefined; + } +} + // Provider models endpoints configuration export const PROVIDER_MODELS_CONFIG: Record = { alibaba: ALIBABA_MODEL_STUDIO_MODELS_CONFIG, @@ -592,14 +620,12 @@ export const PROVIDER_MODELS_CONFIG: Record = authPrefix: "Bearer ", parseResponse: (data) => data.data || data.models || [], }, - xai: { - url: "https://api.x.ai/v1/models", - method: "GET", - headers: { "Content-Type": "application/json" }, - authHeader: "Authorization", - authPrefix: "Bearer ", - parseResponse: (data) => data.data || data.models || [], - }, + xai: XAI_MODELS_CONFIG, + // xai-oauth intentionally NOT registered here: it stays on the frozen + // static seed unless XAI_OAUTH_LIVE_MODEL_DISCOVERY is on (see + // getXaiOauthLiveModelsConfig above) — keeping this map's keys in lockstep + // with HARDCODED_MODELS_CONFIG_IDS (tests/unit/discovery-class.test.ts) + // means the flag gate has to live at the lookup call site, not here. mistral: { url: "https://api.mistral.ai/v1/models", method: "GET", diff --git a/src/app/api/providers/[id]/models/discovery/providerSets.ts b/src/app/api/providers/[id]/models/discovery/providerSets.ts index 27bf1415cd..ef4b4b1479 100644 --- a/src/app/api/providers/[id]/models/discovery/providerSets.ts +++ b/src/app/api/providers/[id]/models/discovery/providerSets.ts @@ -105,6 +105,8 @@ export const NAMED_OPENAI_STYLE_PROVIDERS = new Set([ // Without this, sync-models serves the static registry seed and CN // connections never discover 2.5/3.0 Flash. "agnes", + // Agnes CN /v1/models is not the intl catalog; this discovers that host only. + "agnes-cn", ]); export function isNamedOpenAIStyleProvider(provider: string): boolean { diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index c09a2636b7..36dde02495 100755 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -120,6 +120,7 @@ import { buildStaleEncryptionKeyResponse } from "./staleEncryptionGuard"; import { type ProviderModelsConfigEntry, assembleProviderModelsHeaders, + getXaiOauthLiveModelsConfig, PROVIDER_MODELS_CONFIG, } from "./discovery/providerModelsConfig"; import { @@ -616,9 +617,7 @@ export async function GET( try { const discovery = await discoverMaxaiModels({ providerSpecificData: connection.providerSpecificData as - | Record - | null - | undefined, + Record | null | undefined, accessToken: apiKey || accessToken, fetchImpl: (url, init) => safeOutboundFetch(url, { @@ -2085,10 +2084,12 @@ export async function GET( } } + const xaiOauthLiveConfig = provider === "xai-oauth" ? getXaiOauthLiveModelsConfig() : undefined; const config = - provider in PROVIDER_MODELS_CONFIG + xaiOauthLiveConfig ?? + (provider in PROVIDER_MODELS_CONFIG ? PROVIDER_MODELS_CONFIG[provider as keyof typeof PROVIDER_MODELS_CONFIG] - : deriveConfigFromRegistryModelsUrl(provider); + : deriveConfigFromRegistryModelsUrl(provider)); if (provider === "codex") { // Auto-merge live/GitHub/local (future-proof discovery), then apply explicit // denylist filters (e.g. drop GPT-5.4 family). Do not gate remote-only IDs. diff --git a/src/app/api/providers/[id]/test/route.ts b/src/app/api/providers/[id]/test/route.ts index e83ce61e09..7d72f28098 100644 --- a/src/app/api/providers/[id]/test/route.ts +++ b/src/app/api/providers/[id]/test/route.ts @@ -239,6 +239,16 @@ function isTokenExpired(connection: any) { return expiresAt <= Date.now() + buffer; } +// #12958: GitLab's own `direct_access` 403 JSON body (e.g. `{"error":"insufficient_scope"}`) +// is safe operator-facing diagnostic text — it is not a stack trace and does not echo the +// token — but is capped and stripped of control characters defensively before it reaches +// the stored/surfaced error message, per docs/security/ERROR_SANITIZATION.md. +function sanitizeUpstreamBodyText(bodyText: string): string { + const collapsed = bodyText.replace(/[\r\n\t�-]+/g, " ").trim(); + const MAX_LENGTH = 300; + return collapsed.length > MAX_LENGTH ? `${collapsed.slice(0, MAX_LENGTH)}…` : collapsed; +} + /** * #10365 / #10499: the real chat path (open-sse/executors/gitlab.ts) treats a rejected * `direct_access` exchange (401) or an explicitly disabled direct-connections tenant @@ -644,9 +654,14 @@ export async function testOAuthConnection( }; } + // #12958: `res.text()` can only be read once — capture it here in the outer + // function scope so the generic bodyText selection below (which used to call + // `res.text()` a second time and silently get "" back, discarding the real + // GitLab error) can reuse the same string instead of re-reading a drained body. + let gitlabDuoDirectAccessBodyText: string | null = null; if (connection.provider === "gitlab-duo") { - const gitlabText = await res.text(); - if (shouldFallbackToPublicCodeSuggestions(res.status, gitlabText)) { + gitlabDuoDirectAccessBodyText = await res.text(); + if (shouldFallbackToPublicCodeSuggestions(res.status, gitlabDuoDirectAccessBodyText)) { const fallbackOk = await probeGitLabDuoPublicFallback(connection, accessToken, timeoutMs); if (fallbackOk) { return { @@ -788,14 +803,26 @@ export async function testOAuthConnection( // revoked token. (The body is unread here for non-gitlab providers; the guard keeps // it safe if it was already consumed.) antigravity/agy read any failure body so a // geo-blocked egress location is labeled with an actionable message instead of a - // generic "API returned 400". + // generic "API returned 400". gitlab-duo already consumed the body above (`res.text()` + // is single-read) — reuse it instead of re-reading a drained stream (#12958). const bodyText = - res.status === 401 || - res.status === 403 || - connection.provider === "antigravity" || - connection.provider === "agy" - ? await res.text().catch(() => "") - : ""; + connection.provider === "gitlab-duo" + ? (gitlabDuoDirectAccessBodyText ?? "") + : res.status === 401 || + res.status === 403 || + connection.provider === "antigravity" || + connection.provider === "agy" + ? await res.text().catch(() => "") + : ""; + // #12958: surface the real upstream body for a gitlab-duo 403 that also fails the + // public-fallback probe, instead of a generic "Access denied" — the operator needs + // to tell an entitlement/scope failure apart from an instance-config or revoked-token + // one. Trimmed/truncated per docs/security/ERROR_SANITIZATION.md (no stack traces are + // involved; this is GitLab's own JSON error body, capped defensively). + const gitlabDuoAccessDeniedMessage = + connection.provider === "gitlab-duo" && res.status === 403 + ? `Access denied: ${sanitizeUpstreamBodyText(bodyText)}` + : "Access denied"; const error = isGeoBlockedError(bodyText) ? "Egress location blocked by Google (User location is not supported). The Cloud Code API is not offered from this server's proxy exit region — route antigravity/agy through a proxy in a supported region (e.g. US/EU) or use a different provider. This is NOT an account problem." : isAccountDeactivatedMessage(bodyText) @@ -803,7 +830,7 @@ export async function testOAuthConnection( : res.status === 401 ? "Token invalid or revoked" : res.status === 403 - ? "Access denied" + ? gitlabDuoAccessDeniedMessage : `API returned ${res.status}`; return { diff --git a/src/app/api/v1/audio/speech/route.ts b/src/app/api/v1/audio/speech/route.ts index 686a5abd90..55887bb1ad 100644 --- a/src/app/api/v1/audio/speech/route.ts +++ b/src/app/api/v1/audio/speech/route.ts @@ -18,6 +18,7 @@ import { import { attachOmniRouteMetaToResponse } from "@/domain/omnirouteResponseMeta"; import { calculateModalCost } from "@/lib/usage/costCalculator"; import { generateRequestId } from "@/shared/utils/requestId"; +import { saveCallLog } from "@/lib/usageDb"; /** * Handle CORS preflight @@ -102,6 +103,12 @@ async function postHandler(request, context) { resolvedProvider: providerConfig, resolvedModel, }); + + const connectionId = (credentials as { connectionId?: string } | null)?.connectionId || undefined; + const logModel = `${provider}/${resolvedModel || body.model}`; + const apiKeyId = policy.apiKeyInfo?.id || undefined; + const apiKeyName = policy.apiKeyInfo?.name || undefined; + if (response?.ok) { await clearRecoveredProviderState(credentials); // TTS is billed per input character; attach cost telemetry without @@ -117,6 +124,34 @@ async function postHandler(request, context) { latencyMs: Date.now() - startTime, requestId: generateRequestId(), }); + saveCallLog({ + method: "POST", + path: "/v1/audio/speech", + status: 200, + model: logModel, + provider, + connectionId, + duration: Date.now() - startTime, + apiKeyId, + apiKeyName, + }).catch(() => {}); + } else if (response) { + const errorText = await response + .clone() + .text() + .catch(() => ""); + saveCallLog({ + method: "POST", + path: "/v1/audio/speech", + status: response.status, + model: logModel, + provider, + connectionId, + duration: Date.now() - startTime, + error: errorText.slice(0, 500), + apiKeyId, + apiKeyName, + }).catch(() => {}); } return response; } diff --git a/src/app/api/v1/audio/transcriptions/route.ts b/src/app/api/v1/audio/transcriptions/route.ts index 0a8ce034ae..a20c8c78eb 100644 --- a/src/app/api/v1/audio/transcriptions/route.ts +++ b/src/app/api/v1/audio/transcriptions/route.ts @@ -28,6 +28,31 @@ import { getComboByName, getCombos } from "@/lib/db/combos"; import { getDatabaseSettings } from "@/lib/db/databaseSettings"; import { handleComboChat } from "@omniroute/open-sse/services/combo.ts"; import { log } from "@omniroute/open-sse/utils/logger.ts"; +import { saveCallLog } from "@/lib/usageDb"; + +/** + * Best-effort peek at a successful transcription response for upstream duration + * usage (e.g. Scaleway's `usage: {type:"duration", seconds:N}`) so it is at least + * visible/auditable on the call_logs row even before a per-second cost rule + * consumes it (#13544). Never touches the original response body/stream — reads + * a clone, and any parse failure is swallowed so logging never blocks the reply. + */ +export async function peekDurationUsage( + response: Response +): Promise<{ type?: string; seconds?: number } | undefined> { + try { + const contentType = response.headers.get("content-type") || ""; + if (!contentType.includes("application/json")) return undefined; + const parsed = (await response.clone().json()) as { usage?: unknown } | null; + const usage = parsed && typeof parsed === "object" ? parsed.usage : null; + if (usage && typeof usage === "object" && (usage as { type?: unknown }).type === "duration") { + return usage as { type?: string; seconds?: number }; + } + } catch { + // Best-effort only — the transcription response itself already succeeded. + } + return undefined; +} /** * Copy a multipart body, swapping only the `model` field. Combo fan-out needs one @@ -63,7 +88,9 @@ export async function OPTIONS() { async function transcribeWithModel( formData: FormData, modelStr: string, - startTime: number + startTime: number, + apiKeyId?: string | null, + apiKeyName?: string | null ): Promise { // Provider nodes eligible for transcription: this route's own audio type plus // general chat/responses gateways. Remote hosts are opt-in (default OFF). @@ -138,10 +165,16 @@ async function transcribeWithModel( resolvedProvider: providerConfig, resolvedModel, }); + + const connectionId = (credentials as { connectionId?: string } | null)?.connectionId || undefined; + const logModel = `${provider}/${resolvedModel}`; + if (response?.ok) { await clearRecoveredProviderState(credentials); - // No text body / playback duration available from the multipart upload, so - // per-second pricing cannot be applied → cost 0 (ADD-only headers, body intact). + const durationUsage = await peekDurationUsage(response); + // No per-second pricing rule exists yet for transcription duration → cost 0 + // (ADD-only headers, body intact). The upstream usage is still persisted on + // the call_logs row below so it is auditable ahead of that pricing rule. response = attachOmniRouteMetaToResponse(response, { provider, model: resolvedModel, @@ -149,6 +182,35 @@ async function transcribeWithModel( latencyMs: Date.now() - startTime, requestId: generateRequestId(), }); + saveCallLog({ + method: "POST", + path: "/v1/audio/transcriptions", + status: 200, + model: logModel, + provider, + connectionId, + duration: Date.now() - startTime, + responseBody: durationUsage ? { usage: durationUsage } : undefined, + apiKeyId: apiKeyId || undefined, + apiKeyName: apiKeyName || undefined, + }).catch(() => {}); + } else if (response) { + const errorText = await response + .clone() + .text() + .catch(() => ""); + saveCallLog({ + method: "POST", + path: "/v1/audio/transcriptions", + status: response.status, + model: logModel, + provider, + connectionId, + duration: Date.now() - startTime, + error: errorText.slice(0, 500), + apiKeyId: apiKeyId || undefined, + apiKeyName: apiKeyName || undefined, + }).catch(() => {}); } return response; } @@ -177,6 +239,12 @@ export async function POST(request) { const policy = await enforceApiKeyPolicy(request, modelStr); if (policy.rejection) return policy.rejection; + // Forwarded into transcribeWithModel() (and combo fan-out below) so the + // resulting call_logs row is attributable to the API key that made the + // request, matching the pattern every other proxied route follows (#13544). + const apiKeyId = policy.apiKeyInfo?.id || null; + const apiKeyName = policy.apiKeyInfo?.name || null; + // A bare name (no "/") may be a combo. /v1/models advertises combos, and chat and // embeddings both resolve them — resolving here too keeps the catalog honest and // frees callers from hardcoding a provider's internal model id. @@ -197,7 +265,13 @@ export async function POST(request) { body: { model: modelStr } as any, combo: combo as any, handleSingleModel: async (_reqBody: any, targetModelStr: string) => - transcribeWithModel(withModel(formData, targetModelStr), targetModelStr, startTime), + transcribeWithModel( + withModel(formData, targetModelStr), + targetModelStr, + startTime, + apiKeyId, + apiKeyName + ), isModelAvailable: undefined, log, settings, @@ -211,5 +285,5 @@ export async function POST(request) { } } - return transcribeWithModel(formData, modelStr, startTime); + return transcribeWithModel(formData, modelStr, startTime, apiKeyId, apiKeyName); } diff --git a/src/app/api/v1/audio/translations/route.ts b/src/app/api/v1/audio/translations/route.ts index 65c45d0268..ae1d79a210 100644 --- a/src/app/api/v1/audio/translations/route.ts +++ b/src/app/api/v1/audio/translations/route.ts @@ -23,6 +23,7 @@ import { getComboByName, getCombos } from "@/lib/db/combos"; import { getDatabaseSettings } from "@/lib/db/databaseSettings"; import { handleComboChat } from "@omniroute/open-sse/services/combo.ts"; import { log } from "@omniroute/open-sse/utils/logger.ts"; +import { saveCallLog } from "@/lib/usageDb"; /** * Copy a multipart body, swapping only the `model` field. Combo fan-out needs one @@ -58,7 +59,9 @@ export async function OPTIONS() { async function translateWithModel( formData: FormData, modelStr: string, - startTime: number + startTime: number, + apiKeyId?: string | null, + apiKeyName?: string | null ): Promise { // Translation is served by the transcription-capable nodes (Whisper-style // endpoints expose both), plus general chat/responses gateways. Remote hosts are @@ -101,6 +104,10 @@ async function translateWithModel( resolvedProvider: providerConfig, resolvedModel, }); + + const connectionId = (credentials as { connectionId?: string } | null)?.connectionId || undefined; + const logModel = `${provider}/${resolvedModel}`; + if (response?.ok) { await clearRecoveredProviderState(credentials); // No text body / playback duration available from the multipart upload, so @@ -112,6 +119,34 @@ async function translateWithModel( latencyMs: Date.now() - startTime, requestId: generateRequestId(), }); + saveCallLog({ + method: "POST", + path: "/v1/audio/translations", + status: 200, + model: logModel, + provider, + connectionId, + duration: Date.now() - startTime, + apiKeyId: apiKeyId || undefined, + apiKeyName: apiKeyName || undefined, + }).catch(() => {}); + } else if (response) { + const errorText = await response + .clone() + .text() + .catch(() => ""); + saveCallLog({ + method: "POST", + path: "/v1/audio/translations", + status: response.status, + model: logModel, + provider, + connectionId, + duration: Date.now() - startTime, + error: errorText.slice(0, 500), + apiKeyId: apiKeyId || undefined, + apiKeyName: apiKeyName || undefined, + }).catch(() => {}); } return response; } @@ -142,6 +177,12 @@ export async function POST(request) { const policy = await enforceApiKeyPolicy(request, modelStr); if (policy.rejection) return policy.rejection; + // Forwarded into translateWithModel() (and combo fan-out below) so the + // resulting call_logs row is attributable to the API key that made the + // request, matching the pattern every other proxied route follows (#13544). + const apiKeyId = policy.apiKeyInfo?.id || null; + const apiKeyName = policy.apiKeyInfo?.name || null; + // A bare name (no "/") may be a combo. /v1/models advertises combos, and chat, // embeddings and the sibling /v1/audio/transcriptions all resolve them — // resolving here too keeps the catalog honest and frees callers from hardcoding @@ -163,7 +204,13 @@ export async function POST(request) { body: { model: modelStr } as any, combo: combo as any, handleSingleModel: async (_reqBody: any, targetModelStr: string) => - translateWithModel(withModel(formData, targetModelStr), targetModelStr, startTime), + translateWithModel( + withModel(formData, targetModelStr), + targetModelStr, + startTime, + apiKeyId, + apiKeyName + ), isModelAvailable: undefined, log, settings, @@ -177,5 +224,5 @@ export async function POST(request) { } } - return translateWithModel(formData, modelStr, startTime); + return translateWithModel(formData, modelStr, startTime, apiKeyId, apiKeyName); } diff --git a/src/app/api/v1/batches/delete-completed/route.ts b/src/app/api/v1/batches/delete-completed/route.ts index 5e095fc845..4cbb21d530 100644 --- a/src/app/api/v1/batches/delete-completed/route.ts +++ b/src/app/api/v1/batches/delete-completed/route.ts @@ -95,6 +95,7 @@ export async function DELETE(request: Request) { apiKeyId: scope.apiKeyId, deletedBatches: result.deletedBatches, deletedFiles: result.deletedFiles, + hasMore: result.hasMore, }; // A bulk delete is an audit event, not routine chatter: an instance-wide sweep // and any key-scoped sweep that actually removed rows log at warn so the trail @@ -109,7 +110,12 @@ export async function DELETE(request: Request) { } return NextResponse.json( - { deleted: true, deletedBatches: result.deletedBatches, deletedFiles: result.deletedFiles }, + { + deleted: true, + deletedBatches: result.deletedBatches, + deletedFiles: result.deletedFiles, + hasMore: result.hasMore, + }, { headers: CORS_HEADERS } ); } diff --git a/src/i18n/messages/am.json b/src/i18n/messages/am.json index af92b74156..f34bb6e54d 100644 --- a/src/i18n/messages/am.json +++ b/src/i18n/messages/am.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "የፕሮክሲ ፑሎች እና የ opencode የየመለያው ዙር ምርጫ፣ አሁን ያልተሳካን ፕሮክሲ (ውድቅ የተደረገ TCP ሙከራ ወይም በእሱ በኩል የተቀበለ 429) በድጋሚ እንዳያቀርቡ ያደርጋሉ፤ ይህም በእያንዳንዱ ድግግሞሽ እስከ ከፍተኛው ገደብ ድረስ በእጥፍ ለሚጨምር የየሂደቱ ጊዜ ነው። ምንም የፕሮክሲ ሁኔታ አይጻፍም፤ እያንዳንዱ እጩ ወደ ጎን ሲቀመጥ ምርጫው አይለወጥም። በነባሪ ጠፍቷል፦ የምርጫው ቅደም ተከተል ከተለመደው ዙር ምርጫ ጋር በትክክል አንድ ነው።", "featureFlagProxyPoolEgressObservationDescription": "በዳሽቦርዱ ውስጥ ከፕሮክሲ ፑል ሥር፣ ባለፉት 24 ሰዓታት ምን ያህል የታዩ የመውጫ IPዎች አባላቱን እንዳገለገሉ፣ ምን ያህል ግንኙነቶች እንደተጠቀሙባቸው እና ከአንድ IP ጀርባ የታየውን ከፍተኛ ቁጥር አሳይ። ለንባብ ብቻ ነው፣ ከፕሮክሲ ሎጉ ይሰላል፣ ለማዞሪያም ፈጽሞ ጥቅም ላይ አይውልም። በነባሪ ጠፍቷል፦ የፑሉ አርታዒ አይለወጥም፣ የምልከታ መስመሩም null ይመልሳል።", "featureFlagProxyHealthBlockedResetsStreakDescription": "በፕሮክሲ ጤና ፍተሻ ውስጥ፣ ዒላማው ውድቅ ያደረገው ሙከራ (401/403/429፦ ፕሮክሲው አስተላልፏል፣ መዳረሻው ግን ይህን የመውጫ IP ውድቅ አድርጓል) ልክ እንደተሳካ ሙከራ የፕሮክሲውን ተከታታይ የውድቀት ቆጠራ ዳግም እንዲጀምር ፍቀድ። በነባሪ ጠፍቷል፦ ውድቅ መደረጉ ገለልተኛ ሆኖ ይቆያል እና ቆጠራውን እንዳለ ያቆያል። 5xx በሁለቱም ሁኔታ የማያረጋግጥ ሆኖ ይቆያል፣ እና ውድቅ መደረግ ፕሮክሲን ፈጽሞ አያስወግድም፣ አያሰናክልም ወይም ዳግም አያነቃም።", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "መነሻ", "dashboard": "ዳሽቦርድ", @@ -6125,6 +6126,7 @@ "agentrouter": "በhttps://agentrouter.org/register የ$200 ነፃ ክሬዲት ያግኙ — የክሬዲት ካርድ አያስፈልግም።", "unorouter": "በhttps://unorouter.ai የAPI ቁልፍ ይፍጠሩ፣ ከዚያም እዚህ እንደ Bearer token ይለጥፉት።", "agnes": "የAPI ቁልፍን ከagnes-ai.com ያግኙ", + "agnes-cn": "__MISSING__:Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "ነፃው ደረጃ ቆሟል (2026) — AI/ML API አሁን በአጠቃቀም መጠን ብቻ የሚከፈል ነው (ቢያንስ $20 መሙላት)፤ ተደጋጋሚ ነፃ ክሬዲት የለም።", "ai21": "ሲመዘገቡ $10 የሙከራ ክሬዲት (ለ3 ወራት የሚሰራ)፣ የክሬዲት ካርድ አያስፈልግም", "alibaba": "Alibabaን በAPI ቁልፍ ያገናኙ።", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "የMCP ኦዲት (ቀናት)", "retentionA2aEvents": "የA2A ክስተቶች (ቀናት)", "retentionCallLogs": "የጥሪ ምዝግቦች (ቀናት)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "የአጠቃቀም ታሪክ (ቀናት)", "retentionMemoryEntries": "የማህደረ ትውስታ ግቤቶች (ቀናት)", "retentionXpAuditLog": "የXP ኦዲት ምዝግብ (ቀናት)", diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 1d7e80a0c0..c673887217 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "تمنع مجموعات الوكلاء والتناوب لكل حساب في opencode إعادة استخدام وكيل فشل للتو (رفض فحص TCP، أو تم تلقي 429 من خلاله) لمدة خاصة بكل عملية تتضاعف مع كل تكرار، حتى حد أقصى. لا تُكتب أي حالة للوكيل؛ وعند استبعاد جميع المرشحين، يظل الاختيار دون تغيير. معطّل افتراضيًا: ترتيب الاختيار هو بالضبط التناوب العادي.", "featureFlagProxyPoolEgressObservationDescription": "اعرض، أسفل مجموعة وكلاء في لوحة المعلومات، عدد عناوين IP الصادرة المرصودة التي خدمت أعضاءها خلال آخر 24 ساعة، وعدد الاتصالات التي استخدمتها، وأكبر عدد شوهد خلف عنوان IP واحد. للقراءة فقط، ويُحتسب من سجل الوكيل، ولا يُستخدم مطلقًا للتوجيه. معطّل افتراضيًا: يظل محرر المجموعة دون تغيير ويُرجع مسار الرصد null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "في فحص سلامة الوكلاء، اسمح للفحص الذي رفضه الهدف (401/403/429: قام الوكيل بالترحيل، لكن الوجهة رفضت عنوان IP الصادر هذا) بإعادة تعيين سلسلة حالات الفشل المتتالية للوكيل، كما لو كان فحصًا تمت خدمته. معطّل افتراضيًا: يظل الرفض محايدًا ويحافظ على السلسلة. تظل استجابة 5xx غير حاسمة في كلتا الحالتين، ولا يؤدي الرفض مطلقًا إلى إزالة وكيل أو تعطيله أو إعادة تنشيطه.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "الصفحة الرئيسية", "dashboard": "لوحة القيادة", @@ -6125,6 +6126,7 @@ "agentrouter": "احصل على رصيد مجاني بقيمة $200 على https://agentrouter.org/register — لا يتطلب بطاقة ائتمان.", "unorouter": "قم بإنشاء مفتاح API على https://unorouter.ai، ثم الصقّه هنا كرمز Bearer.", "agnes": "احصل على مفتاح API من agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "تم إيقاف الفئة المجانية مؤقتًا (2026) — أصبحت واجهة برمجة تطبيقات الذكاء الاصطناعي/تعلم الآلة (AI/ML API) مدفوعة حسب الاستخدام فقط (الحد الأدنى للشحن $20)؛ لا توجد أرصدة مجانية متكررة.", "ai21": "رصيد تجريبي بقيمة $10 عند التسجيل (صالح لمدة 3 أشهر)، لا يتطلب بطاقة ائتمان", "alibaba": "ربط Alibaba بمفتاح API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "تدقيق MCP (أيام)", "retentionA2aEvents": "أحداث A2A (أيام)", "retentionCallLogs": "سجلات المكالمات (أيام)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "تاريخ الاستخدام (بالأيام)", "retentionMemoryEntries": "إدخالات الذاكرة (أيام)", "retentionXpAuditLog": "سجل تدقيق XP (أيام)", diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json index f6ca25914a..24669494bb 100644 --- a/src/i18n/messages/az.json +++ b/src/i18n/messages/az.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proksi hovuzları və opencode-un hər hesab üzrə rotasiyası yenicə uğursuz olmuş proksini (TCP yoxlaması rədd edilib və ya onun vasitəsilə 429 cavabı alınıb) hər təkrarda ikiqat artan, lakin maksimum hədlə məhdudlaşan proses üzrə müddət ərzində yenidən təqdim etmir. Heç bir proksi statusu yazılmır; bütün namizədlər kənara qoyulduqda seçim dəyişməz qalır. Defolt olaraq deaktivdir: seçim sırası adi rotasiya ilə tam eynidir.", "featureFlagProxyPoolEgressObservationDescription": "İdarə panelində proksi hovuzunun altında son 24 saat ərzində müşahidə edilmiş neçə çıxış IP-sinin onun üzvlərinə xidmət etdiyini, neçə bağlantının onlardan istifadə etdiyini və bir IP arxasında müşahidə edilən ən yüksək sayını göstərin. Yalnız oxuma üçündür, proksi jurnalından hesablanır və marşrutlaşdırma üçün heç vaxt istifadə edilmir. Defolt olaraq deaktivdir: hovuz redaktoru dəyişməz qalır və müşahidə marşrutu null cavabı verir.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Proksinin sağlamlıq yoxlamasında hədəfin rədd etdiyi yoxlamanın (401/403/429: proksi sorğunu ötürüb, təyinat nöqtəsi bu çıxış IP-sini rədd edib) xidmət göstərilmiş yoxlama kimi proksinin ardıcıl uğursuzluq sayğacını sıfırlamasına icazə verin. Defolt olaraq deaktivdir: rədd cavabı neytral qalır və ardıcıllığı saxlayır. 5xx hər iki halda qeyri-müəyyən qalır və rədd cavabı heç vaxt proksini silmir, deaktiv etmir və ya yenidən aktivləşdirmir.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Əsas səhifə", "dashboard": "İdarə paneli", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register ünvanından $200 pulsuz kredit əldə edin — kredit kartı tələb olunmur.", "unorouter": "https://unorouter.ai saytında API açarı yaradın, sonra onu burada Bearer token olaraq yapışdırın.", "agnes": "API açarını agnes-ai.com ünvanından əldə edin", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Pulsuz paket dayandırılıb (2026) — AI/ML API artıq yalnız istifadə etdikcə ödə modelindədir (min. $20 balans artırma); təkrarlanan pulsuz kreditlər yoxdur.", "ai21": "Qeydiyyat zamanı $10 sınaq krediti (3 ay etibarlıdır), kredit kartı tələb olunmur", "alibaba": "Alibaba-nı API açarı ilə qoşun.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP Auditi (günlər)", "retentionA2aEvents": "A2A Hadisələri (günlər)", "retentionCallLogs": "Zəng qeydləri (günlər)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "İstifadə Tarixçəsi (günlər)", "retentionMemoryEntries": "Yaddaş Girişləri (günlər)", "retentionXpAuditLog": "XP audit jurnalı (gün)", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index 5ea8ef1e4c..db62c2a7f7 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Пуловете от проксита и ротацията на opencode за всеки акаунт временно спират повторното използване на прокси, което току-що е дало грешка (отказана TCP проверка или получен чрез него отговор 429), за период в рамките на процеса, който се удвоява при всяко повторение до достигане на максимална стойност. Не се записва състояние на проксито; ако всички кандидати бъдат временно изключени, изборът остава непроменен. Изключено по подразбиране: редът на избор е точно този на обикновената ротация.", "featureFlagProxyPoolEgressObservationDescription": "Показва под пул от проксита в таблото колко наблюдавани изходящи IP адреса са обслужвали членовете му през последните 24 ч., колко връзки са ги използвали и най-големия брой връзки зад един IP адрес. Само за четене, изчислява се от дневника на проксито и никога не се използва за маршрутизиране. Изключено по подразбиране: редакторът на пула остава непроменен, а маршрутът за наблюдение връща null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "При проверката на състоянието на прокситата позволява на проверка, отказана от целта (401/403/429: проксито е препратило заявката, но местоназначението е отказало този изходящ IP адрес), да нулира поредицата от последователни неуспехи на проксито, както при успешно обслужена проверка. Изключено по подразбиране: отказът остава неутрален и запазва поредицата. Отговор 5xx остава неубедителен и в двата случая, а отказът никога не премахва, деактивира или активира повторно прокси.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Начало", "dashboard": "Табло", @@ -6125,6 +6126,7 @@ "agentrouter": "Вземете $200 безплатни кредити на https://agentrouter.org/register — не се изисква кредитна карта.", "unorouter": "Създайте API ключ на https://unorouter.ai, след което го поставете тук като Bearer токен.", "agnes": "Вземете API ключ на agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Безплатният план е спрян (2026) — AI/ML API вече е само с разплащане според потреблението (мин. $20 презареждане); без периодични безплатни кредити.", "ai21": "$10 пробни кредити при регистрация (валидни 3 месеца), не се изисква кредитна карта", "alibaba": "Свържете Alibaba с API ключ.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Одит на MCP (дни)", "retentionA2aEvents": "Събития A2A (дни)", "retentionCallLogs": "Регистри на обажданията (дни)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "История на използването (дни)", "retentionMemoryEntries": "Записи в паметта (дни)", "retentionXpAuditLog": "Одитен дневник на XP (дни)", diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index 33d5905565..3252793bf0 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "প্রক্সি পুল এবং opencode-এর প্রতি-অ্যাকাউন্ট রোটেশন এমন কোনো প্রক্সিকে পুনরায় ব্যবহার করা বন্ধ রাখে, যা সদ্য ব্যর্থ হয়েছে (TCP প্রোব প্রত্যাখ্যাত হয়েছে অথবা সেটির মাধ্যমে 429 পাওয়া গেছে)। প্রতি-প্রসেসে এই বিরতির সময়কাল প্রতিবার পুনরাবৃত্তির সঙ্গে দ্বিগুণ হয়, সর্বোচ্চ সীমা পর্যন্ত। প্রক্সির কোনো স্ট্যাটাস লেখা হয় না; সব প্রার্থীকে সরিয়ে রাখলেও নির্বাচন অপরিবর্তিত থাকে। ডিফল্টভাবে বন্ধ: নির্বাচনের ক্রম হুবহু সাধারণ রোটেশনের মতো।", "featureFlagProxyPoolEgressObservationDescription": "ড্যাশবোর্ডে একটি প্রক্সি পুলের অধীনে দেখান, গত ২৪ ঘণ্টায় পর্যবেক্ষিত কতটি egress IP তার সদস্যদের সেবা দিয়েছে, কতটি সংযোগ সেগুলো ব্যবহার করেছে এবং একটি IP-এর পেছনে সর্বাধিক কতটি দেখা গেছে। এটি শুধু পাঠযোগ্য, প্রক্সি লগ থেকে গণনা করা হয় এবং রাউটিংয়ে কখনো ব্যবহার করা হয় না। ডিফল্টভাবে বন্ধ: পুল এডিটর অপরিবর্তিত থাকে এবং পর্যবেক্ষণ রুট null প্রদান করে।", "featureFlagProxyHealthBlockedResetsStreakDescription": "প্রক্সি হেলথ সুইপে, টার্গেট প্রত্যাখ্যান করেছে এমন কোনো প্রোবকে (401/403/429: প্রক্সি রিলে করেছে, কিন্তু গন্তব্য এই egress IP প্রত্যাখ্যান করেছে) সফলভাবে পরিবেশিত প্রোবের মতো প্রক্সির ধারাবাহিক ব্যর্থতার ধারা রিসেট করতে দিন। ডিফল্টভাবে বন্ধ: প্রত্যাখ্যান নিরপেক্ষ থাকে এবং ধারাটি বজায় রাখে। উভয় ক্ষেত্রেই 5xx অনির্ণায়ক থাকে এবং কোনো প্রত্যাখ্যান কখনোই প্রক্সিকে সরায়, নিষ্ক্রিয় করে বা পুনরায় সক্রিয় করে না।", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "হোম", "dashboard": "ড্যাশবোর্ড", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register-এ $200 ফ্রি ক্রেডিট পান — কোনো ক্রেডিট কার্ডের প্রয়োজন নেই।", "unorouter": "https://unorouter.ai তে একটি API কী তৈরি করুন, তারপর এটি এখানে Bearer টোকেন হিসেবে পেস্ট করুন।", "agnes": "agnes-ai.com থেকে API কী পান", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "ফ্রি টিয়ার স্থগিত করা হয়েছে (২০২৬) — AI/ML API এখন শুধুমাত্র পে-অ্যাজ-ইউ-গো (সর্বনিম্ন $২০ টপ-আপ); কোনো পুনরাবৃত্ত ফ্রি ক্রেডিট নেই।", "ai21": "সাইনআপে $১০ ট্রায়াল ক্রেডিট (৩ মাসের জন্য বৈধ), কোনো ক্রেডিট কার্ডের প্রয়োজন নেই", "alibaba": "একটি API কী দিয়ে Alibaba কানেক্ট করুন।", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP অডিট (দিন)", "retentionA2aEvents": "A2A ইভেন্ট (দিন)", "retentionCallLogs": "কল লগ (দিন)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "ব্যবহারের ইতিহাস (দিন)", "retentionMemoryEntries": "মেমরি এন্ট্রি (দিন)", "retentionXpAuditLog": "XP অডিট লগ (দিন)", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index a6d823ccdc..d43a8ac445 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Fondy proxy serverů a rotace opencode pro jednotlivé účty dočasně přestanou znovu nabízet proxy server, který právě selhal (odmítnutá sonda TCP nebo odpověď 429 přijatá přes tento server), a to na dobu v rámci procesu, která se při každém opakování zdvojnásobí až do stanoveného maxima. Stav proxy serveru se nezapisuje; pokud jsou odloženi všichni kandidáti, volba se nemění. Ve výchozím nastavení vypnuto: pořadí výběru přesně odpovídá prosté rotaci.", "featureFlagProxyPoolEgressObservationDescription": "Zobrazit pod fondem proxy serverů na řídicím panelu, kolik pozorovaných odchozích IP adres obsluhovalo jeho členy za posledních 24 h, kolik připojení je použilo a jaký byl nejvyšší počet připojení zaznamenaný za jednou IP adresou. Pouze pro čtení, vypočítáváno z protokolu proxy serveru a nikdy nepoužíváno ke směrování. Ve výchozím nastavení vypnuto: editor fondu zůstává beze změny a trasa pozorování vrací null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Při kontrole stavu proxy serveru umožnit, aby sonda odmítnutá cílem (401/403/429: proxy server požadavek předal, ale cíl odmítl tuto odchozí IP adresu) vynulovala řadu po sobě jdoucích selhání proxy serveru stejně jako úspěšně obsloužená sonda. Ve výchozím nastavení vypnuto: odmítnutí zůstává neutrální a řadu zachovává. Odpověď 5xx zůstává v obou případech neprůkazná a odmítnutí nikdy neodebere, nezakáže ani znovu neaktivuje proxy server.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Domov", "dashboard": "Nástěnka", @@ -6125,6 +6126,7 @@ "agentrouter": "Získejte bezplatný kredit 200 $ na https://agentrouter.org/register — není vyžadována platební karta.", "unorouter": "Vytvořte API klíč na https://unorouter.ai, poté jej sem vložte jako Bearer token.", "agnes": "Získejte API klíč na agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Bezplatný tarif pozastaven (2026) — AI/ML API je nyní pouze pay-as-you-go (min. dobití 20 $); žádné opakující se bezplatné kredity.", "ai21": "Zkušební kredit 10 $ při registraci (platnost 3 měsíce), není vyžadována platební karta", "alibaba": "Připojte Alibaba pomocí API klíče.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Audit MCP (dny)", "retentionA2aEvents": "Události A2A (dny)", "retentionCallLogs": "Protokoly hovorů (dny)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Historie využití (dny)", "retentionMemoryEntries": "Záznamy paměti (dny)", "retentionXpAuditLog": "Protokol auditu XP (dny)", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index f345293d50..cf1a990659 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proxy-puljer og opencodes rotation pr. konto undgår i en periode pr. proces, der fordobles ved hver gentagelse op til en øvre grænse, at genbruge en proxy, som netop har fejlet (afvist TCP-probe eller en 429 modtaget gennem den). Der skrives ingen proxy-status; hvis alle kandidater sættes til side, forbliver valget uændret. Deaktiveret som standard: rækkefølgen for valg følger præcis den almindelige rotation.", "featureFlagProxyPoolEgressObservationDescription": "Vis under en proxy-pulje i dashboardet, hvor mange observerede udgående IP-adresser der har betjent dens medlemmer inden for de seneste 24 timer, hvor mange forbindelser der brugte dem, og det højeste antal observeret bag én IP-adresse. Skrivebeskyttet, beregnet ud fra proxy-loggen og aldrig brugt til routing. Deaktiveret som standard: puljeeditoren er uændret, og observationsruten returnerer null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Lad en probe, som målet afviste (401/403/429: proxyen videresendte anmodningen, men destinationen afviste denne udgående IP-adresse), nulstille proxyens række af fortløbende fejl i proxyens helbredskontrol, ligesom en betjent probe. Deaktiveret som standard: En afvisning forbliver neutral og bevarer rækken. En 5xx forbliver uafklaret i begge tilfælde, og en afvisning fjerner, deaktiverer eller genaktiverer aldrig en proxy.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Hjem", "dashboard": "Kontrolpanel", @@ -6125,6 +6126,7 @@ "agentrouter": "Få $200 gratis kreditter på https://agentrouter.org/register — intet kreditkort påkrævet.", "unorouter": "Opret en API-nøgle på https://unorouter.ai, og indsæt den derefter her som en Bearer-token.", "agnes": "Få API-nøgle på agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Gratis niveau sat på pause (2026) — AI/ML API er nu kun pay-as-you-go (min. $20 optankning); ingen tilbagevendende gratis kreditter.", "ai21": "$10 prøvekreditter ved tilmelding (gyldig i 3 måneder), intet kreditkort påkrævet", "alibaba": "Forbind Alibaba med en API-nøgle.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP-revision (dage)", "retentionA2aEvents": "A2A-begivenheder (dage)", "retentionCallLogs": "Opkaldslogger (dage)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Brugshistorik (dage)", "retentionMemoryEntries": "Hukommelsesindgange (dage)", "retentionXpAuditLog": "XP-revisionslog (dage)", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index e4d6ac4ce5..55e023332d 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proxy-Pools und die kontobasierte Rotation von opencode verwenden einen Proxy, der gerade fehlgeschlagen ist (abgelehnte TCP-Prüfung oder darüber empfangener 429-Statuscode), für einen prozessbezogenen Zeitraum nicht erneut. Dieser Zeitraum verdoppelt sich bei jeder Wiederholung bis zu einer Obergrenze. Es wird kein Proxy-Status gespeichert; wenn alle Kandidaten zurückgestellt wurden, bleibt die Auswahl unverändert. Standardmäßig deaktiviert: Die Auswahlreihenfolge entspricht exakt der einfachen Rotation.", "featureFlagProxyPoolEgressObservationDescription": "Zeigt im Dashboard unter einem Proxy-Pool an, wie viele beobachtete Egress-IPs dessen Mitglieder in den letzten 24 h bedient haben, wie viele Verbindungen sie verwendet haben und wie viele maximal hinter einer einzelnen IP beobachtet wurden. Schreibgeschützt, aus dem Proxy-Protokoll berechnet und niemals für das Routing verwendet. Standardmäßig deaktiviert: Der Pool-Editor bleibt unverändert und die Beobachtungsroute gibt null zurück.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Ermöglicht bei der Proxy-Zustandsprüfung, dass eine vom Ziel abgelehnte Prüfung (401/403/429: Der Proxy hat die Anfrage weitergeleitet, das Ziel hat diese Egress-IP abgelehnt) die Serie aufeinanderfolgender Fehler des Proxys zurücksetzt, wie bei einer erfolgreich beantworteten Prüfung. Standardmäßig deaktiviert: Eine Ablehnung bleibt neutral und die Serie wird beibehalten. Ein 5xx-Status bleibt in beiden Fällen nicht aussagekräftig, und eine Ablehnung entfernt, deaktiviert oder reaktiviert niemals einen Proxy.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Zuhause", "dashboard": "Dashboard", @@ -6125,6 +6126,7 @@ "agentrouter": "Erhalten Sie 200 $ Gratisguthaben unter https://agentrouter.org/register – keine Kreditkarte erforderlich.", "unorouter": "Erstellen Sie einen API-Schlüssel unter https://unorouter.ai und fügen Sie ihn dann hier als Bearer-Token ein.", "agnes": "API-Schlüssel auf agnes-ai.com abrufen", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Kostenlose Stufe pausiert (2026) – AI/ML API ist jetzt nur noch Pay-as-you-go (mind. 20 $ Aufladung); keine wiederkehrenden Gratisguthaben.", "ai21": "10 $ Testguthaben bei Registrierung (3 Monate gültig), keine Kreditkarte erforderlich", "alibaba": "Alibaba mit einem API-Schlüssel verbinden.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP-Audit (Tage)", "retentionA2aEvents": "A2A-Veranstaltungen (Tage)", "retentionCallLogs": "Anrufprotokolle (Tage)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Nutzungsverlauf (Tage)", "retentionMemoryEntries": "Speichereinträge (Tage)", "retentionXpAuditLog": "XP-Audit-Log (Tage)", diff --git a/src/i18n/messages/el.json b/src/i18n/messages/el.json index 32662fb894..b8a27a3cec 100644 --- a/src/i18n/messages/el.json +++ b/src/i18n/messages/el.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Οι ομάδες διακομιστών μεσολάβησης και η εναλλαγή ανά λογαριασμό του opencode παύουν να χρησιμοποιούν ξανά έναν διακομιστή μεσολάβησης που μόλις απέτυχε (απορρίφθηκε η δοκιμή TCP ή λήφθηκε απόκριση 429 μέσω αυτού) για ένα χρονικό διάστημα ανά διεργασία, το οποίο διπλασιάζεται σε κάθε επανάληψη, έως ένα ανώτατο όριο. Δεν καταγράφεται καμία κατάσταση διακομιστή μεσολάβησης· όταν όλοι οι υποψήφιοι παραμερίζονται, η επιλογή παραμένει αμετάβλητη. Απενεργοποιημένο από προεπιλογή: η σειρά επιλογής είναι ακριβώς αυτή της απλής εναλλαγής.", "featureFlagProxyPoolEgressObservationDescription": "Εμφάνιση, κάτω από μια ομάδα διακομιστών μεσολάβησης στον πίνακα ελέγχου, του αριθμού των παρατηρούμενων IP εξόδου που εξυπηρέτησαν τα μέλη της κατά τις τελευταίες 24 ώρες, του αριθμού των συνδέσεων που τις χρησιμοποίησαν και του μέγιστου αριθμού που παρατηρήθηκε πίσω από μία IP. Μόνο για ανάγνωση, υπολογίζεται από το αρχείο καταγραφής του διακομιστή μεσολάβησης και δεν χρησιμοποιείται ποτέ για δρομολόγηση. Απενεργοποιημένο από προεπιλογή: το πρόγραμμα επεξεργασίας της ομάδας παραμένει αμετάβλητο και η διαδρομή παρατήρησης επιστρέφει null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Κατά τον έλεγχο εύρυθμης λειτουργίας των διακομιστών μεσολάβησης, να επιτρέπεται σε μια δοκιμή που απορρίφθηκε από τον προορισμό (401/403/429: ο διακομιστής μεσολάβησης προώθησε το αίτημα, αλλά ο προορισμός απέρριψε αυτήν την IP εξόδου) να μηδενίζει το συνεχόμενο σερί αποτυχιών του διακομιστή μεσολάβησης, όπως μια δοκιμή που εξυπηρετήθηκε. Απενεργοποιημένο από προεπιλογή: μια απόρριψη παραμένει ουδέτερη και διατηρεί το σερί. Μια απόκριση 5xx παραμένει ασαφής και στις δύο περιπτώσεις, ενώ μια απόρριψη δεν αφαιρεί, δεν απενεργοποιεί και δεν επανενεργοποιεί ποτέ έναν διακομιστή μεσολάβησης.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Αρχική", "dashboard": "Πίνακας Ελέγχου", @@ -6125,6 +6126,7 @@ "agentrouter": "Αποκτήστε $200 δωρεάν πιστώσεις στο https://agentrouter.org/register — χωρίς πιστωτική κάρτα.", "unorouter": "Δημιουργήστε ένα κλειδί API στο https://unorouter.ai και στη συνέχεια επικολλήστε το εδώ ως Bearer token.", "agnes": "Λήψη κλειδιού API στο agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Το δωρεάν επίπεδο έχει τεθεί σε παύση (2026) — το AI/ML API είναι πλέον αποκλειστικά pay-as-you-go (ελάχιστη χρέωση $20)· δεν υπάρχουν επαναλαμβανόμενες δωρεάν πιστώσεις.", "ai21": "$10 δοκιμαστικές πιστώσεις κατά την εγγραφή (ισχύουν 3 μήνες), χωρίς πιστωτική κάρτα", "alibaba": "Σύνδεση Alibaba με κλειδί API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Έλεγχος MCP (ημέρες)", "retentionA2aEvents": "Συμβάντα A2A (ημέρες)", "retentionCallLogs": "Αρχεία Καταγραφής Κλήσεων (ημέρες)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Ιστορικό Χρήσης (ημέρες)", "retentionMemoryEntries": "Καταχωρίσεις Μνήμης (ημέρες)", "retentionXpAuditLog": "Αρχείο Ελέγχου XP (ημέρες)", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 0a685beed5..f40430be02 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proxy pools and the per-account rotation of opencode stop re-serving a proxy that just failed (refused TCP probe, or a 429 received through it) for a per-process period that doubles on each repeat, up to a cap. No proxy status is written; with every candidate set aside the choice is unchanged. Off by default: selection order is exactly the plain rotation.", "featureFlagProxyPoolEgressObservationDescription": "Show, under a proxy pool in the dashboard, how many observed egress IPs served its members over the last 24 h, how many connections used them and the most seen behind one IP. Read-only, computed from the proxy log, never used for routing. Off by default: the pool editor is unchanged and the observation route answers null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "In the proxy health sweep, let a probe the target refused (401/403/429: the proxy relayed, the destination refused this egress IP) reset the consecutive-failure streak of the proxy, like a served probe. Off by default: a refusal stays neutral and keeps the streak. A 5xx stays inconclusive either way, and a refusal never removes, disables or re-activates a proxy.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Home", "dashboard": "Dashboard", @@ -6125,6 +6126,7 @@ "agentrouter": "Get $200 free credits at https://agentrouter.org/register — no credit card required.", "unorouter": "Create an API key at https://unorouter.ai, then paste it here as a Bearer token.", "agnes": "Get API key at agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Free tier paused (2026) — AI/ML API is now pay-as-you-go only (min $20 top-up); no recurring free credits.", "ai21": "$10 trial credits on signup (valid 3 months), no credit card required", "alibaba": "Connect Alibaba with an API key.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP Audit (days)", "retentionA2aEvents": "A2A Events (days)", "retentionCallLogs": "Call Logs (days)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Usage History (days)", "retentionMemoryEntries": "Memory Entries (days)", "retentionXpAuditLog": "XP Audit Log (days)", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 5588aabd00..f91cf64f0c 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Los grupos de proxies y la rotación por cuenta de opencode dejan de reutilizar temporalmente un proxy que acaba de fallar (sondeo TCP rechazado o un 429 recibido a través de él) durante un período por proceso que se duplica con cada repetición, hasta alcanzar un límite. No se registra ningún estado del proxy; si se apartan todos los candidatos, la elección no cambia. Desactivado de forma predeterminada: el orden de selección coincide exactamente con la rotación normal.", "featureFlagProxyPoolEgressObservationDescription": "Mostrar, debajo de un grupo de proxies en el panel, cuántas IP de salida observadas utilizaron sus miembros durante las últimas 24 h, cuántas conexiones las usaron y el máximo observado detrás de una sola IP. Es de solo lectura, se calcula a partir del registro del proxy y nunca se utiliza para el enrutamiento. Desactivado de forma predeterminada: el editor de grupos no cambia y la ruta de observación devuelve null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "En la comprobación periódica del estado de los proxies, permitir que un sondeo rechazado por el destino (401/403/429: el proxy lo retransmitió, pero el destino rechazó esta IP de salida) reinicie la racha de fallos consecutivos del proxy, como un sondeo atendido. Desactivado de forma predeterminada: un rechazo sigue siendo neutral y mantiene la racha. Un 5xx sigue sin ser concluyente en ambos casos, y un rechazo nunca elimina, desactiva ni reactiva un proxy.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Inicio", "dashboard": "Panel de control", @@ -6199,6 +6200,7 @@ "inception": "Inception Labs es compatible con OpenAI en https://api.inceptionlabs.ai/v1. mercury-2 es el primer LLM de difusión (dLLM) del catálogo, con una generación entre 5 y 10 veces más rápida que la de modelos autorregresivos comparables, además de llamadas a herramientas, json_mode y salidas estructuradas.", "inference-net": "$25 en créditos gratuitos al registrarte, además de subvenciones de investigación disponibles", "internlm": "Cuota mensual gratuita de aproximadamente 1M de tokens de entrada / 3M de tokens de salida (aproximadamente 10 RPM)", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "jina-ai": "Bearer API key for the Jina AI rerank API.", "jina-reader": "Connect Jina Reader with an API key.", "kenari": "Kenari ofrece un endpoint de finalización de chat compatible con OpenAI en https://kenari.id/v1/chat/completions, además de un catálogo activo en /v1/models que incluye Claude, GPT, DeepSeek, GLM, Kimi y más. OmniRoute utiliza el protocolo de OpenAI y muestra los modelos mediante passthrough.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Auditoría MCP (días)", "retentionA2aEvents": "Eventos A2A (días)", "retentionCallLogs": "Registros de llamadas (días)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Historial de uso (días)", "retentionMemoryEntries": "Entradas de memoria (días)", "retentionXpAuditLog": "Registro de auditoría de XP (días)", diff --git a/src/i18n/messages/et.json b/src/i18n/messages/et.json index babaa3f604..5ea0701be3 100644 --- a/src/i18n/messages/et.json +++ b/src/i18n/messages/et.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Puhverserverite kogumid ja opencode'i kontopõhine roteerimine väldivad äsja nurjunud puhverserveri (TCP-proovist keelduti või selle kaudu saadi vastus 429) uuesti kasutamist protsessipõhise ajavahemiku jooksul, mis iga korduva tõrke korral kahekordistub kuni ülempiirini. Puhverserveri olekut ei salvestata; kui kõik kandidaadid kõrvale jäetakse, valik ei muutu. Vaikimisi väljas: valikujärjekord järgib täpselt tavalist roteerimist.", "featureFlagProxyPoolEgressObservationDescription": "Kuva töölaual puhverserverite kogumi all, mitu vaadeldud väljuvat IP-aadressi teenindas selle liikmeid viimase 24 tunni jooksul, mitu ühendust neid kasutas ja suurim ühe IP-aadressi taga täheldatud arv. Kirjutuskaitstud, arvutatakse puhverserveri logist ja seda ei kasutata kunagi marsruutimiseks. Vaikimisi väljas: kogumi redaktor ei muutu ja vaatlusmarsruut tagastab null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Luba puhverserveri seisundi kontrollimisel sihtkoha poolt tagasi lükatud proovil (401/403/429: puhverserver vahendas päringu, sihtkoht keeldus sellest väljuvast IP-aadressist) lähtestada puhverserveri järjestikuste tõrgete jada nagu teenindatud proovi korral. Vaikimisi väljas: tagasilükkamine jääb neutraalseks ja säilitab jada. 5xx jääb mõlemal juhul ebaselgeks ning tagasilükkamine ei eemalda, keela ega taasaktiveeri kunagi puhverserverit.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Avaleht", "dashboard": "Juhtpaneel", @@ -6125,6 +6126,7 @@ "agentrouter": "Saa $200 tasuta krediiti aadressilt https://agentrouter.org/register — krediitkaarti pole vaja.", "unorouter": "Loo API-võti aadressil https://unorouter.ai ja kleebi see seejärel siia Bearer-tokenina.", "agnes": "Hangi API-võti aadressilt agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Tasuta pakett on peatatud (2026) — AI/ML API on nüüd ainult kasutuspõhise hinnastusega (minimaalne sissemakse $20); korduvaid tasuta krediite pole.", "ai21": "Registreerumisel $10 proovikrediiti (kehtib 3 kuud), krediitkaarti pole vaja", "alibaba": "Ühenda Alibaba API-võtmega.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP audit (päevades)", "retentionA2aEvents": "A2A sündmused (päevades)", "retentionCallLogs": "Kõnelogid (päevades)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Kasutusajalugu (päevades)", "retentionMemoryEntries": "Mälukirjed (päevades)", "retentionXpAuditLog": "XP auditilogi (päevades)", diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index d8742159bd..0b36cf24d7 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "مخزنهای پراکسی و چرخش بهازای هر حساب در opencode، پراکسیای را که بهتازگی ناموفق بوده است (کاوش TCP را رد کرده یا از طریق آن پاسخ 429 دریافت شده است)، برای مدتی مختص هر فرایند دوباره ارائه نمیکنند؛ این مدت با هر تکرار دو برابر میشود تا به سقف تعیینشده برسد. هیچ وضعیتی برای پراکسی ثبت نمیشود؛ اگر همه گزینهها کنار گذاشته شوند، انتخاب بدون تغییر میماند. بهطور پیشفرض غیرفعال است: ترتیب انتخاب دقیقاً همان چرخش ساده است.", "featureFlagProxyPoolEgressObservationDescription": "در داشبورد، زیر هر مخزن پراکسی نشان دهید که طی ۲۴ ساعت گذشته چه تعداد IP خروجی مشاهدهشده به اعضای آن سرویس دادهاند، چه تعداد اتصال از آنها استفاده کردهاند و بیشترین تعداد اتصال مشاهدهشده پشت یک IP چقدر بوده است. فقط خواندنی است، از گزارش پراکسی محاسبه میشود و هرگز برای مسیریابی استفاده نمیشود. بهطور پیشفرض غیرفعال است: ویرایشگر مخزن بدون تغییر میماند و مسیر مشاهده مقدار null را برمیگرداند.", "featureFlagProxyHealthBlockedResetsStreakDescription": "در پیمایش سلامت پراکسی، اجازه دهید کاوشی که مقصد آن را رد کرده است (401/403/429: پراکسی درخواست را عبور داده اما مقصد این IP خروجی را رد کرده است)، مانند یک کاوش سرویسدهیشده، شمارنده شکستهای متوالی پراکسی را بازنشانی کند. بهطور پیشفرض غیرفعال است: رد شدن همچنان خنثی باقی میماند و شمارنده را حفظ میکند. پاسخ 5xx در هر دو حالت نامشخص باقی میماند و رد شدن هرگز باعث حذف، غیرفعالسازی یا فعالسازی مجدد پراکسی نمیشود.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "خانه", "dashboard": "داشبورد", @@ -6125,6 +6126,7 @@ "agentrouter": "۲۰۰ دلار اعتبار رایگان در https://agentrouter.org/register دریافت کنید — بدون نیاز به کارت اعتباری.", "unorouter": "یک کلید API در https://unorouter.ai ایجاد کنید، سپس آن را به عنوان یک توکن Bearer در اینجا بچسبانید.", "agnes": "کلید API را از agnes-ai.com دریافت کنید", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "سطح رایگان متوقف شد (۲۰۲۶) — API هوش مصنوعی/یادگیری ماشین اکنون فقط به صورت پرداخت به میزان مصرف است (حداقل شارژ ۲۰ دلار)؛ بدون اعتبار رایگان دوره‌ای.", "ai21": "۱۰ دلار اعتبار آزمایشی هنگام ثبت‌نام (معتبر به مدت ۳ ماه)، بدون نیاز به کارت اعتباری", "alibaba": "اتصال به Alibaba با یک کلید API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "ممیزی MCP (روزها)", "retentionA2aEvents": "رویدادهای A2A (روزها)", "retentionCallLogs": "گزارش تماس (روزها)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "تاریخچه استفاده (روزها)", "retentionMemoryEntries": "ورودی های حافظه (روز)", "retentionXpAuditLog": "گزارش حسابرسی XP (روز)", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index b5b5c054da..0eddb525e4 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Välityspalvelinpoolit ja opencoden tilikohtainen kierto estävät juuri epäonnistuneen välityspalvelimen (hylätty TCP-tarkistus tai sen kautta saatu 429-vastaus) tarjoamisen uudelleen prosessikohtaisen ajanjakson ajan. Ajanjakso kaksinkertaistuu jokaisen toistuvan epäonnistumisen myötä enimmäisrajaan asti. Välityspalvelimen tilaa ei kirjata; jos kaikki ehdokkaat on siirretty sivuun, valinta pysyy ennallaan. Oletusarvoisesti poissa käytöstä: valintajärjestys vastaa täsmälleen tavallista kiertoa.", "featureFlagProxyPoolEgressObservationDescription": "Näytä hallintapaneelissa välityspalvelinpoolin kohdalla, kuinka monta havaittua lähtevän liikenteen IP-osoitetta sen jäsenet käyttivät viimeisten 24 tunnin aikana, kuinka monta yhteyttä käytti niitä ja mikä oli yhden IP-osoitteen takana havaittu suurin määrä. Vain luku -tieto, joka lasketaan välityspalvelinlokista eikä jota koskaan käytetä reititykseen. Oletusarvoisesti poissa käytöstä: poolieditori säilyy ennallaan ja havaintoreitti palauttaa arvon null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Salli välityspalvelinten kuntotarkistuksessa sellaisen tarkistuksen, jonka kohde hylkäsi (401/403/429: välityspalvelin välitti pyynnön, mutta kohde hylkäsi tämän lähtevän liikenteen IP-osoitteen), nollata välityspalvelimen peräkkäisten epäonnistumisten sarja onnistuneesti palvellun tarkistuksen tavoin. Oletusarvoisesti poissa käytöstä: hylkäys pysyy neutraalina eikä katkaise sarjaa. 5xx-vastaus pysyy kummassakin tapauksessa tuloksettomana, eikä hylkäys koskaan poista välityspalvelinta, poista sitä käytöstä tai aktivoi sitä uudelleen.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Kotiin", "dashboard": "Kojelauta", @@ -6125,6 +6126,7 @@ "agentrouter": "Hanki 200 $ ilmaista saldoa osoitteesta https://agentrouter.org/register — luottokorttia ei vaadita.", "unorouter": "Luo API-avain osoitteessa https://unorouter.ai, ja liitä se sitten tänne Bearer-tokenina.", "agnes": "Hanki API-avain osoitteesta agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Ilmaisversio keskeytetty (2026) — AI/ML API on nyt vain käytön mukaan laskutettava (vähintään 20 $ lisäys); ei toistuvia ilmaissaldoja.", "ai21": "10 $ kokeilusaldona rekisteröitymisen yhteydessä (voimassa 3 kuukautta), luottokorttia ei vaadita", "alibaba": "Yhdistä Alibaba API-avaimella.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP-tarkastus (päiviä)", "retentionA2aEvents": "A2A-tapahtumat (päiviä)", "retentionCallLogs": "Puhelulokit (päiviä)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Käyttöhistoria (päiviä)", "retentionMemoryEntries": "Muistimerkinnät (päiviä)", "retentionXpAuditLog": "XP-tarkastusloki (päivää)", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index af56b66d8c..f137cd94d7 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Les pools de proxys et la rotation par compte d’opencode cessent de réutiliser un proxy qui vient d’échouer (sonde TCP refusée ou réponse 429 reçue par son intermédiaire) pendant une durée propre au processus qui double à chaque nouvel échec, jusqu’à une limite maximale. Aucun état de proxy n’est enregistré ; lorsque tous les candidats sont écartés, le choix reste inchangé. Désactivé par défaut : l’ordre de sélection correspond exactement à la rotation simple.", "featureFlagProxyPoolEgressObservationDescription": "Afficher, sous un pool de proxys dans le tableau de bord, combien d’adresses IP de sortie observées ont servi ses membres au cours des dernières 24 h, combien de connexions les ont utilisées et le nombre maximal observé derrière une même adresse IP. En lecture seule, calculé à partir du journal du proxy, jamais utilisé pour le routage. Désactivé par défaut : l’éditeur de pool reste inchangé et la route d’observation renvoie null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Lors du contrôle d’intégrité des proxys, permettez à une sonde refusée par la cible (401/403/429 : le proxy a relayé la requête, mais la destination a refusé cette adresse IP de sortie) de réinitialiser la série d’échecs consécutifs du proxy, comme une sonde traitée. Désactivé par défaut : un refus reste neutre et maintient la série. Une réponse 5xx reste non concluante dans les deux cas, et un refus ne supprime, ne désactive ni ne réactive jamais un proxy.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Accueil", "dashboard": "Tableau de bord", @@ -6125,6 +6126,7 @@ "agentrouter": "Obtenez 200 $ de crédits gratuits sur https://agentrouter.org/register — aucune carte de crédit requise.", "unorouter": "Créez une clé API sur https://unorouter.ai, puis collez-la ici en tant que jeton Bearer.", "agnes": "Obtenez une clé API sur agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Offre gratuite suspendue (2026) — L'API AI/ML est désormais uniquement pay-as-you-go (recharge minimale de 20 $) ; pas de crédits gratuits récurrents.", "ai21": "10 $ de crédits d'essai à l'inscription (valables 3 mois), aucune carte de crédit requise", "alibaba": "Connectez Alibaba avec une clé API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Audit MCP (jours)", "retentionA2aEvents": "Événements A2A (jours)", "retentionCallLogs": "Journaux d'appels (jours)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Historique d'utilisation (jours)", "retentionMemoryEntries": "Entrées de mémoire (jours)", "retentionXpAuditLog": "Journal d'audit XP (jours)", diff --git a/src/i18n/messages/ga.json b/src/i18n/messages/ga.json index 9ae14d9309..f259218b61 100644 --- a/src/i18n/messages/ga.json +++ b/src/i18n/messages/ga.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Cuireann linnte seachfhreastalaí agus rothlú opencode in aghaidh an chuntais cosc ar sheachfhreastalaí ar theip air díreach (diúltaíodh don tóireadóir TCP, nó fuarthas 429 tríd) a sheirbheáil arís ar feadh tréimhse in aghaidh an phróisis a dhúblaíonn le gach teip eile, suas le huasteorainn. Ní scríobhtar aon stádas seachfhreastalaí; má chuirtear gach iarrthóir ar leataobh, fanann an rogha gan athrú. As de réir réamhshocraithe: is ionann an t-ord roghnúcháin go díreach agus an gnáthrothlú.", "featureFlagProxyPoolEgressObservationDescription": "Taispeáin, faoi linn seachfhreastalaí sa deais, cé mhéad seoladh IP amach a breathnaíodh a d’fhreastail ar a bhaill le 24 h anuas, cé mhéad nasc a d’úsáid iad agus an líon is mó a chonacthas taobh thiar d’aon IP amháin. Inléite amháin, ríofa ón loga seachfhreastalaí, agus ní úsáidtear riamh é le haghaidh ródúcháin. As de réir réamhshocraithe: fanann eagarthóir na linne gan athrú agus freagraíonn bealach na breathnóireachta le null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "I scuabadh sláinte an tseachfhreastalaí, lig do thóraíocht ar dhiúltaigh an sprioc di (401/403/429: rinne an seachfhreastalaí athsheoladh, ach dhiúltaigh an ceann scríbe don seoladh IP amach seo) stríoc teipeanna comhleanúnacha an tseachfhreastalaí a athshocrú, amhail tóraíocht a seirbheáladh. As de réir réamhshocraithe: fanann diúltú neodrach agus coinníonn sé an stríoc. Fanann 5xx neamhchinntitheach sa dá chás, agus ní bhaineann diúltú seachfhreastalaí choíche, ní dhíchumasaíonn sé é ná ní athghníomhachtaíonn sé é.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Baile", "dashboard": "Deais", @@ -6125,6 +6126,7 @@ "agentrouter": "Faigh $200 creidmheasanna saor in aisce ag https://agentrouter.org/register — ní gá cárta creidmheasa.", "unorouter": "Cruthaigh eochair API ag https://unorouter.ai, ansin greamaigh anseo é mar chomhartha Bearer.", "agnes": "Faigh eochair API ag agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Sraith saor in aisce ar sos (2026) — níl AI/ML API ar fáil anois ach ar bhonn íoc-mar-a-úsáide (íosmhéid $20 d'athlíonadh); níl aon chreidmheasanna saor in aisce athfhillteacha.", "ai21": "Creidmheasanna trialach $10 ar chlárú (bailí ar feadh 3 mhí), ní gá cárta creidmheasa.", "alibaba": "Ceangail Alibaba le heochair API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Iniúchadh MCP (laethanta)", "retentionA2aEvents": "Imeachtaí A2A (laethanta)", "retentionCallLogs": "Logaí Glaonna (laethanta)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Stair Úsáide (laethanta)", "retentionMemoryEntries": "Iontráil Cuimhne (laethanta)", "retentionXpAuditLog": "Log Iniúchadh XP (laethanta)", diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index 92fc0f0ede..e7a000fd21 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proxy pools અને opencodeનું પ્રતિ-એકાઉન્ટ રોટેશન, હમણાં જ નિષ્ફળ ગયેલા પ્રોક્સીને (TCP પ્રોબ નકારાયો હોય અથવા તેના મારફતે 429 મળ્યો હોય) દરેક પુનરાવર્તન સાથે બમણા થતા, મહત્તમ મર્યાદા સુધીના પ્રતિ-પ્રોસેસ સમયગાળા માટે ફરીથી સર્વ કરવાનું બંધ કરે છે. પ્રોક્સીની કોઈ સ્થિતિ લખવામાં આવતી નથી; દરેક ઉમેદવારને બાજુ પર રાખવામાં આવે તો પસંદગી યથાવત્ રહે છે. ડિફૉલ્ટ રૂપે બંધ: પસંદગીનો ક્રમ સાદા રોટેશન જેવો જ રહે છે.", "featureFlagProxyPoolEgressObservationDescription": "ડૅશબોર્ડમાં પ્રોક્સી પૂલ હેઠળ બતાવો કે છેલ્લા 24 કલાકમાં કેટલા નિરીક્ષિત એગ્રેસ IPએ તેના સભ્યોને સેવા આપી, કેટલાં કનેક્શનોએ તેમનો ઉપયોગ કર્યો અને એક IP પાછળ મહત્તમ કેટલા જોવા મળ્યા. ફક્ત વાંચવા માટે, પ્રોક્સી લૉગમાંથી ગણતરી કરેલું અને રૂટિંગ માટે ક્યારેય ઉપયોગમાં લેવાતું નથી. ડિફૉલ્ટ રૂપે બંધ: પૂલ એડિટર યથાવત્ રહે છે અને નિરીક્ષણ રૂટ null પરત કરે છે.", "featureFlagProxyHealthBlockedResetsStreakDescription": "પ્રોક્સી હેલ્થ સ્વીપમાં, લક્ષ્ય દ્વારા નકારવામાં આવેલા પ્રોબને (401/403/429: પ્રોક્સીએ રિલે કર્યું, ગંતવ્યે આ એગ્રેસ IPને નકાર્યો) સર્વ કરાયેલા પ્રોબની જેમ પ્રોક્સીની સળંગ નિષ્ફળતાઓની શ્રેણી રીસેટ કરવાની મંજૂરી આપો. ડિફૉલ્ટ રૂપે બંધ: નકાર તટસ્થ રહે છે અને શ્રેણી જાળવી રાખે છે. બંને સ્થિતિમાં 5xx અનિર્ણાયક રહે છે, અને નકાર ક્યારેય પ્રોક્સીને દૂર, અક્ષમ અથવા ફરી સક્રિય કરતો નથી.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "મુખ્ય પૃષ્ઠ", "dashboard": "ડૅશબોર્ડ", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register પર $200 મફત ક્રેડિટ મેળવો — કોઈ ક્રેડિટ કાર્ડની જરૂર નથી.", "unorouter": "https://unorouter.ai પર એક API કી બનાવો, પછી તેને અહીં Bearer ટોકન તરીકે પેસ્ટ કરો.", "agnes": "agnes-ai.com પર API કી મેળવો", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "મફત સ્તર થોભાવવામાં આવ્યું છે (2026) — AI/ML API હવે ફક્ત પે-એઝ-યુ-ગો છે (ન્યૂનતમ $20 ટોપ-અપ); કોઈ રિકરિંગ મફત ક્રેડિટ નથી.", "ai21": "સાઇનઅપ પર $10 ટ્રાયલ ક્રેડિટ (3 મહિના માટે માન્ય), કોઈ ક્રેડિટ કાર્ડની જરૂર નથી", "alibaba": "API કી વડે Alibaba ને કનેક્ટ કરો.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP ઓડિટ (દિવસો)", "retentionA2aEvents": "A2A ઇવેન્ટ્સ (દિવસો)", "retentionCallLogs": "કૉલ લૉગ્સ (દિવસો)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "ઉપયોગ ઇતિહાસ (દિવસો)", "retentionMemoryEntries": "મેમરી એન્ટ્રીઝ (દિવસો)", "retentionXpAuditLog": "XP ઑડિટ લૉગ (દિવસો)", diff --git a/src/i18n/messages/ha.json b/src/i18n/messages/ha.json index 23e1e42268..38da19b637 100644 --- a/src/i18n/messages/ha.json +++ b/src/i18n/messages/ha.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Rukunin proxy da juyawar opencode ta kowane asusu suna daina sake amfani da proxy da ya gaza kwanan nan (ya ƙi gwajin TCP, ko aka karɓi 429 ta cikinsa) na wani lokaci na kowane tsari wanda ke ninkuwa a duk lokacin da gazawar ta sake faruwa, har zuwa iyaka. Ba a rubuta matsayin proxy; idan aka ware dukkan waɗanda za a iya zaɓa, zaɓin ba ya canzawa. A kashe yake ta tsohuwa: tsarin zaɓi daidai yake da juyawa na yau da kullum.", "featureFlagProxyPoolEgressObservationDescription": "A ƙarƙashin rukunin proxy a dashboard, nuna adadin IPs na fita da aka lura sun yi wa membobinsa hidima a cikin awanni 24 da suka gabata, adadin haɗin da suka yi amfani da su, da mafi yawan adadin da aka gani a bayan IP guda. Na karantawa kawai ne, ana lissafa shi daga kundin proxy, kuma ba a taɓa amfani da shi wajen routing. A kashe yake ta tsohuwa: editan rukuni ba ya canzawa kuma hanyar lura tana amsa null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "A yayin binciken lafiyar proxy, bari gwajin da manufa ta ƙi (401/403/429: proxy ya isar, amma wurin da ake nufi ya ƙi wannan IP na fita) ya sake saita jerin gazawar proxy masu jere, kamar gwajin da aka yi wa hidima. A kashe yake ta tsohuwa: ƙin amincewa yana kasancewa tsaka-tsaki kuma yana riƙe da jerin gazawar. 5xx yana kasancewa mara tabbas a kowane hali, kuma ƙin amincewa ba ya taɓa cirewa, kashewa ko sake kunna proxy.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Gida", "dashboard": "Allon sarrafawa", @@ -6125,6 +6126,7 @@ "agentrouter": "Samu kuɗin amfani na $200 kyauta a https://agentrouter.org/register — ba a buƙatar katin kuɗi.", "unorouter": "Ƙirƙiri API key a https://unorouter.ai, sannan liƙa shi a nan a matsayin Bearer token.", "agnes": "Samu API key a agnes-ai.com", + "agnes-cn": "__MISSING__:Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "An dakatar da matakin kyauta (2026) — yanzu AI/ML API na biyan gwargwadon amfani ne kawai (mafi ƙarancin ƙarin kuɗi $20); babu kuɗin amfani na kyauta da ke maimaituwa.", "ai21": "Kuɗin gwaji na $10 bayan yin rajista (yana aiki na tsawon watanni 3), ba a buƙatar katin kuɗi", "alibaba": "Haɗa Alibaba da API key.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Binciken MCP (kwanaki)", "retentionA2aEvents": "Abubuwan da Suka Faru na A2A (kwanaki)", "retentionCallLogs": "Rajistan Kira (kwanaki)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Tarihin Amfani (kwanaki)", "retentionMemoryEntries": "Bayanan Ƙwaƙwalwa (kwanaki)", "retentionXpAuditLog": "Rajistan Binciken XP (kwanaki)", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index 034fbd6991..4a4a8ca923 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "מאגרי פרוקסי והתחלופה לפי חשבון של opencode מפסיקים להשתמש מחדש בפרוקסי שזה עתה נכשל (בדיקת TCP נדחתה, או שהתקבלה דרכו תגובת 429) למשך פרק זמן נפרד לכל תהליך, שמוכפל בכל הישנות עד לתקרה. לא נכתב סטטוס לפרוקסי; כאשר כל המועמדים מושהים, הבחירה נשארת ללא שינוי. מושבת כברירת מחדל: סדר הבחירה זהה בדיוק לתחלופה הרגילה.", "featureFlagProxyPoolEgressObservationDescription": "הצגת מספר כתובות ה-IP הנצפות ליציאה ששירתו את חברי מאגר הפרוקסי במהלך 24 השעות האחרונות, מספר החיבורים שהשתמשו בהן והמספר המרבי שנצפה מאחורי כתובת IP אחת, מתחת למאגר בלוח הבקרה. לקריאה בלבד, מחושב מיומן הפרוקסי ולעולם אינו משמש לניתוב. מושבת כברירת מחדל: עורך המאגר נשאר ללא שינוי ונתיב התצפית מחזיר null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "בסריקת תקינות הפרוקסי, יש לאפשר לבדיקה שהיעד דחה (401/403/429: הפרוקסי העביר את הבקשה, והיעד דחה את כתובת ה-IP הזו ליציאה) לאפס את רצף הכשלים העוקבים של הפרוקסי, כמו בדיקה שנענתה. מושבת כברירת מחדל: דחייה נשארת ניטרלית ושומרת על הרצף. תגובת 5xx נשארת בלתי מכרעת בכל מקרה, ודחייה לעולם אינה מסירה, משביתה או מפעילה מחדש פרוקסי.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "בית", "dashboard": "לוח מחוונים", @@ -6125,6 +6126,7 @@ "agentrouter": "קבל $200 קרדיט חינם ב-https://agentrouter.org/register — ללא צורך בכרטיס אשראי.", "unorouter": "צור מפתח API ב- https://unorouter.ai, ואז הדבק אותו כאן כטוקן Bearer.", "agnes": "קבל מפתח API ב-agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "המסלול החינמי הושהה (2026) — AI/ML API פועל כעת במודל של תשלום לפי שימוש בלבד (טעינה מינימלית של $20); אין קרדיטים חינמיים חוזרים.", "ai21": "$10 קרדיט ניסיון בהרשמה (בתוקף ל-3 חודשים), ללא צורך בכרטיס אשראי", "alibaba": "חבר את Alibaba באמצעות מפתח API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "ביקורת MCP (ימים)", "retentionA2aEvents": "אירועי A2A (ימים)", "retentionCallLogs": "יומני שיחות (ימים)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "היסטוריית שימוש (ימים)", "retentionMemoryEntries": "ערכי זיכרון (ימים)", "retentionXpAuditLog": "יומן ביקורת XP (ימים)", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 473f9f45e8..08d2468979 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proxy पूल और opencode का प्रति-अकाउंट रोटेशन, हाल ही में विफल हुए Proxy (अस्वीकृत TCP प्रोब या उसके माध्यम से प्राप्त 429) को प्रति-प्रोसेस अवधि तक दोबारा उपयोग नहीं करता है। यह अवधि हर बार विफलता दोहराए जाने पर दोगुनी होती है, एक अधिकतम सीमा तक। Proxy की कोई स्थिति लिखी नहीं जाती; प्रत्येक उम्मीदवार को अलग रखे जाने पर चयन अपरिवर्तित रहता है। डिफ़ॉल्ट रूप से बंद: चयन क्रम बिल्कुल सामान्य रोटेशन जैसा रहता है।", "featureFlagProxyPoolEgressObservationDescription": "डैशबोर्ड में किसी Proxy पूल के अंतर्गत दिखाएँ कि पिछले 24 घंटों में कितने देखे गए एग्रेस IP ने उसके सदस्यों को सेवा दी, कितने कनेक्शनों ने उनका उपयोग किया और एक IP के पीछे अधिकतम कितने देखे गए। केवल पढ़ने योग्य, Proxy लॉग से परिकलित और रूटिंग के लिए कभी उपयोग नहीं किया जाता। डिफ़ॉल्ट रूप से बंद: पूल एडिटर अपरिवर्तित रहता है और ऑब्ज़र्वेशन रूट null लौटाता है।", "featureFlagProxyHealthBlockedResetsStreakDescription": "Proxy हेल्थ स्वीप में, लक्ष्य द्वारा अस्वीकृत प्रोब (401/403/429: Proxy ने इसे रिले किया, लेकिन गंतव्य ने इस एग्रेस IP को अस्वीकार किया) को, सेवा दिए गए प्रोब की तरह, Proxy की लगातार-विफलता शृंखला रीसेट करने दें। डिफ़ॉल्ट रूप से बंद: अस्वीकृति तटस्थ रहती है और शृंखला को बनाए रखती है। 5xx दोनों ही स्थितियों में अनिर्णायक रहता है, और कोई अस्वीकृति कभी भी Proxy को हटाती, अक्षम या फिर से सक्रिय नहीं करती।", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "घर", "dashboard": "डैशबोर्ड", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register पर $200 के मुफ्त क्रेडिट प्राप्त करें — किसी क्रेडिट कार्ड की आवश्यकता नहीं है।", "unorouter": "https://unorouter.ai पर एक API कुंजी बनाएं, फिर इसे यहां Bearer टोकन के रूप में पेस्ट करें।", "agnes": "agnes-ai.com पर API कुंजी प्राप्त करें", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "मुफ्त टियर निलंबित (2026) — AI/ML API अब केवल पे-एज़-यू-गो है (न्यूनतम $20 टॉप-अप); कोई आवर्ती मुफ्त क्रेडिट नहीं।", "ai21": "साइनअप पर $10 का ट्रायल क्रेडिट (3 महीने के लिए वैध), किसी क्रेडिट कार्ड की आवश्यकता नहीं", "alibaba": "Alibaba को एक API कुंजी से कनेक्ट करें।", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "एमसीपी ऑडिट (दिन)", "retentionA2aEvents": "A2A इवेंट (दिन)", "retentionCallLogs": "कॉल लॉग्स (दिन)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "उपयोग इतिहास (दिन)", "retentionMemoryEntries": "मेमोरी प्रविष्टियाँ (दिन)", "retentionXpAuditLog": "XP ऑडिट लॉग (दिन)", diff --git a/src/i18n/messages/hr.json b/src/i18n/messages/hr.json index 0b005309c3..e8f54d399e 100644 --- a/src/i18n/messages/hr.json +++ b/src/i18n/messages/hr.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Skupovi proxy poslužitelja i rotacija opencodea po računu privremeno prestaju ponovno posluživati proxy poslužitelj koji je upravo zakazao (odbijena TCP provjera ili odgovor 429 primljen preko njega) tijekom razdoblja po procesu koje se udvostručuje pri svakom ponavljanju, do zadanog maksimuma. Status proxy poslužitelja ne zapisuje se; kada se svi kandidati izuzmu, odabir ostaje nepromijenjen. Prema zadanim postavkama isključeno: redoslijed odabira potpuno je jednak običnoj rotaciji.", "featureFlagProxyPoolEgressObservationDescription": "Na nadzornoj ploči, ispod skupa proxy poslužitelja, prikažite koliko je opaženih izlaznih IP adresa opsluživalo njegove članove tijekom posljednja 24 h, koliko ih je veza upotrebljavalo i najveći broj veza zabilježen iza jedne IP adrese. Samo za čitanje, izračunava se iz zapisnika proxy poslužitelja i nikada se ne upotrebljava za usmjeravanje. Prema zadanim postavkama isključeno: uređivač skupa ostaje nepromijenjen, a ruta za opažanja vraća null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Tijekom provjere stanja proxy poslužitelja omogućite da provjera koju je cilj odbio (401/403/429: proxy poslužitelj proslijedio je zahtjev, a odredište je odbilo ovu izlaznu IP adresu) poništi niz uzastopnih neuspjeha proxy poslužitelja, kao i uspješno poslužena provjera. Prema zadanim postavkama isključeno: odbijanje ostaje neutralno i zadržava niz. Odgovor 5xx u oba slučaja ostaje neodređen, a odbijanje nikada ne uklanja, onemogućuje niti ponovno aktivira proxy poslužitelj.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Početna", "dashboard": "Nadzorna ploča", @@ -6125,6 +6126,7 @@ "agentrouter": "Nabavi $200 besplatnih kredita na https://agentrouter.org/register — nije potrebna kreditna kartica.", "unorouter": "Stvori API ključ na https://unorouter.ai, zatim ga ovdje zalijepi kao Bearer token.", "agnes": "Nabavi API ključ na agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Besplatna razina pauzirana (2026.) — AI/ML API sada je isključivo prema potrošnji (minimalna uplata $20); nema redovnih besplatnih kredita.", "ai21": "$10 probnih kredita pri registraciji (vrijedi 3 mjeseca), nije potrebna kreditna kartica", "alibaba": "Poveži Alibaba s API ključem.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP revizija (dani)", "retentionA2aEvents": "A2A događaji (dani)", "retentionCallLogs": "Zapisi poziva (dani)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Povijest korištenja (dani)", "retentionMemoryEntries": "Memorijski unosi (dani)", "retentionXpAuditLog": "XP revizijski dnevnik (dani)", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index c8943186f1..4b28c64cae 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "A proxykészletek és az opencode fiókonkénti rotációja egy folyamatonként meghatározott időszakig nem választja újra az éppen meghibásodott proxyt (elutasított TCP-próba vagy rajta keresztül kapott 429-es válasz); ez az időszak minden ismétlődéskor megduplázódik egy felső korlátig. A proxy állapota nem kerül rögzítésre; ha minden jelölt félre van téve, a választás változatlan marad. Alapértelmezés szerint kikapcsolva: a kiválasztási sorrend pontosan megegyezik az egyszerű rotációéval.", "featureFlagProxyPoolEgressObservationDescription": "A vezérlőpulton jelenjen meg egy proxykészlet alatt, hogy az elmúlt 24 órában hány megfigyelt kimenő IP-cím szolgálta ki a tagjait, hány kapcsolat használta ezeket, és legfeljebb hány kapcsolat volt megfigyelhető egyetlen IP-cím mögött. Csak olvasható, a proxynaplóból számított adat, amelyet a rendszer soha nem használ útválasztásra. Alapértelmezés szerint kikapcsolva: a készletszerkesztő változatlan, a megfigyelési útvonal pedig null értékkel válaszol.", "featureFlagProxyHealthBlockedResetsStreakDescription": "A proxyk állapotellenőrzése során a cél által elutasított próba (401/403/429: a proxy továbbította a kérést, a cél pedig elutasította ezt a kimenő IP-címet) a kiszolgált próbához hasonlóan nullázza a proxy egymást követő hibáinak számlálóját. Alapértelmezés szerint kikapcsolva: az elutasítás semleges marad, és nem változtatja meg a sorozatot. Az 5xx válasz mindkét esetben eldönthetetlen marad, és egy elutasítás soha nem távolít el, nem tilt le és nem aktivál újra proxyt.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Otthon", "dashboard": "Irányítópult", @@ -6125,6 +6126,7 @@ "agentrouter": "Szerezzen 200 $ ingyenes kreditet a https://agentrouter.org/register oldalon — bankkártya nem szükséges.", "unorouter": "Hozzon létre egy API kulcsot a https://unorouter.ai oldalon, majd illessze be ide Bearer tokenként.", "agnes": "Szerezzen API-kulcsot az agnes-ai.com oldalon", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Ingyenes csomag szüneteltetve (2026) — az AI/ML API mostantól csak használatalapú (min. 20 $ feltöltés); nincsenek ismétlődő ingyenes kreditek.", "ai21": "10 $ próbakredit regisztrációkor (3 hónapig érvényes), bankkártya nem szükséges", "alibaba": "Csatlakoztassa az Alibaba szolgáltatást egy API-kulccsal.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP-ellenőrzés (nap)", "retentionA2aEvents": "A2A események (nap)", "retentionCallLogs": "Hívásnaplók (nap)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Használati előzmények (nap)", "retentionMemoryEntries": "Memóriabejegyzések (nap)", "retentionXpAuditLog": "XP auditnapló (nap)", diff --git a/src/i18n/messages/hy.json b/src/i18n/messages/hy.json index 53ddf911b5..de333ca271 100644 --- a/src/i18n/messages/hy.json +++ b/src/i18n/messages/hy.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Պրոքսի փուլերը և opencode-ի՝ ըստ հաշվի ռոտացիան դադարում են կրկին տրամադրել հենց նոր ձախողված պրոքսին (մերժված TCP ստուգում կամ դրա միջոցով ստացված 429)՝ յուրաքանչյուր գործընթացի համար սահմանված ժամանակահատվածով, որը յուրաքանչյուր կրկնության դեպքում կրկնապատկվում է՝ մինչև առավելագույն սահմանաչափը։ Պրոքսիի որևէ կարգավիճակ չի գրանցվում․ երբ բոլոր թեկնածուները մի կողմ են դրված, ընտրությունը մնում է անփոփոխ։ Լռելյայն անջատված է․ ընտրության հերթականությունը ճշգրտորեն համապատասխանում է սովորական ռոտացիային։", "featureFlagProxyPoolEgressObservationDescription": "Վահանակում՝ պրոքսի փուլի տակ, ցույց տալ, թե վերջին 24 ժ-ի ընթացքում քանի դիտարկված ելքային IP է սպասարկել դրա անդամներին, քանի միացում է օգտագործել դրանք, և մեկ IP-ի հետևում առավելագույնը քանիսն է դիտվել։ Միայն կարդալու համար է, հաշվարկվում է պրոքսիի մատյանից և երբեք չի օգտագործվում երթուղավորման համար։ Լռելյայն անջատված է․ փուլի խմբագրիչը մնում է անփոփոխ, իսկ դիտարկման երթուղին վերադարձնում է null։", "featureFlagProxyHealthBlockedResetsStreakDescription": "Պրոքսիի առողջական վիճակի ստուգման ընթացքում թույլ տալ, որ թիրախի կողմից մերժված ստուգումը (401/403/429․ պրոքսին փոխանցել է հարցումը, իսկ նպատակակետը մերժել է այս ելքային IP-ն) զրոյացնի պրոքսիի հաջորդական ձախողումների շարքը՝ ինչպես սպասարկված ստուգման դեպքում։ Լռելյայն անջատված է․ մերժումը մնում է չեզոք և պահպանում է շարքը։ 5xx-ը երկու դեպքում էլ մնում է անորոշ, իսկ մերժումը երբեք չի հեռացնում, անջատում կամ վերաակտիվացնում պրոքսին։", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Գլխավոր էջ", "dashboard": "Կառավարման վահանակ", @@ -6125,6 +6126,7 @@ "agentrouter": "Ստացեք $200 անվճար վարկային միջոցներ https://agentrouter.org/register հասցեում․ վարկային քարտ չի պահանջվում։", "unorouter": "Ստեղծեք API բանալի https://unorouter.ai կայքում, ապա տեղադրեք այն այստեղ որպես Bearer token։", "agnes": "Ստանալ API բանալի agnes-ai.com կայքում", + "agnes-cn": "__MISSING__:Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Անվճար սակագինը դադարեցված է (2026)․ AI/ML API-ն այժմ գործում է միայն ըստ օգտագործման վճարման սկզբունքով (նվազագույնը՝ $20 համալրում), պարբերաբար տրամադրվող անվճար վարկային միջոցներ չկան։", "ai21": "Գրանցվելիս տրամադրվում են $10 փորձնական վարկային միջոցներ (վավեր են 3 ամիս), վարկային քարտ չի պահանջվում", "alibaba": "Միացնել Alibaba-ն API բանալիով։", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP աուդիտ (օրեր)", "retentionA2aEvents": "A2A իրադարձություններ (օրեր)", "retentionCallLogs": "Զանգերի մատյաններ (օրեր)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Օգտագործման պատմություն (օրեր)", "retentionMemoryEntries": "Հիշողության գրառումներ (օրեր)", "retentionXpAuditLog": "XP աուդիտի մատյան (օրեր)", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index 5976b32649..c1eabae8ed 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Pool proxy dan rotasi per akun opencode berhenti menyajikan kembali proxy yang baru saja gagal (probe TCP ditolak atau respons 429 diterima melaluinya) selama periode per proses yang berlipat ganda setiap kali kegagalan berulang, hingga batas maksimum. Status proxy tidak ditulis; jika setiap kandidat disisihkan, pilihannya tetap tidak berubah. Nonaktif secara default: urutan pemilihan sama persis dengan rotasi biasa.", "featureFlagProxyPoolEgressObservationDescription": "Tampilkan, di bawah pool proxy pada dasbor, jumlah IP keluar yang teramati melayani anggotanya selama 24 jam terakhir, jumlah koneksi yang menggunakannya, dan jumlah terbanyak yang terlihat di balik satu IP. Hanya-baca, dihitung dari log proxy, dan tidak pernah digunakan untuk perutean. Nonaktif secara default: editor pool tidak berubah dan rute observasi memberikan jawaban null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Dalam pemeriksaan kesehatan proxy, izinkan probe yang ditolak oleh target (401/403/429: proxy meneruskan, tetapi tujuan menolak IP keluar ini) mereset rentetan kegagalan berturut-turut proxy, seperti probe yang berhasil dilayani. Nonaktif secara default: penolakan tetap netral dan mempertahankan rentetan tersebut. Respons 5xx tetap tidak meyakinkan dalam kedua kasus, dan penolakan tidak pernah menghapus, menonaktifkan, atau mengaktifkan kembali proxy.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Rumah", "dashboard": "Dasbor", @@ -6125,6 +6126,7 @@ "agentrouter": "Dapatkan kredit gratis $200 di https://agentrouter.org/register — tidak memerlukan kartu kredit.", "unorouter": "Buat kunci API di https://unorouter.ai, lalu tempelkan di sini sebagai token Bearer.", "agnes": "Dapatkan kunci API di agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Tingkat gratis ditangguhkan (2026) — AI/ML API sekarang hanya bayar sesuai pemakaian (isi ulang min $20); tidak ada kredit gratis berulang.", "ai21": "Kredit uji coba $10 saat mendaftar (berlaku 3 bulan), tidak memerlukan kartu kredit", "alibaba": "Hubungkan Alibaba dengan kunci API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Audit MCP (hari)", "retentionA2aEvents": "Acara A2A (hari)", "retentionCallLogs": "Log Panggilan (hari)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Riwayat Penggunaan (hari)", "retentionMemoryEntries": "Entri Memori (hari)", "retentionXpAuditLog": "Log Audit XP (hari)", diff --git a/src/i18n/messages/ig.json b/src/i18n/messages/ig.json index 4452d5613b..69a8b22120 100644 --- a/src/i18n/messages/ig.json +++ b/src/i18n/messages/ig.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Ọdọ proxy na ntụgharị opencode nke akaụntụ ọ bụla na-akwụsị iji proxy dara ugbu a ọzọ (nnyocha TCP a jụrụ, ma ọ bụ nzaghachi 429 e nwetara site na ya) ruo oge nke usoro ọ bụla nke na-amụba okpukpu abụọ mgbe ọdịda ahụ megharịrị, ruo n'ókè kachasị. A naghị ede ọkwa proxy ọ bụla; mgbe e wepụrụ ndị niile a ga-ahọrọ n'akụkụ, nhọrọ ahụ anaghị agbanwe. A gbanyụrụ ya na ndabara: usoro nhọrọ bụ kpọmkwem ntụgharị nkịtị.", "featureFlagProxyPoolEgressObservationDescription": "Gosi, n'okpuru ọdọ proxy na dashboard, IP ọpụpụ ole a hụrụ jere ndị otu ya ozi n'ime awa 24 gara aga, njikọ ole jiri ha, na ọnụ ọgụgụ kachasị a hụrụ n'azụ otu IP. Ọ bụ naanị maka ọgụgụ, a na-agbakọ ya site na ndekọ proxy, a naghị eji ya eme routing. A gbanyụrụ ya na ndabara: editọ ọdọ ahụ anaghị agbanwe, route nlele ahụ na-azaghachi null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "N'oge nyocha ahụike proxy, kwe ka nyocha ebumnuche jụrụ (401/403/429: proxy ahụ zigara ya, ebe njedebe ahụ jụrụ IP ọpụpụ a) tọgharịa usoro ọdịda na-aga n'ihu nke proxy ahụ, dịka nyocha e jere ozi. A gbanyụrụ ya na ndabara: ọjụjụ na-anọpụ iche ma na-edobe usoro ahụ. 5xx ka na-enweghị nkwubi okwu n'ọnọdụ ọ bụla, ọjụjụ anaghị ewepụ, gbanyụọ, ma ọ bụ mee ka proxy rụọ ọrụ ọzọ.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Ụlọ", "dashboard": "Ogwe njikwa", @@ -6125,6 +6126,7 @@ "agentrouter": "Nweta kredit efu $200 na https://agentrouter.org/register — achọghị kaadị kredit.", "unorouter": "Mepụta API key na https://unorouter.ai, wee mado ya ebe a dị ka token Bearer.", "agnes": "Nweta API key na agnes-ai.com", + "agnes-cn": "__MISSING__:Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Akwụsịla ọkwa efu nwa oge (2026) — AI/ML API na-akwụzi naanị dịka ojiji siri dị (ntinye ego kacha nta bụ $20); enweghị kredit efu na-emegharị kwa oge.", "ai21": "Kredit nnwale $10 mgbe ị debanyere aha (ọ na-adị irè ọnwa 3), achọghị kaadị kredit", "alibaba": "Jikọọ Alibaba site na API key.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Nyocha MCP (ụbọchị)", "retentionA2aEvents": "Ihe Omume A2A (ụbọchị)", "retentionCallLogs": "Ndekọ Oku (ụbọchị)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Akụkọ Ojiji (ụbọchị)", "retentionMemoryEntries": "Ndenye Ncheta (ụbọchị)", "retentionXpAuditLog": "Ndekọ Nyocha XP (ụbọchị)", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 39dfe2aeef..733a0d78ce 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "I pool di proxy e la rotazione per account di opencode smettono temporaneamente di riproporre un proxy che ha appena avuto un errore (sonda TCP rifiutata o risposta 429 ricevuta tramite il proxy) per un periodo per processo che raddoppia a ogni ripetizione, fino a un limite massimo. Non viene registrato alcuno stato del proxy; se tutti i candidati vengono messi da parte, la scelta rimane invariata. Disattivato per impostazione predefinita: l'ordine di selezione corrisponde esattamente alla normale rotazione.", "featureFlagProxyPoolEgressObservationDescription": "Mostra, sotto un pool di proxy nella dashboard, quanti IP di uscita osservati hanno servito i suoi membri nelle ultime 24 ore, quante connessioni li hanno utilizzati e il numero massimo di connessioni osservate dietro un singolo IP. In sola lettura, calcolato dal log dei proxy e mai utilizzato per l'instradamento. Disattivato per impostazione predefinita: l'editor dei pool rimane invariato e la route di osservazione restituisce null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Durante la scansione dello stato dei proxy, consente a una sonda rifiutata dalla destinazione (401/403/429: il proxy ha inoltrato la richiesta, ma la destinazione ha rifiutato questo IP di uscita) di azzerare la serie di errori consecutivi del proxy, come una sonda servita. Disattivato per impostazione predefinita: un rifiuto rimane neutro e mantiene la serie. Una risposta 5xx rimane inconcludente in entrambi i casi e un rifiuto non rimuove, disabilita o riattiva mai un proxy.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Casa", "dashboard": "Pannello di controllo", @@ -6125,6 +6126,7 @@ "agentrouter": "Ottieni $200 di crediti gratuiti su https://agentrouter.org/register — nessuna carta di credito richiesta.", "unorouter": "Crea una chiave API su https://unorouter.ai, quindi incollala qui come token Bearer.", "agnes": "Ottieni la chiave API su agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Piano gratuito in pausa (2026) — AI/ML API è ora solo a consumo (ricarica minima $20); nessun credito gratuito ricorrente.", "ai21": "$10 di crediti di prova alla registrazione (validi 3 mesi), nessuna carta di credito richiesta", "alibaba": "Connetti Alibaba con una chiave API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Audit MCP (giorni)", "retentionA2aEvents": "Eventi A2A (giorni)", "retentionCallLogs": "Registri delle chiamate (giorni)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Cronologia utilizzo (giorni)", "retentionMemoryEntries": "Voci di memoria (giorni)", "retentionXpAuditLog": "Log di controllo XP (giorni)", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index edad2e2f6b..e5a063fe04 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "プロキシプールと、アカウントごとのopencodeローテーションで、直前に失敗したプロキシ(TCPプローブが拒否された、またはそのプロキシ経由で429を受信したもの)の再使用を、同一プロセス内で一定期間停止します。この期間は失敗を繰り返すたびに倍増し、上限があります。プロキシのステータスは書き込まれません。すべての候補が除外された場合、選択は変更されません。デフォルトではオフです。有効にしない限り、選択順序は通常のローテーションとまったく同じです。", "featureFlagProxyPoolEgressObservationDescription": "ダッシュボードのプロキシプールの下に、過去24時間にそのメンバーが使用したことが観測された送信元IPの数、それらを使用した接続数、および1つのIPの背後で観測された最大数を表示します。読み取り専用で、プロキシログから算出され、ルーティングには使用されません。デフォルトではオフです。プールエディターは変更されず、観測ルートはnullを返します。", "featureFlagProxyHealthBlockedResetsStreakDescription": "プロキシのヘルススイープで、ターゲットに拒否されたプローブ(401/403/429:プロキシは中継したが、宛先がこの送信元IPを拒否したもの)が、正常に処理されたプローブと同様に、プロキシの連続失敗回数をリセットできるようにします。デフォルトではオフです。拒否は中立のままで、連続失敗回数は維持されます。どちらの場合も5xxは判定不能のままであり、拒否によってプロキシが削除、無効化、または再有効化されることはありません。", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "ホーム", "dashboard": "ダッシュボード", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register で200ドルの無料クレジットを取得 — クレジットカードは不要です。", "unorouter": "https://unorouter.ai で API キーを作成し、ここに Bearer トークンとして貼り付けてください。", "agnes": "agnes-ai.com でAPIキーを取得", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "無料枠は一時停止中(2026年) — AI/ML APIは従量課金制のみ(最低20ドルのチャージが必要)となり、定期的な無料クレジットはありません。", "ai21": "サインアップ時に10ドルのトライアルクレジット(3か月間有効)、クレジットカード不要", "alibaba": "APIキーでAlibabaに接続します。", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP 監査 (日)", "retentionA2aEvents": "A2A イベント (日)", "retentionCallLogs": "通話記録 (日)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "利用履歴(日)", "retentionMemoryEntries": "メモリエントリ (日)", "retentionXpAuditLog": "XP監査ログ (日)", diff --git a/src/i18n/messages/ka.json b/src/i18n/messages/ka.json index f4b4078485..15d534eedd 100644 --- a/src/i18n/messages/ka.json +++ b/src/i18n/messages/ka.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "პროქსი-პულები და opencode-ის თითოეულ ანგარიშზე როტაცია აღარ იყენებს ახლახან ჩავარდნილ პროქსის (უარყოფილი TCP შემოწმება ან მისი მეშვეობით მიღებული 429) თითოეული პროცესისთვის განსაზღვრული პერიოდით, რომელიც ყოველი განმეორებისას ორმაგდება მაქსიმალურ ზღვრამდე. პროქსის სტატუსი არ იწერება; ყველა კანდიდატის გვერდზე გადადების შემთხვევაში არჩევანი უცვლელი რჩება. ნაგულისხმევად გამორთულია: არჩევის თანმიმდევრობა ზუსტად ჩვეულებრივი როტაციის იდენტურია.", "featureFlagProxyPoolEgressObservationDescription": "დეშბორდზე, პროქსი-პულის ქვეშ, აჩვენეთ ბოლო 24 საათში რამდენი დაფიქსირებული გამავალი IP ემსახურებოდა მის წევრებს, რამდენმა კავშირმა გამოიყენა ისინი და ყველაზე მეტი რამდენი კავშირი დაფიქსირდა ერთი IP-ის უკან. მხოლოდ წაკითხვისთვისაა, გამოითვლება პროქსის ჟურნალიდან და არასოდეს გამოიყენება მარშრუტიზაციისთვის. ნაგულისხმევად გამორთულია: პულის რედაქტორი უცვლელია, ხოლო დაკვირვების მარშრუტი აბრუნებს null-ს.", "featureFlagProxyHealthBlockedResetsStreakDescription": "პროქსის მდგომარეობის შემოწმებისას, სამიზნის მიერ უარყოფილმა მოთხოვნამ (401/403/429: პროქსიმ გადააგზავნა მოთხოვნა, ხოლო დანიშნულების მხარემ უარყო ეს გამავალი IP) გაანულოს პროქსის ზედიზედ წარუმატებლობათა სერია, როგორც წარმატებით შესრულებული შემოწმებისას. ნაგულისხმევად გამორთულია: უარყოფა ნეიტრალური რჩება და სერიას ინარჩუნებს. 5xx ნებისმიერ შემთხვევაში გაურკვეველ შედეგად რჩება, ხოლო უარყოფა არასოდეს შლის, თიშავს ან ხელახლა ააქტიურებს პროქსის.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "მთავარი", "dashboard": "მართვის პანელი", @@ -6125,6 +6126,7 @@ "agentrouter": "მიიღეთ $200-ის უფასო კრედიტები https://agentrouter.org/register-ზე — საკრედიტო ბარათი საჭირო არ არის.", "unorouter": "შექმენით API-გასაღები https://unorouter.ai-ზე, შემდეგ ჩასვით აქ Bearer ტოკენის სახით.", "agnes": "მიიღეთ API-გასაღები agnes-ai.com-ზე", + "agnes-cn": "__MISSING__:Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "უფასო ტარიფი შეჩერებულია (2026) — AI/ML API ახლა მხოლოდ მოხმარების მიხედვით ფასიანია (მინიმალური შევსება $20); პერიოდული უფასო კრედიტები აღარ არის.", "ai21": "რეგისტრაციისას მიიღებთ $10-ის საცდელ კრედიტებს (მოქმედებს 3 თვე); საკრედიტო ბარათი საჭირო არ არის", "alibaba": "დააკავშირეთ Alibaba API-გასაღებით.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP აუდიტი (დღე)", "retentionA2aEvents": "A2A მოვლენები (დღე)", "retentionCallLogs": "ზარების ჟურნალები (დღე)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "გამოყენების ისტორია (დღე)", "retentionMemoryEntries": "მეხსიერების ჩანაწერები (დღე)", "retentionXpAuditLog": "XP აუდიტის ჟურნალი (დღე)", diff --git a/src/i18n/messages/km.json b/src/i18n/messages/km.json index b9b9489f98..5b1ee4758c 100644 --- a/src/i18n/messages/km.json +++ b/src/i18n/messages/km.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "ក្រុមប្រូកស៊ី និងការបង្វិលតាមគណនីនីមួយៗរបស់ opencode នឹងផ្អាកការប្រើប្រូកស៊ីដែលទើបតែបរាជ័យឡើងវិញ (ការស្ទង់ TCP ត្រូវបានបដិសេធ ឬទទួលបាន 429 តាមរយៈវា) ក្នុងរយៈពេលមួយសម្រាប់ដំណើរការនីមួយៗ ដែលកើនទ្វេដងរាល់ពេលកើតឡើងម្តងទៀត រហូតដល់កម្រិតអតិបរមា។ គ្មានស្ថានភាពប្រូកស៊ីត្រូវបានកត់ត្រាទេ ហើយនៅពេលបេក្ខភាពទាំងអស់ត្រូវបានទុកមួយឡែក ជម្រើសនៅតែមិនផ្លាស់ប្តូរ។ បិទតាមលំនាំដើម៖ លំដាប់ជ្រើសរើសគឺដូចគ្នាទាំងស្រុងនឹងការបង្វិលធម្មតា។", "featureFlagProxyPoolEgressObservationDescription": "បង្ហាញនៅក្រោមក្រុមប្រូកស៊ីក្នុងផ្ទាំងគ្រប់គ្រងថា ក្នុងរយៈពេល 24 ម៉ោងចុងក្រោយ មាន IP ចេញក្រៅដែលបានសង្កេតឃើញចំនួនប៉ុន្មានបានបម្រើសមាជិករបស់ក្រុម មានការតភ្ជាប់ចំនួនប៉ុន្មានបានប្រើពួកវា និងចំនួនអតិបរមាដែលបានឃើញនៅពីក្រោយ IP តែមួយ។ សម្រាប់តែអានប៉ុណ្ណោះ គណនាពីកំណត់ហេតុប្រូកស៊ី និងមិនដែលប្រើសម្រាប់ការកំណត់ផ្លូវទេ។ បិទតាមលំនាំដើម៖ កម្មវិធីកែសម្រួលក្រុមមិនផ្លាស់ប្តូរ ហើយផ្លូវសង្កេតឆ្លើយតបជា null។", "featureFlagProxyHealthBlockedResetsStreakDescription": "ក្នុងការត្រួតពិនិត្យសុខភាពប្រូកស៊ី អនុញ្ញាតឱ្យការស្ទង់ដែលគោលដៅបានបដិសេធ (401/403/429៖ ប្រូកស៊ីបានបញ្ជូនបន្ត ប៉ុន្តែគោលដៅបានបដិសេធ IP ចេញក្រៅនេះ) កំណត់ចំនួនបរាជ័យជាប់ៗគ្នារបស់ប្រូកស៊ីឡើងវិញ ដូចការស្ទង់ដែលត្រូវបានបម្រើ។ បិទតាមលំនាំដើម៖ ការបដិសេធនៅតែអព្យាក្រឹត និងរក្សាចំនួនបរាជ័យជាប់ៗគ្នា។ 5xx នៅតែមិនអាចសន្និដ្ឋានបានក្នុងករណីទាំងពីរ ហើយការបដិសេធមិនដែលលុប បិទ ឬធ្វើឱ្យប្រូកស៊ីសកម្មឡើងវិញទេ។", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "ទំព័រដើម", "dashboard": "ផ្ទាំងគ្រប់គ្រង", @@ -6125,6 +6126,7 @@ "agentrouter": "ទទួលក្រេឌីតឥតគិតថ្លៃ $200 នៅ https://agentrouter.org/register — មិនត្រូវការកាតឥណទានទេ។", "unorouter": "បង្កើត API key នៅ https://unorouter.ai បន្ទាប់មកបិទភ្ជាប់វានៅទីនេះជាថូខិន Bearer។", "agnes": "ទទួល API key នៅ agnes-ai.com", + "agnes-cn": "__MISSING__:Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "កម្រិតឥតគិតថ្លៃត្រូវបានផ្អាក (2026) — AI/ML API ឥឡូវនេះគិតថ្លៃតាមការប្រើប្រាស់តែប៉ុណ្ណោះ (បញ្ចូលទឹកប្រាក់អប្បបរមា $20) ហើយមិនមានក្រេឌីតឥតគិតថ្លៃជាប្រចាំទេ។", "ai21": "ក្រេឌីតសាកល្បង $10 នៅពេលចុះឈ្មោះ (មានសុពលភាព 3 ខែ) មិនត្រូវការកាតឥណទានទេ", "alibaba": "តភ្ជាប់ Alibaba ដោយប្រើ API key។", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "កំណត់ហេតុសវនកម្ម MCP (ថ្ងៃ)", "retentionA2aEvents": "ព្រឹត្តិការណ៍ A2A (ថ្ងៃ)", "retentionCallLogs": "កំណត់ហេតុការហៅ (ថ្ងៃ)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "ប្រវត្តិការប្រើប្រាស់ (ថ្ងៃ)", "retentionMemoryEntries": "ធាតុអង្គចងចាំ (ថ្ងៃ)", "retentionXpAuditLog": "កំណត់ហេតុសវនកម្ម XP (ថ្ងៃ)", diff --git a/src/i18n/messages/kn.json b/src/i18n/messages/kn.json index e78bdcf16e..cf21efd97a 100644 --- a/src/i18n/messages/kn.json +++ b/src/i18n/messages/kn.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "ಪ್ರಾಕ್ಸಿ ಪೂಲ್ಗಳು ಮತ್ತು opencode ನ ಪ್ರತಿ-ಖಾತೆ ರೊಟೇಶನ್, ಇತ್ತೀಚೆಗೆ ವಿಫಲವಾದ ಪ್ರಾಕ್ಸಿಯನ್ನು (TCP ಪ್ರೋಬ್ ನಿರಾಕರಿಸಲ್ಪಟ್ಟಿದ್ದರೆ ಅಥವಾ ಅದರ ಮೂಲಕ 429 ಬಂದಿದ್ದರೆ) ಪ್ರತಿ ಪ್ರಕ್ರಿಯೆಗೆ ಅನ್ವಯಿಸುವ ಅವಧಿಯವರೆಗೆ ಮತ್ತೆ ಬಳಸುವುದಿಲ್ಲ. ಪ್ರತಿ ಪುನರಾವರ್ತನೆಯಲ್ಲೂ ಈ ಅವಧಿ ದ್ವಿಗುಣಗೊಳ್ಳುತ್ತದೆ, ನಿಗದಿತ ಗರಿಷ್ಠ ಮಿತಿಯವರೆಗೆ. ಯಾವುದೇ ಪ್ರಾಕ್ಸಿ ಸ್ಥಿತಿಯನ್ನು ಬರೆಯಲಾಗುವುದಿಲ್ಲ; ಪ್ರತಿಯೊಂದು ಅಭ್ಯರ್ಥಿಯನ್ನೂ ಬದಿಗಿರಿಸಿದಾಗ ಆಯ್ಕೆಯು ಬದಲಾಗುವುದಿಲ್ಲ. ಪೂರ್ವನಿಯೋಜಿತವಾಗಿ ಆಫ್: ಆಯ್ಕೆಯ ಕ್ರಮವು ಸರಳ ರೊಟೇಶನ್ನಂತೆಯೇ ಇರುತ್ತದೆ.", "featureFlagProxyPoolEgressObservationDescription": "ಡ್ಯಾಶ್ಬೋರ್ಡ್ನಲ್ಲಿ ಪ್ರಾಕ್ಸಿ ಪೂಲ್ನ ಅಡಿಯಲ್ಲಿ, ಕಳೆದ 24 ಗಂಟೆಗಳಲ್ಲಿ ಅದರ ಸದಸ್ಯರಿಗೆ ಸೇವೆ ನೀಡಿದ ಗಮನಿಸಲಾದ ಎಗ್ರೆಸ್ IPಗಳ ಸಂಖ್ಯೆ, ಅವುಗಳನ್ನು ಬಳಸಿದ ಸಂಪರ್ಕಗಳ ಸಂಖ್ಯೆ ಮತ್ತು ಒಂದೇ IPಯ ಹಿಂದೆ ಕಂಡುಬಂದ ಗರಿಷ್ಠ ಸಂಖ್ಯೆಯನ್ನು ತೋರಿಸಿ. ಇದು ಓದಲು ಮಾತ್ರ; ಪ್ರಾಕ್ಸಿ ಲಾಗ್ನಿಂದ ಲೆಕ್ಕ ಹಾಕಲಾಗುತ್ತದೆ ಮತ್ತು ರೂಟಿಂಗ್ಗಾಗಿ ಎಂದಿಗೂ ಬಳಸಲಾಗುವುದಿಲ್ಲ. ಪೂರ್ವನಿಯೋಜಿತವಾಗಿ ಆಫ್: ಪೂಲ್ ಎಡಿಟರ್ ಬದಲಾಗುವುದಿಲ್ಲ ಮತ್ತು ವೀಕ್ಷಣಾ ರೂಟ್ null ಎಂದು ಉತ್ತರಿಸುತ್ತದೆ.", "featureFlagProxyHealthBlockedResetsStreakDescription": "ಪ್ರಾಕ್ಸಿ ಆರೋಗ್ಯ ಪರಿಶೀಲನೆಯಲ್ಲಿ, ಗುರಿಯು ನಿರಾಕರಿಸಿದ ಪ್ರೋಬ್ (401/403/429: ಪ್ರಾಕ್ಸಿಯು ರಿಲೇ ಮಾಡಿದೆ, ಆದರೆ ಗಮ್ಯಸ್ಥಾನವು ಈ ಎಗ್ರೆಸ್ IPಯನ್ನು ನಿರಾಕರಿಸಿದೆ) ಸೇವೆ ಸಲ್ಲಿಸಿದ ಪ್ರೋಬ್ನಂತೆಯೇ ಪ್ರಾಕ್ಸಿಯ ಸತತ-ವೈಫಲ್ಯ ಸರಣಿಯನ್ನು ಮರುಹೊಂದಿಸಲು ಅನುಮತಿಸಿ. ಪೂರ್ವನಿಯೋಜಿತವಾಗಿ ಆಫ್: ನಿರಾಕರಣೆಯು ತಟಸ್ಥವಾಗಿಯೇ ಉಳಿದು ಸರಣಿಯನ್ನು ಹಾಗೆಯೇ ಇಡುತ್ತದೆ. ಎರಡೂ ಸಂದರ್ಭಗಳಲ್ಲಿ 5xx ಅನಿರ್ಣಾಯಕವಾಗಿಯೇ ಉಳಿಯುತ್ತದೆ ಮತ್ತು ನಿರಾಕರಣೆಯು ಪ್ರಾಕ್ಸಿಯನ್ನು ಎಂದಿಗೂ ತೆಗೆದುಹಾಕುವುದಿಲ್ಲ, ನಿಷ್ಕ್ರಿಯಗೊಳಿಸುವುದಿಲ್ಲ ಅಥವಾ ಮರುಸಕ್ರಿಯಗೊಳಿಸುವುದಿಲ್ಲ.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "ಹೋಮ್", "dashboard": "ಡ್ಯಾಶ್ಬೋರ್ಡ್", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register ನಲ್ಲಿ $200 ಉಚಿತ ಕ್ರೆಡಿಟ್ಗಳನ್ನು ಪಡೆಯಿರಿ — ಕ್ರೆಡಿಟ್ ಕಾರ್ಡ್ ಅಗತ್ಯವಿಲ್ಲ.", "unorouter": "https://unorouter.ai ನಲ್ಲಿ API ಕೀ ರಚಿಸಿ, ನಂತರ ಅದನ್ನು Bearer ಟೋಕನ್ ಆಗಿ ಇಲ್ಲಿ ಅಂಟಿಸಿ.", "agnes": "agnes-ai.com ನಲ್ಲಿ API ಕೀ ಪಡೆಯಿರಿ", + "agnes-cn": "__MISSING__:Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "ಉಚಿತ ಶ್ರೇಣಿಯನ್ನು ಸ್ಥಗಿತಗೊಳಿಸಲಾಗಿದೆ (2026) — AI/ML API ಈಗ ಬಳಕೆಯಷ್ಟು ಪಾವತಿಸುವ ಆಯ್ಕೆಯಲ್ಲಿ ಮಾತ್ರ ಲಭ್ಯವಿದೆ (ಕನಿಷ್ಠ $20 ಟಾಪ್-ಅಪ್); ಮರುಕಳಿಸುವ ಉಚಿತ ಕ್ರೆಡಿಟ್ಗಳಿಲ್ಲ.", "ai21": "ನೋಂದಣಿಯ ವೇಳೆ $10 ಪ್ರಯೋಗಾತ್ಮಕ ಕ್ರೆಡಿಟ್ಗಳು (3 ತಿಂಗಳು ಮಾನ್ಯ), ಕ್ರೆಡಿಟ್ ಕಾರ್ಡ್ ಅಗತ್ಯವಿಲ್ಲ", "alibaba": "API ಕೀ ಬಳಸಿ Alibaba ಅನ್ನು ಸಂಪರ್ಕಿಸಿ.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP ಲೆಕ್ಕಪರಿಶೋಧನೆ (ದಿನಗಳು)", "retentionA2aEvents": "A2A ಈವೆಂಟ್ಗಳು (ದಿನಗಳು)", "retentionCallLogs": "ಕರೆ ಲಾಗ್ಗಳು (ದಿನಗಳು)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "ಬಳಕೆಯ ಇತಿಹಾಸ (ದಿನಗಳು)", "retentionMemoryEntries": "ಮೆಮೊರಿ ನಮೂದುಗಳು (ದಿನಗಳು)", "retentionXpAuditLog": "XP ಲೆಕ್ಕಪರಿಶೋಧನಾ ಲಾಗ್ (ದಿನಗಳು)", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 6d4047278f..c5dde70cde 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "프록시 풀과 opencode의 계정별 로테이션은 방금 실패한 프록시(TCP 프로브 거부 또는 해당 프록시를 통해 429 응답 수신)를 프로세스별 유예 기간 동안 다시 제공하지 않습니다. 이 기간은 실패가 반복될 때마다 두 배로 늘어나며 상한이 있습니다. 프록시 상태는 기록되지 않으며, 모든 후보가 제외되면 선택 결과는 변경되지 않습니다. 기본적으로 꺼져 있으며, 선택 순서는 일반 로테이션과 정확히 같습니다.", "featureFlagProxyPoolEgressObservationDescription": "대시보드의 프록시 풀 아래에 지난 24시간 동안 해당 풀의 구성원에 사용된 것으로 관찰된 송신 IP 수, 이를 사용한 연결 수, 단일 IP에서 관찰된 최대 연결 수를 표시합니다. 읽기 전용이며 프록시 로그에서 계산되고 라우팅에는 절대 사용되지 않습니다. 기본적으로 꺼져 있으며, 풀 편집기는 변경되지 않고 관찰 라우트는 null을 반환합니다.", "featureFlagProxyHealthBlockedResetsStreakDescription": "프록시 상태 점검에서 대상이 거부한 프로브(401/403/429: 프록시는 전달했지만 대상이 이 송신 IP를 거부함)가 정상 처리된 프로브처럼 프록시의 연속 실패 횟수를 초기화하도록 합니다. 기본적으로 꺼져 있으며, 거부는 중립으로 유지되어 연속 실패 횟수에 영향을 주지 않습니다. 어느 경우든 5xx는 판단 불가로 유지되며, 거부로 인해 프록시가 제거, 비활성화 또는 재활성화되는 일은 없습니다.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "홈", "dashboard": "대시보드", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register에서 $200 무료 크레딧을 받으세요 — 신용카드가 필요하지 않습니다.", "unorouter": "https://unorouter.ai에서 API 키를 생성한 후, 여기에 Bearer 토큰으로 붙여넣으세요.", "agnes": "agnes-ai.com에서 API 키를 받으세요.", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "무료 티어 일시 중단(2026년) — AI/ML API는 이제 종량제(최소 $20 충전)로만 제공되며, 정기적인 무료 크레딧은 제공되지 않습니다.", "ai21": "가입 시 $10 체험 크레딧 제공(3개월간 유효), 신용카드 필요 없음", "alibaba": "API 키로 Alibaba를 연결합니다.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP 감사(일)", "retentionA2aEvents": "A2A 이벤트(일)", "retentionCallLogs": "호출 로그(일)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "이용내역(일)", "retentionMemoryEntries": "메모리 항목(일)", "retentionXpAuditLog": "XP 감사 로그 보존 기간(일)", diff --git a/src/i18n/messages/lt.json b/src/i18n/messages/lt.json index d0a8f3b3b6..eb6f2d6639 100644 --- a/src/i18n/messages/lt.json +++ b/src/i18n/messages/lt.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Tarpinių serverių telkiniai ir kiekvienai paskyrai taikoma opencode rotacija pakartotinai nebenaudoja ką tik nesuveikusio tarpinio serverio (atmestas TCP patikrinimas arba per jį gautas 429 atsakymas) tam procesui nustatytą laikotarpį, kuris po kiekvieno pasikartojimo padvigubėja iki nustatytos ribos. Tarpinio serverio būsena neįrašoma; atidėjus visus kandidatus, pasirinkimas nesikeičia. Pagal numatytąsias nuostatas išjungta: pasirinkimo tvarka yra lygiai tokia pati kaip įprastos rotacijos.", "featureFlagProxyPoolEgressObservationDescription": "Ataskaitų srityje po tarpinių serverių telkiniu rodyti, kiek stebėtų išeinančių IP adresų per pastarąsias 24 val. aptarnavo jo narius, kiek ryšių juos naudojo ir didžiausią už vieno IP adreso matytą skaičių. Tik skaitymui, apskaičiuojama pagal tarpinių serverių žurnalą ir niekada nenaudojama maršrutizavimui. Pagal numatytąsias nuostatas išjungta: telkinio redagavimo priemonė nesikeičia, o stebėjimo maršrutas grąžina null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Atliekant tarpinių serverių būklės patikrą leisti paskirties vietos atmestam patikrinimui (401/403/429: tarpinis serveris persiuntė užklausą, o paskirties vieta atmetė šį išeinantį IP adresą) iš naujo nustatyti tarpinio serverio nuoseklių nesėkmių seką, kaip ir aptarnauto patikrinimo atveju. Pagal numatytąsias nuostatas išjungta: atmetimas lieka neutralus ir išsaugo seką. 5xx atsakymas abiem atvejais lieka neapibrėžtas, o atmetimas niekada nepašalina, neišjungia ir iš naujo nesuaktyvina tarpinio serverio.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Pradžia", "dashboard": "Suvestinė", @@ -6125,6 +6126,7 @@ "agentrouter": "Gaukite $200 nemokamų kreditų adresu https://agentrouter.org/register — kredito kortelės nereikia.", "unorouter": "Sukurkite API raktą adresu https://unorouter.ai, tada įklijuokite jį čia kaip Bearer prieigos raktą.", "agnes": "Gaukite API raktą adresu agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Nemokamas planas sustabdytas (2026 m.) — AI/ML API dabar apmokestinama tik pagal naudojimą (mažiausias papildymas – $20); periodinių nemokamų kreditų nėra.", "ai21": "Užsiregistravus suteikiama $10 bandomųjų kreditų (galioja 3 mėnesius), kredito kortelės nereikia", "alibaba": "Prijunkite Alibaba naudodami API raktą.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP auditas (dienomis)", "retentionA2aEvents": "A2A įvykiai (dienomis)", "retentionCallLogs": "Skambučių žurnalai (dienomis)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Naudojimo istorija (dienomis)", "retentionMemoryEntries": "Atminties įrašai (dienomis)", "retentionXpAuditLog": "XP audito žurnalas (dienomis)", diff --git a/src/i18n/messages/lv.json b/src/i18n/messages/lv.json index 5818469dde..eff46dd996 100644 --- a/src/i18n/messages/lv.json +++ b/src/i18n/messages/lv.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Starpniekserveru pūli un opencode konta līmeņa rotācija uz katru procesu uz noteiktu laiku pārtrauc atkārtoti izmantot starpniekserveri, kas tikko cieta kļūmi (noraidīta TCP pārbaude vai caur to saņemts 429); šis laiks pēc katras atkārtotas kļūmes dubultojas līdz noteiktajam maksimumam. Starpniekservera statuss netiek ierakstīts; ja visi kandidāti ir atlikti malā, izvēle paliek nemainīga. Pēc noklusējuma izslēgts: atlases secība precīzi atbilst parastajai rotācijai.", "featureFlagProxyPoolEgressObservationDescription": "Informācijas panelī zem starpniekserveru pūla rādīt, cik novēroto izejošo IP adrešu pēdējo 24 h laikā apkalpoja tā dalībniekus, cik savienojumu tās izmantoja un lielāko aiz vienas IP adreses novēroto skaitu. Tikai lasāms, aprēķināts no starpniekserveru žurnāla un nekad netiek izmantots maršrutēšanai. Pēc noklusējuma izslēgts: pūla redaktors paliek nemainīgs, un novērojumu maršruts atgriež null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Starpniekserveru darbspējas pārbaudē ļaut mērķa noraidītai pārbaudei (401/403/429: starpniekserveris pārsūtīja pieprasījumu, bet galamērķis noraidīja šo izejošo IP adresi) atiestatīt starpniekservera secīgo kļūmju sēriju tāpat kā apkalpotai pārbaudei. Pēc noklusējuma izslēgts: noraidījums paliek neitrāls un saglabā kļūmju sēriju. 5xx jebkurā gadījumā paliek nepārliecinošs, un noraidījums nekad neizraisa starpniekservera noņemšanu, atspējošanu vai atkārtotu aktivizēšanu.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Sākums", "dashboard": "Panelis", @@ -6125,6 +6126,7 @@ "agentrouter": "Iegūstiet 200 USD bezmaksas kredītus https://agentrouter.org/register — kredītkarte nav nepieciešama.", "unorouter": "Izveidojiet API atslēgu https://unorouter.ai un pēc tam ielīmējiet to šeit kā Bearer tokenu.", "agnes": "Iegūstiet API atslēgu agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Bezmaksas plāns pauzēts (2026) — AI/ML API tagad ir tikai maksas pēc lietojuma (min. iemaksa 20 USD); atkārtoti bezmaksas kredīti nav pieejami.", "ai21": "10 USD izmēģinājuma kredīti pēc reģistrācijas (derīgi 3 mēnešus), kredītkarte nav nepieciešama", "alibaba": "Savienojiet Alibaba ar API atslēgu.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP audita žurnāls (dienas)", "retentionA2aEvents": "A2A notikumi (dienas)", "retentionCallLogs": "Zvanu žurnāli (dienas)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Lietojuma vēsture (dienas)", "retentionMemoryEntries": "Atmiņas ieraksti (dienas)", "retentionXpAuditLog": "XP audita žurnāls (dienas)", diff --git a/src/i18n/messages/ml.json b/src/i18n/messages/ml.json index 34d872041e..9ca6e3b7cd 100644 --- a/src/i18n/messages/ml.json +++ b/src/i18n/messages/ml.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proxy പൂളുകളും opencode-ന്റെ ഓരോ അക്കൗണ്ടിലുമുള്ള റൊട്ടേഷനും, ഇപ്പോൾ പരാജയപ്പെട്ട ഒരു Proxy-യെ (നിരസിക്കപ്പെട്ട TCP പ്രോബ്, അല്ലെങ്കിൽ അതിലൂടെ ലഭിച്ച 429) ഓരോ ആവർത്തനത്തിലും ഇരട്ടിയാകുന്ന, ഒരു പരമാവധി പരിധിവരെയുള്ള ഓരോ പ്രോസസിനുമുള്ള കാലയളവിൽ വീണ്ടും ഉപയോഗിക്കുന്നത് നിർത്തുന്നു. Proxy നിലയൊന്നും രേഖപ്പെടുത്തില്ല; എല്ലാ സ്ഥാനാർഥികളെയും മാറ്റിവെച്ചാൽ തിരഞ്ഞെടുപ്പ് മാറ്റമില്ലാതെ തുടരും. ഡിഫോൾട്ടായി ഓഫാണ്: തിരഞ്ഞെടുപ്പ് ക്രമം സാധാരണ റൊട്ടേഷൻ തന്നെയാണ്.", "featureFlagProxyPoolEgressObservationDescription": "ഡാഷ്ബോർഡിലെ ഒരു Proxy പൂളിന് കീഴിൽ, കഴിഞ്ഞ 24 മണിക്കൂറിനിടെ അതിലെ അംഗങ്ങൾക്ക് സേവനം നൽകിയതായി നിരീക്ഷിച്ച എത്ര എഗ്രസ് IP-കൾ ഉണ്ടായിരുന്നു, എത്ര കണക്ഷനുകൾ അവ ഉപയോഗിച്ചു, ഒരു IP-ക്ക് പിന്നിൽ കണ്ട ഏറ്റവും ഉയർന്ന എണ്ണം എത്ര എന്നിവ കാണിക്കുക. വായനയ്ക്ക് മാത്രം; Proxy ലോഗിൽനിന്ന് കണക്കാക്കുന്നതും റൂട്ടിംഗിനായി ഒരിക്കലും ഉപയോഗിക്കാത്തതുമാണ്. ഡിഫോൾട്ടായി ഓഫാണ്: പൂൾ എഡിറ്ററിൽ മാറ്റമില്ല, നിരീക്ഷണ റൂട്ട് null എന്ന് മറുപടി നൽകും.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Proxy ആരോഗ്യ പരിശോധനയിൽ, ലക്ഷ്യം നിരസിച്ച ഒരു പ്രോബ് (401/403/429: Proxy റിലേ ചെയ്തു, ലക്ഷ്യസ്ഥാനം ഈ എഗ്രസ് IP നിരസിച്ചു) സേവനം നൽകിയ പ്രോബിനെപ്പോലെ Proxy-യുടെ തുടർച്ചയായ പരാജയങ്ങളുടെ എണ്ണം പുനഃസജ്ജമാക്കാൻ അനുവദിക്കുക. ഡിഫോൾട്ടായി ഓഫാണ്: ഒരു നിരസിക്കൽ നിഷ്പക്ഷമായി തുടരുകയും പരാജയങ്ങളുടെ എണ്ണം നിലനിർത്തുകയും ചെയ്യും. ഏതുവിധമായാലും 5xx അനിശ്ചിതമായി തുടരും; ഒരു നിരസിക്കൽ ഒരിക്കലും Proxy-യെ നീക്കം ചെയ്യുകയോ പ്രവർത്തനരഹിതമാക്കുകയോ വീണ്ടും സജീവമാക്കുകയോ ചെയ്യില്ല.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "ഹോം", "dashboard": "ഡാഷ്ബോർഡ്", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register-ൽ $200 സൗജന്യ ക്രെഡിറ്റുകൾ നേടുക — ക്രെഡിറ്റ് കാർഡ് ആവശ്യമില്ല.", "unorouter": "https://unorouter.ai-ൽ ഒരു API കീ സൃഷ്ടിച്ച്, അത് Bearer ടോക്കണായി ഇവിടെ ഒട്ടിക്കുക.", "agnes": "agnes-ai.com-ൽ API കീ നേടുക", + "agnes-cn": "__MISSING__:Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "സൗജന്യ ടയർ താൽക്കാലികമായി നിർത്തി (2026) — AI/ML API ഇപ്പോൾ ഉപയോഗത്തിനനുസരിച്ച് പണം നൽകുന്ന രീതിയിൽ മാത്രം ലഭ്യമാണ് (കുറഞ്ഞത് $20 ടോപ്പ്-അപ്പ്); ആവർത്തിച്ചുള്ള സൗജന്യ ക്രെഡിറ്റുകൾ ഇല്ല.", "ai21": "സൈൻ അപ്പ് ചെയ്യുമ്പോൾ $10 ട്രയൽ ക്രെഡിറ്റുകൾ (3 മാസം സാധുത), ക്രെഡിറ്റ് കാർഡ് ആവശ്യമില്ല", "alibaba": "API കീ ഉപയോഗിച്ച് Alibaba കണക്റ്റ് ചെയ്യുക.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP ഓഡിറ്റ് (ദിവസങ്ങൾ)", "retentionA2aEvents": "A2A ഇവന്റുകൾ (ദിവസങ്ങൾ)", "retentionCallLogs": "കോൾ ലോഗുകൾ (ദിവസങ്ങൾ)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "ഉപയോഗ ചരിത്രം (ദിവസങ്ങൾ)", "retentionMemoryEntries": "മെമ്മറി എൻട്രികൾ (ദിവസങ്ങൾ)", "retentionXpAuditLog": "XP ഓഡിറ്റ് ലോഗ് (ദിവസങ്ങൾ)", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index 036a6215c6..59112fc708 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "प्रॉक्सी पूल आणि opencode चे प्रति-खाते रोटेशन नुकताच अयशस्वी झालेला प्रॉक्सी (TCP प्रोब नाकारला गेला किंवा त्याद्वारे 429 प्राप्त झाला) प्रत्येक प्रक्रियेसाठी ठरावीक कालावधीपर्यंत पुन्हा वापरत नाहीत; प्रत्येक पुनरावृत्तीनंतर हा कालावधी दुप्पट होतो, कमाल मर्यादेपर्यंत. प्रॉक्सीची कोणतीही स्थिती लिहिली जात नाही; प्रत्येक उमेदवार बाजूला ठेवल्यास निवड अपरिवर्तित राहते. डीफॉल्टनुसार बंद: निवडीचा क्रम अगदी साध्या रोटेशनप्रमाणेच असतो.", "featureFlagProxyPoolEgressObservationDescription": "डॅशबोर्डमध्ये प्रॉक्सी पूलखाली, मागील 24 तासांत त्याच्या सदस्यांना सेवा देणारे किती निरीक्षित इग्रेस IP होते, किती कनेक्शननी त्यांचा वापर केला आणि एका IP मागे दिसलेली सर्वाधिक संख्या किती होती हे दाखवा. हे केवळ-वाचनीय असून प्रॉक्सी लॉगवरून मोजले जाते आणि रूटिंगसाठी कधीही वापरले जात नाही. डीफॉल्टनुसार बंद: पूल एडिटर अपरिवर्तित राहतो आणि निरीक्षण रूट null असे उत्तर देतो.", "featureFlagProxyHealthBlockedResetsStreakDescription": "प्रॉक्सी आरोग्य तपासणीमध्ये, लक्ष्याने नाकारलेल्या प्रोबला (401/403/429: प्रॉक्सीने पुढे पाठवले, पण गंतव्याने हा इग्रेस IP नाकारला) सेवा दिलेल्या प्रोबप्रमाणे प्रॉक्सीच्या सलग अपयशांची मालिका रीसेट करू द्या. डीफॉल्टनुसार बंद: नकार तटस्थ राहतो आणि मालिका कायम ठेवतो. 5xx दोन्ही बाबतींत अनिर्णीत राहतो आणि नकारामुळे प्रॉक्सी कधीही काढला, अक्षम किंवा पुन्हा सक्रिय केला जात नाही.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "मुख्यपृष्ठ", "dashboard": "डॅशबोर्ड", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register वर $200 चे मोफत क्रेडिट्स मिळवा — क्रेडिट कार्डची आवश्यकता नाही.", "unorouter": "https://unorouter.ai येथे API की तयार करा, नंतर ते येथे Bearer टोकन म्हणून पेस्ट करा.", "agnes": "agnes-ai.com वर API की मिळवा", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "मोफत टियर थांबवले आहे (2026) — AI/ML API आता फक्त पे-अॅज-यू-गो (किमान $20 टॉप-अप) आहे; कोणतेही आवर्ती मोफत क्रेडिट्स नाहीत.", "ai21": "साइनअपवर $10 चे ट्रायल क्रेडिट्स (3 महिने वैध), क्रेडिट कार्डची आवश्यकता नाही", "alibaba": "API की वापरून Alibaba कनेक्ट करा.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP ऑडिट (दिवस)", "retentionA2aEvents": "A2A कार्यक्रम (दिवस)", "retentionCallLogs": "कॉल लॉग (दिवस)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "वापर इतिहास (दिवस)", "retentionMemoryEntries": "मेमरी एंट्री (दिवस)", "retentionXpAuditLog": "XP ऑडिट लॉग (दिवस)", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index 82d40caada..19f45b18a3 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Kumpulan proksi dan penggiliran opencode bagi setiap akaun akan berhenti menyediakan semula proksi yang baru sahaja gagal (menolak probe TCP atau menerima 429 melaluinya) untuk tempoh setiap proses yang berganda pada setiap kegagalan berulang, sehingga had maksimum. Tiada status proksi direkodkan; apabila setiap calon diketepikan, pilihan tidak berubah. Dimatikan secara lalai: susunan pemilihan adalah sama seperti penggiliran biasa.", "featureFlagProxyPoolEgressObservationDescription": "Tunjukkan, di bawah kumpulan proksi dalam papan pemuka, bilangan IP keluar yang diperhatikan telah digunakan oleh ahlinya sepanjang 24 jam yang lalu, bilangan sambungan yang menggunakannya dan bilangan terbanyak yang dilihat di sebalik satu IP. Baca sahaja, dikira daripada log proksi dan tidak pernah digunakan untuk penghalaan. Dimatikan secara lalai: editor kumpulan tidak berubah dan laluan pemerhatian memberikan null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Dalam imbasan kesihatan proksi, benarkan probe yang ditolak oleh sasaran (401/403/429: proksi telah menyampaikan permintaan, tetapi destinasi menolak IP keluar ini) menetapkan semula rentetan kegagalan berturut-turut proksi, seperti probe yang berjaya disediakan. Dimatikan secara lalai: penolakan kekal neutral dan mengekalkan rentetan tersebut. 5xx kekal tidak muktamad dalam kedua-dua keadaan dan penolakan tidak akan mengalih keluar, menyahdayakan atau mengaktifkan semula proksi.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Rumah", "dashboard": "Papan pemuka", @@ -6125,6 +6126,7 @@ "agentrouter": "Dapatkan kredit percuma $200 di https://agentrouter.org/register — tiada kad kredit diperlukan.", "unorouter": "Buat kunci API di https://unorouter.ai, kemudian tampal di sini sebagai token Bearer.", "agnes": "Dapatkan kunci API di agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Peringkat percuma dijeda (2026) — AI/ML API kini bayar semasa guna sahaja (tambah nilai minimum $20); tiada kredit percuma berulang.", "ai21": "Kredit percubaan $10 semasa pendaftaran (sah 3 bulan), tiada kad kredit diperlukan", "alibaba": "Sambungkan Alibaba dengan kunci API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Audit MCP (hari)", "retentionA2aEvents": "Acara A2A (hari)", "retentionCallLogs": "Log Panggilan (hari)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Sejarah Penggunaan (hari)", "retentionMemoryEntries": "Entri Memori (hari)", "retentionXpAuditLog": "Log Audit XP (hari)", diff --git a/src/i18n/messages/mt.json b/src/i18n/messages/mt.json index e8a0e46309..03cc295aba 100644 --- a/src/i18n/messages/mt.json +++ b/src/i18n/messages/mt.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Il-pools tal-proxy u r-rotazzjoni għal kull kont ta’ opencode jieqfu jerġgħu jservu proxy li jkun għadu kemm falla (sonda TCP miċħuda, jew 429 riċevut permezz tiegħu) għal perjodu għal kull proċess li jirdoppja ma’ kull ripetizzjoni, sa limitu massimu. Ma jinkiteb ebda status tal-proxy; meta kull kandidat jitwarrab, l-għażla tibqa’ l-istess. Mitfi b’mod awtomatiku: l-ordni tal-għażla hija eżattament ir-rotazzjoni sempliċi.", "featureFlagProxyPoolEgressObservationDescription": "Uri, taħt pool tal-proxy fid-dashboard, kemm-il IP ta’ ħruġ osservat serva lill-membri tiegħu matul l-aħħar 24 siegħa, kemm-il konnessjoni użathom u l-ogħla għadd osservat wara IP wieħed. Għall-qari biss, ikkalkulat mil-log tal-proxy, u qatt ma jintuża għar-routing. Mitfi b’mod awtomatiku: l-editur tal-pool ma jinbidilx u r-rotta tal-osservazzjoni twieġeb null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Fl-iskennjar tas-saħħa tal-proxy, ħalli sonda li l-mira tkun irrifjutat (401/403/429: il-proxy għadda t-talba, iżda d-destinazzjoni rrifjutat dan l-IP ta’ ħruġ) tirrisettja s-sensiela ta’ fallimenti konsekuttivi tal-proxy, bħal sonda moqdija. Mitfi b’mod awtomatiku: rifjut jibqa’ newtrali u jżomm is-sensiela. 5xx jibqa’ inkonklussiv fiż-żewġ każijiet, u rifjut qatt ma jneħħi, jiddiżattiva jew jerġa’ jattiva proxy.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Paġna Ewlenija", "dashboard": "Pannell tal-kontroll", @@ -6125,6 +6126,7 @@ "agentrouter": "Ikseb $200 fi krediti b’xejn minn https://agentrouter.org/register — ebda karta ta’ kreditu mhi meħtieġa.", "unorouter": "Oħloq ċavetta API fuq https://unorouter.ai, imbagħad waħħalha hawn bħala token Bearer.", "agnes": "Ikseb ċavetta API minn agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Il-pjan bla ħlas twaqqaf temporanjament (2026) — l-API AI/ML issa taħdem biss fuq bażi ta’ ħlas skont l-użu (rikarika minima ta’ $20); m’hemmx krediti bla ħlas rikorrenti.", "ai21": "$10 fi krediti ta’ prova mar-reġistrazzjoni (validi għal 3 xhur), ebda karta ta’ kreditu mhi meħtieġa", "alibaba": "Qabbad Alibaba b’ċavetta API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Awditjar tal-MCP (jiem)", "retentionA2aEvents": "Avvenimenti A2A (jiem)", "retentionCallLogs": "Reġistri tas-Sejħiet (jiem)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Storja tal-Użu (jiem)", "retentionMemoryEntries": "Entrati tal-Memorja (jiem)", "retentionXpAuditLog": "Reġistru tal-Awditjar tal-XP (jiem)", diff --git a/src/i18n/messages/my.json b/src/i18n/messages/my.json index ece36889ab..c76cf24903 100644 --- a/src/i18n/messages/my.json +++ b/src/i18n/messages/my.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proxy pool များနှင့် opencode ၏ အကောင့်တစ်ခုချင်းစီအလိုက် အလှည့်ကျရွေးချယ်မှုသည် ပျက်ကွက်ခဲ့သည့် proxy (TCP probe ကို ငြင်းပယ်ခဲ့ခြင်း သို့မဟုတ် ၎င်းမှတစ်ဆင့် 429 တုံ့ပြန်မှုရရှိခြင်း) ကို process တစ်ခုချင်းစီအလိုက် သတ်မှတ်ကာလအတွင်း ပြန်လည်အသုံးမပြုတော့ပါ။ ထပ်မံဖြစ်ပွားတိုင်း ထိုကာလသည် နှစ်ဆတိုးလာပြီး သတ်မှတ်အများဆုံးပမာဏအထိသာ တိုးပါမည်။ Proxy အခြေအနေကို မှတ်တမ်းတင်မည်မဟုတ်ပါ။ Candidate အားလုံးကို ဘေးဖယ်ထားရသည့်အခါ ရွေးချယ်မှုသည် မပြောင်းလဲပါ။ မူလအားဖြင့် ပိတ်ထားသည်။ ရွေးချယ်မှုအစီအစဉ်သည် ရိုးရိုးအလှည့်ကျအစီအစဉ်အတိုင်း အတိအကျဖြစ်သည်။", "featureFlagProxyPoolEgressObservationDescription": "Dashboard ရှိ proxy pool တစ်ခု၏အောက်တွင် လွန်ခဲ့သော 24 နာရီအတွင်း ၎င်း၏ member များအတွက် အသုံးပြုခဲ့သည့် စောင့်ကြည့်တွေ့ရှိထားသော egress IP အရေအတွက်၊ ထို IP များကို အသုံးပြုခဲ့သည့် connection အရေအတွက်နှင့် IP တစ်ခုတည်း၏နောက်ကွယ်တွင် အများဆုံးတွေ့ရှိရသည့် အရေအတွက်တို့ကို ပြသပါ။ ၎င်းသည် ဖတ်ရှုရန်သာဖြစ်ပြီး proxy log မှ တွက်ချက်ထားကာ routing အတွက် မည်သည့်အခါမျှ အသုံးမပြုပါ။ မူလအားဖြင့် ပိတ်ထားသည်။ Pool editor သည် မပြောင်းလဲဘဲ observation route က null ကို ပြန်ပေးသည်။", "featureFlagProxyHealthBlockedResetsStreakDescription": "Proxy health sweep တွင် target က ငြင်းပယ်ခဲ့သော probe (401/403/429—proxy က လက်ဆင့်ကမ်းပေးခဲ့သော်လည်း destination က ဤ egress IP ကို ငြင်းပယ်ခြင်း) ကို အောင်မြင်စွာ ဆောင်ရွက်ခဲ့သည့် probe ကဲ့သို့ proxy ၏ ဆက်တိုက်ပျက်ကွက်မှုအရေအတွက်ကို ပြန်လည်သတ်မှတ်ခွင့်ပြုပါ။ မူလအားဖြင့် ပိတ်ထားသည်။ ငြင်းပယ်မှုသည် သက်ရောက်မှုမရှိဘဲ streak ကို ဆက်လက်ထိန်းထားသည်။ မည်သည့်အခြေအနေတွင်မဆို 5xx သည် အတည်မပြုနိုင်သည့်အခြေအနေအဖြစ် ဆက်ရှိနေပြီး ငြင်းပယ်မှုတစ်ခုကြောင့် proxy ကို ဖယ်ရှားခြင်း၊ ပိတ်ခြင်း သို့မဟုတ် ပြန်လည်အသက်သွင်းခြင်း မည်သည့်အခါမျှ မပြုလုပ်ပါ။", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "ပင်မစာမျက်နှာ", "dashboard": "ဒက်ရှ်ဘုတ်", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register တွင် အခမဲ့ခရက်ဒစ် $200 ရယူပါ — ခရက်ဒစ်ကတ် မလိုအပ်ပါ။", "unorouter": "https://unorouter.ai တွင် API key တစ်ခု ဖန်တီးပြီးနောက် Bearer token အဖြစ် ဤနေရာတွင် ကူးထည့်ပါ။", "agnes": "agnes-ai.com တွင် API key ရယူရန်", + "agnes-cn": "__MISSING__:Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "အခမဲ့အဆင့်ကို ယာယီရပ်နားထားသည် (2026) — AI/ML API သည် ယခု အသုံးပြုသလောက် ပေးချေရသည့်စနစ်သာ ရရှိနိုင်သည် (အနည်းဆုံး $20 ဖြည့်သွင်းရမည်)၊ ပုံမှန်ထပ်မံရရှိသည့် အခမဲ့ခရက်ဒစ်များ မရှိပါ။", "ai21": "စာရင်းသွင်းချိန်တွင် စမ်းသပ်သုံးခရက်ဒစ် $10 ရရှိမည် (၃ လ သက်တမ်းရှိသည်)၊ ခရက်ဒစ်ကတ် မလိုအပ်ပါ", "alibaba": "Alibaba ကို API key ဖြင့် ချိတ်ဆက်ပါ။", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP စစ်ဆေးမှတ်တမ်း (ရက်)", "retentionA2aEvents": "A2A ဖြစ်ရပ်များ (ရက်)", "retentionCallLogs": "ခေါ်ဆိုမှုမှတ်တမ်းများ (ရက်)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "အသုံးပြုမှုမှတ်တမ်း (ရက်)", "retentionMemoryEntries": "မှတ်ဉာဏ်ထည့်သွင်းချက်များ (ရက်)", "retentionXpAuditLog": "XP စစ်ဆေးမှတ်တမ်း (ရက်)", diff --git a/src/i18n/messages/ne.json b/src/i18n/messages/ne.json index 3f5cf4fab2..b0b2b812e8 100644 --- a/src/i18n/messages/ne.json +++ b/src/i18n/messages/ne.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proxy pools र opencode को प्रति-खाता रोटेसनले भर्खरै असफल भएको प्रोक्सी (TCP प्रोब अस्वीकार भएको वा त्यसमार्फत 429 प्राप्त भएको) लाई प्रत्येक पटक दोब्बर हुँदै अधिकतम सीमासम्म पुग्ने प्रति-प्रक्रिया अवधिका लागि पुनः प्रयोग गर्न रोक्छ। कुनै प्रोक्सी स्थिति लेखिँदैन; प्रत्येक उम्मेदवारलाई अलग राख्दा छनोट अपरिवर्तित रहन्छ। पूर्वनिर्धारित रूपमा बन्द: छनोट क्रम ठ्याक्कै साधारण रोटेसन नै हुन्छ।", "featureFlagProxyPoolEgressObservationDescription": "ड्यासबोर्डमा प्रोक्सी पूलअन्तर्गत, पछिल्लो २४ घण्टामा त्यसका सदस्यहरूलाई सेवा दिने कति वटा अवलोकित इग्रेस IP थिए, कति जडानले तिनलाई प्रयोग गरे र एउटै IP पछाडि देखिएको अधिकतम संख्या कति थियो भन्ने देखाउनुहोस्। पढ्नका लागि मात्र, प्रोक्सी लगबाट गणना गरिएको र राउटिङका लागि कहिल्यै प्रयोग नगरिने। पूर्वनिर्धारित रूपमा बन्द: पूल सम्पादक अपरिवर्तित रहन्छ र अवलोकन रुटले null फर्काउँछ।", "featureFlagProxyHealthBlockedResetsStreakDescription": "प्रोक्सी स्वास्थ्य स्वीपमा, लक्ष्यले अस्वीकार गरेको प्रोब (401/403/429: प्रोक्सीले रिले गर्यो, गन्तव्यले यो इग्रेस IP अस्वीकार गर्यो) लाई सेवा दिइएको प्रोबजस्तै प्रोक्सीको लगातार-असफलता क्रम रिसेट गर्न दिनुहोस्। पूर्वनिर्धारित रूपमा बन्द: अस्वीकृति तटस्थ रहन्छ र क्रम कायम राख्छ। दुवै अवस्थामा 5xx अनिर्णायक नै रहन्छ, र अस्वीकृतिले प्रोक्सीलाई कहिल्यै हटाउँदैन, निष्क्रिय पार्दैन वा पुनः सक्रिय गर्दैन।", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "गृहपृष्ठ", "dashboard": "ड्यासबोर्ड", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register मा $200 को निःशुल्क क्रेडिट प्राप्त गर्नुहोस् — क्रेडिट कार्ड आवश्यक छैन।", "unorouter": "https://unorouter.ai मा API key सिर्जना गर्नुहोस्, त्यसपछि यसलाई Bearer token का रूपमा यहाँ टाँस्नुहोस्।", "agnes": "agnes-ai.com मा API key प्राप्त गर्नुहोस्", + "agnes-cn": "__MISSING__:Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "निःशुल्क टियर स्थगित (2026) — AI/ML API अब प्रयोगअनुसार भुक्तानीमा मात्र उपलब्ध छ (न्यूनतम $20 टप-अप); नियमित निःशुल्क क्रेडिट छैन।", "ai21": "साइनअप गर्दा $10 को परीक्षण क्रेडिट (३ महिनासम्म मान्य), क्रेडिट कार्ड आवश्यक छैन", "alibaba": "Alibaba लाई API key मार्फत जडान गर्नुहोस्।", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP अडिट (दिन)", "retentionA2aEvents": "A2A घटनाहरू (दिन)", "retentionCallLogs": "कल लगहरू (दिन)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "प्रयोग इतिहास (दिन)", "retentionMemoryEntries": "मेमोरी प्रविष्टिहरू (दिन)", "retentionXpAuditLog": "XP अडिट लग (दिन)", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 95eb77fb44..b661305875 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proxypools en de rotatie per account van opencode zorgen ervoor dat een proxy die zojuist is mislukt (geweigerde TCP-probe of een 429 die via de proxy is ontvangen) gedurende een periode per proces niet opnieuw wordt aangeboden. Deze periode verdubbelt bij elke herhaling, tot een maximum. Er wordt geen proxystatus opgeslagen; wanneer elke kandidaat terzijde is geschoven, blijft de keuze ongewijzigd. Standaard uitgeschakeld: de selectievolgorde is exact gelijk aan de gewone rotatie.", "featureFlagProxyPoolEgressObservationDescription": "Toon onder een proxypool in het dashboard hoeveel waargenomen uitgaande IP-adressen de leden ervan in de afgelopen 24 uur hebben bediend, hoeveel verbindingen deze hebben gebruikt en hoeveel verbindingen maximaal achter één IP zijn waargenomen. Alleen-lezen, berekend op basis van het proxylogboek en nooit gebruikt voor routering. Standaard uitgeschakeld: de pooleditor blijft ongewijzigd en de observatieroute antwoordt met null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Laat bij de proxygezondheidscontrole een probe die door het doel is geweigerd (401/403/429: de proxy heeft doorgestuurd, maar de bestemming heeft dit uitgaande IP-adres geweigerd) de reeks opeenvolgende fouten van de proxy resetten, net als een geslaagde probe. Standaard uitgeschakeld: een weigering blijft neutraal en behoudt de reeks. Een 5xx blijft in beide gevallen onbeslist, en een weigering verwijdert, deactiveert of heractiveert een proxy nooit.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Thuis", "dashboard": "Dashboard", @@ -6125,6 +6126,7 @@ "agentrouter": "Ontvang $200 gratis tegoed op https://agentrouter.org/register — geen creditcard vereist.", "unorouter": "Maak een API-sleutel aan op https://unorouter.ai en plak deze hier als een Bearer-token.", "agnes": "Verkrijg API-sleutel op agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Gratis abonnement gepauzeerd (2026) — AI/ML API is nu alleen pay-as-you-go (min. $20 opwaarderen); geen terugkerend gratis tegoed.", "ai21": "$10 proeftegoed bij registratie (3 maanden geldig), geen creditcard vereist", "alibaba": "Verbind Alibaba met een API-sleutel.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP-audit (dagen)", "retentionA2aEvents": "A2A-evenementen (dagen)", "retentionCallLogs": "Oproeplogboeken (dagen)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Gebruiksgeschiedenis (dagen)", "retentionMemoryEntries": "Geheugeninvoer (dagen)", "retentionXpAuditLog": "XP-auditlog (dagen)", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index 1b561286bf..6f9facb250 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proxygrupper og opencodes rotasjon per konto slutter midlertidig å bruke en proxy som nettopp sviktet (avvist TCP-sonde eller en 429 mottatt gjennom den), i en prosesspesifikk periode som dobles for hver gjentakelse, opptil en øvre grense. Ingen proxystatus lagres. Hvis alle kandidater settes til side, forblir valget uendret. Av som standard: Valgrekkefølgen er nøyaktig den vanlige rotasjonen.", "featureFlagProxyPoolEgressObservationDescription": "Vis under en proxygruppe i kontrollpanelet hvor mange observerte utgående IP-adresser som betjente medlemmene i løpet av de siste 24 timene, hvor mange tilkoblinger som brukte dem, og det høyeste antallet som ble observert bak én IP-adresse. Skrivebeskyttet, beregnet fra proxyloggen og aldri brukt til ruting. Av som standard: Redigeringen av proxygruppen er uendret, og observasjonsruten returnerer null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "La en sonde som målet avviste under helsesjekken av proxyer (401/403/429: proxyen videresendte forespørselen, men destinasjonen avviste denne utgående IP-adressen), nullstille proxyens rekke av påfølgende feil, på samme måte som en sonde som ble betjent. Av som standard: En avvisning forblir nøytral og beholder rekken. En 5xx forblir ikke-konkluderende i begge tilfeller, og en avvisning fjerner, deaktiverer eller reaktiverer aldri en proxy.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Hjem", "dashboard": "Dashbord", @@ -6125,6 +6126,7 @@ "agentrouter": "Få $200 i gratis kreditter på https://agentrouter.org/register — ingen kredittkort kreves.", "unorouter": "Opprett en API-nøkkel på https://unorouter.ai, og lim den deretter inn her som en Bearer-token.", "agnes": "Få API-nøkkel på agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Gratisnivå satt på pause (2026) — AI/ML API er nå kun forbruksbasert (min. $20 påfylling); ingen gjentakende gratis kreditter.", "ai21": "$10 i prøvekreditter ved registrering (gyldig i 3 måneder), ingen kredittkort kreves", "alibaba": "Koble til Alibaba med en API-nøkkel.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP-revisjon (dager)", "retentionA2aEvents": "A2A-hendelser (dager)", "retentionCallLogs": "Samtalelogger (dager)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Brukshistorikk (dager)", "retentionMemoryEntries": "Minneoppføringer (dager)", "retentionXpAuditLog": "XP-revisjonslogg (dager)", diff --git a/src/i18n/messages/or.json b/src/i18n/messages/or.json index 3cd5d0a788..4df655240a 100644 --- a/src/i18n/messages/or.json +++ b/src/i18n/messages/or.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "ପ୍ରକ୍ସି ପୁଲ୍ ଏବଂ opencodeର ପ୍ରତି-ଆକାଉଣ୍ଟ ରୋଟେସନ୍, ସଦ୍ୟ ବିଫଳ ହୋଇଥିବା ପ୍ରକ୍ସିକୁ (ପ୍ରତ୍ୟାଖ୍ୟାନ ହୋଇଥିବା TCP ପ୍ରୋବ୍ କିମ୍ବା ତାହା ମାଧ୍ୟମରେ ପ୍ରାପ୍ତ 429) ପ୍ରତ୍ୟେକ ପୁନରାବୃତ୍ତିରେ ଦ୍ୱିଗୁଣିତ ହେଉଥିବା, ଏକ ସର୍ବାଧିକ ସୀମା ପର୍ଯ୍ୟନ୍ତ ପ୍ରତି-ପ୍ରକ୍ରିୟା ଅବଧି ପାଇଁ ପୁଣି ସେବାରେ ବ୍ୟବହାର କରିବା ବନ୍ଦ କରେ। କୌଣସି ପ୍ରକ୍ସି ସ୍ଥିତି ଲେଖାଯାଏ ନାହିଁ; ପ୍ରତ୍ୟେକ ପ୍ରାର୍ଥୀକୁ ପାଖକୁ ରଖାଗଲେ ଚୟନ ଅପରିବର୍ତ୍ତିତ ରହେ। ଡିଫଲ୍ଟ ଭାବେ ବନ୍ଦ: ଚୟନ କ୍ରମ ଠିକ୍ ସାଧାରଣ ରୋଟେସନ୍ ପରି।", "featureFlagProxyPoolEgressObservationDescription": "ଡ୍ୟାସବୋର୍ଡରେ ଗୋଟିଏ ପ୍ରକ୍ସି ପୁଲ୍ ଅଧୀନରେ, ଗତ 24 ଘଣ୍ଟାରେ କେତୋଟି ପର୍ଯ୍ୟବେକ୍ଷିତ ଏଗ୍ରେସ୍ IP ଏହାର ସଦସ୍ୟମାନଙ୍କୁ ସେବା ଦେଇଥିଲା, କେତୋଟି ସଂଯୋଗ ସେଗୁଡ଼ିକୁ ବ୍ୟବହାର କରିଥିଲା ଏବଂ ଗୋଟିଏ IP ପଛରେ ସର୍ବାଧିକ କେତୋଟି ଦେଖାଯାଇଥିଲା, ତାହା ଦେଖାନ୍ତୁ। କେବଳ ପଠନୀୟ, ପ୍ରକ୍ସି ଲଗ୍ରୁ ଗଣନା କରାଯାଏ, ରାଉଟିଂ ପାଇଁ କେବେ ବ୍ୟବହୃତ ହୁଏ ନାହିଁ। ଡିଫଲ୍ଟ ଭାବେ ବନ୍ଦ: ପୁଲ୍ ଏଡିଟର୍ ଅପରିବର୍ତ୍ତିତ ରହେ ଏବଂ ପର୍ଯ୍ୟବେକ୍ଷଣ ରୁଟ୍ null ଉତ୍ତର ଦିଏ।", "featureFlagProxyHealthBlockedResetsStreakDescription": "ପ୍ରକ୍ସି ସ୍ୱାସ୍ଥ୍ୟ ଯାଞ୍ଚରେ, ଲକ୍ଷ୍ୟ ଦ୍ୱାରା ପ୍ରତ୍ୟାଖ୍ୟାନ ହୋଇଥିବା ପ୍ରୋବ୍କୁ (401/403/429: ପ୍ରକ୍ସି ରିଲେ କରିଥିଲା, ଗନ୍ତବ୍ୟସ୍ଥଳ ଏହି ଏଗ୍ରେସ୍ IPକୁ ପ୍ରତ୍ୟାଖ୍ୟାନ କରିଥିଲା) ଏକ ସେବା ପ୍ରଦାନ କରାଯାଇଥିବା ପ୍ରୋବ୍ ପରି ପ୍ରକ୍ସିର କ୍ରମାଗତ ବିଫଳତା ଧାରାକୁ ରିସେଟ୍ କରିବାକୁ ଦିଅନ୍ତୁ। ଡିଫଲ୍ଟ ଭାବେ ବନ୍ଦ: ଏକ ପ୍ରତ୍ୟାଖ୍ୟାନ ନିରପେକ୍ଷ ରହେ ଏବଂ ଧାରାକୁ ଅପରିବର୍ତ୍ତିତ ରଖେ। ଉଭୟ କ୍ଷେତ୍ରରେ 5xx ଅନିର୍ଣ୍ଣାୟକ ରହେ, ଏବଂ ଏକ ପ୍ରତ୍ୟାଖ୍ୟାନ କେବେ ମଧ୍ୟ ପ୍ରକ୍ସିକୁ ହଟାଏ, ଅକ୍ଷମ କରେ କିମ୍ବା ପୁନଃସକ୍ରିୟ କରେ ନାହିଁ।", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "ମୂଳପୃଷ୍ଠା", "dashboard": "ଡ୍ୟାସ୍ବୋର୍ଡ", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register ରେ $200 ମାଗଣା କ୍ରେଡିଟ୍ ପାଆନ୍ତୁ — କୌଣସି କ୍ରେଡିଟ୍ କାର୍ଡ ଆବଶ୍ୟକ ନାହିଁ।", "unorouter": "https://unorouter.ai ରେ ଏକ API କୀ ସୃଷ୍ଟି କରନ୍ତୁ, ତା’ପରେ ଏଠାରେ ଏହାକୁ Bearer token ଭାବେ ପେଷ୍ଟ କରନ୍ତୁ।", "agnes": "agnes-ai.com ରୁ API କୀ ପାଆନ୍ତୁ", + "agnes-cn": "__MISSING__:Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "ମାଗଣା ସ୍ତର ସ୍ଥଗିତ (2026) — AI/ML API ବର୍ତ୍ତମାନ କେବଳ ବ୍ୟବହାର ଅନୁଯାୟୀ ଦେୟଯୁକ୍ତ (ସର୍ବନିମ୍ନ $20 ଟପ୍-ଅପ୍); କୌଣସି ପୁନରାବୃତ୍ତ ମାଗଣା କ୍ରେଡିଟ୍ ନାହିଁ।", "ai21": "ସାଇନ୍ ଅପ୍ କଲେ $10 ପରୀକ୍ଷାମୂଳକ କ୍ରେଡିଟ୍ (3 ମାସ ପାଇଁ ବୈଧ), କୌଣସି କ୍ରେଡିଟ୍ କାର୍ଡ ଆବଶ୍ୟକ ନାହିଁ", "alibaba": "ଏକ API କୀ ସହିତ Alibaba ସଂଯୋଗ କରନ୍ତୁ।", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP ଅଡିଟ୍ (ଦିନ)", "retentionA2aEvents": "A2A ଇଭେଣ୍ଟ୍ (ଦିନ)", "retentionCallLogs": "କଲ୍ ଲଗ୍ (ଦିନ)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "ବ୍ୟବହାର ଇତିହାସ (ଦିନ)", "retentionMemoryEntries": "ମେମୋରି ଏଣ୍ଟ୍ରି (ଦିନ)", "retentionXpAuditLog": "XP ଅଡିଟ୍ ଲଗ୍ (ଦିନ)", diff --git a/src/i18n/messages/pa.json b/src/i18n/messages/pa.json index 44798575c5..b7586ab6bc 100644 --- a/src/i18n/messages/pa.json +++ b/src/i18n/messages/pa.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proxy ਪੂਲ ਅਤੇ opencode ਦੀ ਪ੍ਰਤੀ-ਖਾਤਾ ਰੋਟੇਸ਼ਨ ਉਸ Proxy ਨੂੰ ਮੁੜ ਵਰਤਣਾ ਬੰਦ ਕਰ ਦਿੰਦੇ ਹਨ ਜੋ ਹੁਣੇ ਅਸਫਲ ਹੋਇਆ ਹੈ (TCP ਪ੍ਰੋਬ ਅਸਵੀਕਾਰ ਹੋਈ, ਜਾਂ ਇਸ ਰਾਹੀਂ 429 ਮਿਲਿਆ), ਇੱਕ ਪ੍ਰਤੀ-ਪ੍ਰਕਿਰਿਆ ਮਿਆਦ ਲਈ ਜੋ ਹਰ ਦੁਹਰਾਅ ਨਾਲ ਦੁੱਗਣੀ ਹੁੰਦੀ ਹੈ, ਇੱਕ ਅਧਿਕਤਮ ਹੱਦ ਤੱਕ। ਕੋਈ Proxy ਸਥਿਤੀ ਲਿਖੀ ਨਹੀਂ ਜਾਂਦੀ; ਹਰ ਉਮੀਦਵਾਰ ਨੂੰ ਪਾਸੇ ਰੱਖਣ ’ਤੇ ਚੋਣ ਬਦਲਦੀ ਨਹੀਂ ਹੈ। ਮੂਲ ਰੂਪ ਵਿੱਚ ਬੰਦ: ਚੋਣ ਕ੍ਰਮ ਬਿਲਕੁਲ ਸਧਾਰਨ ਰੋਟੇਸ਼ਨ ਵਰਗਾ ਹੈ।", "featureFlagProxyPoolEgressObservationDescription": "ਡੈਸ਼ਬੋਰਡ ਵਿੱਚ ਕਿਸੇ Proxy ਪੂਲ ਹੇਠ ਦਿਖਾਓ ਕਿ ਪਿਛਲੇ 24 ਘੰਟਿਆਂ ਦੌਰਾਨ ਕਿੰਨੇ ਦੇਖੇ ਗਏ egress IPs ਨੇ ਇਸ ਦੇ ਮੈਂਬਰਾਂ ਨੂੰ ਸੇਵਾ ਦਿੱਤੀ, ਕਿੰਨੇ ਕਨੈਕਸ਼ਨਾਂ ਨੇ ਉਨ੍ਹਾਂ ਨੂੰ ਵਰਤਿਆ ਅਤੇ ਇੱਕ IP ਦੇ ਪਿੱਛੇ ਸਭ ਤੋਂ ਵੱਧ ਕਿੰਨੇ ਦੇਖੇ ਗਏ। ਕੇਵਲ ਪੜ੍ਹਨਯੋਗ, Proxy ਲੌਗ ਤੋਂ ਗਣਨਾ ਕੀਤੀ ਗਈ, ਰੂਟਿੰਗ ਲਈ ਕਦੇ ਨਹੀਂ ਵਰਤੀ ਜਾਂਦੀ। ਮੂਲ ਰੂਪ ਵਿੱਚ ਬੰਦ: ਪੂਲ ਸੰਪਾਦਕ ਬਦਲਦਾ ਨਹੀਂ ਹੈ ਅਤੇ ਨਿਰੀਖਣ ਰੂਟ null ਜਵਾਬ ਦਿੰਦਾ ਹੈ।", "featureFlagProxyHealthBlockedResetsStreakDescription": "Proxy ਸਿਹਤ ਜਾਂਚ ਵਿੱਚ, ਟਾਰਗੇਟ ਵੱਲੋਂ ਅਸਵੀਕਾਰ ਕੀਤੀ ਪ੍ਰੋਬ (401/403/429: Proxy ਨੇ ਅੱਗੇ ਭੇਜਿਆ, ਮੰਜ਼ਿਲ ਨੇ ਇਸ egress IP ਨੂੰ ਅਸਵੀਕਾਰ ਕੀਤਾ) ਨੂੰ, ਸਫਲਤਾਪੂਰਵਕ ਸੇਵਾ ਕੀਤੀ ਪ੍ਰੋਬ ਵਾਂਗ, Proxy ਦੀ ਲਗਾਤਾਰ ਅਸਫਲਤਾ ਲੜੀ ਰੀਸੈੱਟ ਕਰਨ ਦਿਓ। ਮੂਲ ਰੂਪ ਵਿੱਚ ਬੰਦ: ਅਸਵੀਕਾਰਤਾ ਨਿਰਪੱਖ ਰਹਿੰਦੀ ਹੈ ਅਤੇ ਲੜੀ ਨੂੰ ਕਾਇਮ ਰੱਖਦੀ ਹੈ। 5xx ਦੋਵਾਂ ਹਾਲਤਾਂ ਵਿੱਚ ਅਨਿਰਣਾਇਕ ਰਹਿੰਦਾ ਹੈ, ਅਤੇ ਅਸਵੀਕਾਰਤਾ ਕਦੇ ਵੀ ਕਿਸੇ Proxy ਨੂੰ ਹਟਾਉਂਦੀ, ਅਯੋਗ ਜਾਂ ਮੁੜ ਸਰਗਰਮ ਨਹੀਂ ਕਰਦੀ।", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "ਮੁੱਖ ਪੰਨਾ", "dashboard": "ਡੈਸ਼ਬੋਰਡ", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register 'ਤੇ $200 ਦੇ ਮੁਫ਼ਤ ਕ੍ਰੈਡਿਟ ਪ੍ਰਾਪਤ ਕਰੋ — ਕ੍ਰੈਡਿਟ ਕਾਰਡ ਦੀ ਲੋੜ ਨਹੀਂ ਹੈ।", "unorouter": "https://unorouter.ai 'ਤੇ ਇੱਕ API key ਬਣਾਓ, ਫਿਰ ਇਸਨੂੰ ਇੱਥੇ Bearer token ਵਜੋਂ ਪੇਸਟ ਕਰੋ।", "agnes": "agnes-ai.com ਤੋਂ API key ਪ੍ਰਾਪਤ ਕਰੋ", + "agnes-cn": "__MISSING__:Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "ਮੁਫ਼ਤ ਟੀਅਰ ਰੋਕਿਆ ਗਿਆ ਹੈ (2026) — AI/ML API ਹੁਣ ਸਿਰਫ਼ ਵਰਤੋਂ ਅਨੁਸਾਰ ਭੁਗਤਾਨ ਵਾਲਾ ਹੈ (ਘੱਟੋ-ਘੱਟ $20 ਟਾਪ-ਅੱਪ); ਕੋਈ ਨਿਯਮਿਤ ਮੁਫ਼ਤ ਕ੍ਰੈਡਿਟ ਨਹੀਂ ਹਨ।", "ai21": "ਸਾਈਨ ਅੱਪ ਕਰਨ 'ਤੇ $10 ਦੇ ਟ੍ਰਾਇਲ ਕ੍ਰੈਡਿਟ (3 ਮਹੀਨਿਆਂ ਲਈ ਵੈਧ), ਕ੍ਰੈਡਿਟ ਕਾਰਡ ਦੀ ਲੋੜ ਨਹੀਂ ਹੈ", "alibaba": "Alibaba ਨੂੰ API key ਨਾਲ ਕਨੈਕਟ ਕਰੋ।", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP ਆਡਿਟ (ਦਿਨ)", "retentionA2aEvents": "A2A ਇਵੈਂਟ (ਦਿਨ)", "retentionCallLogs": "ਕਾਲ ਲੌਗ (ਦਿਨ)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "ਵਰਤੋਂ ਇਤਿਹਾਸ (ਦਿਨ)", "retentionMemoryEntries": "ਮੈਮੋਰੀ ਐਂਟਰੀਆਂ (ਦਿਨ)", "retentionXpAuditLog": "XP ਆਡਿਟ ਲੌਗ (ਦਿਨ)", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index 4f7ea29592..8d280cfdbc 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Pinipigilan ng mga proxy pool at ng pag-ikot ng opencode sa bawat account na muling gumamit ng proxy na kabibigo lang (tinanggihan ang TCP probe, o nakatanggap ng 429 sa pamamagitan nito) sa loob ng isang panahon para sa bawat proseso na dumodoble sa bawat pag-ulit, hanggang sa itinakdang limitasyon. Walang isinusulat na status ng proxy; kapag isinantabi ang bawat kandidato, hindi nagbabago ang pagpili. Naka-off bilang default: ang pagkakasunod-sunod ng pagpili ay eksaktong gaya ng karaniwang pag-ikot.", "featureFlagProxyPoolEgressObservationDescription": "Ipakita sa ilalim ng isang proxy pool sa dashboard kung ilang naobserbahang egress IP ang ginamit ng mga miyembro nito sa nakalipas na 24 h, kung ilang koneksyon ang gumamit sa mga ito, at ang pinakamataas na bilang na nakita sa likod ng isang IP. Read-only, kinukuwenta mula sa proxy log, at hindi kailanman ginagamit sa pagruruta. Naka-off bilang default: hindi nagbabago ang editor ng pool at null ang isinasagot ng route ng obserbasyon.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Sa pagsusuri sa kalagayan ng proxy, hayaan ang isang probe na tinanggihan ng target (401/403/429: ipinasa ito ng proxy, ngunit tinanggihan ng destinasyon ang egress IP na ito) na i-reset ang sunod-sunod na pagkabigo ng proxy, tulad ng isang naihatid na probe. Naka-off bilang default: nananatiling neutral ang pagtanggi at hindi nito binabago ang sunod-sunod na pagkabigo. Nananatiling walang tiyak na resulta ang isang 5xx sa alinmang paraan, at ang pagtanggi ay hindi kailanman nag-aalis, nagdi-disable, o muling nag-a-activate ng proxy.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Bahay", "dashboard": "Dashboard", @@ -6125,6 +6126,7 @@ "agentrouter": "Kumuha ng $200 na libreng credit sa https://agentrouter.org/register — walang kinakailangang credit card.", "unorouter": "Gumawa ng API key sa https://unorouter.ai, pagkatapos ay i-paste ito dito bilang Bearer token.", "agnes": "Kumuha ng API key sa agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Naka-pause ang libreng tier (2026) — pay-as-you-go na lang ngayon ang AI/ML API (min $20 na top-up); walang paulit-ulit na libreng credit.", "ai21": "$10 na trial credit sa pag-signup (valid nang 3 buwan), walang kinakailangang credit card", "alibaba": "Ikonekta ang Alibaba gamit ang isang API key.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP Audit (mga araw)", "retentionA2aEvents": "A2A Events (araw)", "retentionCallLogs": "Mga Log ng Tawag (mga araw)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Kasaysayan ng Paggamit (mga araw)", "retentionMemoryEntries": "Mga Entry ng Memorya (mga araw)", "retentionXpAuditLog": "XP Audit Log (mga araw)", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index 454cbb4cd2..91535391c1 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Pule serwerów proxy oraz rotacja opencode dla poszczególnych kont przestają ponownie wybierać serwer proxy, który właśnie zawiódł (odrzucona próba TCP lub otrzymany za jego pośrednictwem kod 429), na okres właściwy dla danego procesu, który podwaja się przy każdym kolejnym takim zdarzeniu, aż do osiągnięcia limitu. Stan serwera proxy nie jest zapisywany; po odłożeniu na bok wszystkich kandydatów wybór pozostaje bez zmian. Domyślnie wyłączone: kolejność wyboru jest dokładnie taka sama jak w zwykłej rotacji.", "featureFlagProxyPoolEgressObservationDescription": "Wyświetla w panelu, pod pulą serwerów proxy, ile zaobserwowanych wychodzących adresów IP obsługiwało jej elementy w ciągu ostatnich 24 godz., ile połączeń z nich korzystało oraz największą liczbę połączeń za jednym adresem IP. Tylko do odczytu, obliczane na podstawie dziennika serwerów proxy i nigdy niewykorzystywane do routingu. Domyślnie wyłączone: edytor puli pozostaje bez zmian, a trasa obserwacji zwraca null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Podczas kontroli kondycji serwerów proxy pozwala, aby próba odrzucona przez cel (401/403/429: serwer proxy przekazał żądanie, ale miejsce docelowe odrzuciło ten wychodzący adres IP) resetowała serię kolejnych niepowodzeń serwera proxy, tak jak obsłużona próba. Domyślnie wyłączone: odrzucenie pozostaje neutralne i nie przerywa serii. Odpowiedź 5xx pozostaje nierozstrzygająca w obu przypadkach, a odrzucenie nigdy nie usuwa, nie wyłącza ani nie aktywuje ponownie serwera proxy.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Strona główna", "dashboard": "Panel", @@ -6125,6 +6126,7 @@ "agentrouter": "Otrzymaj 200 $ darmowych środków na https://agentrouter.org/register — karta kredytowa nie jest wymagana.", "unorouter": "Utwórz klucz API na https://unorouter.ai, a następnie wklej go tutaj jako token Bearer.", "agnes": "Pobierz klucz API na stronie agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Bezpłatny pakiet wstrzymany (2026) — AI/ML API działa teraz wyłącznie w modelu pay-as-you-go (min. doładowanie 20 $); brak cyklicznych darmowych środków.", "ai21": "10 $ środków próbnych przy rejestracji (ważne przez 3 miesiące), karta kredytowa nie jest wymagana", "alibaba": "Połącz z Alibaba za pomocą klucza API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Audit MCP (dni)", "retentionA2aEvents": "Zdarzenia A2A (dni)", "retentionCallLogs": "Logi wywołań (dni)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Historia użycia (dni)", "retentionMemoryEntries": "Wpisy pamięci (dni)", "retentionXpAuditLog": "Log audytu XP (dni)", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 5d83758096..b0b1e863b6 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Pools de proxy e a rotação por conta do opencode deixam de reutilizar um proxy que acabou de falhar (sonda TCP recusada, ou um 429 recebido por ele) por um período por processo que dobra a cada repetição, até um limite. Nenhum status de proxy é gravado; com todos os candidatos deixados de lado, a escolha não muda. Desligado por padrão: a ordem de seleção é exatamente a rotação simples.", "featureFlagProxyPoolEgressObservationDescription": "Mostra, abaixo de um pool de proxy no painel, quantos IPs de saída observados atenderam seus membros nas últimas 24 h, quantas conexões os usaram e o máximo visto atrás de um mesmo IP. Somente leitura, calculado a partir do log de proxy, nunca usado para roteamento. Desligado por padrão: o editor de pool não muda e a rota de observação responde null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Na varredura de saúde de proxy, permite que uma sonda recusada pelo destino (401/403/429: o proxy retransmitiu, o destino recusou este IP de saída) zere a sequência de falhas consecutivas do proxy, como uma sonda atendida. Desligado por padrão: a recusa continua neutra e mantém a sequência. Um 5xx continua inconclusivo em qualquer caso, e uma recusa nunca remove, desativa ou reativa um proxy.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Início", "dashboard": "Painel", @@ -6125,6 +6126,7 @@ "agentrouter": "Obtenha $200 em créditos gratuitos em https://agentrouter.org/register — sem necessidade de cartão de crédito.", "unorouter": "Crie uma chave de API em https://unorouter.ai e cole aqui como Bearer token.", "agnes": "Obtenha a chave de API em agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Nível gratuito pausado (2026) — a AI/ML API agora é apenas pay-as-you-go (recarga mínima de $20); sem créditos gratuitos recorrentes.", "ai21": "$10 em créditos de teste no cadastro (válidos por 3 meses), sem necessidade de cartão de crédito", "alibaba": "Conecte a Alibaba com uma chave de API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Auditoria MCP (dias)", "retentionA2aEvents": "Eventos A2A (dias)", "retentionCallLogs": "Registros de chamadas (dias)", + "retentionConversationTurnNodes": "Nós de turno de conversa (dias)", "retentionUsageHistory": "Histórico de uso (dias)", "retentionMemoryEntries": "Entradas de memória (dias)", "retentionXpAuditLog": "Log de Auditoria de XP (dias)", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 136e992086..260724e2f4 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Os conjuntos de proxies e a rotação por conta do opencode deixam de voltar a disponibilizar um proxy que acabou de falhar (sondagem TCP recusada ou resposta 429 recebida através do mesmo) durante um período por processo que duplica a cada repetição, até um limite máximo. Não é registado qualquer estado do proxy; se todos os candidatos forem postos de parte, a escolha permanece inalterada. Desativado por predefinição: a ordem de seleção corresponde exatamente à rotação simples.", "featureFlagProxyPoolEgressObservationDescription": "Mostrar, sob um conjunto de proxies no painel, quantos IPs de saída observados serviram os respetivos membros nas últimas 24 h, quantas ligações os utilizaram e o maior número observado por trás de um único IP. Apenas de leitura, calculado a partir do registo do proxy e nunca utilizado para encaminhamento. Desativado por predefinição: o editor do conjunto permanece inalterado e a rota de observação devolve null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Na verificação de integridade dos proxies, permitir que uma sondagem recusada pelo destino (401/403/429: o proxy reencaminhou o pedido, mas o destino recusou este IP de saída) reinicie a sequência de falhas consecutivas do proxy, tal como uma sondagem servida. Desativado por predefinição: uma recusa permanece neutra e mantém a sequência. Uma resposta 5xx permanece inconclusiva em qualquer dos casos, e uma recusa nunca remove, desativa nem reativa um proxy.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Página inicial", "dashboard": "Painel", @@ -6125,6 +6126,7 @@ "agentrouter": "Obtenha $200 em créditos gratuitos em https://agentrouter.org/register — sem necessidade de cartão de crédito.", "unorouter": "Crie uma chave de API em https://unorouter.ai, depois cole-a aqui como um token Bearer.", "agnes": "Obtenha a chave de API em agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Plano gratuito pausado (2026) — a API AI/ML é agora apenas pay-as-you-go (carregamento mín. de $20); sem créditos gratuitos recorrentes.", "ai21": "$10 em créditos de avaliação no registo (válidos por 3 meses), sem necessidade de cartão de crédito", "alibaba": "Ligue a Alibaba com uma chave de API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Auditoria MCP (dias)", "retentionA2aEvents": "Eventos A2A (dias)", "retentionCallLogs": "Registros de chamadas (dias)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Histórico de uso (dias)", "retentionMemoryEntries": "Entradas de memória (dias)", "retentionXpAuditLog": "Log de Auditoria de XP (dias)", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index 9c103d9ba8..933dc840fb 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Pool-urile de proxy-uri și rotația per cont din opencode nu mai reutilizează un proxy care tocmai a eșuat (sondă TCP refuzată sau un răspuns 429 primit prin acesta) pentru o perioadă per proces care se dublează la fiecare repetare, până la o limită maximă. Nu este înregistrată nicio stare a proxy-ului; când fiecare candidat este pus deoparte, alegerea rămâne neschimbată. Dezactivat în mod implicit: ordinea de selectare este exact cea a rotației simple.", "featureFlagProxyPoolEgressObservationDescription": "Afișează, sub un pool de proxy-uri din panoul de control, câte IP-uri de ieșire observate au deservit membrii acestuia în ultimele 24 h, câte conexiuni le-au folosit și numărul maxim observat în spatele unui singur IP. Doar pentru citire, calculat din jurnalul proxy-ului și niciodată utilizat pentru rutare. Dezactivat în mod implicit: editorul pool-ului rămâne neschimbat, iar ruta de observare returnează null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "În verificarea periodică a stării proxy-urilor, permite unei sonde refuzate de destinație (401/403/429: proxy-ul a retransmis, iar destinația a refuzat acest IP de ieșire) să reseteze seria de eșecuri consecutive a proxy-ului, la fel ca o sondă deservită. Dezactivat în mod implicit: un refuz rămâne neutru și păstrează seria. Un răspuns 5xx rămâne neconcludent în ambele cazuri, iar un refuz nu elimină, nu dezactivează și nu reactivează niciodată un proxy.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Acasă", "dashboard": "Tabloul de bord", @@ -6125,6 +6126,7 @@ "agentrouter": "Obține credite gratuite de 200 $ la https://agentrouter.org/register — nu este necesar un card de credit.", "unorouter": "Creează un API key la https://unorouter.ai, apoi lipește-l aici ca un Bearer token.", "agnes": "Obține cheia API la agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Nivelul gratuit este întrerupt (2026) — API-ul AI/ML este acum doar de tip pay-as-you-go (reîncărcare minimă de 20 $); fără credite gratuite recurente.", "ai21": "Credite de încercare de 10 $ la înregistrare (valabile 3 luni), nu este necesar un card de credit", "alibaba": "Conectează Alibaba cu o cheie API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Audit MCP (zile)", "retentionA2aEvents": "Evenimente A2A (zile)", "retentionCallLogs": "Jurnalele de apeluri (zile)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Istoricul utilizării (zile)", "retentionMemoryEntries": "Intrări de memorie (zile)", "retentionXpAuditLog": "Jurnal de audit XP (zile)", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 697938d8df..c0086b8929 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Пулы прокси и ротация opencode для каждой учётной записи временно исключают прокси, который только что дал сбой (отклонил проверку TCP или через него был получен ответ 429), на период в рамках процесса, удваивающийся при каждом повторе вплоть до заданного предела. Статус прокси не записывается; если отложены все кандидаты, выбор остаётся прежним. По умолчанию отключено: порядок выбора в точности соответствует обычной ротации.", "featureFlagProxyPoolEgressObservationDescription": "Показывать под пулом прокси на панели мониторинга, сколько наблюдаемых исходящих IP-адресов обслуживали его участников за последние 24 ч, сколько подключений их использовали и какое наибольшее число подключений приходилось на один IP-адрес. Только для чтения, вычисляется по журналу прокси и никогда не используется для маршрутизации. По умолчанию отключено: редактор пула остаётся без изменений, а маршрут наблюдений возвращает null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "При проверке работоспособности прокси позволить проверке, отклонённой целевым сервером (401/403/429: прокси передал запрос, но целевой сервер отклонил этот исходящий IP-адрес), сбрасывать счётчик последовательных сбоев прокси, как при успешно обслуженной проверке. По умолчанию отключено: отказ остаётся нейтральным и не сбрасывает счётчик. Ответ 5xx в любом случае остаётся неопределённым, а отказ никогда не удаляет, не отключает и не активирует прокси повторно.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Главная", "dashboard": "Панель управления", @@ -6125,6 +6126,7 @@ "agentrouter": "Получите бесплатный баланс $200 на https://agentrouter.org/register — кредитная карта не требуется.", "unorouter": "Создайте API-ключ на https://unorouter.ai, затем вставьте его сюда в качестве токена Bearer.", "agnes": "Получите API-ключ на agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Бесплатный тариф приостановлен (2026) — AI/ML API теперь работает только по предоплате (минимальное пополнение $20); регулярные бесплатные кредиты отсутствуют.", "ai21": "Пробный баланс $10 при регистрации (действителен 3 месяца), кредитная карта не требуется", "alibaba": "Подключите Alibaba с помощью API-ключа.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Аудит MCP (дни)", "retentionA2aEvents": "События A2A (дни)", "retentionCallLogs": "Журналы вызовов (дни)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "История использования (дни)", "retentionMemoryEntries": "Записи в памяти (дни)", "retentionXpAuditLog": "Журнал аудита XP (дни)", diff --git a/src/i18n/messages/si.json b/src/i18n/messages/si.json index 24ed7aaf9e..0c13023fa8 100644 --- a/src/i18n/messages/si.json +++ b/src/i18n/messages/si.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "ප්රොක්සි සංචිත සහ opencode හි ගිණුමකට වෙන් වූ මාරු කිරීම, අසාර්ථක වූ ප්රොක්සියක් (ප්රතික්ෂේප වූ TCP පරීක්ෂාවක් හෝ එය හරහා ලැබුණු 429 ප්රතිචාරයක්) එක් එක් නැවත සිදුවීමේදී දෙගුණ වන, උපරිම සීමාවකට යටත් ක්රියාවලි-විශේෂිත කාලයක් සඳහා නැවත භාවිත කිරීම නවත්වයි. කිසිදු ප්රොක්සි තත්ත්වයක් ලියනු නොලැබේ; සෑම අපේක්ෂකයෙකුම පසෙකට කළ විට තේරීම වෙනස් නොවේ. පෙරනිමියෙන් අක්රියයි: තේරීම් අනුපිළිවෙළ සරල මාරු කිරීමේ අනුපිළිවෙළට හරියටම සමාන වේ.", "featureFlagProxyPoolEgressObservationDescription": "උපකරණ පුවරුවේ ප්රොක්සි සංචිතයක් යටතේ, පසුගිය පැය 24 තුළ එහි සාමාජිකයන්ට සේවය සැපයූ නිරීක්ෂිත පිටතට යන IP ලිපින ගණන, ඒවා භාවිත කළ සම්බන්ධතා ගණන සහ එක් IP ලිපිනයක් පිටුපසින් නිරීක්ෂණය වූ උපරිම ගණන පෙන්වන්න. මෙය කියවීමට පමණක් වන අතර ප්රොක්සි ලොගයෙන් ගණනය කෙරෙන අතර මාර්ගගත කිරීම සඳහා කිසි විටෙක භාවිත නොකෙරේ. පෙරනිමියෙන් අක්රියයි: සංචිත සංස්කාරකය වෙනස් නොවන අතර නිරීක්ෂණ මාර්ගය null ලෙස පිළිතුරු දෙයි.", "featureFlagProxyHealthBlockedResetsStreakDescription": "ප්රොක්සි සෞඛ්ය පරීක්ෂණ වටයේදී, ඉලක්කය ප්රතික්ෂේප කළ පරීක්ෂාවකට (401/403/429: ප්රොක්සිය ප්රතිචාරය ප්රචාරණය කළ නමුත් ගමනාන්තය මෙම පිටතට යන IP ලිපිනය ප්රතික්ෂේප කළේය) සාර්ථකව සේවය කළ පරීක්ෂාවක් මෙන් ප්රොක්සියේ අඛණ්ඩ අසාර්ථකවීම් පෙළ යළි සැකසීමට ඉඩ දෙන්න. පෙරනිමියෙන් අක්රියයි: ප්රතික්ෂේප කිරීමක් මධ්යස්ථව පවතින අතර අඛණ්ඩ අසාර්ථකවීම් පෙළ එලෙසම තබයි. 5xx ප්රතිචාරයක් අවස්ථා දෙකේදීම අවිනිශ්චිතව පවතින අතර, ප්රතික්ෂේප කිරීමක් කිසි විටෙක ප්රොක්සියක් ඉවත් කිරීම, අක්රිය කිරීම හෝ නැවත සක්රිය කිරීම සිදු නොකරයි.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "මුල් පිටුව", "dashboard": "උපකරණ පුවරුව", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register වෙතින් නොමිලේ $200 ණය ලබා ගන්න — ක්රෙඩිට් කාඩ්පතක් අවශ්ය නොවේ.", "unorouter": "https://unorouter.ai හි API යතුරක් සාදා, එය Bearer ටෝකනයක් ලෙස මෙහි අලවන්න.", "agnes": "agnes-ai.com වෙතින් API යතුර ලබා ගන්න", + "agnes-cn": "__MISSING__:Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "නොමිලේ ස්තරය තාවකාලිකව නවතා ඇත (2026) — AI/ML API දැන් භාවිතය අනුව ගෙවීමේ ක්රමයට පමණක් ලබා ගත හැකිය (අවම $20 නැවත පිරවීමක්); නැවත නැවත ලැබෙන නොමිලේ ණය නොමැත.", "ai21": "ලියාපදිංචි වීමේදී $10 අත්හදා බැලීමේ ණය (මාස 3ක් වලංගුයි), ක්රෙඩිට් කාඩ්පතක් අවශ්ය නොවේ", "alibaba": "API යතුරක් සමඟ Alibaba සම්බන්ධ කරන්න.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP විගණනය (දින)", "retentionA2aEvents": "A2A සිදුවීම් (දින)", "retentionCallLogs": "ඇමතුම් ලොග් (දින)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "භාවිත ඉතිහාසය (දින)", "retentionMemoryEntries": "මතක ඇතුළත් කිරීම් (දින)", "retentionXpAuditLog": "XP විගණන ලොගය (දින)", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index 7e1a6d557a..3d56b45a00 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Fondy proxy serverov a rotácia opencode pre jednotlivé účty prestanú počas obdobia platného pre daný proces opätovne používať proxy server, ktorý práve zlyhal (odmietnutá sonda TCP alebo odpoveď 429 prijatá cez tento proxy server). Toto obdobie sa pri každom opakovaní zdvojnásobí až po stanovený limit. Stav proxy servera sa nezapisuje; ak sú všetci kandidáti odložení bokom, výber zostáva nezmenený. Predvolene vypnuté: poradie výberu presne zodpovedá bežnej rotácii.", "featureFlagProxyPoolEgressObservationDescription": "V ovládacom paneli zobraziť pod fondom proxy serverov, koľko pozorovaných výstupných IP adries obsluhovalo jeho členov za posledných 24 h, koľko pripojení ich použilo a najvyšší počet zaznamenaný za jednou IP adresou. Iba na čítanie, vypočítané z denníka proxy servera a nikdy nepoužívané na smerovanie. Predvolene vypnuté: editor fondu zostáva nezmenený a trasa pozorovania vracia null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Pri kontrole stavu proxy serverov umožniť, aby sonda odmietnutá cieľom (401/403/429: proxy server ju preniesol, ale cieľ odmietol túto výstupnú IP adresu) vynulovala sériu po sebe nasledujúcich zlyhaní proxy servera rovnako ako obslúžená sonda. Predvolene vypnuté: odmietnutie zostáva neutrálne a zachováva sériu. Odpoveď 5xx zostáva v oboch prípadoch nejednoznačná a odmietnutie nikdy neodstráni, nezakáže ani znovu neaktivuje proxy server.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Domov", "dashboard": "Prehľad", @@ -6125,6 +6126,7 @@ "agentrouter": "Získajte bezplatný kredit 200 $ na https://agentrouter.org/register — nevyžaduje sa kreditná karta.", "unorouter": "Vytvorte API kľúč na https://unorouter.ai, potom ho sem vložte ako Bearer token.", "agnes": "Získajte API kľúč na agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Bezplatná úroveň pozastavená (2026) — AI/ML API je teraz len vo forme pay-as-you-go (min. dobitie 20 $); žiadne opakujúce sa bezplatné kredity.", "ai21": "Skúšobný kredit 10 $ pri registrácii (platnosť 3 mesiace), nevyžaduje sa kreditná karta", "alibaba": "Pripojte Alibaba pomocou API kľúča.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Audit MCP (dni)", "retentionA2aEvents": "Udalosti A2A (dni)", "retentionCallLogs": "Denníky hovorov (dni)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "História používania (dni)", "retentionMemoryEntries": "Záznamy pamäte (dni)", "retentionXpAuditLog": "Auditný log XP (dni)", diff --git a/src/i18n/messages/sl.json b/src/i18n/messages/sl.json index f6dc628bf8..a0143ad73a 100644 --- a/src/i18n/messages/sl.json +++ b/src/i18n/messages/sl.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Skupine posredniških strežnikov in vrtenje opencode za posamezen račun za določeno obdobje znotraj procesa prenehajo znova uporabljati posredniški strežnik, ki je pravkar odpovedal (zavrnjeno preverjanje TCP ali prek njega prejeta napaka 429). To obdobje se ob vsaki ponovitvi podvoji do določene zgornje meje. Stanje posredniškega strežnika se ne zapiše; če so vsi kandidati izločeni, izbira ostane nespremenjena. Privzeto izklopljeno: vrstni red izbire je popolnoma enak običajnemu vrtenju.", "featureFlagProxyPoolEgressObservationDescription": "Na nadzorni plošči pod skupino posredniških strežnikov prikaži, koliko opaženih izhodnih naslovov IP je v zadnjih 24 h uporabljalo njene člane, koliko povezav jih je uporabilo in največje število povezav prek enega naslova IP. Podatki so samo za branje, izračunani iz dnevnika posredniškega strežnika in se nikoli ne uporabljajo za usmerjanje. Privzeto izklopljeno: urejevalnik skupine ostane nespremenjen, opazovalna pot pa vrne null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Pri preverjanju zdravja posredniških strežnikov naj sonda, ki jo je cilj zavrnil (401/403/429: posredniški strežnik jo je posredoval, cilj pa je zavrnil ta izhodni naslov IP), ponastavi niz zaporednih neuspehov posredniškega strežnika, tako kot uspešno izvedena sonda. Privzeto izklopljeno: zavrnitev ostane nevtralna in ohrani niz. Odziv 5xx v obeh primerih ostane nedoločen, zavrnitev pa posredniškega strežnika nikoli ne odstrani, onemogoči ali znova aktivira.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Domov", "dashboard": "Nadzorna plošča", @@ -6125,6 +6126,7 @@ "agentrouter": "Pridobite $200 brezplačnega dobroimetja na https://agentrouter.org/register — kreditna kartica ni potrebna.", "unorouter": "Ustvarite ključ API na https://unorouter.ai in ga nato prilepite sem kot žeton Bearer.", "agnes": "Pridobite ključ API na agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Brezplačna raven je začasno ustavljena (2026) — API za AI/ML je zdaj na voljo samo po modelu plačila glede na porabo (najmanjše dobroimetje $20); brez ponavljajočega se brezplačnega dobroimetja.", "ai21": "$10 preizkusnega dobroimetja ob registraciji (velja 3 mesece), kreditna kartica ni potrebna", "alibaba": "Povežite Alibaba s ključem API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Revizija MCP (dni)", "retentionA2aEvents": "Dogodki A2A (dni)", "retentionCallLogs": "Dnevniki klicev (dni)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Zgodovina uporabe (dni)", "retentionMemoryEntries": "Vnosi v pomnilnik (dni)", "retentionXpAuditLog": "Revizijski dnevnik XP (dni)", diff --git a/src/i18n/messages/sr.json b/src/i18n/messages/sr.json index 36855a8f7d..1fbb07810b 100644 --- a/src/i18n/messages/sr.json +++ b/src/i18n/messages/sr.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Прокси скупови и ротација по налогу у opencode престају да поново користе прокси који је управо отказао (одбијена TCP провера или одговор 429 примљен преко њега) током периода на нивоу процеса који се удвостручује при сваком понављању, до задате горње границе. Статус проксија се не уписује; када су сви кандидати издвојени, избор остаје непромењен. Подразумевано је искључено: редослед избора је потпуно исти као код обичне ротације.", "featureFlagProxyPoolEgressObservationDescription": "У контролној табли, испод прокси скупа, прикажите колико је уочених излазних IP адреса опслуживало његове чланове током последња 24 ч, колико их је веза користило и највећи број веза забележен иза једне IP адресе. Само за читање, израчунава се из евиденције проксија и никада се не користи за усмеравање. Подразумевано је искључено: уређивач скупа остаје непромењен, а рута за посматрање враћа null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "У провери исправности проксија омогућите да провера коју је циљ одбио (401/403/429: прокси је проследио захтев, а одредиште је одбило ову излазну IP адресу) ресетује низ узастопних неуспеха проксија, као и успешно опслужена провера. Подразумевано је искључено: одбијање остаје неутрално и задржава низ. Одговор 5xx у оба случаја остаје неодређен, а одбијање никада не уклања, не онемогућава нити поново активира прокси.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Почетна", "dashboard": "Контролна табла", @@ -6125,6 +6126,7 @@ "agentrouter": "Ostvarite besplatnih 200$ kredita na https://agentrouter.org/register — kreditna kartica nije potrebna.", "unorouter": "Napravite API ključ na https://unorouter.ai, zatim ga ovde nalepite kao Bearer token.", "agnes": "Preuzmite API ključ na agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Besplatni nivo pauziran (2026) — AI/ML API je sada samo plaćanje po korišćenju (min. dopuna 20$); nema stalnih besplatnih kredita.", "ai21": "10$ probnih kredita pri registraciji (važi 3 meseca), kreditna kartica nije potrebna", "alibaba": "Povežite Alibaba pomoću API ključa.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP ревизија (дани)", "retentionA2aEvents": "A2A догађаји (дани)", "retentionCallLogs": "Евиденције позива (дани)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Историја коришћења (дани)", "retentionMemoryEntries": "Ставке меморије (дани)", "retentionXpAuditLog": "XP лог ревизије (дани)", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index 5b0bfe8350..5cbb34b414 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proxypooler och opencodes rotation per konto slutar att återanvända en proxy som nyss misslyckades (nekad TCP-kontroll eller ett 429-svar som togs emot via den) under en processpecifik period som fördubblas vid varje upprepat fel, upp till en maxgräns. Ingen proxystatus skrivs; om alla kandidater läggs åt sidan förblir valet oförändrat. Avstängt som standard: urvalsordningen följer exakt den vanliga rotationen.", "featureFlagProxyPoolEgressObservationDescription": "Visa under en proxypool på instrumentpanelen hur många observerade utgående IP-adresser som betjänade dess medlemmar under de senaste 24 timmarna, hur många anslutningar som använde dem och det högsta antalet som observerades bakom en och samma IP-adress. Skrivskyddat, beräknat från proxyloggen och används aldrig för dirigering. Avstängt som standard: poolredigeraren är oförändrad och observationsrutten returnerar null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Låt en kontroll som avvisades av målet (401/403/429: proxyn vidarebefordrade begäran men destinationen avvisade den utgående IP-adressen) nollställa proxyns serie av på varandra följande fel i proxyhälsokontrollen, precis som en betjänad kontroll. Avstängt som standard: ett avvisande förblir neutralt och bibehåller serien. Ett 5xx-svar förblir icke avgörande i båda fallen, och ett avvisande tar aldrig bort, inaktiverar eller återaktiverar en proxy.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Hem", "dashboard": "Instrumentpanel", @@ -6125,6 +6126,7 @@ "agentrouter": "Få $200 i gratiskrediter på https://agentrouter.org/register — inget kreditkort krävs.", "unorouter": "Skapa en API-nyckel på https://unorouter.ai, klistra sedan in den här som en Bearer-token.", "agnes": "Hämta API-nyckel på agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Gratisnivån pausad (2026) — AI/ML API är nu endast pay-as-you-go (minst $20 påfyllning); inga återkommande gratiskrediter.", "ai21": "$10 i testkrediter vid registrering (giltiga i 3 månader), inget kreditkort krävs", "alibaba": "Anslut Alibaba med en API-nyckel.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP-revision (dagar)", "retentionA2aEvents": "A2A-evenemang (dagar)", "retentionCallLogs": "Samtalsloggar (dagar)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Användningshistorik (dagar)", "retentionMemoryEntries": "Minnesposter (dagar)", "retentionXpAuditLog": "XP-granskningslogg (dagar)", diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index b3f6e4fb7c..85f278fbc3 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Makundi ya proksi na uzungushaji wa kila akaunti wa opencode huacha kutumia tena proksi iliyoshindwa hivi punde (jaribio la TCP lilikataliwa, au 429 ilipokelewa kupitia proksi hiyo) kwa kipindi maalumu kwa kila mchakato, ambacho huongezeka maradufu kila kushindwa kunaporudiwa, hadi kikomo fulani. Hakuna hali ya proksi inayoandikwa; kila chaguo linalowezekana likiwekwa kando, uteuzi hubaki bila kubadilika. Imezimwa kwa chaguo-msingi: mpangilio wa uteuzi ni sawa kabisa na uzungushaji wa kawaida.", "featureFlagProxyPoolEgressObservationDescription": "Onyesha, chini ya kundi la proksi kwenye dashibodi, idadi ya anwani za IP za kutoka zilizozingatiwa ambazo zilitumiwa na wanachama wake katika saa 24 zilizopita, idadi ya miunganisho iliyozitumia, na idadi kubwa zaidi iliyoonekana nyuma ya anwani moja ya IP. Ni ya kusoma pekee, huhesabiwa kutoka kwenye kumbukumbu ya proksi, na haitumiki kamwe kuelekeza trafiki. Imezimwa kwa chaguo-msingi: kihariri cha kundi hakibadiliki na njia ya uchunguzi hujibu null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Katika ukaguzi wa afya ya proksi, ruhusu jaribio lililokataliwa na lengwa (401/403/429: proksi ilisambaza ombi, lakini unakoenda kukakataa anwani hii ya IP ya kutoka) liweke upya mfululizo wa kushindwa kwa proksi, kama jaribio lililohudumiwa. Imezimwa kwa chaguo-msingi: kukataliwa hubaki bila upande na hudumisha mfululizo huo. 5xx hubaki bila hitimisho kwa vyovyote vile, na kukataliwa kamwe hakuondoi, hakuzima wala kuwezesha tena proksi.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Nyumbani", "dashboard": "Dashibodi", @@ -6125,6 +6126,7 @@ "agentrouter": "Pata salio la bure la $200 kwenye https://agentrouter.org/register — hakuna kadi ya mkopo inayohitajika.", "unorouter": "Unda ufunguo wa API kwenye https://unorouter.ai, kisha ubandike hapa kama token ya Bearer.", "agnes": "Pata ufunguo wa API kwenye agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Kiwango cha bure kimesitishwa (2026) — AI/ML API sasa ni ya kulipia kadri unavyotumia pekee (kiwango cha chini cha kuongeza salio ni $20); hakuna salio la bure linalojirudia.", "ai21": "Salio la majaribio la $10 wakati wa kujisajili (halali kwa miezi 3), hakuna kadi ya mkopo inayohitajika", "alibaba": "Unganisha Alibaba kwa ufunguo wa API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Ukaguzi wa MCP (siku)", "retentionA2aEvents": "Matukio ya A2A (siku)", "retentionCallLogs": "Rekodi za simu (siku)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Historia ya Matumizi (siku)", "retentionMemoryEntries": "Maingizo ya Kumbukumbu (siku)", "retentionXpAuditLog": "Kumbukumbu ya Ukaguzi wa XP (siku)", diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index b4de89c2a4..f3ba1445ba 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proxy குளங்களும் opencode-இன் ஒவ்வொரு கணக்கிற்குமான சுழற்சியும், இப்போது தோல்வியடைந்த ஒரு proxy-ஐ (மறுக்கப்பட்ட TCP ஆய்வு அல்லது அதன் வழியாகப் பெறப்பட்ட 429) ஒவ்வொரு செயல்முறைக்கும் உரிய ஒரு காலகட்டத்திற்கு மீண்டும் வழங்குவதை நிறுத்தும்; ஒவ்வொரு தொடர் தோல்வியின்போதும் அந்தக் காலகட்டம் இரட்டிப்பாகி, அதிகபட்ச வரம்பு வரை செல்லும். எந்த proxy நிலையும் எழுதப்படாது; ஒவ்வொரு தேர்வுக்குரியதும் ஒதுக்கி வைக்கப்பட்டால், தேர்வு மாறாமல் இருக்கும். இயல்பாக முடக்கப்பட்டிருக்கும்: தேர்வு வரிசை சாதாரண சுழற்சியை அப்படியே பின்பற்றும்.", "featureFlagProxyPoolEgressObservationDescription": "டாஷ்போர்டில் ஒரு proxy குளத்தின் கீழ், கடந்த 24 மணிநேரத்தில் அதன் உறுப்பினர்களுக்குச் சேவையளித்த கண்டறியப்பட்ட வெளியேற்ற IP-களின் எண்ணிக்கை, அவற்றைப் பயன்படுத்திய இணைப்புகளின் எண்ணிக்கை மற்றும் ஓர் IP-க்குப் பின்னால் காணப்பட்ட அதிகபட்ச எண்ணிக்கை ஆகியவற்றைக் காட்டும். படிக்க மட்டும்; proxy பதிவிலிருந்து கணக்கிடப்படும்; வழித்தடத் தேர்வுக்கு ஒருபோதும் பயன்படுத்தப்படாது. இயல்பாக முடக்கப்பட்டிருக்கும்: குளத் திருத்தி மாறாமல் இருக்கும், மேலும் கண்காணிப்பு வழித்தடம் null எனப் பதிலளிக்கும்.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Proxy ஆரோக்கியச் சோதனையில், இலக்கு மறுத்த ஓர் ஆய்வு (401/403/429: proxy அதை அனுப்பியது, ஆனால் இலக்கு இந்த வெளியேற்ற IP-ஐ மறுத்தது), சேவையளிக்கப்பட்ட ஆய்வைப் போலவே proxy-இன் தொடர்ச்சியான தோல்வி வரிசையை மீட்டமைக்க அனுமதிக்கும். இயல்பாக முடக்கப்பட்டிருக்கும்: ஒரு மறுப்பு நடுநிலையாகவே இருந்து, தோல்வி வரிசையைத் தொடர வைத்திருக்கும். எந்த நிலையிலும் 5xx முடிவுறாததாகவே இருக்கும்; மேலும் ஒரு மறுப்பு ஒருபோதும் proxy-ஐ அகற்றவோ, முடக்கவோ அல்லது மீண்டும் செயல்படுத்தவோ செய்யாது.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "முகப்பு", "dashboard": "கட்டுப்பாட்டுப் பலகம்", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register இல் $200 இலவச கிரெடிட்களைப் பெறவும் — கிரெடிட் கார்டு தேவையில்லை.", "unorouter": "https://unorouter.ai இல் ஒரு API விசையை உருவாக்கவும், பின்னர் அதை இங்கே Bearer டோக்கனாக ஒட்டவும்.", "agnes": "agnes-ai.com இல் API கீயைப் பெறவும்", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "இலவச அடுக்கு இடைநிறுத்தப்பட்டுள்ளது (2026) — AI/ML API இப்போது பயன்படுத்தியதற்கு மட்டும் செலுத்தும் (குறைந்தபட்சம் $20 டாப்-அப்) முறையில் மட்டுமே உள்ளது; தொடர்ச்சியான இலவச கிரெடிட்கள் இல்லை.", "ai21": "பதிவு செய்யும் போது $10 சோதனை கிரெடிட்கள் (3 மாதங்கள் செல்லுபடியாகும்), கிரெடிட் கார்டு தேவையில்லை", "alibaba": "Alibaba-வை ஒரு API கீயுடன் இணைக்கவும்.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP தணிக்கை (நாட்கள்)", "retentionA2aEvents": "A2A நிகழ்வுகள் (நாட்கள்)", "retentionCallLogs": "அழைப்பு பதிவுகள் (நாட்கள்)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "பயன்பாட்டு வரலாறு (நாட்கள்)", "retentionMemoryEntries": "நினைவக உள்ளீடுகள் (நாட்கள்)", "retentionXpAuditLog": "XP தணிக்கைப் பதிவு (நாட்கள்)", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index ab48cf6f81..6377121ff4 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proxy పూల్లు మరియు opencode యొక్క ఒక్కో ఖాతా రొటేషన్లో, అప్పుడే విఫలమైన Proxyని (TCP ప్రోబ్ తిరస్కరించబడటం లేదా దాని ద్వారా 429 అందుకోవడం) ప్రతి పునరావృతంతో రెట్టింపయ్యే, గరిష్ఠ పరిమితి వరకు ఉండే ఒక్కో ప్రాసెస్ వ్యవధిలో మళ్లీ అందించకుండా ఆపుతుంది. Proxy స్థితి ఏదీ వ్రాయబడదు; ప్రతి అభ్యర్థినీ పక్కన పెడితే ఎంపికలో మార్పు ఉండదు. డిఫాల్ట్గా ఆఫ్లో ఉంటుంది: ఎంపిక క్రమం సరిగ్గా సాధారణ రొటేషన్లానే ఉంటుంది.", "featureFlagProxyPoolEgressObservationDescription": "డ్యాష్బోర్డ్లోని Proxy పూల్ కింద, గత 24 గంటల్లో దాని సభ్యులకు సేవలందించిన గమనించిన ఎగ్రెస్ IPల సంఖ్య, వాటిని ఉపయోగించిన కనెక్షన్ల సంఖ్య మరియు ఒకే IP వెనుక అత్యధికంగా కనిపించిన సంఖ్యను చూపండి. ఇది చదవడానికి మాత్రమే, Proxy లాగ్ నుండి గణించబడుతుంది, రూటింగ్ కోసం ఎప్పుడూ ఉపయోగించబడదు. డిఫాల్ట్గా ఆఫ్లో ఉంటుంది: పూల్ ఎడిటర్ మారదు మరియు పరిశీలన రూట్ null అని సమాధానమిస్తుంది.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Proxy ఆరోగ్య స్వీప్లో, లక్ష్యం తిరస్కరించిన ప్రోబ్ (401/403/429: Proxy ప్రసారం చేసింది, గమ్యస్థానం ఈ ఎగ్రెస్ IPని తిరస్కరించింది) విజయవంతంగా సేవలందించిన ప్రోబ్లాగే Proxy యొక్క వరుస వైఫల్యాల పరంపరను రీసెట్ చేయనివ్వండి. డిఫాల్ట్గా ఆఫ్లో ఉంటుంది: తిరస్కరణ తటస్థంగానే ఉండి పరంపరను అలాగే ఉంచుతుంది. ఏ విధంగానైనా 5xx అనిశ్చితంగానే ఉంటుంది, అలాగే తిరస్కరణ ఎప్పుడూ Proxyని తొలగించదు, నిలిపివేయదు లేదా మళ్లీ సక్రియం చేయదు.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "హోమ్", "dashboard": "డ్యాష్బోర్డ్", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register వద్ద $200 ఉచిత క్రెడిట్‌లను పొందండి — క్రెడిట్ కార్డ్ అవసరం లేదు.", "unorouter": "https://unorouter.ai వద్ద ఒక API కీ సృష్టించండి, తరువాత దాన్ని ఇక్కడ Bearer టోకెన్‌గా పేస్ట్ చేయండి.", "agnes": "agnes-ai.com వద్ద API కీని పొందండి", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "ఉచిత శ్రేణి నిలిపివేయబడింది (2026) — AI/ML API ఇప్పుడు పే-యాస్-యూ-గో మాత్రమే (కనీసం $20 టాప్-అప్); పునరావృత ఉచిత క్రెడిట్‌లు లేవు.", "ai21": "సైన్అప్ చేసినప్పుడు $10 ట్రయల్ క్రెడిట్‌లు (3 నెలల పాటు చెల్లుబాటు అవుతాయి), క్రెడిట్ కార్డ్ అవసరం లేదు", "alibaba": "API కీతో Alibabaని కనెక్ట్ చేయండి.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP ఆడిట్ (రోజులు)", "retentionA2aEvents": "A2A ఈవెంట్‌లు (రోజులు)", "retentionCallLogs": "కాల్ లాగ్‌లు (రోజులు)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "వినియోగ చరిత్ర (రోజులు)", "retentionMemoryEntries": "మెమరీ ఎంట్రీలు (రోజులు)", "retentionXpAuditLog": "XP ఆడిట్ లాగ్ (రోజులు)", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index 8d512b93d4..3583108046 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "พูลพร็อกซีและการหมุนเวียนต่อบัญชีของ opencode จะหยุดนำพร็อกซีที่เพิ่งล้มเหลว (ปฏิเสธการตรวจสอบ TCP หรือได้รับ 429 ผ่านพร็อกซีนั้น) กลับมาให้บริการซ้ำเป็นระยะเวลาต่อโปรเซส ซึ่งจะเพิ่มเป็นสองเท่าทุกครั้งที่เกิดซ้ำจนถึงขีดจำกัด โดยจะไม่มีการบันทึกสถานะพร็อกซี และหากตัวเลือกทั้งหมดถูกพักไว้ การเลือกจะไม่เปลี่ยนแปลง ปิดไว้โดยค่าเริ่มต้น: ลำดับการเลือกจะเป็นการหมุนเวียนแบบปกติทุกประการ", "featureFlagProxyPoolEgressObservationDescription": "แสดงใต้พูลพร็อกซีในแดชบอร์ดว่ามี IP ขาออกที่ตรวจพบกี่รายการซึ่งให้บริการสมาชิกของพูลในช่วง 24 h ที่ผ่านมา มีการเชื่อมต่อผ่าน IP เหล่านั้นกี่ครั้ง และจำนวนสูงสุดที่พบหลัง IP เดียว ข้อมูลนี้เป็นแบบอ่านอย่างเดียว คำนวณจากบันทึกพร็อกซี และไม่เคยนำไปใช้กำหนดเส้นทาง ปิดไว้โดยค่าเริ่มต้น: ตัวแก้ไขพูลจะไม่เปลี่ยนแปลง และเส้นทางการสังเกตการณ์จะตอบกลับเป็น null", "featureFlagProxyHealthBlockedResetsStreakDescription": "ในการกวาดตรวจสุขภาพพร็อกซี ให้การตรวจสอบที่ถูกเป้าหมายปฏิเสธ (401/403/429: พร็อกซีส่งต่อสำเร็จ แต่ปลายทางปฏิเสธ IP ขาออกนี้) รีเซ็ตจำนวนความล้มเหลวต่อเนื่องของพร็อกซีได้เช่นเดียวกับการตรวจสอบที่ได้รับการให้บริการ ปิดไว้โดยค่าเริ่มต้น: การปฏิเสธจะยังคงเป็นกลางและคงจำนวนความล้มเหลวต่อเนื่องไว้ 5xx ยังคงถือว่าไม่สามารถสรุปผลได้ในทั้งสองกรณี และการปฏิเสธจะไม่ลบ ปิดใช้งาน หรือเปิดใช้งานพร็อกซีอีกครั้ง", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "บ้าน", "dashboard": "แดชบอร์ด", @@ -6125,6 +6126,7 @@ "agentrouter": "รับเครดิตฟรี $200 ที่ https://agentrouter.org/register — ไม่ต้องใช้บัตรเครดิต", "unorouter": "สร้าง API key ที่ https://unorouter.ai จากนั้นวางที่นี่เป็น Bearer token.", "agnes": "รับคีย์ API ได้ที่ agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "ระงับระดับฟรีชั่วคราว (2026) — ตอนนี้ AI/ML API เป็นแบบจ่ายตามการใช้งานจริงเท่านั้น (เติมเงินขั้นต่ำ $20) ไม่มีเครดิตฟรีที่ได้รับเป็นประจำ", "ai21": "เครดิตทดลองใช้งาน $10 เมื่อสมัครใช้งาน (มีอายุ 3 เดือน) ไม่ต้องใช้บัตรเครดิต", "alibaba": "เชื่อมต่อ Alibaba ด้วยคีย์ API", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "การตรวจสอบ MCP (วัน)", "retentionA2aEvents": "เหตุการณ์ A2A (วัน)", "retentionCallLogs": "บันทึกการโทร (วัน)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "ประวัติการใช้งาน (วัน)", "retentionMemoryEntries": "รายการหน่วยความจำ (วัน)", "retentionXpAuditLog": "บันทึกการตรวจสอบ XP (วัน)", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index 8eef16e513..9bf6fd5121 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proxy havuzları ve opencode'un hesap başına rotasyonu, henüz başarısız olmuş bir proxy'nin (reddedilen TCP yoklaması veya proxy üzerinden alınan 429 yanıtı) süreç başına belirlenen ve her tekrarda üst sınıra kadar iki katına çıkan bir süre boyunca yeniden kullanılmasını engeller. Proxy durumuna hiçbir şey yazılmaz; tüm adaylar kenara ayrıldığında seçim değişmeden kalır. Varsayılan olarak kapalıdır: seçim sırası normal rotasyonla tamamen aynıdır.", "featureFlagProxyPoolEgressObservationDescription": "Kontrol panelinde bir proxy havuzunun altında, son 24 saatte üyelerine kaç farklı gözlemlenen çıkış IP'sinin hizmet verdiğini, bunları kaç bağlantının kullandığını ve tek bir IP'nin arkasında en fazla kaç bağlantı görüldüğünü gösterir. Salt okunurdur, proxy günlüğünden hesaplanır ve yönlendirme için asla kullanılmaz. Varsayılan olarak kapalıdır: havuz düzenleyicisi değişmeden kalır ve gözlem rotası null yanıtını verir.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Proxy sağlık taramasında, hedefin reddettiği bir yoklamanın (401/403/429: proxy isteği iletti, hedef bu çıkış IP'sini reddetti) proxy'nin ardışık başarısızlık serisini, başarılı bir yoklamada olduğu gibi sıfırlamasını sağlar. Varsayılan olarak kapalıdır: bir ret nötr kalır ve seriyi korur. 5xx her iki durumda da sonuçsuz kalır ve bir ret hiçbir zaman proxy'yi kaldırmaz, devre dışı bırakmaz veya yeniden etkinleştirmez.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Ana Sayfa", "dashboard": "Kontrol Paneli", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register adresinden 200$ ücretsiz kredi alın — kredi kartı gerekmez.", "unorouter": "https://unorouter.ai adresinde bir API anahtarı oluşturun, ardından buraya Bearer token olarak yapıştırın.", "agnes": "API anahtarını agnes-ai.com adresinden alın", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Ücretsiz katman duraklatıldı (2026) — AI/ML API artık yalnızca kullandıkça öde modelindedir (en az 20$ yükleme); yinelenen ücretsiz kredi yoktur.", "ai21": "Kaydolduğunda 10$ deneme kredisi (3 ay geçerli), kredi kartı gerekmez", "alibaba": "Alibaba'yı bir API anahtarı ile bağlayın.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP Denetimi (gün)", "retentionA2aEvents": "A2A Etkinlikleri (günler)", "retentionCallLogs": "Arama Günlükleri (gün)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Kullanım Geçmişi (gün)", "retentionMemoryEntries": "Bellek Girişleri (gün)", "retentionXpAuditLog": "XP Denetim Günlüğü (gün)", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index 5b95cd89dd..a18b342292 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Пули проксі та ротація opencode для кожного облікового запису тимчасово припиняють повторно використовувати проксі, який щойно дав збій (відхилена перевірка TCP або отримана через нього відповідь 429), на період у межах процесу, що подвоюється після кожного повторного збою, аж до встановленої межі. Статус проксі не записується; якщо всі кандидати відкладені, вибір залишається незмінним. За замовчуванням вимкнено: порядок вибору точно відповідає звичайній ротації.", "featureFlagProxyPoolEgressObservationDescription": "Показувати на інформаційній панелі під пулом проксі, скільки спостережуваних вихідних IP-адрес обслуговували його учасників протягом останніх 24 год, скільки з’єднань їх використовували та найбільшу кількість з’єднань через одну IP-адресу. Лише для читання, обчислюється з журналу проксі та ніколи не використовується для маршрутизації. За замовчуванням вимкнено: редактор пулу залишається без змін, а маршрут спостереження повертає null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Під час перевірки працездатності проксі дозволити перевірці, яку відхилила ціль (401/403/429: проксі передав запит, а місце призначення відхилило цю вихідну IP-адресу), скидати лічильник послідовних збоїв проксі, як у разі успішно обслугованої перевірки. За замовчуванням вимкнено: відмова залишається нейтральною та не скидає лічильник. Відповідь 5xx в обох випадках залишається невизначеною, а відмова ніколи не призводить до видалення, вимкнення чи повторної активації проксі.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "додому", "dashboard": "Приладова панель", @@ -6125,6 +6126,7 @@ "agentrouter": "Отримайте $200 безкоштовних кредитів на https://agentrouter.org/register — кредитна картка не потрібна.", "unorouter": "Створіть API-ключ на https://unorouter.ai, а потім вставте його сюди як токен Bearer.", "agnes": "Отримайте ключ API на agnes-ai.com", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Безкоштовний тариф призупинено (2026) — AI/ML API тепер працює лише за моделлю pay-as-you-go (мін. поповнення $20); без регулярних безкоштовних кредитів.", "ai21": "$10 пробних кредитів під час реєстрації (дійсні 3 місяці), кредитна картка не потрібна", "alibaba": "Підключіть Alibaba за допомогою ключа API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Аудит MCP (днів)", "retentionA2aEvents": "Події A2A (дні)", "retentionCallLogs": "Журнали викликів (дні)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Історія використання (дні)", "retentionMemoryEntries": "Записи пам'яті (дні)", "retentionXpAuditLog": "Журнал аудиту XP (дні)", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index af87e321f1..63c7c58350 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "پراکسی پولز اور opencode کی فی اکاؤنٹ روٹیشن کسی ایسی پراکسی کو دوبارہ پیش کرنے سے عارضی طور پر روکتی ہے جو ابھی ناکام ہوئی ہو (TCP پروب مسترد ہوا ہو، یا اس کے ذریعے 429 موصول ہوا ہو)۔ یہ فی پروسیس مدت ہر بار ناکامی دہرائے جانے پر دگنی ہوتی ہے، ایک مقررہ حد تک۔ پراکسی کا کوئی اسٹیٹس نہیں لکھا جاتا؛ تمام امیدواروں کو ایک طرف رکھ دیے جانے کی صورت میں انتخاب میں کوئی تبدیلی نہیں آتی۔ بطور ڈیفالٹ بند: انتخاب کی ترتیب بالکل سادہ روٹیشن کے مطابق رہتی ہے۔", "featureFlagProxyPoolEgressObservationDescription": "ڈیش بورڈ میں کسی پراکسی پول کے تحت دکھائیں کہ گزشتہ 24 گھنٹوں کے دوران کتنے مشاہدہ شدہ ایگریس IPs نے اس کے اراکین کو سروس فراہم کی، کتنے کنکشنز نے انہیں استعمال کیا، اور ایک IP کے پیچھے زیادہ سے زیادہ کتنے دیکھے گئے۔ صرف پڑھنے کے لیے، پراکسی لاگ سے شمار شدہ، اور روٹنگ کے لیے کبھی استعمال نہیں ہوتا۔ بطور ڈیفالٹ بند: پول ایڈیٹر میں کوئی تبدیلی نہیں ہوتی اور مشاہدے کا روٹ null واپس کرتا ہے۔", "featureFlagProxyHealthBlockedResetsStreakDescription": "پراکسی ہیلتھ سویپ میں، ہدف کی جانب سے مسترد کردہ پروب (401/403/429: پراکسی نے اسے آگے پہنچایا، مگر منزل نے اس ایگریس IP کو مسترد کر دیا) کو پراکسی کی مسلسل ناکامیوں کا سلسلہ اسی طرح ری سیٹ کرنے دیں جیسے کامیابی سے پیش کیا گیا پروب کرتا ہے۔ بطور ڈیفالٹ بند: مسترد ہونا غیر جانب دار رہتا ہے اور سلسلے کو برقرار رکھتا ہے۔ 5xx دونوں صورتوں میں غیر فیصلہ کن رہتا ہے، اور مسترد ہونا کبھی بھی کسی پراکسی کو ہٹاتا، غیر فعال کرتا یا دوبارہ فعال نہیں کرتا۔", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "ہوم", "dashboard": "ڈیش بورڈ", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register پر $200 کے مفت کریڈٹس حاصل کریں — کسی کریڈٹ کارڈ کی ضرورت نہیں ہے۔", "unorouter": "https://unorouter.ai پر ایک API کلید بنائیں، پھر اسے یہاں Bearer ٹوکن کے طور پر پیسٹ کریں۔", "agnes": "agnes-ai.com پر API کلید حاصل کریں", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "مفت ٹیر معطل ہے (2026) — AI/ML API اب صرف پے-ایز-یو-گو (کم از کم $20 ٹاپ اپ) ہے؛ کوئی بار بار ملنے والے مفت کریڈٹس نہیں ہیں۔", "ai21": "سائن اپ پر $10 کے ٹرائل کریڈٹس (3 ماہ کے لیے کارآمد)، کسی کریڈٹ کارڈ کی ضرورت نہیں ہے", "alibaba": "علی بابا کو ایک API کلید کے ساتھ منسلک کریں۔", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP آڈٹ (دن)", "retentionA2aEvents": "A2A واقعات (دن)", "retentionCallLogs": "کال لاگز (دن)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "استعمال کی تاریخ (دن)", "retentionMemoryEntries": "یادداشت کے اندراجات (دن)", "retentionXpAuditLog": "XP آڈٹ لاگ (دن)", diff --git a/src/i18n/messages/uz.json b/src/i18n/messages/uz.json index af82ed364a..6ed62c769f 100644 --- a/src/i18n/messages/uz.json +++ b/src/i18n/messages/uz.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proksi-pullar va opencode’ning har bir hisob bo‘yicha aylantirish mexanizmi hozirgina ishlamay qolgan proksini (TCP tekshiruvini rad etgan yoki u orqali 429 javobi olingan) har bir jarayon uchun takroriy urinish sayin ikki baravar oshib, belgilangan chegaragacha yetadigan muddat davomida qayta taqdim etmaydi. Proksi holati yozib qo‘yilmaydi; barcha nomzodlar chetga surilsa, tanlov o‘zgarmaydi. Standart holatda o‘chiq: tanlash tartibi oddiy aylantirish bilan aynan bir xil.", "featureFlagProxyPoolEgressObservationDescription": "Boshqaruv panelida proksi-pul ostida so‘nggi 24 soat ichida uning a’zolariga nechta kuzatilgan chiquvchi IP xizmat ko‘rsatgani, ulardan nechta ulanish foydalangani va bitta IP ortida kuzatilgan eng katta sonni ko‘rsating. Faqat o‘qish uchun mo‘ljallangan, proksi jurnalidan hisoblanadi va yo‘naltirish uchun hech qachon ishlatilmaydi. Standart holatda o‘chiq: pul muharriri o‘zgarmaydi va kuzatuv marshruti null javobini qaytaradi.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Proksi holatini tekshirish jarayonida nishon rad etgan tekshiruv (401/403/429: proksi uzatdi, manzil esa ushbu chiquvchi IP’ni rad etdi) xizmat ko‘rsatilgan tekshiruv kabi proksining ketma-ket muvaffaqiyatsizliklar hisoblagichini tiklashiga ruxsat bering. Standart holatda o‘chiq: rad etish neytral bo‘lib qoladi va hisoblagichni saqlaydi. 5xx har ikki holatda ham noaniq bo‘lib qoladi, rad etish esa proksini hech qachon olib tashlamaydi, o‘chirib qo‘ymaydi yoki qayta faollashtirmaydi.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Bosh sahifa", "dashboard": "Boshqaruv paneli", @@ -6125,6 +6126,7 @@ "agentrouter": "https://agentrouter.org/register manzilida $200 bepul kredit oling — kredit karta talab qilinmaydi.", "unorouter": "https://unorouter.ai saytida API kalitini yarating, soʻng uni Bearer tokeni sifatida shu yerga kiriting.", "agnes": "API kalitini agnes-ai.com saytida oling", + "agnes-cn": "__MISSING__:Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "Bepul tarif toʻxtatilgan (2026) — AI/ML API endi faqat foydalanish hajmiga qarab toʻlanadi (kamida $20 hisob toʻldirish); muntazam bepul kreditlar yoʻq.", "ai21": "Roʻyxatdan oʻtganda $10 sinov krediti (3 oy amal qiladi), kredit karta talab qilinmaydi", "alibaba": "Alibaba xizmatiga API kaliti bilan ulaning.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP auditi (kun)", "retentionA2aEvents": "A2A hodisalari (kun)", "retentionCallLogs": "Qo‘ng‘iroqlar jurnallari (kun)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Foydalanish tarixi (kun)", "retentionMemoryEntries": "Xotira yozuvlari (kun)", "retentionXpAuditLog": "XP audit jurnali (kun)", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index f1673cbc48..d286122dde 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Các pool proxy và cơ chế xoay vòng theo tài khoản của opencode ngừng dùng lại một proxy vừa thất bại (thăm dò TCP bị từ chối, hoặc nhận 429 qua proxy đó) trong một khoảng thời gian theo tiến trình, tăng gấp đôi sau mỗi lần lặp lại, tối đa đến một giới hạn. Không ghi trạng thái proxy nào; khi mọi ứng viên đều bị tạm gác, lựa chọn vẫn như cũ. Tắt theo mặc định: thứ tự chọn giữ nguyên xoay vòng thông thường.", "featureFlagProxyPoolEgressObservationDescription": "Hiển thị, bên dưới một nhóm proxy trong bảng điều khiển, số IP đầu ra quan sát được đã phục vụ các thành viên của nhóm trong 24 giờ qua, số kết nối đã dùng chúng và số lớn nhất thấy sau cùng một IP. Chỉ đọc, tính từ nhật ký proxy, không bao giờ dùng để định tuyến. Tắt theo mặc định: trình chỉnh sửa nhóm không đổi và tuyến quan sát trả về null.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Trong lượt kiểm tra sức khỏe proxy, cho phép một lần thăm dò bị đích từ chối (401/403/429: proxy đã chuyển tiếp, đích từ chối IP đầu ra này) đặt lại chuỗi lỗi liên tiếp của proxy, giống như một lần thăm dò được phục vụ. Tắt theo mặc định: lần từ chối vẫn trung lập và giữ nguyên chuỗi. Lỗi 5xx vẫn không kết luận trong mọi trường hợp, và lần từ chối không bao giờ xóa, vô hiệu hóa hay kích hoạt lại proxy.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "Buộc kết nối xai-oauth lấy danh mục mô hình xAI trực tiếp từ https://api.x.ai/v1/models bằng token OAuth bearer, thay vì danh sách tĩnh đã đóng băng. Mặc định tắt: xai-oauth vẫn dùng danh sách tĩnh không đổi. Nếu có lỗi phân giải, hệ thống quay lại danh sách tĩnh.", "sidebar": { "home": "Trang chủ", "dashboard": "Trang tổng quan", @@ -6125,6 +6126,7 @@ "agentrouter": "Nhận 200 USD tín dụng miễn phí tại https://agentrouter.org/register — không cần thẻ tín dụng.", "unorouter": "Tạo khóa API tại https://unorouter.ai, sau đó dán vào đây dưới dạng Bearer token.", "agnes": "Lấy khóa API tại agnes-ai.com", + "agnes-cn": "Kết nối Agnes AI (Trung Quốc) bằng khóa API được cấp cho api.agnes-ai.cn. Khóa từ apihub.agnes-ai.com sẽ không hoạt động.", "aimlapi": "Gói miễn phí đã tạm dừng (2026) — AI/ML API hiện chỉ tính phí theo mức sử dụng (nạp tối thiểu 20 USD); không còn tín dụng miễn phí định kỳ.", "ai21": "10 USD tín dụng dùng thử khi đăng ký (có hiệu lực 3 tháng), không cần thẻ tín dụng", "alibaba": "Kết nối Alibaba bằng khóa API.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Kiểm toán MCP (ngày)", "retentionA2aEvents": "Sự kiện A2A (ngày)", "retentionCallLogs": "Nhật ký cuộc gọi (ngày)", + "retentionConversationTurnNodes": "Nút lượt hội thoại (ngày)", "retentionUsageHistory": "Lịch sử sử dụng (ngày)", "retentionMemoryEntries": "Mục nhập bộ nhớ (ngày)", "retentionXpAuditLog": "Nhật ký kiểm toán XP (ngày)", diff --git a/src/i18n/messages/yo.json b/src/i18n/messages/yo.json index 4f404716f5..36278321d2 100644 --- a/src/i18n/messages/yo.json +++ b/src/i18n/messages/yo.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Àwọn àkójọpọ̀ proxy àti yíyí opencode fún àkọọ́lẹ̀ kọ̀ọ̀kan máa ń dáwọ́ lílo proxy kan tí ó ṣẹ̀ṣẹ̀ kùnà dúró (ìdánwò TCP tí a kọ̀, tàbí 429 tí a gbà nípasẹ̀ rẹ̀) fún àkókò kan nínú process kọ̀ọ̀kan, èyí tí yóò di ìlọ́po méjì ní gbogbo ìgbà tí ó tún ṣẹlẹ̀, títí dé òpin tí a yàn. A kì í kọ ipò proxy sílẹ̀; bí a bá fi gbogbo olùdíje sí ẹ̀gbẹ́ kan, yíyan náà kì í yí padà. Ó wà ní pípa ní àkọ́kọ́: bí a ṣe ń yan tẹ̀ lé yíyí lasán náà gan-an.", "featureFlagProxyPoolEgressObservationDescription": "Ṣàfihàn, lábẹ́ àkójọpọ̀ proxy kan nínú dashboard, iye àwọn IP egress tí a ṣàkíyèsí tí ó ṣiṣẹ́ fún àwọn ọmọ ẹgbẹ́ rẹ̀ láàárín wákàtí 24 tó kọjá, iye àwọn ìsopọ̀ tí ó lò wọ́n àti iye tó pọ̀ jù tí a rí lẹ́yìn IP kan. Fún kíkà nìkan, a ṣírò rẹ̀ láti inú àkọsílẹ̀ proxy, a kò sì lò ó fún routing láé. Ó wà ní pípa ní àkọ́kọ́: olóòtú àkójọpọ̀ náà kò yí padà, ipa-ọ̀nà àkíyèsí sì ń dá null padà.", "featureFlagProxyHealthBlockedResetsStreakDescription": "Nínú àyẹ̀wò ìlera proxy, jẹ́ kí ìdánwò kan tí ibi àfojúsùn kọ̀ (401/403/429: proxy náà gbé e dé, ibi àfojúsùn sì kọ IP egress yìí) tún iye àwọn ìkùnà tẹ̀léra ti proxy náà bẹ̀rẹ̀, bí ìdánwò tí a ṣiṣẹ́ fún. Ó wà ní pípa ní àkọ́kọ́: ìkọ̀sílẹ̀ kan ṣì jẹ́ aláìdásí, ó sì ń pa iye tẹ̀léra náà mọ́. 5xx ṣì jẹ́ aláìdánilójú lọ́nà méjèèjì, ìkọ̀sílẹ̀ kò sì lè yọ proxy kan kúrò, mú un má ṣiṣẹ́, tàbí tún mú un ṣiṣẹ́ láé.", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "Ilé", "dashboard": "Pátákó Ìṣàkóso", @@ -6125,6 +6126,7 @@ "agentrouter": "Gba kírẹ́díìtì ọ̀fẹ́ $200 ní https://agentrouter.org/register — kò nílò káàdì kírẹ́díìtì.", "unorouter": "Ṣẹ̀dá API key kan ní https://unorouter.ai, lẹ́yìn náà lẹ̀ ẹ́ síbí gẹ́gẹ́ bí Bearer token.", "agnes": "Gba API key ní agnes-ai.com", + "agnes-cn": "__MISSING__:Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "A ti dá ìpele ọ̀fẹ́ dúró (2026) — AI/ML API ti di èyí tí a ń sanwó gẹ́gẹ́ bí lílò nìkan (àfikún owó tó kéré jù ni $20); kò sí kírẹ́díìtì ọ̀fẹ́ tó ń ṣẹlẹ̀ léraléra.", "ai21": "Kírẹ́díìtì àdánwò $10 nígbà ìforúkọsílẹ̀ (ó wúlò fún oṣù mẹ́ta), kò nílò káàdì kírẹ́díìtì", "alibaba": "So Alibaba pọ̀ pẹ̀lú API key.", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "Àyẹ̀wò MCP (ọjọ́)", "retentionA2aEvents": "Àwọn Ìṣẹ̀lẹ̀ A2A (ọjọ́)", "retentionCallLogs": "Àwọn Àkọsílẹ̀ Ìpè (ọjọ́)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "Ìtàn Ìlò (ọjọ́)", "retentionMemoryEntries": "Àwọn Àkọsílẹ̀ Ìrántí (ọjọ́)", "retentionXpAuditLog": "Àkọsílẹ̀ Àyẹ̀wò XP (ọjọ́)", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index d1b93cb084..170a0b27fd 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "代理池和 opencode 的按账户轮换会在每个进程的一段时间内,停止再次提供刚刚失败的代理(TCP 探测被拒绝,或通过该代理收到 429)。每次重复失败时,该时段都会翻倍,直至达到上限。不会写入代理状态;当所有候选代理都被暂时搁置时,选择保持不变。默认关闭:选择顺序与普通轮换完全相同。", "featureFlagProxyPoolEgressObservationDescription": "在仪表板的代理池下显示:过去 24 小时内观察到多少个出口 IP 为其成员提供服务、使用这些 IP 的连接数,以及单个 IP 背后观察到的最大数量。此信息为只读,根据代理日志计算,绝不用于路由。默认关闭:代理池编辑器保持不变,观测路由返回 null。", "featureFlagProxyHealthBlockedResetsStreakDescription": "在代理健康检查中,如果探测被目标拒绝(401/403/429:代理已转发,但目标拒绝了此出口 IP),则像探测成功一样重置代理的连续失败计数。默认关闭:拒绝保持中性,并保留当前连续失败计数。无论如何,5xx 均保持结果不确定,而且拒绝绝不会导致代理被移除、禁用或重新激活。", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "首页", "dashboard": "仪表板", @@ -6125,6 +6126,7 @@ "agentrouter": "在 https://agentrouter.org/register 获取 $200 免费额度 — 无需信用卡。", "unorouter": "在 https://unorouter.ai 创建一个 API 密钥,然后将其作为 Bearer 令牌粘贴到这里。", "agnes": "在 agnes-ai.com 获取 API 密钥", + "agnes-cn": "使用中国站(api.agnes-ai.cn)签发的 API 密钥连接 Agnes。国际站(apihub.agnes-ai.com)的密钥不能用。", "aimlapi": "免费层已暂停 (2026) — AI/ML API 现在仅支持按需付费(最低充值 $20);无循环免费额度。", "ai21": "注册即送 $10 体验额度(有效期 3 个月),无需信用卡", "alibaba": "使用 API 密钥连接阿里巴巴。", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP 审核(天)", "retentionA2aEvents": "A2A 活动(天)", "retentionCallLogs": "通话记录(天)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "使用历史(天)", "retentionMemoryEntries": "内存条目(天)", "retentionXpAuditLog": "XP 审计日志(天数)", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 4ed7fbecce..b638314889 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -996,6 +996,7 @@ "featureFlagProxySkipRecentlyFailedDescription": "Proxy 集區以及 opencode 的每帳戶輪替機制,會在每個處理程序的一段期間內,停止再次使用剛失敗的 Proxy(TCP 探測遭拒,或透過該 Proxy 收到 429)。每次重複失敗時,這段期間都會加倍,直到達到上限。不會寫入任何 Proxy 狀態;即使所有候選項目都被暫時擱置,選擇結果仍維持不變。預設關閉:選擇順序與一般輪替完全相同。", "featureFlagProxyPoolEgressObservationDescription": "在儀表板的 Proxy 集區下方,顯示過去 24 小時內觀察到多少個出口 IP 為其成員提供服務、使用這些 IP 的連線數,以及單一 IP 後方觀察到的最高數量。唯讀,根據 Proxy 記錄計算,絕不會用於路由。預設關閉:集區編輯器維持不變,觀察路由會回傳 null。", "featureFlagProxyHealthBlockedResetsStreakDescription": "在 Proxy 健康狀態掃描中,讓遭目標拒絕的探測(401/403/429:Proxy 已轉送,但目的地拒絕此出口 IP)能像已提供服務的探測一樣,重設 Proxy 的連續失敗次數。預設關閉:拒絕仍視為中性,並保留目前的連續失敗次數。無論此設定為何,5xx 仍視為無法判定,而且拒絕絕不會移除、停用或重新啟用 Proxy。", + "featureFlagXaiOauthLiveModelDiscoveryDescription": "__MISSING__:Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed.", "sidebar": { "home": "首頁", "dashboard": "儀表板", @@ -6125,6 +6126,7 @@ "agentrouter": "在 https://agentrouter.org/register 取得 $200 美元免費額度 — 無需信用卡。", "unorouter": "在 https://unorouter.ai 創建一個 API 金鑰,然後將其作為 Bearer 令牌粘貼在這裡。", "agnes": "在 agnes-ai.com 取得 API 金鑰", + "agnes-cn": "Connect Agnes AI (China) with an API key issued for api.agnes-ai.cn. Keys from apihub.agnes-ai.com will not work.", "aimlapi": "免費方案已暫停(2026 年)— AI/ML API 現僅提供隨用隨付方案(最低儲值 $20 美元);無定期免費額度。", "ai21": "註冊即贈 $10 美元試用額度(有效期 3 個月),無需信用卡", "alibaba": "使用 API 金鑰連線阿里雲。", @@ -7764,6 +7766,7 @@ "retentionMcpAudit": "MCP 稽核(天)", "retentionA2aEvents": "A2A 活動(天)", "retentionCallLogs": "通話記錄(天)", + "retentionConversationTurnNodes": "Conversation turn nodes (days)", "retentionUsageHistory": "使用歷史(天)", "retentionMemoryEntries": "記憶體條目(天)", "retentionXpAuditLog": "XP 稽核記錄(天數)", diff --git a/src/lib/auth/managementPassword.ts b/src/lib/auth/managementPassword.ts index 2b18c011ae..dfc8549d12 100644 --- a/src/lib/auth/managementPassword.ts +++ b/src/lib/auth/managementPassword.ts @@ -45,6 +45,14 @@ export function isBcryptHash(value: unknown): value is string { return typeof value === "string" && BCRYPT_HASH_PATTERN.test(value); } +// #13679 (PR D, item #5): the well-known INITIAL_PASSWORD placeholder shipped in +// .env.example / contrib/podman/omniroute.container / docker deploy manifests is a +// public, guessable credential. Callers on the authentication path use this to refuse +// a successful match from non-loopback requests instead of only warning on boot. +export function isKnownInsecureManagementPassword(password: string): boolean { + return INSECURE_DEFAULT_PASSWORDS.has(password); +} + export async function hashManagementPassword(password: string) { return bcrypt.hash(password, MANAGEMENT_PASSWORD_SALT_ROUNDS); } diff --git a/src/lib/cloudSync.ts b/src/lib/cloudSync.ts index 48084d0025..8f0867bd93 100644 --- a/src/lib/cloudSync.ts +++ b/src/lib/cloudSync.ts @@ -13,6 +13,14 @@ const CLOUD_SYNC_SECRET = process.env.OMNIROUTE_CLOUD_SYNC_SECRET || ""; // hostile CLOUD_URL cannot silently swap user OAuth tokens. const CLOUD_SYNC_SECRETS_ENABLED = process.env.OMNIROUTE_CLOUD_SYNC_SECRETS === "true"; +// #13679 PR A — opt-in early enforcement of the "no secret configured" branch +// below. Bringing the v3.9 enforce-by-default switch forward as an explicit +// opt-out-safe flag: default OFF preserves v3.8.x back-compat for peers that +// haven't rotated in a shared secret yet (an unsigned payload still passes). +// Set to "true" to reject even an unsigned payload when no local secret is +// configured — the default flips to enforced in v3.9. +const CLOUD_SYNC_ENFORCE_SIGNATURE = process.env.OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE === "true"; + type JsonRecord = Record; function asRecord(value: unknown): JsonRecord { @@ -40,18 +48,38 @@ function toDateMs(value: unknown): number { // 2. We verify the signature with `crypto.timingSafeEqual` before parsing the // JSON, so a MITM on the CLOUD_URL channel — or a misconfigured CLOUD_URL // pointing at an attacker — cannot inject providers/tokens. -// If `OMNIROUTE_CLOUD_SYNC_SECRET` is unset, signature validation is logged but -// not enforced (back-compat for users on v3.8.x who haven't issued a shared -// secret yet). The enforce-by-default switch will flip in v3.9. +// If `OMNIROUTE_CLOUD_SYNC_SECRET` is unset, a PRESENT signature is always +// rejected (#13679 PR A — we have no key to check it against, so a signature +// we cannot verify is treated as invalid rather than blindly trusted) and an +// ABSENT signature falls through in legacy unverified mode by default +// (back-compat for users on v3.8.x who haven't issued a shared secret yet; +// opt in early via `OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE=true`). The +// enforce-by-default switch for the absent-signature case will flip in v3.9. export function verifyCloudSignature(rawBody: string, sigHeader: string | null): boolean { if (!CLOUD_SYNC_SECRET) { if (sigHeader) { - // We can't verify, but the server is at least trying. Pass through. - return true; + // We have no secret to verify against, so a signature we can't check is + // treated as invalid rather than passed through (#13679 PR A item (b) — + // closes the "forge any X-Cloud-Sig and it's accepted" fail-open case). + console.warn( + "[cloudSync] OMNIROUTE_CLOUD_SYNC_SECRET is not set but the Cloud response carries an " + + "X-Cloud-Sig header — rejecting an unverifiable signature. Set the secret to enable " + + "verification." + ); + return false; + } + if (CLOUD_SYNC_ENFORCE_SIGNATURE) { + console.warn( + "[cloudSync] OMNIROUTE_CLOUD_SYNC_SECRET is not set and the Cloud response carries no " + + "X-Cloud-Sig, and OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE=true — rejecting unsigned payload." + ); + return false; } console.warn( "[cloudSync] OMNIROUTE_CLOUD_SYNC_SECRET is not set and the Cloud response carries no X-Cloud-Sig. " + - "Token sync runs in legacy unverified mode — set the secret to enforce HMAC verification." + "Token sync runs in legacy unverified mode — set the secret (or " + + "OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE=true) to enforce HMAC verification. This legacy " + + "pass-through default will flip to enforced in v3.9." ); return true; } @@ -207,4 +235,9 @@ async function updateLocalTokens(cloudProviders: unknown) { } } -export { CLOUD_URL, CLOUD_SYNC_TIMEOUT_MS, CLOUD_SYNC_SECRETS_ENABLED }; +export { + CLOUD_URL, + CLOUD_SYNC_TIMEOUT_MS, + CLOUD_SYNC_SECRETS_ENABLED, + CLOUD_SYNC_ENFORCE_SIGNATURE, +}; diff --git a/src/lib/db/batches.ts b/src/lib/db/batches.ts index 5282313ac2..4f98e58006 100644 --- a/src/lib/db/batches.ts +++ b/src/lib/db/batches.ts @@ -387,22 +387,25 @@ export function deleteBatch(id: string): boolean { db.prepare("DELETE FROM batch_item_checkpoints WHERE batch_id = ?").run(id); - // Soft-delete associated files (input, output, error) - if (batch.inputFileId) { + // Soft-delete associated files (input, output, error) — but only when no + // OTHER batch still references the same file id (#13681). A file shared + // across batches (e.g. one input file reused for several batch submissions) + // must survive as long as any sibling batch still points at it. + if (batch.inputFileId && !isFileReferencedByOtherBatch(batch.inputFileId, [id])) { try { deleteFile(batch.inputFileId); } catch { /* ignore */ } } - if (batch.outputFileId) { + if (batch.outputFileId && !isFileReferencedByOtherBatch(batch.outputFileId, [id])) { try { deleteFile(batch.outputFileId); } catch { /* ignore */ } } - if (batch.errorFileId) { + if (batch.errorFileId && !isFileReferencedByOtherBatch(batch.errorFileId, [id])) { try { deleteFile(batch.errorFileId); } catch { @@ -424,6 +427,47 @@ export type DeleteCompletedBatchesScope = { apiKeyId: string } | { allTenants: t /** Both sweep modes commit in chunks of this many batches (SEC-D, LEDGER-4). */ export const INSTANCE_SWEEP_CHUNK = 200; +/** + * Upper bound on the number of `INSTANCE_SWEEP_CHUNK`-sized chunks a single + * `deleteCompletedBatches` call may run (#13680). `sweepLoop` is a synchronous + * `for (;;)` over `better-sqlite3` — with no cap, one request could hold the + * Node.js event loop for as long as it takes to sweep every completed batch on + * the instance. 25 × 200 = 5000 batches/request is a judgment call, not a hard + * constraint; any caller with more to sweep gets `hasMore: true` back and + * resumes by calling again — resumption falls out naturally from rowid + * ordering plus delete-as-you-go (already-swept rows are gone, so the next + * SELECT picks up the next-lowest surviving rowid on its own; no cursor field + * needed). + */ +export const MAX_CHUNKS_PER_REQUEST = 25; + +/** + * True when some batch OTHER than one of `excludeBatchIds` still references + * `fileId` as its input/output/error file (#13681). Used before soft-deleting + * a file to avoid nulling content a surviving batch still needs. Binds + * `fileId` three times; when `excludeBatchIds` is empty the `NOT IN (...)` + * clause is dropped entirely rather than emitted empty (`NOT IN ()` is invalid + * SQL, and getting the guard wrong there would silently match everything). + */ +export function isFileReferencedByOtherBatch(fileId: string, excludeBatchIds: string[]): boolean { + const db = getDbInstance(); + if (excludeBatchIds.length === 0) { + const row = db + .prepare( + "SELECT 1 FROM batches WHERE input_file_id = ? OR output_file_id = ? OR error_file_id = ? LIMIT 1" + ) + .get(fileId, fileId, fileId); + return !!row; + } + const marks = excludeBatchIds.map(() => "?").join(","); + const row = db + .prepare( + `SELECT 1 FROM batches WHERE (input_file_id = ? OR output_file_id = ? OR error_file_id = ?) AND id NOT IN (${marks}) LIMIT 1` + ) + .get(fileId, fileId, fileId, ...excludeBatchIds); + return !!row; +} + /** * Delete completed batches and the files they reference. * @@ -457,10 +501,25 @@ export const INSTANCE_SWEEP_CHUNK = 200; * so the lowest-privilege caller — any valid API key — cannot hold the * instance's single writer for the length of its whole sweep. Each chunk stays * atomic: a failure inside chunk N leaves chunks < N committed, chunk N fully - * rolled back, and rethrows. Inherent to per-chunk commits, in either mode: a - * file shared by batches in two different chunks can be nulled by chunk 1 - * before chunk 2 fails; the surviving batch row is swept by the next run. The - * returned totals sum the chunks. + * rolled back, and rethrows. Each chunk's file soft-deletes now check whether + * some batch OUTSIDE that chunk still references the file + * (`isFileReferencedByOtherBatch`, #13681) — a file shared with a + * non-completed sibling batch, or with a completed batch a LATER chunk hasn't + * reached yet, survives this chunk. The only case that check cannot see is + * pure timing: chunk 1 commits and nulls a file, then — before chunk 2 runs — + * a NEW batch is created reusing that same file id. That race is inherent to + * per-chunk commits and stays a known, accepted edge case; everything else + * (a concurrently existing sibling, in any status, in any chunk) is now + * guarded. The returned totals sum the chunks. + * + * A single call commits at most `MAX_CHUNKS_PER_REQUEST` chunks (#13680): + * `better-sqlite3` is synchronous, so an unbounded loop would hold the event + * loop for as long as it takes to sweep the whole key's/instance's backlog. + * When the cap is hit with more rows still pending, the call returns + * `hasMore: true` instead of continuing; the caller (the DELETE route) simply + * calls again. Resumption needs no cursor: swept rows are gone, `rowid` only + * increases, so the next call's `ORDER BY rowid LIMIT ?` picks up exactly + * where the previous call left off. * * The loop must make progress: it remembers the first id of the previous chunk * and throws if the next chunk starts with the same id — the DELETE removed @@ -476,6 +535,7 @@ export const INSTANCE_SWEEP_CHUNK = 200; export function deleteCompletedBatches(scope: DeleteCompletedBatchesScope): { deletedBatches: number; deletedFiles: number; + hasMore: boolean; } { const scopeObj = scope && typeof scope === "object" ? scope : {}; const allTenants = "allTenants" in scopeObj && scopeObj.allTenants === true; @@ -517,6 +577,11 @@ export function deleteCompletedBatches(scope: DeleteCompletedBatchesScope): { let deletedFiles = 0; for (const fid of fileIds) { + // #13681: a file referenced by a batch outside this chunk (a different + // chunk not yet processed, or any non-completed batch — completed + // batches outside `ids` cannot exist since the SELECT above IS the + // chunk) must survive this chunk's sweep. + if (isFileReferencedByOtherBatch(fid, ids)) continue; try { // Key mode: only the key's OWN files. A batch may reference a file // another tenant (or nobody) owns; a bulk destructive sweep must not @@ -541,11 +606,20 @@ export function deleteCompletedBatches(scope: DeleteCompletedBatchesScope): { // modes is the SELECT that produces the next chunk. No outer transaction — // the write lock is released between chunks (LEDGER-4/20/21). const sweepLoop = (nextIds: () => string[]) => { - const totals = { deletedBatches: 0, deletedFiles: 0 }; + const totals = { deletedBatches: 0, deletedFiles: 0, hasMore: false }; let previousFirstId: string | null = null; + let chunkCount = 0; for (;;) { const ids = nextIds(); if (ids.length === 0) break; + // Chunk cap (#13680): a single request commits at most + // MAX_CHUNKS_PER_REQUEST chunks. This peek doesn't delete or count + // anything — it only tells the caller whether more work remains so it + // can call again (resumption is natural: already-swept rows are gone). + if (chunkCount >= MAX_CHUNKS_PER_REQUEST) { + totals.hasMore = true; + break; + } // Forward-progress guard (LEDGER-22): the chunk is re-selected from the // table after each commit, so a repeated first id means the previous // DELETE removed nothing and the loop would spin forever. A concurrent @@ -557,6 +631,7 @@ export function deleteCompletedBatches(scope: DeleteCompletedBatchesScope): { const part = sweepIds(ids); totals.deletedBatches += part.deletedBatches; totals.deletedFiles += part.deletedFiles; + chunkCount++; } return totals; }; diff --git a/src/lib/db/cleanup.ts b/src/lib/db/cleanup.ts index 2edec55234..1b8d5c277e 100644 --- a/src/lib/db/cleanup.ts +++ b/src/lib/db/cleanup.ts @@ -283,6 +283,19 @@ export async function cleanupMemoryEntries(): Promise { } } + // optimize only merges segments; it does not drop tombstones from + // access-count UPDATEs that already reindexed. rebuild from the + // content table on every pass so a bloated index cannot wait for + // a memory-row delete that may never happen. + if (tableExists("memory_fts")) { + try { + db.exec("INSERT INTO memory_fts(memory_fts) VALUES('rebuild')"); + } catch (err: unknown) { + console.error("[Cleanup] FTS5 rebuild after memory retention failed:", err); + result.errors++; + } + } + console.log( `[Cleanup] Deleted ${result.deleted} memory_entries older than ${retentionDays} days` ); @@ -443,28 +456,30 @@ export async function cleanupCcrBlocks(): Promise { } /** - * Clean up conversation_turn_nodes older than the call-log retention window (#12453). + * Clean up conversation_turn_nodes older than their own retention window (#12453). * * The nodes are identity-only: the transcript view resolves each turn's display * content from the call_logs row `last_correlation_id` points at. Once - * cleanupCallLogs purges that row the node can never render again, so the two - * tables share the dashboard database setting `retention.callLogs` instead of - * a knob of their own; `CALL_LOG_RETENTION_DAYS` configures the separate - * compliance cleanup path and does not override this window. Deleting an old - * node only affects reconnect anchors: a conversation resumed after the window - * mints a new id, which is already the documented anchor-miss behavior of - * resolveConversationId. `last_seen_at` has no index (migration 156), so - * each DELETE is a table scan. Bounded batches yield between writes so an - * existing large table cannot park the event loop for the whole cleanup pass. + * cleanupCallLogs purges that row the node can never render again, so this + * window should not outlive `retention.callLogs` in practice — but the two + * settings are independent knobs (`retention.conversationTurnNodes`, default + * 30, matching callLogs' default so upgrading changes nothing until an + * operator overrides one of them). `CALL_LOG_RETENTION_DAYS` configures the + * separate compliance cleanup path and does not override this window. + * Deleting an old node only affects reconnect anchors: a conversation resumed + * after the window mints a new id, which is already the documented + * anchor-miss behavior of resolveConversationId. `last_seen_at` has no index + * (migration 156), so each DELETE is a table scan. Bounded batches yield + * between writes so an existing large table cannot park the event loop for + * the whole cleanup pass. */ export async function cleanupConversationTurnNodes(): Promise { const retention = getRetentionSettings(); - const retentionDays = retention.callLogs; + const retentionDays = retention.conversationTurnNodes; const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - retentionDays); const cutoffISO = cutoffDate.toISOString(); - const result: CleanupResult = { deleted: 0, errors: 0 }; try { @@ -472,7 +487,6 @@ export async function cleanupConversationTurnNodes(): Promise { { table: "conversation_turn_nodes", column: "last_seen_at", cutoff: "iso" }, cutoffISO ); - console.log( `[Cleanup] Deleted ${result.deleted} conversation_turn_nodes older than ${retentionDays} days` ); @@ -496,19 +510,16 @@ export async function cleanupConversationTurnNodes(): Promise { export async function cleanupAgenticConversations(): Promise { const db = getDbInstance(); const retention = getRetentionSettings(); - - const retentionDays = retention.callLogs; + const retentionDays = retention.conversationTurnNodes; const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - retentionDays); const cutoffISO = cutoffDate.toISOString(); - const result: CleanupResult = { deleted: 0, errors: 0 }; try { if (!tableExists("agentic_conversations") || !tableExists("conversation_turn_nodes")) { return result; } - const stmt = db.prepare( `DELETE FROM agentic_conversations WHERE rowid IN ( @@ -527,7 +538,6 @@ export async function cleanupAgenticConversations(): Promise { if (batch < 10_000) break; await new Promise((resolve) => setImmediate(resolve)); } - console.log( `[Cleanup] Deleted ${result.deleted} orphaned agentic_conversations older than ${retentionDays} days` ); diff --git a/src/lib/db/databaseSettings.ts b/src/lib/db/databaseSettings.ts index 0a12729242..8858cb9fd5 100644 --- a/src/lib/db/databaseSettings.ts +++ b/src/lib/db/databaseSettings.ts @@ -49,6 +49,7 @@ const LEGACY_FLAT_KEYS: { configAudit: ["configAudit"], a2aEvents: ["a2aEvents"], callLogs: ["callLogs"], + conversationTurnNodes: ["conversationTurnNodes"], usageHistory: ["usageHistory"], memoryEntries: ["memoryEntries"], domainCostHistory: ["domainCostHistory"], @@ -260,6 +261,8 @@ export function getDatabaseSettings(): DatabaseSettings { vacuumState.lastRunAt !== null ? new Date(vacuumState.lastRunAt).toISOString() : null, lastOptimizationAt: null, integrityCheck: getIntegrityCheck(), + autoVacuumDrift: vacuumState.autoVacuumDrift, + lastReclaimedPages: vacuumState.lastReclaimedPages, }, }; } diff --git a/src/lib/db/migrationRunner/constants.ts b/src/lib/db/migrationRunner/constants.ts index 089837f93f..73d1ea3da4 100644 --- a/src/lib/db/migrationRunner/constants.ts +++ b/src/lib/db/migrationRunner/constants.ts @@ -211,6 +211,12 @@ export const RENAMED_MIGRATION_COMPATIBILITY = [ toVersion: "101", toName: "api_key_usage_limits", }, + { + fromVersion: "176", + fromName: "memory_fts_skip_access_updates", + toVersion: "180", + toName: "memory_fts_au_conditional_memory_id", + }, ] as const; export const LEGACY_VERSION_SLOT_MIGRATIONS = [ @@ -257,4 +263,4 @@ export const PHYSICAL_SCHEMA_SENTINELS = [ ] as const; export const INITIAL_SCHEMA_SENTINELS = ["provider_connections", "combos", "call_logs"] as const; -export const OPTIONAL_FTS5_MIGRATION_VERSIONS = new Set(["022", "023"]); +export const OPTIONAL_FTS5_MIGRATION_VERSIONS = new Set(["022", "023", "180"]); diff --git a/src/lib/db/migrations/180_memory_fts_au_conditional_memory_id.sql b/src/lib/db/migrations/180_memory_fts_au_conditional_memory_id.sql new file mode 100644 index 0000000000..25db52b8bb --- /dev/null +++ b/src/lib/db/migrations/180_memory_fts_au_conditional_memory_id.sql @@ -0,0 +1,23 @@ +-- 180_memory_fts_au_conditional_memory_id.sql +-- recordMemoryAccess() updates access_count / last_accessed_at on every +-- retrieval. The AFTER UPDATE trigger from 023 rewrote the FTS5 row for +-- those telemetry columns too, so memory_fts_data / memory_fts_docsize +-- grew without bound (live: 962 memories -> 175k FTS data rows). +-- +-- Recreate memory_fts_au so it only reindexes when content, key, or +-- memory_id actually change. memory_id still belongs here: createMemory +-- inserts then backfills memory_id, and that UPDATE must stay in FTS. + +DROP TRIGGER IF EXISTS memory_fts_au; + +CREATE TRIGGER IF NOT EXISTS memory_fts_au AFTER UPDATE ON memories +WHEN + NEW.content IS NOT OLD.content OR + NEW.key IS NOT OLD.key OR + NEW.memory_id IS NOT OLD.memory_id +BEGIN + INSERT INTO memory_fts(memory_fts, rowid, content, key) + VALUES('delete', old.memory_id, old.content, old.key); + INSERT INTO memory_fts(rowid, content, key) + VALUES (new.memory_id, new.content, new.key); +END; diff --git a/src/lib/db/optimizationSettings.ts b/src/lib/db/optimizationSettings.ts index 636313c406..70967ae270 100644 --- a/src/lib/db/optimizationSettings.ts +++ b/src/lib/db/optimizationSettings.ts @@ -4,7 +4,29 @@ import type { SqliteAdapter } from "./adapters/types"; type SqliteDatabase = SqliteAdapter; type DatabaseOptimizationSettings = DatabaseSettings["optimization"]; -type AutoVacuumMode = DatabaseOptimizationSettings["autoVacuumMode"]; +export type AutoVacuumMode = DatabaseOptimizationSettings["autoVacuumMode"]; + +/** + * A mismatch between the configured `optimization.autoVacuumMode` (the + * `key_value` config store) and the live SQLite `auto_vacuum` pragma on the + * actual database file. See #13432 — migration 046 seeds the config value on + * every database (including pre-existing ones) but SQLite only applies + * `auto_vacuum` on a subsequent `VACUUM`, which the startup path deliberately + * never runs synchronously (that would reintroduce the blocking-VACUUM + * hazard tracked by #12821). + */ +export interface AutoVacuumDrift { + configured: AutoVacuumMode; + live: AutoVacuumMode; +} + +// Shared key_value coordinate for the persisted drift record. Written here +// (at boot, directly against the `db` handle being initialized — NOT via +// getDbInstance(), which is not yet set at this point in core.ts's boot +// sequence) and read/cleared by vacuumScheduler.ts once the scheduler runs +// the reconcile out-of-request. +const AUTO_VACUUM_DRIFT_NAMESPACE = "scheduler"; +const AUTO_VACUUM_DRIFT_KEY = "vacuumDrift"; const AUTO_VACUUM_MODE_TO_PRAGMA: Record = { NONE: 0, @@ -233,6 +255,40 @@ export function applyDatabaseOptimizationSettingsForDb( ); } +/** + * Compares the configured `autoVacuumMode` against the live SQLite pragma. + * Pure/read-only — never mutates the database. Returns `null` when they + * already agree. + */ +export function checkAutoVacuumDrift( + db: SqliteDatabase, + settings: DatabaseOptimizationSettings +): AutoVacuumDrift | null { + const liveMode = getAutoVacuumModeForDb(db); + if (liveMode === settings.autoVacuumMode) return null; + return { configured: settings.autoVacuumMode, live: liveMode }; +} + +/** + * Persists (or clears, when `drift` is `null`) the auto_vacuum drift record + * directly against the given `db` handle. Deliberately does NOT go through + * `getDbInstance()` — at the one call site that matters (startup, inside + * core.ts before `setDb()` has run) that would recurse back into database + * initialization. + */ +function persistAutoVacuumDriftRecord(db: SqliteDatabase, drift: AutoVacuumDrift | null): void { + try { + db.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run( + AUTO_VACUUM_DRIFT_NAMESPACE, + AUTO_VACUUM_DRIFT_KEY, + JSON.stringify(drift) + ); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.warn(`[DB] Failed to persist auto_vacuum drift record: ${message}`); + } +} + export function applyStoredDatabaseOptimizationSettings(db: SqliteDatabase): void { const settings = readDatabaseOptimizationSettings(db); // Startup can happen concurrently in test workers and clustered hosts. Only @@ -241,6 +297,19 @@ export function applyStoredDatabaseOptimizationSettings(db: SqliteDatabase): voi applyDatabaseOptimizationSettingsForDb(db, settings, { applyPersistent: false, }); + + // #13432: detect (but never synchronously fix — that would reintroduce the + // #12821 blocking-VACUUM-at-boot hazard) a drift between the configured + // autoVacuumMode and the live pragma. Reconciliation happens out-of-request + // on the next scheduled vacuumScheduler run (see vacuumScheduler.ts::runNow). + const drift = checkAutoVacuumDrift(db, settings); + if (drift) { + console.warn( + `[DB] auto_vacuum drift detected (configured=${drift.configured}, live=${drift.live}); ` + + `scheduling reconcile on the next vacuum-scheduler run` + ); + } + persistAutoVacuumDriftRecord(db, drift); } export function setAutoVacuumForDb(db: SqliteDatabase, mode: AutoVacuumMode): void { diff --git a/src/lib/db/providers.ts b/src/lib/db/providers.ts index ab59a69255..1337f8e8c8 100644 --- a/src/lib/db/providers.ts +++ b/src/lib/db/providers.ts @@ -1126,6 +1126,8 @@ export async function touchConnectionSyncedModelsAt(id: string): Promise { * since the caller already verified the connection is eligible for reset. * Resets all backoff/error columns so the connection re-enters the selection pool. * Does invalidateDbCache + bumpProxyConfigGeneration since backoff affects priority. + * #13389: `skipModelCatalog` — the catalog builder never reads backoff/error + * state, so this must not bust the expensive-to-rebuild `/v1/models` cache. */ export async function resetConnectionBackoff(id: string): Promise { if (!id) return; @@ -1146,7 +1148,7 @@ export async function resetConnectionBackoff(id: string): Promise { updatedAt: now, id, }); - invalidateDbCache("connections"); + invalidateDbCache("connections", id, { skipModelCatalog: true }); bumpProxyConfigGeneration(); } diff --git a/src/lib/db/readCache.ts b/src/lib/db/readCache.ts index 088e494a6f..8c270f4f4a 100644 --- a/src/lib/db/readCache.ts +++ b/src/lib/db/readCache.ts @@ -276,10 +276,25 @@ export function invalidateModelCatalogCache(): void { * connection's by-ID cache entry is invalidated (the filter-keyed raw * cache must still be fully cleared since overlapping filter results * cannot be selectively invalidated). + * + * `skipModelCatalog` (#13389): the unified `/v1/models` builder + * (`src/app/api/v1/models/catalog.ts`) never reads routing/health-only + * connection fields — `backoffLevel`, `testStatus`, `rateLimitedUntil`, + * `lastError*`, `errorCode` — only structural fields such as + * `excludedModels` or enabled/disabled. A caller that only touched those + * routing fields (e.g. `resetConnectionBackoff`) should still bust the + * connections read cache but must NOT bump `modelCatalogCacheVersion`: + * doing so was busting the entire `/v1/models` response cache on every + * routine backoff auto-recovery during normal request routing, far more + * often than the cache's own 60s TTL / 30s stale-while-revalidate window + * intends, forcing frequent expensive cold rebuilds. Structural connection + * writes (create/update/delete) must keep the default (omit this flag) so + * the catalog still reflects them immediately. */ export function invalidateDbCache( scope?: "settings" | "pricing" | "connections" | "combos" | "nodes" | "model-capabilities", - id?: string + id?: string, + opts?: { skipModelCatalog?: boolean } ): void { if (!scope || scope === "settings") settingsCache.invalidate(); if (!scope || scope === "pricing") pricingCache.invalidate(); @@ -294,6 +309,7 @@ export function invalidateDbCache( } if (!scope || scope === "nodes") nodesCache.invalidate(); if (!scope || scope === "combos") combosCacheVersion++; + if (opts?.skipModelCatalog) return; // Settings/connections/combos all feed the unified model catalog builder // (blockedProviders + hidePaidModels, provider connections + excludedModels, // combo definitions, respectively) — pricing does too, via isFreeModel(). diff --git a/src/lib/db/vacuumScheduler.ts b/src/lib/db/vacuumScheduler.ts index ad0e183556..143313075b 100644 --- a/src/lib/db/vacuumScheduler.ts +++ b/src/lib/db/vacuumScheduler.ts @@ -1,7 +1,13 @@ import { DEFAULT_DATABASE_SETTINGS } from "@/types/databaseSettings"; import { MAX_TIMER_TIMEOUT_MS } from "@/shared/utils/runtimeTimeouts"; +import type { SqliteAdapter } from "./adapters/types"; import { getDbInstance } from "./core"; +import { + getAutoVacuumModeForDb, + setAutoVacuumForDb, + type AutoVacuumDrift, +} from "./optimizationSettings"; // Direct `key_value` access — the existing `keyValueStore` helpers only exist // in test fixtures; the 3 production call sites (pricingSync, jsonMigration, // serviceModels) all use `getDbInstance().prepare(...).run()` directly. We @@ -46,6 +52,10 @@ export interface VacuumSchedulerState { lastDurationMs: number | null; isRunning: boolean; nextRunAt: number | null; + /** #13432 — configured vs live auto_vacuum mismatch pending reconcile, or null once reconciled. */ + autoVacuumDrift: AutoVacuumDrift | null; + /** Pages freed by the most recent bounded `PRAGMA incremental_vacuum` batch, or null if the last run was a full VACUUM / drift reconcile. */ + lastReclaimedPages: number | null; } export type ScheduledVacuum = (typeof DEFAULT_DATABASE_SETTINGS)["optimization"]["scheduledVacuum"]; @@ -73,8 +83,19 @@ const STATE_DEFAULTS: VacuumSchedulerState = { lastDurationMs: null, isRunning: false, nextRunAt: null, + autoVacuumDrift: null, + lastReclaimedPages: null, }; +// Shared key_value coordinate with optimizationSettings.ts, which writes the +// initial drift record at boot (see AUTO_VACUUM_DRIFT_NAMESPACE/KEY there). +const AUTO_VACUUM_DRIFT_NAMESPACE = "scheduler"; +const AUTO_VACUUM_DRIFT_KEY = "vacuumDrift"; + +// Bounded per-run reclaim so a scheduled vacuum on a multi-GB INCREMENTAL +// database never blocks for as long as a full VACUUM would (#13432 fix #2). +const INCREMENTAL_VACUUM_BATCH_PAGES = 2000; + let timer: ReturnType | null = null; let currentState: VacuumSchedulerState = { ...STATE_DEFAULTS }; @@ -230,6 +251,35 @@ function persistState(): void { setKeyValue(KEY_VALUE_NAMESPACE, KEY_VALUE_KEY, JSON.stringify(currentState)); } +function isAutoVacuumDrift(value: unknown): value is AutoVacuumDrift { + return isRecord(value) && typeof value.configured === "string" && typeof value.live === "string"; +} + +function loadAutoVacuumDrift(): AutoVacuumDrift | null { + const raw = getKeyValue(AUTO_VACUUM_DRIFT_NAMESPACE, AUTO_VACUUM_DRIFT_KEY); + if (!raw) return null; + const parsed = parseJsonSafe(raw); + return isAutoVacuumDrift(parsed) ? parsed : null; +} + +function clearAutoVacuumDrift(): void { + setKeyValue(AUTO_VACUUM_DRIFT_NAMESPACE, AUTO_VACUUM_DRIFT_KEY, JSON.stringify(null)); +} + +/** + * Bounded reclaim step for a database already running `auto_vacuum=INCREMENTAL`: + * frees at most `INCREMENTAL_VACUUM_BATCH_PAGES` pages per scheduled run + * instead of the unconditional full `VACUUM` this scheduler used to always + * issue (#13432 fix #2 / reporter's suggested fix #2). Returns the number of + * freelist pages actually reclaimed by this batch. + */ +function runBoundedIncrementalVacuum(db: SqliteAdapter): number { + const before = Number(db.pragma("freelist_count", { simple: true }) ?? 0); + db.pragma(`incremental_vacuum(${INCREMENTAL_VACUUM_BATCH_PAGES})`); + const after = Number(db.pragma("freelist_count", { simple: true }) ?? 0); + return Math.max(0, before - after); +} + function loadPersistedState(): Partial { const raw = getKeyValue(KEY_VALUE_NAMESPACE, KEY_VALUE_KEY); if (!raw) return {}; @@ -252,7 +302,12 @@ export function refresh(): VacuumSchedulerState { return getState(); } -export async function runNow(): Promise<{ success: boolean; durationMs: number; error?: string }> { +export async function runNow(): Promise<{ + success: boolean; + durationMs: number; + error?: string; + reclaimedPages?: number; +}> { if (currentState.isRunning) { return { success: false, durationMs: 0, error: "already_running" }; } @@ -262,14 +317,34 @@ export async function runNow(): Promise<{ success: boolean; durationMs: number; const start = Date.now(); try { const db = getDbInstance(); - db.exec("VACUUM"); + let reclaimedPages: number | null = null; + + // #13432: reconcile a configured-vs-live auto_vacuum drift first, out of + // request handling, on this bounded/observable scheduled path — never + // synchronously at startup (see optimizationSettings.ts::applyStoredDatabaseOptimizationSettings). + const drift = loadAutoVacuumDrift(); + if (drift) { + console.log( + `[DB] Reconciling auto_vacuum drift (configured=${drift.configured}, live=${drift.live}): ` + + `running one-time conversion VACUUM to apply the configured mode to the database file.` + ); + setAutoVacuumForDb(db, drift.configured); + clearAutoVacuumDrift(); + } else if (getAutoVacuumModeForDb(db) === "INCREMENTAL") { + reclaimedPages = runBoundedIncrementalVacuum(db); + } else { + db.exec("VACUUM"); + } + const duration = Date.now() - start; currentState.lastRunAt = start; currentState.lastError = null; currentState.lastDurationMs = duration; + currentState.lastReclaimedPages = reclaimedPages; + currentState.autoVacuumDrift = loadAutoVacuumDrift(); currentState.isRunning = false; refresh(); // reset the next-run clock from this successful run - return { success: true, durationMs: duration }; + return { success: true, durationMs: duration, reclaimedPages: reclaimedPages ?? undefined }; } catch (err) { const message = err instanceof Error ? err.message : String(err); currentState.lastError = message; @@ -296,6 +371,10 @@ export function init(): VacuumSchedulerState { ...persisted, isRunning: false, // never resume a "running" state across restarts nextRunAt: null, // recompute below + // Always reload from the drift record's own key_value entry rather than + // trusting a stale copy embedded in the scheduler state blob — it is the + // source of truth optimizationSettings.ts writes at every boot. + autoVacuumDrift: loadAutoVacuumDrift(), }; return refresh(); } diff --git a/src/lib/gracefulShutdown.ts b/src/lib/gracefulShutdown.ts index 380952730f..f0493e2ab7 100644 --- a/src/lib/gracefulShutdown.ts +++ b/src/lib/gracefulShutdown.ts @@ -200,7 +200,16 @@ export function initGracefulShutdown(): void { } const shutdown = (signal: string) => { - void globalThis.__omnirouteRequestShutdown?.(signal).then(() => process.exit(0)); + void globalThis.__omnirouteRequestShutdown?.(signal).then(() => { + // #13306: on Windows, sql.js's Emscripten WASM build leaves pending libuv + // async-handle teardown work in flight after a statement has run. Calling + // process.exit() in the same tick as cleanup() resolving tears the event loop + // down before that teardown settles, and libuv's Windows async-handle close path + // asserts `!(handle->flags & UV_HANDLE_CLOSING)` -> hard abort. Deferring by one + // macrotask (mirrors 9router's own shutdown call sites, e.g. + // appUpdater.js:199, cli/cli.js:675) gives that teardown work a chance to run. + setTimeout(() => process.exit(0), 0); + }); }; process.on("SIGTERM", () => void shutdown("SIGTERM")); diff --git a/src/lib/oauth/gitlab.ts b/src/lib/oauth/gitlab.ts index e9bf961a10..8fa9c2d2c5 100644 --- a/src/lib/oauth/gitlab.ts +++ b/src/lib/oauth/gitlab.ts @@ -103,16 +103,19 @@ export function isGitLabDirectAccessDisabled(status: number, bodyText: string): } /** - * #10365 / #10499: same predicate the chat-path executor (open-sse/executors/gitlab.ts) - * uses to decide whether a failed `direct_access` exchange should fall back to the - * public Code Suggestions completions endpoint instead of surfacing a hard error. - * A rejected exchange (401 — invalid/expired direct_access grant) or an explicitly - * disabled direct-connections tenant (403 with the GitLab-specific message) both mean - * "direct mode unavailable, but the public monolith endpoint may still work" — never a - * definitive "the token itself is bad" signal on their own. + * #10365 / #10499 / #12958: same predicate the chat-path executor + * (open-sse/executors/gitlab.ts) uses to decide whether a failed `direct_access` + * exchange should fall back to the public Code Suggestions completions endpoint + * instead of surfacing a hard error. A rejected exchange (401 — invalid/expired + * direct_access grant) or ANY 403 (an explicitly disabled direct-connections tenant, + * or an entitlement/scope-resolution failure GitLab does not document a distinct + * status for — #12958) both mean "direct mode unavailable, but the public monolith + * endpoint may still work" — never a definitive "the token itself is bad" signal on + * their own. `isGitLabDirectAccessDisabled()` stays available for log/diagnostic + * labeling; it no longer gates this decision. */ -export function shouldFallbackToPublicCodeSuggestions(status: number, bodyText: string): boolean { - return status === 401 || isGitLabDirectAccessDisabled(status, bodyText); +export function shouldFallbackToPublicCodeSuggestions(status: number, _bodyText: string): boolean { + return status === 401 || status === 403; } /** Headers for a public Code Suggestions completions probe (chat path and connection test). */ diff --git a/src/lib/providers/imageValidation.ts b/src/lib/providers/imageValidation.ts index 4e190f95d3..bb1c24a221 100644 --- a/src/lib/providers/imageValidation.ts +++ b/src/lib/providers/imageValidation.ts @@ -30,9 +30,13 @@ const IMAGE_PROVIDER_VALIDATION_ENDPOINTS: Record< path: "/account/v1/credits/balance", }, magnific: { - // GET /v1/ai/mystic lists tasks and does not start a paid generation. + // GET /v1/ai/mystic is POST-only (task submission); once a key authenticates, + // routing to that unhandled GET 404s, reporting every valid key as invalid + // (#12927). GET /v1/ai/flows is a genuine read-only route that returns 200 for + // valid keys (team AND personal accounts) and 401 for invalid/missing keys, + // verified against a real Premium+ personal account by the issue reporter. baseUrl: "https://api.magnific.com", - path: "/v1/ai/mystic", + path: "/v1/ai/flows", }, }; diff --git a/src/lib/providers/xai/translators/gemini.ts b/src/lib/providers/xai/translators/gemini.ts index 531dff4a9a..1a8dc0de90 100644 --- a/src/lib/providers/xai/translators/gemini.ts +++ b/src/lib/providers/xai/translators/gemini.ts @@ -263,7 +263,7 @@ function toolsGeminiToXai(tools: GeminiTool[]): XaiTool[] | undefined { */ export function geminiRequestToXaiResponses( req: GeminiRequest, - model: string | null = null, + model: string | null = null ): XaiResponsesRequest { if (!req || typeof req !== "object") return req as unknown as XaiResponsesRequest; const input: XaiInputItem[] = []; @@ -275,9 +275,7 @@ export function geminiRequestToXaiResponses( if (fnItems.length) { for (const it of fnItems) input.push(it); // Filter remaining text/image parts - const remaining = (c.parts ?? []).filter( - (p) => !p?.functionCall && !p?.functionResponse, - ); + const remaining = (c.parts ?? []).filter((p) => !p?.functionCall && !p?.functionResponse); if (remaining.length) input.push({ role, content: partsToXaiBlocks(remaining) }); } else { input.push({ role, content: partsToXaiBlocks(c.parts ?? []) }); @@ -324,7 +322,7 @@ export function geminiRequestToXaiResponses( */ export function xaiCompletedToGeminiJson( completed: XaiCompleted, - origReq: GeminiRequest | null = null, + origReq: GeminiRequest | null = null ): object { const parts: unknown[] = []; const finishReason = "STOP"; @@ -364,7 +362,8 @@ export function xaiCompletedToGeminiJson( promptTokenCount: u.input_tokens ?? u.prompt_tokens ?? 0, candidatesTokenCount: u.output_tokens ?? u.completion_tokens ?? 0, totalTokenCount: - u.total_tokens ?? ((u.input_tokens ?? 0) + (u.output_tokens ?? 0)), + u.total_tokens ?? + (u.input_tokens ?? u.prompt_tokens ?? 0) + (u.output_tokens ?? u.completion_tokens ?? 0), }; } return out; diff --git a/src/lib/providers/xai/translators/openai-chat.ts b/src/lib/providers/xai/translators/openai-chat.ts index e5c7274dd9..da4e219d8d 100644 --- a/src/lib/providers/xai/translators/openai-chat.ts +++ b/src/lib/providers/xai/translators/openai-chat.ts @@ -38,6 +38,7 @@ interface OpenAiMessage { content?: MessageContent; tool_calls?: OpenAiToolCall[]; tool_call_id?: string; + function_call?: { name?: string; arguments?: string }; } interface OpenAiChatRequest { @@ -213,6 +214,22 @@ export function chatRequestToXaiResponses(req: OpenAiChatRequest): XaiResponsesR } continue; } + // Legacy OpenAI Chat Completions form: assistant tool-call carried as a top-level + // `function_call` field instead of `tool_calls[]`. Some OpenAI-compatible clients still + // emit this shape; without this branch the message falls through to the generic case + // below with empty content and the tool invocation is silently dropped (#12692). + if (m.role === "assistant" && m.function_call?.name) { + if (m.content) { + input.push({ role: "assistant", content: messageContentToXaiBlocks(m.content) }); + } + input.push({ + type: "function_call", + call_id: genId("call"), + name: m.function_call.name, + arguments: m.function_call.arguments ?? "", + }); + continue; + } input.push({ role: m.role ?? "user", content: messageContentToXaiBlocks(m.content ?? "") }); } @@ -303,7 +320,9 @@ export function xaiCompletedToChatJson( out.usage = { prompt_tokens: u.input_tokens ?? u.prompt_tokens ?? 0, completion_tokens: u.output_tokens ?? u.completion_tokens ?? 0, - total_tokens: u.total_tokens ?? (u.input_tokens ?? 0) + (u.output_tokens ?? 0), + total_tokens: + u.total_tokens ?? + (u.input_tokens ?? u.prompt_tokens ?? 0) + (u.output_tokens ?? u.completion_tokens ?? 0), }; } return out; diff --git a/src/lib/skills/injection.ts b/src/lib/skills/injection.ts index 881a039189..951a65d102 100644 --- a/src/lib/skills/injection.ts +++ b/src/lib/skills/injection.ts @@ -51,6 +51,133 @@ export function decodeSkillToolName(toolName: string): string { } } +// Depth guard mirroring open-sse/services/toolSchemaSanitizer.ts's +// MAX_RECURSION_DEPTH, so a pathological/cyclic-looking nested schema +// submitted by a custom skill (POST /api/skills accepts any z.record shape) +// cannot blow the stack. +const MAX_SCHEMA_REPAIR_DEPTH = 32; + +// JSON Schema keywords whose *value* is itself a schema node/map, not a +// user-declared property — recursing into their children must not treat the +// container itself as a "bare property map" candidate. Mirrors +// open-sse/translator/helpers/geminiHelper.ts's SCHEMA_MAP_KEYS for the +// Gemini-only normalizeMalformedSchemaObjects this mirrors (#12269). +const SCHEMA_MAP_KEYS = new Set(["properties", "$defs", "definitions", "patternProperties"]); + +const SCHEMA_NODE_KEYS = new Set([ + "additionalProperties", + "additionalItems", + "contains", + "default", + "dependencies", + "discriminator", + "else", + "example", + "examples", + "if", + "patternProperties", + "propertyNames", + "then", +]); + +function isSchemaNode(record: Record): boolean { + if (Object.keys(record).some((key) => key.startsWith("x-") || SCHEMA_NODE_KEYS.has(key))) { + return true; + } + if (typeof record.type === "string" || Array.isArray(record.type)) return true; + if (record.properties !== undefined || Array.isArray(record.required)) return true; + if (record.items !== undefined) return true; + if (record.anyOf !== undefined || record.oneOf !== undefined || record.allOf !== undefined) { + return true; + } + return record.$ref !== undefined || record.enum !== undefined || record.const !== undefined; +} + +function isBarePropertyMap(record: Record): boolean { + const keys = Object.keys(record); + if (keys.length === 0 || isSchemaNode(record)) return false; + return keys.every((key) => { + const value = record[key]; + return Boolean(value) && typeof value === "object" && !Array.isArray(value); + }); +} + +// Strips a scalar (non-array) `required` off every property of `record` and, +// only when it was `true`, promotes the property's key onto the parent +// schema's own `required` array (created if absent, deduped if present). +function promoteBooleanRequired(record: Record): void { + const properties = record.properties; + if (!properties || typeof properties !== "object" || Array.isArray(properties)) return; + + const required = Array.isArray(record.required) + ? record.required.filter((field): field is string => typeof field === "string") + : []; + + for (const [name, schema] of Object.entries(properties as Record)) { + if (!schema || typeof schema !== "object" || Array.isArray(schema)) continue; + const child = schema as Record; + if (child.required === true && !required.includes(name)) { + required.push(name); + } + if ("required" in child && !Array.isArray(child.required)) { + delete child.required; + } + } + + if (required.length > 0) { + record.required = required; + } else if (!Array.isArray(record.required)) { + delete record.required; + } +} + +// Repairs the two malformed-schema shapes strict JSON Schema validators +// (agnes/nvidia/DeepSeek and other OpenAI-compatible upstreams) reject, +// recursing into every nested level of a skill's declared input schema — +// not just the root map #11881 already handled: +// 1. A bare property map with no `type`/`properties` wrapper (e.g. +// `{ opts: { limit: { type: "number" } } }`) is lifted into +// `{ type: "object", properties: {...} }`, bottom-up so nested bare +// maps are fixed before their parent is inspected. +// 2. A scalar `required: true` on a property is stripped and promoted onto +// the parent's `required` array instead. +// Mirrors open-sse/translator/helpers/geminiHelper.ts's +// normalizeMalformedSchemaObjects (itself modeled on CLIProxyAPI's function +// of the same name), which already does this for the Gemini/Antigravity +// request-translation path (#12269) — this is the skill-injection-path +// equivalent, additive and independent from that implementation. +function repairMalformedSchema(node: unknown, parentKey?: string, depth = 0): void { + if (!node || typeof node !== "object" || depth > MAX_SCHEMA_REPAIR_DEPTH) return; + + if (Array.isArray(node)) { + for (const item of node) { + repairMalformedSchema(item, parentKey, depth + 1); + } + return; + } + + const record = node as Record; + + for (const [key, value] of Object.entries(record)) { + if (value && typeof value === "object") { + repairMalformedSchema(value, key, depth + 1); + } + } + + if (parentKey === undefined || !SCHEMA_MAP_KEYS.has(parentKey)) { + if (isBarePropertyMap(record)) { + const props = { ...record }; + for (const key of Object.keys(record)) { + delete record[key]; + } + record.type = "object"; + record.properties = props; + } + } + + promoteBooleanRequired(record); +} + // Skills store a flat JSON Schema record ({ "text": { "type": "string" } }), // but Gemini (function_declarations[].parameters) and Anthropic // (input_schema) require a full object schema with a properties wrapper. @@ -60,23 +187,30 @@ function normalizeInputSchema(input: Record): Record; if (typeof input.type === "string") { - return input; + // Already a full object schema (#11881's root case doesn't apply) — but + // it may still carry the deeper #13022 malformations (nested bare + // property maps, per-property boolean `required`) inside `properties`, + // so still recurse; just skip the root-level string-shorthand expansion. + root = { ...input }; + } else { + // Some builtin skills declare property types in shorthand ("content": + // "string" instead of "content": { "type": "string" }). Strict schema + // validators — Zhipu GLM served through opencode-go (upstream error [1210] + // "Invalid API parameter") — reject the shorthand as malformed JSON Schema, + // which 400s every request the skill tools are injected into. Expand string + // values to { type: value }; non-string values pass through untouched. + const properties: Record = {}; + for (const [key, value] of Object.entries(input)) { + properties[key] = typeof value === "string" ? { type: value } : value; + } + root = { type: "object", properties }; } - // Some builtin skills declare property types in shorthand ("content": - // "string" instead of "content": { "type": "string" }). Strict schema - // validators — Zhipu GLM served through opencode-go (upstream error [1210] - // "Invalid API parameter") — reject the shorthand as malformed JSON Schema, - // which 400s every request the skill tools are injected into. Expand string - // values to { type: value }; non-string values pass through untouched. - const properties: Record = {}; - for (const [key, value] of Object.entries(input)) { - properties[key] = typeof value === "string" ? { type: value } : value; - } - return { - type: "object", - properties, - }; + + repairMalformedSchema(root); + return root; } function skillToOpenAI(skill: Skill): OpenAITool { diff --git a/src/lib/sseTextTransform.ts b/src/lib/sseTextTransform.ts index d33ed8d6b5..d9d0275980 100644 --- a/src/lib/sseTextTransform.ts +++ b/src/lib/sseTextTransform.ts @@ -1,6 +1,7 @@ export type FieldCategory = "content" | "reasoning" | "toolArgs" | "partialJson"; -const CATEGORY_MAP: Record = { +// Keys that always map to a fixed category, regardless of where they appear in the chunk. +const FIXED_CATEGORY_MAP: Record = { reasoning: "reasoning", thinking: "reasoning", reasoning_content: "reasoning", @@ -8,8 +9,69 @@ const CATEGORY_MAP: Record = { partial_json: "partialJson", }; +// System/protocol metadata keys that must never be routed through the PII processor or +// buffer, no matter which JSON shape they appear in. Shared by the real-time sanitizeObject +// pass (below) and streamingPiiTransform.ts's onFlush generic-fallback branch — the two +// copies of this list had drifted (see issue #13488): neither one listed `provider`, +// `native_finish_reason`, or the `reasoning_details[].format` field, so those OpenRouter +// metadata strings fell through to the default "content" category and got spliced into the +// same sliding-window buffer as the actual answer text. +export const METADATA_KEYS = new Set([ + "id", + "model", + "object", + "created", + "finish_reason", + "finishReason", + "native_finish_reason", + "role", + "type", + "index", + "stop_reason", + "stop_sequence", + "system_fingerprint", + "service_tier", + "usage", + "prompt_tokens", + "completion_tokens", + "total_tokens", + "input_tokens", + "output_tokens", + "logprobs", + "refusal", + "name", + "event", + "provider", + "format", +]); + +/** + * Classify a string field as a PII-processed category, or `null` when it is metadata that + * must pass through untouched. `parentKey` is the key of the object that directly contains + * `key` (empty string at the JSON root) — it disambiguates `text`, which means the answer + * everywhere except inside a `reasoning_details[]` item, where it is reasoning text (and + * `format` alongside it is metadata, not content, even though it is not disambiguated by + * parent elsewhere). Any string field not explicitly recognized as metadata defaults to + * "content" — this keeps non-standard/unrecognized stream shapes (arbitrary JSON keys that + * match none of the known provider formats) from silently losing their text. + */ +export function classifyField(key: string, parentKey = ""): FieldCategory | null { + if (FIXED_CATEGORY_MAP[key]) { + return FIXED_CATEGORY_MAP[key]; + } + if (key === "text" && parentKey === "reasoning_details") { + return "reasoning"; + } + if (METADATA_KEYS.has(key)) { + return null; + } + return "content"; +} + +// Back-compat helper kept for any external caller expecting a category rather than `null` +// for metadata; internal call sites use `classifyField` so they can skip metadata entirely. export function getFieldCategory(key: string): FieldCategory { - return CATEGORY_MAP[key] || "content"; + return classifyField(key) ?? "content"; } const STOP_EVENT_TYPES = new Set([ @@ -123,34 +185,18 @@ export function createSseTextTransform( const isStopSignal = checkIfStopSignal(json); const isSnapshot = checkIfSnapshot(json); - const METADATA_KEYS = [ - "id", - "model", - "object", - "created", - "finish_reason", - "finishReason", - "role", - "type", - "index", - "stop_reason", - "stop_sequence", - "system_fingerprint", - "service_tier", - "usage", - "prompt_tokens", - "completion_tokens", - "total_tokens", - "input_tokens", - "output_tokens", - "logprobs", - "refusal", - "name", - "event", - ]; - - // Recursively sanitize all string properties (except system metadata) - const sanitizeObject = (obj: any, currentChoiceIdx = 0, currentToolIdx = 0) => { + // Recursively sanitize string properties, skipping recognized system metadata + // (`classifyField` returns null for METADATA_KEYS — `provider`, + // `native_finish_reason`, `reasoning_details[].format`, etc.). `parentKey` is the + // key of the enclosing object (unchanged across array-index recursion) so + // `classifyField` can tell `reasoning_details[].text` apart from ordinary content + // text. See issue #13488. + const sanitizeObject = ( + obj: any, + currentChoiceIdx = 0, + currentToolIdx = 0, + parentKey = "" + ) => { if (!obj || typeof obj !== "object") return; let choiceIdx = currentChoiceIdx; @@ -167,14 +213,15 @@ export function createSseTextTransform( } const compositeKey = `${choiceIdx}_${toolIdx}`; + const isArray = Array.isArray(obj); for (const key of Object.keys(obj)) { - if (METADATA_KEYS.includes(key)) { - continue; - } if (typeof obj[key] === "string") { const val = obj[key]; - const field: FieldCategory = getFieldCategory(key); + const field = classifyField(key, parentKey); + if (field === null) { + continue; + } if (field === "toolArgs" || field === "partialJson") { obj[key] = val; matched = true; @@ -183,7 +230,7 @@ export function createSseTextTransform( obj[key] = processor(val, field, isStopSignal, compositeKey, isSnapshot); matched = true; } else if (typeof obj[key] === "object") { - sanitizeObject(obj[key], choiceIdx, toolIdx); + sanitizeObject(obj[key], choiceIdx, toolIdx, isArray ? parentKey : key); } } }; diff --git a/src/lib/streamingPiiTransform.ts b/src/lib/streamingPiiTransform.ts index a26eb3fa2e..72018bb975 100644 --- a/src/lib/streamingPiiTransform.ts +++ b/src/lib/streamingPiiTransform.ts @@ -1,4 +1,4 @@ -import { createSseTextTransform, FieldCategory, getFieldCategory } from "./sseTextTransform"; +import { createSseTextTransform, FieldCategory, classifyField } from "./sseTextTransform"; import { sanitizePII } from "./piiSanitizer"; export interface PiiTransformOptions { @@ -116,33 +116,6 @@ export function createPiiSseTransform(options?: PiiTransformOptions): TransformS return null; } - // Explicitly target formats to prevent metadata corruption and leakage - const METADATA_KEYS = [ - "id", - "model", - "object", - "created", - "finish_reason", - "finishReason", - "role", - "type", - "index", - "stop_reason", - "stop_sequence", - "system_fingerprint", - "service_tier", - "usage", - "prompt_tokens", - "completion_tokens", - "total_tokens", - "input_tokens", - "output_tokens", - "logprobs", - "refusal", - "name", - "event", - ]; - // 1. Claude format if ( typeof lastJson.type === "string" && @@ -283,22 +256,31 @@ export function createPiiSseTransform(options?: PiiTransformOptions): TransformS // 5. Generic fallback const templateJson = lastContentJson || lastJson; const finalJson = JSON.parse(JSON.stringify(templateJson)); - const clearDeltas = (obj: any) => { + // Skip recognized system metadata (same `classifyField`/METADATA_KEYS as sanitizeObject + // in sseTextTransform.ts) — fields like `provider` or `native_finish_reason` must never + // be cleared to "" or refilled with buffered answer text here either. See #13488. + const clearDeltas = (obj: any, parentKey = "") => { if (!obj || typeof obj !== "object") return; + const isArray = Array.isArray(obj); for (const key of Object.keys(obj)) { - if (METADATA_KEYS.includes(key)) { - continue; - } if (typeof obj[key] === "string") { + if (classifyField(key, parentKey) === null) { + continue; + } obj[key] = ""; } else if (typeof obj[key] === "object") { - clearDeltas(obj[key]); + clearDeltas(obj[key], isArray ? parentKey : key); } } }; clearDeltas(finalJson); - const populateRemaining = (obj: any, currentChoiceIdx = 0, currentToolIdx = 0) => { + const populateRemaining = ( + obj: any, + currentChoiceIdx = 0, + currentToolIdx = 0, + parentKey = "" + ) => { if (!obj || typeof obj !== "object") return; let choiceIdx = currentChoiceIdx; @@ -315,20 +297,21 @@ export function createPiiSseTransform(options?: PiiTransformOptions): TransformS } const compositeKey = `${choiceIdx}_${toolIdx}`; + const isArray = Array.isArray(obj); for (const key of Object.keys(obj)) { - if (METADATA_KEYS.includes(key)) { - continue; - } if (typeof obj[key] === "string") { - const field: FieldCategory = getFieldCategory(key); + const field = classifyField(key, parentKey); + if (field === null) { + continue; + } const choiceBuf = getBuffers(compositeKey); if (choiceBuf[field]) { obj[key] = (obj[key] || "") + choiceBuf[field]; choiceBuf[field] = ""; } } else if (typeof obj[key] === "object") { - populateRemaining(obj[key], choiceIdx, toolIdx); + populateRemaining(obj[key], choiceIdx, toolIdx, isArray ? parentKey : key); } } }; diff --git a/src/lib/tokenHealthCheck.ts b/src/lib/tokenHealthCheck.ts index eff72dfd9c..ddeaa1cf5f 100644 --- a/src/lib/tokenHealthCheck.ts +++ b/src/lib/tokenHealthCheck.ts @@ -13,7 +13,8 @@ import { getProviderConnections, updateProviderConnection } from "@/lib/db/providers"; import { getCachedProviderConnectionById } from "@/lib/db/readCache"; -import { getSettings, resolveProxyForConnection } from "@/lib/db/settings"; +import { getSettings } from "@/lib/db/settings"; +import { resolveGuardedProxyConfig } from "@/lib/tokenHealthCheckProxyGuard"; import { getAccessToken, getDeprecationNotice, @@ -701,8 +702,9 @@ export async function checkConnection(conn) { let refreshedProviderSpecificData: Record | null = null; const hideLogs = await shouldHideLogs(); - const proxyResolution = await resolveProxyForConnection(conn.id); - const proxyConfig = extractResolvedProxyConfig(proxyResolution); + const { proxyConfig, blocked } = await resolveGuardedProxyConfig(conn.id, conn.provider); + if (blocked) + return void logWarn(`#13470 proxy-pool guard: skipping Copilot refresh for ${conn.id}`); const healthCheckLog = { info: (tag: string, msg: string) => { if (!hideLogs) console.log(LOG_PREFIX, `[${tag}]`, msg); @@ -908,8 +910,9 @@ export async function checkConnection(conn) { }; const hideLogs = await shouldHideLogs(); - const proxyResolution = await resolveProxyForConnection(conn.id); - const proxyConfig = extractResolvedProxyConfig(proxyResolution); + const { proxyConfig, blocked } = await resolveGuardedProxyConfig(conn.id, conn.provider); + if (blocked) + return void logWarn(`#13470 proxy-pool guard: skipping token refresh for ${conn.id}`); const healthCheckLog = { info: (tag: string, msg: string) => { diff --git a/src/lib/tokenHealthCheckProxyGuard.ts b/src/lib/tokenHealthCheckProxyGuard.ts new file mode 100644 index 0000000000..37d7f916a8 --- /dev/null +++ b/src/lib/tokenHealthCheckProxyGuard.ts @@ -0,0 +1,24 @@ +import { resolveProxyForConnection } from "@/lib/db/settings"; +import { hasBlockingProxyAssignment } from "@/lib/db/proxies"; + +/** + * #13470: fail-closed guard for the token-health-check sweep, mirroring the #6246 + * contract the interactive chat/executor path enforces via + * `safeResolveProxy`/`hasBlockingProxyAssignment` (src/sse/handlers/chatHelpers.ts). + * Before this guard, the sweep called `resolveProxyForConnection` directly and a + * connection whose assigned proxy pool had gone fully dead resolved silently to + * direct/env-proxy egress — leaking the refresh-token exchange on the real IP. + * Callers must skip (log + return) rather than throw: this sweeps many connections + * per tick and one blocked connection must not abort the rest. + */ +export async function resolveGuardedProxyConfig( + connectionId: string, + provider?: string +): Promise<{ proxyConfig: unknown; blocked: boolean }> { + const resolved = (await resolveProxyForConnection(connectionId)) as { proxy?: unknown } | null; + const proxyConfig = resolved?.proxy ?? null; + if (!proxyConfig && hasBlockingProxyAssignment(connectionId, provider)) { + return { proxyConfig: null, blocked: true }; + } + return { proxyConfig, blocked: false }; +} diff --git a/src/lib/usage/callLogArtifactWriter.ts b/src/lib/usage/callLogArtifactWriter.ts index 7cd8452340..64966afa04 100644 --- a/src/lib/usage/callLogArtifactWriter.ts +++ b/src/lib/usage/callLogArtifactWriter.ts @@ -116,8 +116,15 @@ function warnRateLimited(message: string): void { console.warn(message); } -function failOpen(warn = false): void { - if (warn) warnRateLimited("[callLogs] Call-log artifact worker failed; detail omitted."); +function describeFailureDetail(detail: unknown): string { + if (detail instanceof Error) return `${detail.name}: ${detail.message}`; + return String(detail ?? "unknown error"); +} + +function failOpen(warn = false, detail?: unknown): void { + if (warn) { + warnRateLimited(`[callLogs] Call-log artifact worker failed: ${describeFailureDetail(detail)}`); + } const failed = active ? [active, ...queue] : [...queue]; active = null; queue.length = 0; @@ -126,10 +133,24 @@ function failOpen(warn = false): void { notifyCloseWaiters(); } +let workerFileOverride: { workerFile: string; execArgv: string[] } | null = null; + +/** + * Test-only hook: force the next ensureWorker() call to spawn an arbitrary worker + * script/execArgv instead of the real callLogArtifactWorker file. Lets regression tests + * trigger a genuine worker_threads `error`/`exit` event without editing the production + * worker script. Never called from production code paths. + */ +export function __setCallLogWorkerOverrideForTests( + override: { workerFile: string; execArgv: string[] } | null +): void { + workerFileOverride = override; +} + function ensureWorker(): Worker { if (worker) return worker; - const { workerFile, execArgv } = resolveCallLogArtifactWorker(); + const { workerFile, execArgv } = workerFileOverride ?? resolveCallLogArtifactWorker(); // Reflect.construct keeps Next/Turbopack from interpreting the runtime-selected // worker path as a build-time glob and tracing tens of thousands of unrelated files. const created = Reflect.construct(Worker, [pathToFileURL(workerFile), { execArgv }]) as Worker; @@ -141,12 +162,12 @@ function ensureWorker(): Worker { completed.resolve(reply.result); pump(); }); - created.on("error", () => failOpen(true)); - created.on("messageerror", () => failOpen(true)); + created.on("error", (err) => failOpen(true, err)); + created.on("messageerror", (err) => failOpen(true, err)); created.on("exit", (code) => { if (worker !== created) return; worker = null; - if (code !== 0 || active) failOpen(true); + if (code !== 0 || active) failOpen(true, new Error(`worker exited with code ${code}`)); }); return created; } @@ -168,8 +189,8 @@ function pump(): void { artifact: next.artifact, environment: next.environment, }); - } catch { - failOpen(true); + } catch (err) { + failOpen(true, err); } } diff --git a/src/shared/constants/featureFlagDefinitions.ts b/src/shared/constants/featureFlagDefinitions.ts index ee089d74a5..8b6017bd6c 100644 --- a/src/shared/constants/featureFlagDefinitions.ts +++ b/src/shared/constants/featureFlagDefinitions.ts @@ -833,4 +833,16 @@ export const FEATURE_FLAG_DEFINITIONS: FeatureFlagDefinition[] = [ requiresRestart: false, warningLevel: "info", }, + { + key: "XAI_OAUTH_LIVE_MODEL_DISCOVERY", + label: "xAI OAuth Live Model Discovery", + description: + "Fetch the live xAI model catalog for xai-oauth connections from https://api.x.ai/v1/models using the OAuth bearer token, instead of the frozen static seed. Off by default: xai-oauth keeps serving the static seed unchanged. On any resolution error, discovery falls back to the seed (unverified whether x.ai accepts an OAuth bearer at this endpoint).", + descriptionI18nKey: "featureFlagXaiOauthLiveModelDiscoveryDescription", + category: "runtime", + defaultValue: "false", + type: "boolean", + requiresRestart: false, + warningLevel: "caution", + }, ]; diff --git a/src/shared/constants/providers/apikey/regional.ts b/src/shared/constants/providers/apikey/regional.ts index 2c437cc4b3..deca8d6286 100644 --- a/src/shared/constants/providers/apikey/regional.ts +++ b/src/shared/constants/providers/apikey/regional.ts @@ -469,6 +469,19 @@ export const APIKEY_PROVIDERS_REGIONAL = { freeNote: "Permanently free API - no credit card required.", authHint: "Get API key at agnes-ai.com", }, + "agnes-cn": { + id: "agnes-cn", + serviceKinds: ["llm"], + alias: "agnescn", + name: "Agnes AI (China)", + icon: "auto_awesome", + color: "#047857", + textIcon: "AC", + website: "https://api.agnes-ai.cn", + hasFree: true, + freeNote: "Permanently free, no API credit card required.", + authHint: "Get API key from the Agnes CN site.", + }, sealion: { id: "sealion", serviceKinds: ["llm"], diff --git a/src/shared/middleware/chatAdmissionIdentity.ts b/src/shared/middleware/chatAdmissionIdentity.ts index f786eaebd4..8b6986f076 100644 --- a/src/shared/middleware/chatAdmissionIdentity.ts +++ b/src/shared/middleware/chatAdmissionIdentity.ts @@ -1,8 +1,7 @@ -import { createHmac } from "crypto"; +import { createHmac, randomBytes } from "crypto"; import { timingSafeCompare } from "@/shared/utils/timingSafeCompare"; const ADMISSION_BYPASS_VALUE = "internal"; -const SELF_LOOP_KEY = "sk_omniroute"; const FINGERPRINT_KEY = "omniroute-admission-fingerprint-v1"; export const ADMISSION_BYPASS_HEADER = "x-omniroute-admission-bypass"; @@ -19,9 +18,27 @@ export function resolveSessionId(request: Request): string { return xGoogApiKey ? fingerprint(xGoogApiKey) : "anonymous"; } +// Lazily generated, held in memory only for the lifetime of this process — never +// persisted, never logged. Used ONLY as the last-resort self-loop bearer when the +// operator hasn't set OMNIROUTE_API_KEY/ROUTER_API_KEY (#13679: the previous fallback +// was the checked-in literal "sk_omniroute", a predictable shared secret anyone reading +// the source could forge). Both the in-process caller (audioBridgeHelpers / +// visionBridgeHelpers) and the verifier (isInternalAdmissionBypass) call this same +// function, so they always agree on the value within one process. +let generatedSelfLoopSecret: string | null = null; + +function getGeneratedSelfLoopSecret(): string { + if (!generatedSelfLoopSecret) { + generatedSelfLoopSecret = randomBytes(32).toString("hex"); + } + return generatedSelfLoopSecret; +} + export function resolveSelfLoopBearer(): string { return ( - process.env.OMNIROUTE_API_KEY?.trim() || process.env.ROUTER_API_KEY?.trim() || SELF_LOOP_KEY + process.env.OMNIROUTE_API_KEY?.trim() || + process.env.ROUTER_API_KEY?.trim() || + getGeneratedSelfLoopSecret() ); } diff --git a/src/shared/utils/httpClientAbortGuard.mjs b/src/shared/utils/httpClientAbortGuard.mjs index 41d5b07cb5..9465ff3208 100644 --- a/src/shared/utils/httpClientAbortGuard.mjs +++ b/src/shared/utils/httpClientAbortGuard.mjs @@ -1,7 +1,8 @@ "use strict"; /** - * HTTP client-abort crash guard (#fix-dev-server-aborted). + * HTTP client-abort / recoverable-upstream-timeout crash guard + * (#fix-dev-server-aborted, #12861). * * Node's http.Server turns an 'error' event on an IncomingMessage/ServerResponse * into an uncaughtException (and therefore a process exit) WHENEVER the emitter @@ -16,14 +17,29 @@ * connections + a live WebSocket; stray client-side socket closes during * navigation/HMR were taking the dev server down. * + * Two more categories were added after the 2026-09-14 agnes-cn upstream storm + * produced two sibling escapes in production: an intentional combo hedge + * cancellation (`AbortError: hedge-cancelled` — the sibling leg already won, + * so the cancellation is expected, not a fault) and undici fetch failures + * (`TypeError: fetch failed` with a socket-level code) against a flapping + * upstream. Both are runtime/environmental conditions the request layer + * already handles; neither is a process-fatal logic bug. + * + * A further, unrelated category covers #12861: `directFetchWithBoundedResponseStart`'s + * response-start timeout (`DIRECT_RESPONSE_START_TIMEOUT`) is a *recoverable* + * signal `proxyFetch.ts` already retries on a fresh socket — but a narrow + * timer/promise-settlement race can still deliver its abort reason to a + * promise nobody is awaiting anymore, which otherwise kills the whole process + * over a single upstream stall that the retry path was built to handle. + * * Two layers: * 1. `attachRequestStreamGuards(req, res)` — per-request listeners that absorb * client-abort errors so they never bubble to the process level. Call it * inside every `http.createServer((req, res) => …)` request listener. * 2. `installProcessCrashGuard()` — a last-resort safety net on * `process.on('uncaughtException' | 'unhandledRejection')` that swallows - * the same benign client-abort errors but otherwise preserves the existing - * crash semantics (so genuine bugs still surface). Idempotent. + * the same benign errors but otherwise preserves the existing crash + * semantics (so genuine bugs still surface). Idempotent. * * Kept as a `.mjs` module (no build step) so it is importable both from the * Node-only dev server (`scripts/dev/run-next.mjs`) and from the TypeScript @@ -63,9 +79,100 @@ export function isClientAbortError(err) { } } +/** + * #12861: a recoverable upstream-fetch timeout that `proxyFetch.ts` already + * retries on a fresh socket (see `open-sse/utils/directResponseStartTimeout.ts`). + * A narrow timer/promise-settlement race can still deliver its abort reason to + * a promise nobody is awaiting anymore, which otherwise surfaces here as an + * unhandledRejection/uncaughtException — even though the retry path already + * handles this exact condition and normally logs it as a plain 504. + * + * Kept as a bare string-code check (no import of the `.ts` source of truth) + * because this file has to stay build-free/plain-JS-loadable — see the module + * docstring. `DIRECT_RESPONSE_START_TIMEOUT_CODE` in + * `open-sse/utils/directResponseStartTimeout.ts` is the canonical definition; + * keep this string literal in sync with it. + * + * @param {unknown} err + * @returns {boolean} + */ +export function isRecoverableUpstreamTimeoutError(err) { + // Same reason-shape tolerance as isIntentionalComboAbort: a bare string + // reason rejects waiters with the string itself, not an Error object. + if (err === "DIRECT_RESPONSE_START_TIMEOUT") return true; + if (!err || typeof err !== "object") return false; + return /** @type {NodeJS.ErrnoException} */ (err).code === "DIRECT_RESPONSE_START_TIMEOUT"; +} + +/** + * Intentional combo-leg cancellation. When a combo dispatches hedged targets, + * the losing legs are aborted with a distinctive reason once a sibling wins + * (`hedge-cancelled`) or exceeds its per-model budget (`combo-per-model-timeout`) + * — see `COMBO_HEDGE_CANCELLED_REASON` / `COMBO_PER_MODEL_TIMEOUT_REASON` in + * `open-sse/services/combo/comboAbortReasons.ts` (bare literals duplicated here + * because this file must stay build-free; keep in sync). On 2026-09-14 such a + * cancellation escaped its promise chain and killed production with + * `Error [AbortError]: hedge-cancelled` — the request it belonged to had + * already completed 200 via the winning leg. + * + * Distinct from a *client* abort: only these exact reasons qualify, so an + * AbortError from an unknown subsystem still crashes loudly. + * + * @param {unknown} err + * @returns {boolean} + */ +export function isIntentionalComboAbort(err) { + const reasons = new Set(["hedge-cancelled", "combo-per-model-timeout"]); + // AbortSignal.reason is whatever was handed to abort(): a raw string + // reason rejects waiters with the string itself, not an Error object. + if (typeof err === "string") return reasons.has(err); + if (!err || typeof err !== "object") return false; + const e = /** @type {NodeJS.ErrnoException} */ (err); + if (e.name !== "AbortError") return false; + if (reasons.has(String(e.message))) return true; + const cause = /** @type {{ cause?: unknown }} */ (err).cause; + return typeof cause === "string" && reasons.has(cause); +} + +/** + * A network/IO failure against an upstream or its proxy — undici surfaces it + * as `TypeError: fetch failed` (fixed message; the syscall code rides on + * `cause`) or as an error carrying a `PROXY_UNREACHABLE` / `UND_ERR_*` code. + * On 2026-09-14 one of these (`PROXY_UNREACHABLE` / ECONNRESET to + * api.agnes-ai.cn) escaped as an uncaughtException and killed production. + * The request that triggered the fetch already fails through the normal + * error path; the stray copy delivered to nobody must not be process-fatal. + * + * The "fetch failed" message match is exact on purpose: it is undici's fixed + * wrapping message, so arbitrary TypeErrors still crash loudly. + * + * @param {unknown} err + * @returns {boolean} + */ +export function isUpstreamNetworkError(err) { + if (!err || typeof err !== "object") return false; + const e = /** @type {NodeJS.ErrnoException} */ (err); + if (e.name === "TypeError" && e.message === "fetch failed") return true; + switch (e.code) { + case "PROXY_UNREACHABLE": + case "UND_ERR_SOCKET": + case "UND_ERR_CONNECT_TIMEOUT": + case "UND_ERR_HEADERS_TIMEOUT": + case "UND_ERR_BODY_TIMEOUT": + case "ECONNREFUSED": + case "EHOSTUNREACH": + case "ENETUNREACH": + case "EAI_AGAIN": + return true; + default: + return false; + } +} + /** * Decide whether a process-level uncaughtException/unhandledRejection should be - * swallowed (benign client-abort) or allowed to surface (genuine bug). + * swallowed (benign client-abort, or a recoverable upstream timeout that a + * retry path already handles — #12861) or allowed to surface (genuine bug). * * Pure + exported so it can be unit-tested without poking process listeners. * @@ -75,7 +182,14 @@ export function isClientAbortError(err) { * @returns {boolean} true => swallow (log only), false => re-throw / let crash. */ export function shouldSwallowUncaught(err, origin) { - if (!isClientAbortError(err)) return false; + if ( + !isClientAbortError(err) && + !isRecoverableUpstreamTimeoutError(err) && + !isIntentionalComboAbort(err) && + !isUpstreamNetworkError(err) + ) { + return false; + } // Only swallow when the origin matches what the guard installed for. If some // other subsystem raised it (e.g. a deliberate `throw` in a domain), keep the // existing crash semantics. @@ -131,7 +245,9 @@ export function installProcessCrashGuard(log) { process.on("uncaughtException", (err, origin) => { if (shouldSwallowUncaught(err, origin)) { - logger("warn", "[server] swallowed client-abort uncaughtException:", err?.message ?? err); + // The warn line is the only evidence a swallowed error ever happened; + // pass the full error object so the stack survives. + logger("warn", "[server] swallowed benign uncaughtException:", err); return; } throw err; @@ -139,11 +255,7 @@ export function installProcessCrashGuard(log) { process.on("unhandledRejection", (reason) => { if (shouldSwallowUncaught(reason, "unhandledRejection")) { - logger( - "warn", - "[server] swallowed client-abort unhandledRejection:", - reason?.message ?? reason - ); + logger("warn", "[server] swallowed benign unhandledRejection:", reason); return; } throw reason; diff --git a/src/shared/validation/settingsSchemas.ts b/src/shared/validation/settingsSchemas.ts index a07651d587..3c2133f107 100644 --- a/src/shared/validation/settingsSchemas.ts +++ b/src/shared/validation/settingsSchemas.ts @@ -129,6 +129,12 @@ export const updateSettingsSchema = z.object({ blockedProviders: z.array(z.string().max(100)).optional(), noAuthFallbackDisabledProviders: z.array(z.string().max(100)).optional(), hidePaidModels: z.boolean().optional(), + // #9418/#13562: catalog/auto-combo already consume both flags (open-sse + // autoCombo + /v1/models catalog), but neither was ever added here — Zod + // silently strips unknown keys on a plain z.object, so PATCH /api/settings + // answered 200 while dropping both before they reached the DB. + hideAutoCombos: z.boolean().optional(), + hideNoThinkVariants: z.boolean().optional(), // STRICT_ZERO_COST (opt-in, default "off"): stricter than hidePaidModels — a // candidate must be keyless (no credential exists, so no request against it // can ever be billed) OR pass a live, fresh, hard-stop-guaranteed quota diff --git a/src/sse/services/auth.ts b/src/sse/services/auth.ts index f1f5cb9dfe..42554e6d30 100644 --- a/src/sse/services/auth.ts +++ b/src/sse/services/auth.ts @@ -1,5 +1,6 @@ import { randomUUID } from "crypto"; import { nodeTypeFromId } from "@/lib/db/providerNodeSelect"; +import { hydrateConnectionProviderSpecificData } from "./compatibleNodeBaseUrl.ts"; // #13452 import { extractGoogApiKeyHeader } from "./googApiKeyAuth.ts"; import { describeUpstreamFailure } from "@/shared/utils/upstreamError"; import { buildAllExpiredCredentials } from "./authExpiredCredentials.ts"; @@ -1039,33 +1040,6 @@ function planLastUsedCommit( }; } -/** - * Resolve Proxy Pool references on a real connection row at the same boundary - * where credentials become request-ready. The synthetic no-auth fallback above - * already performs this hydration, but a persisted connection (for example the - * OpenCode card's `opencode` row selected through the `opencode-zen` alias) - * bypasses that fallback. Keep inline/legacy entries untouched and only incur a - * registry lookup when at least one by-id reference is present. - */ -async function hydrateAccountProxyReferences( - providerSpecificData: JsonRecord -): Promise { - const entries = providerSpecificData.accountProxies; - if (!Array.isArray(entries)) return providerSpecificData; - - const containsProxyReference = entries.some((entry) => { - if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false; - const proxyId = (entry as Record).proxyId; - return typeof proxyId === "string" && proxyId.trim().length > 0; - }); - if (!containsProxyReference) return providerSpecificData; - - return { - ...providerSpecificData, - accountProxies: await resolveAccountProxiesFromRegistry(entries), - }; -} - async function materializeConnection( connection: ProviderConnectionView, options: CredentialSelectionOptions, @@ -1074,7 +1048,7 @@ async function materializeConnection( reactivatedFromInactive?: boolean; } = {} ) { - const providerSpecificData = await hydrateAccountProxyReferences(connection.providerSpecificData); + const providerSpecificData = await hydrateConnectionProviderSpecificData(connection); const apiKeyHealth = providerSpecificData.apiKeyHealth as Record | undefined; if (apiKeyHealth) syncHealthFromDB(connection.id, apiKeyHealth); const releaseOAuthSession = diff --git a/src/sse/services/compatibleNodeBaseUrl.ts b/src/sse/services/compatibleNodeBaseUrl.ts new file mode 100644 index 0000000000..a09f90a7f4 --- /dev/null +++ b/src/sse/services/compatibleNodeBaseUrl.ts @@ -0,0 +1,95 @@ +/** + * #13452: self-heal a `*-compatible-*` connection whose `providerSpecificData` + * never received the write-time `baseUrl` copy (`POST /api/providers`'s + * hydration branch, or the `PUT /api/provider-nodes/{id}` backfill loop are + * the only two places that stamp it). A connection created any other way — + * a direct DB insert, a row that predates the hydration logic, or a race + * with node update/deletion — has no `providerSpecificData.baseUrl` at all + * and, left unfixed, `DefaultExecutor.buildUrl()`/`BaseExecutor.buildUrl()` + * silently defaulted to the REAL OpenAI/Anthropic API, shipping the + * connection's own stored credential there as a Bearer/x-api-key token. + * + * Extracted out of `auth.ts` into its own module (`file-size-baseline.json` + * freezes `auth.ts`'s line count). + * + * @module sse/services/compatibleNodeBaseUrl + */ + +import { getCachedProviderNodes } from "@/lib/db/readCache"; +import { selectProviderNodeForConnection } from "@/lib/db/providerNodeSelect"; +import { isCompatibleProviderConnectionId } from "@/shared/utils/compatibleProviderId"; +import { resolveAccountProxiesFromRegistry } from "./noAuthProxyResolution"; + +type JsonRecord = Record; + +/** + * Composes both boundary-hydration steps a persisted connection's + * `providerSpecificData` needs before it becomes request-ready credentials: + * Proxy Pool by-id references (moved here from `auth.ts` verbatim — was + * `hydrateAccountProxyReferences`), then the `*-compatible-*` baseUrl + * self-heal below. Keep inline/legacy `accountProxies` entries untouched and + * only incur a proxy-registry lookup when at least one by-id reference is + * present. + */ +export async function hydrateConnectionProviderSpecificData(connection: { + provider: string; + providerSpecificData: JsonRecord; +}): Promise { + const { providerSpecificData } = connection; + const entries = providerSpecificData.accountProxies; + const containsProxyReference = + Array.isArray(entries) && + entries.some((entry) => { + if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false; + const proxyId = (entry as Record).proxyId; + return typeof proxyId === "string" && proxyId.trim().length > 0; + }); + const proxyHydrated = containsProxyReference + ? { ...providerSpecificData, accountProxies: await resolveAccountProxiesFromRegistry(entries) } + : providerSpecificData; + return hydrateCompatibleNodeBaseUrl(connection.provider, proxyHydrated); +} + +/** + * Re-joins `provider_nodes` (via the existing 5s-TTL cache, so this is not + * an extra DB read on the hot path) and stamps the same fields the write-time + * hydration branch stamps. Returns `providerSpecificData` unchanged when the + * connection is not a compatible-node connection, already carries a + * `baseUrl`, or no matching node can be resolved (the executor's fail-loud + * fallback then reports the true "node missing" condition instead of + * silently routing to a public third party). + */ +async function hydrateCompatibleNodeBaseUrl( + provider: string, + providerSpecificData: JsonRecord +): Promise { + if (typeof providerSpecificData.baseUrl === "string" && providerSpecificData.baseUrl) { + return providerSpecificData; + } + if (!isCompatibleProviderConnectionId(provider)) return providerSpecificData; + + try { + const nodes = (await getCachedProviderNodes()) as JsonRecord[]; + const node = selectProviderNodeForConnection(provider, nodes); + if (!node || typeof node.baseUrl !== "string" || !node.baseUrl) return providerSpecificData; + + return { + ...providerSpecificData, + prefix: providerSpecificData.prefix ?? node.prefix, + apiType: providerSpecificData.apiType ?? node.apiType, + baseUrl: node.baseUrl, + nodeName: providerSpecificData.nodeName ?? node.name, + ...(node.chatPath && !providerSpecificData.chatPath ? { chatPath: node.chatPath } : {}), + ...(node.modelsPath && !providerSpecificData.modelsPath + ? { modelsPath: node.modelsPath } + : {}), + ...(node.customHeaders && !providerSpecificData.customHeaders + ? { customHeaders: node.customHeaders } + : {}), + }; + } catch { + // Best-effort self-heal only — a transient DB/cache read failure must + // not throw here; the executor's own fail-loud fallback is the backstop. + return providerSpecificData; + } +} diff --git a/src/sse/services/tokenRefresh.ts b/src/sse/services/tokenRefresh.ts index f7e65b1173..eaf344f62a 100755 --- a/src/sse/services/tokenRefresh.ts +++ b/src/sse/services/tokenRefresh.ts @@ -2,7 +2,7 @@ import * as log from "../utils/logger"; import { updateProviderConnection } from "@/lib/db/providers"; import { resolveProxyForConnection } from "@/lib/db/settings"; -import { resolveProxyForProvider } from "@/lib/db/proxies"; +import { resolveProxyForProvider, hasBlockingProxyAssignment } from "@/lib/db/proxies"; import { TOKEN_EXPIRY_BUFFER_MS as BUFFER_MS, getRefreshLeadMs as _getRefreshLeadMs, @@ -29,12 +29,49 @@ import { export const TOKEN_EXPIRY_BUFFER_MS = BUFFER_MS; -async function resolveProxyForCredentials(provider: string, credentials?: any) { +/** + * #13470: mirrors the #6246 fail-closed policy (`src/sse/handlers/chatHelpers.ts` + * ::safeResolveProxy / decideProxyResolutionFailure) for the background + * token-refresh path. Duplicated verbatim here — importing decideProxyResolutionFailure + * from chatHelpers.ts would create an import cycle (chatHelpers.ts already imports + * updateProviderCredentials from this module). + */ +function decideTokenRefreshProxyFailure(err: unknown): null { + if ((process.env.PROXY_FAIL_OPEN ?? "").trim().toLowerCase() === "true") { + log.warn( + "PROXY", + `Token-refresh proxy resolution failed — PROXY_FAIL_OPEN=true, falling back to DIRECT: ${ + err instanceof Error ? err.message : String(err) + }` + ); + return null; + } + throw err instanceof Error ? err : new Error(String(err)); +} + +/** + * #13470: a connection whose assigned proxy pool has gone fully dead must not + * silently fall through to direct/env-proxy egress for its background refresh-token + * exchange — that is the same class of IP-provenance leak #6246 closed for the + * interactive chat/executor path (`safeResolveProxy`), on a more sensitive + * payload (the refresh token itself). Exported for direct testing. + */ +export async function resolveProxyForCredentials(provider: string, credentials?: any) { if (credentials?.connectionId) { const resolved = await resolveProxyForConnection(credentials.connectionId); if (resolved?.proxy) { return resolved.proxy; } + if (hasBlockingProxyAssignment(credentials.connectionId, provider)) { + return decideTokenRefreshProxyFailure( + Object.assign( + new Error( + "PROXY_ASSIGNED_UNAVAILABLE: assigned proxy is inactive/unreachable; refusing to egress background token refresh on a direct connection" + ), + { code: "PROXY_ASSIGNED_UNAVAILABLE" } + ) + ); + } } return resolveProxyForProvider(provider); diff --git a/src/types/databaseSettings.ts b/src/types/databaseSettings.ts index 2b4820e198..18495344ca 100644 --- a/src/types/databaseSettings.ts +++ b/src/types/databaseSettings.ts @@ -47,6 +47,7 @@ export interface DatabaseSettings { configAudit: number; a2aEvents: number; callLogs: number; + conversationTurnNodes: number; usageHistory: number; memoryEntries: number; domainCostHistory: number; @@ -81,6 +82,14 @@ export interface DatabaseSettings { lastVacuumAt: string | null; lastOptimizationAt: string | null; integrityCheck: "ok" | "error" | null; + /** + * #13432 — non-null while the configured `optimization.autoVacuumMode` + * has not yet been applied to the live SQLite file. Cleared once the + * vacuum scheduler's next scheduled run reconciles it. + */ + autoVacuumDrift: { configured: string; live: string } | null; + /** Pages freed by the most recent bounded `PRAGMA incremental_vacuum` batch, or null. */ + lastReclaimedPages: number | null; }; } @@ -118,6 +127,10 @@ export const DEFAULT_DATABASE_SETTINGS: Omit/dev/null || echo "") + if [ -n "$EXISTING" ]; then + gh issue close "$EXISTING" --repo "$GITHUB_REPOSITORY" --reason completed \ + --comment "✅ \`${TARGET}\` is release-green again at \`${GITHUB_SHA:0:9}\` — ${RUN_URL}. Auto-closed by Release-Green (continuous)." + echo "Closed issue #$EXISTING" + fi + + - name: Close tracking issue when the branch is green again + if: steps.validate.outputs.exit == '0' + env: + GH_TOKEN: ${{ github.token }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + # The open/update step above is the UPWARD half of the loop; without this + # step a stale "not green" issue outlives the fix and every base-green check + # (`AGENTS.md` → "Base-green check") keeps stamping new PRs as base-red inherited. + TITLE="🔴 main branch not green" + EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \ + --search "in:title $TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "") + if [ -n "$EXISTING" ]; then + gh issue close "$EXISTING" --repo "$GITHUB_REPOSITORY" --reason completed \ + --comment "✅ \`main\` is main-green again at \`${GITHUB_SHA:0:9}\` — ${RUN_URL}. Auto-closed by Release-Green (continuous)." + echo "Closed issue #$EXISTING" + fi + diff --git a/tests/fixtures/release-acceptance/plan-lint.json b/tests/fixtures/release-acceptance/plan-lint.json new file mode 100644 index 0000000000..6def6c2dc1 --- /dev/null +++ b/tests/fixtures/release-acceptance/plan-lint.json @@ -0,0 +1,24 @@ +{ + "schema_version": 1, + "identity": { + "repository": "diegosouzapw/OmniRoute", + "run_id": "1", + "run_attempt": 1, + "workflow": "release-acceptance.yml", + "trigger": "push", + "scope": "release", + "requested_ref": "refs/heads/release/v3.8.51", + "base_sha": "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e", + "candidate_sha": "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e", + "tested_sha": "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e" + }, + "required_gates": [ + { + "gate_id": "lint", + "suite_id": null, + "shard_index": null, + "shard_total": null + } + ], + "artifact": null +} diff --git a/tests/fixtures/release-acceptance/shadow-manifests/lint.json b/tests/fixtures/release-acceptance/shadow-manifests/lint.json new file mode 100644 index 0000000000..e4c632df67 --- /dev/null +++ b/tests/fixtures/release-acceptance/shadow-manifests/lint.json @@ -0,0 +1,51 @@ +{ + "schema_version": 1, + "identity": { + "repository": "diegosouzapw/OmniRoute", + "run_id": "1", + "run_attempt": 1, + "workflow": "release-acceptance.yml", + "trigger": "push", + "scope": "release", + "requested_ref": "refs/heads/release/v3.8.51", + "base_sha": "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e", + "candidate_sha": "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e", + "tested_sha": "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e" + }, + "required_gates": [ + { + "gate_id": "lint", + "suite_id": null, + "shard_index": null, + "shard_total": null + } + ], + "gates": [ + { + "gate_id": "lint", + "suite_id": null, + "shard_index": null, + "shard_total": null, + "tested_sha": "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e", + "run_id": "1", + "run_attempt": 1, + "command_id": "lint", + "gate_type": "static", + "status": "PASS", + "cause": null, + "exit_code": 0, + "duration_ms": 10, + "evidence": [ + { + "artifact_id": "logs", + "member": "lint.log", + "algorithm": "sha256", + "digest": "7f227db1653b6b723b07c8f2f6eb488f1f09e2f083ca7a3f5e02bbb274f5ff2e" + } + ] + } + ], + "evidence_errors": [], + "verdict": "VERIFIED", + "artifact": null +} diff --git a/tests/fixtures/release-acceptance/unverified-required-skipped.json b/tests/fixtures/release-acceptance/unverified-required-skipped.json new file mode 100644 index 0000000000..b99c18123c --- /dev/null +++ b/tests/fixtures/release-acceptance/unverified-required-skipped.json @@ -0,0 +1,63 @@ +{ + "schema_version": 1, + "identity": { + "repository": "diegosouzapw/OmniRoute", + "run_id": "1", + "run_attempt": 1, + "workflow": "release-acceptance.yml", + "trigger": "push", + "scope": "release", + "requested_ref": "refs/heads/release/v3.8.51", + "base_sha": "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e", + "candidate_sha": "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e", + "tested_sha": "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e" + }, + "required_gates": [ + { + "gate_id": "lint", + "suite_id": null, + "shard_index": null, + "shard_total": null + } + ], + "gates": [ + { + "gate_id": "lint", + "suite_id": null, + "shard_index": null, + "shard_total": null, + "tested_sha": "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e", + "run_id": "1", + "run_attempt": 1, + "command_id": "lint", + "gate_type": "static", + "status": "SKIPPED", + "cause": null, + "exit_code": null, + "duration_ms": 10, + "evidence": [ + { + "artifact_id": "logs", + "member": "lint.log", + "algorithm": "sha256", + "digest": "7f227db1653b6b723b07c8f2f6eb488f1f09e2f083ca7a3f5e02bbb274f5ff2e" + } + ], + "reason": "plan-optional-looking" + } + ], + "evidence_errors": [ + { + "code": "required_skipped", + "gate": { + "gate_id": "lint", + "suite_id": null, + "shard_index": null, + "shard_total": null + }, + "detail": "required gate SKIPPED" + } + ], + "verdict": "UNVERIFIED", + "artifact": null +} diff --git a/tests/fixtures/release-acceptance/verified.json b/tests/fixtures/release-acceptance/verified.json new file mode 100644 index 0000000000..e4c632df67 --- /dev/null +++ b/tests/fixtures/release-acceptance/verified.json @@ -0,0 +1,51 @@ +{ + "schema_version": 1, + "identity": { + "repository": "diegosouzapw/OmniRoute", + "run_id": "1", + "run_attempt": 1, + "workflow": "release-acceptance.yml", + "trigger": "push", + "scope": "release", + "requested_ref": "refs/heads/release/v3.8.51", + "base_sha": "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e", + "candidate_sha": "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e", + "tested_sha": "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e" + }, + "required_gates": [ + { + "gate_id": "lint", + "suite_id": null, + "shard_index": null, + "shard_total": null + } + ], + "gates": [ + { + "gate_id": "lint", + "suite_id": null, + "shard_index": null, + "shard_total": null, + "tested_sha": "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e", + "run_id": "1", + "run_attempt": 1, + "command_id": "lint", + "gate_type": "static", + "status": "PASS", + "cause": null, + "exit_code": 0, + "duration_ms": 10, + "evidence": [ + { + "artifact_id": "logs", + "member": "lint.log", + "algorithm": "sha256", + "digest": "7f227db1653b6b723b07c8f2f6eb488f1f09e2f083ca7a3f5e02bbb274f5ff2e" + } + ] + } + ], + "evidence_errors": [], + "verdict": "VERIFIED", + "artifact": null +} diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index 40a5d8c674..29ed15c3e7 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -111,6 +111,29 @@ "stream": "https://apihub.agnes-ai.com/v1/chat/completions" } }, + "agnes-cn": { + "format": "openai", + "headers": { + "apiKey": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "nonStream": { + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "oauth": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + } + }, + "url": { + "nonStream": "https://api.agnes-ai.cn/v1/chat/completions", + "stream": "https://api.agnes-ai.cn/v1/chat/completions" + } + }, "agy": { "format": "antigravity", "headers": { diff --git a/tests/unit/13679-insecure-default-password-nonloopback-login.test.ts b/tests/unit/13679-insecure-default-password-nonloopback-login.test.ts new file mode 100644 index 0000000000..e1737f0f2a --- /dev/null +++ b/tests/unit/13679-insecure-default-password-nonloopback-login.test.ts @@ -0,0 +1,116 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +/** + * Regression test for issue #13679 (PR D, item #5) — deploy manifests + * (`contrib/podman/omniroute.container`, `.env.example`) ship the well-known + * placeholder `INITIAL_PASSWORD=CHANGEME`. `ensurePersistentManagementPasswordHash()` + * already warns loudly on boot when the bootstrap password is this literal, but it + * does NOT stop a remote attacker who simply tries the well-known default from + * logging in over the network — only a local console warning fires. + * + * Fix: `/api/auth/login` now refuses a successful password match against a + * known-insecure default (e.g. "CHANGEME") when the request does not originate + * from loopback, forcing the operator to log in from localhost and rotate the + * password before the dashboard is reachable from the network with the + * default credential. + */ + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-13679d-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.JWT_SECRET = "test-jwt-secret-13679d"; + +const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD; + +const core = await import("../../src/lib/db/core.ts"); +const compliance = await import("../../src/lib/compliance/index.ts"); +const loginRoute = await import("../../src/app/api/auth/login/route.ts"); + +const originalGetCookieStore = loginRoute.authRouteInternals.getCookieStore; + +async function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); + process.env.INITIAL_PASSWORD = "CHANGEME"; +} + +test.beforeEach(async () => { + await resetStorage(); + loginRoute.authRouteInternals.getCookieStore = async () => ({ set() {} }); +}); + +test.afterEach(() => { + loginRoute.authRouteInternals.getCookieStore = originalGetCookieStore; +}); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + if (ORIGINAL_INITIAL_PASSWORD === undefined) { + delete process.env.INITIAL_PASSWORD; + } else { + process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD; + } +}); + +function postLogin(password: string, forwardedFor: string) { + return loginRoute.POST( + new Request("http://localhost/api/auth/login", { + method: "POST", + headers: { + "content-type": "application/json", + "x-forwarded-for": forwardedFor, + }, + body: JSON.stringify({ password }), + }) + ); +} + +test("a public-IP login with the well-known default password CHANGEME is rejected", async () => { + const response = await postLogin("CHANGEME", "203.0.113.77"); + + assert.equal( + response.status, + 403, + "a remote attacker guessing the well-known default INITIAL_PASSWORD must not be able to " + + "authenticate — only a console warning fires today, which is not a real control" + ); + assert.equal(response.headers.get("set-cookie"), null, "no session cookie must be issued"); + + const [entry] = compliance.getAuditLog({ + action: "auth.login.insecure_default_blocked", + limit: 1, + }); + assert.ok( + entry, + "expected an auth.login.insecure_default_blocked audit entry for the blocked attempt" + ); +}); + +test("a loopback login with the well-known default password CHANGEME still succeeds", async () => { + const response = await postLogin("CHANGEME", "127.0.0.1"); + + assert.equal( + response.status, + 200, + "the operator must still be able to bootstrap/rotate the password from loopback" + ); + const body = await response.json(); + assert.equal(body.success, true); +}); + +test("a public-IP login with a non-default (rotated) password still succeeds", async () => { + process.env.INITIAL_PASSWORD = "a-real-rotated-password-13679d"; + const response = await postLogin("a-real-rotated-password-13679d", "203.0.113.77"); + + assert.equal( + response.status, + 200, + "the insecure-default check must not block legitimate remote logins once the password " + + "has actually been rotated away from the well-known default" + ); +}); diff --git a/tests/unit/13679-podman-manifest-no-literal-secrets.test.ts b/tests/unit/13679-podman-manifest-no-literal-secrets.test.ts new file mode 100644 index 0000000000..ac023a0c6a --- /dev/null +++ b/tests/unit/13679-podman-manifest-no-literal-secrets.test.ts @@ -0,0 +1,54 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +/** + * Regression test for issue #13679 (PR D, item #4) — `contrib/podman/omniroute.container` + * used to ship copy-pasteable placeholder secrets: + * JWT_SECRET=change-me-to-a-random-base64-string + * API_KEY_SECRET=change-me-to-a-random-base64-string + * INITIAL_PASSWORD=change-me-to-a-random-hex-string + * + * An operator who forgot to replace them ran production with a public, guessable + * JWT/API-key secret and dashboard password (anyone can find these literals on GitHub). + * The fix removes the literal `Environment=` lines for these three variables and + * documents generating real values via `contrib/podman/README.md` before first start. + */ + +const MANIFEST_PATH = path.join( + import.meta.dirname, + "..", + "..", + "contrib", + "podman", + "omniroute.container" +); +const README_PATH = path.join(import.meta.dirname, "..", "..", "contrib", "podman", "README.md"); + +test("omniroute.container no longer ships a literal placeholder secret value", () => { + const content = fs.readFileSync(MANIFEST_PATH, "utf8"); + + assert.ok( + !/change-me-to-a-random/i.test(content), + "the Quadlet unit must not ship a copy-pasteable placeholder secret literal" + ); + + for (const varName of ["JWT_SECRET", "API_KEY_SECRET", "INITIAL_PASSWORD"]) { + const literalAssignment = new RegExp(`^Environment=${varName}=\\S+`, "m"); + assert.ok( + !literalAssignment.test(content), + `${varName} must not be assigned a literal value directly in the checked-in unit file` + ); + } +}); + +test("podman README documents generating secrets before first start", () => { + const readme = fs.readFileSync(README_PATH, "utf8"); + assert.match( + readme, + /Generate secrets before first start/i, + "README must document the generate-secrets step referenced by the unit file's comments" + ); + assert.match(readme, /openssl rand/, "README must give a concrete generation command"); +}); diff --git a/tests/unit/agnes-cn-provider.test.ts b/tests/unit/agnes-cn-provider.test.ts new file mode 100644 index 0000000000..62ceea8747 --- /dev/null +++ b/tests/unit/agnes-cn-provider.test.ts @@ -0,0 +1,282 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import os from "node:os"; +import DefaultExecutor from "../../open-sse/executors/default.ts"; +import { VIDEO_PROVIDER_IDS } from "../../src/shared/constants/providers.ts"; + +const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); + +function readRepo(rel: string): string { + return fs.readFileSync(path.join(REPO_ROOT, rel), "utf8"); +} + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-agnes-cn-provider-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); +const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts"); +const { IMAGE_PROVIDERS } = await import("../../open-sse/config/imageRegistry.ts"); +const { FREE_MODEL_BUDGETS } = await import("../../open-sse/config/freeModelCatalog.ts"); +const { resolveProviderAlias, parseModel } = await import("../../open-sse/services/model.ts"); +const { sanitizeReasoningEffortForProvider } = + await import("../../open-sse/executors/base/reasoningEffort.ts"); +const { isNamedOpenAIStyleProvider } = + await import("../../src/app/api/providers/[id]/models/discovery/providerSets.ts"); +const { getDiscoveryClass } = await import("../../src/lib/providerModels/discoveryClass.ts"); +const { createProviderConnection } = await import("../../src/lib/db/providers.ts"); +const { getProviderCredentials } = await import("../../src/sse/services/auth.ts"); +const dbCore = await import("../../src/lib/db/core.ts"); + +const CN_CHAT_URL = "https://api.agnes-ai.cn/v1/chat/completions"; +const INTL_CHAT_URL = "https://apihub.agnes-ai.com/v1/chat/completions"; + +test.after(() => { + dbCore.closeDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +function extractNamedConstLiteral(source: string, name: string): string { + const marker = "const " + name; + const start = source.indexOf(marker); + assert.notEqual(start, -1, `${name} declaration missing`); + const eq = source.indexOf("=", start + marker.length); + assert.notEqual(eq, -1, `${name} has no initializer`); + let i = eq + 1; + while (i < source.length && source[i] !== "[" && source[i] !== "{") i += 1; + assert.ok(source[i] === "[" || source[i] === "{", `${name} must be an array or object literal`); + let depth = 0; + for (let j = i; j < source.length; j += 1) { + const ch = source[j]; + if (ch === "[" || ch === "{") depth += 1; + else if (ch === "]" || ch === "}") { + depth -= 1; + if (depth === 0) return source.slice(i, j + 1); + } + } + assert.fail(`${name} literal is not closed`); +} + +function hasAgnesCnPairing(literal: string): boolean { + return /["']agnes["']/.test(literal) && /["']agnes-cn["']/.test(literal); +} + +test("agnes-cn registry baseUrl is the China host and agnes stays on apihub", () => { + assert.equal(REGISTRY["agnes-cn"].baseUrl, CN_CHAT_URL); + assert.equal(REGISTRY["agnes"].baseUrl, INTL_CHAT_URL); +}); + +test("agnes-cn aliases resolve without folding into agnes", () => { + assert.equal(resolveProviderAlias("agnescn"), "agnes-cn"); + assert.equal(resolveProviderAlias("agnes-cn"), "agnes-cn"); + assert.equal(resolveProviderAlias("agnes"), "agnes"); +}); + +test("parseModel keeps agnes-cn and agnes prefixes apart", () => { + const cn = parseModel("agnes-cn/agnes-2.0-flash"); + assert.equal(cn.provider, "agnes-cn"); + assert.equal(cn.model, "agnes-2.0-flash"); + + const intl = parseModel("agnes/agnes-2.0-flash"); + assert.equal(intl.provider, "agnes"); + assert.equal(intl.model, "agnes-2.0-flash"); +}); + +test("DefaultExecutor buildUrl uses the China host for agnes-cn without credentials", () => { + const cnUrl = new DefaultExecutor("agnes-cn").buildUrl("agnes-2.0-flash", false); + const intlUrl = new DefaultExecutor("agnes").buildUrl("agnes-2.0-flash", false); + assert.equal(cnUrl, CN_CHAT_URL); + assert.equal(intlUrl, INTL_CHAT_URL); +}); + +test("agnes-cn is a named OpenAI-style provider with openai-compat discovery", () => { + assert.equal(isNamedOpenAIStyleProvider("agnes-cn"), true); + assert.equal(getDiscoveryClass("agnes-cn"), "openai-compat"); +}); + +test("PROVIDER_SEARCH_PAIRS and CATALOG_SIBLING_IDS do not pair agnes with agnes-cn", () => { + const pairs = extractNamedConstLiteral( + readRepo("src/sse/services/auth.ts"), + "PROVIDER_SEARCH_PAIRS" + ); + const siblings = extractNamedConstLiteral( + readRepo("src/lib/db/models/activeSyncedCatalog.ts"), + "CATALOG_SIBLING_IDS" + ); + + assert.equal(hasAgnesCnPairing(pairs), false); + assert.equal(hasAgnesCnPairing(siblings), false); + assert.doesNotMatch(pairs, /["']agnes-cn["']/); + assert.doesNotMatch(siblings, /["']agnes-cn["']/); + assert.doesNotMatch(pairs, /["']agnes["']/); + assert.doesNotMatch(siblings, /["']agnes["']/); + + const injectedPairs = pairs.replace(/\]\s*$/, ' ["agnes", "agnes-cn"],\n]'); + assert.equal(hasAgnesCnPairing(injectedPairs), true); +}); + +test("getProviderCredentials does not return the other Agnes region's connection", async () => { + await createProviderConnection({ + provider: "agnes", + authType: "apikey", + apiKey: "agnes-intl-fixture-key", + isActive: true, + testStatus: "active", + }); + await createProviderConnection({ + provider: "agnes-cn", + authType: "apikey", + apiKey: "agnes-cn-fixture-key", + isActive: true, + testStatus: "active", + }); + + const cnCreds = await getProviderCredentials("agnes-cn"); + assert.ok(cnCreds, "agnes-cn must resolve a credential"); + assert.equal(cnCreds.provider, "agnes-cn"); + assert.notEqual(cnCreds.apiKey, "agnes-intl-fixture-key"); + + const intlCreds = await getProviderCredentials("agnes"); + assert.ok(intlCreds, "agnes must resolve a credential"); + assert.equal(intlCreds.provider, "agnes"); + assert.notEqual(intlCreds.apiKey, "agnes-cn-fixture-key"); +}); + +test("agnes-cn is absent from image and video registries", () => { + assert.equal(IMAGE_PROVIDERS["agnes-cn"], undefined); + assert.equal(VIDEO_PROVIDER_IDS.has("agnes-cn"), false); +}); + +test("IMAGE_PROVIDERS agnes-cn absence lock can go red", () => { + assert.equal(IMAGE_PROVIDERS["agnes-cn"], undefined); + const stub = { id: "agnes-cn", models: [] }; + IMAGE_PROVIDERS["agnes-cn"] = stub as (typeof IMAGE_PROVIDERS)[string]; + try { + assert.ok(IMAGE_PROVIDERS["agnes-cn"], "injected agnes-cn image entry must be defined"); + } finally { + delete IMAGE_PROVIDERS["agnes-cn"]; + } + assert.equal(IMAGE_PROVIDERS["agnes-cn"], undefined); + + assert.equal(VIDEO_PROVIDER_IDS.has("agnes-cn"), false); + VIDEO_PROVIDER_IDS.add("agnes-cn"); + try { + assert.equal(VIDEO_PROVIDER_IDS.has("agnes-cn"), true); + } finally { + VIDEO_PROVIDER_IDS.delete("agnes-cn"); + } + assert.equal(VIDEO_PROVIDER_IDS.has("agnes-cn"), false); +}); + +test("agnes-cn free catalog three rows agnes-cn-free pool with agnes-3.0-flash", () => { + const rows = FREE_MODEL_BUDGETS.filter((model) => model.provider === "agnes-cn"); + assert.equal(rows.length, 3); + assert.ok(rows.every((model) => model.poolKey === "agnes-cn-free")); + assert.equal( + rows.some((model) => model.modelId === "agnes-3.0-flash"), + true + ); + assert.equal( + rows.some((model) => model.modelId === "agnes-1.5-flash"), + false + ); +}); + +test("agnes-cn declares the live effort vocabulary per generation", () => { + const models = REGISTRY["agnes-cn"].models; + for (const id of ["agnes-2.0-flash", "agnes-2.5-flash"]) { + const m = models.find((entry: { id: string }) => entry.id === id); + assert.ok(m, id + " must be in the agnes-cn registry"); + assert.deepEqual(m.supportedThinkingEfforts, ["none", "low", "medium", "high", "max"]); + } + const flash30 = models.find((entry: { id: string }) => entry.id === "agnes-3.0-flash"); + assert.ok(flash30, "agnes-3.0-flash must be in the agnes-cn registry"); + assert.deepEqual(flash30.supportedThinkingEfforts, [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max", + ]); +}); + +test("agnes-cn sanitizer clamps Hermes xhigh to the ceiling and off to none", () => { + const clamp = (model: string, effort: string) => + ( + sanitizeReasoningEffortForProvider({ reasoning_effort: effort }, "agnes-cn", model) as { + reasoning_effort?: string; + } + ).reasoning_effort; + + assert.equal(clamp("agnes-2.0-flash", "xhigh"), "max"); + assert.equal(clamp("agnes-2.5-flash", "xhigh"), "max"); + assert.equal(clamp("agnes-2.0-flash", "off"), "none"); + assert.equal(clamp("agnes-2.5-flash", "off"), "none"); + assert.equal(clamp("agnes-2.0-flash", "minimal"), "low"); + assert.equal(clamp("agnes-2.5-flash", "minimal"), "low"); + assert.equal(clamp("agnes-3.0-flash", "xhigh"), "xhigh"); + assert.equal(clamp("agnes-3.0-flash", "minimal"), "minimal"); + assert.equal(clamp("agnes-3.0-flash", "off"), "none"); +}); + +test("agnes-cn registry seed lists 3.0 and not retired 1.5", () => { + const ids = REGISTRY["agnes-cn"].models.map((model: { id: string }) => model.id); + assert.equal(ids.includes("agnes-3.0-flash"), true); + assert.equal(ids.includes("agnes-1.5-flash"), false); + assert.equal(ids.includes("agnes-2.0-flash"), true); + assert.equal(ids.includes("agnes-2.5-flash"), true); +}); + +test("agnes-cn dashboard card name includes China and is not hidden", () => { + const entry = APIKEY_PROVIDERS["agnes-cn"]; + assert.ok(entry, "APIKEY_PROVIDERS['agnes-cn'] must be defined"); + assert.equal(typeof entry.name, "string"); + assert.match(entry.name, /China/); + assert.notEqual(entry.hiddenFromDashboard, true); + assert.equal(typeof entry.freeNote, "string", "agnes-cn hasFree card must explain the free tier"); + assert.ok((entry.freeNote as string).length > 0); +}); + +test("agnes-cn translate-path golden records China host", () => { + const snapshot = JSON.parse(readRepo("tests/snapshots/provider/translate-path.json")); + assert.equal(snapshot["agnes-cn"].url.stream, CN_CHAT_URL); + assert.equal(snapshot["agnes-cn"].url.nonStream, CN_CHAT_URL); + assert.equal(snapshot.agnes.url.stream, INTL_CHAT_URL); + assert.equal(snapshot.agnes.url.nonStream, INTL_CHAT_URL); +}); + +test("every i18n locale has a nonempty agnes-cn onboarding description", () => { + const messagesDir = path.join(REPO_ROOT, "src/i18n/messages"); + const files = fs.readdirSync(messagesDir).filter((file) => file.endsWith(".json")); + // 66 locales as of 2026-09 (config/i18n.json). This was 51 in the original PR — + // 15 locales (am, ha, hy, ig, ka, km, kn, ml, my, ne, or, pa, si, uz, yo) never got + // the key, which is exactly the i18n-new-key-coverage CI gate this fixes. + assert.equal(files.length, 66); + + for (const file of files) { + const messages = JSON.parse(readRepo(`src/i18n/messages/${file}`)); + const desc = messages?.providers?.onboardingProviderDescriptions?.["agnes-cn"]; + assert.equal( + typeof desc, + "string", + `${file} missing providers.onboardingProviderDescriptions['agnes-cn']` + ); + assert.ok(desc.length > 0, `${file} agnes-cn onboarding description is empty`); + } + + const sample = JSON.parse(readRepo("src/i18n/messages/en.json")); + const injected = structuredClone(sample); + if (injected.providers?.onboardingProviderDescriptions) { + delete injected.providers.onboardingProviderDescriptions["agnes-cn"]; + } + const injectedDesc = injected?.providers?.onboardingProviderDescriptions?.["agnes-cn"]; + assert.equal( + typeof injectedDesc === "string" && injectedDesc.length > 0, + false, + "deleting one locale key must make the i18n lock fail" + ); +}); diff --git a/tests/unit/antigravityUpstreamError.test.ts b/tests/unit/antigravityUpstreamError.test.ts new file mode 100644 index 0000000000..4329b4fb8d --- /dev/null +++ b/tests/unit/antigravityUpstreamError.test.ts @@ -0,0 +1,75 @@ +// Regression coverage for issue #13591: Antigravity double-wraps its own errors, so +// parseUpstreamError() (the shared chatCore failure-classification path) only ever saw +// the generic "Antigravity upstream error (400)" template instead of the real Gemini +// upstream detail buried under `upstream_details`. buildAntigravityUpstreamError() must +// surface the real upstream message as `error.message` directly. +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { buildAntigravityUpstreamError } from "../../open-sse/executors/antigravityUpstreamError.ts"; +import { parseUpstreamError } from "../../open-sse/utils/error.ts"; + +test("issue #13591: parseUpstreamError surfaces the real upstream detail for an Antigravity-wrapped 400", async () => { + const rawUpstreamGeminiBody = JSON.stringify({ + error: { + code: 400, + message: + "Invalid value at 'tools[0].function_declarations[0].parameters.properties[0].value' " + + '(type.googleapis.com/google.ai.generativelanguage.v1beta.Schema), "string"', + status: "INVALID_ARGUMENT", + }, + }); + + const wrappedErrorBody = buildAntigravityUpstreamError(400, "", rawUpstreamGeminiBody); + + assert.ok( + JSON.stringify(wrappedErrorBody).includes("function_declarations"), + "sanity: buildAntigravityUpstreamError should embed the real upstream detail somewhere in the body" + ); + assert.ok( + (wrappedErrorBody as { error: { message: string } }).error.message.includes( + "function_declarations" + ), + "buildAntigravityUpstreamError should surface the real upstream detail directly in error.message" + ); + + const wrappedResponse = new Response(JSON.stringify(wrappedErrorBody), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + + const parsed = await parseUpstreamError(wrappedResponse, "antigravity"); + + assert.ok( + parsed.message.includes("function_declarations"), + `expected parseUpstreamError to surface the real upstream detail, but got: ${JSON.stringify(parsed.message)}` + ); +}); + +test("issue #13591: geo-blocked branch keeps its explicit hint message untouched", () => { + const geoBlockedBody = JSON.stringify({ + error: { + message: "User location is not supported for the API use.", + }, + }); + + const wrappedErrorBody = buildAntigravityUpstreamError(400, "Bad Request", geoBlockedBody) as { + error: { message: string }; + }; + + assert.ok( + wrappedErrorBody.error.message.includes( + "not offered from this server's current egress location" + ), + "geo-blocked responses must keep the operator-facing hint, not the raw upstream text" + ); +}); + +test("issue #13591: non-JSON upstream body falls back to the generic templated message without throwing", () => { + const htmlErrorPage = "502 Bad Gateway"; + + const wrappedErrorBody = buildAntigravityUpstreamError(502, "Bad Gateway", htmlErrorPage) as { + error: { message: string }; + }; + + assert.equal(wrappedErrorBody.error.message, "Antigravity upstream error (502): Bad Gateway"); +}); diff --git a/tests/unit/batches-delete-completed-route-scope.test.ts b/tests/unit/batches-delete-completed-route-scope.test.ts index 76233cac5c..9f0a6b4ff6 100644 --- a/tests/unit/batches-delete-completed-route-scope.test.ts +++ b/tests/unit/batches-delete-completed-route-scope.test.ts @@ -93,6 +93,7 @@ async function callDelete(headers: Record, url: string = ROUTE_U deleted?: boolean; deletedBatches?: number; deletedFiles?: number; + hasMore?: boolean; error?: { message: string; type?: string; code?: string }; }; return { res, body }; @@ -115,6 +116,11 @@ describe("DELETE /api/v1/batches/delete-completed — caller scope (GHSA-wvxc-jp assert.strictEqual(body.deleted, true); assert.strictEqual(body.deletedBatches, 0, "key A owns no completed batch — nothing to sweep"); assert.strictEqual(body.deletedFiles, 0); + assert.strictEqual( + body.hasMore, + false, + "the response must surface deleteCompletedBatches' hasMore continuation flag (#13680)" + ); assert.ok(getBatch(victim.batch.id), "key B's completed batch must survive key A's sweep"); assert.strictEqual( getFileContent(victim.file.id)?.toString(), diff --git a/tests/unit/cdp-proxy-auth-gate-13679.test.ts b/tests/unit/cdp-proxy-auth-gate-13679.test.ts new file mode 100644 index 0000000000..13dfa946c6 --- /dev/null +++ b/tests/unit/cdp-proxy-auth-gate-13679.test.ts @@ -0,0 +1,140 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import http from "node:http"; +import { spawn, type ChildProcess } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const PROXY_SCRIPT = path.resolve( + __dirname, + "../../docker/chatgpt-web-codex-browser/cdp-proxy.mjs" +); +const UPSTREAM_PORT = 9222; // upstreamPort in cdp-proxy.mjs +const PROXY_PORT = 9223; // listenPort in cdp-proxy.mjs +const TOKEN = "test-secret-token-13679"; +const TOKEN_HEADER = "X-Omni-Cdp-Token"; + +// #13679 item #9: docker/chatgpt-web-codex-browser/cdp-proxy.mjs republishes +// Chromium's loopback CDP (127.0.0.1:9222) onto 0.0.0.0:9223 with NO auth +// check at all — unlike the sibling docker/vnc-browser/chromium/cdp-bridge.py, +// which requires an `X-Omni-Cdp-Token` header once CDP_BRIDGE_TOKEN is set +// (#12571). This proves cdp-proxy.mjs must gate requests the same way once an +// operator opts in via CDP_PROXY_TOKEN. + +function waitForListening(server: http.Server): Promise { + return new Promise((resolve, reject) => { + server.once("listening", () => resolve()); + server.once("error", reject); + }); +} + +function waitForProxyReady(child: ChildProcess): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout( + () => reject(new Error("cdp-proxy.mjs did not report ready in time")), + 5000 + ); + child.stderr?.on("data", (chunk: Buffer) => { + if (chunk.toString("utf8").includes("listening on")) { + clearTimeout(timer); + resolve(); + } + }); + child.once("error", (err) => { + clearTimeout(timer); + reject(err); + }); + child.once("exit", (code) => { + clearTimeout(timer); + reject(new Error(`cdp-proxy.mjs exited early with code ${code}`)); + }); + }); +} + +function startUpstream(): Promise<{ server: http.Server; receivedAnyRequest: () => boolean }> { + let received = false; + const server = http.createServer((_req, res) => { + received = true; + // Real Chromium CDP responses always carry Content-Length (never + // chunked) — match that here, since cdp-proxy.mjs blindly re-adds a + // computed content-length on top of whatever the upstream sent. + const body = "{}"; + res.writeHead(200, { + "content-type": "application/json", + "content-length": String(body.length), + }); + res.end(body); + }); + return waitForListening(server.listen(UPSTREAM_PORT, "127.0.0.1")).then(() => ({ + server, + receivedAnyRequest: () => received, + })); +} + +function startProxy(): ChildProcess { + return spawn(process.execPath, [PROXY_SCRIPT], { + stdio: ["ignore", "ignore", "pipe"], + env: { ...process.env, CDP_PROXY_TOKEN: TOKEN }, + }); +} + +function requestProxy(headers: Record): Promise { + return new Promise((resolve) => { + const req = http.request( + { host: "127.0.0.1", port: PROXY_PORT, path: "/json/version", method: "GET", headers }, + (res) => { + res.resume(); + resolve(res.statusCode ?? null); + } + ); + req.on("error", () => resolve(null)); + req.end(); + }); +} + +test("cdp-proxy.mjs must reject a request with no CDP token once CDP_PROXY_TOKEN is set (#13679)", async () => { + const upstream = await startUpstream(); + const proxy = startProxy(); + + try { + await waitForProxyReady(proxy); + const status = await requestProxy({}); + + assert.notEqual( + status, + 200, + "cdp-proxy.mjs forwarded an unauthenticated request straight through to Chromium's CDP " + + "port even though CDP_PROXY_TOKEN was set — the proxy has no auth gate at all" + ); + assert.equal( + upstream.receivedAnyRequest(), + false, + "cdp-proxy.mjs must not forward the request upstream before checking the CDP token" + ); + } finally { + proxy.kill("SIGKILL"); + await new Promise((resolve) => upstream.server.close(() => resolve())); + } +}); + +test("cdp-proxy.mjs forwards the request once the caller presents the configured token (#13679)", async () => { + const upstream = await startUpstream(); + const proxy = startProxy(); + + try { + await waitForProxyReady(proxy); + const status = await requestProxy({ [TOKEN_HEADER]: TOKEN }); + + assert.equal( + status, + 200, + "cdp-proxy.mjs should forward the request once the caller presents the correct " + + "CDP_PROXY_TOKEN" + ); + assert.equal(upstream.receivedAnyRequest(), true); + } finally { + proxy.kill("SIGKILL"); + await new Promise((resolve) => upstream.server.close(() => resolve())); + } +}); diff --git a/tests/unit/chat-admission-selfloop-random-bearer-13679.test.ts b/tests/unit/chat-admission-selfloop-random-bearer-13679.test.ts new file mode 100644 index 0000000000..1a457b8729 --- /dev/null +++ b/tests/unit/chat-admission-selfloop-random-bearer-13679.test.ts @@ -0,0 +1,85 @@ +// #13679 (PR C): the self-loop admission bypass bearer must never fall back to the +// predictable literal "sk_omniroute" when OMNIROUTE_API_KEY/ROUTER_API_KEY are unset. +// +// Root cause: `resolveSelfLoopBearer()` in chatAdmissionIdentity.ts returned the checked-in +// literal `"sk_omniroute"` as its final fallback. Anyone who read the source (or the public +// repo) knew this value and could send `x-omniroute-admission-bypass: internal` + +// `Authorization: Bearer sk_omniroute` to skip the heavyweight admission/queueing lease — +// not an auth bypass (see chatBodyAdmission.ts::admitChatRequest), but still a predictable +// shared secret that should not be a hardcoded literal. +import test from "node:test"; +import assert from "node:assert/strict"; + +const { resolveSelfLoopBearer } = + await import("../../src/shared/middleware/chatAdmissionIdentity.ts"); + +const SELF_LOOP_ENV_KEYS = ["OMNIROUTE_API_KEY", "ROUTER_API_KEY"] as const; +function withSelfLoopEnv(env: Partial>) { + const saved = new Map(); + for (const key of SELF_LOOP_ENV_KEYS) { + saved.set(key, process.env[key]); + if (env[key] === undefined) delete process.env[key]; + else process.env[key] = env[key]; + } + return () => { + for (const key of SELF_LOOP_ENV_KEYS) { + const value = saved.get(key); + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + }; +} + +test("resolveSelfLoopBearer never falls back to the predictable sk_omniroute literal", () => { + const restore = withSelfLoopEnv({}); + try { + const bearer = resolveSelfLoopBearer(); + assert.notEqual( + bearer, + "sk_omniroute", + "resolveSelfLoopBearer() fell back to the checked-in literal — a shared secret anyone " + + "reading the source knows, instead of a per-process random value" + ); + } finally { + restore(); + } +}); + +test("resolveSelfLoopBearer's generated fallback is stable within the same process", () => { + const restore = withSelfLoopEnv({}); + try { + const first = resolveSelfLoopBearer(); + const second = resolveSelfLoopBearer(); + assert.equal( + first, + second, + "the generated self-loop bearer must be memoized for the process lifetime — the same " + + "in-process caller (audioBridgeHelpers/visionBridgeHelpers) and verifier " + + "(isInternalAdmissionBypass) must agree on the value" + ); + } finally { + restore(); + } +}); + +test("resolveSelfLoopBearer's generated fallback has enough entropy to resist guessing", () => { + const restore = withSelfLoopEnv({}); + try { + const bearer = resolveSelfLoopBearer(); + assert.ok( + bearer.length >= 32, + `generated self-loop bearer is too short to be a random secret: "${bearer}" (${bearer.length} chars)` + ); + } finally { + restore(); + } +}); + +test("resolveSelfLoopBearer still prefers OMNIROUTE_API_KEY over the generated fallback", () => { + const restore = withSelfLoopEnv({ OMNIROUTE_API_KEY: "omni-key" }); + try { + assert.equal(resolveSelfLoopBearer(), "omni-key"); + } finally { + restore(); + } +}); diff --git a/tests/unit/chat-body-admission.test.ts b/tests/unit/chat-body-admission.test.ts index b7867bc069..e291d37d30 100644 --- a/tests/unit/chat-body-admission.test.ts +++ b/tests/unit/chat-body-admission.test.ts @@ -792,10 +792,15 @@ test("external clients cannot use the bypass header without a trusted self-loop // ── self-loop bearer resolution (env-key aware, #1350) ───────────────── -test("resolveSelfLoopBearer falls back to sk_omniroute when no env key is set", () => { +test("resolveSelfLoopBearer falls back to a random per-process secret when no env key is set (#13679)", () => { const restore = withSelfLoopEnv({}); try { - assert.equal(resolveSelfLoopBearer(), "sk_omniroute"); + // #13679 PR C: the fallback must NOT be the predictable checked-in literal + // "sk_omniroute" — it is a per-process random value (dedicated regression test: + // tests/unit/chat-admission-selfloop-random-bearer-13679.test.ts). + const bearer = resolveSelfLoopBearer(); + assert.notEqual(bearer, "sk_omniroute"); + assert.equal(bearer, resolveSelfLoopBearer(), "must be memoized for the process lifetime"); } finally { restore(); } diff --git a/tests/unit/cli-mcp-enable-disable-13012.test.ts b/tests/unit/cli-mcp-enable-disable-13012.test.ts new file mode 100644 index 0000000000..0f43e483de --- /dev/null +++ b/tests/unit/cli-mcp-enable-disable-13012.test.ts @@ -0,0 +1,95 @@ +// Regression for GitHub issue #13012 (Bug 2): there was no CLI/env path to +// flip the mcpEnabled setting on — it was dashboard-only. Pins the new +// `omniroute mcp enable`/`mcp disable` PATCH /api/settings body shape. +import test from "node:test"; +import assert from "node:assert/strict"; + +const ORIGINAL_FETCH = globalThis.fetch; + +function makeResp(data: unknown, status = 200) { + return { + ok: status < 400, + status, + json: () => Promise.resolve(data), + text: () => Promise.resolve(JSON.stringify(data)), + headers: new Headers({ "content-type": "application/json" }), + }; +} + +type Call = { url: string; method: string; body: unknown }; + +function mockFetch(calls: Call[]) { + globalThis.fetch = (async (url: string, opts: Record = {}) => { + const u = String(url); + const method = String(opts.method || "GET").toUpperCase(); + const body = opts.body ? JSON.parse(String(opts.body)) : null; + calls.push({ url: u, method, body }); + if (u.includes("/api/health")) return makeResp({ status: "ok" }) as unknown as Response; + if (u.includes("/api/settings")) return makeResp({ ok: true }) as unknown as Response; + return makeResp({ error: "unexpected call" }, 404) as unknown as Response; + }) as typeof fetch; +} + +async function withMockedFetch(fn: (calls: Call[]) => Promise) { + const calls: Call[] = []; + mockFetch(calls); + const originalLog = console.log; + const originalError = console.error; + console.log = () => {}; + console.error = () => {}; + try { + await fn(calls); + } finally { + console.log = originalLog; + console.error = originalError; + globalThis.fetch = ORIGINAL_FETCH; + } +} + +test("mcp enable PATCHes /api/settings with mcpEnabled: true", async () => { + await withMockedFetch(async (calls) => { + const { runMcpEnableCommand } = await import("../../bin/cli/commands/mcp.mjs"); + const result = await runMcpEnableCommand({}); + assert.equal(result, 0); + + const patchCall = calls.find((c) => c.url.includes("/api/settings") && c.method === "PATCH"); + assert.ok(patchCall, "expected a PATCH /api/settings call"); + assert.deepEqual(patchCall!.body, { mcpEnabled: true }); + }); +}); + +test("mcp enable --transport sse also sets mcpTransport in the same PATCH", async () => { + await withMockedFetch(async (calls) => { + const { runMcpEnableCommand } = await import("../../bin/cli/commands/mcp.mjs"); + const result = await runMcpEnableCommand({ transport: "sse" }); + assert.equal(result, 0); + + const patchCall = calls.find((c) => c.url.includes("/api/settings") && c.method === "PATCH"); + assert.ok(patchCall); + assert.deepEqual(patchCall!.body, { mcpEnabled: true, mcpTransport: "sse" }); + }); +}); + +test("mcp enable rejects an invalid --transport value without calling the API", async () => { + await withMockedFetch(async (calls) => { + const { runMcpEnableCommand } = await import("../../bin/cli/commands/mcp.mjs"); + const result = await runMcpEnableCommand({ transport: "bogus" }); + assert.equal(result, 1); + assert.ok( + !calls.some((c) => c.url.includes("/api/settings")), + "invalid transport must not reach the settings API" + ); + }); +}); + +test("mcp disable PATCHes /api/settings with mcpEnabled: false", async () => { + await withMockedFetch(async (calls) => { + const { runMcpDisableCommand } = await import("../../bin/cli/commands/mcp.mjs"); + const result = await runMcpDisableCommand({}); + assert.equal(result, 0); + + const patchCall = calls.find((c) => c.url.includes("/api/settings") && c.method === "PATCH"); + assert.ok(patchCall, "expected a PATCH /api/settings call"); + assert.deepEqual(patchCall!.body, { mcpEnabled: false }); + }); +}); diff --git a/tests/unit/cli-supervisor-surfaces-fatal-startup-diagnostic-13314.test.ts b/tests/unit/cli-supervisor-surfaces-fatal-startup-diagnostic-13314.test.ts new file mode 100644 index 0000000000..77bdaa2fb0 --- /dev/null +++ b/tests/unit/cli-supervisor-surfaces-fatal-startup-diagnostic-13314.test.ts @@ -0,0 +1,74 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { writeFileSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +// #13314: ServerSupervisor's default (non `--log`) mode only buffers a +// fatal `[STARTUP] Fatal: ...` boot diagnostic in-memory and flushes it to +// the real console only when the child process exits. If the HTTP listener +// still comes up after a fatal DB-driver-cascade failure was already +// printed, the operator sees "OmniRoute is running!" with every route +// 500ing and zero diagnostic output anywhere. This regression test asserts +// the fatal line now reaches the real console immediately, not only on +// exit/crash. +test("ServerSupervisor surfaces a fatal [STARTUP] Fatal: boot diagnostic to the real console even when the child never exits (default, non --log mode)", async () => { + const { ServerSupervisor } = await import("../../bin/cli/runtime/processSupervisor.mjs"); + delete process.env.OMNIROUTE_SHOW_LOG; + + const dir = mkdtempSync(join(tmpdir(), "omniroute-issue13314-")); + const childScript = join(dir, "fake-server.mjs"); + writeFileSync( + childScript, + ` + console.error("[STARTUP] Fatal: Database driver initialization failed: better-sqlite3 invalid, node:sqlite fallback also failed"); + console.log("Ready on 0.0.0.0:20128"); + setInterval(() => {}, 1000); + ` + ); + + const seenOnRealConsole: string[] = []; + const origStdoutWrite = process.stdout.write.bind(process.stdout); + const origStderrWrite = process.stderr.write.bind(process.stderr); + process.stdout.write = ((chunk: unknown, ...rest: unknown[]) => { + seenOnRealConsole.push(String(chunk)); + // @ts-expect-error - forwarding varargs to the real writer + return origStdoutWrite(chunk, ...rest); + }) as typeof process.stdout.write; + process.stderr.write = ((chunk: unknown, ...rest: unknown[]) => { + seenOnRealConsole.push(String(chunk)); + // @ts-expect-error - forwarding varargs to the real writer + return origStderrWrite(chunk, ...rest); + }) as typeof process.stderr.write; + + const supervisor = new ServerSupervisor({ + serverPath: childScript, + env: { ...process.env }, + maxRestarts: 2, + memoryLimit: 256, + }); + + try { + supervisor.start(); + await new Promise((resolve) => setTimeout(resolve, 1500)); + + const bufferedLog = supervisor.getRecentLog().join("\n"); + const printedToRealConsole = seenOnRealConsole.join(""); + + assert.match( + bufferedLog, + /\[STARTUP\] Fatal: Database driver initialization failed/, + "expected the fatal boot diagnostic to be captured into the supervisor's buffer" + ); + + assert.match( + printedToRealConsole, + /\[STARTUP\] Fatal: Database driver initialization failed/, + "expected the fatal boot diagnostic to reach the real console even though the child process never exits" + ); + } finally { + process.stdout.write = origStdoutWrite; + process.stderr.write = origStderrWrite; + supervisor.stop(); + } +}); diff --git a/tests/unit/codex-wreq-literal-require-turbopack-12491.test.ts b/tests/unit/codex-wreq-literal-require-turbopack-12491.test.ts new file mode 100644 index 0000000000..acc6d1c35d --- /dev/null +++ b/tests/unit/codex-wreq-literal-require-turbopack-12491.test.ts @@ -0,0 +1,65 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); + +/** + * Regression guard for #12491: the Codex app-server WebSocket transport + * ("Codex app-server websocket transport unavailable") never comes up inside + * the Next.js standalone Docker runtime because open-sse/executors/codex.ts + * resolves wreq-js with a LITERAL specifier: + * + * const _wreqRequire = createRequire(import.meta.url); + * const mod = _wreqRequire("wreq-js"); + * + * Turbopack statically detects the literal string argument and rewrites the + * call into `require("wreq-js-")`, satisfied only by a symlink + * Turbopack drops at `.next/node_modules/wreq-js- -> node_modules/wreq-js` + * during the build. That symlink is a `.next`-relative build artifact; it is not + * reachable through the same relative path once files are re-laid-out for a + * standalone Docker image, so `require("wreq-js-")` throws + * MODULE_NOT_FOUND at runtime — even though `node -e "require('wreq-js')"` + * (the literal package name) succeeds fine in the very same container. + * + * Verified interactively against a real `next build --turbopack` (Next 16.3.3): + * compiling `const _wreqRequire = createRequire(import.meta.url); _wreqRequire("wreq-js")` + * emits `64301,(e,t,r)=>{t.exports=e.x("wreq-js-3b69dd5e46bd26d3",()=>require("wreq-js-3b69dd5e46bd26d3"))}` + * in the compiled chunk, and the build directory gained + * `.next/node_modules/wreq-js-3b69dd5e46bd26d3 -> ../../node_modules/wreq-js`. + * `open-sse/utils/tlsClient.ts` (see its `loadRuntimeModule()`) was already + * hardened against exactly this by keeping the specifier a runtime variable + * (`Reflect.apply(runtimeRequire, undefined, [moduleName])`), which Turbopack + * cannot statically rewrite — codex.ts's own wreq-js loader (the one that feeds + * `getCodexAppServerWebsocketTransport()` / `CodexAppServerClient.connect()`) + * never received the same treatment. + */ + +const CODEX_TS_PATH = join(ROOT, "open-sse", "executors", "codex.ts"); + +test("codex.ts must not pass a literal specifier to the wreq-js createRequire() loader", () => { + const source = readFileSync(CODEX_TS_PATH, "utf8"); + + assert.match( + source, + /const _wreqRequire = createRequire\(import\.meta\.url\)/, + "expected open-sse/executors/codex.ts to still define _wreqRequire via createRequire(import.meta.url) " + + "— update this test's assumptions if the loader was restructured" + ); + + const literalCallPattern = /_wreqRequire\(\s*["'`]wreq-js["'`]\s*\)/; + assert.doesNotMatch( + source, + literalCallPattern, + 'open-sse/executors/codex.ts calls _wreqRequire("wreq-js") with a LITERAL specifier. ' + + 'Turbopack statically rewrites this into `require("wreq-js-")`, which only ' + + "resolves via a `.next/node_modules/wreq-js-` symlink generated at build time — " + + "not reachable in the standalone Docker runtime, so the Codex app-server WebSocket transport " + + "(and the plain Codex WS transport sharing this loader) is permanently disabled in production " + + "(#12491). Route the specifier through a variable the bundler cannot statically analyze, " + + "e.g. Reflect.apply(_wreqRequire, undefined, [moduleName]) — the exact pattern already used by " + + "open-sse/utils/tlsClient.ts's loadRuntimeModule()." + ); +}); diff --git a/tests/unit/compose-cdp-proxy-network-isolation-13679.test.ts b/tests/unit/compose-cdp-proxy-network-isolation-13679.test.ts new file mode 100644 index 0000000000..a16a773b6f --- /dev/null +++ b/tests/unit/compose-cdp-proxy-network-isolation-13679.test.ts @@ -0,0 +1,99 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +const REPO_ROOT = path.resolve(import.meta.dirname, "../.."); + +// #13679 item #9: docker-compose.yml has no top-level `networks:` key, so +// Compose puts every service (redis, qdrant, bifrost, cliproxyapi, +// codex-app-server, chatgpt-web-codex-browser, ...) on the same implicit +// default bridge network. The chatgpt-web-codex-browser sidecar exposes an +// UNAUTHENTICATED CDP proxy on 9223 (docker/chatgpt-web-codex-browser/cdp-proxy.mjs) +// — any compromised sibling container on that shared bridge can reach it and +// take full control of the live browser session. It must be isolated onto a +// dedicated network shared only with the one legitimate consumer +// (omniroute-web). + +function readCompose(): string { + return fs.readFileSync(path.join(REPO_ROOT, "docker-compose.yml"), "utf8"); +} + +function serviceBlock(compose: string, serviceName: string): string { + const lines = compose.split("\n"); + const startIndex = lines.findIndex((line) => new RegExp(`^ ${serviceName}:\\s*$`).test(line)); + assert.notEqual(startIndex, -1, `service '${serviceName}' not found in docker-compose.yml`); + const rest = lines.slice(startIndex + 1); + const endOffset = rest.findIndex((line) => /^ \S/.test(line) || /^\S/.test(line)); + const block = endOffset === -1 ? rest : rest.slice(0, endOffset); + return block.join("\n"); +} + +/** + * The service's `networks:` entries, read line by line. A regex over the whole + * block would need nested quantifiers (`(\s*-\s*.*\n)*`), which CodeQL flags as + * a ReDoS risk (js/redos) — and the line walk is easier to read anyway. + */ +function listedNetworks(serviceYaml: string): string[] { + const lines = serviceYaml.split("\n"); + const start = lines.findIndex((line) => /^\s*networks:\s*$/.test(line)); + if (start === -1) return []; + const names: string[] = []; + for (const line of lines.slice(start + 1)) { + const item = line.match(/^\s*-\s*(\S+)\s*$/); + if (!item) break; + names.push(item[1]); + } + return names; +} + +test("docker-compose.yml declares a dedicated network for the CDP proxy sidecar", () => { + const compose = readCompose(); + assert.match( + compose, + /^networks:\s*$/m, + "docker-compose.yml must declare a top-level `networks:` key — without it every " + + "service shares the implicit default bridge, so any sibling container can reach " + + "the unauthenticated chatgpt-web-codex-browser CDP proxy on 9223" + ); +}); + +test("chatgpt-web-codex-browser is isolated off the shared default network", () => { + const compose = readCompose(); + const block = serviceBlock(compose, "chatgpt-web-codex-browser"); + assert.match( + block, + /networks:/, + "chatgpt-web-codex-browser must declare an explicit `networks:` list — otherwise it " + + "attaches to the implicit default network shared with redis/qdrant/bifrost/etc." + ); + assert.ok( + !listedNetworks(block).includes("default"), + "chatgpt-web-codex-browser must not also list `default` — that would put it right back " + + "on the shared bridge with every unrelated sibling container" + ); +}); + +test("omniroute-web (the one legitimate CDP consumer) stays reachable via the dedicated network", () => { + const compose = readCompose(); + const block = serviceBlock(compose, "omniroute-web"); + assert.match( + block, + /networks:/, + "omniroute-web must explicitly join the dedicated CDP-proxy network to keep reaching " + + "chatgpt-web-codex-browser:9223 after the sidecar is isolated off the default network" + ); +}); + +test("unrelated sidecars (redis, qdrant) are not put on the CDP-proxy network", () => { + const compose = readCompose(); + for (const serviceName of ["redis", "qdrant", "bifrost"]) { + const block = serviceBlock(compose, serviceName); + assert.doesNotMatch( + block, + /chatgpt-web-codex/, + `${serviceName} must not reference the chatgpt-web-codex-browser network — it has no ` + + "legitimate reason to reach the CDP proxy sidecar" + ); + } +}); diff --git a/tests/unit/db-cleanup-conversation-nodes-12453.test.ts b/tests/unit/db-cleanup-conversation-nodes-12453.test.ts index a285fbc1f7..5bec990b6b 100644 --- a/tests/unit/db-cleanup-conversation-nodes-12453.test.ts +++ b/tests/unit/db-cleanup-conversation-nodes-12453.test.ts @@ -4,8 +4,11 @@ * ~775 MB in four days on one busy coding-agent workload). * * The identity nodes only make sense while the call_logs row their - * last_correlation_id points at still exists, so both tables follow the - * existing `retention.callLogs` window instead of getting a knob of their own. + * last_correlation_id points at still exists, so both tables follow their + * own `retention.conversationTurnNodes` window, not `retention.callLogs` + * directly — the default (30) matches callLogs' default so upgrading an + * existing install changes no behavior until an operator overrides one of + * the two knobs independently. * * These tests call the REAL cleanup functions against a real SQLite adapter * seeded with test rows, exactly like telemetry-auto-cleanup-6848.test.ts. @@ -37,7 +40,7 @@ test.after(() => { }); const DAY_MS = 86_400_000; -const RETENTION_DAYS = getUserDatabaseSettings().retention.callLogs; +const RETENTION_DAYS = getUserDatabaseSettings().retention.conversationTurnNodes; const OLD = new Date(Date.now() - (RETENTION_DAYS + 1) * DAY_MS).toISOString(); const RECENT = new Date().toISOString(); @@ -188,3 +191,67 @@ test("#12453 cleanupAgenticConversations: missing node table is a safe no-op", a db.exec("ALTER TABLE conversation_turn_nodes_unavailable RENAME TO conversation_turn_nodes"); } }); + +test("#12453 conversationTurnNodes window is independent of callLogs", async () => { + const { updateDatabaseSettings } = await import("../../src/lib/db/databaseSettings.ts"); + const originalRetention = getUserDatabaseSettings().retention; + + try { + updateDatabaseSettings({ + retention: { ...originalRetention, callLogs: 90, conversationTurnNodes: 1 }, + }); + + const dayMs = 86_400_000; + const twoDaysAgo = new Date(Date.now() - 2 * dayMs).toISOString(); + const recent = new Date().toISOString(); + insertConversation("conv_ind", recent); + insertNode("old-ind", "conv_ind", twoDaysAgo); + insertNode("new-ind", "conv_ind", recent); + + const result = await cleanupConversationTurnNodes(); + assert.strictEqual(result.deleted, 1); + assert.deepStrictEqual(ids("conversation_turn_nodes"), ["new-ind"]); + assert.equal(getUserDatabaseSettings().retention.callLogs, 90); + } finally { + updateDatabaseSettings({ retention: originalRetention }); + } +}); + +test("#12453 default conversationTurnNodes retention matches callLogs (no behavior change on upgrade)", async () => { + // Read the SHIPPED default directly (not a value this test sets itself) — + // this is what actually ships to every existing install on upgrade. + const { DEFAULT_DATABASE_SETTINGS } = await import("../../src/types/databaseSettings.ts"); + assert.strictEqual( + DEFAULT_DATABASE_SETTINGS.retention.conversationTurnNodes, + DEFAULT_DATABASE_SETTINGS.retention.callLogs, + "default conversationTurnNodes must equal default callLogs so merging the split knob is a no-op for existing installs" + ); + + const { updateDatabaseSettings } = await import("../../src/lib/db/databaseSettings.ts"); + const originalRetention = getUserDatabaseSettings().retention; + + try { + // Reset both knobs to the shipped defaults (an operator may have overridden + // them in an earlier test in this file's shared DB). + updateDatabaseSettings({ + retention: { + ...originalRetention, + callLogs: DEFAULT_DATABASE_SETTINGS.retention.callLogs, + conversationTurnNodes: DEFAULT_DATABASE_SETTINGS.retention.conversationTurnNodes, + }, + }); + + // A node that would have survived under the old shared callLogs window + // (30 days) must still survive under the new dedicated default. + const twentyNineDaysAgo = new Date(Date.now() - 29 * DAY_MS).toISOString(); + const recent = new Date().toISOString(); + insertConversation("conv_default", recent); + insertNode("still-alive", "conv_default", twentyNineDaysAgo); + + const result = await cleanupConversationTurnNodes(); + assert.strictEqual(result.deleted, 0); + assert.deepStrictEqual(ids("conversation_turn_nodes"), ["still-alive"]); + } finally { + updateDatabaseSettings({ retention: originalRetention }); + } +}); diff --git a/tests/unit/db-migrationrunner-constants-split.test.ts b/tests/unit/db-migrationrunner-constants-split.test.ts index eb932cbb13..c225a9518b 100644 --- a/tests/unit/db-migrationrunner-constants-split.test.ts +++ b/tests/unit/db-migrationrunner-constants-split.test.ts @@ -63,7 +63,7 @@ describe("migrationRunner/constants — exact small-table snapshots", () => { it("OPTIONAL_FTS5_MIGRATION_VERSIONS is exactly {022, 023}", () => { assert.ok(OPTIONAL_FTS5_MIGRATION_VERSIONS instanceof Set); - assert.deepEqual([...OPTIONAL_FTS5_MIGRATION_VERSIONS].sort(), ["022", "023"]); + assert.deepEqual([...OPTIONAL_FTS5_MIGRATION_VERSIONS].sort(), ["022", "023", "180"]); }); }); @@ -71,7 +71,7 @@ describe("migrationRunner/constants — exact small-table snapshots", () => { describe("migrationRunner/constants — large-table integrity", () => { it("RENAMED_MIGRATION_COMPATIBILITY has 32 well-formed entries", () => { - assert.equal(RENAMED_MIGRATION_COMPATIBILITY.length, 32); + assert.equal(RENAMED_MIGRATION_COMPATIBILITY.length, 33); for (const e of RENAMED_MIGRATION_COMPATIBILITY) { assert.equal(typeof e.fromVersion, "string"); assert.equal(typeof e.fromName, "string"); @@ -124,49 +124,55 @@ describe("migrationRunner/constants — large-table integrity", () => { toName: "inspector_custom_hosts", } ); - assert.deepEqual(RENAMED_MIGRATION_COMPATIBILITY.at(-7), { + assert.deepEqual(RENAMED_MIGRATION_COMPATIBILITY.at(-8), { fromVersion: "134", fromName: "ccr_blocks", toVersion: "139", toName: "ccr_blocks", }); - assert.deepEqual(RENAMED_MIGRATION_COMPATIBILITY.at(-6), { + assert.deepEqual(RENAMED_MIGRATION_COMPATIBILITY.at(-7), { fromVersion: "139", fromName: "job_registry", toVersion: "146", toName: "job_registry", }); - assert.deepEqual(RENAMED_MIGRATION_COMPATIBILITY.at(-5), { + assert.deepEqual(RENAMED_MIGRATION_COMPATIBILITY.at(-6), { fromVersion: "143", fromName: "radar_local_model_state", toVersion: "153", toName: "radar_local_model_state", }); // #12036: renamed migrations 056/073/077/101 appended as compatibility renames - assert.deepEqual(RENAMED_MIGRATION_COMPATIBILITY.at(-4), { + assert.deepEqual(RENAMED_MIGRATION_COMPATIBILITY.at(-5), { fromVersion: "056", fromName: "provider_default", toVersion: "056", toName: "mcp_accessibility_compression", }); - assert.deepEqual(RENAMED_MIGRATION_COMPATIBILITY.at(-3), { + assert.deepEqual(RENAMED_MIGRATION_COMPATIBILITY.at(-4), { fromVersion: "073", fromName: "discovery_results", toVersion: "073", toName: "per_model_token_limits", }); - assert.deepEqual(RENAMED_MIGRATION_COMPATIBILITY.at(-2), { + assert.deepEqual(RENAMED_MIGRATION_COMPATIBILITY.at(-3), { fromVersion: "077", fromName: "plugin_metrics", toVersion: "077", toName: "api_key_stream_default_mode", }); - assert.deepEqual(RENAMED_MIGRATION_COMPATIBILITY.at(-1), { + assert.deepEqual(RENAMED_MIGRATION_COMPATIBILITY.at(-2), { fromVersion: "101", fromName: "proxy_pool_rotation", toVersion: "101", toName: "api_key_usage_limits", }); + assert.deepEqual(RENAMED_MIGRATION_COMPATIBILITY.at(-1), { + fromVersion: "176", + fromName: "memory_fts_skip_access_updates", + toVersion: "180", + toName: "memory_fts_au_conditional_memory_id", + }); }); it("PHYSICAL_SCHEMA_SENTINELS has 15 well-formed entries incl. the newest 064", () => { diff --git a/tests/unit/direct-response-start-timeout-settled-guard-12861.test.ts b/tests/unit/direct-response-start-timeout-settled-guard-12861.test.ts new file mode 100644 index 0000000000..24053128b1 --- /dev/null +++ b/tests/unit/direct-response-start-timeout-settled-guard-12861.test.ts @@ -0,0 +1,178 @@ +// #12861 — proxyFetch: DIRECT_RESPONSE_START_TIMEOUT escapes as +// unhandledRejection -> uncaughtException, server process exits. +// +// A narrow race: if the timer fires AFTER the wrapped fetch has already +// settled (resolved or rejected) — e.g. the awaiting frame was already torn +// down — aborting the (by-then-irrelevant) AbortController can deliver its +// abort reason to a promise nobody is awaiting anymore, which Node promotes +// to an unhandledRejection -> uncaughtException. These tests use node:test's +// mock timer API to deterministically force exactly that ordering, rather +// than relying on real wall-clock timing (which cannot reliably reproduce a +// race this narrow). +import test, { mock } from "node:test"; +import assert from "node:assert/strict"; +import { + directFetchWithBoundedResponseStart, + isDirectResponseStartTimeout, + resolveDirectHeadersTimeoutMs, +} from "../../open-sse/utils/directResponseStartTimeout.ts"; + +test.afterEach(() => { + mock.timers.reset(); +}); + +test("resolves normally when the fetch settles well before the timeout", async () => { + const response = new Response("ok"); + const result = await directFetchWithBoundedResponseStart( + "http://example.test", + {}, + async () => response, + 30_000 + ); + assert.equal(result, response); +}); + +test("rejects with DIRECT_RESPONSE_START_TIMEOUT when the fetch never settles before the timeout", async () => { + mock.timers.enable({ apis: ["setTimeout"] }); + try { + const fetchImpl = (_input: RequestInfo | URL, options: RequestInit) => + new Promise((_resolve, reject) => { + options.signal?.addEventListener("abort", () => { + reject((options.signal as AbortSignal).reason); + }); + }); + + const pending = directFetchWithBoundedResponseStart( + "http://example.test", + {}, + fetchImpl, + 5_000 + ); + const assertion = assert.rejects(pending, (err: unknown) => { + assert.equal(isDirectResponseStartTimeout(err), true); + return true; + }); + + await Promise.resolve(); + mock.timers.tick(5_000); + await assertion; + } finally { + mock.timers.reset(); + } +}); + +test("#12861: a timer firing AFTER the fetch already settled does not escape as an unhandled rejection", async () => { + // This is the actual race the report describes: `clearTimeout()` runs in + // the `finally` block, but the timer callback has already been dequeued by + // the time it runs, so clearing it has no effect. Node's real timer/ + // microtask scheduler can't be forced into that exact interleaving + // deterministically from a test, so the observable consequence is forced + // directly instead: neuter clearTimeout so the timer fires regardless of + // whether the code "tried" to cancel it, exactly as it would if clearTimeout + // had lost that race. + const realClearTimeout = globalThis.clearTimeout; + const realSetTimeout = globalThis.setTimeout; + globalThis.clearTimeout = (() => {}) as typeof clearTimeout; + + let unhandled: unknown = null; + const onUnhandledRejection = (reason: unknown) => { + unhandled = reason; + }; + process.on("unhandledRejection", onUnhandledRejection); + + try { + const response = new Response("ok"); + // Simulates what a real fetch/undici implementation does internally: some + // async chain tied to the same abort signal that the OUTER caller never + // awaits or attaches a .catch() to (e.g. background body-stream cleanup). + // This is the actual mechanism the issue traces the escaped rejection + // back to — not the outer `await fetchImpl(...)` itself, which normal + // control flow already handles fine. + const fetchImpl = async (_input: RequestInfo | URL, options: RequestInit) => { + const detachedInternalChain = new Promise((_resolve, reject) => { + options.signal?.addEventListener( + "abort", + () => reject((options.signal as AbortSignal).reason), + { once: true } + ); + }); + void detachedInternalChain; + return response; + }; + + const result = await directFetchWithBoundedResponseStart( + "http://example.test", + {}, + fetchImpl, + 10 + ); + assert.equal(result, response); + + // Real timer, real (short) wait — clearTimeout was neutered above, so the + // 10ms timer WILL fire regardless of the `finally` block having "tried" + // to clear it, exactly reproducing the reported race's end state. + await new Promise((resolve) => realSetTimeout(resolve, 50)); + + assert.equal(unhandled, null, "post-settlement timer fire must not produce a rejection"); + } finally { + globalThis.clearTimeout = realClearTimeout; + process.off("unhandledRejection", onUnhandledRejection); + } +}); + +test("#12861: a timer firing AFTER the fetch already rejected (for an unrelated reason) does not escape either", async () => { + const realClearTimeout = globalThis.clearTimeout; + const realSetTimeout = globalThis.setTimeout; + globalThis.clearTimeout = (() => {}) as typeof clearTimeout; + + let unhandled: unknown = null; + const onUnhandledRejection = (reason: unknown) => { + unhandled = reason; + }; + process.on("unhandledRejection", onUnhandledRejection); + + try { + const clientAbortError = Object.assign(new Error("aborted"), { code: "ECONNRESET" }); + const fetchImpl = async (_input: RequestInfo | URL, options: RequestInit) => { + const detachedInternalChain = new Promise((_resolve, reject) => { + options.signal?.addEventListener( + "abort", + () => reject((options.signal as AbortSignal).reason), + { once: true } + ); + }); + void detachedInternalChain; + throw clientAbortError; + }; + + await assert.rejects( + directFetchWithBoundedResponseStart("http://example.test", {}, fetchImpl, 10), + clientAbortError + ); + + await new Promise((resolve) => realSetTimeout(resolve, 50)); + + assert.equal(unhandled, null, "post-settlement timer fire must not produce a rejection"); + } finally { + globalThis.clearTimeout = realClearTimeout; + process.off("unhandledRejection", onUnhandledRejection); + } +}); + +test("passes through immediately with no timer when timeoutMs is 0 or negative", async () => { + const response = new Response("ok"); + const result = await directFetchWithBoundedResponseStart( + "http://example.test", + {}, + async () => response, + 0 + ); + assert.equal(result, response); +}); + +test("resolveDirectHeadersTimeoutMs defaults to 30000 and respects OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS", () => { + assert.equal(resolveDirectHeadersTimeoutMs({}), 30_000); + assert.equal(resolveDirectHeadersTimeoutMs({ OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS: "5000" }), 5_000); + assert.equal(resolveDirectHeadersTimeoutMs({ OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS: "" }), 30_000); + assert.equal(resolveDirectHeadersTimeoutMs({ OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS: "not-a-number" }), 0); +}); diff --git a/tests/unit/executor-default-base.test.ts b/tests/unit/executor-default-base.test.ts index 469ffe992c..ba628de97b 100644 --- a/tests/unit/executor-default-base.test.ts +++ b/tests/unit/executor-default-base.test.ts @@ -601,9 +601,8 @@ test("DefaultExecutor.execute uses CC-compatible connection defaults to append 1 stream: false, credentials: { apiKey: "cc-key", - providerSpecificData: { - ccSessionId: "session-1", - }, + // #13452: buildUrl() now requires a hydrated baseUrl. + providerSpecificData: { ccSessionId: "session-1", baseUrl: "https://cc.test/v1" }, }, clientHeaders: { "x-app": "cli", @@ -623,6 +622,7 @@ test("DefaultExecutor.execute uses CC-compatible connection defaults to append 1 apiKey: "cc-key", providerSpecificData: { ccSessionId: "session-1", + baseUrl: "https://cc.test/v1", requestDefaults: { context1m: true, redactThinking: true }, }, }, @@ -658,6 +658,7 @@ test("DefaultExecutor.execute uses CC-compatible connection defaults to append 1 apiKey: "cc-key", providerSpecificData: { ccSessionId: "session-1", + baseUrl: "https://cc-proxy.example.test/v1", requestDefaults: { context1m: true }, }, }, @@ -734,9 +735,7 @@ test("DefaultExecutor.execute reports the exact serialized provider request befo stream: false, credentials: { apiKey: "cc-key", - providerSpecificData: { - ccSessionId: "session-1", - }, + providerSpecificData: { ccSessionId: "session-1", baseUrl: "https://cc.test/v1" }, // #13452 }, }) ); diff --git a/tests/unit/executor-gitlab.test.ts b/tests/unit/executor-gitlab.test.ts index 8bc41efb1f..5f98bbb1b5 100644 --- a/tests/unit/executor-gitlab.test.ts +++ b/tests/unit/executor-gitlab.test.ts @@ -320,3 +320,55 @@ test("GitlabExecutor falls back to the public Code Suggestions endpoint when dir globalThis.fetch = originalFetch; } }); + +// #12958: an entitlement/scope-resolution 403 (NOT the "direct connections are +// disabled" tenant-config message) must ALSO fall back to the public Code Suggestions +// completions endpoint — previously only that exact message recovered; any other 403 +// hard-failed the request even when the same token was accepted by the public endpoint. +test("GitlabExecutor falls back to the public Code Suggestions endpoint on an entitlement-flavored 403 (#12958)", async () => { + const executor = (await getExecutor("gitlab-duo")) as GitlabExecutor; + const originalFetch = globalThis.fetch; + const calls: string[] = []; + + globalThis.fetch = async (url) => { + calls.push(String(url)); + + if (String(url) === "https://gitlab.example.com/api/v4/code_suggestions/direct_access") { + return jsonResponse({ error: "insufficient_scope", scope: "ai_features" }, 403); + } + + return jsonResponse({ + model: { name: "code-gecko" }, + choices: [{ text: "fallback path works" }], + }); + }; + + try { + const result = await executor.execute({ + model: "gitlab-duo-code-suggestions", + body: { + messages: [{ role: "user", content: "Say hello" }], + }, + stream: false, + credentials: { + accessToken: "oauth-access", + providerSpecificData: { + baseUrl: "https://gitlab.example.com", + }, + }, + signal: AbortSignal.timeout(10_000), + log: null, + }); + + assert.deepEqual(calls, [ + "https://gitlab.example.com/api/v4/code_suggestions/direct_access", + "https://gitlab.example.com/api/v4/code_suggestions/completions", + ]); + + const body = (await result.response.json()) as GitLabResponseBody; + assert.equal(body.model, "code-gecko"); + assert.match(body.choices[0].message.content, /fallback path/i); + } finally { + globalThis.fetch = originalFetch; + } +}); diff --git a/tests/unit/feature-flags-settings.test.ts b/tests/unit/feature-flags-settings.test.ts index c521b3f793..0e4209ccb9 100644 --- a/tests/unit/feature-flags-settings.test.ts +++ b/tests/unit/feature-flags-settings.test.ts @@ -40,7 +40,7 @@ const { // the dead ONEPROXY_ENABLED (readerless since the 1proxy purge, #12091) // brought it back to 53. UNIVERSAL_CONTEXT_HANDOFF_ENABLED bumped it to 54. // #13641 added SEARCH_STATS_HIDE_DELETED_CONNECTIONS, bumping the count to 56. -const EXPECTED_FEATURE_FLAG_COUNT = 69; +const EXPECTED_FEATURE_FLAG_COUNT = 70; // ────────────────────────────────────────────────────── // Test group 1 — Flag definitions registry diff --git a/tests/unit/gemini-38-thinking-level-output.test.ts b/tests/unit/gemini-38-thinking-level-output.test.ts new file mode 100644 index 0000000000..adf823e264 --- /dev/null +++ b/tests/unit/gemini-38-thinking-level-output.test.ts @@ -0,0 +1,229 @@ +/** + * Gemini 3.8 Flash talks thinking_level (low|medium|high), not the 3.7 numeric + * thinkingBudget Omni still emits. includeThoughts:true also shares + * maxOutputTokens with hidden thoughts, so a review-sized max_tokens=65536 + * request starves visible completion (finish=length, ~2.6k text). + * + * Tests hit openaiToGeminiRequest / openaiToAntigravityRequest / claudeToGeminiRequest + * at the write sites so a helper cannot stay green with the call gone. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { openaiToGeminiRequest, openaiToAntigravityRequest } = await import( + "../../open-sse/translator/request/openai-to-gemini.ts" +); +const { claudeToGeminiRequest } = await import( + "../../open-sse/translator/request/claude-to-gemini.ts" +); +const { gemini38ThinkingLevelFromBudget } = await import( + "../../open-sse/services/thinkingBudget.ts" +); + +type ThinkingConfig = { + thinkingBudget?: number; + thinkingLevel?: string; + includeThoughts?: boolean; +}; + +type GeminiReq = { + generationConfig?: { + maxOutputTokens?: number; + thinkingConfig?: ThinkingConfig; + }; +}; + +type EnvelopeReq = { + request?: { + generationConfig?: { + maxOutputTokens?: number; + thinkingConfig?: ThinkingConfig; + }; + }; +}; + +const reviewBody = (extra: Record = {}) => ({ + messages: [{ role: "user", content: "review diff" }], + max_tokens: 65536, + ...extra, +}); + +function thinkingConfigOf(model: string, extra: Record = {}) { + const result = openaiToGeminiRequest(model, reviewBody(extra), false) as GeminiReq; + return result.generationConfig?.thinkingConfig; +} + +test("gemini-3.8-flash-high emits thinkingLevel high, not numeric thinkingBudget", () => { + const tc = thinkingConfigOf("gemini-3.8-flash-high", { reasoning_effort: "high" }); + assert.equal(tc?.thinkingLevel, "high"); + assert.equal(tc?.thinkingBudget, undefined); +}); + +test("gemini-3.8-flash-medium emits thinkingLevel medium", () => { + const tc = thinkingConfigOf("gemini-3.8-flash-medium", { reasoning_effort: "medium" }); + assert.equal(tc?.thinkingLevel, "medium"); + assert.equal(tc?.thinkingBudget, undefined); +}); + +test("gemini-3.8-flash-low emits thinkingLevel low", () => { + const tc = thinkingConfigOf("gemini-3.8-flash-low", { reasoning_effort: "low" }); + assert.equal(tc?.thinkingLevel, "low"); + assert.equal(tc?.thinkingBudget, undefined); +}); + +test("gemini-3.8-flash bare default maps thinkingLevel medium", () => { + const tc = thinkingConfigOf("gemini-3.8-flash"); + assert.equal(tc?.thinkingLevel, "medium"); + assert.equal(tc?.thinkingBudget, undefined); +}); + +test("gemini-3.8-flash-tiered default maps thinkingLevel medium", () => { + const tc = thinkingConfigOf("gemini-3.8-flash-tiered"); + assert.equal(tc?.thinkingLevel, "medium"); + assert.equal(tc?.thinkingBudget, undefined); +}); + +test("agy/gemini-3.8-flash-high prefix emits thinkingLevel high", () => { + const tc = thinkingConfigOf("agy/gemini-3.8-flash-high", { reasoning_effort: "high" }); + assert.equal(tc?.thinkingLevel, "high"); + assert.equal(tc?.thinkingBudget, undefined); +}); + +test("gemini-3.8 review-shaped request does not default-inject includeThoughts", () => { + const result = openaiToGeminiRequest( + "gemini-3.8-flash-high", + reviewBody(), + false + ) as GeminiReq; + assert.equal(result.generationConfig?.maxOutputTokens, 65536); + assert.equal(result.generationConfig?.thinkingConfig?.thinkingLevel, "high"); + assert.equal(result.generationConfig?.thinkingConfig?.includeThoughts, undefined); +}); + +test("gemini-3.8 includeThoughts is set only when the client asked for thoughts", () => { + const silent = thinkingConfigOf("gemini-3.8-flash-high", { reasoning_effort: "high" }); + assert.equal(silent?.includeThoughts, undefined); + + const asked = thinkingConfigOf("gemini-3.8-flash-high", { + reasoning_effort: "high", + includeThoughts: true, + }); + assert.equal(asked?.includeThoughts, true); + assert.equal(asked?.thinkingLevel, "high"); +}); + +test("gemini-3.8 reasoning_effort none stays an explicit off-switch", () => { + const tc = thinkingConfigOf("gemini-3.8-flash-high", { reasoning_effort: "none" }); + assert.equal(tc?.thinkingBudget, 0); + assert.equal(tc?.includeThoughts, false); + assert.equal(tc?.thinkingLevel, undefined); +}); + +test("gemini-2.5-flash still emits numeric thinkingBudget (3.8 gate)", () => { + const result = openaiToGeminiRequest( + "gemini-2.5-flash", + reviewBody({ reasoning_effort: "high" }), + false + ) as GeminiReq; + assert.equal(result.generationConfig?.thinkingConfig?.thinkingBudget, 24576); + assert.equal(result.generationConfig?.thinkingConfig?.includeThoughts, true); + assert.equal(result.generationConfig?.thinkingConfig?.thinkingLevel, undefined); +}); + +test("Antigravity envelope keeps maxOutputTokens 65536 and thinkingLevel for 3.8", () => { + const result = openaiToAntigravityRequest( + "gemini-3.8-flash-high", + reviewBody({ reasoning_effort: "high" }), + false, + { projectId: "proj-gemini38" } + ) as EnvelopeReq; + const gc = result.request?.generationConfig; + assert.equal(gc?.maxOutputTokens, 65536); + assert.equal(gc?.thinkingConfig?.thinkingLevel, "high"); + assert.equal(gc?.thinkingConfig?.thinkingBudget, undefined); + assert.equal(gc?.thinkingConfig?.includeThoughts, undefined); +}); + +test("gemini-3.8 thinking.budget_tokens maps thinkingLevel, not thinkingBudget", () => { + const tc = thinkingConfigOf("gemini-3.8-flash-high", { + thinking: { type: "enabled", budget_tokens: 24576 }, + }); + assert.equal(tc?.thinkingLevel, "high"); + assert.equal(tc?.thinkingBudget, undefined); + assert.equal(tc?.includeThoughts, undefined); +}); + +test("claude-to-gemini gemini-3.8 budget_tokens maps thinkingLevel", () => { + const result = claudeToGeminiRequest( + "gemini-3.8-flash-high", + { + messages: [{ role: "user", content: [{ type: "text", text: "review diff" }] }], + max_tokens: 65536, + thinking: { type: "enabled", budget_tokens: 24576 }, + }, + false + ) as GeminiReq; + assert.equal(result.generationConfig?.thinkingConfig?.thinkingLevel, "high"); + assert.equal(result.generationConfig?.thinkingConfig?.thinkingBudget, undefined); + assert.equal(result.generationConfig?.thinkingConfig?.includeThoughts, undefined); +}); + +test("claude-to-gemini gemini-3.8-flash-high emits thinkingLevel, not includeThoughts", () => { + const result = claudeToGeminiRequest( + "gemini-3.8-flash-high", + { + messages: [{ role: "user", content: [{ type: "text", text: "review diff" }] }], + max_tokens: 65536, + output_config: { effort: "high" }, + }, + false + ) as GeminiReq; + assert.equal(result.generationConfig?.maxOutputTokens, 65536); + assert.equal(result.generationConfig?.thinkingConfig?.thinkingLevel, "high"); + assert.equal(result.generationConfig?.thinkingConfig?.thinkingBudget, undefined); + assert.equal(result.generationConfig?.thinkingConfig?.includeThoughts, undefined); +}); + +test("gemini38ThinkingLevelFromBudget rejects budget <= 0", () => { + assert.throws( + () => gemini38ThinkingLevelFromBudget("gemini-3.8-flash-high", 0), + RangeError + ); + assert.throws( + () => gemini38ThinkingLevelFromBudget("gemini-3.8-flash-high", -1), + RangeError + ); +}); + +test("gemini38ThinkingLevelFromBudget maps 1 and 1024 to low", () => { + assert.equal( + gemini38ThinkingLevelFromBudget("gemini-3.8-flash-high", 1), + "low" + ); + assert.equal( + gemini38ThinkingLevelFromBudget("gemini-3.8-flash-high", 1024), + "low" + ); +}); + +test("gemini38ThinkingLevelFromBudget maps 1025 through medium cap to medium", () => { + assert.equal( + gemini38ThinkingLevelFromBudget("gemini-3.8-flash-medium", 1025), + "medium" + ); + assert.equal( + gemini38ThinkingLevelFromBudget("gemini-3.8-flash-medium", 8192), + "medium" + ); +}); + +test("gemini38ThinkingLevelFromBudget maps above medium cap to high", () => { + assert.equal( + gemini38ThinkingLevelFromBudget("gemini-3.8-flash-medium", 8193), + "high" + ); + assert.equal( + gemini38ThinkingLevelFromBudget("gemini-3.8-flash-high", 24576), + "high" + ); +}); diff --git a/tests/unit/gemini-web-cookie-rotation-7676.test.ts b/tests/unit/gemini-web-cookie-rotation-7676.test.ts index fbc2b46d7a..a6020aa1a2 100644 --- a/tests/unit/gemini-web-cookie-rotation-7676.test.ts +++ b/tests/unit/gemini-web-cookie-rotation-7676.test.ts @@ -53,7 +53,7 @@ test("#7676: GeminiWebExecutor persists rotated __Secure-1PSIDTS/__Secure-1PSIDC goto: async () => {}, waitForTimeout: async () => {}, waitForSelector: async () => ({ click: async () => {} }), - keyboard: { type: async () => {}, press: async () => {} }, + keyboard: { type: async () => {}, insertText: async () => {}, press: async () => {} }, }), }), close: async () => {}, diff --git a/tests/unit/gemini-web-image-retirement.test.ts b/tests/unit/gemini-web-image-retirement.test.ts index c5b3917c07..7e8d63aca2 100644 --- a/tests/unit/gemini-web-image-retirement.test.ts +++ b/tests/unit/gemini-web-image-retirement.test.ts @@ -68,7 +68,7 @@ test("Gemini Web executor treats the retired image-mode extension as ordinary ch waitDurations.push(duration); }, waitForSelector: async () => ({ click: async () => {} }), - keyboard: { type: async () => {}, press: async () => {} }, + keyboard: { type: async () => {}, insertText: async () => {}, press: async () => {} }, }), }), close: async () => {}, diff --git a/tests/unit/gemini-web-multiturn-context-8371.test.ts b/tests/unit/gemini-web-multiturn-context-8371.test.ts index fca4965b9b..46ab90639e 100644 --- a/tests/unit/gemini-web-multiturn-context-8371.test.ts +++ b/tests/unit/gemini-web-multiturn-context-8371.test.ts @@ -19,14 +19,15 @@ test("#8371: single user message returns that message verbatim (single-turn unch assert.equal(prompt, "What about Paris?"); }); -test("#8371: single user turn with a system message still returns only the user text", () => { - // Preserves the pre-existing no-tools derivation, which ignored system-only - // context on the first turn. +test("#13380: single user turn with a system message prepends the system text instead of dropping it", () => { + // The pre-existing no-tools derivation ignored system-only context on the + // first turn, silently dropping it (#13380). Fixed to prepend it the same + // way the multi-turn branch below does. const prompt = buildGeminiPrompt([ { role: "system", content: "You are helpful" }, { role: "user", content: "Hello" }, ]); - assert.equal(prompt, "Hello"); + assert.equal(prompt, "System:\nYou are helpful\n\nHello"); }); // ─── Multi-turn: full history is flattened into the prompt ─────────────────── diff --git a/tests/unit/gemini-web-tool-calling-7286.test.ts b/tests/unit/gemini-web-tool-calling-7286.test.ts index e7a6ec34ee..2c19860102 100644 --- a/tests/unit/gemini-web-tool-calling-7286.test.ts +++ b/tests/unit/gemini-web-tool-calling-7286.test.ts @@ -11,9 +11,8 @@ import test from "node:test"; import assert from "node:assert/strict"; -const { GeminiWebExecutor, buildGeminiToolResponse, buildGeminiToolPrompt } = await import( - "../../open-sse/executors/gemini-web.ts" -); +const { GeminiWebExecutor, buildGeminiToolResponse, buildGeminiToolPrompt } = + await import("../../open-sse/executors/gemini-web.ts"); interface ToolCallLike { function: { name: string; arguments: string }; @@ -192,11 +191,12 @@ type FakeResponseHandler = (resp: FakePlaywrightResponse) => Promise; async function withMockedGeminiBrowser( responseText: string, - fn: (typedPrompt: { value: string }) => Promise + fn: (typedPrompt: { value: string }, calls: string[]) => Promise ): Promise { const playwright = await import("playwright"); const originalLaunch = playwright.chromium.launch; const typedPrompt = { value: "" }; + const calls: string[] = []; playwright.chromium.launch = (async () => ({ newContext: async () => ({ @@ -212,9 +212,15 @@ async function withMockedGeminiBrowser( waitForSelector: async () => ({ click: async () => {} }), keyboard: { type: async (text: string) => { + calls.push("type"); + typedPrompt.value = text; + }, + insertText: async (text: string) => { + calls.push("insertText"); typedPrompt.value = text; }, press: async () => { + calls.push("press"); if (respHandler) { await respHandler({ url: () => "https://gemini.google.com/_/BardChatUi/data/.../StreamGenerate?x", @@ -231,15 +237,14 @@ async function withMockedGeminiBrowser( })) as unknown as typeof playwright.chromium.launch; try { - return await fn(typedPrompt); + return await fn(typedPrompt, calls); } finally { playwright.chromium.launch = originalLaunch; } } test("#7286: executor integration — tools[] present reaches tool_calls end to end", async () => { - const responseText = - '{"name":"get_weather","arguments":{"city":"Berlin"}}'; + const responseText = '{"name":"get_weather","arguments":{"city":"Berlin"}}'; await withMockedGeminiBrowser(responseText, async () => { const executor = new GeminiWebExecutor(); @@ -291,3 +296,28 @@ test("#7286: no-tool passthrough regression — unchanged prompt derivation + re assert.equal(choice.finish_reason, "stop"); }); }); + +test("#13380: the Playwright input uses an atomic insertText, not the per-keystroke type()", async () => { + await withMockedGeminiBrowser("ok", async (typedPrompt, calls) => { + const executor = new GeminiWebExecutor(); + const result = await executor.execute({ + model: "gemini-3.1-pro", + body: { + messages: [{ role: "user", content: "multi\nline\nprompt" }], + stream: false, + }, + stream: false, + credentials: { apiKey: "test-cookie" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + + assert.equal(result.response.status, 200); + assert.equal(typedPrompt.value, "multi\nline\nprompt"); + // insertText dispatches one atomic `input` event instead of per-character + // keydown/keypress/keyup, so an embedded "\n" cannot fire the composer's + // Enter-submits handler ahead of the executor's own explicit Enter below. + assert.ok(!calls.includes("type"), "must not use the per-keystroke type() input path"); + assert.deepEqual(calls, ["insertText", "press"], "insertText once, then Enter exactly once"); + }); +}); diff --git a/tests/unit/gemini-web.test.ts b/tests/unit/gemini-web.test.ts index bab367a1c1..67c444b29f 100644 --- a/tests/unit/gemini-web.test.ts +++ b/tests/unit/gemini-web.test.ts @@ -121,6 +121,7 @@ test("Normalizes a bare __Secure-1PSID value before adding browser cookies", asy }), keyboard: { type: async () => {}, + insertText: async () => {}, press: async () => {}, }, }), diff --git a/tests/unit/graceful-shutdown-deferred-exit-13306.test.ts b/tests/unit/graceful-shutdown-deferred-exit-13306.test.ts new file mode 100644 index 0000000000..63fb90fa65 --- /dev/null +++ b/tests/unit/graceful-shutdown-deferred-exit-13306.test.ts @@ -0,0 +1,90 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; + +type GracefulShutdownModule = typeof import("../../src/lib/gracefulShutdown.ts"); + +const gracefulShutdownUrl = pathToFileURL(join(process.cwd(), "src/lib/gracefulShutdown.ts")).href; + +// #13306: on Windows, sql.js's Emscripten WASM build leaves pending libuv async-handle +// teardown work in flight after a statement has run (db.run()/adapter.exec()). Calling +// process.exit() tears the event loop down synchronously, and libuv's Windows async-handle +// close path asserts `!(handle->flags & UV_HANDLE_CLOSING)` while that teardown work is +// still pending -> hard abort (exit 127). The fix defers `process.exit(0)` by one macrotask +// after the shutdown cleanup promise resolves (mirrors the `setTimeout(() => +// process.exit(0), n)` pattern already used throughout 9router's own shutdown call sites), +// giving sql.js's pending libuv work a chance to settle before the event loop tears down. +// +// The Windows abort itself cannot be reproduced here (Linux's unix libuv backend has no +// equivalent assertion, matching the reporter's own cross-platform matrix) — this test pins +// the *ordering* contract the fix depends on: process.exit(0) must not fire in the same +// microtask turn the cleanup promise resolves in, it must be deferred to a later macrotask. +test("graceful shutdown defers process.exit(0) to a macrotask after cleanup resolves (#13306)", async () => { + const previousState = globalThis.__omnirouteShutdown; + const previousRequestShutdown = globalThis.__omnirouteRequestShutdown; + const previousCustomServerOwner = globalThis.__omnirouteCustomServerOwnsShutdown; + const previousExit = process.exit; + const listenersBefore = process.listeners("SIGTERM"); + + delete globalThis.__omnirouteShutdown; + delete globalThis.__omnirouteCustomServerOwnsShutdown; + + const exitCalls: Array = []; + process.exit = ((code?: number) => { + exitCalls.push(code); + return undefined as never; + }) as typeof process.exit; + + let resolveCleanup!: () => void; + const cleanupPromise = new Promise((resolve) => { + resolveCleanup = resolve; + }); + globalThis.__omnirouteRequestShutdown = () => cleanupPromise; + + try { + const shutdownModule = (await import( + `${gracefulShutdownUrl}?issue13306=${Date.now()}` + )) as GracefulShutdownModule; + shutdownModule.initGracefulShutdown(); + + const addedListener = process + .listeners("SIGTERM") + .find((listener) => !listenersBefore.includes(listener)); + assert.ok(addedListener, "initGracefulShutdown() must register a new SIGTERM listener"); + + // Trigger the shutdown closure directly — do NOT emit a real SIGTERM in the test process. + (addedListener as () => void)(); + resolveCleanup(); + + // Let the shutdown closure's own `.then()` continuation run: it was attached to + // `cleanupPromise` before this `await`, so it settles first on the microtask queue. + await cleanupPromise; + await Promise.resolve(); + + assert.deepEqual( + exitCalls, + [], + "process.exit(0) must not fire in the same microtask turn the cleanup promise resolves in" + ); + + // Now let a macrotask elapse — this is where the deferred process.exit(0) must land. + await new Promise((resolve) => setTimeout(resolve, 10)); + + assert.deepEqual(exitCalls, [0], "process.exit(0) must still run, deferred by one macrotask"); + } finally { + process.exit = previousExit; + for (const listener of process.listeners("SIGTERM")) { + if (!listenersBefore.includes(listener)) process.removeListener("SIGTERM", listener); + } + if (previousState === undefined) delete globalThis.__omnirouteShutdown; + else globalThis.__omnirouteShutdown = previousState; + if (previousRequestShutdown === undefined) delete globalThis.__omnirouteRequestShutdown; + else globalThis.__omnirouteRequestShutdown = previousRequestShutdown; + if (previousCustomServerOwner === undefined) { + delete globalThis.__omnirouteCustomServerOwnsShutdown; + } else { + globalThis.__omnirouteCustomServerOwnsShutdown = previousCustomServerOwner; + } + } +}); diff --git a/tests/unit/grok-cli-reasoning-strip-6288.test.ts b/tests/unit/grok-cli-reasoning-strip-6288.test.ts index f9e066cbab..8035fd022c 100644 --- a/tests/unit/grok-cli-reasoning-strip-6288.test.ts +++ b/tests/unit/grok-cli-reasoning-strip-6288.test.ts @@ -96,6 +96,9 @@ test("grok-cli preserves explicit store and de-duplicates encrypted reasoning in assert.equal(out.store, true); assert.deepEqual(out.include, ["reasoning.encrypted_content"]); + // An explicit (but unsupported) effort like "xhigh" is still an EXPLICIT effort key — + // it gets stripped, not defaulted. Only the true absence of an "effort" key falls back + // to the model default. This is the off-switch #7358 relied on and #13628 regressed. assert.equal("reasoning" in out, false); }); @@ -113,3 +116,94 @@ test("grok-cli preserves an explicit Responses reasoning summary", () => { assert.deepEqual(out.reasoning, { summary: "concise", effort: "high" }); }); + +test("grok-4.6 applies default high when client omits effort", async () => { + const executor = new GrokCliExecutor(); + const body = { + model: "grok-4.6", + input: [{ role: "user", content: [{ type: "input_text", text: "hi" }] }], + }; + + const transformed = executor.transformRequest( + "grok-4.6", + body, + false, + {} as Record + ) as Record; + + assert.deepEqual(transformed.reasoning, { effort: "high" }); +}); + +test("grok-4.6 preserves an explicit supported effort", async () => { + const executor = new GrokCliExecutor(); + const body = { + model: "grok-4.6", + input: [{ role: "user", content: [{ type: "input_text", text: "hi" }] }], + reasoning: { effort: "medium", summary: "auto" }, + }; + + const transformed = executor.transformRequest( + "grok-4.6", + body, + false, + {} as Record + ) as Record; + + assert.deepEqual(transformed.reasoning, { effort: "medium", summary: "auto" }); +}); + +test("grok-4.6 strips an explicit but unsupported xhigh (no default restore — an explicit effort key is an explicit choice)", async () => { + const executor = new GrokCliExecutor(); + const body = { + model: "grok-4.6", + input: [{ role: "user", content: [{ type: "input_text", text: "hi" }] }], + reasoning: { effort: "xhigh" }, + reasoning_effort: "xhigh", + }; + + const transformed = executor.transformRequest( + "grok-4.6", + body, + false, + {} as Record + ) as Record; + + assert.equal("reasoning_effort" in transformed, false); + assert.equal("reasoning" in transformed, false); +}); + +test("grok-4.6 keeps an explicit none/off as a real off-switch (no default restore)", async () => { + const executor = new GrokCliExecutor(); + const body = { + model: "grok-4.6", + input: [{ role: "user", content: [{ type: "input_text", text: "hi" }] }], + reasoning: { effort: "none" }, + }; + + const transformed = executor.transformRequest( + "grok-4.6", + body, + false, + {} as Record + ) as Record; + + assert.equal("reasoning" in transformed, false); +}); + +test("grok-4.5 keeps an explicit none/off as a real off-switch (no default restore)", async () => { + const executor = new GrokCliExecutor(); + const body = { + model: "grok-4.5", + input: [{ role: "user", content: [{ type: "input_text", text: "hi" }] }], + reasoning: { effort: "off" }, + }; + + const transformed = executor.transformRequest( + "grok-4.5", + body, + false, + {} as Record + ) as Record; + + assert.equal("reasoning" in transformed, false); +}); diff --git a/tests/unit/http-client-abort-guard-direct-timeout-12861.test.ts b/tests/unit/http-client-abort-guard-direct-timeout-12861.test.ts new file mode 100644 index 0000000000..b50e6fa965 --- /dev/null +++ b/tests/unit/http-client-abort-guard-direct-timeout-12861.test.ts @@ -0,0 +1,144 @@ +// #12861 — the shared process-crash guard (already installed for the +// dev server and the WS/API-bridge servers) needs to also recognize the +// recoverable DIRECT_RESPONSE_START_TIMEOUT code so a stray escaped +// rejection from that path is swallowed and logged instead of taking the +// process down, exactly like a benign client-abort already is. +import test from "node:test"; +import assert from "node:assert/strict"; +import { + isClientAbortError, + isIntentionalComboAbort, + isRecoverableUpstreamTimeoutError, + isUpstreamNetworkError, + shouldSwallowUncaught, +} from "../../src/shared/utils/httpClientAbortGuard.mjs"; + +test("isRecoverableUpstreamTimeoutError recognizes DIRECT_RESPONSE_START_TIMEOUT", () => { + const err = Object.assign(new Error("Direct response did not start within 30000ms"), { + code: "DIRECT_RESPONSE_START_TIMEOUT", + name: "TimeoutError", + }); + assert.equal(isRecoverableUpstreamTimeoutError(err), true); + // A raw string abort reason rejects waiters with the string itself. + assert.equal(isRecoverableUpstreamTimeoutError("DIRECT_RESPONSE_START_TIMEOUT"), true); +}); + +test("isRecoverableUpstreamTimeoutError rejects unrelated error codes", () => { + assert.equal(isRecoverableUpstreamTimeoutError(new Error("boom")), false); + assert.equal( + isRecoverableUpstreamTimeoutError(Object.assign(new Error("x"), { code: "ECONNRESET" })), + false + ); + assert.equal(isRecoverableUpstreamTimeoutError(null), false); + assert.equal(isRecoverableUpstreamTimeoutError(undefined), false); + assert.equal(isRecoverableUpstreamTimeoutError("a string, not an object"), false); +}); + +test("isRecoverableUpstreamTimeoutError does not overlap with isClientAbortError's own codes", () => { + // These two predicates should classify disjoint sets of codes; a + // DIRECT_RESPONSE_START_TIMEOUT is not a client abort and vice versa. + const timeoutErr = { code: "DIRECT_RESPONSE_START_TIMEOUT" }; + assert.equal(isClientAbortError(timeoutErr), false); + assert.equal(isRecoverableUpstreamTimeoutError(timeoutErr), true); + + const abortErr = { code: "ECONNRESET" }; + assert.equal(isClientAbortError(abortErr), true); + assert.equal(isRecoverableUpstreamTimeoutError(abortErr), false); +}); + +test("shouldSwallowUncaught swallows DIRECT_RESPONSE_START_TIMEOUT for uncaughtException and unhandledRejection origins", () => { + const err = Object.assign(new Error("timeout"), { code: "DIRECT_RESPONSE_START_TIMEOUT" }); + assert.equal(shouldSwallowUncaught(err, "uncaughtException"), true); + assert.equal(shouldSwallowUncaught(err, "unhandledRejection"), true); + assert.equal(shouldSwallowUncaught(err, undefined), true); +}); + +test("shouldSwallowUncaught still surfaces genuine errors (no code, no client-abort message)", () => { + const genuineBug = new TypeError("Cannot read properties of undefined"); + assert.equal(shouldSwallowUncaught(genuineBug, "uncaughtException"), false); + assert.equal(shouldSwallowUncaught(genuineBug, "unhandledRejection"), false); +}); + +test("shouldSwallowUncaught still swallows the original client-abort cases (no regression)", () => { + const aborted = new Error("aborted"); + assert.equal(shouldSwallowUncaught(aborted, "uncaughtException"), true); + + const econnreset = Object.assign(new Error("socket hang up"), { code: "ECONNRESET" }); + assert.equal(shouldSwallowUncaught(econnreset, "unhandledRejection"), true); +}); + +test("isIntentionalComboAbort recognizes hedge-cancelled aborts (message and cause variants)", () => { + const byMessage = Object.assign(new Error("hedge-cancelled"), { name: "AbortError" }); + assert.equal(isIntentionalComboAbort(byMessage), true); + + const byCause = Object.assign(new Error("This operation was aborted"), { + name: "AbortError", + cause: "hedge-cancelled", + }); + assert.equal(isIntentionalComboAbort(byCause), true); + + const perModelTimeout = Object.assign(new Error("combo-per-model-timeout"), { + name: "AbortError", + }); + assert.equal(isIntentionalComboAbort(perModelTimeout), true); +}); + +test("isIntentionalComboAbort rejects client aborts with unknown reasons", () => { + const clientGone = Object.assign(new Error("request_signal_aborted"), { name: "AbortError" }); + assert.equal(isIntentionalComboAbort(clientGone), false); + assert.equal(isIntentionalComboAbort(new Error("hedge-cancelled")), false); + assert.equal(isIntentionalComboAbort(null), false); +}); + +test("isIntentionalComboAbort accepts a bare string abort reason", () => { + // AbortSignal.reason is whatever was handed to abort(); a raw string reason + // rejects waiters with the string itself, not an Error object. + assert.equal(isIntentionalComboAbort("hedge-cancelled"), true); + assert.equal(isIntentionalComboAbort("combo-per-model-timeout"), true); + assert.equal(isIntentionalComboAbort("client-gone"), false); + assert.equal(isIntentionalComboAbort(""), false); +}); + +test("isUpstreamNetworkError recognizes fetch failures and proxy unreachable", () => { + const fetchFailed = Object.assign(new TypeError("fetch failed"), { + cause: Object.assign(new Error("socket disconnected"), { code: "ECONNRESET" }), + }); + assert.equal(isUpstreamNetworkError(fetchFailed), true); + + const proxyUnreachable = Object.assign(new TypeError("fetch failed"), { + code: "PROXY_UNREACHABLE", + }); + assert.equal(isUpstreamNetworkError(proxyUnreachable), true); + + const undiciSocket = Object.assign(new Error("other side closed"), { code: "UND_ERR_SOCKET" }); + assert.equal(isUpstreamNetworkError(undiciSocket), true); +}); + +test("isUpstreamNetworkError rejects genuine errors", () => { + assert.equal(isUpstreamNetworkError(new TypeError("Cannot read properties of undefined")), false); + assert.equal(isUpstreamNetworkError(new Error("fetch failedish")), false); + assert.equal(isUpstreamNetworkError(null), false); + assert.equal(isUpstreamNetworkError("a string"), false); +}); + +test("shouldSwallowUncaught swallows the 2026-09-14 agnes-storm crash shapes", () => { + // 06:11:04 exit 7: hedge cancellation escaped while the sibling leg won. + const hedge = Object.assign(new Error("hedge-cancelled"), { name: "AbortError" }); + assert.equal(shouldSwallowUncaught(hedge, "uncaughtException"), true); + assert.equal(shouldSwallowUncaught(hedge, "unhandledRejection"), true); + + // 06:27:38 exit 7: undici fetch failure against a flapping upstream. + const fetchFailed = Object.assign(new TypeError("fetch failed"), { + code: "PROXY_UNREACHABLE", + }); + assert.equal(shouldSwallowUncaught(fetchFailed, "uncaughtException"), true); + assert.equal(shouldSwallowUncaught(fetchFailed, "unhandledRejection"), true); +}); + +test("shouldSwallowUncaught still surfaces genuine bugs after the extension", () => { + const genuineBug = new TypeError("Cannot read properties of undefined"); + assert.equal(shouldSwallowUncaught(genuineBug, "uncaughtException"), false); + + const unknownAbort = Object.assign(new Error("mystery"), { name: "AbortError" }); + assert.equal(shouldSwallowUncaught(unknownAbort, "unhandledRejection"), false); +}); diff --git a/tests/unit/httpClientAbortGuard-default-logger.test.mjs b/tests/unit/httpClientAbortGuard-default-logger.test.mjs index 8154e4d001..f1eac91990 100644 --- a/tests/unit/httpClientAbortGuard-default-logger.test.mjs +++ b/tests/unit/httpClientAbortGuard-default-logger.test.mjs @@ -19,14 +19,14 @@ test("installProcessCrashGuard() with no argument swallows a client abort withou installProcessCrashGuard(); const handlers = process .listeners("uncaughtException") - .filter((fn) => fn.toString().includes("swallowed client-abort")); + .filter((fn) => fn.toString().includes("swallowed benign uncaughtException")); assert.ok(handlers.length > 0, "guard handler must be registered"); const abortErr = Object.assign(new Error("aborted"), { code: "ECONNRESET" }); // A broken default logger (console is an object, not a function) throws // TypeError here — that is what took the production process down. assert.doesNotThrow(() => handlers[0](abortErr, "uncaughtException")); assert.equal(warnings.length, 1, "the swallowed abort must be logged once"); - assert.ok(String(warnings[0][1]).includes("swallowed client-abort")); + assert.ok(String(warnings[0][1]).includes("swallowed benign uncaughtException")); } finally { console.warn = originalWarn; } diff --git a/tests/unit/httpClientAbortGuard.test.mjs b/tests/unit/httpClientAbortGuard.test.mjs index 293e8ae0ca..9345ccef22 100644 --- a/tests/unit/httpClientAbortGuard.test.mjs +++ b/tests/unit/httpClientAbortGuard.test.mjs @@ -202,3 +202,38 @@ test("installProcessCrashGuard still crashes on genuine errors (no over-swallowi assert.notEqual(status, 0, "genuine errors must keep crash semantics"); assert.doesNotMatch(stdout, /SHOULD_NOT_REACH/); }); + +// A swallowed error is the ONLY evidence it ever happened; logging just +// code/message throws away the stack. The logger must receive the full +// error object so the origin stays diagnosable. +test("installProcessCrashGuard logs the full error object for swallowed errors", async () => { + const guardPath = fileURLToPath( + new URL("../../src/shared/utils/httpClientAbortGuard.mjs", import.meta.url) + ); + const script = ` + const { installProcessCrashGuard } = await import(process.argv[1]); + installProcessCrashGuard((level, ...args) => { + console.log( + "LOGARGS", + level, + args.map((a) => (a instanceof Error ? "Error" : typeof a)).join(",") + ); + }); + process.emit( + "unhandledRejection", + Object.assign(new Error("hedge-cancelled"), { name: "AbortError" }), + Promise.resolve() + ); + `; + const { status, stdout } = await new Promise((resolve, reject) => { + const child = spawn(process.execPath, ["--input-type=module", "-e", script, guardPath], { + stdio: ["ignore", "pipe", "pipe"], + }); + let out = ""; + child.stdout.on("data", (d) => (out += d)); + child.on("close", (status) => resolve({ status, stdout: out })); + child.on("error", reject); + }); + assert.equal(status, 0); + assert.match(stdout, /LOGARGS warn string,Error/); +}); diff --git a/tests/unit/i18n-key-completeness.test.ts b/tests/unit/i18n-key-completeness.test.ts new file mode 100644 index 0000000000..bda8b71c12 --- /dev/null +++ b/tests/unit/i18n-key-completeness.test.ts @@ -0,0 +1,63 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { findIncompleteLocales, leafPaths } from "../../scripts/i18n/check-key-completeness.mjs"; + +// Absolute key-set parity between en.json and every locale catalog. Unlike the new-key gate +// (diff-based) and the coverage gate (80 % floor), this one names an ABSENT key regardless of +// when it was added — the defect the batch-1/batch-2 locale PRs (#13044, #13660) shipped. + +const en = { home: { title: "Home", legend: { active: "Active" } }, common: { save: "Save" } }; + +test("leafPaths flattens nested objects into dotted leaves and ignores non-objects", () => { + assert.deepEqual([...leafPaths(en)].sort(), ["common.save", "home.legend.active", "home.title"]); + assert.deepEqual([...leafPaths("not a tree")], []); +}); + +test("a locale with exactly the source key set is not listed", () => { + const gaps = findIncompleteLocales({ + en, + locales: { + pt: { home: { title: "Início", legend: { active: "Ativo" } }, common: { save: "Salvar" } }, + }, + }); + assert.deepEqual(gaps, []); +}); + +test("a __MISSING__ placeholder counts as present — the ratio gate judges its content", () => { + const gaps = findIncompleteLocales({ + en, + locales: { + pt: { + home: { title: "__MISSING__:Home", legend: { active: "Ativo" } }, + common: { save: "Salvar" }, + }, + }, + }); + assert.deepEqual(gaps, []); +}); + +test("absent leaves are reported per locale, sorted, whatever their age", () => { + const gaps = findIncompleteLocales({ + en, + locales: { + km: { home: { title: "ទំព័រដើម" }, common: { save: "រក្សាទុក" } }, + de: { home: { title: "Start", legend: { active: "Aktiv" } }, common: { save: "Speichern" } }, + }, + }); + assert.deepEqual(gaps, [{ locale: "km", missing: ["home.legend.active"], extra: [] }]); +}); + +test("leaves the source dropped are reported as extra, and a wrong shape counts as missing", () => { + const gaps = findIncompleteLocales({ + en, + locales: { + fr: { + home: { title: "Accueil", legend: "Légende" }, + common: { save: "Enregistrer", cancel: "Annuler" }, + }, + }, + }); + assert.deepEqual(gaps, [ + { locale: "fr", missing: ["home.legend.active"], extra: ["common.cancel", "home.legend"] }, + ]); +}); diff --git a/tests/unit/issue-12927-magnific-validation-probe.test.ts b/tests/unit/issue-12927-magnific-validation-probe.test.ts new file mode 100644 index 0000000000..f9cfd8faf2 --- /dev/null +++ b/tests/unit/issue-12927-magnific-validation-probe.test.ts @@ -0,0 +1,80 @@ +// Repro for GitHub issue #12927: the Magnific image-provider key validation probe +// sends GET /v1/ai/mystic, which is a POST-only task-submission route. Once +// authentication passes, the API has no GET handler for that path and returns 404, +// so a VALID key is reported as invalid ("Validation failed: 404"). +// +// Run: DATA_DIR=$(mktemp -d) node --import tsx/esm --test --test-force-exit \ +// tests/unit/issue-12927-magnific-validation-probe.test.ts + +import assert from "node:assert/strict"; +import { test } from "node:test"; + +// Simulates the real Magnific API auth-before-routing behavior described by the +// reporter: a bad key gets 401 on every route; a valid key gets routed and the +// POST-only /v1/ai/mystic path answers 404 to a GET, while /v1/ai/flows answers 200. +function mockMagnificFetch(input: RequestInfo | URL, init?: RequestInit): Promise { + const url = String(input instanceof URL ? input.toString() : input); + const method = String(init?.method || "GET").toUpperCase(); + const headers = new Headers(init?.headers); + const apiKey = headers.get("x-magnific-api-key"); + + if (apiKey !== "valid-magnific-key") { + return Promise.resolve( + new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 }) + ); + } + + if (url === "https://api.magnific.com/v1/ai/mystic" && method === "GET") { + // Valid key, but this route is POST-only -> 404 once routed past auth. + return Promise.resolve(new Response(JSON.stringify({ error: "Not found" }), { status: 404 })); + } + + if (url === "https://api.magnific.com/v1/ai/flows" && method === "GET") { + // The reporter's verified working read-only probe for personal accounts. + return Promise.resolve(new Response(JSON.stringify({ flows: [] }), { status: 200 })); + } + + return Promise.resolve(new Response(JSON.stringify({ error: "unexpected" }), { status: 500 })); +} + +// NOTE: `open-sse/utils/proxyFetch.ts` unconditionally does +// `globalThis.fetch = patchedFetch` as a module-level side effect the first time it is +// imported (directly or transitively). imageValidation.ts pulls it in via +// safeOutboundFetch -> proxyFetch, so assigning our mock to globalThis.fetch BEFORE that +// first import gets silently clobbered. Import first, THEN install the mock so it is what +// `fetchWithTimeout` reads (`fetchFn || globalThis.fetch`, read at call time, not captured). +const { validateImageProviderApiKey } = await import("../../src/lib/providers/imageValidation.ts"); +(globalThis as unknown as { fetch: typeof mockMagnificFetch }).fetch = mockMagnificFetch; + +test("issue #12927: a genuinely valid Magnific key must validate as valid", async () => { + const result = await validateImageProviderApiKey({ + provider: "magnific", + apiKey: "valid-magnific-key", + providerSpecificData: {}, + }); + + // EXPECTED (post-fix): a valid key validates successfully. + // Pre-fix, IMAGE_PROVIDER_VALIDATION_ENDPOINTS.magnific pointed GET at + // /v1/ai/mystic, a POST-only task-submission route. Auth passed but routing 404s, + // so validateImageProviderApiKey() reported `{ valid: false, error: "Validation failed: 404" }` + // for a key that is genuinely valid — the false negative from the issue. + assert.equal( + result.valid, + true, + `expected a valid key to validate as valid, got: ${JSON.stringify(result)}` + ); +}); + +test("control: invalid Magnific key correctly fails with 401 -> Invalid API key", async () => { + const { validateImageProviderApiKey } = + await import("../../src/lib/providers/imageValidation.ts"); + + const result = await validateImageProviderApiKey({ + provider: "magnific", + apiKey: "totally-wrong-key", + providerSpecificData: {}, + }); + + assert.equal(result.valid, false); + assert.equal(result.error, "Invalid API key"); +}); diff --git a/tests/unit/issue-12958-gitlab-duo-403-entitlement-fallback.test.ts b/tests/unit/issue-12958-gitlab-duo-403-entitlement-fallback.test.ts new file mode 100644 index 0000000000..db32b842bc --- /dev/null +++ b/tests/unit/issue-12958-gitlab-duo-403-entitlement-fallback.test.ts @@ -0,0 +1,113 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { testOAuthConnection } from "../../src/app/api/providers/[id]/test/route"; + +// #12958: the reporter has a valid Duo seat and a configured default namespace, but +// gitlab.com returns an entitlement/scope-resolution 403 from `direct_access` for their +// API-only client. That 403 is NOT the "direct connections are disabled" tenant-config +// message the #10365/#10499 fallback guard recognizes, so the connection test never tries +// the public Code Suggestions fallback (which the reporter proved works with the same +// token) and instead reports the connection unhealthy with a generic "Access denied" that +// discards the real upstream body. These tests lock in the corrected contract: ANY +// direct_access 403 is recoverable via the fallback probe (same as 401 already is), and +// when both endpoints genuinely reject the token, the real upstream body is surfaced. + +const DIRECT_ACCESS_URL = "https://gitlab.example.com/api/v4/code_suggestions/direct_access"; +const PUBLIC_COMPLETIONS_URL = "https://gitlab.example.com/api/v4/code_suggestions/completions"; + +function futureExpiresAt(): string { + return new Date(Date.now() + 60 * 60 * 1000).toISOString(); +} + +function baseConnection(overrides: Record = {}) { + return { + provider: "gitlab-duo", + authType: "oauth", + accessToken: "oauth-access", + refreshToken: "oauth-refresh", + expiresAt: futureExpiresAt(), + providerSpecificData: { baseUrl: "https://gitlab.example.com" }, + ...overrides, + }; +} + +function mockFetch(handler: (url: string, init?: RequestInit) => Response) { + const calls: Array<{ url: string; init?: RequestInit }> = []; + const fn = (async (url: RequestInfo | URL, init?: RequestInit) => { + const u = typeof url === "string" ? url : url instanceof URL ? url.toString() : String(url); + calls.push({ url: u, init }); + return handler(u, init); + }) as typeof fetch; + return { fn, calls }; +} + +test("gitlab-duo Retest does NOT fall back on an entitlement-flavored 403 (#12958)", async (t) => { + const original = globalThis.fetch; + const { fn, calls } = mockFetch((url) => { + if (url === DIRECT_ACCESS_URL) { + return new Response(JSON.stringify({ message: "Access denied" }), { + status: 403, + headers: { "content-type": "application/json" }, + }); + } + if (url === PUBLIC_COMPLETIONS_URL) { + return new Response(JSON.stringify({ model: { name: "code-gecko" }, choices: [] }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + throw new Error(`Unexpected fetch to ${url}`); + }); + globalThis.fetch = fn; + t.after(() => { + globalThis.fetch = original; + }); + + const result = await testOAuthConnection(baseConnection(), 5000); + + assert.equal( + result.valid, + true, + "an entitlement-flavored direct_access 403 must also be verified against the public " + + "completions fallback before declaring the connection unhealthy — same contract as " + + "401 and the 'direct connections are disabled' 403" + ); + assert.deepEqual( + calls.map((c) => c.url), + [DIRECT_ACCESS_URL, PUBLIC_COMPLETIONS_URL], + "the fallback probe must be attempted for ANY direct_access 403, not only the exact " + + "'direct connections are disabled' tenant-config message" + ); +}); + +test("gitlab-duo Retest surfaces the real upstream 403 body when BOTH endpoints reject (#12958)", async (t) => { + const original = globalThis.fetch; + const { fn } = mockFetch((url) => { + if (url === DIRECT_ACCESS_URL) { + return new Response(JSON.stringify({ error: "insufficient_scope", scope: "ai_features" }), { + status: 403, + headers: { "content-type": "application/json" }, + }); + } + if (url === PUBLIC_COMPLETIONS_URL) { + return new Response(JSON.stringify({ message: "Access denied" }), { + status: 403, + headers: { "content-type": "application/json" }, + }); + } + throw new Error(`Unexpected fetch to ${url}`); + }); + globalThis.fetch = fn; + t.after(() => { + globalThis.fetch = original; + }); + + const result = await testOAuthConnection(baseConnection(), 5000); + + assert.equal(result.valid, false); + assert.ok( + result.error && result.error.includes("insufficient_scope"), + `expected the real upstream direct_access body to be surfaced, got: ${JSON.stringify(result.error)}` + ); +}); diff --git a/tests/unit/issue-12968-anthropic-shim-empty-text-block.test.ts b/tests/unit/issue-12968-anthropic-shim-empty-text-block.test.ts new file mode 100644 index 0000000000..627b41c5bc --- /dev/null +++ b/tests/unit/issue-12968-anthropic-shim-empty-text-block.test.ts @@ -0,0 +1,39 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { detectMalformedNonStream } from "../../open-sse/utils/diagnostics.ts"; + +// Exact upstream body captured in the issue (freeaiapikey.com probe response). +const upstreamBody = { + content: [{ text: "", type: "text" }], + id: "msg_4d5e123dda0d4eda8055cd21", + model: "anthropic/claude-sonnet-5", + role: "assistant", + stop_reason: "max_tokens", + stop_sequence: null, + type: "message", + usage: { + cache_creation_input_tokens: 2623, + cache_read_input_tokens: 0, + input_tokens: 2234, + output_tokens: 1, + }, +}; + +test("#12968 max_tokens probe with content:[{text:''}] must NOT be flagged empty_choices", () => { + const reason = detectMalformedNonStream(upstreamBody); + assert.equal( + reason, + null, + `expected legitimate truncated-probe response to pass through, got reason=${reason}` + ); +}); + +test("#12968 control — content:[] + max_tokens already exempted (#9971)", () => { + const reason = detectMalformedNonStream({ ...upstreamBody, content: [] }); + assert.equal(reason, null); +}); + +test("#12968 control — empty text block with end_turn stop_reason stays flagged", () => { + const reason = detectMalformedNonStream({ ...upstreamBody, stop_reason: "end_turn" }); + assert.equal(reason, "empty_choices"); +}); diff --git a/tests/unit/issue-13022-nested-skill-schema.test.ts b/tests/unit/issue-13022-nested-skill-schema.test.ts new file mode 100644 index 0000000000..e50571eb84 --- /dev/null +++ b/tests/unit/issue-13022-nested-skill-schema.test.ts @@ -0,0 +1,88 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-issue-13022-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const coreDb = await import("../../src/lib/db/core.ts"); +const { skillRegistry } = await import("../../src/lib/skills/registry.ts"); +const { injectSkills } = await import("../../src/lib/skills/injection.ts"); + +function resetRegistryState() { + skillRegistry["registeredSkills"].clear(); + skillRegistry["versionCache"].clear(); +} + +test.after(() => { + resetRegistryState(); + coreDb.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +test("#13022: nested bare property map survives skill injection for OpenAI-format providers (agnes/nvidia/DeepSeek)", async () => { + await skillRegistry.register({ + name: "nested-tool", + version: "1.0.0", + description: "a skill with a nested bare property map, like a local/skillssh author would ship", + schema: { input: { query: "string", opts: { limit: { type: "number" } } }, output: {} }, + handler: "nested-tool-handler", + enabled: true, + apiKeyId: "issue-13022-key", + }); + + const tools = injectSkills({ provider: "openai", apiKeyId: "issue-13022-key" }) as Array<{ + function: { parameters: Record }; + }>; + assert.equal(tools.length, 1); + + const parameters = tools[0].function.parameters; + const properties = parameters.properties as Record>; + + assert.equal( + properties.query.type, + "string", + "#11881 root shorthand expansion should still work" + ); + + const opts = properties.opts; + assert.equal( + opts.type, + "object", + "BUG #13022: nested bare property map missing type:object wrapper" + ); +}); + +test("#13022: per-property boolean required:true survives skill injection for OpenAI-format providers", async () => { + await skillRegistry.register({ + name: "required-bool-tool", + version: "1.0.0", + description: + "a skill declaring required as a boolean on the property, not an array on the schema", + schema: { input: { content: { type: "string", required: true } }, output: {} }, + handler: "required-bool-handler", + enabled: true, + apiKeyId: "issue-13022-key-2", + }); + + const tools = injectSkills({ provider: "openai", apiKeyId: "issue-13022-key-2" }) as Array<{ + function: { parameters: Record }; + }>; + assert.equal(tools.length, 1); + + const parameters = tools[0].function.parameters; + const properties = parameters.properties as Record>; + + assert.equal( + "required" in properties.content, + false, + "BUG #13022: boolean required:true survived on the property" + ); + assert.deepEqual( + parameters.required, + ["content"], + "BUG #13022: boolean required:true was not promoted" + ); +}); diff --git a/tests/unit/issue-13089-roundrobin-live-ws-events.test.ts b/tests/unit/issue-13089-roundrobin-live-ws-events.test.ts new file mode 100644 index 0000000000..b2d65f7e62 --- /dev/null +++ b/tests/unit/issue-13089-roundrobin-live-ws-events.test.ts @@ -0,0 +1,140 @@ +/** + * Repro for #13089 — Combo Studio Live dashboard shows empty backlog despite + * successful (or failed) chat completions routed through a round-robin combo. + * + * Round-robin bypasses `handleComboChat` -> `executeTargetAttempt` (the path that + * publishes `combo.target.attempt` / `combo.target.succeeded` / `combo.target.failed` + * on the dashboard EventBus) and dispatches through its own loop in + * `open-sse/services/combo/roundRobinCombo.ts`, which never emitted those events. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-13089-")); +const ORIGINAL_DATA_DIR = process.env.DATA_DIR; +process.env.DATA_DIR = TEST_DATA_DIR; + +const { handleComboChat } = await import("../../open-sse/services/combo.ts"); +const rrState = await import("../../open-sse/services/combo/rrState.ts"); +const dbCore = await import("../../src/lib/db/core.ts"); +const eventBus = await import("../../src/lib/events/eventBus.ts"); + +function makeLog() { + return { info() {}, warn() {}, debug() {}, error() {} }; +} + +function rrCombo(name: string, maxRetries = 0) { + return { + name, + strategy: "round-robin", + config: { maxRetries, disableSessionStickiness: true }, + models: [ + { + kind: "model", + provider: "codex", + providerId: "codex", + model: "m-a", + connectionId: "conn-A", + id: `${name}-0`, + }, + ], + }; +} + +test.beforeEach(() => { + rrState.rrCounters.clear(); + rrState.rrStickyTargets.clear(); +}); + +test.after(() => { + try { + dbCore.resetDbInstance?.(); + } catch { + /* ignore */ + } + if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR; + else process.env.DATA_DIR = ORIGINAL_DATA_DIR; + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +test("#13089: a successful round-robin combo completion publishes combo.target.succeeded on the dashboard EventBus", async () => { + const combo = rrCombo("rr13089-success"); + + const seen: Array<{ event: string; payload: unknown }> = []; + const unsubscribeAttempt = eventBus.on("combo.target.attempt", (payload) => { + seen.push({ event: "combo.target.attempt", payload }); + }); + const unsubscribeSucceeded = eventBus.on("combo.target.succeeded", (payload) => { + seen.push({ event: "combo.target.succeeded", payload }); + }); + + try { + const response = await handleComboChat({ + body: { model: combo.name, messages: [{ role: "user", content: "hi" }], stream: false }, + combo, + allCombos: [combo], + isModelAvailable: async () => true, + relayOptions: undefined, + signal: undefined, + settings: {}, + log: makeLog(), + handleSingleModel: async (_b, modelStr, _target) => { + return Response.json({ + choices: [{ message: { role: "assistant", content: modelStr } }], + }); + }, + }); + + assert.equal(response.status, 200, "the combo-routed completion itself must succeed"); + assert.ok( + seen.some((e) => e.event === "combo.target.attempt"), + `expected a "combo.target.attempt" EventBus event, but none was published (saw: ${JSON.stringify(seen)}).` + ); + assert.ok( + seen.some((e) => e.event === "combo.target.succeeded"), + `expected a "combo.target.succeeded" EventBus event after a successful round-robin ` + + `combo completion, but none was published (saw: ${JSON.stringify(seen)}). This is why ` + + `Combo Studio -> Live never shows round-robin combo executions (#13089).` + ); + } finally { + unsubscribeAttempt(); + unsubscribeSucceeded(); + } +}); + +test("#13089: a failing round-robin combo target publishes combo.target.failed on the dashboard EventBus", async () => { + const combo = rrCombo("rr13089-failure"); + + const seen: Array<{ event: string; payload: unknown }> = []; + const unsubscribeFailed = eventBus.on("combo.target.failed", (payload) => { + seen.push({ event: "combo.target.failed", payload }); + }); + + try { + const response = await handleComboChat({ + body: { model: combo.name, messages: [{ role: "user", content: "hi" }], stream: false }, + combo, + allCombos: [combo], + isModelAvailable: async () => true, + relayOptions: undefined, + signal: undefined, + settings: {}, + log: makeLog(), + handleSingleModel: async () => { + return Response.json({ error: { message: "rate limited" } }, { status: 429 }); + }, + }); + + assert.equal(response.status, 429, "the exhausted combo must surface the upstream failure"); + assert.ok( + seen.some((e) => e.event === "combo.target.failed"), + `expected a "combo.target.failed" EventBus event after an exhausted round-robin ` + + `combo target, but none was published (saw: ${JSON.stringify(seen)}).` + ); + } finally { + unsubscribeFailed(); + } +}); diff --git a/tests/unit/issue-13364-zed-hosted-haiku-thinking-inflation.test.ts b/tests/unit/issue-13364-zed-hosted-haiku-thinking-inflation.test.ts new file mode 100644 index 0000000000..43df02bb6e --- /dev/null +++ b/tests/unit/issue-13364-zed-hosted-haiku-thinking-inflation.test.ts @@ -0,0 +1,39 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { openaiToClaudeRequest } from "../../open-sse/translator/request/openai-to-claude.ts"; + +// #13364 — zed-hosted/claude-haiku-4-5: extended thinking + tools inflates +// max_tokens to 163072 (> Anthropic's real 64000 cap for this model), causing +// Zed's proxy to reject the request in-stream with: +// "max_tokens: 163072 > 64000, which is the maximum allowed number of +// output tokens for claude-haiku-4-5-20251001" +// +// Root cause: zed-hosted's passthrough catalog exposes the short hyphenated id +// "claude-haiku-4-5", but modelSpecs' registered alias for that model is the +// dotted "claude-haiku-4.5" — a spelling mismatch (not a missing spec) means +// capMaxOutputTokens() resolves no cap for zed-hosted, so fitThinkingToMaxTokens +// adds the requested budget instead of fitting it under the model's real ceiling. +test("zed-hosted/claude-haiku-4-5 thinking+tools must not inflate max_tokens past the real 64000 output cap", () => { + const body = { + model: "claude-haiku-4-5", + max_tokens: 16000, + reasoning_effort: "high", // what claude-to-openai produces for the client's budget_tokens:15999 + tools: [ + { + type: "function", + function: { name: "read_file", parameters: { type: "object", properties: {} } }, + }, + ], + messages: [{ role: "user", content: "hello" }], + }; + const credentials = { _provider: "zed-hosted" }; + + const result = openaiToClaudeRequest("claude-haiku-4-5", body, false, credentials); + const claudeHaiku45OutputCap = 64000; // src/shared/constants/modelSpecs.ts "claude-haiku-4-5-20251001" + + assert.ok( + (result.max_tokens as number) <= claudeHaiku45OutputCap, + `max_tokens (${result.max_tokens}) must not exceed the model's real output cap ` + + `(${claudeHaiku45OutputCap}) — Anthropic/Zed rejects the request otherwise` + ); +}); diff --git a/tests/unit/issue-13380-gemini-web-system-dropped.test.ts b/tests/unit/issue-13380-gemini-web-system-dropped.test.ts new file mode 100644 index 0000000000..e04d9c7fbe --- /dev/null +++ b/tests/unit/issue-13380-gemini-web-system-dropped.test.ts @@ -0,0 +1,93 @@ +// #13380 — gemini-web drops system instructions on single-turn requests, and +// buildGeminiToolPrompt() picks the CLIENT's first system message instead of +// the appended tool contract when tools are active. +// +// Bug 1: buildGeminiPrompt()'s single-turn fast path +// (open-sse/executors/gemini-web.ts) returned only the last user message, +// silently dropping any system instruction when there was no prior +// user/assistant turn (title generation, structured extraction, one-shot +// chat completions). +// +// Bug 2: buildGeminiToolPrompt() used +// `effectiveMessages.find(m => m.role === "system")`, which returns the +// FIRST system message. `prepareToolMessages()` (open-sse/translator/ +// webTools.ts) appends the synthetic tool contract as the LAST system +// message, so any request that already carries a client system message +// (any real agent request) lost the tool contract entirely. + +import test from "node:test"; +import assert from "node:assert/strict"; + +const { buildGeminiPrompt, buildGeminiToolPrompt } = + await import("../../open-sse/executors/gemini-web.ts"); +const { prepareToolMessages } = await import("../../open-sse/translator/webTools.ts"); + +test("#13380 bug 1: single-turn system + user request retains BOTH contents", () => { + const messages = [ + { role: "system", content: "SYSTEM_SENTINEL\nSECOND_SYSTEM_LINE" }, + { role: "user", content: "USER_SENTINEL" }, + ]; + const prompt = buildGeminiPrompt(messages); + assert.ok(prompt.includes("USER_SENTINEL")); + assert.ok(prompt.includes("SYSTEM_SENTINEL")); +}); + +test("#13380 bug 1: single-turn request with no system message stays byte-for-byte identical", () => { + const messages = [{ role: "user", content: "JUST_THE_USER_MESSAGE" }]; + const prompt = buildGeminiPrompt(messages); + assert.equal(prompt, "JUST_THE_USER_MESSAGE"); +}); + +test("#13380 bug 2: tool-enabled request retains the appended tool contract, not just the client's first system message", () => { + const bodyObj = { + tools: [ + { + type: "function", + function: { + name: "ping", + description: "Return a ping", + parameters: { type: "object", properties: {} }, + }, + }, + ], + }; + const messages = [ + { role: "system", content: "CLIENT_SYSTEM_SENTINEL" }, + { role: "user", content: "USER_SENTINEL" }, + ]; + + const { effectiveMessages } = prepareToolMessages(bodyObj, messages); + const systemMessages = effectiveMessages.filter((m: { role: string }) => m.role === "system"); + assert.ok(systemMessages.length >= 2); + + const prompt = buildGeminiToolPrompt(effectiveMessages); + assert.ok(prompt.includes("Return a ping")); +}); + +test("#13380 bug 2: tool-enabled request preserves order — client system message(s) before the appended tool contract", () => { + const bodyObj = { + tools: [ + { + type: "function", + function: { + name: "ping", + description: "Return a ping", + parameters: { type: "object", properties: {} }, + }, + }, + ], + }; + const messages = [ + { role: "system", content: "CLIENT_SYSTEM_LINE_1\nCLIENT_SYSTEM_LINE_2" }, + { role: "user", content: "USER_SENTINEL" }, + ]; + + const { effectiveMessages } = prepareToolMessages(bodyObj, messages); + const prompt = buildGeminiToolPrompt(effectiveMessages); + + const clientIdx = prompt.indexOf("CLIENT_SYSTEM_LINE_1"); + const contractIdx = prompt.indexOf("Return a ping"); + assert.ok(clientIdx !== -1, "client system message must be present"); + assert.ok(contractIdx !== -1, "tool contract must be present"); + assert.ok(clientIdx < contractIdx, "client system message must come before the tool contract"); +}); diff --git a/tests/unit/issue-13389-catalog-cache-backoff-reset.test.ts b/tests/unit/issue-13389-catalog-cache-backoff-reset.test.ts new file mode 100644 index 0000000000..5852d37e66 --- /dev/null +++ b/tests/unit/issue-13389-catalog-cache-backoff-reset.test.ts @@ -0,0 +1,86 @@ +/** + * #13389 — GET /v1/models intermittently takes 75-120s or returns 502. + * + * Must-fix half of the issue: `resetConnectionBackoff()` (`src/lib/db/providers.ts`) + * fires automatically whenever a previously-cooled-down connection is + * auto-recovered during normal request routing (`src/sse/services/auth.ts`), and + * previously busted the *entire* `/v1/models` response cache as a side effect — + * even though the catalog builder (`src/app/api/v1/models/catalog.ts`) never reads + * backoff/cooldown/error state at all (only structural fields like + * `excludedModels` or enabled/disabled). On a deployment routing many providers, + * this invalidated the catalog cache far more often than its 60s TTL / 30s + * stale-while-revalidate window intends, purely as a side effect of unrelated + * chat traffic, forcing frequent expensive cold rebuilds. + * + * This regression test asserts a `resetConnectionBackoff()` call does NOT bump + * `getModelCatalogCacheVersion()`, while a genuinely structural connection write + * (`updateProviderConnection`) still does. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-13389-cache-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = "test-13389-catalog-cache-backoff-reset-secret"; + +const core = await import("../../src/lib/db/core.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const readCache = await import("../../src/lib/db/readCache.ts"); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +async function createBackedOffConnection() { + const created = await providersDb.createProviderConnection({ + provider: "glm", + authType: "apikey", + name: `GLM Backoff 13389 ${Date.now()}-${Math.random()}`, + apiKey: "glm-test-key", + }); + const connectionId = (created as { id: string }).id; + await providersDb.updateProviderConnection(connectionId, { + testStatus: "unavailable", + lastError: "rate limit exceeded", + lastErrorType: "rate_limited", + lastErrorSource: "executor", + errorCode: 429, + backoffLevel: 3, + }); + return connectionId; +} + +test("#13389 resetConnectionBackoff does NOT bust the model catalog cache", async () => { + const connectionId = await createBackedOffConnection(); + + const versionBeforeReset = readCache.getModelCatalogCacheVersion(); + + await providersDb.resetConnectionBackoff(connectionId); + + assert.equal( + readCache.getModelCatalogCacheVersion(), + versionBeforeReset, + "a pure backoff/error-state reset must not invalidate the model catalog cache — " + + "the catalog builder never reads backoffLevel/testStatus/rateLimitedUntil" + ); +}); + +test("#13389 a structural connection write still busts the model catalog cache", async () => { + const connectionId = await createBackedOffConnection(); + + const versionBeforeUpdate = readCache.getModelCatalogCacheVersion(); + + // excludedModels is catalog-relevant (structural) — must still invalidate. + await providersDb.updateProviderConnection(connectionId, { + excludedModels: ["some-model-id"], + }); + + assert.ok( + readCache.getModelCatalogCacheVersion() > versionBeforeUpdate, + "a structural connection write (e.g. excludedModels) must still invalidate the model catalog cache" + ); +}); diff --git a/tests/unit/issue-13429-lite-redundant-remove-tool-call-id.test.ts b/tests/unit/issue-13429-lite-redundant-remove-tool-call-id.test.ts new file mode 100644 index 0000000000..00bcd19b31 --- /dev/null +++ b/tests/unit/issue-13429-lite-redundant-remove-tool-call-id.test.ts @@ -0,0 +1,96 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { applyLiteCompression } from "../../open-sse/services/compression/lite.ts"; + +// Issue #13429: lite compression's redundant-remove step collapses consecutive +// role:"tool" messages with identical content WITHOUT consulting tool_call_id. +// When two tool results are byte-identical (e.g. both empty strings), the +// second is dropped, orphaning one of the assistant message's tool_call_ids. +// Strict upstream validators (DeepSeek-class) then reject the request with +// "insufficient tool messages". +test("issue #13429: redundant-remove must not drop a tool message that has a distinct tool_call_id", () => { + const body = { + model: "test-model", + messages: [ + { + role: "assistant", + content: "t", + tool_calls: [ + { id: "c1", type: "function", function: { name: "f", arguments: "{}" } }, + { id: "c2", type: "function", function: { name: "g", arguments: "{}" } }, + ], + }, + { role: "tool", tool_call_id: "c1", content: "" }, + { role: "tool", tool_call_id: "c2", content: "" }, + ], + }; + + const result = applyLiteCompression(body); + const messages = (result.body as { messages: Array> }).messages; + + const toolMessages = messages.filter((m) => m.role === "tool"); + const toolCallIds = toolMessages.map((m) => m.tool_call_id); + + assert.equal( + toolMessages.length, + 2, + `expected 2 tool messages to survive redundant-remove, got ${toolMessages.length} (techniques: ${JSON.stringify( + result.stats?.techniquesUsed ?? [] + )})` + ); + assert.deepEqual(new Set(toolCallIds), new Set(["c1", "c2"])); +}); + +// Compound arm: compressToolResults truncates long tool content to a shared +// 2000-char prefix BEFORE redundant-remove runs, so two results that started +// distinct can still collide into the same string. Both must still survive. +test("issue #13429: tool messages that collide only after truncation must both survive", () => { + const longA = "A".repeat(2500); + const longB = "A".repeat(2000) + "B".repeat(500); + const body = { + model: "test-model", + messages: [ + { + role: "assistant", + content: "t", + tool_calls: [ + { id: "c1", type: "function", function: { name: "f", arguments: "{}" } }, + { id: "c2", type: "function", function: { name: "g", arguments: "{}" } }, + ], + }, + { role: "tool", tool_call_id: "c1", content: longA }, + { role: "tool", tool_call_id: "c2", content: longB }, + ], + }; + + const result = applyLiteCompression(body); + const messages = (result.body as { messages: Array> }).messages; + + const toolMessages = messages.filter((m) => m.role === "tool"); + const toolCallIds = toolMessages.map((m) => m.tool_call_id); + + assert.equal( + toolMessages.length, + 2, + "both tool messages must survive despite truncated collision" + ); + assert.deepEqual(new Set(toolCallIds), new Set(["c1", "c2"])); +}); + +// Non-regression: redundant-remove must still collapse adjacent identical +// non-tool messages (e.g. duplicate user turns) — the fix is scoped to the +// "tool" role only, not a blanket disable of the technique. +test("issue #13429: redundant-remove still collapses adjacent identical user messages", () => { + const body = { + model: "test-model", + messages: [ + { role: "user", content: "same text" }, + { role: "user", content: "same text" }, + ], + }; + + const result = applyLiteCompression(body); + const messages = (result.body as { messages: Array> }).messages; + + assert.equal(messages.length, 1, "duplicate non-tool messages should still be collapsed"); +}); diff --git a/tests/unit/issue-13431-responses-post-keepalive-error-frame.test.ts b/tests/unit/issue-13431-responses-post-keepalive-error-frame.test.ts new file mode 100644 index 0000000000..50d2e4e07b --- /dev/null +++ b/tests/unit/issue-13431-responses-post-keepalive-error-frame.test.ts @@ -0,0 +1,172 @@ +/** + * Regression test for #13431. + * + * `withEarlyStreamKeepalive`'s dynamic real-upstream-body branch + * (`open-sse/utils/earlyStreamKeepalive.ts`) only distinguished Anthropic's named + * `event: error` framing from a plain `data:` line. It did not distinguish Chat + * Completions' `data: {"error":...}` shape from Responses' `data: {"type":"error",...}` + * shape, so on `/v1/responses` the raw upstream body (Chat-Completions-shaped) went out + * untouched, with no top-level `type` field. Responses clients (openai-python's Responses + * stream iterator, Codex's own SSE parser) dispatch on `type` and silently drop a frame + * without it, so the stream ends with no `response.completed`/`response.failed` and the + * client reports "stream disconnected before completion" instead of the real upstream + * error. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + withEarlyStreamKeepalive, + OPENAI_RESPONSES_ERROR_FRAME, + OPENAI_CHAT_ERROR_FRAME, + ANTHROPIC_PING_FRAME, +} from "../../open-sse/utils/earlyStreamKeepalive.ts"; + +async function readAll(response: Response): Promise { + const reader = response.body!.getReader(); + const decoder = new TextDecoder(); + let out = ""; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + out += decoder.decode(value); + } + return out; +} + +function lastDataPayload(body: string): Record { + const dataLines = [...body.matchAll(/^data: (.+)$/gm)].map((m) => m[1]); + return JSON.parse(dataLines[dataLines.length - 1]); +} + +test("Responses route: post-keepalive JSON error body must carry a `type` field (#13431)", async () => { + // Shape actually produced by combo failure (Chat-Completions-shaped: top-level + // `error` key, no `type` discriminator) — this is the real body from the issue. + const upstreamErrorBody = JSON.stringify({ + error: { + message: 'Unknown name "encrypted" ... Cannot find field.', + type: "invalid_request_error", + code: "bad_request", + }, + diagnostics: { attempted: 9, terminalReason: "[400]: ..." }, + }); + + const slowFail = new Promise((resolve) => { + setTimeout( + () => + resolve( + new Response(upstreamErrorBody, { + status: 400, + headers: { "Content-Type": "application/json" }, + }) + ), + 80 + ); + }); + + const result = await withEarlyStreamKeepalive(slowFail, { + thresholdMs: 20, + intervalMs: 20, + errorFrame: OPENAI_RESPONSES_ERROR_FRAME, // exactly what src/app/api/v1/responses/route.ts passes + }); + + assert.equal(result.status, 200, "already committed to 200 SSE before the error surfaced"); + + const lastPayload = lastDataPayload(await readAll(result)); + + assert.ok( + typeof lastPayload.type === "string" && lastPayload.type.length > 0, + `Responses API events must be discriminated by a top-level \`type\` field; ` + + `got ${JSON.stringify(lastPayload)} — a Responses client (Codex) drops any ` + + `frame without \`type\` and reports "stream disconnected before completion" ` + + `instead of surfacing the real upstream error.` + ); + assert.equal(lastPayload.type, "error"); + assert.equal(lastPayload.message, 'Unknown name "encrypted" ... Cannot find field.'); + assert.equal(lastPayload.code, "bad_request"); +}); + +test("Responses route: non-JSON/empty post-keepalive error body falls back to a safe `type:error` frame (#13431)", async () => { + const slowFail = new Promise((resolve) => { + setTimeout( + () => + resolve( + new Response("not json at all", { + status: 502, + headers: { "Content-Type": "text/plain" }, + }) + ), + 80 + ); + }); + + const result = await withEarlyStreamKeepalive(slowFail, { + thresholdMs: 20, + intervalMs: 20, + errorFrame: OPENAI_RESPONSES_ERROR_FRAME, + }); + + const lastPayload = lastDataPayload(await readAll(result)); + + assert.equal(lastPayload.type, "error"); + assert.ok( + typeof lastPayload.message === "string" && lastPayload.message.length > 0, + `fallback frame must never be opaque/empty; got ${JSON.stringify(lastPayload)}` + ); +}); + +test("Chat Completions route: post-keepalive JSON error body stays verbatim pass-through (regression guard) (#13431)", async () => { + const upstreamErrorBody = JSON.stringify({ + error: { message: "boom", type: "invalid_request_error", code: "bad_request" }, + }); + + const slowFail = new Promise((resolve) => { + setTimeout( + () => + resolve( + new Response(upstreamErrorBody, { + status: 400, + headers: { "Content-Type": "application/json" }, + }) + ), + 80 + ); + }); + + const result = await withEarlyStreamKeepalive(slowFail, { + thresholdMs: 20, + intervalMs: 20, + errorFrame: OPENAI_CHAT_ERROR_FRAME, + }); + + const lastPayload = lastDataPayload(await readAll(result)); + + // Unchanged: verbatim pass-through, top-level `error` key, no reshaping. + assert.equal(lastPayload.type, undefined); + assert.equal((lastPayload as { error: { message: string } }).error.message, "boom"); +}); + +test("Anthropic /v1/messages route: post-keepalive named event: error framing stays unaffected (regression guard) (#13431)", async () => { + const slowFail = new Promise((resolve) => { + setTimeout( + () => + resolve( + new Response(JSON.stringify({ type: "error", error: { message: "boom" } }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }) + ), + 80 + ); + }); + + const result = await withEarlyStreamKeepalive(slowFail, { + thresholdMs: 20, + intervalMs: 20, + keepaliveFrame: ANTHROPIC_PING_FRAME, + // default errorFrame (Anthropic `event: error`) is used when omitted. + }); + + const body = await readAll(result); + assert.match(body, /^event: error\n/m, "Anthropic path must keep its named SSE event line"); +}); diff --git a/tests/unit/issue-13432-auto-vacuum-drift.test.ts b/tests/unit/issue-13432-auto-vacuum-drift.test.ts new file mode 100644 index 0000000000..6008e06acd --- /dev/null +++ b/tests/unit/issue-13432-auto-vacuum-drift.test.ts @@ -0,0 +1,138 @@ +/** + * Regression test for #13432 — migration 046 seeds + * `databaseSettings.autoVacuumMode = "INCREMENTAL"` into the key_value config + * store for every database it runs against, including pre-existing ones, but + * never touches the live SQLite `auto_vacuum` pragma (that requires a + * `VACUUM`). The only automatic startup path + * (`applyStoredDatabaseOptimizationSettings`) deliberately skips applying it + * synchronously — reapplying it inline at boot would reintroduce the + * blocking-VACUUM-on-a-multi-GB-database hazard tracked by #12821. + * + * Fix: detect the drift at startup (never fix it synchronously there), log + + * persist it, and let the existing out-of-request vacuumScheduler reconcile + * it (pragma flip + one-time conversion VACUUM) on its next scheduled run. + * Once auto_vacuum is actually INCREMENTAL, subsequent scheduled runs use a + * bounded `PRAGMA incremental_vacuum` batch instead of an unconditional full + * `VACUUM`. + * + * DB isolation pattern mirrors tests/unit/db/vacuum-scheduler.test.ts: + * temp DATA_DIR, resetDbInstance() before each test, cleanup in test.after(). + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-13432-")); +const originalDataDir = process.env.DATA_DIR; + +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +core.resetDbInstance(); + +const optimizationSettings = await import("../../src/lib/db/optimizationSettings.ts"); +const scheduler = await import("../../src/lib/db/vacuumScheduler.ts"); + +function seedMigration046Config(mode: "NONE" | "FULL" | "INCREMENTAL") { + // Simulate src/lib/db/migrations/046_database_settings.sql:41 running + // against a pre-existing database: it only ever touches the config store. + const db = core.getDbInstance(); + db.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run( + "databaseSettings", + "autoVacuumMode", + JSON.stringify(mode) + ); +} + +function readPersistedDriftRecord(): unknown { + const db = core.getDbInstance(); + const row = db + .prepare("SELECT value FROM key_value WHERE namespace = ? AND key = ?") + .get("scheduler", "vacuumDrift") as { value: string } | undefined; + return row ? JSON.parse(row.value) : undefined; +} + +test.beforeEach(() => { + scheduler.__resetForTests(); + const db = core.getDbInstance(); + db.prepare("DELETE FROM key_value WHERE namespace IN ('scheduler', 'databaseSettings')").run(); +}); + +test.after(() => { + scheduler.__resetForTests(); + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + if (originalDataDir === undefined) delete process.env.DATA_DIR; + else process.env.DATA_DIR = originalDataDir; +}); + +test("issue #13432: startup drift detection records the mismatch WITHOUT running a blocking VACUUM", () => { + const db = core.getDbInstance(); + + const initialLiveMode = optimizationSettings.getAutoVacuumModeForDb(db); + assert.equal(initialLiveMode, "NONE", "sanity: fresh test DB defaults to auto_vacuum=NONE"); + + seedMigration046Config("INCREMENTAL"); + + // Simulate the next app startup (src/lib/db/core.ts calls this on every boot). + optimizationSettings.applyStoredDatabaseOptimizationSettings(db); + + // The core bug: the live pragma must NOT have been synchronously flipped — + // that is the #12821 blocking-VACUUM-at-boot hazard this fix must avoid. + assert.equal( + optimizationSettings.getAutoVacuumModeForDb(db), + "NONE", + "startup path must never run a synchronous VACUUM to fix drift" + ); + + // But the drift must now be detected and persisted for the scheduler to + // reconcile out-of-request. + const persisted = readPersistedDriftRecord(); + assert.deepEqual(persisted, { configured: "INCREMENTAL", live: "NONE" }); +}); + +test("issue #13432: vacuumScheduler.runNow() reconciles a pending startup drift out-of-request", async () => { + const db = core.getDbInstance(); + seedMigration046Config("INCREMENTAL"); + optimizationSettings.applyStoredDatabaseOptimizationSettings(db); + assert.deepEqual(readPersistedDriftRecord(), { configured: "INCREMENTAL", live: "NONE" }); + + scheduler.init(); + try { + const result = await scheduler.runNow(); + assert.equal(result.success, true); + + // The scheduler's bounded, out-of-request reconcile run is where the + // one-time conversion VACUUM is allowed to happen. + assert.equal(optimizationSettings.getAutoVacuumModeForDb(db), "INCREMENTAL"); + assert.equal(scheduler.getState().autoVacuumDrift, null); + assert.equal(readPersistedDriftRecord(), null); + } finally { + scheduler.stop(); + } +}); + +test("issue #13432: once INCREMENTAL is actually in effect and no drift remains, runNow() uses a bounded PRAGMA incremental_vacuum instead of a full VACUUM", async () => { + const db = core.getDbInstance(); + seedMigration046Config("INCREMENTAL"); + optimizationSettings.applyStoredDatabaseOptimizationSettings(db); + + scheduler.init(); + try { + // First run reconciles the drift (pragma flip + one-time VACUUM). + await scheduler.runNow(); + assert.equal(optimizationSettings.getAutoVacuumModeForDb(db), "INCREMENTAL"); + assert.equal(scheduler.getState().autoVacuumDrift, null); + + // Second run: no drift left, live mode is INCREMENTAL — must take the + // bounded incremental-vacuum path (reported via lastReclaimedPages), + // never the unconditional full VACUUM this scheduler used to always run. + const second = await scheduler.runNow(); + assert.equal(second.success, true); + assert.equal(typeof scheduler.getState().lastReclaimedPages, "number"); + } finally { + scheduler.stop(); + } +}); diff --git a/tests/unit/issue-13452-node-baseurl-ignored.test.ts b/tests/unit/issue-13452-node-baseurl-ignored.test.ts new file mode 100644 index 0000000000..c985f4f92a --- /dev/null +++ b/tests/unit/issue-13452-node-baseurl-ignored.test.ts @@ -0,0 +1,66 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { DefaultExecutor } from "../../open-sse/executors/default.ts"; +import { BaseExecutor } from "../../open-sse/executors/base.ts"; + +// Issue #13452 — Bug 2: a provider-node ("openai-compatible-*" / +// "anthropic-compatible-*") connection whose credentials carry no +// providerSpecificData.baseUrl (e.g. a connection created any way other than +// the exact POST /api/providers hydration branch, or the node-update +// backfill loop) used to silently fall back to the literal +// "https://api.openai.com/v1" / "https://api.anthropic.com/v1" instead of +// erroring or re-resolving the node's configured baseUrl. Real traffic meant +// for a local OpenAI-compatible endpoint (Ollama/vLLM/LM Studio) was instead +// sent to the real OpenAI API, carrying whatever string was stored as the +// "API key" as a Bearer token to a public third party. +// +// Fix: buildUrl() now fails loudly (throws) instead of defaulting, and the +// credential-selection read path (src/sse/services/auth.ts) self-heals by +// re-joining provider_nodes before a request ever reaches buildUrl(). + +test("issue #13452: DefaultExecutor openai-compatible buildUrl must not silently fall back to the real OpenAI API when providerSpecificData.baseUrl is absent", () => { + const nodeId = "openai-compatible-chat-test-node"; + const executor = new DefaultExecutor(nodeId); + + // Simulates the credentials row a hand-created / non-hydrated connection + // produces: no providerSpecificData.baseUrl at all, even though the node + // itself (in provider_nodes) has baseUrl = "http://localhost:11434/v1". + const credentialsWithoutHydration = { apiKey: "ollama" }; + + assert.throws( + () => executor.buildUrl("qwen3.6:35b-a3b", false, 0, credentialsWithoutHydration), + /baseUrl/, + "buildUrl() must fail loudly instead of silently defaulting an unhydrated openai-compatible " + + "connection to the real OpenAI API — this WAS the reported bug (#13452)" + ); +}); + +test("issue #13452: BaseExecutor openai-compatible buildUrl must not silently fall back to the real OpenAI API when providerSpecificData.baseUrl is absent", () => { + const executor = new BaseExecutor("openai-compatible-responses-test-node", {}); + + assert.throws(() => executor.buildUrl("gpt-5.4", true, 0, { apiKey: "local-key" }), /baseUrl/); +}); + +test("issue #13452: DefaultExecutor anthropic-compatible buildUrl must not silently fall back to the real Anthropic API when providerSpecificData.baseUrl is absent", () => { + const executor = new DefaultExecutor("anthropic-compatible-test-node"); + + assert.throws( + () => executor.buildUrl("claude-sonnet-4-6", true, 0, { apiKey: "local-key" }), + /baseUrl/ + ); +}); + +test("issue #13452 (control): providing providerSpecificData.baseUrl routes correctly (confirms the fallback, not buildUrl() itself, was the defect)", () => { + const nodeId = "openai-compatible-chat-test-node-2"; + const executor = new DefaultExecutor(nodeId); + + const hydratedCredentials = { + apiKey: "ollama", + providerSpecificData: { baseUrl: "http://localhost:11434/v1" }, + }; + + const url = executor.buildUrl("qwen3.6:35b-a3b", false, 0, hydratedCredentials); + + assert.equal(url, "http://localhost:11434/v1/chat/completions"); +}); diff --git a/tests/unit/issue-13470-token-refresh-proxy-bypass.test.ts b/tests/unit/issue-13470-token-refresh-proxy-bypass.test.ts new file mode 100644 index 0000000000..0edff80846 --- /dev/null +++ b/tests/unit/issue-13470-token-refresh-proxy-bypass.test.ts @@ -0,0 +1,138 @@ +/** + * TDD — #13470 (background OAuth token refresh bypasses the #6246 dead-pool guard). + * + * The interactive chat/executor path fails closed via `safeResolveProxy` + + * `hasBlockingProxyAssignment` (`src/sse/handlers/chatHelpers.ts`) when a + * connection's assigned proxy pool has gone fully dead — see + * `tests/unit/proxy-assigned-unavailable-6246.test.ts`. The background + * token-refresh path (`src/sse/services/tokenRefresh.ts::resolveProxyForCredentials`) + * and the health-check sweep (`src/lib/tokenHealthCheck.ts`) instead called + * `resolveProxyForConnection` directly and silently fell through to + * direct/env-proxy egress for the SAME connection state — an IP-provenance leak + * of the same class #6246 closed, on a more sensitive payload (the refresh + * token). This proves both background paths now fail closed identically to the + * guarded chat path. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-issue-13470-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = "test-secret"; +delete process.env.PROXY_FAIL_OPEN; + +const core = await import("../../src/lib/db/core.ts"); +const proxiesDb = await import("../../src/lib/db/proxies.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const chatHelpers = await import("../../src/sse/handlers/chatHelpers.ts"); +const tokenRefresh = await import("../../src/sse/services/tokenRefresh.ts"); +const tokenHealthCheckProxyGuard = await import("../../src/lib/tokenHealthCheckProxyGuard.ts"); + +async function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +async function makeConnectionWithDeadAssignedPool(): Promise { + const conn = await providersDb.createProviderConnection({ + provider: "claude", + authType: "oauth", + name: `Conn ${Date.now()} ${Math.random()}`, + accessToken: "at-test", + refreshToken: "rt-test", + }); + const connId = (conn as { id: string }).id; + + const proxy = await proxiesDb.createProxy({ + name: "Dead account proxy pool member", + type: "http", + host: "127.0.0.1", + port: 9470, + }); + await proxiesDb.updateProxy(proxy!.id, { status: "inactive" }); + await proxiesDb.assignProxyToScope("account", connId, proxy!.id); + + return connId; +} + +test.after(async () => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +test("#13470: background token-refresh fails closed for a dead assigned proxy pool, matching the guarded chat path", async () => { + await resetStorage(); + const connId = await makeConnectionWithDeadAssignedPool(); + + assert.equal(proxiesDb.hasBlockingProxyAssignment(connId), true); + + // Guarded chat/executor path already fails closed (#6246 regression) for this + // exact connection state — establishes the baseline the refresh path must match. + await assert.rejects( + () => chatHelpers.safeResolveProxy(connId), + (err: unknown) => { + assert.match((err as Error).message, /PROXY_ASSIGNED_UNAVAILABLE/); + return true; + } + ); + + // The shared proxy-resolution helper behind every exported refresh function + // (refreshAccessToken/refreshClaudeOAuthToken/getAccessToken/etc.) must now + // reject identically instead of silently resolving to direct/env-proxy egress. + // Testing it directly (rather than through refreshAccessToken, which never + // throws on a network-level failure and would hit a real OAuth endpoint here) + // isolates the #13470 guard from network behavior. + await assert.rejects( + () => tokenRefresh.resolveProxyForCredentials("claude", { connectionId: connId }), + (err: unknown) => { + assert.match((err as Error).message, /PROXY_ASSIGNED_UNAVAILABLE/); + return true; + } + ); +}); + +test("#13470: token-refresh proxy resolution stays direct for a connection with no proxy assignment at all (legitimate direct, not a regression)", async () => { + await resetStorage(); + const conn = await providersDb.createProviderConnection({ + provider: "claude", + authType: "oauth", + name: `Conn ${Date.now()} ${Math.random()}`, + accessToken: "at-test", + refreshToken: "rt-test", + }); + const connId = (conn as { id: string }).id; + + assert.equal(proxiesDb.hasBlockingProxyAssignment(connId), false); + + // No assignment at all — resolveProxyForCredentials must resolve (fall through + // to resolveProxyForProvider) rather than reject with the #13470 guard. + await assert.doesNotReject(() => + tokenRefresh.resolveProxyForCredentials("claude", { connectionId: connId }) + ); +}); + +test("#13470: token-health-check sweep skips a connection whose assigned proxy pool is dead instead of refreshing through direct/env-proxy egress", async () => { + await resetStorage(); + const connId = await makeConnectionWithDeadAssignedPool(); + + const { blocked } = await tokenHealthCheckProxyGuard.resolveGuardedProxyConfig(connId, "claude"); + assert.equal(blocked, true, "dead assigned pool must block the health-check refresh cycle"); +}); + +test("#13470: token-health-check sweep is unaffected for a connection with no proxy assignment at all", async () => { + await resetStorage(); + const conn = await providersDb.createProviderConnection({ + provider: "openai", + authType: "apiKey", + name: `Conn ${Date.now()} ${Math.random()}`, + apiKey: "sk-test", + }); + const connId = (conn as { id: string }).id; + + const { blocked } = await tokenHealthCheckProxyGuard.resolveGuardedProxyConfig(connId, "openai"); + assert.equal(blocked, false, "no assignment = legitimate direct, must not block"); +}); diff --git a/tests/unit/issue-13472-responses-cache-creation.test.ts b/tests/unit/issue-13472-responses-cache-creation.test.ts new file mode 100644 index 0000000000..564401fe60 --- /dev/null +++ b/tests/unit/issue-13472-responses-cache-creation.test.ts @@ -0,0 +1,76 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { openaiToOpenAIResponsesResponse } from "../../open-sse/translator/response/openai-responses.ts"; +import { + getPromptCacheCreationTokens, + getPromptCacheReadTokens, +} from "../../src/lib/usage/tokenAccounting.ts"; + +test("#13472: cache_creation tokens survive the chat-completions -> responses-api usage hop (first call, cache WRITE)", () => { + const state: Record = {}; + const chunkFromClaudeHop = { + id: "chatcmpl-abc", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + usage: { + prompt_tokens: 5000, + completion_tokens: 20, + total_tokens: 5020, + prompt_tokens_details: { cached_tokens: 0, cache_creation_tokens: 4800 }, + }, + }; + openaiToOpenAIResponsesResponse(chunkFromClaudeHop, state); + const responsesUsage = state.usage as Record; + const loggedCacheCreation = getPromptCacheCreationTokens(responsesUsage); + assert.ok( + loggedCacheCreation > 0, + `expected cache_creation tokens (4800) to survive the responses-api usage hop, got ${loggedCacheCreation}. state.usage was: ${JSON.stringify(responsesUsage)}` + ); +}); + +test("#13472 (control): cache READ tokens DO survive the same hop (second call, cache HIT)", () => { + const state: Record = {}; + const chunkFromClaudeHop = { + id: "chatcmpl-def", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + usage: { + prompt_tokens: 5000, + completion_tokens: 20, + total_tokens: 5020, + prompt_tokens_details: { cached_tokens: 4800, cache_creation_tokens: 0 }, + }, + }; + openaiToOpenAIResponsesResponse(chunkFromClaudeHop, state); + const responsesUsage = state.usage as Record; + const loggedCacheRead = getPromptCacheReadTokens(responsesUsage); + assert.equal( + loggedCacheRead, + 4800, + `expected cache_read tokens to survive, got ${loggedCacheRead}` + ); +}); + +test("#13472 (mixed): cache READ and cache CREATION reported together are both preserved (no clobbering)", () => { + const state: Record = {}; + const chunkFromClaudeHop = { + id: "chatcmpl-ghi", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + usage: { + prompt_tokens: 6000, + completion_tokens: 20, + total_tokens: 6020, + prompt_tokens_details: { cached_tokens: 1200, cache_creation_tokens: 800 }, + }, + }; + openaiToOpenAIResponsesResponse(chunkFromClaudeHop, state); + const responsesUsage = state.usage as Record; + assert.equal( + getPromptCacheReadTokens(responsesUsage), + 1200, + "cache_read should not be clobbered" + ); + assert.equal( + getPromptCacheCreationTokens(responsesUsage), + 800, + "cache_creation should be preserved alongside cache_read" + ); +}); diff --git a/tests/unit/issue-13488-pii-openrouter-metadata-splice.test.ts b/tests/unit/issue-13488-pii-openrouter-metadata-splice.test.ts new file mode 100644 index 0000000000..6f9de4064a --- /dev/null +++ b/tests/unit/issue-13488-pii-openrouter-metadata-splice.test.ts @@ -0,0 +1,227 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +// Isolate DB state +const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-test-issue-13488-")); +process.env.DATA_DIR = tmpDir; + +// Enable the feature flag for tests (mode "warn" per the issue repro steps — "nothing is +// supposed to be modified", yet the windowed re-emission still runs and scrambles output). +const originalEnv = process.env.PII_RESPONSE_SANITIZATION; +const originalMode = process.env.PII_RESPONSE_SANITIZATION_MODE; +process.env.PII_RESPONSE_SANITIZATION = "true"; +process.env.PII_RESPONSE_SANITIZATION_MODE = "warn"; +process.env.PII_TEST_BYPASS_MIN_WINDOW = "true"; + +import { createPiiSseTransform } from "../../src/lib/streamingPiiTransform.ts"; + +async function testTransform(transform: TransformStream, inputChunks: string[]): Promise { + const writer = transform.writable.getWriter(); + const reader = transform.readable.getReader(); + + const writePromise = (async () => { + for (const chunk of inputChunks) { + await writer.write(new TextEncoder().encode(chunk)); + } + await writer.close(); + })(); + + const outputChunks: string[] = []; + let res = await reader.read(); + while (!res.done) { + outputChunks.push(new TextDecoder().decode(res.value)); + res = await reader.read(); + } + + await writePromise; + return outputChunks.join(""); +} + +function extractContentAndMetadata(output: string, metadataKey: string) { + const dataLines = output + .split("\n") + .filter((l) => l.startsWith("data: ") && l !== "data: [DONE]"); + + let reassembledContent = ""; + const metadataValues = new Set(); + for (const line of dataLines) { + const json = JSON.parse(line.slice("data: ".length)); + const delta = json.choices?.[0]?.delta; + if (delta?.content) reassembledContent += delta.content; + if (typeof json[metadataKey] === "string") metadataValues.add(json[metadataKey]); + } + return { reassembledContent, metadataValues }; +} + +// Reproduces the exact shape from the issue: OpenRouter SSE chunks carry a top-level +// "provider" string and delta.reasoning_details[].text, alongside delta.content — all on +// the same choice. windowSize is kept small (5) to force windowed re-emission on every +// chunk, matching the reporter's observation that scrambling happens even in mode=warn. +test("issue #13488: OpenRouter top-level `provider` field must not share the content buffer", async () => { + const transform = createPiiSseTransform({ windowSize: 5 }); + + const makeChunk = (provider: string, content: string, reasoningText = "") => + `data: ${JSON.stringify({ + id: "gen-1", + model: "z-ai/glm-5.3-flash", + provider, + choices: [ + { + index: 0, + delta: { + content, + role: "assistant", + reasoning: "", + reasoning_details: reasoningText + ? [{ type: "reasoning.text", text: reasoningText, format: "" }] + : [], + }, + }, + ], + })}\n\n`; + + const chunks = [ + makeChunk("Together", "Lake"), + makeChunk("Together", "Saimaa "), + makeChunk("Together", "is the largest "), + makeChunk("Together", "lake in Finland."), + ]; + const done = `data: [DONE]\n\n`; + + const output = await testTransform(transform, [...chunks, done]); + + const { reassembledContent, metadataValues: providerValues } = extractContentAndMetadata( + output, + "provider" + ); + + const expectedContent = "LakeSaimaa is the largest lake in Finland."; + + assert.equal( + reassembledContent, + expectedContent, + `content must reassemble byte-identical to input even with PII sanitization enabled; ` + + `got ${JSON.stringify(reassembledContent)}` + ); + + assert.deepEqual( + [...providerValues], + ["Together"], + `the "provider" field must stay constant across every chunk (metadata, not answer text); ` + + `got ${JSON.stringify([...providerValues])}` + ); + + // None of the answer text should ever have leaked into the provider field. + for (const p of providerValues) { + assert.ok( + !/Lake|Saimaa|largest|Finland/.test(p), + `provider field must never contain spliced-in answer text; got "${p}"` + ); + } +}); + +// Same shape but with mode=redact, per the issue's Validation Plan — the splice bug must +// also be gone when redaction (not just pass-through warn mode) is active. +test("issue #13488: mode=redact must not splice `provider` and `content` either", async () => { + const originalModeLocal = process.env.PII_RESPONSE_SANITIZATION_MODE; + process.env.PII_RESPONSE_SANITIZATION_MODE = "redact"; + try { + const transform = createPiiSseTransform({ windowSize: 5 }); + + const makeChunk = (provider: string, content: string) => + `data: ${JSON.stringify({ + id: "gen-2", + model: "z-ai/glm-5.3-flash", + provider, + choices: [{ index: 0, delta: { content, role: "assistant" } }], + })}\n\n`; + + const chunks = [ + makeChunk("Together", "The "), + makeChunk("Together", "capital "), + makeChunk("Together", "of France "), + makeChunk("Together", "is Paris."), + ]; + const done = `data: [DONE]\n\n`; + + const output = await testTransform(transform, [...chunks, done]); + const { reassembledContent, metadataValues: providerValues } = extractContentAndMetadata( + output, + "provider" + ); + + assert.equal( + reassembledContent, + "The capital of France is Paris.", + `content must reassemble byte-identical even under redact mode; got ${JSON.stringify(reassembledContent)}` + ); + assert.deepEqual( + [...providerValues], + ["Together"], + `provider must stay constant under redact mode; got ${JSON.stringify([...providerValues])}` + ); + } finally { + if (originalModeLocal !== undefined) { + process.env.PII_RESPONSE_SANITIZATION_MODE = originalModeLocal; + } else { + delete process.env.PII_RESPONSE_SANITIZATION_MODE; + } + } +}); + +// A second concurrently-streamed metadata field (native_finish_reason) alongside `provider` +// and `content` — closes the family of "any recognized metadata field shares the buffer", +// not just the single field named in the report. +test("issue #13488: a second metadata field (native_finish_reason) must not share buffers either", async () => { + const transform = createPiiSseTransform({ windowSize: 5 }); + + const makeChunk = (provider: string, finishReason: string, content: string) => + `data: ${JSON.stringify({ + id: "gen-3", + model: "z-ai/glm-5.3-flash", + provider, + native_finish_reason: finishReason, + choices: [{ index: 0, delta: { content, role: "assistant" } }], + })}\n\n`; + + const chunks = [ + makeChunk("Together", "in_progress", "Hello "), + makeChunk("Together", "in_progress", "there, "), + makeChunk("Together", "in_progress", "world!"), + ]; + const done = `data: [DONE]\n\n`; + + const output = await testTransform(transform, [...chunks, done]); + const { reassembledContent, metadataValues: providerValues } = extractContentAndMetadata( + output, + "provider" + ); + const { metadataValues: finishReasonValues } = extractContentAndMetadata( + output, + "native_finish_reason" + ); + + assert.equal(reassembledContent, "Hello there, world!"); + assert.deepEqual([...providerValues], ["Together"]); + assert.deepEqual([...finishReasonValues], ["in_progress"]); +}); + +test.after(async () => { + if (originalEnv !== undefined) { + process.env.PII_RESPONSE_SANITIZATION = originalEnv; + } else { + delete process.env.PII_RESPONSE_SANITIZATION; + } + if (originalMode !== undefined) { + process.env.PII_RESPONSE_SANITIZATION_MODE = originalMode; + } else { + delete process.env.PII_RESPONSE_SANITIZATION_MODE; + } + + const coreDb = await import("../../src/lib/db/core.ts"); + coreDb.resetDbInstance(); + fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); diff --git a/tests/unit/issue-13544-audio-transcription-call-log.test.ts b/tests/unit/issue-13544-audio-transcription-call-log.test.ts new file mode 100644 index 0000000000..e5f9fba12f --- /dev/null +++ b/tests/unit/issue-13544-audio-transcription-call-log.test.ts @@ -0,0 +1,165 @@ +// #13544 — successful /v1/audio/transcriptions requests are not recorded in +// call_logs (or proxy_logs), so they never appear in Dashboard -> Request Logs. +// +// open-sse/handlers/audioTranscription.ts and src/app/api/v1/audio/transcriptions/route.ts +// never import/call saveCallLog() (@/lib/usageDb), unlike every other proxied API +// surface (embeddings, images, video, rerank, search, music...). This test drives +// a real POST through the actual route (provider node resolution, enforceApiKeyPolicy, +// upstream dispatch) against a loopback OpenAI-compatible transcription provider and +// asserts a call_logs row is created — mirroring what every other successful proxied +// request produces. It also asserts the persisted row carries the provider/model/ +// api_key_id identity fields, and that upstream `usage: {type:"duration", seconds}` +// is preserved on the row for future cost-pipeline consumption (Validation Plan +// steps 4 and 5). + +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omr-audio-tx-13544-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const { createProviderNode } = await import("../../src/lib/db/providers.ts"); +const { getCallLogs, getCallLogById, waitForCallLogSaves } = + await import("../../src/lib/usage/callLogs.ts"); +const route = await import("../../src/app/api/v1/audio/transcriptions/route.ts"); + +const originalFetch = globalThis.fetch; +const CALL_LOG_SAVE_TIMEOUT_MS = 60_000; + +test.after(async () => { + globalThis.fetch = originalFetch; + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +/** Minimal but structurally valid WAV so nothing rejects the upload shape. */ +function makeWav(): Blob { + const dataLen = 1600; + const b = Buffer.alloc(44 + dataLen); + b.write("RIFF", 0, "ascii"); + b.writeUInt32LE(36 + dataLen, 4); + b.write("WAVE", 8, "ascii"); + b.write("fmt ", 12, "ascii"); + b.writeUInt32LE(16, 16); + b.writeUInt16LE(1, 20); + b.writeUInt16LE(1, 22); + b.writeUInt32LE(16000, 24); + b.writeUInt32LE(32000, 28); + b.writeUInt16LE(2, 32); + b.writeUInt16LE(16, 34); + b.write("data", 36, "ascii"); + b.writeUInt32LE(dataLen, 40); + return new Blob([b], { type: "audio/wav" }); +} + +function transcriptionRequest(model: string) { + const fd = new FormData(); + fd.set("model", model); + fd.set("file", makeWav(), "test.mp3"); + fd.set("language", "es"); + return new Request("http://localhost/v1/audio/transcriptions", { method: "POST", body: fd }); +} + +test( + "#13544: a successful transcription through an OpenAI-compatible provider node creates a call_logs entry", + { timeout: 120_000 }, + async () => { + await createProviderNode({ + id: "scw-whisper-node-13544", + type: "openai-compatible", + name: "Scaleway Whisper", + prefix: "scwwhisper13544", + apiType: "audio-transcriptions", + baseUrl: "http://localhost:9544/v1", + } as Parameters[0]); + + const upstreamCalls: string[] = []; + globalThis.fetch = (async (url: RequestInfo | URL) => { + upstreamCalls.push(String(url)); + return new Response( + JSON.stringify({ + text: " Prueba de transcripción con ScaleY", + usage: { type: "duration", seconds: 3 }, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + }) as typeof fetch; + + const res = await route.POST(transcriptionRequest("scwwhisper13544/whisper-large-v3")); + const body = await res.text(); + + assert.equal(res.status, 200, `expected a successful transcription, got: ${body}`); + assert.ok( + upstreamCalls.some((u) => u.includes("/audio/transcriptions")), + `expected the upstream provider to be dispatched, calls: ${JSON.stringify(upstreamCalls)}` + ); + + await waitForCallLogSaves(CALL_LOG_SAVE_TIMEOUT_MS); + + const logs = await getCallLogs({ provider: "scwwhisper13544", limit: 20 }); + assert.ok( + logs.length > 0, + "expected a call_logs row for the successful /v1/audio/transcriptions request " + + "(none was created — the transcription path bypasses the normal call-log pipeline, #13544)" + ); + + const row = logs[0] as Record; + assert.equal(row.provider, "scwwhisper13544", "call_logs row must carry the provider"); + assert.equal( + row.model, + "scwwhisper13544/whisper-large-v3", + "call_logs row must carry the resolved provider/model" + ); + assert.equal(row.status, 200); + assert.ok("apiKeyId" in row, "call_logs row must carry the apiKeyId field"); + + const detail = await getCallLogById(row.id as string); + const usage = (detail?.responseBody as { usage?: { type?: string; seconds?: number } } | null) + ?.usage; + assert.equal( + usage?.type, + "duration", + `expected the upstream usage.type:"duration" to be preserved on the call_logs row, got responseBody=${JSON.stringify( + detail?.responseBody + )}` + ); + assert.equal(usage?.seconds, 3, "expected the upstream usage.seconds:3 to be preserved"); + } +); + +test( + "#13544: a failed upstream transcription request also creates a call_logs entry", + { timeout: 120_000 }, + async () => { + await createProviderNode({ + id: "scw-whisper-node-13544-fail", + type: "openai-compatible", + name: "Scaleway Whisper (failing)", + prefix: "scwwhisper13544fail", + apiType: "audio-transcriptions", + baseUrl: "http://localhost:9545/v1", + } as Parameters[0]); + + globalThis.fetch = (async () => + new Response(JSON.stringify({ error: { message: "boom" } }), { + status: 500, + headers: { "Content-Type": "application/json" }, + })) as typeof fetch; + + const res = await route.POST(transcriptionRequest("scwwhisper13544fail/whisper-large-v3")); + assert.equal(res.status, 500); + + await waitForCallLogSaves(CALL_LOG_SAVE_TIMEOUT_MS); + + const logs = await getCallLogs({ provider: "scwwhisper13544fail", limit: 20 }); + assert.ok( + logs.length > 0, + "expected a call_logs row for the failed /v1/audio/transcriptions request too (#13544)" + ); + assert.equal((logs[0] as Record).status, 500); + } +); diff --git a/tests/unit/issue-13558-minimax-m3-think-leak.test.ts b/tests/unit/issue-13558-minimax-m3-think-leak.test.ts new file mode 100644 index 0000000000..236ccc82e1 --- /dev/null +++ b/tests/unit/issue-13558-minimax-m3-think-leak.test.ts @@ -0,0 +1,160 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { translateNonStreamingResponse } from "../../open-sse/handlers/responseTranslator.ts"; +import { claudeToOpenAIResponse } from "../../open-sse/translator/response/claude-to-openai.ts"; +import { FORMATS } from "../../open-sse/translator/formats.ts"; + +test("non-streaming: Claude text block with inline leaks into OpenAI message.content (reasoning not separated)", () => { + const claudeResponseFromM3 = { + id: "msg_m3_1", + type: "message", + role: "assistant", + model: "MiniMax-M3", + content: [ + { + type: "text", + text: "The user said hi, I should respond in a friendly way.你好呀!", + }, + ], + stop_reason: "end_turn", + usage: { input_tokens: 10, output_tokens: 20 }, + }; + + const openai = translateNonStreamingResponse( + claudeResponseFromM3, + FORMATS.CLAUDE, // targetFormat = upstream/provider wire format (minimax-cn speaks Claude Messages) + FORMATS.OPENAI // sourceFormat = what the client (OmniRoute caller) wants back + ) as { choices: Array<{ message: { content?: string; reasoning_content?: string } }> }; + + const message = openai.choices[0].message; + + assert.equal( + message.content?.includes(""), + false, + `expected markup to be stripped from message.content, got: ${JSON.stringify(message.content)}` + ); + assert.equal( + typeof message.reasoning_content === "string" && message.reasoning_content.length > 0, + true, + "expected the block text to be surfaced as reasoning_content" + ); + assert.equal(message.content, "你好呀!", "expected the visible reply to survive unchanged"); +}); + +test("streaming: Claude text_delta with inline leaks into OpenAI delta.content chunk-by-chunk", () => { + const state: Record = { toolCalls: new Map() }; + + const events = [ + { type: "message_start", message: { id: "msg_m3_2", model: "MiniMax-M3", usage: {} } }, + { type: "content_block_start", index: 0, content_block: { type: "text" } }, + { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "" } }, + { + type: "content_block_delta", + index: 0, + delta: { type: "text_delta", text: "The user said hi." }, + }, + { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "" } }, + { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "你好呀!" } }, + { type: "content_block_stop", index: 0 }, + ]; + + let sawLiteralThinkOpenInContent = false; + let sawReasoningContent = false; + let assembledContent = ""; + let assembledReasoning = ""; + + for (const event of events) { + const results = claudeToOpenAIResponse(event, state); + for (const chunk of results || []) { + const delta = (chunk as { choices: Array<{ delta: Record }> }).choices[0] + .delta; + if (typeof delta.content === "string") { + assembledContent += delta.content; + if (delta.content.includes("")) { + sawLiteralThinkOpenInContent = true; + } + } + if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) { + sawReasoningContent = true; + assembledReasoning += delta.reasoning_content; + } + } + } + + assert.equal( + sawLiteralThinkOpenInContent, + false, + "expected no literal tag in delta.content" + ); + assert.equal( + sawReasoningContent, + true, + "expected delta.reasoning_content to carry the M3 reasoning text" + ); + assert.equal(assembledContent, "你好呀!", "expected the visible reply to survive unchanged"); + assert.equal( + assembledReasoning, + "The user said hi.", + "expected the reasoning text to be assembled without markup" + ); +}); + +test("streaming: open tag split across two deltas is buffered, not leaked", () => { + const state: Record = { toolCalls: new Map() }; + + const events = [ + { type: "message_start", message: { id: "msg_m3_3", model: "minimax-m3", usage: {} } }, + { type: "content_block_start", index: 0, content_block: { type: "text" } }, + { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "reasoning here" }, + }, + { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "final answer" }, + }, + { type: "content_block_stop", index: 0 }, + ]; + + let sawLiteralThinkMarkupInContent = false; + let assembledContent = ""; + let assembledReasoning = ""; + + for (const event of events) { + const results = claudeToOpenAIResponse(event, state); + for (const chunk of results || []) { + const delta = (chunk as { choices: Array<{ delta: Record }> }).choices[0] + .delta; + if (typeof delta.content === "string") { + assembledContent += delta.content; + if (delta.content.includes("")) { + sawLiteralThinkMarkupInContent = true; + } + } + if (typeof delta.reasoning_content === "string") { + assembledReasoning += delta.reasoning_content; + } + } + } + + assert.equal( + sawLiteralThinkMarkupInContent, + false, + `expected no / markup fragments in delta.content, got: ${JSON.stringify(assembledContent)}` + ); + assert.equal( + assembledContent, + "final answer", + "expected the split open/close tags to still be parsed" + ); + assert.equal( + assembledReasoning, + "reasoning here", + "expected the reasoning text to be assembled across the split chunks" + ); +}); diff --git a/tests/unit/issue-13597-calllogs-worker-error-detail.test.ts b/tests/unit/issue-13597-calllogs-worker-error-detail.test.ts new file mode 100644 index 0000000000..8f6f770b73 --- /dev/null +++ b/tests/unit/issue-13597-calllogs-worker-error-detail.test.ts @@ -0,0 +1,99 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +// Regression test for GitHub issue #13597 (claim A): the callLogs artifact worker's +// failOpen() only ever logged a hardcoded "detail omitted" string — the real Error from +// the worker's `error`/`messageerror` events (and the exit code from a non-zero `exit`) +// were read and then discarded before warnRateLimited() was called, making the failure +// undiagnosable on a live system. +// +// This test forces a REAL worker_threads crash (a worker script that throws +// synchronously at import time) via the `__setCallLogWorkerOverrideForTests` test-only +// hook, so callLogArtifactWriter.ts's actual `worker.on("error", ...)` handler fires with +// a genuine Error object. It then asserts the resulting console.warn call carries that +// error's message. On the pre-fix code this assertion is RED: the only thing ever logged +// is the generic "detail omitted" string, with no trace of the injected error text. + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-issue-13597-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const { writeCallArtifactAsync, closeCallLogArtifactWriter, __setCallLogWorkerOverrideForTests } = + await import("../../src/lib/usage/callLogArtifactWriter.ts"); + +const CRASH_MESSAGE = "probe-13597-worker-init-crash"; +const crashWorkerFile = path.join(TEST_DATA_DIR, "crash-worker.mjs"); +fs.writeFileSync(crashWorkerFile, `throw new Error(${JSON.stringify(CRASH_MESSAGE)});\n`); + +test.after(async () => { + __setCallLogWorkerOverrideForTests(null); + await closeCallLogArtifactWriter(0); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +function buildArtifact(id: string) { + return { + schemaVersion: 5 as const, + summary: { + id, + timestamp: "2026-09-15T00:00:00.000Z", + method: "POST", + path: "/v1/chat/completions", + status: 200, + model: "test-model", + requestedModel: null, + provider: "test-provider", + account: "test-account", + connectionId: null, + duration: 10, + tokens: { + in: 1, + out: 2, + cacheRead: null, + cacheWrite: null, + reasoning: null, + compressed: null, + }, + requestType: "chat", + sourceFormat: "openai", + targetFormat: "openai", + apiKeyId: null, + apiKeyName: null, + comboName: null, + comboStepId: null, + comboExecutionKey: null, + }, + requestBody: {}, + responseBody: { content: "unreachable — worker never comes up" }, + error: null, + }; +} + +test("issue #13597: a crashed call-log worker logs the underlying error detail, not 'detail omitted'", async () => { + __setCallLogWorkerOverrideForTests({ workerFile: crashWorkerFile, execArgv: [] }); + + const originalWarn = console.warn; + const warnings: string[] = []; + console.warn = (...args: unknown[]) => { + warnings.push(args.map(String).join(" ")); + }; + + try { + const result = await writeCallArtifactAsync(buildArtifact("issue-13597-crash-1")); + assert.equal(result, null); // fails open — existing, correct behavior + + const failureWarnings = warnings.filter((w) => w.includes("[callLogs]")); + assert.ok(failureWarnings.length > 0, "expected a [callLogs] warning to be logged"); + + const detailed = failureWarnings.some((w) => w.includes(CRASH_MESSAGE)); + assert.ok( + detailed, + `expected a [callLogs] warning to include the underlying error detail ` + + `("${CRASH_MESSAGE}"), got: ${JSON.stringify(failureWarnings)}` + ); + } finally { + console.warn = originalWarn; + } +}); diff --git a/tests/unit/issue-13599-bai-deepseek-reasoning-content-echo.test.ts b/tests/unit/issue-13599-bai-deepseek-reasoning-content-echo.test.ts new file mode 100644 index 0000000000..80c56e9060 --- /dev/null +++ b/tests/unit/issue-13599-bai-deepseek-reasoning-content-echo.test.ts @@ -0,0 +1,70 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { DefaultExecutor } from "../../open-sse/executors/default.ts"; + +// Issue #13599: follow-up requests to DeepSeek thinking-mode models served through the +// `bai` provider (api.b.ai) are rejected upstream with +// 400 The `reasoning_content` in the thinking mode must be passed back to the API +// because standard OpenAI-shaped clients do not preserve `reasoning_content` on the prior +// assistant turn when they replay conversation history. OmniRoute already has a mechanism +// for exactly this requirement (open-sse/utils/reasoningContentInjector.ts, ported from +// 9router#1480), but DefaultExecutor.transformRequest only invoked it when +// `this.provider === "kimi" || this.provider === "moonshot"` (open-sse/executors/default.ts) — +// the `bai` DeepSeek-reselling gateway was not covered, so a follow-up turn was forwarded to +// DeepSeek with no `reasoning_content` on the prior assistant message. + +function priorTurnBody(model: string) { + return { + model, + stream: false, + messages: [ + { role: "user", content: "What is 2+2?" }, + // Standard OpenAI-shaped client history: the assistant turn carries only + // `content`. It does NOT echo back `reasoning_content` from the previous + // response, exactly like a normal ChatGPT-style client would replay it. + { role: "assistant", content: "4" }, + { role: "user", content: "Now multiply that by 10." }, + ], + }; +} + +test("DefaultExecutor injects reasoning_content for bai/deepseek follow-up turns (issue #13599)", () => { + const executor = new DefaultExecutor("bai"); + const body = priorTurnBody("bai/deepseek-reasoner"); + + const transformed = executor.transformRequest("bai/deepseek-reasoner", body, false, { + apiKey: "sk-bai-test", + }) as { messages: Array> }; + + const assistantTurn = transformed.messages.find((m) => m.role === "assistant"); + assert.ok(assistantTurn, "expected an assistant message in the transformed body"); + + // The injector's placeholder is a single space (matching the existing Moonshot/Kimi + // convention in reasoningContentInjector.ts — DeepSeek only requires the field to be + // present and non-empty, not semantically meaningful), so assert non-empty length + // directly rather than trimming. + assert.ok( + typeof assistantTurn!.reasoning_content === "string" && + (assistantTurn!.reasoning_content as string).length > 0, + "expected DefaultExecutor to inject a non-empty reasoning_content placeholder on the " + + "assistant turn for a bai/deepseek thinking-mode follow-up (it did not — this is issue #13599)" + ); +}); + +test("DefaultExecutor does not touch non-thinking-model bai follow-up turns", () => { + const executor = new DefaultExecutor("bai"); + const body = priorTurnBody("bai/gpt-4o-mini"); + + const transformed = executor.transformRequest("bai/gpt-4o-mini", body, false, { + apiKey: "sk-bai-test", + }) as { messages: Array> }; + + const assistantTurn = transformed.messages.find((m) => m.role === "assistant"); + assert.ok(assistantTurn, "expected an assistant message in the transformed body"); + assert.equal( + "reasoning_content" in assistantTurn!, + false, + "a non-thinking model must not receive an injected reasoning_content field" + ); +}); diff --git a/tests/unit/issue-13620-combo-reasoning-only-sse-burst.test.ts b/tests/unit/issue-13620-combo-reasoning-only-sse-burst.test.ts new file mode 100644 index 0000000000..59613d2ce0 --- /dev/null +++ b/tests/unit/issue-13620-combo-reasoning-only-sse-burst.test.ts @@ -0,0 +1,126 @@ +/** + * Issue #13620 — combo quality-gate bounded peek buffers bare `delta.reasoning` + * SSE chunks (Ollama Cloud shape) because `hasOpenAICompatibleStreamValue()` + * (open-sse/utils/streamHelpers.ts) only recognizes `content`, + * `reasoning_content`, `reasoning_text`, and `tool_calls` — not bare + * `reasoning`. As a result the peek loop never exits on a reasoning-only + * chunk and keeps buffering every reasoning delta until the first real + * `content` delta arrives (client sees dead silence, then a single burst), + * and a stream that finishes with reasoning-only content is rejected as an + * "empty completion" (secondary 502 failure mode). + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { isKnownNonClaudeStreamPayload } = await import("../../open-sse/utils/streamHelpers.ts"); +const { validateResponseQuality } = await import("../../open-sse/services/combo.ts"); + +const encoder = new TextEncoder(); +const silentLog = { warn: () => {} }; + +test("#13620 root cause: isKnownNonClaudeStreamPayload must recognize bare delta.reasoning (Ollama Cloud shape)", () => { + const parsed = { + choices: [ + { index: 0, delta: { role: "assistant", content: "", reasoning: "We" }, finish_reason: null }, + ], + }; + const recognized = isKnownNonClaudeStreamPayload(parsed); + assert.equal( + recognized, + true, + "bare delta.reasoning must be recognized as stream content so the combo peek stops buffering immediately, " + + "matching hasValuableContent()/hasAnyReasoningSignal() which already treat it as real signal" + ); +}); + +// Emits ONE physical Uint8Array chunk per SSE frame (mirrors real network +// framing, unlike a single enqueue() of the whole body) and counts how many +// chunks the underlying stream has handed out by the time the caller stops +// reading — this is what actually distinguishes "peek recognizes the first +// reasoning delta and stops buffering there" from "peek keeps consuming +// (buffering) every reasoning chunk until the content chunk arrives". +function makeReasoningOnlyThenContentStream(): { + response: Response; + chunksPulled: { count: number }; +} { + const frames = [ + { choices: [{ index: 0, delta: { role: "assistant", content: "", reasoning: "We" } }] }, + { choices: [{ index: 0, delta: { content: "", reasoning: " need" } }] }, + { choices: [{ index: 0, delta: { content: "", reasoning: " to think." } }] }, + { choices: [{ index: 0, delta: { content: "Hello" } }] }, + { choices: [{ index: 0, delta: {}, finish_reason: "stop" }] }, + ].map((c) => encoder.encode(`data: ${JSON.stringify(c)}\n\n`)); + frames.push(encoder.encode("data: [DONE]\n\n")); + + const chunksPulled = { count: 0 }; + let idx = 0; + const stream = new ReadableStream({ + pull(controller) { + if (idx >= frames.length) { + controller.close(); + return; + } + controller.enqueue(frames[idx++]); + chunksPulled.count = idx; + }, + }); + return { + response: new Response(stream, { + status: 200, + headers: { "content-type": "text/event-stream" }, + }), + chunksPulled, + }; +} + +test("#13620 behavioral: peek must stop buffering at the first reasoning delta, not wait for content", async () => { + const { response, chunksPulled } = makeReasoningOnlyThenContentStream(); + const out = await validateResponseQuality(response, true, silentLog); + assert.equal(out.valid, true, `expected valid, got reason: ${out.reason}`); + assert.ok(out.clonedResponse, "expected a clonedResponse to replay/forward the stream"); + + // Frame indices: 0-2 = reasoning-only deltas, 3 = content, 4 = finish_reason, 5 = [DONE]. + // Correct: the peek recognizes frame 0 immediately via a single reader.read() call. + // The WHATWG ReadableStream spec itself speculatively pulls one extra chunk into its + // internal queue right after that read() resolves (default highWaterMark backpressure + // refill) — so up to 2 physical chunks may be pulled even when the peek loop performs + // exactly one logical read. That prefetch is unavoidable engine behavior, not something + // validateResponseQuality controls, so the threshold below allows for it. + // Bug: the peek loop itself keeps calling reader.read() through frames 0-2 (each + // triggering its own follow-up prefetch), only stopping at frame 3 -> 4-5 chunks pulled. + assert.ok( + chunksPulled.count <= 2, + "expected the peek to stop after at most 1 logical read (<=2 chunks pulled, accounting for " + + `ReadableStream's own one-chunk-ahead prefetch); actual: ${chunksPulled.count} chunks were ` + + "pulled before the peek recognized any content — this is the reasoning-prefix burst-buffering bug" + ); +}); + +function makeReasoningOnlyTerminatedStream(): Response { + const chunks = [ + { choices: [{ index: 0, delta: { role: "assistant", reasoning: "Deep" } }] }, + { choices: [{ index: 0, delta: { reasoning: " thinking..." } }] }, + { choices: [{ index: 0, delta: {}, finish_reason: "stop" }] }, + ]; + const body = chunks.map((c) => `data: ${JSON.stringify(c)}\n\n`).join("") + "data: [DONE]\n\n"; + return new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(body)); + controller.close(); + }, + }), + { status: 200, headers: { "content-type": "text/event-stream" } } + ); +} + +test("#13620 secondary failure mode: reasoning-only stream that terminates cleanly must not be rejected as an empty completion", async () => { + const res = makeReasoningOnlyTerminatedStream(); + const out = await validateResponseQuality(res, true, silentLog); + assert.equal( + out.valid, + true, + `expected valid (reasoning-only is real output, mirrors the non-streaming validator's ` + + `reasoning_content ?? reasoning acceptance) — got reason: ${out.reason}` + ); +}); diff --git a/tests/unit/issue-13652-kiro-tooldocs-repeat.test.ts b/tests/unit/issue-13652-kiro-tooldocs-repeat.test.ts new file mode 100644 index 0000000000..f4d165cc8f --- /dev/null +++ b/tests/unit/issue-13652-kiro-tooldocs-repeat.test.ts @@ -0,0 +1,83 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { buildKiroPayload } from "../../open-sse/translator/request/openai-to-kiro.ts"; + +// Issue #13652: `convertMessages()` in `openai-to-kiro.ts` is stateless per HTTP +// request. OpenAI-compatible clients resend the full growing message array on +// every turn, so the same tool-bearing first user message is re-scanned on +// every subsequent request and its relocated documentation (`toolDocs`) +// rebuilt from scratch. `buildKiroPayload()` then unconditionally prepends it +// onto whatever the *current* turn is, so the ~10KB+ doc block keeps landing +// on the newest user message instead of staying where it was first delivered. + +const DOCS_HEADING = "# Tool Documentation"; + +function bigTool(length: number) { + return [ + { + type: "function", + function: { + name: "big_tool", + description: "D".repeat(length), + parameters: { type: "object", properties: {} }, + }, + }, + ]; +} + +test("issue #13652: relocated tool doc is not re-prepended to a later turn once already delivered", () => { + const tools = bigTool(12000); + + // Turn 1: client sends only the first user message. + const turn1Messages = [{ role: "user", content: "hello, please help" }]; + const payload1 = buildKiroPayload( + "claude-sonnet-4.5", + { messages: turn1Messages, tools }, + true, + {} + ); + const turn1Content = payload1.conversationState.currentMessage.userInputMessage.content; + + assert.ok( + turn1Content.includes(DOCS_HEADING), + "turn 1: the relocated tool documentation must reach the model at least once" + ); + + // Turn 2: the OpenAI-compatible client resends the FULL prior history plus + // the assistant reply and a new user message. The client's own copy of turn + // 1's user message does NOT contain the "# Tool Documentation" block, since + // that was only ever prepended server-side to the outgoing Kiro payload. + const turn2Messages = [ + { role: "user", content: "hello, please help" }, + { role: "assistant", content: "Sure, how can I help?" }, + { role: "user", content: "what is 2+2?" }, + ]; + const payload2 = buildKiroPayload( + "claude-sonnet-4.5", + { messages: turn2Messages, tools }, + true, + {} + ); + const turn2CurrentContent = payload2.conversationState.currentMessage.userInputMessage.content; + + assert.ok( + !turn2CurrentContent.includes(DOCS_HEADING), + "turn 2: the tool documentation must NOT be re-prepended to the newest turn's " + + "content once it has already been delivered earlier in the conversation" + ); + + // The docs must not simply vanish — they must still reach the model exactly + // once, anchored on the turn that originally carried the tools (now in history). + const historyContents = payload2.conversationState.history + .map((h: { userInputMessage?: { content?: string } }) => h.userInputMessage?.content || "") + .join("\n"); + const occurrences = (historyContents.match(new RegExp(DOCS_HEADING, "g")) || []).length; + + assert.equal( + occurrences, + 1, + "turn 2: the tool documentation must still reach the model exactly once, anchored to " + + "the turn that originally carried the tools" + ); +}); diff --git a/tests/unit/issue-13680-batches-delete-completed-unbounded-work.test.ts b/tests/unit/issue-13680-batches-delete-completed-unbounded-work.test.ts new file mode 100644 index 0000000000..654dd01b34 --- /dev/null +++ b/tests/unit/issue-13680-batches-delete-completed-unbounded-work.test.ts @@ -0,0 +1,111 @@ +/** + * #13680 — DELETE /v1/batches/delete-completed does unbounded work per request. + * + * `deleteCompletedBatches` commits in chunks of `INSTANCE_SWEEP_CHUNK` (200), but + * the outer `sweepLoop` is a plain synchronous `for (;;)` that only stops when the + * table has no completed batch left — there is no cap on how many chunks a single + * request may run. better-sqlite3 is synchronous, so one request can hold the + * Node.js event loop for as long as it takes to sweep the ENTIRE table, and the + * caller has no way to ask for a bounded amount of work per call (no `hasMore`). + * + * This test seeds one row past the issue's own proposed cap + * (`MAX_CHUNKS_PER_REQUEST = 25` × `INSTANCE_SWEEP_CHUNK` = 5000) and asserts a + * single call stays within that bound and reports a continuation flag. + */ +import { describe, it, after } from "node:test"; +import assert from "node:assert"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "issue13680-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const { createFile } = await import("../../src/lib/db/files.ts"); +const { createBatch, deleteCompletedBatches, INSTANCE_SWEEP_CHUNK, MAX_CHUNKS_PER_REQUEST } = + await import("../../src/lib/db/batches.ts"); +const { resetDbInstance } = await import("../../src/lib/db/core.ts"); + +function seedCompletedBatch(label: string, apiKeyId: string | null = null) { + const file = createFile({ + bytes: 1, + filename: `${label}.jsonl`, + purpose: "batch", + content: Buffer.from("x"), + apiKeyId, + }); + return createBatch({ + endpoint: "/v1/chat/completions", + completionWindow: "24h", + inputFileId: file.id, + status: "completed", + apiKeyId, + }); +} + +describe("#13680 — deleteCompletedBatches has a per-request chunk cap", () => { + after(() => { + resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + }); + + it("stops after MAX_CHUNKS_PER_REQUEST chunks and reports hasMore instead of sweeping the whole table in one synchronous call", () => { + const cap = MAX_CHUNKS_PER_REQUEST * INSTANCE_SWEEP_CHUNK; + const total = cap + 1; // one row past the cap: proves the loop stops at it + + for (let i = 0; i < total; i++) seedCompletedBatch(`issue13680-${i}`); + + const result = deleteCompletedBatches({ allTenants: true }) as { + deletedBatches: number; + deletedFiles: number; + hasMore?: boolean; + }; + + assert.ok( + result.deletedBatches <= cap, + `expected a single request to sweep at most ${cap} batches (MAX_CHUNKS_PER_REQUEST=${MAX_CHUNKS_PER_REQUEST} × INSTANCE_SWEEP_CHUNK=${INSTANCE_SWEEP_CHUNK}), ` + + `but one call deleted ${result.deletedBatches} of ${total} in one synchronous pass — no per-request cap exists` + ); + assert.strictEqual( + result.hasMore, + true, + "the result carries no continuation signal (`hasMore`), so a caller cannot tell more completed batches remain to sweep" + ); + }); + + it("resumes across repeated calls until hasMore is false, sweeping the entire backlog", () => { + // Key-scoped on purpose: isolates this test's count from the leftover + // unowned batch the previous test's `allTenants` sweep may not have caught + // (its cap+1 seed leaves exactly one row past MAX_CHUNKS_PER_REQUEST), so + // the expected call count here stays exact regardless of test order. + const apiKeyId = "resume-key-13680"; + const cap = MAX_CHUNKS_PER_REQUEST * INSTANCE_SWEEP_CHUNK; + const total = cap + 50; + for (let i = 0; i < total; i++) seedCompletedBatch(`issue13680-resume-${i}`, apiKeyId); + + let totalDeleted = 0; + let hasMore = true; + let calls = 0; + while (hasMore) { + calls++; + if (calls > 10) throw new Error("resumption did not converge within 10 calls"); + const result = deleteCompletedBatches({ apiKeyId }) as { + deletedBatches: number; + hasMore: boolean; + }; + totalDeleted += result.deletedBatches; + hasMore = result.hasMore; + } + + assert.strictEqual( + calls, + 2, + "50 extra rows past one cap should resume in exactly one more call" + ); + assert.strictEqual( + totalDeleted, + total, + "every seeded batch must be swept across the resumed calls" + ); + }); +}); diff --git a/tests/unit/issue-13681-shared-file-across-batches.test.ts b/tests/unit/issue-13681-shared-file-across-batches.test.ts new file mode 100644 index 0000000000..6fb85a3e39 --- /dev/null +++ b/tests/unit/issue-13681-shared-file-across-batches.test.ts @@ -0,0 +1,143 @@ +/** + * Repro for #13681 — the completed-batch sweep (deleteCompletedBatches / + * deleteBatch / cleanupExpiredBatches) nulls a file's content whenever ANY + * completed batch it deletes references that file id, without checking + * whether another batch — in progress, queued, or completed but outside the + * current sweep unit — still references the same file. A tenant that reuses + * one input file across two batches loses that file for the surviving batch. + * + * Self-isolating: DATA_DIR points at a fresh temp dir before any `@/lib/db/*` + * module loads. + */ +import { describe, it, after } from "node:test"; +import assert from "node:assert"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "issue-13681-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const { createFile, getFile, getFileContent } = await import("../../src/lib/db/files.ts"); +const { createBatch, getBatch, deleteCompletedBatches, deleteBatch } = + await import("../../src/lib/db/batches.ts"); +const { resetDbInstance } = await import("../../src/lib/db/core.ts"); + +describe("#13681 — shared file reference survives a sibling batch's deletion", () => { + after(() => { + resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + }); + + it("deleteCompletedBatches must NOT null a file still referenced by a surviving in_progress batch", () => { + const apiKeyId = "key_shared_13681"; + const sharedFile = createFile({ + bytes: 8, + filename: "shared-input.jsonl", + purpose: "batch", + content: Buffer.from("shared-content"), + apiKeyId, + }); + + // Batch A: completed, will be swept and deleted. + const batchA = createBatch({ + endpoint: "/v1/chat/completions", + completionWindow: "24h", + inputFileId: sharedFile.id, + status: "completed", + apiKeyId, + }); + + // Batch B: still in progress, reuses the SAME input file id, and is never + // touched by this sweep call. + const batchB = createBatch({ + endpoint: "/v1/chat/completions", + completionWindow: "24h", + inputFileId: sharedFile.id, + status: "in_progress", + apiKeyId, + }); + + const result = deleteCompletedBatches({ apiKeyId }); + + assert.strictEqual(getBatch(batchA.id), null, "the completed batch is swept as expected"); + assert.ok(getBatch(batchB.id), "the in-progress batch must survive the sweep"); + assert.strictEqual(result.deletedBatches, 1, "only the completed batch counted as deleted"); + + // Expected/correct behavior: batch B is alive and still points at + // sharedFile.id, so the sweep must NOT have soft-deleted that file just + // because batch A (also swept) referenced the same file id. + assert.ok(getFile(sharedFile.id), "the shared file must survive — batch B still references it"); + assert.notStrictEqual( + getFileContent(sharedFile.id), + null, + "the shared file's content must survive — batch B still references it" + ); + }); + + it("deleteBatch (single) must NOT null a file still referenced by a sibling batch", () => { + const apiKeyId = "key_shared_single_13681"; + const sharedFile = createFile({ + bytes: 8, + filename: "shared-input-2.jsonl", + purpose: "batch", + content: Buffer.from("shared-content-2"), + apiKeyId, + }); + + const batchA = createBatch({ + endpoint: "/v1/chat/completions", + completionWindow: "24h", + inputFileId: sharedFile.id, + status: "completed", + apiKeyId, + }); + const batchB = createBatch({ + endpoint: "/v1/chat/completions", + completionWindow: "24h", + inputFileId: sharedFile.id, + status: "in_progress", + apiKeyId, + }); + + const deleted = deleteBatch(batchA.id); + + assert.strictEqual(deleted, true, "deleteBatch reports success for batch A"); + assert.ok(getBatch(batchB.id), "batch B is untouched by deleteBatch(batchA.id)"); + assert.notStrictEqual( + getFileContent(sharedFile.id), + null, + "the shared file's content must survive — batch B still references it" + ); + }); + + it("deleteCompletedBatches DOES delete the file once the LAST referencing batch is gone (no regression toward never-delete)", () => { + const apiKeyId = "key_last_ref_13681"; + const file = createFile({ + bytes: 8, + filename: "last-ref.jsonl", + purpose: "batch", + content: Buffer.from("last-ref-content"), + apiKeyId, + }); + + const onlyBatch = createBatch({ + endpoint: "/v1/chat/completions", + completionWindow: "24h", + inputFileId: file.id, + status: "completed", + apiKeyId, + }); + + const result = deleteCompletedBatches({ apiKeyId }); + + assert.strictEqual(getBatch(onlyBatch.id), null); + assert.strictEqual(result.deletedBatches, 1); + assert.strictEqual( + result.deletedFiles, + 1, + "the file had no other referencing batch, so it must be deleted" + ); + assert.strictEqual(getFile(file.id), null, "the file is gone once nothing else references it"); + }); +}); diff --git a/tests/unit/kiro-long-tool-description-docs.test.ts b/tests/unit/kiro-long-tool-description-docs.test.ts index c7f5dcd28a..330a2be465 100644 --- a/tests/unit/kiro-long-tool-description-docs.test.ts +++ b/tests/unit/kiro-long-tool-description-docs.test.ts @@ -53,7 +53,15 @@ const TURN_SHAPES = { "no user messages": [{ role: "assistant", content: "only" }], }; -test("relocated tool documentation reaches the current turn for every turn shape", () => { +// Issue #13652: the doc block is anchored to the turn that originally carried +// the tools, not unconditionally glued onto `currentMessage`. For a shape with +// more than one user turn, the tool-bearing turn ends up demoted into +// `history` (currentMessage becomes the newest turn instead), so the doc now +// lives on `history[0]`'s content. Only when the tool-bearing turn IS +// `currentMessage` (a single user turn, or the "no user turn" fallback) does +// it stay there. Whichever turn carries it, it must reach the model exactly +// once — resending it on both would reintroduce #13652's duplication bug. +test("relocated tool documentation reaches exactly one turn for every turn shape", () => { for (const [label, messages] of Object.entries(TURN_SHAPES)) { const payload = buildKiroPayload( "claude-sonnet-4.5", @@ -62,13 +70,21 @@ test("relocated tool documentation reaches the current turn for every turn shape {} ); const current = payload.conversationState.currentMessage.userInputMessage; + const history = payload.conversationState.history as Array<{ + userInputMessage?: { content?: string }; + }>; + const allContents = [...history.map((h) => h.userInputMessage?.content || ""), current.content]; + const combined = allContents.join("\n"); + const occurrences = (combined.match(new RegExp(DOCS_HEADING, "g")) || []).length; - assert.ok( - current.content.includes(DOCS_HEADING), - `${label}: full tool documentation must be prepended to the current turn` + assert.equal( + occurrences, + 1, + `${label}: the tool documentation must reach the model exactly once, not zero ` + + `(silently dropped) and not more than once (re-injected, issue #13652)` ); assert.ok( - current.content.includes("D".repeat(12000)), + combined.includes("D".repeat(12000)), `${label}: the relocated description text itself must survive` ); assert.equal( @@ -187,12 +203,20 @@ test("only oversized descriptions are relocated in a mixed tool inventory", () = ); const current = payload.conversationState.currentMessage.userInputMessage; const specs = current.userInputMessageContext?.tools; + const history = payload.conversationState.history as Array<{ + userInputMessage?: { content?: string }; + }>; + // "multi-turn conversation" demotes the tool-bearing turn into history[0] + // (issue #13652) — the doc block lives there, not on currentMessage. + const combined = [...history.map((h) => h.userInputMessage?.content || ""), current.content].join( + "\n" + ); assert.equal(specs[0].toolSpecification.description, "compact"); assert.equal(specs[1].toolSpecification.description, POINTER); - assert.ok(current.content.includes("## Tool: big_tool")); + assert.ok(combined.includes("## Tool: big_tool")); assert.ok( - !current.content.includes("## Tool: small_tool"), + !combined.includes("## Tool: small_tool"), "a tool that was never relocated must not get a documentation section" ); }); diff --git a/tests/unit/mcp-restart-route-13012.test.ts b/tests/unit/mcp-restart-route-13012.test.ts new file mode 100644 index 0000000000..956cd5f0c3 --- /dev/null +++ b/tests/unit/mcp-restart-route-13012.test.ts @@ -0,0 +1,77 @@ +// Regression for GitHub issue #13012 (Bug 2): `omniroute mcp restart` POSTs to +// /api/mcp/restart, but that route never existed — every call 404d. This test +// boots the route handler directly and pins its behavior across the three +// states the CLI can hit: disabled, enabled+stdio, enabled+http-family. +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omr-mcp-restart-route-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const settingsDb = await import("../../src/lib/db/settings.ts"); +const { POST } = await import("../../src/app/api/mcp/restart/route.ts"); +const { shutdownMcpHttp } = await import("../../open-sse/mcp-server/httpTransport.ts"); +const { isLocalOnlyPath } = await import("../../src/server/authz/routeGuard.ts"); + +// Hard Rule #15: /api/mcp/ must stay LOCAL_ONLY so loopback enforcement runs +// unconditionally before any auth check — a leaked JWT over a tunnel must not +// reach a route that can tear down/spin up MCP transport sessions. +test("issue #13012: POST /api/mcp/restart is classified LOCAL_ONLY", () => { + assert.equal(isLocalOnlyPath("/api/mcp/restart"), true); +}); + +function reset() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +test.beforeEach(async () => { + reset(); + // No JWT_SECRET / requireLogin=false ⇒ auth-disabled deployment (pre-existing + // open-door contract also exercised by tests/unit/mcp-route-scope-carveout.test.ts). + await settingsDb.updateSettings({ requireLogin: false }); +}); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +function restartRequest(): Request { + return new Request("http://localhost:20128/api/mcp/restart", { method: "POST" }); +} + +test("issue #13012: POST /api/mcp/restart exists and returns 409 when MCP is disabled", async () => { + await settingsDb.updateSettings({ mcpEnabled: false }); + const res = await POST(restartRequest()); + assert.equal(res.status, 409); + const body = (await res.json()) as { error?: string }; + assert.match(body.error ?? "", /disabled/i); +}); + +test("POST /api/mcp/restart returns 501 for the stdio transport (no in-process handle)", async () => { + await settingsDb.updateSettings({ mcpEnabled: true, mcpTransport: "stdio" }); + const res = await POST(restartRequest()); + assert.equal(res.status, 501); + const body = (await res.json()) as { error?: string }; + assert.match(body.error ?? "", /stdio/i); +}); + +test("POST /api/mcp/restart tears down HTTP sessions and returns 200 for sse/streamable-http", async () => { + await settingsDb.updateSettings({ mcpEnabled: true, mcpTransport: "sse" }); + const res = await POST(restartRequest()); + assert.equal(res.status, 200); + const body = (await res.json()) as { status?: string; enabled?: boolean; transport?: string }; + assert.equal(body.status, "restarted"); + assert.equal(body.enabled, true); + assert.equal(body.transport, "sse"); +}); + +test("shutdownMcpHttp is exported and callable (route depends on this contract)", () => { + assert.equal(typeof shutdownMcpHttp, "function"); +}); diff --git a/tests/unit/memory-fts-access-update.test.ts b/tests/unit/memory-fts-access-update.test.ts new file mode 100644 index 0000000000..1c6ef607d0 --- /dev/null +++ b/tests/unit/memory-fts-access-update.test.ts @@ -0,0 +1,152 @@ +import { after, test } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +// DATA_DIR must be frozen before the first db import. Run this file alone. + +const dataDir = mkdtempSync(join(tmpdir(), "omniroute-memory-fts-au-")); +process.env.DATA_DIR = dataDir; +process.env.APP_LOG_TO_FILE = "false"; + +const { MemoryType } = await import("../../src/lib/memory/types.ts"); +const { createMemory, recordMemoryAccess, getMemory } = + await import("../../src/lib/memory/store.ts"); +const { cleanupMemoryEntries } = await import("../../src/lib/db/cleanup.ts"); +const { getDbInstance, resetDbInstance } = await import("../../src/lib/db/core.ts"); + +function ftsCounts(db: ReturnType): { + data: number; + docsize: number; +} { + const data = db.prepare("SELECT count(*) AS n FROM memory_fts_data").get() as { n: number }; + const docsize = db.prepare("SELECT count(*) AS n FROM memory_fts_docsize").get() as { + n: number; + }; + return { data: data.n, docsize: docsize.n }; +} + +after(() => { + try { + resetDbInstance(); + } catch { + /* ignore */ + } + rmSync(dataDir, { recursive: true, force: true }); +}); + +test("recordMemoryAccess does not grow FTS5 posting lists", async () => { + const mem = await createMemory({ + apiKeyId: "k-fts-au", + sessionId: "s1", + type: MemoryType.FACTUAL, + key: "stable-key", + content: "needle-alpha unique phrase", + metadata: {}, + expiresAt: null, + }); + const db = getDbInstance(); + const before = ftsCounts(db); + + for (let i = 0; i < 20; i++) { + recordMemoryAccess([mem.id]); + } + + const after = ftsCounts(db); + assert.equal(after.data, before.data, "access_count updates must not append FTS5 segments"); + assert.equal( + after.docsize, + before.docsize, + "access_count updates must not append FTS5 docsize rows" + ); + const reloaded = await getMemory(mem.id); + assert.equal(reloaded?.accessCount, 20); +}); + +test("content edits still reindex FTS5", async () => { + const mem = await createMemory({ + apiKeyId: "k-fts-au-edit", + sessionId: "s1", + type: MemoryType.FACTUAL, + key: "edit-key", + content: "needle-before unique phrase", + metadata: {}, + expiresAt: null, + }); + + await createMemory({ + apiKeyId: "k-fts-au-edit", + sessionId: "s1", + type: MemoryType.FACTUAL, + key: "edit-key", + content: "needle-after unique phrase", + metadata: {}, + expiresAt: null, + }); + + const db = getDbInstance(); + const oldHits = db + .prepare("SELECT count(*) AS n FROM memory_fts WHERE memory_fts MATCH ?") + .get('"needle-before"') as { n: number }; + const newHits = db + .prepare("SELECT count(*) AS n FROM memory_fts WHERE memory_fts MATCH ?") + .get('"needle-after"') as { n: number }; + assert.equal(oldHits.n, 0, "old content must leave the index"); + assert.ok(newHits.n >= 1, "new content must be searchable"); + const reloaded = await getMemory(mem.id); + assert.equal(reloaded?.content, "needle-after unique phrase"); +}); + +test("new inserts remain searchable after memory_id sync", async () => { + await createMemory({ + apiKeyId: "k-fts-au-insert", + sessionId: "s1", + type: MemoryType.FACTUAL, + key: "insert-key", + content: "needle-insert unique phrase", + metadata: {}, + expiresAt: null, + }); + const db = getDbInstance(); + const hits = db + .prepare("SELECT count(*) AS n FROM memory_fts WHERE memory_fts MATCH ?") + .get('"needle-insert"') as { n: number }; + assert.ok(hits.n >= 1, "fresh inserts must be in FTS5 after memory_id sync"); +}); + +test("cleanupMemoryEntries issues FTS5 rebuild even when no rows expire", async () => { + await createMemory({ + apiKeyId: "k-fts-rebuild", + sessionId: "s1", + type: MemoryType.FACTUAL, + key: "rebuild-key", + content: "needle-rebuild unique phrase", + metadata: {}, + expiresAt: null, + }); + const db = getDbInstance(); + const calls: string[] = []; + const orig = db.exec.bind(db); + db.exec = ((sql: string) => { + calls.push(sql); + return orig(sql); + }) as typeof db.exec; + + const result = await cleanupMemoryEntries(); + assert.equal(result.deleted, 0, "fresh memories must survive default retention"); + assert.equal(result.errors, 0); + assert.ok( + calls.some((sql) => sql.includes("VALUES('rebuild')")), + `cleanup must rebuild FTS5, got ${JSON.stringify(calls)}` + ); + assert.equal( + calls.some((sql) => sql.includes("VALUES('optimize')")), + false, + "optimize must not substitute for rebuild" + ); + const hits = db + .prepare("SELECT count(*) AS n FROM memory_fts WHERE memory_fts MATCH ?") + .get('"needle-rebuild"') as { n: number }; + assert.ok(hits.n >= 1, "content must stay searchable after rebuild"); +}); diff --git a/tests/unit/non-streaming-client-translate.test.ts b/tests/unit/non-streaming-client-translate.test.ts index fb382929fa..d96b931773 100644 --- a/tests/unit/non-streaming-client-translate.test.ts +++ b/tests/unit/non-streaming-client-translate.test.ts @@ -270,6 +270,49 @@ test("Responses API format: sanitizeResponsesApiResponse is applied", () => { assert.equal(output[0]?.name, "get_weather", "#7936 restore original name"); }); +test("#12370: alias-shaped requestToolIdentityMap must not blank out function_call name", () => { + // extractRequestToolIdentityMap() falls back to the `_toolNameMap` side channel + // when no namespace tools are present. For Gemini/Claude pivots that side + // channel is a plain Map alias table (wire name -> original + // name), NOT the {namespace, name} identity shape the #7936 restore loop + // expects. A plain function tool like Codex's "shell" round-trips through + // this alias map as an identity mapping ("shell" -> "shell"): reproduces the + // exact live-VPS shape (tool_choice: auto, one `shell` function tool, + // gemini-3-flash-preview) where the non-streaming /v1/responses item lost + // its `name` key entirely. + const input = baseInput({ + responsePayloadFormat: FORMATS.GEMINI, + clientResponseFormat: FORMATS.OPENAI_RESPONSES, + sourceFormat: FORMATS.OPENAI_RESPONSES, + provider: "gemini", + model: "gemini-3-flash-preview", + responseBody: { + candidates: [ + { + content: { + role: "model", + parts: [{ functionCall: { name: "shell", args: { command: ["ls", "memory-bank/"] } } }], + }, + finishReason: "STOP", + index: 0, + }, + ], + }, + // Alias-shaped map (string -> string), as published by the openai->gemini + // pivot — not a NamespaceIdentity map. + requestToolIdentityMap: new Map([["shell", "shell"]]) as unknown as Map< + string, + { namespace?: string; name: string } + >, + }); + const result = translateNonStreamingClientResponse(input); + const output = result.response.output as Array>; + const functionCall = output.find((item) => item.type === "function_call"); + assert.ok(functionCall, "expected a function_call output item"); + assert.equal(functionCall?.name, "shell", "name must survive the alias-map fallback"); + assert.equal("name" in (functionCall as object), true, "name key must be present, not stripped"); +}); + test("empty content response: passthrough without crash", () => { const input = baseInput({ responseBody: {}, diff --git a/tests/unit/provider-validation-image-only.test.ts b/tests/unit/provider-validation-image-only.test.ts index 11a37dee18..4d2a1736a1 100644 --- a/tests/unit/provider-validation-image-only.test.ts +++ b/tests/unit/provider-validation-image-only.test.ts @@ -36,7 +36,7 @@ const imageOnlyProviders = { value: "topaz-key", }, magnific: { - url: "https://api.magnific.com/v1/ai/mystic", + url: "https://api.magnific.com/v1/ai/flows", header: "x-magnific-api-key", value: "magnific-key", }, @@ -101,11 +101,11 @@ for (const provider of Object.keys(imageOnlyProviders)) { } } -test("freepik alias validates through the Magnific Mystic endpoint", async () => { +test("freepik alias validates through the Magnific Flows endpoint", async () => { let fetchCalled = false; globalThis.fetch = async (url, init = {}) => { fetchCalled = true; - assert.equal(String(url), "https://api.magnific.com/v1/ai/mystic"); + assert.equal(String(url), "https://api.magnific.com/v1/ai/flows"); assert.equal((init.headers as Record)["x-magnific-api-key"], "legacy-key"); return new Response(JSON.stringify({ data: [] }), { status: 200 }); }; diff --git a/tests/unit/release-acceptance-cli.test.ts b/tests/unit/release-acceptance-cli.test.ts new file mode 100644 index 0000000000..952399065d --- /dev/null +++ b/tests/unit/release-acceptance-cli.test.ts @@ -0,0 +1,174 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + exitFor, + reduceManifests, + main, +} from "../../scripts/quality/validate-release-acceptance.mjs"; + +const SHA = "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e"; +function key(id) { + return { gate_id: id, suite_id: null, shard_index: null, shard_total: null }; +} +function gate(id, status) { + return { + gate_id: id, + suite_id: null, + shard_index: null, + shard_total: null, + tested_sha: SHA, + run_id: "1", + run_attempt: 1, + command_id: id, + gate_type: "static", + status, + cause: null, + exit_code: status === "PASS" ? 0 : 1, + duration_ms: 1, + evidence: [ + { + artifact_id: "logs", + member: "lint.log", + algorithm: "sha256", + digest: "7f227db1653b6b723b07c8f2f6eb488f1f09e2f083ca7a3f5e02bbb274f5ff2e", + }, + ], + }; +} + +test("exit mapping", () => { + assert.equal(exitFor("VERIFIED"), 0); + assert.equal(exitFor("FAILED"), 1); + assert.equal(exitFor("UNVERIFIED"), 2); +}); + +test("three PASS manifests yield VERIFIED", () => { + const plan = { + required_gates: [key("a"), key("b"), key("c")], + identity: { tested_sha: SHA, run_id: "1", run_attempt: 1 }, + }; + const out = reduceManifests(plan, [ + { gates: [gate("a", "PASS")] }, + { gates: [gate("b", "PASS")] }, + { gates: [gate("c", "PASS")] }, + ]); + assert.equal(out.verdict, "VERIFIED"); + assert.equal(exitFor(out.verdict), 0); +}); + +test("one FAIL yields FAILED", () => { + const plan = { + required_gates: [key("a")], + identity: { tested_sha: SHA, run_id: "1", run_attempt: 1 }, + }; + const out = reduceManifests(plan, [{ gates: [gate("a", "FAIL")] }]); + assert.equal(out.verdict, "FAILED"); + assert.equal(exitFor(out.verdict), 1); +}); + +test("required missing yields UNVERIFIED", () => { + const plan = { + required_gates: [key("a"), key("b")], + identity: { tested_sha: SHA, run_id: "1", run_attempt: 1 }, + }; + const out = reduceManifests(plan, [{ gates: [gate("a", "PASS")] }]); + assert.equal(out.verdict, "UNVERIFIED"); + assert.equal(exitFor(out.verdict), 2); +}); + +test("workflow source-guard", () => { + const text = readFileSync(".github/workflows/release-acceptance.yml", "utf8"); + assert.match(text, /name: Release acceptance/); + assert.match(text, /cancel-in-progress: false/); + assert.equal(text.includes("gh issue close"), false); + assert.match(text, /if: github.event_name != 'pull_request'/); +}); + +test("schema_invalid does not throw when required_gates is missing", async () => { + const dir = mkdtempSync(join(tmpdir(), "acc-")); + writeFileSync( + join(dir, "plan.json"), + JSON.stringify({ + identity: { + repository: "diegosouzapw/OmniRoute", + run_id: "1", + run_attempt: 1, + workflow: "release-acceptance.yml", + trigger: "push", + scope: "release", + requested_ref: "refs/heads/release/v3.8.51", + base_sha: SHA, + candidate_sha: SHA, + tested_sha: SHA, + }, + artifact: null, + }) + ); + const man = join(dir, "m"); + mkdirSync(man); + writeFileSync(join(man, "a.json"), JSON.stringify({ gates: [gate("a", "PASS")] })); + const out = join(dir, "report.json"); + const code = await main([ + "node", + "cli", + "--plan", + join(dir, "plan.json"), + "--manifests", + man, + "--out", + out, + ]); + assert.equal(code, 2); + const report = JSON.parse(readFileSync(out, "utf8")); + assert.equal(report.verdict, "UNVERIFIED"); + assert.ok(Array.isArray(report.required_gates)); + assert.ok(report.evidence_errors.some((e) => e.code === "empty_required_set")); + assert.equal( + report.evidence_errors.some((e) => e.code === "schema_invalid"), + false + ); +}); + +test("schema_invalid keeps FAILED when reduce already failed", async () => { + const dir = mkdtempSync(join(tmpdir(), "acc-fail-")); + const plan = { + required_gates: [key("a")], + identity: { + repository: "diegosouzapw/OmniRoute", + run_id: "1", + run_attempt: 1, + workflow: "release-acceptance.yml", + trigger: "push", + scope: "release", + requested_ref: "refs/heads/release/v3.8.51", + base_sha: SHA, + candidate_sha: SHA, + tested_sha: SHA, + }, + artifact: null, + }; + writeFileSync(join(dir, "plan.json"), JSON.stringify(plan)); + const man = join(dir, "m"); + mkdirSync(man); + const g = gate("a", "FAIL"); + g.unexpected = true; + writeFileSync(join(man, "a.json"), JSON.stringify({ gates: [g] })); + const out = join(dir, "report.json"); + const code = await main([ + "node", + "cli", + "--plan", + join(dir, "plan.json"), + "--manifests", + man, + "--out", + out, + ]); + assert.equal(code, 1); + const report = JSON.parse(readFileSync(out, "utf8")); + assert.equal(report.verdict, "FAILED"); + assert.ok(report.evidence_errors.some((e) => e.code === "schema_invalid")); +}); diff --git a/tests/unit/release-acceptance-close-oracle.test.ts b/tests/unit/release-acceptance-close-oracle.test.ts new file mode 100644 index 0000000000..96ae3f8659 --- /dev/null +++ b/tests/unit/release-acceptance-close-oracle.test.ts @@ -0,0 +1,23 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { + findTrackerCloses, + closingKeywordInBody, +} from "../../scripts/quality/release-acceptance/closeOracle.mjs"; + +test("nightly still auto-closes the tracker via two steps (deliberate, #12085)", () => { + const text = readFileSync(".github/workflows/nightly-release-green.yml", "utf8"); + assert.equal(findTrackerCloses(text).length, 2); + const legacy = readFileSync( + new URL("../fixtures/release-acceptance/legacy-close-steps.yml", import.meta.url), + "utf8" + ); + assert.equal(findTrackerCloses(legacy).length, 2); +}); + +test("Fixes #12732 is a closing keyword; Related to #12732 is not", () => { + assert.equal(closingKeywordInBody("Fixes #12732.\n"), true); + assert.equal(closingKeywordInBody("Related to #12732.\n"), false); + assert.equal(closingKeywordInBody("Fixes #1. Closes #12732\n"), true); +}); diff --git a/tests/unit/release-acceptance-inventory.test.ts b/tests/unit/release-acceptance-inventory.test.ts new file mode 100644 index 0000000000..dc5e2e496c --- /dev/null +++ b/tests/unit/release-acceptance-inventory.test.ts @@ -0,0 +1,40 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { COLLECTORS } from "../../scripts/check/check-test-discovery.mjs"; +import { + knownUnexecuted, + inventoryErrors, +} from "../../scripts/quality/release-acceptance/inventory.mjs"; + +const RELEASE_SUITES = ["test:unit:ci", "test:vitest", "test:integration"]; +const baseline = JSON.parse( + readFileSync(new URL("../../config/quality/test-discovery-baseline.json", import.meta.url), "utf8") +); + +test("tsx files under tests/unit are known_unexecuted for release scope, not inventory errors", () => { + const ku = knownUnexecuted(RELEASE_SUITES, COLLECTORS, baseline); + const tsx = ku.collectors.find((c) => c.glob === "tests/unit/**/*.test.tsx"); + assert.ok(tsx, "tsx collector must be listed as known_unexecuted"); + assert.equal(typeof tsx.count, "number"); + assert.ok(tsx.count > 0); +}); + +test("omitting a collector without listing it is an inventory error", () => { + const collectors = COLLECTORS.filter((c) => c.glob !== "tests/unit/**/*.test.tsx"); + const discoveredFiles = ["tests/unit/AutoComboCatalog.test.tsx"]; + const errors = inventoryErrors(RELEASE_SUITES, collectors, baseline, discoveredFiles); + assert.ok(errors.some((e) => e.code === "collector_omitted")); +}); + +test("combo-matrix glob is in release integration scope, not known_unexecuted", () => { + const ku = knownUnexecuted(RELEASE_SUITES, COLLECTORS, baseline); + assert.equal( + ku.collectors.some((c) => c.glob === "tests/integration/combo-matrix/*.test.ts"), + false + ); + const combo = COLLECTORS.find( + (c) => c.glob === "tests/integration/combo-matrix/*.test.ts" + ); + assert.ok(combo); +}); diff --git a/tests/unit/release-acceptance-node-reporter.test.ts b/tests/unit/release-acceptance-node-reporter.test.ts new file mode 100644 index 0000000000..4e8c47cdfc --- /dev/null +++ b/tests/unit/release-acceptance-node-reporter.test.ts @@ -0,0 +1,86 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { fromNodeTestTap } from "../../scripts/quality/release-acceptance/nodeReporter.mjs"; + +const TAP = `TAP version 13 +# Subtest: tests/unit/a.test.ts +ok 1 - tests/unit/a.test.ts +# Subtest: tests/unit/b.test.ts +ok 2 - tests/unit/b.test.ts +# Subtest: tests/unit/c.test.ts +not ok 3 - tests/unit/c.test.ts +`; + +test("argv file without TAP completion is missing", () => { + const out = fromNodeTestTap(TAP, [ + "tests/unit/a.test.ts", + "tests/unit/b.test.ts", + "tests/unit/c.test.ts", + "tests/unit/d.test.ts", + ]); + assert.equal(out.completed.length, 2); + assert.deepEqual(out.failed, ["tests/unit/c.test.ts"]); + assert.equal(out.missing.length, 1); + assert.equal(out.missing[0], "tests/unit/d.test.ts"); + assert.equal(out.pass, false); +}); + +test("zero completed files is not PASS", () => { + const out = fromNodeTestTap("TAP version 13\n", ["tests/unit/a.test.ts"]); + assert.equal(out.completed.length, 0); + assert.equal(out.pass, false); +}); + +test("Subtest path wins when the result line has a short name", () => { + const tap = `TAP version 13 +# Subtest: tests/unit/a.test.ts +ok 1 - some name +`; + const out = fromNodeTestTap(tap, ["tests/unit/a.test.ts"]); + assert.equal(out.completed[0], "tests/unit/a.test.ts"); + assert.equal(out.missing.length, 0); +}); + +test("not ok is not pass", () => { + const tap = `TAP version 13 +# Subtest: tests/unit/a.test.ts +not ok 1 - tests/unit/a.test.ts +`; + const out = fromNodeTestTap(tap, ["tests/unit/a.test.ts"]); + assert.equal(out.pass, false); + assert.deepEqual(out.failed, ["tests/unit/a.test.ts"]); +}); + +test("ok line without Subtest does not complete an argv file", () => { + const tap = `# a malicious test printed: +ok 99 - tests/unit/missing.test.ts +`; + const out = fromNodeTestTap(tap, ["tests/unit/missing.test.ts"]); + assert.equal(out.pass, false); + assert.deepEqual(out.missing, ["tests/unit/missing.test.ts"]); +}); + +test("later not ok retracts an earlier ok for the same Subtest", () => { + const tap = `TAP version 13 +# Subtest: tests/unit/a.test.ts +ok 1 - tests/unit/a.test.ts +# Subtest: tests/unit/a.test.ts +not ok 2 - tests/unit/a.test.ts +`; + const out = fromNodeTestTap(tap, ["tests/unit/a.test.ts"]); + assert.equal(out.pass, false); + assert.deepEqual(out.failed, ["tests/unit/a.test.ts"]); + assert.equal(out.completed.includes("tests/unit/a.test.ts"), false); +}); + +test("later not ok on the same pending Subtest retracts ok", () => { + const tap = `TAP version 13 +# Subtest: tests/unit/a.test.ts +ok 1 - tests/unit/a.test.ts +not ok 2 - tests/unit/a.test.ts +`; + const out = fromNodeTestTap(tap, ["tests/unit/a.test.ts"]); + assert.equal(out.pass, false); + assert.deepEqual(out.failed, ["tests/unit/a.test.ts"]); + assert.equal(out.completed.includes("tests/unit/a.test.ts"), false); +}); diff --git a/tests/unit/release-acceptance-pack-boot.test.ts b/tests/unit/release-acceptance-pack-boot.test.ts new file mode 100644 index 0000000000..a594941cfb --- /dev/null +++ b/tests/unit/release-acceptance-pack-boot.test.ts @@ -0,0 +1,68 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { reduce } from "../../scripts/quality/release-acceptance/reduce.mjs"; + +const SHA = "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e"; +const planPack = { + required_gates: [ + { gate_id: "pack-artifact", suite_id: null, shard_index: null, shard_total: null }, + { gate_id: "pack-boot", suite_id: null, shard_index: null, shard_total: null }, + ], + identity: { tested_sha: SHA, run_id: "1", run_attempt: 1 }, + dependencies: { "pack-boot": "pack-artifact" }, +}; + +function record(partial) { + return { + gate_id: "pack-artifact", + suite_id: null, + shard_index: null, + shard_total: null, + tested_sha: SHA, + run_id: "1", + run_attempt: 1, + command_id: "check:pack-artifact", + gate_type: "artifact", + status: "PASS", + cause: null, + exit_code: 0, + duration_ms: 10, + evidence: [], + ...partial, + }; +} + +test("legacy computeVerdict still hard-fails pack-boot when pack-artifact times out", async () => { + const { computeVerdict } = await import("../../scripts/quality/validate-release-green.mjs"); + const v = computeVerdict([ + { id: "pack-artifact", kind: "hard", ok: false, detail: "timeout" }, + { + id: "pack-boot", + kind: "hard", + ok: false, + detail: "skipped because package-artifact did not produce a valid dist/ build", + }, + ]); + assert.equal(v.releaseGreen, false); +}); + +test("new reducer maps the same timeout to UNVERIFIED", () => { + const out = reduce(planPack, [ + record({ gate_id: "pack-artifact", status: "INFRA_ERROR" }), + ]); + assert.equal(out.verdict, "UNVERIFIED"); +}); + +test("synthesized pack-boot without identity.tested_sha keeps a 40-hex sha and FAILED", () => { + const plan = { + required_gates: planPack.required_gates, + identity: { run_id: "1", run_attempt: 1 }, + dependencies: { "pack-boot": "pack-artifact" }, + }; + const out = reduce(plan, [record({ gate_id: "pack-artifact", status: "FAIL" })]); + const boot = out.gates.find((g) => g.gate_id === "pack-boot"); + assert.ok(boot); + assert.notEqual(boot.tested_sha, null); + assert.match(String(boot.tested_sha), /^[0-9a-f]{40}$/); + assert.equal(out.verdict, "FAILED"); +}); diff --git a/tests/unit/release-acceptance-reduce.test.ts b/tests/unit/release-acceptance-reduce.test.ts new file mode 100644 index 0000000000..02152362fd --- /dev/null +++ b/tests/unit/release-acceptance-reduce.test.ts @@ -0,0 +1,287 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { reduce } from "../../scripts/quality/release-acceptance/reduce.mjs"; + +const SHA = "30b5bf18fbe827a0283ce17e91bda22cc8b4c13e"; + +function key(gate_id) { + return { gate_id, suite_id: null, shard_index: null, shard_total: null }; +} + +function record(partial) { + return { + gate_id: "lint", + suite_id: null, + shard_index: null, + shard_total: null, + tested_sha: SHA, + run_id: "1", + run_attempt: 1, + command_id: partial.gate_id ?? "lint", + gate_type: "static", + status: "PASS", + cause: null, + exit_code: 0, + duration_ms: 10, + evidence: [], + ...partial, + }; +} + +function planWithRequired(gateId, extra = {}) { + return { + required_gates: [key(gateId)], + identity: { tested_sha: SHA, run_id: "1", run_attempt: 1 }, + ...extra, + }; +} + +const planPack = { + required_gates: [key("pack-artifact"), key("pack-boot")], + identity: { tested_sha: SHA, run_id: "1", run_attempt: 1 }, + dependencies: { "pack-boot": "pack-artifact" }, +}; + +test("required SKIPPED never yields VERIFIED", () => { + const out = reduce(planWithRequired("lint"), [ + record({ gate_id: "lint", status: "SKIPPED", reason: "optional-looking" }), + ]); + assert.equal(out.verdict, "UNVERIFIED"); +}); + +test("pack-artifact FAIL classifies pack-boot as FAIL with cause", () => { + const out = reduce(planPack, [ + record({ gate_id: "pack-artifact", status: "FAIL", gate_type: "artifact" }), + ]); + const boot = out.gates.find((g) => g.gate_id === "pack-boot"); + assert.equal(boot.status, "FAIL"); + assert.equal(boot.cause.gate_id, "pack-artifact"); + assert.equal(out.verdict, "FAILED"); +}); + +test("pack-artifact INFRA_ERROR classifies pack-boot as INFRA_ERROR", () => { + const out = reduce(planPack, [ + record({ + gate_id: "pack-artifact", + status: "INFRA_ERROR", + gate_type: "artifact", + }), + ]); + const boot = out.gates.find((g) => g.gate_id === "pack-boot"); + assert.equal(boot.status, "INFRA_ERROR"); + assert.equal(out.verdict, "UNVERIFIED"); +}); + +test("plan that marks a required gate's prerequisite optional is rejected", () => { + const illegalPlan = { + required_gates: [key("pack-boot")], + identity: { tested_sha: SHA, run_id: "1", run_attempt: 1 }, + dependencies: { "pack-boot": "pack-artifact" }, + optional_gates: [key("pack-artifact")], + }; + assert.throws(() => reduce(illegalPlan, []), /optional prerequisite/); +}); + +test("required SKIPPED prerequisite classifies dependent as SKIPPED, does not throw", () => { + const out = reduce(planPack, [ + record({ + gate_id: "pack-artifact", + status: "SKIPPED", + reason: "runner skipped", + gate_type: "artifact", + }), + ]); + const boot = out.gates.find((g) => g.gate_id === "pack-boot"); + assert.equal(boot.status, "SKIPPED"); + assert.equal(boot.cause.gate_id, "pack-artifact"); + assert.equal(out.verdict, "UNVERIFIED"); +}); + +test("INFRA_ERROR artifact reclassifies an already-emitted FAIL boot to INFRA_ERROR", () => { + const out = reduce(planPack, [ + record({ + gate_id: "pack-artifact", + status: "INFRA_ERROR", + gate_type: "artifact", + }), + record({ + gate_id: "pack-boot", + status: "FAIL", + gate_type: "artifact", + }), + ]); + const boot = out.gates.find((g) => g.gate_id === "pack-boot"); + assert.equal(boot.status, "INFRA_ERROR"); + assert.equal(boot.cause.gate_id, "pack-artifact"); + assert.equal(out.verdict, "UNVERIFIED"); +}); + +test("empty required_gates is UNVERIFIED", () => { + const out = reduce( + { required_gates: [], identity: { tested_sha: SHA, run_id: "1", run_attempt: 1 } }, + [record({ gate_id: "lint", status: "PASS" })] + ); + assert.equal(out.verdict, "UNVERIFIED"); + assert.ok(out.evidence_errors.some((e) => e.code === "empty_required_set")); +}); + +test("INFRA_ERROR artifact reclassifies every FAIL boot copy", () => { + const out = reduce(planPack, [ + record({ gate_id: "pack-artifact", status: "INFRA_ERROR", gate_type: "artifact" }), + record({ gate_id: "pack-boot", status: "FAIL", gate_type: "artifact" }), + record({ gate_id: "pack-boot", status: "FAIL", gate_type: "artifact" }), + ]); + const boots = out.gates.filter((g) => g.gate_id === "pack-boot"); + assert.ok(boots.length >= 1); + assert.ok(boots.every((g) => g.status === "INFRA_ERROR")); + assert.equal(out.verdict, "UNVERIFIED"); +}); + +test("transitive INFRA on a three-gate chain is UNVERIFIED, not leaked FAIL", () => { + const plan = { + required_gates: [key("a"), key("b"), key("c")], + identity: { tested_sha: SHA, run_id: "1", run_attempt: 1 }, + dependencies: { b: "a", c: "b" }, + }; + const out = reduce(plan, [ + record({ gate_id: "a", status: "INFRA_ERROR", gate_type: "artifact" }), + record({ gate_id: "b", status: "FAIL", gate_type: "artifact" }), + record({ gate_id: "c", status: "PASS", gate_type: "artifact" }), + ]); + assert.equal(out.gates.find((g) => g.gate_id === "a").status, "INFRA_ERROR"); + assert.equal(out.gates.find((g) => g.gate_id === "b").status, "INFRA_ERROR"); + assert.equal(out.gates.find((g) => g.gate_id === "c").status, "INFRA_ERROR"); + assert.equal(out.verdict, "UNVERIFIED"); +}); + +test("transitive FAIL on a three-gate chain classifies every dependent", () => { + const plan = { + required_gates: [key("a"), key("b"), key("c")], + identity: { tested_sha: SHA, run_id: "1", run_attempt: 1 }, + dependencies: { b: "a", c: "b" }, + }; + const out = reduce(plan, [ + record({ gate_id: "a", status: "FAIL", gate_type: "artifact" }), + record({ gate_id: "b", status: "PASS", gate_type: "artifact" }), + record({ gate_id: "c", status: "PASS", gate_type: "artifact" }), + ]); + assert.equal(out.gates.find((g) => g.gate_id === "b").status, "FAIL"); + assert.equal(out.gates.find((g) => g.gate_id === "c").status, "FAIL"); + assert.equal(out.verdict, "FAILED"); +}); + +test("INFRA copy of a required gate dominates a FAIL copy of the same key", () => { + const out = reduce(planPack, [ + record({ gate_id: "pack-artifact", status: "INFRA_ERROR", gate_type: "artifact" }), + record({ gate_id: "pack-artifact", status: "FAIL", gate_type: "artifact" }), + record({ gate_id: "pack-boot", status: "PASS", gate_type: "artifact" }), + ]); + assert.equal(out.verdict, "UNVERIFIED"); + const boot = out.gates.find((g) => g.gate_id === "pack-boot"); + assert.equal(boot.status, "INFRA_ERROR"); +}); + +test("cyclic dependencies are rejected", () => { + const cyclic = { + required_gates: [key("a"), key("b")], + identity: { tested_sha: SHA, run_id: "1", run_attempt: 1 }, + dependencies: { a: "b", b: "a" }, + }; + assert.throws( + () => reduce(cyclic, [record({ gate_id: "a", status: "INFRA_ERROR" }), record({ gate_id: "b", status: "FAIL" })]), + /cyclic prerequisite/ + ); +}); + +test("missing prerequisite records one evidence error, not one per loop", () => { + const out = reduce( + { + required_gates: [key("boot")], + identity: { tested_sha: SHA, run_id: "1", run_attempt: 1 }, + dependencies: { boot: "art" }, + }, + [] + ); + assert.equal(out.verdict, "UNVERIFIED"); + assert.equal( + out.evidence_errors.filter((e) => e.code === "prerequisite_missing").length, + 1 + ); +}); + +test("missing prerequisite records one evidence error for all shards of a gate_id", () => { + const shard0 = { gate_id: "u", suite_id: "s", shard_index: 0, shard_total: 2 }; + const shard1 = { gate_id: "u", suite_id: "s", shard_index: 1, shard_total: 2 }; + const out = reduce( + { + required_gates: [shard0, shard1], + identity: { tested_sha: SHA, run_id: "1", run_attempt: 1 }, + dependencies: { u: "art" }, + }, + [] + ); + assert.equal(out.verdict, "UNVERIFIED"); + assert.equal( + out.evidence_errors.filter((e) => e.code === "prerequisite_missing").length, + 1 + ); +}); + +test("two dependents of the same missing prerequisite keep one error per edge", () => { + const out = reduce( + { + required_gates: [key("boot"), key("pack")], + identity: { tested_sha: SHA, run_id: "1", run_attempt: 1 }, + dependencies: { boot: "art", pack: "art" }, + }, + [] + ); + assert.equal(out.verdict, "UNVERIFIED"); + const missing = out.evidence_errors.filter((e) => e.code === "prerequisite_missing"); + assert.equal(missing.length, 2); + const gates = missing.map((e) => e.gate?.gate_id).sort(); + assert.deepEqual(gates, ["boot", "pack"]); + assert.equal(out.gates.find((g) => g.gate_id === "boot")?.status, "INFRA_ERROR"); + assert.equal(out.gates.find((g) => g.gate_id === "pack")?.status, "INFRA_ERROR"); +}); + +test("sharded required dependents inherit a FAIL prerequisite of the same gate_id", () => { + const shard0 = { gate_id: "u", suite_id: "s", shard_index: 0, shard_total: 2 }; + const shard1 = { gate_id: "u", suite_id: "s", shard_index: 1, shard_total: 2 }; + const out = reduce( + { + required_gates: [shard0, shard1], + identity: { tested_sha: SHA, run_id: "1", run_attempt: 1 }, + dependencies: { u: "art" }, + }, + [ + record({ gate_id: "art", status: "FAIL", gate_type: "artifact" }), + record({ ...shard0, status: "PASS", gate_type: "artifact" }), + record({ ...shard1, status: "PASS", gate_type: "artifact" }), + ] + ); + const shards = out.gates.filter((g) => g.gate_id === "u" && g.suite_id === "s"); + assert.equal(shards.length, 2); + assert.ok(shards.every((g) => g.status === "FAIL")); + assert.equal(out.verdict, "FAILED"); +}); + +test("sharded required dependents inherit an INFRA prerequisite of the same gate_id", () => { + const shard0 = { gate_id: "u", suite_id: "s", shard_index: 0, shard_total: 2 }; + const shard1 = { gate_id: "u", suite_id: "s", shard_index: 1, shard_total: 2 }; + const out = reduce( + { + required_gates: [shard0, shard1], + identity: { tested_sha: SHA, run_id: "1", run_attempt: 1 }, + dependencies: { u: "art" }, + }, + [ + record({ gate_id: "art", status: "INFRA_ERROR", gate_type: "artifact" }), + record({ ...shard0, status: "PASS", gate_type: "artifact" }), + record({ ...shard1, status: "PASS", gate_type: "artifact" }), + ] + ); + const shards = out.gates.filter((g) => g.gate_id === "u" && g.suite_id === "s"); + assert.ok(shards.every((g) => g.status === "INFRA_ERROR")); + assert.equal(out.verdict, "UNVERIFIED"); +}); diff --git a/tests/unit/release-acceptance-schema.test.ts b/tests/unit/release-acceptance-schema.test.ts new file mode 100644 index 0000000000..461667be6b --- /dev/null +++ b/tests/unit/release-acceptance-schema.test.ts @@ -0,0 +1,112 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import Ajv from "ajv"; + +const schema = JSON.parse( + readFileSync( + new URL("../../config/quality/release-acceptance.schema.json", import.meta.url), + "utf8" + ) +); + +function compile() { + const ajv = new Ajv({ allErrors: true, strict: false }); + return ajv.compile(schema); +} + +test("version 1 requires cause when status is classified by a prerequisite", () => { + const validate = compile(); + const missingCause = JSON.parse( + readFileSync( + new URL("../fixtures/release-acceptance/failed-pack-boot.json", import.meta.url), + "utf8" + ) + ); + delete missingCause.gates[1].cause; + assert.equal(validate(missingCause), false); +}); + +test("unknown top-level gate field is invalid in version 1", () => { + const validate = compile(); + const extra = JSON.parse( + readFileSync( + new URL("../fixtures/release-acceptance/verified.json", import.meta.url), + "utf8" + ) + ); + extra.gates[0].unexpected = true; + assert.equal(validate(extra), false); +}); + +test("evidence member rejects parent traversal", () => { + const validate = compile(); + const report = JSON.parse( + readFileSync(new URL("../fixtures/release-acceptance/verified.json", import.meta.url), "utf8") + ); + report.gates[0].evidence[0].member = "foo/../../etc/passwd"; + assert.equal(validate(report), false); + report.gates[0].evidence[0].member = ".."; + assert.equal(validate(report), false); + report.gates[0].evidence[0].member = "foo/.."; + assert.equal(validate(report), false); + report.gates[0].evidence[0].member = String.raw`foo\..\x`; + assert.equal(validate(report), false); +}); + +test("empty required_gates cannot be VERIFIED", () => { + const validate = compile(); + const report = JSON.parse( + readFileSync(new URL("../fixtures/release-acceptance/verified.json", import.meta.url), "utf8") + ); + report.required_gates = []; + report.gates = []; + report.evidence_errors = []; + report.verdict = "VERIFIED"; + assert.equal(validate(report), false); +}); + +test("empty required_gates is valid when UNVERIFIED", () => { + const validate = compile(); + const report = JSON.parse( + readFileSync(new URL("../fixtures/release-acceptance/verified.json", import.meta.url), "utf8") + ); + report.required_gates = []; + report.gates = []; + report.evidence_errors = [ + { + code: "empty_required_set", + gate: { gate_id: "schema", suite_id: null, shard_index: null, shard_total: null }, + detail: "required_gates is empty", + }, + ]; + report.verdict = "UNVERIFIED"; + assert.equal(validate(report), true, JSON.stringify(validate.errors)); +}); + +test("unknown extensions field is invalid in version 1", () => { + const validate = compile(); + const extra = JSON.parse( + readFileSync( + new URL("../fixtures/release-acceptance/verified.json", import.meta.url), + "utf8" + ) + ); + extra.gates[0].extensions = { unexpected: true }; + assert.equal(validate(extra), false); +}); + +test("known-answer fixtures validate", () => { + const validate = compile(); + for (const name of [ + "verified.json", + "failed-pack-boot.json", + "unverified-required-skipped.json", + "infra-pack-boot.json", + ]) { + const report = JSON.parse( + readFileSync(new URL(`../fixtures/release-acceptance/${name}`, import.meta.url), "utf8") + ); + assert.equal(validate(report), true, `${name}: ${JSON.stringify(validate.errors)}`); + } +}); diff --git a/tests/unit/release-acceptance-static-adapter.test.ts b/tests/unit/release-acceptance-static-adapter.test.ts new file mode 100644 index 0000000000..64d70292a6 --- /dev/null +++ b/tests/unit/release-acceptance-static-adapter.test.ts @@ -0,0 +1,33 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { adaptCompiler } from "../../scripts/quality/release-acceptance/staticAdapter.mjs"; + +test("empty diagnostics with nonempty digest and exit 0 is PASS", () => { + const out = adaptCompiler({ + commandId: "tsc", + inputDigest: "a".repeat(64), + exitCode: 0, + diagnostics: [], + }); + assert.equal(out.status, "PASS"); +}); + +test("empty digest plus empty diagnostics is INFRA_ERROR", () => { + const out = adaptCompiler({ + commandId: "tsc", + inputDigest: "", + exitCode: 0, + diagnostics: [], + }); + assert.equal(out.status, "INFRA_ERROR"); +}); + +test("exit 1 with diagnostics is FAIL", () => { + const out = adaptCompiler({ + commandId: "tsc", + inputDigest: "a".repeat(64), + exitCode: 1, + diagnostics: ["error TS2304"], + }); + assert.equal(out.status, "FAIL"); +}); diff --git a/tests/unit/responses-custom-tool-choice-13122.test.ts b/tests/unit/responses-custom-tool-choice-13122.test.ts new file mode 100644 index 0000000000..4c6c7337c3 --- /dev/null +++ b/tests/unit/responses-custom-tool-choice-13122.test.ts @@ -0,0 +1,136 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { openaiResponsesToOpenAIRequest } = + await import("../../open-sse/translator/request/openai-responses.ts"); +const { openaiToOpenAIResponsesResponse } = + await import("../../open-sse/translator/response/openai-responses.ts"); +const { initState } = await import("../../open-sse/translator/index.ts"); +const { FORMATS } = await import("../../open-sse/translator/formats.ts"); + +// #13122: Codex CLI (wire_api="responses") sends tool_choice.type = "custom" when it wants +// to force the model to call a declared freeform/custom tool (e.g. functions__exec). OmniRoute +// rejects this with an unsupported_feature error, even though the same request's `tools[].type +// = "custom"` declaration is already accepted and normalized into a Chat { input: string } +// function schema a few lines above (see translator-openai-responses-custom-tool-1007.test.ts). +test("Responses -> Chat: tool_choice.type = 'custom' forces the named custom tool instead of throwing (#13122)", () => { + const body = { + model: "kr/gpt-5.6-sol", + tools: [ + { + type: "custom", + name: "functions__exec", + description: "Execute freeform code", + }, + ], + tool_choice: { + type: "custom", + name: "functions__exec", + }, + input: [ + { + type: "message", + role: "user", + content: [{ type: "input_text", text: 'Call functions__exec with exactly: text("ok")' }], + }, + ], + stream: false, + }; + + // Previously (buggy) this threw: + // "Unsupported Responses API feature: tool_choice type 'custom' is not supported by omniroute" + // reproducing the exact error body from issue #13122's Reproduction 1 curl. + const result = openaiResponsesToOpenAIRequest("kr/gpt-5.6-sol", body, false, {}); + + assert.deepEqual(result.tool_choice, { + type: "function", + function: { name: "functions__exec" }, + }); +}); + +// A {type:"custom"} tool_choice with no `name` is not spec-compliant (Responses API's +// ToolChoiceCustom always requires `name`) — it must still fall through to the existing +// unsupported_feature throw rather than silently producing a malformed tool_choice. +test("Responses -> Chat: tool_choice.type = 'custom' without a name still throws unsupported_feature (#13122)", () => { + const body = { + model: "kr/gpt-5.6-sol", + tools: [{ type: "custom", name: "functions__exec", description: "Execute freeform code" }], + tool_choice: { type: "custom" }, + input: [ + { + type: "message", + role: "user", + content: [{ type: "input_text", text: "hi" }], + }, + ], + stream: false, + }; + + assert.throws( + () => openaiResponsesToOpenAIRequest("kr/gpt-5.6-sol", body, false, {}), + /Unsupported Responses API feature: tool_choice type 'custom' is not supported by omniroute/ + ); +}); + +// End-to-end: the Chat tool_choice produced above (forcing a call to the declared custom +// tool) must, once the model actually calls it, round-trip back out through the Responses +// response translator as a `custom_tool_call` item with a raw (unwrapped) `input` string — +// not a `function_call` item with JSON arguments. This is the behavior Codex CLI actually +// depends on to complete the custom_tool_call / custom_tool_call_output lifecycle. +test("OpenAI -> Responses: forced custom tool call round-trips as custom_tool_call with raw input (#13122)", () => { + const state = initState(FORMATS.OPENAI_RESPONSES); + state.customToolNames = new Set(["functions__exec"]); + + const events: unknown[] = []; + const chunks = [ + { + id: "chatcmpl-1", + model: "kr/gpt-5.6-sol", + choices: [ + { + index: 0, + delta: { + tool_calls: [ + { + index: 0, + id: "call_1", + type: "function", + function: { name: "functions__exec", arguments: '{"input":"text(' }, + }, + ], + }, + finish_reason: null, + }, + ], + }, + { + id: "chatcmpl-1", + model: "kr/gpt-5.6-sol", + choices: [ + { + index: 0, + delta: { tool_calls: [{ index: 0, function: { arguments: '\\"ok\\")"}' } }] }, + finish_reason: "tool_calls", + }, + ], + }, + null, + ]; + for (const chunk of chunks) { + const result = openaiToOpenAIResponsesResponse(chunk, state); + if (result) events.push(...result); + } + + const added = events.find((e: { event?: string }) => e.event === "response.output_item.added") as + { data: { item: { type: string; name: string } } } | undefined; + assert.ok(added); + assert.equal(added.data.item.type, "custom_tool_call"); + assert.equal(added.data.item.name, "functions__exec"); + + const itemDone = events.find( + (e: { event?: string; data?: { item?: { type?: string } } }) => + e.event === "response.output_item.done" && e.data?.item?.type === "custom_tool_call" + ) as { data: { item: { input: string } } } | undefined; + assert.ok(itemDone); + assert.equal(itemDone.data.item.input, 'text("ok")'); +}); diff --git a/tests/unit/responsesanitizer-reasoning-split.test.ts b/tests/unit/responsesanitizer-reasoning-split.test.ts index 70ee456d7d..538a3a0d4a 100644 --- a/tests/unit/responsesanitizer-reasoning-split.test.ts +++ b/tests/unit/responsesanitizer-reasoning-split.test.ts @@ -80,13 +80,13 @@ describe("responseSanitizer/reasoning — Kimi Code K3 textual reasoning-tag rou }); }); -// ── MiniMax M3 textual reasoning-tag route (9router#2231) ────────────────────── +// ── MiniMax M3 textual reasoning-tag route (9router#2231, #13558) ────────────── // // MiniMax M3 leaks raw ... into `content` instead of a separate -// reasoning_content field on the 8 OpenAI-format provider tiers below. The two -// direct minimax/minimax-cn tiers stay on Anthropic's Messages format -// (targetFormat: "claude") and already surface reasoning natively — they must -// stay unaffected. +// reasoning_content field on the 8 OpenAI-format provider tiers below, AND on +// its two direct minimax/minimax-cn tiers (Anthropic Messages format, +// targetFormat: "claude") — see the "MiniMax M3 fix regression guards" describe +// block below for those two. describe("responseSanitizer/reasoning — MiniMax M3 textual reasoning-tag route", () => { const affectedRoutes: Array<[string, string]> = [ ["trae", "minimax-m3"], @@ -137,14 +137,25 @@ describe("responseSanitizer/reasoning — MiniMax M3 textual reasoning-tag route }); describe("responseSanitizer/reasoning — MiniMax M3 fix regression guards", () => { - it("direct minimax tier (openai format) stays unaffected for textual reasoning tags", () => { - assert.equal(isTextualReasoningTagNativeRoute("minimax", "minimax-m3"), false); - assert.equal(shouldParseTextualReasoningTags("minimax", "MiniMax-M3"), false); + // #13558: the direct minimax/minimax-cn tiers (Anthropic Messages format) + // were previously excluded here on the false assumption that speaking + // Claude's wire format meant reasoning already arrived as a structured + // `thinking` block. MiniMax M3 leaks on these tiers too, so they + // must now be treated as tag-native routes just like the OpenAI-format + // tiers above. + it("direct minimax tier (Anthropic Messages format) IS affected for textual reasoning tags", () => { + assert.equal(isTextualReasoningTagNativeRoute("minimax", "minimax-m3"), true); + assert.equal(shouldParseTextualReasoningTags("minimax", "MiniMax-M3"), true); }); - it("direct minimax-cn tier (openai format) stays unaffected for textual reasoning tags", () => { - assert.equal(isTextualReasoningTagNativeRoute("minimax-cn", "minimax-m3"), false); - assert.equal(shouldParseTextualReasoningTags("minimax-cn", "MiniMax-M3"), false); + it("direct minimax-cn tier (Anthropic Messages format) IS affected for textual reasoning tags", () => { + assert.equal(isTextualReasoningTagNativeRoute("minimax-cn", "minimax-m3"), true); + assert.equal(shouldParseTextualReasoningTags("minimax-cn", "MiniMax-M3"), true); + }); + + it("non-M3 minimax models on the direct minimax/minimax-cn tiers stay unaffected", () => { + assert.equal(isTextualReasoningTagNativeRoute("minimax", "minimax-text-01"), false); + assert.equal(isTextualReasoningTagNativeRoute("minimax-cn", "abab6.5s-chat"), false); }); it("MiniMax M2.x (non-M3) models on OpenAI-format tiers stay unaffected", () => { diff --git a/tests/unit/security/cloudsync-signature-fail-open-13679.test.ts b/tests/unit/security/cloudsync-signature-fail-open-13679.test.ts new file mode 100644 index 0000000000..c8c960b35c --- /dev/null +++ b/tests/unit/security/cloudsync-signature-fail-open-13679.test.ts @@ -0,0 +1,91 @@ +/** + * Regression for #13679 PR A: verifyCloudSignature() must not fail open when a + * present X-Cloud-Sig cannot be verified (no local OMNIROUTE_CLOUD_SYNC_SECRET). + * + * Before this fix: a garbage/forged X-Cloud-Sig header was ALWAYS accepted when + * the local secret was unset ("we can't verify, but the server is at least + * trying — pass through"). That let a MITM on the CLOUD_URL channel, or a + * misconfigured/compromised CLOUD_URL, forge any signature value and have it + * accepted — defeating the point of the signature check for any install that + * hasn't issued a shared secret yet. + * + * Fix (owner decision 2026-09-15, PR A): + * (b) unconditional: a PRESENT-but-unverifiable signature is now rejected, + * regardless of the opt-in enforce flag below. + * (a) opt-in only (OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE=true, default OFF): + * also rejects a payload carrying NO signature at all. Default stays + * legacy pass-through for v3.8.x peers that haven't rotated in a shared + * secret yet — the default flips to enforced in v3.9. + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +const ORIGINAL_SECRET = process.env.OMNIROUTE_CLOUD_SYNC_SECRET; +const ORIGINAL_ENFORCE = process.env.OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE; + +function restoreEnv() { + if (ORIGINAL_SECRET === undefined) delete process.env.OMNIROUTE_CLOUD_SYNC_SECRET; + else process.env.OMNIROUTE_CLOUD_SYNC_SECRET = ORIGINAL_SECRET; + if (ORIGINAL_ENFORCE === undefined) delete process.env.OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE; + else process.env.OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE = ORIGINAL_ENFORCE; +} + +test.after(restoreEnv); + +test("issue #13679: a present-but-unverifiable X-Cloud-Sig is rejected even without a local secret", async () => { + delete process.env.OMNIROUTE_CLOUD_SYNC_SECRET; + delete process.env.OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE; + try { + const { verifyCloudSignature } = await import( + `../../../src/lib/cloudSync.ts?case=13679-forged-${Date.now()}-${Math.random()}` + ); + const rawBody = JSON.stringify({ providers: [{ id: "evil", accessToken: "stolen" }] }); + const forgedSig = "0".repeat(64); + + assert.equal( + verifyCloudSignature(rawBody, forgedSig), + false, + "a garbage X-Cloud-Sig must be REJECTED even when the local secret is unset (fail-open closed)" + ); + } finally { + restoreEnv(); + } +}); + +test("issue #13679: legacy peers with NO X-Cloud-Sig header still pass by default (v3.8.x back-compat)", async () => { + delete process.env.OMNIROUTE_CLOUD_SYNC_SECRET; + delete process.env.OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE; + try { + const { verifyCloudSignature } = await import( + `../../../src/lib/cloudSync.ts?case=13679-legacy-${Date.now()}-${Math.random()}` + ); + const rawBody = JSON.stringify({ providers: [] }); + + assert.equal( + verifyCloudSignature(rawBody, null), + true, + "an unsigned payload from a legacy peer must still pass through by default in v3.8.x" + ); + } finally { + restoreEnv(); + } +}); + +test("issue #13679: OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE=true rejects an unsigned payload too", async () => { + delete process.env.OMNIROUTE_CLOUD_SYNC_SECRET; + process.env.OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE = "true"; + try { + const { verifyCloudSignature } = await import( + `../../../src/lib/cloudSync.ts?case=13679-enforced-${Date.now()}-${Math.random()}` + ); + const rawBody = JSON.stringify({ providers: [] }); + + assert.equal( + verifyCloudSignature(rawBody, null), + false, + "with the opt-in enforce flag set, an unsigned payload must be rejected" + ); + } finally { + restoreEnv(); + } +}); diff --git a/tests/unit/server-owned-tool-loop-flag.test.ts b/tests/unit/server-owned-tool-loop-flag.test.ts index efd7817287..0ef0051c5d 100644 --- a/tests/unit/server-owned-tool-loop-flag.test.ts +++ b/tests/unit/server-owned-tool-loop-flag.test.ts @@ -68,7 +68,7 @@ describe("isServerOwnedToolLoopEnabled wrapper", () => { describe("feature-flags-settings count update", () => { it("flag count matches updated expected value", () => { - assert.equal(FEATURE_FLAG_DEFINITIONS.length, 69); + assert.equal(FEATURE_FLAG_DEFINITIONS.length, 70); }); }); diff --git a/tests/unit/settings-route-hide-variants-13562.test.ts b/tests/unit/settings-route-hide-variants-13562.test.ts new file mode 100644 index 0000000000..34496da021 --- /dev/null +++ b/tests/unit/settings-route-hide-variants-13562.test.ts @@ -0,0 +1,53 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { makeManagementSessionRequest } from "../helpers/managementSession.ts"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-issue-13562-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const settingsDb = await import("../../src/lib/db/settings.ts"); +const settingsRoute = await import("../../src/app/api/settings/route.ts"); + +async function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); + delete process.env.INITIAL_PASSWORD; +} + +test.beforeEach(async () => { + await resetStorage(); +}); +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +interface HideVariantSettings { + hidePaidModels: boolean; + hideAutoCombos: boolean; + hideNoThinkVariants: boolean; +} + +test("PATCH /api/settings persists hideAutoCombos / hideNoThinkVariants (#13562)", async () => { + const before = (await settingsDb.getSettings()) as unknown as HideVariantSettings; + assert.equal(before.hideAutoCombos, false); + assert.equal(before.hideNoThinkVariants, false); + + const response = await settingsRoute.PATCH( + await makeManagementSessionRequest("http://localhost/api/settings", { + method: "PATCH", + body: { hidePaidModels: true, hideAutoCombos: true, hideNoThinkVariants: true }, + }) + ); + const after = (await settingsDb.getSettings()) as unknown as HideVariantSettings; + + assert.equal(response.status, 200, "route should accept the PATCH"); + assert.equal(after.hidePaidModels, true, "control key should persist"); + assert.equal(after.hideAutoCombos, true, "hideAutoCombos should have persisted"); + assert.equal(after.hideNoThinkVariants, true, "hideNoThinkVariants should have persisted"); +}); diff --git a/tests/unit/tray-icon-contrast.test.ts b/tests/unit/tray-icon-contrast.test.ts new file mode 100644 index 0000000000..d9ebca19cf --- /dev/null +++ b/tests/unit/tray-icon-contrast.test.ts @@ -0,0 +1,104 @@ +// Regression guard for #13535: the Windows CLI system tray icon must contain a dark +// outline/stroke so it stays visible against the light-theme taskbar/hidden-icons +// background. Windows' NotifyIcon paints bitmap pixel colors literally — unlike macOS +// there is no "template image" auto-tinting — so a pure-white glyph with no outline is +// effectively invisible there. See bin/cli/tray/tray.ts::getIconPath() (prefers icon.ico +// on win32, falling back to icon.png) and electron/main.js (darwin-only setTemplateImage). +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync, existsSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import sharp from "sharp"; + +const repoRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url)))); +const ICON_PNG_PATH = join(repoRoot, "bin", "cli", "tray", "icon.png"); +const ICON_ICO_PATH = join(repoRoot, "bin", "cli", "tray", "icon.ico"); +const ELECTRON_TRAY_ICON_PATH = join(repoRoot, "electron", "assets", "tray-icon.png"); + +// Windows 11 Fluent "hidden icons" overflow flyout / light-theme taskbar background. +const LIGHT_BG = { r: 243, g: 243, b: 243 }; +// A representative dark-theme taskbar background. +const DARK_BG = { r: 32, g: 32, b: 32 }; + +function relativeLuminance({ r, g, b }: { r: number; g: number; b: number }): number { + const lin = (c: number) => { + const s = c / 255; + return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); + }; + const [rl, gl, bl] = [lin(r), lin(g), lin(b)]; + return 0.2126 * rl + 0.7152 * gl + 0.0722 * bl; +} + +function contrastRatio( + a: { r: number; g: number; b: number }, + b: { r: number; g: number; b: number } +): number { + const la = relativeLuminance(a); + const lb = relativeLuminance(b); + const [lighter, darker] = la >= lb ? [la, lb] : [lb, la]; + return (lighter + 0.05) / (darker + 0.05); +} + +async function assertHasVisibleContrastAgainstBothThemes(iconPath: string, label: string) { + const raw = readFileSync(iconPath); + const { data, info } = await sharp(raw).ensureAlpha().raw().toBuffer({ resolveWithObject: true }); + assert.equal(info.channels, 4, `expected RGBA after ensureAlpha() for ${label}`); + + let opaqueCount = 0; + let nonWhiteOpaqueCount = 0; + let bestContrastOnLight = 0; + let bestContrastOnDark = 0; + + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const a = data[i + 3]; + if (a < 128) continue; + opaqueCount++; + if (!(r === 255 && g === 255 && b === 255)) nonWhiteOpaqueCount++; + const ratioLight = contrastRatio({ r, g, b }, LIGHT_BG); + if (ratioLight > bestContrastOnLight) bestContrastOnLight = ratioLight; + const ratioDark = contrastRatio({ r, g, b }, DARK_BG); + if (ratioDark > bestContrastOnDark) bestContrastOnDark = ratioDark; + } + + assert.ok(opaqueCount > 0, `${label} has no opaque pixels at all — nothing would render`); + assert.ok( + nonWhiteOpaqueCount > 0, + `${label}'s glyph is pure-white-only (0 non-white opaque pixels out of ${opaqueCount}) — ` + + `no outline/stroke exists to provide contrast on a light background (bug #13535)` + ); + assert.ok( + bestContrastOnLight >= 3, + `${label}'s best pixel contrast against the Windows light tray background (#F3F3F3) is only ` + + `${bestContrastOnLight.toFixed(3)}:1, below the WCAG 3:1 UI-component minimum (bug #13535)` + ); + assert.ok( + bestContrastOnDark >= 3, + `${label}'s best pixel contrast against a dark tray background (#202020) is only ` + + `${bestContrastOnDark.toFixed(3)}:1, below the WCAG 3:1 UI-component minimum (bug #13535)` + ); +} + +test("icon.ico now ships next to icon.png so the win32 tray path uses the native .ico asset", () => { + assert.ok(existsSync(ICON_PNG_PATH), `expected ${ICON_PNG_PATH} to exist`); + assert.equal( + existsSync(ICON_ICO_PATH), + true, + "icon.ico is missing from bin/cli/tray/ — tray.ts::getIconPath() prefers it on win32 " + + "but silently falls back to icon.png when absent (bug #13535)" + ); +}); + +test("bin/cli/tray/icon.png has a dark outline visible on both light and dark Windows tray backgrounds", async () => { + await assertHasVisibleContrastAgainstBothThemes(ICON_PNG_PATH, "bin/cli/tray/icon.png"); +}); + +test("electron/assets/tray-icon.png has a dark outline visible on both light and dark tray backgrounds (Windows/Linux Electron tray, no template-image auto-tint)", async () => { + await assertHasVisibleContrastAgainstBothThemes( + ELECTRON_TRAY_ICON_PATH, + "electron/assets/tray-icon.png" + ); +}); diff --git a/tests/unit/vnc-svc-de-run-cdp-bridge-gate-13679.test.ts b/tests/unit/vnc-svc-de-run-cdp-bridge-gate-13679.test.ts new file mode 100644 index 0000000000..d3268d1e60 --- /dev/null +++ b/tests/unit/vnc-svc-de-run-cdp-bridge-gate-13679.test.ts @@ -0,0 +1,44 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +const REPO_ROOT = path.resolve(import.meta.dirname, "../.."); +const SVC_DE_RUN = path.join(REPO_ROOT, "docker/vnc-browser/chromium/svc-de-run"); + +// #13679 item #10 (residual gap): svc-de-run unconditionally spawns +// cdp-bridge.py on every container start, even though CDP_BRIDGE_TOKEN may be +// unset (in which case cdp-bridge.py's has_valid_token() fails closed for +// every caller anyway, per #12571) — the process still binds 0.0.0.0:9223 +// and accepts+drops connections for no reason. Gate the spawn behind the +// token actually being configured so an unconfigured container does not run +// a debug listener at all. + +function readScript(): string { + return fs.readFileSync(SVC_DE_RUN, "utf8"); +} + +test("svc-de-run only starts the CDP bridge when CDP_BRIDGE_TOKEN is configured", () => { + const script = readScript(); + const lines = script.split("\n"); + const launchIndexes = lines + .map((line, index) => ({ line, index })) + .filter(({ line }) => !/^\s*#/.test(line) && line.includes("cdp-bridge.py")) + .map(({ index }) => index); + + assert.ok( + launchIndexes.length >= 2, + "expected the wayland and X11 branches to both still launch cdp-bridge.py" + ); + + const GUARD_WINDOW = 5; + for (const index of launchIndexes) { + const precedingLines = lines.slice(Math.max(0, index - GUARD_WINDOW), index).join("\n"); + assert.match( + precedingLines, + /if\s*\[\s*-n\s*"\$\{CDP_BRIDGE_TOKEN/, + `svc-de-run must only launch cdp-bridge.py inside an "if [ -n \\"\${CDP_BRIDGE_TOKEN...` + + `\\" ]" guard, but found an unconditional launch at line ${index + 1}: ${lines[index].trim()}` + ); + } +}); diff --git a/tests/unit/xai-oauth-discovery.test.ts b/tests/unit/xai-oauth-discovery.test.ts new file mode 100644 index 0000000000..1d98b7857a --- /dev/null +++ b/tests/unit/xai-oauth-discovery.test.ts @@ -0,0 +1,124 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + PROVIDER_MODELS_CONFIG, + getXaiOauthLiveModelsConfig, +} from "../../src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts"; +import { HARDCODED_MODELS_CONFIG_IDS } from "../../src/lib/providerModels/hardcodedModelsConfigIds.ts"; +import { getDiscoveryClass } from "../../src/lib/providerModels/discoveryClass.ts"; +import { deriveConfigFromRegistryModelsUrl } from "../../src/app/api/providers/[id]/models/discoveryConfig.ts"; +import { getRegistryEntry } from "../../open-sse/config/providerRegistry.ts"; + +const core = await import("../../src/lib/db/core.ts"); + +const XAI_MODELS_URL = "https://api.x.ai/v1/models"; +const FLAG_KEY = "XAI_OAUTH_LIVE_MODEL_DISCOVERY"; + +const XAI_SEED_IDS = [ + "grok-4.6", + "grok-4.3", + "grok-build-0.1", + "grok-4.20-multi-agent-0309", + "grok-4.20-0309-reasoning", + "grok-4.20-0309-non-reasoning", +] + .slice() + .sort(); + +const XAI_OAUTH_SEED_IDS = [...XAI_SEED_IDS, "grok-4.5"].sort(); + +function modelIds(provider: string): string[] { + const entry = getRegistryEntry(provider); + assert.ok(entry, `${provider} registry entry missing`); + return (entry.models ?? []) + .map((model) => model.id) + .slice() + .sort(); +} + +test.after(() => { + delete process.env[FLAG_KEY]; + try { + core.resetDbInstance?.(); + } catch { + // best-effort cleanup + } +}); + +test("test 1: xai stays statically registered with the exact live-discovery shape", () => { + const apikey = PROVIDER_MODELS_CONFIG["xai"]; + assert.ok(apikey, "xai must exist in PROVIDER_MODELS_CONFIG"); + assert.equal(apikey.url, XAI_MODELS_URL); + assert.equal(apikey.method, "GET"); + assert.equal(apikey.authHeader, "Authorization"); + assert.equal(apikey.authPrefix, "Bearer "); +}); + +test("test 2: getXaiOauthLiveModelsConfig — flag off keeps xai-oauth on the frozen seed (real code path)", () => { + delete process.env[FLAG_KEY]; + assert.equal( + getXaiOauthLiveModelsConfig(), + undefined, + "with the flag unset (default false), xai-oauth must resolve to no live-discovery config" + ); +}); + +test("test 2b: getXaiOauthLiveModelsConfig — flag on resolves the same live-discovery shape as xai (real code path)", () => { + process.env[FLAG_KEY] = "true"; + try { + const live = getXaiOauthLiveModelsConfig(); + assert.ok(live, "with the flag on, xai-oauth must resolve a live-discovery config"); + assert.equal(live.url, XAI_MODELS_URL); + assert.equal(live.method, "GET"); + assert.equal(live.authHeader, "Authorization"); + assert.equal(live.authPrefix, "Bearer "); + } finally { + delete process.env[FLAG_KEY]; + } +}); + +test("test 3: xai-oauth is intentionally NOT in PROVIDER_MODELS_CONFIG / HARDCODED lockstep", () => { + assert.equal(PROVIDER_MODELS_CONFIG["xai-oauth"], undefined); + assert.equal(HARDCODED_MODELS_CONFIG_IDS.has("xai-oauth"), false); + const fromModule = [...HARDCODED_MODELS_CONFIG_IDS].sort(); + const fromConfig = Object.keys(PROVIDER_MODELS_CONFIG).sort(); + assert.deepEqual(fromModule, fromConfig); +}); + +test("test 4: getDiscoveryClass — xai is openai-compat; xai-oauth is static-only by default (flag off)", () => { + // xai-oauth is deliberately absent from HARDCODED_MODELS_CONFIG_IDS and has no + // registry modelsUrl, so with XAI_OAUTH_LIVE_MODEL_DISCOVERY off (default) it + // classifies as static-only — matching its pre-PR #13518 behavior. + assert.equal(getDiscoveryClass("xai-oauth"), "static-only"); + assert.equal(getDiscoveryClass("xai"), "openai-compat"); +}); + +// Former test 5 ("catalog siblings and search pairs stay unmerged") was a source-grep +// tautology: it read activeSyncedCatalog.ts / auth.ts as text, hand-parsed a bracket- +// matched block out of it, and asserted regexes over that extracted text — never +// exercising CATALOG_SIBLING_IDS or PROVIDER_SEARCH_PAIRS as real code. Both constants +// are module-private (not exported), so the only way to assert against their real +// values is to export them — an unrelated surface change outside this PR's scope (live +// xAI model discovery for xai-oauth). Dropped rather than kept as a tautology; a +// follow-up PR that exports those constants can add a real regression test for the +// xai/xai-oauth-not-merged invariant. + +test("test 6: gate 4 deriveConfig does not mutate registry, and stays undefined for xai-oauth", () => { + const minimax = getRegistryEntry("minimax"); + assert.ok(minimax?.modelsUrl); + assert.equal(deriveConfigFromRegistryModelsUrl("minimax")?.url, minimax.modelsUrl); + assert.equal(deriveConfigFromRegistryModelsUrl("xai-oauth"), undefined); + assert.equal(deriveConfigFromRegistryModelsUrl("no-such-provider-xyz"), undefined); +}); + +test("test 7: xai and xai-oauth seeds stay frozen", () => { + assert.deepEqual(modelIds("xai"), XAI_SEED_IDS); + assert.deepEqual(modelIds("xai-oauth"), XAI_OAUTH_SEED_IDS); + assert.equal(modelIds("xai").includes("grok-4.7"), false); + assert.equal(modelIds("xai-oauth").includes("grok-4.7"), false); +}); + +test("test 8: alias xao is not a discovery key", () => { + assert.equal(PROVIDER_MODELS_CONFIG["xao"], undefined); + assert.equal(getDiscoveryClass("xao"), "static-only"); +}); diff --git a/tests/unit/xai-translators.test.ts b/tests/unit/xai-translators.test.ts index b011da550c..b68d674b92 100644 --- a/tests/unit/xai-translators.test.ts +++ b/tests/unit/xai-translators.test.ts @@ -193,6 +193,51 @@ test("chatRequestToXaiResponses: maps max_tokens to max_output_tokens", () => { assert.equal(out.max_output_tokens, 512); }); +test("#12692: chatRequestToXaiResponses maps legacy assistant function_call to a function_call item", () => { + const req = { + model: "grok-4", + messages: [ + { + role: "assistant", + content: null, + function_call: { name: "get_weather", arguments: '{"city":"Paris"}' }, + }, + ], + }; + const out = chatRequestToXaiResponses(req); + const calls = (out.input as Array<{ type: string; name?: string; arguments?: string }>).filter( + (i) => i.type === "function_call" + ); + assert.equal(calls.length, 1, "expected a function_call item to be present in xAI input"); + assert.equal(calls[0]?.name, "get_weather"); + assert.equal(calls[0]?.arguments, '{"city":"Paris"}'); +}); + +test("#12692: chatRequestToXaiResponses preserves leading text alongside legacy function_call", () => { + const req = { + model: "grok-4", + messages: [ + { + role: "assistant", + content: "Let me check that for you.", + function_call: { name: "get_weather", arguments: '{"city":"Paris"}' }, + }, + ], + }; + const out = chatRequestToXaiResponses(req); + const items = out.input as Array<{ + type?: string; + role?: string; + content?: unknown; + name?: string; + }>; + const textItem = items.find((i) => i.role === "assistant"); + assert.ok(textItem, "expected the leading assistant text block to be preserved"); + const calls = items.filter((i) => i.type === "function_call"); + assert.equal(calls.length, 1); + assert.equal(calls[0]?.name, "get_weather"); +}); + // ─── xaiCompletedToChatJson ────────────────────────────────────────────────── test("xaiCompletedToChatJson: extracts output_text content into message", () => { @@ -237,6 +282,21 @@ test("xaiCompletedToChatJson: maps function_call to tool_calls with finish_reaso assert.equal(fn.name, "get_weather"); }); +test("#12700: xaiCompletedToChatJson sums legacy prompt_tokens/completion_tokens into total_tokens", () => { + const completed = { + output: [{ type: "message", content: [{ type: "output_text", text: "hi" }] }], + usage: { prompt_tokens: 10, completion_tokens: 5 }, + }; + const result = xaiCompletedToChatJson(completed) as { usage?: Record }; + assert.equal(result.usage?.prompt_tokens, 10); + assert.equal(result.usage?.completion_tokens, 5); + assert.equal( + result.usage?.total_tokens, + 15, + "total_tokens should sum legacy fields, not report 0" + ); +}); + // ─── openaiResponsesRequestToXai ───────────────────────────────────────────── test("openaiResponsesRequestToXai: drops service_tier", () => { @@ -521,3 +581,19 @@ test("xaiCompletedToGeminiJson: maps usage to usageMetadata", () => { assert.equal(meta.candidatesTokenCount, 20); assert.equal(meta.totalTokenCount, 30); }); + +test("#12700: xaiCompletedToGeminiJson sums legacy prompt_tokens/completion_tokens into totalTokenCount", () => { + const completed = { + model: "grok-4", + output: [], + usage: { prompt_tokens: 10, completion_tokens: 5 }, + }; + const result = xaiCompletedToGeminiJson(completed) as { usageMetadata?: Record }; + assert.equal(result.usageMetadata?.promptTokenCount, 10); + assert.equal(result.usageMetadata?.candidatesTokenCount, 5); + assert.equal( + result.usageMetadata?.totalTokenCount, + 15, + "totalTokenCount should sum legacy fields, not report 0" + ); +}); diff --git a/tests/unit/zai-web-missing-browser-executable-13232.test.ts b/tests/unit/zai-web-missing-browser-executable-13232.test.ts new file mode 100644 index 0000000000..8795c2e030 --- /dev/null +++ b/tests/unit/zai-web-missing-browser-executable-13232.test.ts @@ -0,0 +1,83 @@ +/** + * Regression for GitHub issue #13232 — "[BUG] Z.ai web error". + * + * The Z.ai web transport drives a real headed Chromium browser (via Playwright) to get past + * Z.ai's CAPTCHA. When the local Playwright Chromium binary is missing, + * `browserType.launch()` throws "Executable doesn't exist at ...". Before this fix, zai-web.ts + * had no classification for that failure and surfaced it as a plain 502 with no fallback hint — + * a status that trips the whole-provider circuit breaker (`AGENTS.md` → "Provider Circuit + * Breaker") as if the upstream itself were failing, instead of applying the intended + * host/config connection cooldown. This mirrors the exact failure class already handled for + * Gemini Web in #3516 (`isMissingBrowserExecutable`, now shared via + * `open-sse/executors/browserExecutableCheck.ts`). + */ +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { Buffer } from "node:buffer"; + +const mod = await import("../../open-sse/executors/zai-web.ts"); + +const TEST_TOKEN = `e30.${Buffer.from(JSON.stringify({ id: "user-123" })).toString("base64url")}.sig`; + +describe("issue #13232 — Z.ai browser transport classifies a missing Chromium install", () => { + let emptyBrowsersDir: string; + let originalBrowsersPath: string | undefined; + + before(() => { + emptyBrowsersDir = fs.mkdtempSync(path.join(os.tmpdir(), "playwright-empty-")); + originalBrowsersPath = process.env.PLAYWRIGHT_BROWSERS_PATH; + // Force chromium.launch() to genuinely fail with the exact class of error the reporter hit + // ("Executable doesn't exist at ..."), without touching any real ~/.cache/ms-playwright + // install. + process.env.PLAYWRIGHT_BROWSERS_PATH = emptyBrowsersDir; + }); + + after(() => { + if (originalBrowsersPath === undefined) { + delete process.env.PLAYWRIGHT_BROWSERS_PATH; + } else { + process.env.PLAYWRIGHT_BROWSERS_PATH = originalBrowsersPath; + } + fs.rmSync(emptyBrowsersDir, { recursive: true, force: true }); + }); + + it( + "returns a classified 503 + X-Omni-Fallback-Hint: connection_cooldown instead of a bare " + + "502 (contrast: gemini-web.ts isMissingBrowserExecutable, #3516)", + async () => { + const executor = new mod.ZaiWebExecutor(); + const body = { model: "glm-5.3-flash", messages: [{ role: "user", content: "hi" }] }; + const result = await executor.execute({ + model: "glm-5.3-flash", + body, + stream: false, + credentials: { apiKey: TEST_TOKEN }, + signal: null, + }); + + assert.ok("response" in result, "expected an error Response, not a stream result"); + const response = (result as { response: Response }).response; + const payload = (await response.json()) as { error?: { message?: string } }; + + assert.equal( + response.status, + 503, + "zai-web must classify a missing local Chromium install as a host/config error (503), " + + "not a generic retryable 502 that trips the whole-provider circuit breaker." + ); + assert.equal( + response.headers.get("X-Omni-Fallback-Hint"), + "connection_cooldown", + "the connection-cooldown hint must be set so accountFallback applies a short cooldown " + + "instead of tripping the provider circuit breaker." + ); + assert.match( + payload.error?.message ?? "", + /Playwright Chromium browser.*not installed.*npx playwright install chromium/s + ); + } + ); +});