The per-session drill-down cache now tracks decoded bytes per entry and
evicts least-recently-used entries until an aggregate maxTotalBytes
budget fits (route sets 256 MiB); an entry larger than the whole budget
is rejected. Prevents the previous worst case of 64 x 32 MiB (~2 GiB)
pinned in memory.
The fusion result's availability, partial and failure fields now reach
DescribedVideo.fusion, the guardrail meta (audioFusionRuns/Partials/
FailureCodes), the result-cache metadata and bridge stats. Audio
transcript validation moved inside the fusion's audio branch, so an
invalid audioTranscript records failures.audio and keeps the visual
description instead of failing the whole video.
* fix(providers): validate bailian-coding-plan against the Token Plan host
The catalog entry is the personal Alibaba Token Plan, but the region map still
resolved the retired Coding Plan hosts. #10290 moved only the open-sse registry
(inference) to token-plan.ap-southeast-1.maas.aliyuncs.com, leaving the dashboard's
key validation pointed at coding-intl.dashscope.aliyuncs.com.
That host rejects Token Plan keys with 401, and validateBailianCodingPlanProvider
maps 401/403 to "Invalid API key" — so adding a working key failed at the modal
while the same key served inference fine. Verified live 2026-08-18 with a valid
key: legacy host 401 invalid_api_key, Token Plan host 429 quota (auth OK).
- point both regions of ALIBABA_PROVIDER_ENDPOINTS at the Token Plan hosts,
matching what docs/providers/ALIBABA-QWEN-PROVIDER-FAMILIES.md already stated
- keep the retired hosts recognized as presets, so connections saved with the old
URL still follow the region selector instead of being pinned to a dead host
- keep image/video generation on the DashScope AIGC hosts, which the Token Plan
host does not serve
- probe with a model this plan actually serves (qwen3-coder-plus was Coding Plan)
* test(providers): compare parsed hostnames in the legacy-host guard
CodeQL flags URL .includes() checks as js/incomplete-url-substring-sanitization.
The guard is an assertion, not a sanitizer, but comparing new URL().hostname is
strictly more precise anyway — same coverage, no substring pattern.
---------
Co-authored-by: Xiangzhe <bakryun0718@proton.me>
* feat(responses): virtualize previous_response_id continuation regardless of upstream support
OmniRoute now exposes OpenAI-compatible previous_response_id/store
continuation to clients unconditionally, even when the selected upstream
provider has no native Responses-API state support. Reconstruction happens
server-side in handleChatImplementation, before any downstream validation
or provider translation: OmniRoute resolves the response id back to the
full input/output it previously produced, prepends it to the client's
delta, and forwards the full reconstructed history upstream exactly as it
does today. Client<->OmniRoute traffic shrinks to the new delta only;
OmniRoute<->provider traffic is unchanged.
Storage reuses the existing call-log pipeline artifact (already gated by
call_log_pipeline_enabled, already retained/cleaned up by the existing
call-log lifecycle) instead of duplicating conversation content into a
second store -- only a lightweight call_logs.response_id index is new.
Every lookup is scoped by api_key_id so one client can never resolve
another client's stored conversation, and any unresolvable/missing/
size-limit-omitted state fails closed with OpenAI's own
previous_response_not_found contract.
Stacked on feat/openai-responses-store-toggle (#10121).
* fix(db): re-export responsesContinuationStore from the localDb barrel
check-db-rules requires every db/ module to be re-exported (or explicitly
allowlisted as intentionally-internal) for discoverability. Missed this
when the module was first added.
* fix(db): renumber previous_response_id index migration to 154
The migration was numbered 153, but release/v3.8.50 already carries
153_radar_local_model_state.sql. The emngrating runner's collision guard
throws on two live .sql files sharing a numeric prefix, so the refreshed
merge would fail DB startup. Renumber to the next free slot (154).
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* docs(db): sync migration count to 149 across llm.txt mirrors
The responses-continuation store adds one migration, so the docs'
migration count is now 149 (was 148). Update README/AGENTS/llm.txt and
regenerate the i18n llm.txt mirrors to keep check:docs-all green.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(responses-continuation): respect preserve mode, drop dead export
- Un-export ResponsesContinuationState: it's never imported outside
responsesContinuationStore.ts, its own defining file. Fixes the
check:dead-code regression (410 > baseline 409).
- Scope the previous_response_id virtualization interception in chat.ts to
skip entirely when responsesPreviousResponseIdMode=preserve. The
interception ran unconditionally before target/connection selection,
ahead of applyResponsesPreviousResponseIdPolicy (chatCore.ts) -- the
existing per-target enforcement point for this setting -- so "preserve"
(the explicit, connection-independent contract for "let the upstream
resolve previous_response_id natively") was silently unreachable: the
field was already deleted and replaced with locally-reconstructed input
by the time that policy ran. This also broke Codex's own executor, which
relies on an untouched previous_response_id to delegate history
resolution upstream (see stripOrphanedCodexFunctionCallOutputs in
codex.ts). "auto" and "strip" modes are unaffected -- virtualization is
a strict improvement over their old "drop the field, hope the client
resent everything" behavior.
- Add a regression test exercising the actual chat.ts handler (not just
the policy helper in isolation): confirms mode=preserve now proceeds to
normal routing instead of the virtualization's previous_response_not_found
rejection, and that default/auto mode's existing virtualization behavior
is unchanged. Verified the test fails for the right reason against
pre-fix chat.ts.
Addresses PR review feedback.
---------
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
Co-authored-by: hartmark <hartmark@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(routing): fallback to default model alias seeds when unmapped in database
* fix(routing): rename seed-fallback resolver; hermetic 401 regression test
Maintainer review (PR #10124):
1. Rename resolveModelAlias -> resolveModelAliasWithSeedFallback (and
resolveModelAliasOnBody -> resolveModelAliasWithSeedFallbackOnBody) to
avoid the export collision with the sync resolveModelAlias in
open-sse/services/modelDeprecation.ts and
src/shared/constants/modelSpecs.ts.
2. Regression test now reproduces the 401: alias unmapped in the (empty,
DATA_DIR-isolated) modelAliases namespace but present in the static seed
resolves to the seed target instead of passing through unmapped.
3. Test isolates DATA_DIR (temp dir + resetDbInstance) instead of reading
the operator's live DB.
* fix(models): add outputTokenLimit to CustomModelEntry
Fixes the open-sse typecheck gate regression: catalog.ts reads
model.outputTokenLimit (for max_output_tokens in custom model metadata)
but CustomModelEntry only declared inputTokenLimit — TS2551. The
field exists in the runtime model data and is already consumed; the
interface just never declared it.
* fix(db): align compression_run_telemetry cleanup cutoff with millisecond column
cleanupCompressionRunTelemetry() computed its cutoff in epoch seconds while
insertCompressionRunTelemetryRow() stamps the timestamp column with Date.now()
(epoch milliseconds). A millisecond timestamp is ~1000x larger than a seconds
cutoff, so DELETE WHERE timestamp < cutoff never matched an old row and the
retention sweep added by #6848 to bound storage.sqlite growth was inert.
This is the same defect as domain_cost_history (#9625), whose fix corrected
cleanupDomainCostHistory() ~90 lines earlier in this file and missed this
sibling call site. The stale docstring asserting a unix-epoch column is
corrected too.
The repro test seeds through the real writer to establish the stored unit, so
it also fails if the producer format diverges from the consumer again.
* docs(changelog): add fragment for the telemetry retention unit fix
* fix(db): tolerate a SQLite build without the dbstat virtual table
getDatabaseStats() queried `dbstat` once per table with no guard. `dbstat` is
compile-time optional (ENABLE_DBSTAT_VTAB) and is absent from sql.js/WASM
builds, so on those runtimes the query throws and the error propagates out of
getDatabaseStats().
Every caller dies with it. Most visibly, GET and PATCH /api/settings/database
return HTTP 500, which makes the entire database settings page unusable — users
cannot read or change page size, cache size, or vacuum settings.
The function already anticipated missing virtual-table modules: the COUNT(*)
lookup a few lines above swallows "no such module:" errors. The dbstat query
simply sat outside that guard.
Probe dbstat once per call and skip the per-table size lookups when it is
unavailable, reporting size 0. Database-level figures (total size, page count,
cache size) come from pragmas and stay accurate; only per-table byte sizes are
lost, which is the correct trade against a hard 500.
Unrelated failures (I/O errors, corruption) still propagate.
Both spellings are handled: sql.js reports "no such module: dbstat" while
better-sqlite3 can surface "no such table: dbstat".
* test(db): cover prefixed driver errors and dbstat edge cases
Review follow-up on the previous commit.
The guard is deliberately unanchored because real drivers stringify errors
with their class name attached ("SqliteError: no such table: dbstat",
"RuntimeError: ..."). Nothing pinned that, so anchoring the regex would have
passed the suite while silently breaking every real driver. Add a case for the
prefixed form; it fails if a caret is introduced.
Also cover three shapes the fake previously could not express:
- a database with no user tables, which is what a fresh install hits first
- SUM(pgsize) returning NULL for a table occupying no pages
- dbstat answering the probe but failing on a later table, which documents
that a mid-iteration fault still propagates rather than being mistaken for
an absent module
Correct the source comment: the two error spellings track the SQLite build,
not the driver package, so the earlier attribution to better-sqlite3 was
wrong.
* docs(changelog): add fragment for the dbstat availability guard
Registers the new test with Stryker alongside the sibling db suites and adds
the changelog fragment for this fix.
---------
Co-authored-by: Nick Sullivan <nick@technick.ai>
* fix(deps): bump nanoid, dompurify for 2 new Dependabot alerts (#189, #190)
Bumps: nanoid ^3.3.17 (was transitive, now overridden), dompurify ^3.4.13
(with monaco-editor scoped override). Closes Dependabot #189, #190.
Remaining #182-#188 (js-yaml + mermaid) already closed by #9651 merge —
awaiting Dependabot re-scan.
npm audit → 0 vulnerabilities.
* fix(repo): harden .gitignore to also ignore a _tasks symlink (/_tasks)
_tasks is a SEPARATE nested git repo (gitignored). The pattern _tasks/ (trailing
slash) ignores only a directory, not a SYMLINK named _tasks. A self-referential
_tasks symlink can slip in via git add -A and, once pulled, checkout materializes
it over the real _tasks repo (destroying plans/specs/hands-off). Anchored /_tasks
ignores the symlink too, preventing re-capture.
* Hide health-check excluded models from /v1/models catalog (#10026)
Mirror the request-time exclusion rule (provider_specific_data.excludedModels)
in the unified catalog builder: a model is hidden when its provider has
connections but none of them is eligible for it. Applied across the
PROVIDER_MODELS, synced, custom, alias-backed, and managed-fallback loops
so ghost models no longer appear as available.
Co-authored-by: ritheshcn25 <ritheshcn25@users.noreply.github.com>
* fix(models): memoize getModelsDevPricing (event loop / healthz) (#10055)
* fix(models): memoize getModelsDevPricing for /v1/models catalog
resolveCatalogPricing called getModelsDevPricing once per model while
building GET /v1/models. Each call re-scanned models_dev_pricing and
JSON.parsed every row (~10k SQL scans + multi-GB parse work), pegging
the event loop so even /healthz timed out (#9685, #10052).
Memoize the parsed map until saveModelsDevPricing / clearModelsDevPricing
and add a unit test for invalidation.
Signed-off-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
* fix(db): invalidate modelsDevPricing cache on DB reset (#10055)
Copilot review fixes:
1. Register invalidateModelsDevPricingCache() with DB state reset system
so resetDbInstance() clears the process-local memo, preventing stale
pricing data from surviving across DB reset/restore operations.
2. Add test assertion verifying DB reset bypasses the memo (Copilot #10055).
The process-local memo at modelsDevSync.ts:204 caches getModelsDevPricing()
results until saveModelsDevPricing()/clearModelsDevPricing() to avoid
re-scanning all pricing rows on every /v1/models request. Without this hook,
backup restore and test DB resets would serve stale cached data from the
previous connection.
Tests: npm run test:unit:serial -- tests/unit/modelsDevSync-extended.test.ts
---------
Signed-off-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* fix(models): honor MODELS_DEV_SYNC_ENABLED=0 over dashboard settings
The file header already advertised this env var but nothing read it.
When catalog/compression pin the event loop, the dashboard (same process)
cannot turn models.dev sync off. Let 0/false/off win over sqlite so an
operator can recover with env + restart. Skip getModelsDevPricing SQL
scans while the kill switch is set.
* fix(models): restore prettier formatting after base merge
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* test(models): cover env kill switch during live settings updates
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Signed-off-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@outlook.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
Co-authored-by: ritheshcn25 <rithesh.chandran@snb.ca>
Co-authored-by: ritheshcn25 <ritheshcn25@users.noreply.github.com>
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(proxy): add non-destructive auto-disable mode for the proxy health scheduler
PROXY_AUTO_REMOVE was the only opt-in action the background proxy health
scheduler could take on a consistently failing proxy, and it deletes the row.
For a manually-maintained proxy chain (multi-proxy pool/rotation, #6365) that
is too destructive just to exclude a temporarily-dead member.
Add PROXY_AUTO_DISABLE as a sibling flag: at the same consecutive-failure
threshold it soft-disables the proxy (status "dead") instead of removing it.
"dead" is already one of the statuses the pool/rotation alive-filter excludes,
so a disabled proxy drops out of the active chain immediately with no other
code changes. The scheduler keeps probing dead proxies on its normal interval,
and the existing recovery branch (previously autoRemove-only) re-activates it
automatically once it starts answering again.
decision.ts's decideProxyHealthAction() gets an optional `autoDisable` input
(defaults to false, so existing callers are unaffected) and a "dead" status
value; scheduler.ts wires the new PROXY_AUTO_DISABLE env flag through. If both
flags are set, auto-remove wins. getProxyHealthStats() now also surfaces the
registry `status` so operators can see when a proxy was auto-disabled, and
ProxyStatusBadge now treats the full "not alive" status set (not just the
literal string "inactive") as inactive in the dashboard.
* test(proxy): assert registry status in getProxyHealthStats output
The non-destructive auto-disable change added the live registry status to the
stats object returned by getProxyHealthStats. Align the pre-existing
db-proxies-crud assertion with the intended output shape.
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(proxy): preserve auto-disabled status in dashboard edits
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
Co-authored-by: Gi99lin <Gi99lin@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(oauth): send required CLI headers in claude-auth import bootstrap call
enrichWithBootstrap() in claudeAuthImport.ts was missing the
User-Agent and anthropic-beta headers that the two other callers of
the same /api/claude_cli/bootstrap endpoint (claudeIdentity.ts and
src/lib/oauth/providers/claude.ts) always send. Without them,
Anthropic doesn't recognize the request as coming from a CLI client
and the bootstrap call fails, silently returning a null identity
(accountUUID/organizationUUID/organizationType all null).
createConnectionFromAuthFile()'s identity-verification refusal then
gets bypassed via overwriteExisting: true (the only way imports
currently succeed, since first attempts fail with
identity_unverified because of this same bug), so every imported
Claude connection ends up with unverified identity.
Downstream, resolveAccountUUID() in claudeIdentity.ts falls back to
a hash-derived fake UUID when providerSpecificData.accountUUID is
null. That fake UUID is shape-valid but was never associated with
the real account by Anthropic, so requests carrying it get
classified as unrecognized third-party traffic and routed to the
separate extra-usage pool instead of the account's plan limits --
producing an intermittent (~50% observed) 400:
"Third-party apps now draw from your extra usage, not your plan
limits." on an otherwise perfectly valid, imported subscription
token.
Fixes the header mismatch so bootstrap succeeds and imported
connections get a real, Anthropic-recognized account identity from
the start, same as connections created via the native OAuth flow.
Fixes#10143
* fix(oauth): persist cliUserID device identity on claude-auth import
createConnectionFromAuthFile() in claudeAuthImport.ts never set
providerSpecificData.cliUserID, unlike the native OAuth setup flow in
src/lib/oauth/providers/claude.ts which always mints one. cliUserID is
read by resolveCliUserID() (open-sse/executors/claudeIdentity.ts) as
the request's device_id; when absent it falls back to a lazy-random
device id regenerated fresh every process restart (in-memory Map,
process-lifetime only), so every restart of an imported connection
presents as a brand-new device to Anthropic for the same account --
a second, independent contributor (alongside Part 1's bootstrap
header fix in this same PR) to the intermittent third-party-usage 400
on valid imported subscription tokens.
- "create new connection" branch: always mint a fresh cliUserID.
- "update existing connection" branch: preserve any already-persisted
cliUserID from existing.providerSpecificData (don't rotate a working
device identity on re-import); only mint a fresh one if absent.
Adds changelog.d/fixes/10144-claude-import-cli-user-id.md per
CONTRIBUTING.md.
Fixes#10143
* test(oauth): cover claude-auth import bootstrap headers + cliUserID persistence
Adds tests/unit/claudeAuthImport-bootstrap-headers-10144.test.ts (Rule #18
regression guard for #10143):
1. enrichWithBootstrap() sends the required CLI headers on the
/api/claude_cli/bootstrap call — a claude-cli User-Agent (now sourced
from CLAUDE_CODE_CLIENT_VERSION, matching the two working call-sites)
and anthropic-beta: oauth-2025-04-20 — and still falls back to null
identity fields on non-OK upstream responses.
2. createConnectionFromAuthFile() mints a 64-hex cliUserID device
identity on create, preserves an already-persisted cliUserID on
overwrite re-import (no rotation), and mints a fresh one when the
existing connection has none.
Also aligns the hardcoded claude-cli/1.0.0 User-Agent in the import
bootstrap with the version constant the two working call-sites
(claudeIdentity.ts, oauth/providers/claude.ts) already use.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* refactor(oauth): source claude-auth import UA from canonical constant (#10144 review nit)
Addresses the hardcoded-version nit from review: the bootstrap User-Agent was
re-typed as `claude-cli/${CLAUDE_CODE_CLIENT_VERSION}` instead of importing
getClaudeCodeUserAgent() — the single source of truth the two working
call-sites (claudeIdentity.ts, oauth/providers/claude.ts) use.
- claudeAuthImport.ts: use getClaudeCodeUserAgent("cli") for the bootstrap call
- test: import the same canonical helper instead of a local copy of the pinned
version, and assert the outbound UA byte-for-byte against it, so a future
version bump can't silently desync the wire identity.
Verified: node --import tsx/esm --test on the new test file -> 5/5 pass;
sibling claudeAuthImport.test.ts -> pass; eslint on both changed files ->
no new findings (only the pre-existing @/lib/localDb barrel-import restriction
on an untouched import line).
* test(oauth): exercise claude auth import implementation
Replace copied helper tests with real implementation coverage for bootstrap headers and persistent cliUserID behavior.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: stanleytejakusuma <stanleytejakusuma@users.noreply.github.com>
* fix(antigravity): unblock Gemini and Claude reasoning capabilities
* fix(antigravity): align two unit tests with unblocked Gemini/Claude reasoning
The PR unblocks Antigravity Gemini/Claude reasoning (removed from
REASONING_UNSUPPORTED_PATTERNS, mirroring model-capabilities-registry.test.ts).
models-catalog-combo-metadata and services-branch-hardening still asserted the
pre-PR deny contract; align them to the new verified behavior. No production
code changed.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
---------
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
Co-authored-by: Chewji9875 <Chewji9875@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(api-manager): allow empty combo restrictions
Represent unrestricted Combo access explicitly as combo/* so an empty Allowed Combos list can deny every Combo without affecting direct model routes. Preserve existing keys through migration 149 and cover Dashboard, policy, routing-target, and migration behavior.
* docs: sync migration count to 149 after api-key combo-access migration
Merging release/v3.8.50 forward landed 149_api_key_combo_access.sql,
bumping the real migration count from 148 to 149. Updates README.md,
AGENTS.md, llm.txt (root + all 42 i18n mirrors, exact-copy requirement)
so the strict docs-counts-sync gate matches the live count again.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
---------
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
Co-authored-by: xz-dev <xz-dev@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(sse): exclude search providers from credential-health scheduler sweep
The credential-health scheduler's sweep() tested every active connection
every 5 minutes with no exclusion for search providers. For providers in
SEARCH_VALIDATOR_CONFIGS (tavily-search, exa-search, serper-search,
brave-search, google-pse-search, linkup-search, searchapi-search,
youcom-search), "validation" fires a real billed upstream query
(e.g. POST api.tavily.com/search), so the periodic sweep silently burned
quota with no user-initiated search.
Exclude connections whose provider id is registered in
SEARCH_VALIDATOR_CONFIGS from the sweep's connection-selection filter.
Non-search API-key/OAuth connections remain monitored (#9180, #9289
regressions verified green).
Closes#9970
* fix(docs): drop backticks around SEARCH_VALIDATOR_CONFIGS in ENVIRONMENT.md
The env/docs sync gate (check-env-doc-sync.mjs) treats any backtick-wrapped
SHOUTY_NAME as an env var reference. SEARCH_VALIDATOR_CONFIGS is a code
export, not an env var, so wrapping it in backticks made the #9970 doc note
trip the env/docs contract check (docMissingEnv). Drop the backticks so the
gate stops classifying it as an undocumented env var.
---------
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
* feat(cli): refuse ephemeral container auto-config writes
Detect containerized OmniRoute and block CLI/API config writes into
throwaway homes unless a bind mount or explicit opt-in is present, and
honor compose host-profile CLI_CONFIG_HOME mounts outside the container home.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(changelog): name fragment for #10057
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: yansigit <yansigit@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(guardrails): reroute zero-vision combos through the vision bridge
Named combos whose model targets all lack vision support are never
reroute-eligible: the bridge only attempts the describe path, and when
describing cannot run or fails the raw images stay in the payload and the
request dies in the combo capability filter with capability_mismatch.
getComboVisionBridgeDecision now returns a "no-vision" verdict for combos
with zero vision-capable targets, and preCall treats it as reroute-eligible
with the same credential guards as single text-only models, falling back to
describe only when no usable reroute target exists.
* chore(changelog): fragment for #10415 vision bridge combo reroute
* fix(guardrails): extend allNull stub fallback to no-vision combos
Reviewer follow-up (#10415): the allNull stub-text fallback at the end of
preCall only fired for comboVisionBridgeDecision === 'process'. In the
compound-failure case for a zero-vision combo — reroute target without
usable credentials AND every describe call failing — raw images were
preserved and the original capability_mismatch recurred, because a
no-vision combo has no target that can consume images.
Include 'no-vision' in the guard: stub text is strictly better than raw
bytes no combo target can consume. Adds a double-failure unit test.
* ci: re-run dast-smoke (Build CLI bundle runner timeout flake)
* fix(build): bound and retry the opencode-plugin npm install in prepublish
The plugin's node_modules is gitignored, so every fresh CI checkout runs a
full npm install inside @omniroute/opencode-plugin during build:cli. npm's
unbounded fetch retries turn a stalled registry CDN connection (the recurring
onnxruntime-class ETIMEDOUT flake) into a 20-30 minute hang — the DAST
'Build CLI bundle' step has been cancelled at the 30m cap repeatedly.
- Bound npm fetch: --fetch-timeout 60s, 2 retries with capped backoff — a
stalled connection now fails fast instead of hanging the job.
- Retry the install up to 3 times with a 10s pause between attempts, so
transient CDN failures recover in-build.
Net effect: the step either completes (network OK) or fails quickly with a
clear error (network down) — it can no longer eat the whole job budget.
* ci(dast): use existing npm-ci-retry action instead of bare npm ci
dast-smoke died at 'Run npm ci' with connect ETIMEDOUT to the
onnxruntime-node binary CDN (Microsoft 150.171.x.x) — the same
transient CDN flake class that has hit Vitest/Quality Gates before.
quality.yml already wraps npm ci in ./.github/actions/npm-ci-retry
(3 attempts, exponential backoff); dast-smoke was the one workflow
still using a bare install. Use the existing action for consistency.
* ci(quality): use the npm-ci-retry action on every install step
Fast Quality Gates failed on the recurring onnxruntime-node postinstall
ETIMEDOUT (Microsoft CDN 150.171.x.x) - the same transient flake that has
hit Vitest and dast-smoke today. Only the Build job used the retry action;
the other five jobs (Docs, Fast Quality Gates, Vitest, Unit Tests,
changelog) still ran a bare install and die on any CDN hiccup. Use the
existing retry action (3 attempts, exponential backoff) on every install
step for consistency.
---------
Co-authored-by: Rouzbeh <rqzbeh@users.noreply.github.com>
* fix(db): default debugMode to false in getSettings() defaults
Fresh installs (or installs missing the persisted debugMode key) ran in
debug mode, contradicting the documented opt-in toggle and flooding new
production installs with debug-level logs. Flip the default to false;
installs that persisted debugMode=true keep it — only the missing-key
path changes, no migration needed.
Fixes#10312
* changelog: fragment for #10372
* fix(monitoring): canonicalize provider aliases in health matrix
* fix(monitoring): canonicalize aliases in health autopilot
---------
Co-authored-by: tkgo11 <7.1800574e+07+tkgo11@users.noreply.github.com>
sql.js has no incremental write path, so persist() rewrites the whole image on
every save. Going through fs.writeFileSync(filePath, ...) opened the destination
with O_TRUNC, leaving the on-disk database 0 bytes and then partial for the whole
write -- a window that scales with database size and recurs on every save.
Unlike better-sqlite3 / node:sqlite, that window is not covered by SQLite's
locking protocol, so it is visible to every other process reading the same file:
a backup job, a metrics exporter, an operator running sqlite3. Those readers get
SQLITE_CORRUPT ("database disk image is malformed") while PRAGMA
integrity_check passes moments later, which makes the failure look random and
blames the reader.
Now: temp file in the same directory, fsync, rename() over the destination.
rename is atomic on POSIX and on Windows for a same-volume replace, so a reader
sees either the previous image or the new one, never a truncated one. It also
closes a total-loss window: a crash mid-write used to leave the real database
truncated, and now only leaves a stale temp file behind.
The regression guard asserts the property that separates the two implementations
without racing a timer: a reader that opened the file before a save still reads a
complete, valid image afterwards, and the published file sits on a new inode.
It fails on the previous implementation and passes on this one.
Co-authored-by: Max <maxmad64@gmail.com>
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
Present xAI API-key and OAuth connections through one dashboard card while preserving the distinct backend IDs required for refresh and quota handling.
Co-locate both registry entries and include canonical and legacy connection IDs in provider fetch and batch-test flows.
* feat(providers): add local ZCode ACP backend
* test(snapshots): regenerate translate-path golden for zcode provider
The new local ZCode ACP backend (zcode://app-server/stdio) was added to the
provider catalog but the translate-path golden snapshot was not regenerated,
so the combined suite (provider-translate-path-golden.test.ts) failed on the
merged tip. Regenerate the snapshot to include the zcode translate-path entry.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* docs(env): document ZCODE_* vars for the local zcode provider
Registers the 11 ZCODE_* env vars read by the zcode executor (.env.example
+ docs/reference/ENVIRONMENT.md) so the env-doc-sync gate stays green.
Co-authored-by: Diego Souza <8016841+diegosouzapw@users.noreply.github.com>
* test(autoCombo): include zcode in the glm-family provider set
#10184's local zcode backend advertises the full GLM_SHARED_MODELS
line-up (registry/zcode, authType none) — same documented case as auggie
and devin-cli-agentic. Update auto/glm provider-set assertion to include
it.
Co-authored-by: Diego Souza <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: roomhacker <roomhacker@bezrabotnyi.com>
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
resolvePortPid shelled out to lsof alone. On a host without it, spawn
raises ENOENT, the error handler turned that into null, and the caller
could not tell 'nothing holds this port' from 'I have no way to look' -
so a service adopted on a supervisor restart kept pid: null forever,
silently, which is the regression the adopt-branch test guards against.
Probes lsof, then ss, then netstat, sharing one deadline so the whole
lookup still costs at most PID_RESOLVE_TIMEOUT_MS. Output parsing for
each is a pure exported function so the formats are unit-testable
without the binary being installed.
netstat cannot filter by port, so its parser matches the local-address
column rather than scanning the line, keeping a foreign address that
ends in the same number from being read as a listener.
Implements the secure, opt-in Video Bridge for issue #9760, including bounded FFmpeg frame extraction, capability-aware routing, telemetry, settings UI, localization, documentation, and regression coverage.
The packaged artifact stamped dist/BUILD_SHA but nothing verified the SHA belonged to the release line, so a tarball built from a feature branch installed and served traffic indistinguishably from a release build. That is how the internal gateway ended up running a build that predated #10373 and answered every request with 502 'Executor result must contain a Response' — identifying it required SSH plus grepping the compiled chunks.
scripts/build/buildProvenance.ts classifies a build SHA against the release ref (pure functions, injected git probe). A missing SHA fails even with the canary override: an unidentifiable artifact cannot be vouched for. validate-pack-artifact enforces it on real packs (skipped under --policy-only, which runs without a build); OMNIROUTE_ALLOW_CANARY_BUILD=1 records a deliberate off-release-line build instead of failing it. /api/monitoring/health now exposes system.buildSha — absent when unknown, never fabricated.
Closes#10427
Any process that opened the DB without setting DATA_DIR resolved to ~/.omniroute/storage.sqlite — the operator's live database, provider credentials included. tests/_setup/isolateDataDir.ts only covers the npm scripts; the documented single-file test command and ad-hoc probes bypassed it (one did exactly that during #10334).
resolveWritableDataDir now redirects a test-context process with no DATA_DIR to a throwaway temp dir, stable per process. Redirect rather than throw, so the documented single-file command keeps working; OMNIROUTE_ALLOW_DEFAULT_DATA_DIR=1 opts back in and records the intent.
Closes#10428
* fix(db): prune pre-migration backups so db_backups stops growing unbounded
createPreMigrationBackup() wrote a VACUUM INTO snapshot on every migration run
and never pruned. On a long-lived instance db_backups/ reached 48.999 files /
204 GB against a 5,3 MB live database; a second devbox showed the same shape
(5.711 files / 24 GB).
The retention policy already existed in cleanupDbBackups() but nothing on the
migration path reached it — its only callers are backup.ts and the
/api/db-backups route, neither of which runs during a migration.
migrationRunner.ts cannot import backup.ts: core.ts imports migrationRunner.ts
and backup.ts imports core.ts, so that edge would close a cycle. The policy
therefore moves to a new core-free module, backupRetention.ts, which both call
sites share — cleanupDbBackups() now delegates to it rather than duplicating it.
At the migration call site the operator's maxFiles/retentionDays are read
through the adapter already open for the run; going through getDbInstance()
would re-enter database initialization. Pruning never throws, so housekeeping
cannot fail a migration.
Closes#10421
* chore(db): declare backupRetention as an intentionally-internal db module
check:db-rules requires every src/lib/db/ module to be either re-exported by
localDb.ts or listed in INTENTIONALLY_INTERNAL. backupRetention.ts is a shared
primitive consumed only by db/backup.ts and db/migrationRunner.ts — the same
category as the migrationRunner entry — so it belongs in the allowlist rather
than in the public re-export surface.
* test(db): include backupRetention in the audited INTENTIONALLY_INTERNAL list
check-db-rules-classification.test.ts freezes the exact membership of
INTENTIONALLY_INTERNAL, so adding the 40th entry has to be reflected there too
— the gate script and this test pin the same contract from opposite sides.
---------
Co-authored-by: Xiangzhe <bakryun0718@proton.me>
* fix(sse): surface Qwen/Alibaba personal Token Plan quota in dashboard and preflight
The personal Token Plan (5-hour / 7-day sliding windows) has no official
OpenAPI and the inference API key cannot read it. Add a cookie-authenticated
fetcher for the console gateway shared by home.qwencloud.com and the Model
Studio console (contract captured live from a logged-in session):
- open-sse/services/qwenTokenPlanQuotaFetcher.ts: POST /data/api.json
(IntlBroadScopeAspnGateway / sfm_bailian) for usage + quota-config +
subscription; sec_token resolved best-effort from the dashboard HTML;
per-window parse (fields are omitted while a window is Temporarily
Removed); 60s usage cache, 1h tier cache.
- usage/qwen-token-plan.ts leaf + registration in the usage dispatcher,
USAGE_FETCHER_PROVIDERS, USAGE_SUPPORTED_PROVIDERS,
PROVIDER_LIMITS_APIKEY_PROVIDERS and bespoke preflight/monitor windows.
- Also adds bailian-coding-plan to USAGE_SUPPORTED_PROVIDERS /
PROVIDER_LIMITS_APIKEY_PROVIDERS: the coding-plan fetcher existed but the
dashboard filtered those connections out (UI gap).
Refs #9603 (Problema 1 — quota missing; the 429 recovery half is a
follow-up).
* docs(env): document Qwen Token Plan quota env vars + regen omni-settings skill
QWEN_CLOUD_COOKIE, QWEN_CLOUD_SEC_TOKEN, QWEN_TOKEN_PLAN_HOST and
QWEN_TOKEN_PLAN_DASHBOARD_URL added to .env.example and
docs/reference/ENVIRONMENT.md (check:env-doc-sync), with the generated
omni-settings skill refreshed (check:agent-skills-sync).
Refs #9603
* revert: keep hand-tuned omni-settings thinking-budget section
The agent-skills-sync drift predates this PR (hand improvement from #10169
not yet synced into the generator source) — it fails on every open PR and
belongs to a base-reds fix, not this branch. Regenerating here would erase
the intentional content.
* feat(dashboard): add the Qwen/Model Studio console cookie field to the connection modal
The Token Plan quota fetcher is cookie-authenticated (the inference API key
cannot read the console gateway), but no modal field existed to paste that
cookie — so the quota was unconfigurable from the dashboard and the fetcher
could only ever return its 'needs a cookie' message.
Adds the field for qwen-cloud-token-plan and bailian-coding-plan alongside the
existing ollama-cloud / alibaba console-cookie inputs (same password-input,
blank-keeps-stored semantics), pre-fills it when editing a connection, and
extends the providerSpecificData string/length validation to the two new keys.
Tests: tests/unit/qwen-token-plan-cookie-field.test.ts (RED before, GREEN
after) covers persistence + trimming, the blank-input no-overwrite rule and
schema acceptance/rejection.
Refs #9603
* docs(dashboard): correct the Qwen console cookie instructions
The placeholder claimed the cookie looks like 'token=...'; the qwencloud
portal actually issues 'login_qwencloud_ticket=...' alongside cna/cnaui/aui
(mirroring login_aliyunid_ticket on the Alibaba console), so the hint pointed
at the wrong value.
Replaces the guesswork with the verified retrieval steps in all three places
an operator can hit — the modal field hint, the fetcher's 'needs a cookie'
message and .env.example/ENVIRONMENT.md: log in to home.qwencloud.com >
Billing > Subscription, F12 > Network, reload, filter by api.json, click a
request to cs-data.qwencloud.com and copy the WHOLE Cookie request header.
Also documents that the value must go on one line (it contains '=' and ';')
and that it dies with the browser session.
Refs #9603
* fix(dashboard): tolerate partial form objects in the qwen cookie branch
Adding bailian-coding-plan to QWEN_TOKEN_PLAN_PROVIDERS routed callers that
previously matched NO branch in assignQuotaScrapingProviderData into the new
one, which assumed the two new fields are always present. Older callers build
a partial form object, so buildAddProviderSpecificData threw:
TypeError: Cannot read properties of undefined (reading 'trim')
(tests/unit/dashboard/agentrouter-connection-modal-fields.test.ts)
Reads the new fields with optional chaining and adds a regression test that
calls the helper with those keys deleted for both providers.
Refs #9603
* refactor(dashboard): move quota-scraping form logic into a UI-free module
tests/unit/qwen-token-plan-cookie-field.test.ts imported QuotaScrapingFields
directly, which pulls `@/shared/components` and, through that barrel,
untranspiled ESM (@lobehub/icons). The node:test runner cannot parse it and
the whole test file died in CI with:
SyntaxError: Unexpected token 'export'
at @lobehub/icons/es/Ai21/components/Mono.js
(It passed locally, so only the CI shard surfaced it.)
Extracts the pure pieces — QWEN_TOKEN_PLAN_PROVIDERS, QuotaScrapingFieldValues,
EMPTY_QUOTA_SCRAPING_FIELDS and assignQuotaScrapingProviderData — into
quotaScrapingFieldValues.ts. The component imports them and re-exports the
public names, so every existing importer keeps its current path. The unit test
now targets the UI-free module.
Refs #9603
* fix(providers): point bailian-coding-plan at the Token Plan endpoint and its console
Two independent defects kept this provider unusable with a valid Alibaba
Token Plan key (verified live 2026-08-14 with the owner's key and cookie):
1. Wrong inference host. The catalog entry is named "Alibaba Token Plan",
links to token-plan-overview and its hint asks for a Token Plan key, but
the registry pointed at coding-intl.dashscope.aliyuncs.com — the Coding
Plan host, which rejects Token Plan keys with 401 invalid_api_key. The
documented Anthropic base URL for Token Plan is
token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic
(https://www.alibabacloud.com/help/en/model-studio/more-tools). Against
the new host the same key returns 200 for all six registry models and a
real completion; auth stays on x-api-key.
2. Wrong console identity for quota. The personal Token Plan is sold through
two consoles sharing one backend, and the gateway validates the session
against the console declared in the request: an Alibaba console cookie
(login_aliyunid_ticket) sent with the QwenCloud identity is refused with
BailianGateway.Login.NotLogined. resolveConsoleSite() now picks host,
cornerstoneParam.consoleSite/domain and Origin/Referer from the cookie's
login ticket, falling back to the provider. With that switch the same
cookie returns usage/subscription/quota-config.
Also routes bailian-coding-plan quota through the Token Plan fetcher (the
Coding Plan call returns "Bad Request" for these accounts), keeping the old
fetcher as the fallback for real Coding Plan keys, and labels the plan by
console ("Alibaba Token Plan (Pro)" vs "Qwen …").
Live validation: inference 200 (qwen3.7-plus answered "FUNCIONA"); quota
12,934/40,000 credits, 67.7% remaining, resets 2026-08-20.
Refs #9603
---------
Co-authored-by: Xiangzhe <bakryun0718@proton.me>
* test(bridge): explicit native-vision skip guard + skip log
* feat(bridge): configurable describe output cap (modalityBridgeVisionMaxChars)
* feat(dashboard): maxChars field on Modality Bridge vision tab
Add the "Max description characters" field to the Vision tab's Advanced
panel (modalityBridgeVisionMaxChars, clamped to the 100-50000 schema
range with 0 treated as the explicit "unlimited" sentinel), wire the
en.json copy and sync it across all 42 locales, and document the new
setting in GUARDRAILS.md.
* fix(bridge): allow explicit 0 to disable the describe cap
updateSettingsSchema previously rejected modalityBridgeVisionMaxChars: 0
because the field's range was min(100).max(50000), so a dashboard PATCH
sending the explicit "unlimited" sentinel would 400. Widen the schema to
z.union([z.literal(0), z.number().int().min(100).max(50000)]) so 0
validates as its own valid value, not just an implicit default.
* chore(i18n): resync locale keys after release merge
* chore(quality): rebaseline deadExports for the OCR/image-to-text series
---------
Co-authored-by: Xiangzhe <bakryun0718@proton.me>