Commit Graph

2339 Commits

Author SHA1 Message Date
Rouzbeh†
6ff2e7b2c2 fix(antigravity): heal empty-projectId accounts via retryable auto-onboarding (#10424)
* fix(antigravity): heal empty-projectId accounts via retryable auto-onboarding

Accounts with an empty Cloud Code projectId get a permanent 422 "Missing
Google projectId" when loadCodeAssist returns no project. The 3.8.50
bootstrap attempts to CREATE the project via onboardUser, but a single failed
attempt (transient network/upstream error) was memoized forever in
onboardAttemptedCache: every later request in the process skipped onboarding
and 422'd, even though a retry would succeed.

Replace the permanent per-token Set with a failure-backoff map: failed onboard
attempts are retried after a 5-minute backoff (bounded, self-healing), the
in-flight lock still dedupes concurrent calls, and success clears the failure
marker and memoizes the project as before. Accounts that CAN be onboarded now
heal automatically on a later request or token refresh — no user action.

Tests: the existing "does not retry" case is now framed as the backoff window;
a new case proves the account heals (retries onboarding and recovers the
project) once the backoff expires.

* chore(changelog): fragment for #10424 antigravity project autocreate

* feat(antigravity): BYOP fast-fail + manual GCP project-id override

Port decolua/9router#2934 + VansRouter 802a859:
- tryOnboardUser now returns a three-way status; a 200 onboardUser response
  WITHOUT cloudaicompanionProject means Google deprecated automatic project
  creation for standard-tier (personal) accounts (BYOP). Such accounts are
  cached permanently (no pointless ~18s re-onboard) and the executor fails
  fast with 403 GCP_PROJECT_REQUIRED + actionable 'enter your project id'
  message instead of the generic 422 or a delayed 429.
- Transient onboard failures keep the existing 5-min backoff heal.
- Manual project-id override: the EditConnectionModal now stamps
  providerSpecificData.isProjectIdManual when the operator enters a project
  id, and tokenRefresh skips auto-discovery for flagged accounts so the
  manual value is never overwritten.

* chore(changelog): cover BYOP fast-fail + manual override in #10424 fragment

* test(antigravity): expect fast 403 GCP_PROJECT_REQUIRED when loadCodeAssist finds no project (#10424)

Google now marks accounts without an onboarded project as BYOP (automatic
project creation deprecated for standard-tier accounts, #2934). The PR's
BYOP fast-fail path returns 403 gcp_project_required instead of the old
generic 422 missing_project_id; align the #2334 executor test with that
contract so CI unit-test shard 2/4 passes.

* fix(antigravity): persist isProjectIdManual, fix BYOP citation, dodge refresh-retry

Review follow-up on #10424:

1. EditConnectionModal: isProjectIdManual was set on
   updates.providerSpecificData right after the project-id field, then the
   OAuth path (Antigravity is always OAuth) rebuilt providerSpecificData from
   connection.providerSpecificData before the request went out, discarding the
   flag — tokenRefresh.ts was guarding a field never actually persisted. The
   flag now lands in the single surviving antigravity merge, with a jsdom
   regression test (modeled on edit-connection-modal-openai-store-toggle).

2. The '#2934' citation for the Google BYOP claim pointed at an unrelated
   closed issue. Swapped for the real tracking issue #8491 (empty Google
   projectId -> 422 class) across bootstrap/executor/test comments.

3. BYOP fast-fail now returns 422 instead of 403: chatCore's generic
   401/403 -> refresh-and-retry path was hitting Google's OAuth token
   endpoint on every request from an affected account (pointless — refreshing
   cannot create a GCP project), and 422 matches the sibling
   missing_project_id error the client already maps to an action-needed
   prompt.

Also: eslint-disable-next-line for the pre-existing
react-hooks/set-state-in-effect baseline noise in the modal (repo
convention, same pattern as 11 other dashboard files).

* chore(ci): drop unused eslint-disable in EditConnectionModal form hydration

The react-hooks/set-state-in-effect disable added in the previous commit is
unused under the repo's pinned eslint-plugin-react-hooks (7.0.1) — the rule
does not fire on this line at that version, so the unused directive tripped
the whole-repo 'No new ESLint warnings' gate (max-warnings 0). Verified with
the lockfile-pinned plugin: lint:json is clean (0 errors, 0 warnings).

* 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(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.

* Merge branch 'release/v3.8.50' into fix/antigravity-project-autocreate

* test(fix): refresh expired alibaba quota sample validity and onnxruntime pin for v3.8.50 base

- alibaba-free-tier-quota-fetcher.test.ts: sample quotaValidityPeriod
  (2026-08-16 16:00 UTC) is in the past, making every quota entry classify
  as expired/not_capable; bump to 2028-01-01 UTC so the text/merge
  classification tests exercise the intended path again.
- optional-transformers-dependency.test.ts: onnxruntime-node pin assertion
  updated from ~1.24.3 to ~1.27.0 to match package.json (bumped by #10403);
  the regular-not-optional intent is unchanged.

* test(fix): widen modelsDevSync lastSync wait from 200ms default to 2000ms

The truthy-spellings loop asserted each enabled case completes its first
fetch within waitFor's 200ms default timeout, which trips under CI runner
load (observed on PR 10424 shard 2/4). Match the file's other lastSync
waits (2000ms) so the sync-completion assertion is load-tolerant.

---------

Co-authored-by: Rouzbeh <rqzbeh@users.noreply.github.com>
2026-08-17 08:23:41 -03:00
Ravi Tharuma
722748f7c1 fix(sse): keep Codex quota headers under the forwarding budget (#10306)
* 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>
2026-08-17 08:22:42 -03:00
Markus Hartung
0f402a84a4 feat(responses): virtualize previous_response_id continuation regardless of upstream support (#10262)
* 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>
2026-08-17 08:22:17 -03:00
Rizx
b8b78f7a69 fix(providers): remove invalid CodeBuddy CN glm-4.7 and add hy3 (0.0x… (#10356)
* 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>
2026-08-17 08:03:16 -03:00
Diego Rodrigues de Sa e Souza
8dec2ad472 fix(resilience): mark embed connection terminal on hard upstream failure so dead accounts are not re-hit (#10347) (#10506)
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
2026-08-17 07:06:00 -03:00
Diego Rodrigues de Sa e Souza
31b02ff85f fix(responses): keep stream-aware TextDecoder across SSE transform chunks (#10223) (#10495)
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
2026-08-17 07:05:23 -03:00
Ravi Tharuma
48e5cf7fe4 fix(sse): do not ZWJ-obfuscate the substring hermes in user text (#10488)
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>
2026-08-17 07:04:48 -03:00
Sahil Singh
8ff3a1dda3 fix(mcp): dynamically generate web search provider enum from registry (#10209)
* 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>
2026-08-17 07:01:46 -03:00
Xiangzhe
faeca3bbac fix(providers): scope model target formats to providers (#10072)
Co-authored-by: xz-dev <xz-dev@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
2026-08-17 07:01:09 -03:00
Diego Rodrigues de Sa e Souza
bdc30ca4dd fix(providers): strip uniqueItems from Gemini tool schemas to avoid upstream 400 (#9617) (#10511)
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
2026-08-17 05:50:09 -03:00
Diego Rodrigues de Sa e Souza
9bfdc15cbc fix(providers): emit Cursor kv_after_text before tool calls instead of truncating them (#10215) (#10502)
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
2026-08-17 05:49:58 -03:00
Bob.Hou
33e0fea8b0 fix(sse): flag OpenAI streams that close with content but no terminal marker (#10475)
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>
2026-08-17 05:49:46 -03:00
backryun
c6c134300b perf(electron): ship optional ML/browser deps as installable packs (#10382)
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%).
2026-08-16 02:20:59 -03:00
Jan Leon
e5e1358693 fix(antigravity): discover live chat models dynamically (#10422)
* fix(antigravity): discover Gemini 3.7 Flash models

* fix(antigravity): discover live chat models dynamically

* fix(antigravity): keep provider limits sanitizer strict
2026-08-16 00:42:59 -03:00
Paco Cartones
d010a9979f fix(providers): repoint freeaiapikey to its live API host and resync its catalog (#10233)
* 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>
2026-08-16 00:42:25 -03:00
Chewji
be6f18b849 fix(account-fallback): classify 'insufficient credits' as credits-exhausted (#10116)
* 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>
2026-08-16 00:42:20 -03:00
Alex
20fcb8d205 fix(affinity): evict the sticky session pin on a combo per-model timeout (#10016)
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>
2026-08-16 00:42:09 -03:00
SHANMUGAPRIYAN
579cae32b1 fix(sse): buffer '<think' partial so a split open tag cannot leak into content (#10441)
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.
2026-08-16 00:16:36 -03:00
Rouzbeh†
df226e55f4 fix(usage): read Gemini usageMetadata out of the antigravity response envelope (#10430)
* 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>
2026-08-16 00:16:32 -03:00
Rouzbeh†
e44a409aa9 fix(antigravity): classify geo-blocked egress, exclude account, real connection probe (#10420)
* 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>
2026-08-16 00:16:27 -03:00
Dizzle
94cf4c402a fix(executors): rotate to the next account on network throws when the account has a dedicated proxy (#10402)
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>
2026-08-16 00:16:04 -03:00
Jacky Lam
6e97fbf340 fix(sse): dedupe header-budget drop warns by drop-set fingerprint (#10397)
* 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
2026-08-16 00:16:00 -03:00
azzaouiomar19-sketch
201c234b96 fix(chat): guard search providers from OpenAI fallback (#10394)
Co-authored-by: DarkAngel <48388675+DarkEsteves@users.noreply.github.com>
2026-08-16 00:15:55 -03:00
Markus Hartung
4b76d3b76f fix(sse): close the synthetic keepalive reasoning item + harden output_index allocation (#10330)
* fix(sse): close the synthetic keepalive reasoning item's output_item

RESPONSES_STARTUP_THINKING_FRAME (the /v1/responses early-keepalive
placeholder for slow-starting reasoning models) opened a synthetic
"rs_keepalive" reasoning item at output_index 0 and closed its nested
summary part (response.reasoning_summary_part.done), but never sent
response.output_item.done to close the item itself. The comment
claimed it was "closed within this one frame" — that was true for the
part, not the item.

Since this placeholder has no real upstream counterpart (the real
response starts an independent response.created lifecycle later and
never touches it), nothing else ever closes it. A client tracking open
items by output_index (as the Responses API spec requires — this is
exactly what OpenClaw's parser does) sees index 0 still open when the
real response's own output_item.added later reuses that same index,
and throws a collision.

Live incident (2026-08-13, reliably reproducing by 2026-08-14): traced
via a live tcpdump capture on the OmniRoute-dev container's network
namespace, correlated against the OpenClaw gateway journal and 10
separate real request/response pairs (all wire-clean on the response
side, ruling out provider corruption). The failing request's own
outbound payload confirmed a replayed reasoning item without
encrypted_content feeding a continuation call; the response wire bytes
for that exact exchange showed rs_keepalive's output_item.added at
index 0, then response.created/response.in_progress arriving *after*
it, then a second output_item.added reusing index 0 for the real
reasoning item — never preceded by an output_item.done for
rs_keepalive. Reported upstream as OpenClaw issue #123342 before the
OmniRoute-side root cause was found.

Fix: emit response.output_item.done for the synthetic item, matching
its already-buffered summary text, right after the summary part closes
and before the frame ends.

Test plan:
- tests/unit/early-stream-keepalive.test.ts: updated the frame-shape
  test to assert the full 5-event closed sequence (added the missing
  output_item.done and its field assertions); confirmed it fails
  against pre-fix code (only 4 events) and passes after
- node --test tests/unit/early-stream-keepalive.test.ts,
  tests/unit/earlyStreamKeepalive.test.ts,
  tests/unit/keepalive-cleanup-8140.test.ts,
  tests/unit/chat-body-admission.test.ts: 58 passed, 2 pre-existing
  skips unrelated to this change (Node test runner
  ReadableStream-error-simulation limitation)
- tsgo --noEmit: clean on both touched files

* fix(sse): allocate the keepalive output_index from a stack, not a literal

Follow-up to 03f8345ac. That commit patched the specific symptom (added
the missing response.output_item.done). This commit fixes the class:
RESPONSES_STARTUP_THINKING_FRAME hardcoded output_index: 0 as a literal
across five hand-written events, which is exactly how the missing-close
bug happened in the first place — nothing enforced that every open got
a matching close, so it silently didn't for months.

ResponsesOutputIndexStack (open-sse/utils/responsesOutputIndexStack.ts)
makes that structural: open() allocates the next sequential index,
close() must name the index being closed and throws if it doesn't match
the stack's top, and assertAllClosed() throws if anything is still open.
The keepalive frame now calls assertAllClosed() at module load, so a
future regression of this exact shape fails at import/boot time instead
of shipping a malformed stream to production and surfacing days later
as a live incident.

Also adds tests/helpers/assertResponsesOutputIndexLifecycle.ts: a
reusable version of the same invariant for replaying a full SSE event
sequence (not just checking one frame's own shape), mirroring what a
real client's output-index tracker enforces. Existing coverage for this
bug class (responses-reasoning-close-before-message-466.test.ts) only
asserted it by hand for one specific emitter (the real translator); nothing
generic existed for a hand-rolled synthetic frame like this keepalive to
be checked against, which is why its own test could pass while the actual
downstream contract still failed. Wired into
early-stream-keepalive.test.ts, including a test that concatenates the
keepalive frame with a plausible real subsequent response and asserts no
collision — the scenario that actually reproduced live, not just the
frame's own internal shape.

Test plan:
- tests/unit/responses-output-index-stack.test.ts (new): open/close/
  assertAllClosed behavior, including the exact mismatch and
  never-closed shapes this incident hit
- tests/unit/early-stream-keepalive.test.ts: existing frame-shape test
  plus new collision-simulation test, both passing
- node --test across responses-output-index-stack, early-stream-keepalive,
  earlyStreamKeepalive, keepalive-cleanup-8140, chat-body-admission:
  65 passed, 2 pre-existing skips unrelated to this change
- tsgo --noEmit: clean on all touched files

---------

Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
2026-08-16 00:15:38 -03:00
Anudeep Adiraju
cb51facf12 fix(docker): prefix cache mount ids with Railway service scope (#10288)
* fix(docker): prefix cache mount ids with Railway service scope

Railway's Dockerfile builder rejects --mount=type=cache ids that lack
the s/<service-id>- prefix (dockerfile invalid, caught at syntax
validation before any build step runs). Prefix all 7 cache mount ids
(apt-cache, apt-lists x4 RUN blocks, npm-cache x2, next-cache x1) with
the omni-route service id.

* fix(sse): remove duplicate sseCommentsEnabled import in stream.ts

Turbopack rejected the file with 'the name sseCommentsEnabled is
defined multiple times' — imported once at the top of the file and
again lower down from the same module. Broke every production build
(Docker/Railway) at the release/v3.8.50 tip, independent of the cache
mount fix in this branch. Validated by a full Docker build on Railway
completing past this step.
2026-08-16 00:14:40 -03:00
Harkaran Brar
710e43eb97 fix(sse): answer tiny-budget reasoning probes with a truncated 200 (#10281) (#10284)
* fix(sse): answer tiny-budget reasoning probes with a truncated 200 (#10281)

Claude Code's /model capability check sends max_tokens: 1. Reasoning
models burn the whole probe on thinking, and some upstreams (e.g.
api.cline.bot for deepseek-v4-flash) answer the empty outcome with a
5xx "empty response content" instead of a truncated 200. The relayed
failure also marked the connection unavailable and poisoned
fallback/cooldown bookkeeping for what is only a probe.

Detect tiny-budget reasoning probes in the non-streaming providerFailure
path and synthesize a valid truncated response (200, empty content,
finish_reason "length") — the same semantics errorClassifier.ts already
grants to length-truncated empty 200s. Probes no longer poison
connection health. Refs #10281.

* chore(changelog): add fragment for reasoning-probe truncated-200 fix (#10284)
2026-08-16 00:14:35 -03:00
backryun
5a7487a60a refactor(providers): unify xAI authentication entry point (#10201)
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.
2026-08-16 00:13:46 -03:00
backryun
5239728d6f feat(providers): add Grok 4.6 and refresh DeepSeek V4 (#10195) 2026-08-16 00:13:41 -03:00
Bezrabotnyi
595d04dad9 feat(providers): add local ZCode ACP backend (#10184)
* 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>
2026-08-16 00:13:36 -03:00
Diego Rodrigues de Sa e Souza
5379493bed feat: add Video Bridge frame sampling (#10483)
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.
2026-08-15 14:23:29 -03:00
Benson K B
f466ea91c9 fix(kilocode): strip unsupported response_format for DeepSeek V4 Flash (400 regression) (#10458)
* fix(kilocode): strip unsupported response_format for DeepSeek (400 regression)

kilocode's DeepSeek V4 Flash rejects ANY response_format — both
json_schema AND json_object 400 with 'Invalid input: response_format'
(verified live 2026-08-15 via the Hindsight fact-extraction path on
kilocode/deepseek/deepseek-v4-flash). The default executor's
applyJsonSchemaFallback only covered openai-compatible-* providers and
only downgraded json_schema -> json_object, so kilocode forwarded the
unsupported format raw. Same bug class as the opencode fix #9992.

For kilocode: strip response_format entirely and inject the schema (or a
plain 'valid JSON only' instruction for json_object) into the system
prompt. openai-compatible-* keeps the existing json_schema downgrade and
json_object passthrough (they accept both).

Regression tests: kilocode json_schema is stripped + schema-injected;
kilocode json_object is stripped + JSON-only instruction; both verified
to fail without the fix (sabotage: 2 fail). All 49 executor-default-base
tests pass.

* fix(kilocode): drop as-any casts in new tests to clear the frozen ESLint baseline

The file's frozen no-explicit-any baseline is count 42; the new kilocode
strip tests added 3 net-new 'as any' casts, tripping the --max-warnings 0
lint-guard. Replace them with typed assertions that carry the same checks.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

---------

Co-authored-by: benzntech <benzntech@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-08-15 13:52:11 -03:00
Bob.Hou
8ff7f7daf0 fix(sse): relocate directive-only messages off messages[0] (#10457)
The upstream Messages API rejects directive-style messages (empty content
array with a message-level output_config) when they sit at messages[0] —
the initial system prompt position — while accepting the form at any other
position. Measured in production: 122x 400 on the offical-claude combo in
one hour.

The mid-conversation-system passthrough (official provider + 1M-context
beta models) keeps system-role messages inside messages[], so a directive
that arrived first went upstream unchanged. relocateDirectiveOnlyMessages()
moves the whole leading run of empty system messages: directive-only ones
past the first real turn, plain empties dropped. extractSystemRoleMessages()
now folds a directive's output_config into the top-level parameter instead
of silently discarding it.

Signed-off-by: Minxi Hou <houminxi@gmail.com>
2026-08-15 13:51:40 -03:00
Hernan Javier Ardila Sanchez
e168b2347e fix(combo): restrict auto combo pools to user-visible models (#10456)
Auto combos (virtual auto/* pools via virtualFactory and pure-auto named
combos via expandAutoComboCandidatePool) expanded their candidate pool from
the provider's STATIC registry catalog, which can include models the operator
never synced or approved (e.g. openrouter/auto). The visibility filter
(getHiddenModelsByProvider) only caught models explicitly flagged isHidden,
so catalog-only models passed through and got routed upstream.

Build the credentialed pool from the models the user actually has available
(synced + custom non-hidden), falling back to the static catalog only when
the operator has no synced/custom models for that provider. Applies to every
provider uniformly (openai, kilocode, openrouter, ...), with per-connection
scoping for synced models. Provider wildcards (providerWildcard.ts) already
used the active synced catalog as the authoritative source.

Regression coverage: tests/unit/combo-auto-pool-visible-only.test.ts

Co-authored-by: herjarsa <herjarsa@users.noreply.github.com>
2026-08-15 13:51:34 -03:00
Reza Rezaei
774127be3f feat(providers): add tencent-aistudio-web cookie provider (tasw) (#10174)
* feat(providers): add tencent-aistudio-web cookie provider (tasw)

* fix(sse): remove orphaned DevinDesktopExecutor import from executor index

The "devin-desktop" executor key is unused (devin-desktop provider config
resolves to executor "devin-cli"); the imported ./devin-desktop.ts file
was never present, so executors/index.ts failed to load (ERR_MODULE_NOT_FOUND)
and broke every unit test that imports the executor registry (e.g.
tests/unit/deepseek-web.test.ts). Stale base sync carried this into the branch.
Remove the dead import/registration/export.

* fix(providers): restore DevinDesktopExecutor registration in executor index

The previous commit removed the devin-desktop executor import/registration/
export from open-sse/executors/index.ts, but the devin-desktop provider
registry still resolves executor "devin-desktop" and
tests/unit/devin-providers.test.ts asserts hasSpecializedExecutor("devin-desktop")
is true. The removal broke 6 tests in that file. Restore the three lines so
the live Devin Desktop executor keeps serving the provider.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(providers): correct tencent-aistudio-web wrapper shape + provider count sync

Return {response,url,headers,transformedBody} instead of a raw fetch Response
(the executor contract every other executor in this file follows) and
re-wrap the upstream body so it uses the local Response constructor, not the
undici-patched one from globalThis.fetch.

Regenerate docs/reference/PROVIDER_REFERENCE.md and sync the 339->340
provider-count claims (README, AGENTS.md, llm.txt + 42 i18n mirrors,
package.json, promise-pillars/comparison-table/cli-terminal SVGs) that this
PR's new provider invalidated.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* docs(providers): sync readme-hero.svg provider count claim (339->340)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(providers): register tencent-aistudio-web web-session credential metadata + golden

Add the WEB_SESSION_CREDENTIAL_REQUIREMENTS entry for tencent-aistudio-web
(cookie-based, matching the executor's raw Cookie-header credential) and
regenerate the translate-path golden snapshot to include the new provider —
both were failing CI unit tests that enumerate every registered provider.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(providers): align tencent-aistudio-web test with the wrapper-shape contract

The test asserted res.status/res.json() directly against executor.execute()'s
return value, matching the pre-fix (broken) raw-Response shape. Update it to
read res.response.status/res.response.json() — the {response,url,headers,
transformedBody} contract every executor in this codebase follows.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: MeRezaRezaei <MeRezaRezaei@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
2026-08-15 05:15:17 -03:00
Xiangzhe
27a10da398 Merge remote-tracking branch 'origin/release/v3.8.50' into fix/radar-oss-cumulative-integration 2026-08-14 22:23:58 -03:00
Diego Rodrigues de Sa e Souza
4b914ad297 feat(sse): bring i-have-adhd to vi/ja/id parity and guard the style × language matrix (#10425)
i-have-adhd shipped in #10271 with en + pt-BR only; it now covers vi/ja/id as well,
matching ponytail. Every level keeps the SHARED_BOUNDARIES clause.

Adds a per-MATRIX guard (output-styles-i18n-matrix.test.ts). Every existing test is
per-style, which is how less-code stayed English-only since the 9router port without
anyone noticing. The guard fails on: a new style without pt-BR, a style losing a
translation it had, a translation missing an intensity level, a translation that dropped
the boundaries clause, and a stale KNOWN_ENGLISH_ONLY entry.

Proven by mutation, not just by passing: dropping less-code from the allowlist and
renaming the vi key both turned it red with the expected messages.

less-code stays English-only as declared debt (KNOWN_ENGLISH_ONLY + comment); the
remaining coverage work is tracked in #10426.
2026-08-14 21:18:38 -03:00
Xiangzhe
d2d2598a0d Merge remote-tracking branch 'origin/release/v3.8.50' into fix/radar-oss-cumulative-integration 2026-08-14 21:00:01 -03:00
Diego Rodrigues de Sa e Souza
e05ac345da feat(sse): honor provider-rule lock scope for agentrouter (connection vs model) (#10419)
Makes the ProviderErrorRule `scope` field real at the persistence layer, exclusively for agentrouter (owner decision; every other provider keeps byte-identical behavior).

checkFallbackError now surfaces `ruleScope` behind the HONORS_RULE_LOCK_SCOPE_PROVIDERS allowlist, and the agentrouter 403 path consults the rules before the generic apikey-FORBIDDEN early-return. markAccountUnavailable honors scope "connection" with a temporary connection cooldown instead of a per-model lockout — guarded so a permanent state can never be downgraded to a transient retry loop — and combo now skips the exhausted account within the same request, which also stops force-reusing the just-cooled connection via allowRateLimitedConnection.

Documented in RESILIENCE_GUIDE §7 with the honest limits (disableCooling connections keep per-model behavior; the 6h model-access cooldown is clamped by mlSettings.maxCooldownMs, 30min by default; same-request skip needs targets carrying their own connectionId).

Closes #10334
2026-08-14 20:52:53 -03:00
Xiangzhe
bac3b4eb37 Merge remote-tracking branch 'origin/release/v3.8.50' into fix/radar-oss-cumulative-integration 2026-08-14 15:44:57 -03:00
Diego Rodrigues de Sa e Souza
abd4df63dc fix(sse): surface Qwen/Alibaba personal Token Plan quota in dashboard and preflight (#10290)
* 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>
2026-08-14 15:40:33 -03:00
Diego Rodrigues de Sa e Souza
7837e46908 feat(ocr): Vertex AI DeepSeek-OCR provider (#10398)
* feat(sse): add Vertex AI DeepSeek OCR transformation to the registry

Adds VERTEX_DEEPSEEK_TRANSFORMATION (request/response mapping for the
Vertex AI DeepSeek OCR MaaS endpoint) and registers the
"vertex-deepseek-ocr" provider in OCR_PROVIDERS, modeled on litellm's
VertexAIDeepSeekOCRConfig. buildRequest treats the resolved baseUrl as
the complete Vertex endpoint URL (project/location resolved upstream),
matching the existing Mistral passthrough pattern.

* feat(sse): resolve Vertex AI DeepSeek OCR auth and endpoint URL

Adds resolveVertexOcrAccessToken (mints a Vertex OAuth access token from
a Service Account JSON apiKey, reusing open-sse/executors/vertex.ts's
existing JWT-bearer exchange — no new OAuth flow) and
resolveVertexOcrBaseUrl (derives the project/location "openapi/chat/
completions" endpoint from providerSpecificData or the Service Account
JSON's project_id). Both live in open-sse/handlers/ocr.ts, not the
src/app/api/v1/ocr route, since routes may not import executor
implementations directly (EXECUTOR_IMPORT_RESTRICTION in
eslint.config.mjs) — the route re-exports/consumes them across that
boundary. handleOcr now prefers credentials.accessToken over apiKey so
the minted token (not the raw Service Account JSON) is sent upstream.

* docs(api): document the vertex-deepseek-ocr /v1/ocr provider

Adds the vertex-deepseek-ocr row to the /v1/ocr provider table and a
short section on its Vertex AI auth/endpoint resolution, and lists the
new provider/model id in openapi.yaml alongside mistral and
azure-document-intelligence.

* docs(skills): regenerate omni-inference skill for the Vertex OCR provider

---------

Co-authored-by: Xiangzhe <bakryun0718@proton.me>
2026-08-14 15:15:12 -03:00
Diego Rodrigues de Sa e Souza
fa0b0effbe feat(providers): derive imageToText from the OCR registry + chutes dots.ocr seed (#10400)
* feat(ocr): transformation layer on ocrRegistry (Mistral shape canonical)

* feat(ocr): Azure Document Intelligence provider (prebuilt-read, analyze+poll)

* feat(ocr): generic dispatch with per-provider transformation and DI poll loop

* test(ocr): align sanitized-500 assert with HR#12 error sanitization

The test's own title ("returns a sanitized 500") describes the new
behavior mandated by HR#12 (never leak err.message in a response body).
The old regex asserted the pre-sanitization leak (`OCR request failed:
socket closed`) as expected output, which contradicted its own title
and the sanitization this task intentionally introduced in
open-sse/handlers/ocr.ts. Scoped to this single assertion only.

* fix(ocr): fail fast on non-ok poll responses instead of misleading 504

pollOcrOperation now checks pollRes.ok and returns a sanitized 502
immediately (logging the upstream status via console.error) instead of
looping until the 30-attempt cap and surfacing a misleading timeout for
what was actually an auth/upstream error during polling.

* feat(ocr): route/docs for multi-provider /v1/ocr

- Route: map the connection's providerSpecificData.baseUrl onto
  credentials.baseUrl (resolveOcrCredentials) so azure-document-intelligence
  connections resolve their endpoint the same way every other custom-endpoint
  provider does (src/lib/providers/validation/*); previously handleOcr only
  saw a baseUrl when a caller set it directly, so the DB-backed Azure
  connection endpoint was never forwarded.
- v1OcrSchema.model is already a free-form string, no schema change needed.
- Docs: add the /v1/ocr provider table + example + Azure poll-flow note to
  API_REFERENCE.md, and describe the provider/model prefix + async poll
  behavior in openapi.yaml.
- Test: tests/unit/ocr-route-contract.test.ts covers getAllOcrModels/
  parseOcrModel for both providers and resolveOcrCredentials's mapping.

* feat(providers): derive imageToText serviceKind from the OCR registry

* feat(providers): chutes imageToText (dots.ocr seed)

* chore(quality): rebaseline gateways.ts file-size for imageToText serviceKinds

Same rebaseline as #10275 (frozen 1250 -> 1252): this branch adds the chutes
serviceKinds declaration, the second of the two data lines.

* chore(quality): rebaseline deadExports for the OCR/image-to-text series

---------

Co-authored-by: Xiangzhe <bakryun0718@proton.me>
2026-08-14 14:26:08 -03:00
Diego Rodrigues de Sa e Souza
c62ace5a49 feat(ocr): multi-provider /v1/ocr with transformation layer (Azure Document Intelligence) (#10283)
* feat(ocr): transformation layer on ocrRegistry (Mistral shape canonical)

* feat(ocr): Azure Document Intelligence provider (prebuilt-read, analyze+poll)

* feat(ocr): generic dispatch with per-provider transformation and DI poll loop

* test(ocr): align sanitized-500 assert with HR#12 error sanitization

The test's own title ("returns a sanitized 500") describes the new
behavior mandated by HR#12 (never leak err.message in a response body).
The old regex asserted the pre-sanitization leak (`OCR request failed:
socket closed`) as expected output, which contradicted its own title
and the sanitization this task intentionally introduced in
open-sse/handlers/ocr.ts. Scoped to this single assertion only.

* fix(ocr): fail fast on non-ok poll responses instead of misleading 504

pollOcrOperation now checks pollRes.ok and returns a sanitized 502
immediately (logging the upstream status via console.error) instead of
looping until the 30-attempt cap and surfacing a misleading timeout for
what was actually an auth/upstream error during polling.

* feat(ocr): route/docs for multi-provider /v1/ocr

- Route: map the connection's providerSpecificData.baseUrl onto
  credentials.baseUrl (resolveOcrCredentials) so azure-document-intelligence
  connections resolve their endpoint the same way every other custom-endpoint
  provider does (src/lib/providers/validation/*); previously handleOcr only
  saw a baseUrl when a caller set it directly, so the DB-backed Azure
  connection endpoint was never forwarded.
- v1OcrSchema.model is already a free-form string, no schema change needed.
- Docs: add the /v1/ocr provider table + example + Azure poll-flow note to
  API_REFERENCE.md, and describe the provider/model prefix + async poll
  behavior in openapi.yaml.
- Test: tests/unit/ocr-route-contract.test.ts covers getAllOcrModels/
  parseOcrModel for both providers and resolveOcrCredentials's mapping.

* chore(quality): rebaseline deadExports for the OCR/image-to-text series

* docs(skills): regenerate omni-inference skill for the multi-provider /v1/ocr

The generated agent skill mirrors docs/reference/API_REFERENCE.md; updating the
/v1/ocr section left it stale and tripped the merge-integrity gate.

---------

Co-authored-by: Xiangzhe <bakryun0718@proton.me>
2026-08-14 13:47:35 -03:00
Diego Rodrigues de Sa e Souza
f1673f6bb7 feat(bridge): normalize images to 2048px long edge before vision describe self-call (#10287)
* feat(bridge): optional-sharp image normalization util (long-edge 2048)

* feat(bridge): normalize fetched images before vision describe self-call

Route the bridge's own fetchRemoteImageAsDataUri() output through
normalizeDataUri() (long-edge cap 2048) before handing it to the vision
model — matches the resize cap OpenAI/Anthropic already apply, cutting
upload bytes/latency. Scoped to the bridge's self-fetched images only,
never the user's raw passthrough payload (HR#20 opt-in principle).

* test(bridge): height-dominant long-edge coverage

Add a 100x4096 PNG case to image-normalize.test.ts alongside the existing
width-dominant one, so normalizeImageBuffer's long-edge cap is proven on
both axes.

* fix(bridge): type sharp's callable default export (TS2349)

* chore(quality): rebaseline deadExports for the OCR/image-to-text series

---------

Co-authored-by: Xiangzhe <bakryun0718@proton.me>
2026-08-14 13:06:12 -03:00
Diego Rodrigues de Sa e Souza
20ea78c943 feat(sse): restate agentrouter quota 403/400 as retryable 429 with provider-scoped error rules (#10335)
agentrouter.org signals temporary quota exhaustion with HTTP 403/400 and a Chinese body (用户额度不足) instead of 429, so clients like Claude Code treat it as permanent and abort, and the fallback engine classified it as a generic apikey AUTH_ERROR.

New registry open-sse/config/upstreamStatusRestatement.ts restates those statuses to 429 with a synthetic Retry-After at a single hook in chatCore's providerFailure block (after parseUpstreamError), so classification, combo aggregation and the client response all see a retryable error. 无权访问模型 (permanently no model access) is veto-listed and never restated.

agentrouter classification rules are registered in providerErrorRules.ts and reach the real checkFallbackError path through resolveRuleMatchBody() with an exclusive FULL_TEXT_RULE_PROVIDERS allowlist — every other provider keeps its previous behavior byte-for-byte.

Known limitations tracked in #10334: the rules' scope field is informational (persistence applies per-model lockout for agentrouter), the 403-only model-access rule has no production path yet, and errors embedded in 200 SSE streams are not restated.

Refs #10334
2026-08-14 12:42:58 -03:00
Xiangzhe
7580feb1bf Merge remote-tracking branch 'origin/release/v3.8.50' into fix/radar-oss-cumulative-integration 2026-08-14 12:22:42 -03:00
Diego Rodrigues de Sa e Souza
964a3fe442 feat(sse): add i-have-adhd output style to compression catalog (#10271)
Adds `i-have-adhd` as the 5th entry in OUTPUT_STYLE_CATALOG — a port of the
github.com/ayghri/i-have-adhd skill (MIT), following the same integration shape as
ponytail. Action-first output shaping: the next action leads, multi-step work is
numbered, no preamble/recap/closers — which also trims output tokens.

lite/full/ultra levels in en + pt-BR, each ending in SHARED_BOUNDARIES so code, paths,
commands, errors and URLs stay verbatim. The agent-harness-specific upstream rules
(restate plan state, time estimates) are reworded as conditionals so they hold for plain
chat clients too.

Per the D-A1 registry contract, one catalog entry is the whole change: the injector, the
settings panel, the Zod schema and the telemetry all enumerate the catalog, so no other
production file moves. Dedicated test mirrors ponytail-catalog.test.ts (7 tests).
2026-08-14 11:57:00 -03:00
diegosouzapw
ee6695acad refactor(mcp): modularize Radar catalog tool 2026-08-14 10:58:10 -03:00
diegosouzapw
b6bba896b3 docs(radar): document guided combos and MCP 2026-08-14 10:58:09 -03:00
diegosouzapw
ea95cc2937 feat(mcp): expose Radar catalog tool 2026-08-14 10:52:20 -03:00