* Sanitize test fixtures, add developer .env guidance, and add gitleaks workflow
- Replace realistic-looking AWS keys and PEM fixtures in unit tests with synthetic placeholders to avoid false positives from secret scanners.
- Add docs/DEVELOPER-ENVIRONMENT.md describing postinstall .env behavior and remediation guidance.
- Add .github/workflows/gitleaks.yml to run gitleaks on pull requests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add gitleaks baseline and CI baseline support; update ignore and PR body\n\n- Copy gitleaks-local.json -> gitleaks-baseline.json\n- Add --baseline-path to workflow\n- Allowlist baseline in .gitleaks.toml\n- Ignore gitleaks-local.json\n- Add PR_BODY.md with scan summary\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore(security): fix gitleaks config, drop redundant baseline/CI, clean doc artifacts
- Fix the malformed .gitleaks.toml [[rules]] block: an inline [rules.allowlist]
with only paths (no regex/path at rule level) made gitleaks refuse to load the
config (`FTL Failed to load config ... both |regex| and |path| are empty`),
turning the project's blocking check-secrets ratchet into a hard failure.
Verified: check-secrets config now loads and exits 0.
- Reconcile with the existing gitleaks gate: remove the redundant
.github/workflows/gitleaks.yml and root gitleaks-baseline.json (a second,
differently-scoped scanning mechanism + an unreviewed 430-finding blanket
baseline) — the project already runs scripts/check/check-secrets.mjs as a
blocking ratchet in ci.yml/quality.yml and its .gitleaks.toml policy is to fix
real findings, not blanket-allowlist them.
- Remove the stray PR_BODY.md automation artifact from the repo root.
- Fix the duplicated <div align="center"> tag in README.md.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: OmniRoute Bot <noreply@omniroute.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: blarovse <312250233+blarovse@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* 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(sse): keep Codex quota headers under the forwarding budget
The 768-byte cap plus priority-3 for any name that does not contain
"ratelimit" dropped x-codex-*-used-percent / reset / credits on every
stream. x-codex-turn-state (314 bytes) ate the budget. Raise the cap,
treat Codex quota headers as rate-limit priority, and do not forward
turn-state.
---------
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>
* 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(providers): remove invalid CodeBuddy CN glm-4.7 and add hy3 (0.0x credit)
Swap the GLM-4.7 model for the Hunyuan hy3 model in the codebuddy-cn
registry, keep the catalog at 15 models, and update the matching provider
test expectations. Regenerated the auto-generated provider reference doc.
---------
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 <25951435+RaviTharuma@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: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* 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>
`@huggingface/transformers` 4.2.0 hard-pins `onnxruntime-node` to "1.24.3".
The production-group bump in #10403 raised the root range from "~1.24.3" to
"~1.27.0", so npm stopped deduping and nested a second copy under
`node_modules/@huggingface/transformers/node_modules/onnxruntime-node`.
Both copies ship a native `libonnxruntime.so.1` under the SAME SONAME, so
glibc binds whichever is dlopen()ed first and the other addon dies. The
Dockerfile post-build verification imports `@huggingface/transformers` and
`onnxruntime-node` in one process, so `docker build` has failed on every
commit since #10403:
Error: .../transformers/node_modules/onnxruntime-node/bin/napi-v6/linux/x64/libonnxruntime.so.1:
version `VERS_1.27.0' not found (required by .../onnxruntime-node/bin/napi-v6/linux/x64/onnxruntime_binding.node)
Restore the root range to "~1.24.3" so a single hoisted copy is resolved
again. Copying the nested native binaries into the standalone bundle is NOT
a workaround: it makes both `.so` files present, which is precisely what
triggers the SONAME clash above (verified against a real image build).
Regression guard: tests/unit/onnxruntime-single-copy.test.ts asserts the
lockfile resolves exactly one onnxruntime-node and that it matches the
version transformers pins. Confirmed failing on the pre-fix lockfile
(two copies, 1.27.0 vs 1.24.3) and passing after.
Validated with a full `docker build --target runner-base`: the post-build
verification step now passes (#19 DONE 156.9s) and the image boots healthy
(/api/monitoring/health 200, migrations 134-148 applied).
Keep the #8350 Hermes system-prompt drops, but remove hermes from the
factory obfuscate_words list so hostnames and CLI mentions stay intact.
Co-authored-by: Ravi Tharuma <RaviTharuma@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(providers): add PATCH handler to provider connection route
The OpenAPI spec and the CLI (omniroute providers rotate, generated
api-commands) both use PATCH /api/providers/[id], but the route only
implemented PUT — PATCH requests returned 405 and key rotation via the
CLI silently failed while reporting success (the DB-write fallback only
catches thrown exceptions, not non-OK HTTP responses).
Add a PATCH handler delegating to the PUT handler: both apply the same
partial-update schema, so the semantics are identical.
Regression test proves the PATCH export exists and delegates into the
shared auth path; verified to fail without the fix.
* docs(changelog): note PATCH provider route fix (PR #10366)
* fix(providers): make PATCH delegation test environment-robust
The 'PATCH delegates to PUT' assertion hardcoded a 401, which only holds
when management auth is enforced (dev). In the CI unit-test env auth is not
required, so the flow falls through to 'Connection not found' (404) for an
unknown id — the test failed on the status code while the PATCH->PUT
delegation itself is correct. Assert on delegation equivalence instead:
PATCH must never 405 (the regression) and must return the same status as
PUT for the same input.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* test(providers): use fresh Request per handler in PATCH delegation test
The same Request was passed to both PATCH and PUT — PUT consumes the
body via request.json(), so the second call got an empty body (400
validation) vs the first (404 not-found): a false status mismatch on
bases where management auth is bypassed in the test env (release
v3.8.50). Fresh Request per invocation makes identical inputs produce
identical statuses.
---------
Co-authored-by: benzntech <benzntech@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(mcp): dynamically generate web search provider enum from registry
* test(mcp): add contract test for dynamic web search provider enum
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(mcp): restore search.ts eslint suppression, type builder maps, fix firecrawl searchType arg
The enum-dynamic refactor dropped search.ts's no-explicit-any suppression
while a new Record<string,any> map re-introduced anys, and the response
normalizer map swapped the firecrawl searchType argument with query. Type
both maps explicitly, restore the base suppression (33 pre-existing anys),
and pass searchType (not query) to normalizeFirecrawlSearchResponse.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
Co-authored-by: sadSanta-07 <sadSanta-07@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>
Issue #10443: when the upstream kills an SSE stream mid-generation
(antigravity/Gemini does this under its own rate enforcement), OmniRoute
closed the stream silently for OpenAI-format clients - HTTP 200, a few
content chunks, no finish_reason. The client sees a truncated turn.
resolveSilentCloseReason() only flagged that shape for Claude clients
(#7699). Extend it to OpenAI chat completions guarded on sawContent(),
and teach hasClientTerminalSseMarker() that a non-null finish_reason
chunk is a terminal marker (some providers omit data: [DONE]). Every
known OpenAI-producing path ends with one of the two, so content
forwarded without either is an upstream drop and now surfaces the
in-band 502 error chunk + [DONE] instead of a silent close.
TDD: tests/unit/silent-sse-close-openai-10443.test.ts - core case RED
before / GREEN after, plus guard cases for finish_reason-only close,
[DONE] close, empty-content (#8649 verdict preserved), and literal
finish_reason text inside model content (JSON escaping keeps the raw
bytes from matching the unescaped-field regex).
Signed-off-by: Minxi Hou <houminxi@gmail.com>
The base gemini-3.5-flash entry spread the shared GEMINI_35_FLASH_MODEL_SPEC
constant, which has supportsThinking:false because it is also spread into
several Antigravity flash-tier aliases that reject client-supplied thinking
params. That made the reasoning-routing policy resolve reasoning_effort as
"unsupported" for the base Google AI Studio model, producing a spurious
pre-provider HTTP 400 even though the model supports reasoning (it has an
effort-tier alias gemini-3.5-flash-high).
Set supportsThinking:true as an explicit override on the base
gemini-3.5-flash entry only, leaving the shared spec and the Antigravity
tier aliases unchanged.
Closes#10286
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.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(dashboard): add VS Code Copilot Chat home banner, remove Provider Quota home card
Announce the OmniCopilot extension right below the Kimi sponsor banner on the
dashboard home page (same size/shape, dismissible, no version gate). Also
removes the "pin Provider Quota to home" card and its now-dead settings
toggle — the widget itself, its auto-refresh setting (shared with the
standalone /dashboard/quota page), and its component tests are untouched.
* fix(dashboard): remove now-dead homeWidgets.ts (dead-code gate)
Deleting the AppearanceTab pin-to-home toggle left this file's sole export,
PIN_PROVIDER_QUOTA_TO_HOME_KEY, with zero remaining consumers, which regressed
the dead-code ratchet from 415 to 416. Removing the file restores the exact
baseline count (415).
---------
Co-authored-by: Xiangzhe <bakryun0718@proton.me>
Stage 7 of issue #10321 moves the optional ML and browser automation dependency closures out of the desktop bundle into checksummed, versioned packs installed on demand through the omniroute packs command.
- scripts/build/optionalPackStaging.mjs stages pack members under .build/optional-packs, creates release tarballs, and emits optional-packs.index.json with per-member SHA-256 checksums.
- scripts/packs provides manifest, install, remove, and verification helpers plus the packs CLI commands.
- Runtime lookup includes installed pack node_modules directories, while LLMLingua and browser executors continue to degrade gracefully when packs are absent.
The measured darwin-arm64 staging closure was about 534 MB of the 929 MB standalone node_modules tree (57%).
better-sqlite3 v13 ships Node-API prebuilds for every packaged platform
(darwin/linux/linuxmusl/win32 x x64/arm64) inside the npm tarball, so the
Electron-ABI node-gyp source rebuild in prepare-electron-standalone.mjs is
obsolete. Replace it with a fail-fast prebuild verification that mirrors
better-sqlite3 lib/binding.js selection, and strip build/deps/src so the
packaged loader can only resolve the prebuild.
Verified locally on darwin-arm64: the same darwin-arm64.node prebuild loads
under both Node 24 (NODE_MODULE_VERSION 137) and Electron 43.3.0 under
ELECTRON_RUN_AS_NODE (148); DB create/migrate/read/write/close/reopen pass
in both runtimes and cross-runtime on each other's database files.
Issue #10321 Stage 6.
* fix(chat-body-admission): process-wide budget (#10110)
Remove per-session admission lanes that multiplied the documented
"in one process" heavy/bytes bound by up to 64. All requests now admit
against ONE process-global ChatAdmissionController so the bound holds
against fake-credential sharding.
Per-request session identity survives only as a fairness scheduling key:
waiters are grouped per key and served round-robin (#9654) against the
shared budget — one connection's burst cannot starve others.
- src/shared/middleware/chatBodyAdmission.ts: delete lane map + LRU/TTL
eviction; ChatAdmissionController is now the global budget with per-key
FIFO queues + round-robin dispatchFair(). PerConnectionAdmissionController
returns the same shared controller for every session. resolveSessionId
stays as a scheduling key with honest re-scoping docs. snapshot() emits
process-wide aggregates.
- tests/unit/chat-body-admission-aggregate-10110.test.ts: new U6 suite — 6
deterministic tests (LRU-no-mint, TTL-no-mint, shared byte budget,
16 MiB config, same-session recreation, round-robin fairness). RED on
release/v3.8.50, GREEN post-fix.
- tests/unit/per-connection-admission-9654.test.ts: rewrite the tests that
encoded the defect (per-session isolation) to assert the global-budget
contract.
- docs/reference/ENVIRONMENT.md: OMNIROUTE_CHAT_ADMISSION_MAX_QUEUED_BYTES
documented as process-wide; VIRTUAL_TTL_MS/VIRTUAL_MAX_SESSIONS deprecated.
* docs(changelog): add #10322 fragment for process-wide admission budget
* ci: retrigger checks after transient npm ci network failure in shard 3/4 (ETIMEDOUT)
---------
Co-authored-by: Brandon Bennett <brandonbennett@macbookair.myfiosgateway.com>
The desktop release matrix ran the full Next.js standalone build on all four legs (windows, macos-intel, macos-arm64, linux), duplicating the platform-neutral majority of that work four times and re-exposing every leg to the hosted-runner RAM class of failure that took the linux leg out of v3.8.49.
- scripts/build/standaloneTarball.mjs: deterministic, dependency-free tar.gz writer/reader (uid/gid/mtime pinned, sorted entries, symlink + exec-bit preservation; GNU-tar interop covered by tests).
- scripts/build/standaloneManifest.mjs: byte-level manifest of .build/next (sha256 + size + symlink target per entry, plus the archive's own digest) catching artifact-transfer corruption before extraction and re-verifying the restored tree byte-for-byte, smuggling included.
- scripts/build/standaloneBundle.mjs: pack / restore / hydrate CLI over the two modules above.
- scripts/build/hydrateNativeDeps.mjs: swaps install-machine-forked native optionals (@img/sharp-*, @ngrok/ngrok-*, fsevents) from the leg's own npm ci into the restored tree, then verifies the bundled-native closure (koffi triplets, better-sqlite3 prebuilds, wreq-js, onnxruntime with its documented darwin-x64 exemption) services the leg's platform/arch before packaging starts.
- .github/workflows/electron-release.yml: new web-build job builds the standalone once on ubuntu with webpack and uploads the bundle; legs download, restore, and hydrate it, skipping the per-leg build. The legacy per-leg build remains as a rollback path via the ELECTRON_SHARED_STANDALONE workflow_dispatch input, and legs fail closed if web-build ran and failed.
Regression tests cover archive roundtrip, byte determinism, manifest tamper/smuggle detection, forked-native swaps, and native-closure serviceability.
* fix(security): sanitize test regex and annotate CodeQL hash false-positives
tests/unit/early-sse-route-intent.test.ts built a RegExp from a hardcoded
string but only escaped `?`/`.`, missing `\` — js/incomplete-sanitization
(#816). Not exploitable (fixed literal input) but the escaping was
genuinely incomplete; now escapes backslash too.
reasoningCache.ts::buildAssistantMessageCacheKey and codexIdentity.ts's two
UUID derivation helpers hash a cache-scope/account-seed with SHA-256 to
produce a lookup key / deterministic ID — not a stored, verified password.
CodeQL's js/insufficient-password-hash overfires on any hash of a
secret-like variable, the same false-positive class already annotated at
src/lib/db/apiKeys.ts:624. Added matching lgtm/nosemgrep annotations and
inline rationale so the intent is clear to reviewers and future scans.
Refs #815#816#817#818
* fix(security): keep only the regex sanitization; drop non-functional CodeQL annotations
The lgtm[]/nosemgrep: comments in codexIdentity.ts and reasoningCache.ts use
formats GitHub Actions CodeQL does not honor, and shifting those sha256 lines
re-attributed the already-dismissed base alerts to this PR as two new CodeQL
findings. Revert those two annotation-only files to base so the existing
dismissals apply; retain the real fix (escaping backslash in the test regex),
which resolves the open js/incomplete-sanitization alert.
---------
Co-authored-by: Xiangzhe <bakryun0718@proton.me>
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
* 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(ops): Docker HEALTHCHECK probes /healthz not deep monitoring
/api/monitoring/health does a SQLite ping and more. When the event loop
is busy the official image HEALTHCHECK (5s timeout) marks the container
Unhealthy and orchestrators restart the only replica mid-session.
* fix(ops): keep healthcheck PR scoped to the /healthz probe
Drop the stray catalog ghost-model exclusion that leaked into this branch
from main (already covered upstream). Restore catalog.ts to the release
version so the PR contains only the Docker HEALTHCHECK /healthz fix, its
tests, and the changelog entry.
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>
* fix(providers): point freeaiapikey at the api. host it moved to
Every /v1 route on the freeaiapikey.com apex host answers HTTP 410 with
type "endpoint_moved", and the body names its own replacement:
"This API endpoint has moved. Please update your base_url to
https://api.freeaiapikey.com/v1 - the old endpoint on freeaiapikey.com
no longer works."
Probed 2026-08-13 with paired controls so a network fault could not be
read as an upstream verdict:
GET https://freeaiapikey.com/v1/models -> 410
GET https://freeaiapikey.com/v1/chat/completions -> 410
GET https://api.freeaiapikey.com/v1/models -> 200
GET https://api.freeaiapikey.com/v1/chat/completions -> 405 (POST-only)
GET https://api.openai.com/v1/models -> 401 (control: reachable)
GET https://<nonexistent-domain>/v1/models -> 000 (control: unreachable)
Every request through this provider therefore fails today. Repoint baseUrl
and modelsUrl at the host upstream names.
* fix(providers): resync the freeaiapikey catalog with its live model list
GET https://api.freeaiapikey.com/v1/models (200, probed 2026-08-13) serves 10
models. The registry declared 7, four of which upstream does not serve at all:
openai/gpt-5, openai/gpt-5.2-codex, Alibaba/qwen3.5, Alibaba/qwen3-vl:235b.
Seven live models were missing: openai/gpt-5.4, openai/gpt-5.5,
openai/gpt-5.6-sol, anthropic/claude-opus-4.7, anthropic/claude-opus-4.8,
anthropic/claude-sonnet-5, anthropic/claude-opus-5.
The four phantom ids are selectable in the dashboard and can only ever fail
upstream; the seven real ones are unreachable through the static catalog.
On context windows: the /v1/models response carries only id/object/created/
owned_by, so upstream publishes no window at all. The models added here
therefore declare no contextLength and inherit the entry's existing
defaultContextLength (128000) instead of a fabricated number. The two
pre-existing contextLength values are left untouched for the same reason -
this sweep neither confirms nor refutes them, and rewriting them would be
guesswork in the other direction.
* chore(changelog): name the fragment after the real PR number
* chore(changelog): substitute the PRNUM placeholder in the fragment body
---------
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
* fix(account-fallback): classify 'insufficient credits' as credits-exhausted
Command Code returns 400 'You have insufficient credits to make this
request...' when an account's billing credits run out. The phrase was
missing from CREDITS_EXHAUSTED_SIGNALS, so the error stayed unclassified
(errorType=null) and the connection was never marked credits_exhausted —
getProviderCredentials kept re-selecting the same dead account on every
request instead of rotating to a healthy one.
Add 'insufficient credits'/'insufficient credit' to the signal list
(already used by antigravity429Engine.ts) so the error classifies as
QUOTA_EXHAUSTED and the account is skipped on subsequent selections.
* fix(account-fallback): harden insufficient-credit matching and preserve chatanywhere
Add the common 'insufficient credit balance' variation to
CREDITS_EXHAUSTED_SIGNALS alongside the Command Code 'insufficient
credits'/'insufficient credit' signals, and restore the consolidated
ChatAnywhere gateway entry that the stale snapshot removal would have
deleted when merging into release/v3.8.50.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
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>
A combo target that stalls past comboTargetTimeoutMs is aborted by
buildTargetTimeoutRunner, which swallows the resulting rejection behind its
synthetic 524. Nothing marks the account unavailable — correctly, since a stall
is not a quota/auth failure — so the #6219 eviction on the generic
markAccountUnavailable -> shouldFallback path in chat.ts never ran. The session
pin therefore survived its full TTL and every following request in that session
was handed straight back to the account that had just stalled.
Seen in production on combo "coding" [priority]: one codex account pinned for a
30-minute TTL, four consecutive requests, four 120s timeouts, "all targets
exhausted" each time, while four sibling codex accounts stayed healthy and
unused.
Classify the abort reason (new dependency-free leaf comboAbortReasons.ts) and
evict the connection-matched pin. Only a genuine per-model timeout evicts: a
client disconnect or a hedge cancellation says nothing about account health, so
those keep the pin and its prompt-cache locality. Eviction is best-effort and
never breaks the dispatch path.
The dispatch itself moves into a new seam, chatDispatch.ts, which merges the
per-model abort signal into the outgoing request, runs executeChatWithBreaker,
and owns the eviction on both the rejection and failed-result paths. Keeping
that logic out of the frozen god-file leaves chat.ts one line SHORTER than
before (1844 -> 1843).
Co-authored-by: alexey.nazarov@softmg.ru <alexey.nazarov@softmg.ru>
Co-authored-by: fenix007 <fenix007@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
containsOrMayEndWithThinkOpenTag missed the 6-char partial '<think', so an
open tag arriving as '<think' + '>' across SSE deltas leaked into content
instead of being parsed as reasoning. Derive every proper prefix from
THINK_OPEN itself so the lookahead list can never drift out of sync with
the tag again. Covered by new unit tests for the partial-suffix lookahead
and the split-delta buffering path.
* fix(usage): read Gemini usageMetadata out of the antigravity response envelope
Port decolua/9router#59d858b: antigravity/gemini-cli wrap non-streaming
payloads in { response: {...} }, so extractUsageFromResponse only saw the
top-level usageMetadata and every non-streaming antigravity request logged
zero usage (IN 0 | OUT 0) and zeroed usage-dashboard rows. Top-level
metadata keeps priority; OpenAI/Claude branches untouched.
* chore(changelog): fragment for #10430 antigravity usage envelope
* ci: re-run dast-smoke (Build CLI bundle runner timeout flake)
---------
Co-authored-by: Rouzbeh <rqzbeh@users.noreply.github.com>
* fix(antigravity): classify geo-blocked egress, exclude account, real connection probe
Google refuses the Cloud Code model API from unsupported egress locations
with 400 FAILED_PRECONDITION "User location is not supported for the API
use." Previously this surfaced as a cryptic "Antigravity upstream error
(400)", never excluded the account, and the dashboard connection test stayed
green because it only probed the (non-geo-restricted) OAuth userinfo endpoint.
- errorClassifier: new GEO_BLOCKED type + isGeoBlockedError detection
(400/403 + location-not-supported wording); non-terminal classification.
- chatCore fallback: GEO_BLOCKED marks the connection and caches a 24h
rate-limit-until exclusion so routing moves to other accounts instead of
re-selecting the same one; never bans/expires the account.
- auth: GEO_BLOCKED joins the non-terminal group (no banned/expired state).
- antigravityUpstreamError: geo refusals carry an actionable message (egress
location vs account problem, proxy-in-supported-region guidance).
- connection test: antigravity/agy now probe the REAL streamGenerateContent
surface (buildProbe), so a green tick means the model path actually works
and a geo-blocked egress shows red with a clear diagnosis.
* chore(changelog): fragment for #10420 antigravity geo-block resilience
* chore(pr): drop prettier-version drift noise, keep only real hunks
The earlier format pass (local prettier differs from the repo's pinned
version) rewrapped unrelated lines in chatCore.ts and the provider test
route. Restore the base formatting and re-apply only the GEO_BLOCKED
fallback branch and the buildProbe connection-test changes.
* fix(antigravity): strip competing-agent system prompts (429 RESOURCE_EXHAUSTED)
Port decolua/9router b566b20, generalized: Antigravity flags system prompts
advertising competing agents ('You are a Claude agent, built on Anthropic's
Claude Agent SDK.' — Zed, Claude Code, etc.) and answers with a 429 quota
error. sanitizeAntigravityGeminiRequest now strips known competitor identity
sentences from systemInstruction.parts before dispatch; surrounding
instruction text is untouched and non-matching prompts pass through without
allocation.
* chore(changelog): cover competitive prompt strip in #10420 fragment
* fix(antigravity): scope GEO_BLOCKED classification to Google AI surfaces
Address reviewer feedback: classifyProviderError is shared across every
provider, so a lookalike 'not available in your region' body from an
unrelated upstream must not receive the egress-fixable 24h exclusion
treatment. Gate GEO_BLOCKED behind isGeoBlockEligibleProvider, which
matches the surfaces that actually emit Google's regional-availability
refusal: Cloud Code / Gemini Code Assist (antigravity, agy, cloudcode*),
the Gemini Developer API (gemini, gemini-cli, vertex), plus a
registry-driven fallback on executor/format. Non-Google providers fall
through to their existing 400/403 classification (typically null for an
unclassified 400), so a permanent block still follows its own path.
* ci: re-run quality gates
Trigger a fresh CI run for the PR: the previous run's 'Vitest (fast-path)'
job failed in 'npm ci' because the onnxruntime-node postinstall could not
download its binary from the Microsoft CDN (connect ETIMEDOUT
150.171.109.118:443). No tests ran; no code changed in this commit.
* fix(antigravity): guard provider before registry lookup in geo-block gate
isGeoBlockEligibleProvider passes the raw provider (string | null | undefined)
to getRegistryEntry(provider: string), failing typecheck:core and the
ts7-diagnostics ratchet (TS2345 at errorClassifier.ts:166). Add an explicit
null guard; runtime behavior is unchanged — a falsy provider already resolved
to !entry -> false.
* ci: re-run quality gates (vitest npm ci onnxruntime CDN flake)
---------
Co-authored-by: Rouzbeh <rqzbeh@users.noreply.github.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>
OpencodeExecutor and MimocodeExecutor rotated to the next account only on
HTTP 429. A network exception (timeout, connection refused/reset) on one
account instead propagated out of execute() and failed the whole request,
even when other accounts remained available.
Both executors now rotate on a network exception only when the failed
account has its own dedicated proxy (account.proxy !== null) — a dead
proxy is genuinely account-scoped, so rotating away from it is safe.
Accounts sharing the default egress (no proxy configured) trigger the
same cooldown and are skipped for the rest of the request once the shared
egress is known down, but a later account with its own dedicated proxy is
still tried normally — a throw on a proxy-less account no longer strands
a proxied account further in the rotation. This behavior is gated behind
NETWORK_ROTATION_SHARED_EGRESS_GUARD (Feature Flag, default on); disabled,
it reproduces the immediate-propagation behavior this fix started from.
The shared rotation mechanics (pickAccount/markCooldown/markSuccess) are
extracted into executors/accountRotation.ts, used by both executors —
they had independently implemented the same round-robin+cooldown
skeleton. This also fixes an identical, pre-existing bug in
MimocodeExecutor that predates this PR: its catch block called
markCooldown unconditionally on any throw, with no proxy check and no
warn log (a silent exception swallow on a path that influences the
result).
The cooldown formula for both the proxy and shared-egress cases reuses
the repo's already-established "transient, not clearly attributable"
constants (errorConfig.ts TRANSIENT_COOLDOWN_MS/COOLDOWN_MS.transientMax,
already used by accountFallback.ts for network-error classification)
instead of introducing a separate value.
MimocodeExecutor's network-error 502 body also now goes through
buildErrorBody()/sanitizeErrorMessage() instead of embedding the raw
caught error message directly (Hard Rule #12), matching the sanitization
already used on its #2101 malformed-request path.
Validated by TDD (Hard Rule #18): tests/unit/account-rotation.test.ts
covers the shared module directly; opencode-proxy-rotation-4954.test.ts
and mimocode-executor.test.ts cover the proxy-configured rotation path,
the mixed-fleet case, the shared-egress single-network-call case, and the
NETWORK_ROTATION_SHARED_EGRESS_GUARD-disabled legacy path, for each
executor. tsc, lint, and the provider golden-path gates
(check:provider-consistency, check:provider-assets,
provider-translate-path-golden.test.ts) are clean on all touched files.
Co-authored-by: Max <maxmad64@gmail.com>
* fix(sse): dedupe header-budget drop warns by drop-set fingerprint
The 768-byte forwarded-header budget drop path emitted a full warn (with
up to 20 dropped entries) on every SSE response whose headers exceeded the
budget. The dropped set is usually identical across responses from the same
upstream, so the repeats carried no new information — under Desktop
multi-stream use this buried real errors and added event-loop serialization
work.
Warn once per unique drop fingerprint (sorted dropped-header names, capped
at 1000 fingerprints) per process, then log at debug level.
Fixes#10315
* changelog: fragment for #10397
* 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>