Commit Graph

2309 Commits

Author SHA1 Message Date
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
Diego Rodrigues de Sa e Souza
90458a613c fix(sse): stop the executor-contract guard from hot-looping the router (#10373)
The `instanceof Response` guard from #10256 broke two ways:

1. `instanceof` is nominal against `globalThis.Response`, but proxyFetch dispatches
   through the npm undici package's fetch, whose Response is a different class — so
   valid upstream responses were rejected as contract violations. Replaced with
   `isResponseLike()` (instanceof fast path + structural brand/member probe); genuinely
   malformed shapes still throw.
2. The thrown error had no `.status`, so it fell through to chatCore's BAD_GATEWAY
   default — an internal defect was treated as a flaky provider, cooling the connection
   down and retrying forever. It now carries status 500 + `executor_contract_violation`,
   registered as request-scoped and terminal (no cooldown, no breaker, no retry).

batch_api.test.ts went from exit 124 (infinite hang, pinning Unit shard 4/4 in every
open PR) to exit 0, 22/22 passing.

Closes #10360
2026-08-14 10:03:10 -03:00
backryun
27e163e2c9 fix(types): validate nonstreaming JSON contracts (#10258) 2026-08-14 00:58:04 -03:00
backryun
13098989e8 fix(types): narrow refresh token rotation inputs (#10257) 2026-08-14 00:57:58 -03:00
backryun
9da4e24013 fix(types): normalize executor result contracts (#10256) 2026-08-14 00:57:53 -03:00
backryun
2eec31b84a fix(types): align Responses stream options (#10255) 2026-08-14 00:57:48 -03:00
Xiangzhe
8417ace4b3 feat(codex): add OAuth fingerprint convergence modes (#10243)
* 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.

* feat(codex): converge OAuth fingerprints

* test(codex): preserve identity assertions

* fix(codex): preserve explicit off identity

* fix(codex): close fingerprint transport gaps

---------

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: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-14 00:57:23 -03:00
Diego Rodrigues de Sa e Souza
97aac6ac6c fix(ci): clear base-reds on release/v3.8.50 (round 4) (#10260)
* fix(ci): clear base-reds on release/v3.8.50 (round 4)

Drains the HARD failures reported by Release-Green run 31693210948 on issue #9985
(ESLint errors: 2) plus the merge-integrity red every open PR is inheriting.

- ESLint error 1: @omniroute/opencode-plugin/src/index.ts had a stray extra
  '});' (introduced by #9316) that broke parsing with 'unexpected file in NFT list'
  on the build path.
- ESLint error 2: cli-env-inline-comment-10100.test.ts used new Function to extract
  parseEnvValue from the bin entrypoint (no-new-func, Hard Rule #3). Extracted the
  helper to bin/cli/utils/parseEnvValue.mjs and import it from both the entrypoint
  and the test (same behavior, no eval).
- open-sse-typecheck (Fast Quality Gates): open-sse/utils/stream.ts imported
  sseCommentsEnabled twice (#9378) causing TS2300 Duplicate identifier; removed the
  duplicate import.
- Merge integrity (changelog + generated skills): skills/omni-settings/SKILL.md was
  edited manually by #10169 without updating the generator source, so
  check:agent-skills-sync failed on every PR (Generated: 1). Moved the curated
  thinking-budget content into a <!-- skill:custom-start --> block (the documented
  preservation mechanism), which the generator now keeps in sync.

Refs #9985

* fix(tests): align wave1-a poolside test with #10216 probed catalog

#10216 published Poolside's two authenticated-probe models
(poolside/laguna-xs-2.1, poolside/laguna-s-2.1) as static seeds, but the
wave1-a free-tier test still asserted 'no invented static model ids'
(entry.models === []), failing every open PR. Separate poolside from the
empty-models assertion and pin its probed catalog explicitly so a future
catalog change is a deliberate update, not a silent drift.

* fix(pack): register parseEnvValue.mjs in PACK_ARTIFACT_REQUIRED_PATHS

The extract of parseEnvValue to bin/cli/utils/parseEnvValue.mjs added a new
direct import to bin/omniroute.mjs, which pack-artifact-entrypoint-closures
enforces against PACK_ARTIFACT_REQUIRED_PATHS. Register the module so a future
tarball omission fails loudly.

* fix(combo): restore default same-model retry semantics after #10217

#10217 wired config.failoverBeforeRetry into the same-model retry guard in
both the priority/auto and round-robin loops, but DEFAULT_COMBO_CONFIG
defaulted the flag to true — flipping same-model retry off for every combo
that never touched the setting, not just the opt-in case. Round-4 bisect
(06f41cda63 vs d2fd88dfbc) reproduced this against
tests/unit/combo-499-abort.test.ts, tests/unit/combo-quota-exhaustion-only-fallback.test.ts
and tests/unit/combo-stream-readiness-fallback.test.ts. Flip the default to
false so the historical retry-before-failover behavior returns for combos
that never set the flag, while explicit opt-in (the two new tests #10217
added to combo-routing-engine.test.ts) still works.

* fix(quality): register visionBridge-responses-9597 in stryker tap.testFiles

check-mutation-test-coverage.mjs flagged tests/unit/guardrails/visionBridge-responses-9597.test.ts
as covering open-sse/services/combo/comboStructure.ts without being listed
in stryker.conf.json's tap.testFiles array. Add it so mutation coverage
attribution stays accurate.

* test(pack): expect parseEnvValue.mjs in the missing-artifact-paths fixture

The prior commit on this branch registered bin/cli/utils/parseEnvValue.mjs
in PACK_ARTIFACT_REQUIRED_PATHS but the "findMissingArtifactPaths flags
missing root runtime files in the tarball" test still hardcoded the old
expected list, so it never accounted for the new required path being
absent from the simulated tarball. Add it in its alphabetical slot.

* chore(lint): prune stale no-explicit-any suppression for call-log-file-rotation

--prune-suppressions found tests/unit/call-log-file-rotation.test.ts no
longer produces the 5 suppressed @typescript-eslint/no-explicit-any
warnings recorded in config/quality/eslint-suppressions.json. Remove the
dead entry so a regression would be caught again. Full-tree run with
--max-warnings 0 is clean: 0 errors, 0 warnings.

* fix(combo): decouple failoverBeforeRetry same-model guard from the skipUpstreamRetry default

Audit found that DEFAULT_COMBO_CONFIG.failoverBeforeRetry has defaulted to
true since before #10217 (predates #2417), and that value also feeds the
independent skipUpstreamRetry mechanism (src/sse/handlers/chat.ts:859,1126).
The previous commit on this branch flipped that default to false to fix the
#10217 same-model retry guard, which silently disabled skipUpstreamRetry's
own default-on behavior for every combo without an opt-in — a regression in
the opposite direction (executor-level retries before the loop's own
failover, changing latency/failure behavior).

Revert the default back to true and decouple the two mechanisms instead:
resolveComboConfig/resolveComboSetupConfig now also compute
failoverBeforeRetryExplicit, true only when a cascade layer (combo/provider/
global) literally sets failoverBeforeRetry to true — not merely inherited
from the default. The #10217 same-model retry guards in combo.ts (priority/
auto and round-robin loops) now read failoverBeforeRetryExplicit instead of
config.failoverBeforeRetry, restoring opt-in-only behavior for that guard
while the skipUpstreamRetry pass-through (config.failoverBeforeRetry at
combo.ts:1297,2865) is untouched and keeps its historical default-on.

* fix(combo,i18n): align getDefaultComboConfig with 10217 explicit flag; pt denoRelay entities

Two round-4 follow-ups exposed by the combinated base-red PR run:

1. comboConfig.ts: #10217 round-4 fix (104afeda4e) added
   failoverBeforeRetryExplicit to resolveComboConfig/resolveComboSetupConfig
   but getDefaultComboConfig() returned only DEFAULT_COMBO_CONFIG, so the
   combo-config.test.ts deepEqual (resolveComboConfig(null) ===
   getDefaultComboConfig()) failed on the extra field. Mirror the opt-in flag
   as false in the default.

2. pt.json: denoRelayOrgDomainHint still carried raw <app-name>/<org-slug>
   (the UNCLOSED_TAG RSC regression) — encode as &lt;...&gt; like the other
   42 locales, greening i18n-deno-relay-unclosed-tag.test.ts.

* chore(lint): disable @next/next/no-location-assign-relative-destination pending per-case review (#10292)

The eslint-config-next bump in #10043 shipped this new rule, flagging 6
pre-existing window.location.href navigations — several are deliberate
full-page reloads (login/logout state reset). Off with tracking issue
rather than a blanket router.push rewrite.

* fix(i18n): fill 439 missing UI keys (thinkingMode ×39 locales + pt catch-up) to restore 100% coverage

The #10169 Thinking Budget keys existed only in en/pt-BR/vi and the pt (PT-PT)
catalog from #10250 lagged 88 recent keys, dropping i18nUiCoverage to 99.3%
vs the frozen 100% ratchet baseline. Translated via the i18n:sync-ui marker
pipeline; glossary + ICU placeholder post-pass clean.

* fix(i18n): zh-TW glossary — replace retired 默認 with canonical 預設 in new thinkingMode keys

* chore(quality): rebase dead-code baseline 248 -> 409 for knip 6.32 bump (#10043)

dependabot #10043 upgraded knip 6.27 -> 6.32, which detects 162 MORE
genuinely-unused exports (331 vs 169) that 6.27 missed; DEAD_FILES
unchanged (78). Reproduced identically on the clean release/v3.8.50 tip
266e39d3 with a fresh 6.32 node_modules, so every PR is born red until
the tool change is absorbed. Owner authorized rebaseline (2026-08-13 via
PR #10260). Structural cleanup of the newly-surfaced dead exports remains
separate debt.

---------

Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
Co-authored-by: backryun <bakryun0718@proton.me>
2026-08-13 23:02:48 -03:00
Jeyhun F. Aslanov
05b1311884 fix(sse): extract perplexity-web answers from workflow_block (#10259)
Perplexity moved the answer text out of `markdown_block` into
`workflow_block` (`intended_usage: "workflow_root"`), streaming it as
RFC-6902 patches whose `field` is `"workflow_block"` and whose paths
address `/steps/<n>/items/<m>/payload/text_payload/chunks/<k>`.

`extractContent` recognised neither shape. Two independent guards dropped
every answer frame:

  - `isAnswerTextUsage("workflow_root")` is false, so the block loop
    `continue`d before any accumulation.
  - the diff guard skipped every patch whose `field !== "markdown_block"`.

The stream therefore ran to `COMPLETED` with an empty accumulator and the
executor surfaced `Provider returned empty content` (502) even though the
upstream SSE carried the full answer. Every model was affected — the
carrying block is model-independent — so the provider was unusable.

Adds `workflow_block` to `PplxBlock`, an `applyWorkflowDiff` patch
applier for the streaming path, and `applyWorkflowBlock` for a
materialized block on the terminal frame. Answer tracks are keyed per
step+item so concurrent items cannot overwrite each other's chunk
indices, and only `variant: "answer"` payloads are accumulated — search
queries, sources and "thinking" items stay out of the message.

Fixtures in the regression test are trimmed from a live capture
(pplx-auto, mode=copilot); replaying the full 96 KB capture through the
patched extractor yields the complete 247-char answer over 7 incremental
deltas, against an empty string before the fix.

Co-authored-by: Jeyhun F. Aslanov <jeyhun.f.aslanov@Jeyhuns-MacBook-Pro.local>
2026-08-13 12:38:11 -03:00
小妍儿 ✨
9a4cca4bc2 fix(sse): honor comment opt-out for final metadata (#9305) (#9378)
* test(sse): add RED coverage for comment opt-out

* fix(sse): honor comment opt-out for final metadata

---------

Co-authored-by: 千乘妍 (Xiaoyaner) <xiaoyaner0201@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-13 07:54:53 -03:00
James
2355f7beb3 fix(translator): resolve the Claude thinking output cap with the routed provider (#10139) (#10238)
fitThinkingToMaxTokens() clamps the synthesized max_tokens to the model
output cap, but resolved that cap from a bare model id via
safeCapMaxOutputTokens(model) -> capMaxOutputTokens(model). A cap that is
only known per provider -- an operator max_output_tokens override, a
synced catalog limit_output, or a registry entry -- is invisible to a
bare-model lookup, so modelCap came back null and the unbounded
responseRoom + requestedBudget branch ran.

When the client sends no max-token field at all, adjustMaxTokens()
supplies DEFAULT_MAX_TOKENS (64000) and reasoning_effort: "high" supplies
a 131072 thinking budget, so the provider request carried
max_tokens: 195072 and every such request was rejected upstream with a
bare 400.

Thread the already-in-scope routedProvider (openai-to-claude.ts:122, used
two lines later for the Kimi-coding check) through fitThinkingToMaxTokens()
into capMaxOutputTokens({ provider, model }), which already supports
provider-scoped resolution via resolveCapabilityInput() -- no new lookup
path needed. Omitting the provider (existing callers, tests) keeps the
bare-model behavior unchanged; verified in the added regression test.

Follow-up to #6637, whose token-budgeting half was never addressed: #6893
fixed only the combo fallback classification. Rebased onto the
open-sse/translator/request/openai-to-claude/thinkingBudget.ts extraction
that landed after the original patch was written against the inline code
in openai-to-claude.ts.
2026-08-13 07:53:52 -03:00
adevwithpurpose
ce4abd7ef4 fix(opencode): force CLI User-Agent when CLI identity synthesis is enabled (#10222) 2026-08-13 07:53:47 -03:00
Markus Hartung
d2fd88dfbc fix(combo): make failoverBeforeRetry actually skip the same-model retry (#10217)
* 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.

* fix(combo): make failoverBeforeRetry actually skip the same-model retry

Both same-target retry loops (priority/auto and round-robin) checked
isTransient/maxRetries/providerExhausted but never consulted
config.failoverBeforeRetry, so a rate-limited model still got
maxRetries+1 back-to-back attempts on itself before falling back to a
sibling — the config option (#2417) was only ever wired into
skipUpstreamRetry, a separate lower-level mechanism. Now the same-model
retry is skipped when failoverBeforeRetry is set AND a sibling target
is actually available; with no sibling left, it still retries same-model
since skipping would just burn the last attempt for nothing.

---------

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: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-13 07:53:38 -03:00
Paco Cartones
568888d7f1 feat(providers): publish Poolside's probed Laguna Preview catalog (#10216)
The Poolside entry landed with an empty `models` list because the public
matrix could only reach the unauthenticated endpoint, which answers 401
`No Authorization header provided` — the same response that a generic probe
once read back as "invalid key" and that got the provider dropped (#2723,
#3054). An authenticated probe against `/v1/models` (2026-08-07, #9085)
returned 200 and the full Preview catalog, so the two models are now static:

  poolside/laguna-xs-2.1  Laguna XS 2.1
  poolside/laguna-s-2.1   Laguna S 2.1

Both report 262144 context, 32768 max completion tokens, `tools` and
`reasoning`, and are text-only and free during Preview. The XS id is the
catalog form; the `laguna-xs.2` variant circulating in third-party listings
does not address this host. `passthroughModels` stays on, so live discovery
still admits models the Preview adds later.

Closes #9085
2026-08-13 07:53:32 -03:00
Ravi Tharuma
4e1d21f756 docs(settings): Thinking Budget modes + fix Auto i18n collision (#10169)
Co-authored-by: RaviTharuma <RaviTharuma@users.noreply.github.com>
2026-08-13 07:53:12 -03:00
Nathan
06f41cda63 fix(combo): isolate session stickiness by combo (#10137)
Co-authored-by: Bryan Nathan <bryan@users.noreply.github.com>
2026-08-13 07:53:02 -03:00
Hernan Javier Ardila Sanchez
a2e5bd1dfc fix(combo): default chaos SSE to comment-only for OpenAI-compatible clients (#10128)
* 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.

* fix(combo): default chaos SSE to comment-only for OpenAI-compatible clients

---------

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: herjarsa <herjarsa@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-13 07:52:56 -03:00
Xiangzhe
99d19f8f35 fix(kimi): normalize MFJS tool schemas (#10079)
Remove unsupported root-level anyOf constraints only on Kimi and Moonshot OpenAI tool requests while preserving nested schemas and caller-owned inputs. Mark Kimi Web models as unable to execute function tools so combo routing filters them correctly.
2026-08-13 07:52:41 -03:00
Jonathan Bailey
5d873e42d7 feat(crof): advertise reasoning effort tiers incl. max from live discovery and registry (#10062)
* feat(crof): advertise reasoning effort tiers incl. max from live discovery and registry

CrofAI's /v1/models exposes only a boolean reasoning_effort flag, so
discovery previously produced synced rows with no supportedThinkingEfforts
and the catalog/Combo Builder had nothing from which to derive -<tier>
aliases. Map the boolean to the full supported tier list (none/low/
medium/high/max) provider-scoped in discovery, thread providerId through
persistence, and declare the same tiers on every reasoning-capable seed
model (incl. glm-5.2, deepseek-v4-flash-0731, kimi-k3, and the rest of the
live roster) so stale synced caches still resolve effort aliases. max is
verified live: cache-bypassed fixed-seed requests produce distinctly more
reasoning than high, corroborating the Crof owner's statement.

* chore(changelog): add Crof reasoning effort feature fragment

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-13 07:52:32 -03:00
Arnav Rastogi
48124fca5a fix(zed-hosted): send the provider wire values cloud.zed.dev accepts (#10051)
Every zed-hosted completion failed with

  500 {"error":{"message":"[500]: An internal server error occurred."}}

for every model id, including deliberately invalid ones.

Root cause: ZED_PROVIDER held display-cased names ("Anthropic", "OpenAi",
"Google", "XAi"), and normalizeZedProvider's return value is serialized
straight into the `provider` field of the POST /completions envelope. Zed
matches that field exactly and fails the request before looking at the model,
which is why the model id never mattered.

Verified live against cloud.zed.dev with an otherwise identical request:

  {"provider":"anthropic",...} -> 200
  {"provider":"Anthropic",...} -> 500 {"message":"An internal server error occurred."}
  {"provider":"open_ai",...}   -> reaches the OpenAI request parser
  {"provider":"openai",...}    -> 500 (same internal error)

The spellings now follow Zed's own GET /models catalog, which reports
`anthropic`, `open_ai` and `google`. That also makes normalizeZedProvider
identity on catalog values instead of corrupting a value Zed just supplied —
previously it accepted the correct lowercase input and re-cased it into the
form that 500s.

`x_ai` follows the same underscore convention; this account's catalog exposes
no xAI models, so that one spelling is by convention rather than observation.

The constant is module-local and every branch compares against it, so internal
dispatch (initProviderState / convertProviderEvent / buildProviderRequest) is
unaffected. Two existing tests asserted the display-cased values and one passed
"Anthropic" to wrapZedCompletionStream directly; all are updated to the wire
values the executor now produces.

Co-authored-by: root <root@srv1710948.hstgr.cloud>
2026-08-13 07:52:27 -03:00
Tiangao
7f2d75d6d5 feat(open-sse): expose provider-level circuit breaker thresholds via env vars (#10040) (#10046)
The provider-level breaker fields in PROVIDER_PROFILES
(providerFailureThreshold, providerFailureWindowMs, providerCooldownMs,
degradationThreshold, maxBackoffMultiplier, backoffEscalationCount) are
now env-overridable via OMNIROUTE_PROVIDER_BREAKER_<CATEGORY>_<FIELD>
variables, with the historical hardcoded defaults preserved when unset.

This makes the provider-level fuse (the entire-provider cooldown applied
after repeated upstream failures) tunable from the deployment surface,
matching the existing per-key circuit breaker knobs. Operators can now
raise thresholds to tolerate transient upstream sheds without
blacklisting the provider, or lower them to fail over faster on
premium routes — without rebuilding from source.

Closes #10040

Category-by-category field map (defaults preserved):

- oauth: FAILURE_THRESHOLD=10, FAILURE_WINDOW_MS=900000, COOLDOWN_MS=300000,
  DEGRADATION_THRESHOLD=5, MAX_BACKOFF_MULTIPLIER=8, BACKOFF_ESCALATION_COUNT=2
- apikey: [REDACTED:auth_header], FAILURE_WINDOW_MS=1800000, COOLDOWN_MS=600000,
  DEGRADATION_THRESHOLD=7, MAX_BACKOFF_MULTIPLIER=4, BACKOFF_ESCALATION_COUNT=3
- local: FAILURE_THRESHOLD=2, FAILURE_WINDOW_MS=300000, COOLDOWN_MS=60000
  (local category omits the adaptive v2 fields)

Docs:
- .env.example — 15 new commented entries grouped under a
  "Provider-level circuit breaker thresholds and cooldowns" section.
- docs/reference/ENVIRONMENT.md — 15 new rows documenting the
  provider-level breaker surface.

Tests:
- tests/unit/provider-breaker-env-overrides.test.ts — 4 cases:
  1. Every new env var is wired in constants.ts via envInt().
  2. Every new env var is documented in ENVIRONMENT.md.
  3. Every new env var is listed in .env.example.
  4. The historical defaults are preserved as the envInt fallback.

Behavior tests (loading the actual module with controlled env vars) are
left to upstream CI; the static source-shape test is sufficient here
because the envInt() helper is a plain function whose only dependency
is process.env at module load time.

Co-authored-by: Tiangao (hermes) <montigaud@aikumi.pro>
2026-08-13 07:52:17 -03:00
Ke Jin
1a8d38655d fix(reasoning): preserve and replay assistant turns (#10045) 2026-08-13 07:52:12 -03:00
backryun
7366bb6c3a fix(types): tighten chatCore helper contracts (#10175) 2026-08-13 07:52:07 -03:00
TengSivtean
8718d2b62f fix(providers): kilo-gateway authType should be optional, not apikey (#10086)
Probed live: /chat/completions answers HTTP 200 with no Authorization
header (kilo-auto/free routed to stepfun/step-3.7-flash). A real key
still raises limits, so this matches the ovhcloud/pollinations pattern
of authType: "optional" rather than "apikey".

Fixes #10068
2026-08-13 04:42:34 -03:00
Markus Hartung
d925f6bf73 fix(logging): document CHAT_LOG_MAX_BODY_KB, capture messageCount for Responses API bodies (#10038)
* fix(logging): document CHAT_LOG_MAX_BODY_KB, capture messageCount for Responses API bodies

Extracted from PR #9439 (agentic conversation tracking). Most of the
original scope this commit was cherry-picked from (CHAT_LOG_MAX_BODY_KB
env var support, the estimateSizeFast() earlyExitAt parameterization)
turned out to already be present on the current upstream/release/v3.8.50
tip -- confirmed via diff and by running check-env-doc-sync.test.ts /
tests/unit/chatcore-log-truncation.test.ts against pristine upstream
before making any changes here. Only two genuine gaps remained:

1. CHAT_LOG_MAX_BODY_KB was read by getChatLogMaxBodyBytes() but
   undocumented in .env.example and docs/reference/ENVIRONMENT.md --
   tests/unit/check-env-doc-sync.test.ts flags any env var read in code
   but missing from both doc files. Documented it (both required --
   the same test enforces the pairing).

2. truncateForLog()'s summary only computed messageCount from
   obj.messages (OpenAI-chat/Gemini field name) -- a large /v1/responses
   request (which uses input[], not messages[]) got summarized with no
   count at all, leaving the dashboard's "Full Conversation" panel
   nothing to base its "N messages not shown" placeholder on for any
   Responses-API conversation, even though the same summarization logic
   applies to it.

Test plan:
- TDD: tests/unit/chatcore-log-truncation.test.ts's new regression test
  ("captures a message count for Responses API bodies too") confirmed
  failing against the pre-fix code, passing after.
- tests/unit/check-env-doc-sync.test.ts confirms CHAT_LOG_MAX_BODY_KB no
  longer appears in codeMissingEnv (remaining drift in that test is
  pre-existing/unrelated -- ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS,
  COMMANDCODE_API_URL, OMNIROUTE_STRICT_SYSTEM_PROVIDERS,
  TLS_FINGERPRINT_PROVIDERS -- confirmed identical on a pristine
  upstream/release/v3.8.50 checkout, base-red inherited: #9985).
- tests/unit/chatcore-log-truncation.test.ts -- 19/19 passing.
- npx tsc --noEmit / npm run lint -- clean.

⚠️ base-red inherited: #9985

* docs(logging): consolidate CHAT_LOG_MAX_BODY_KB into a single entry per file

The variable was already documented (with a stale src/lib/chatLogTruncation.ts
reference in .env.example); keep the new richer entries next to the CHAT_LOG_*
family and drop the old duplicates.

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

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-13 04:02:42 -03:00
Markus Hartung
2f264d96dc fix(dashboard): expose OpenAI Responses store toggle for non-Codex connections (#10121)
* fix(dashboard): expose OpenAI Responses store toggle for non-Codex connections

`EditConnectionModal` only rendered and saved the "OpenAI Responses store"
toggle (providerSpecificData.openaiStoreEnabled) inside the Codex-only
settings block, even though the backend policy that reads this flag
(open-sse/utils/responsesStatePolicy.ts::isOpenAIResponsesStoreEnabled,
applyResponsesPreviousResponseIdPolicy) is already fully provider-agnostic,
and the component already computes a generic `isResponsesConnection` flag
(provider === "openai" or any openai-compatible-responses-* connection, in
addition to codex) that the sibling `preserveEncryptedReasoning` toggle
already correctly uses.

Net effect: an operator with a plain OpenAI API-key connection, or any
generic OpenAI-Responses-compatible proxy connection, had no way anywhere in
the dashboard to opt that connection into `store`/`previous_response_id`
continuation — the policy layer was ready, the control just never rendered
for anything but Codex.

Move the toggle (and its save-time write) out of the isCodex-only block and
gate it on isResponsesConnection instead, matching preserveEncryptedReasoning.
Renamed the local formData field from codexOpenaiStoreEnabled to
openaiResponsesStoreEnabled since it is no longer Codex-specific.

Regression test added (TDD): renders the modal for a plain provider:"openai"
connection and asserts the toggle is present and reflects a persisted flag —
fails on the pre-fix code, passes after.

* fix(responses): stop store-marker leak into Chat Completions requests

The OpenAI Responses store toggle exposed in the previous commit was only
half the fix: the actual store functionality was broken for any model
routed to /v1/chat/completions instead of /v1/responses (e.g. gpt-5-nano,
which lacks the responses-only targetFormat capability). translateRequest
stashes the client's Responses-shaped store intent under an internal
_omnirouteResponsesStore marker so a later re-conversion back to Responses
shape can restore it as store -- but when the destination stays in Chat
Completions shape, that re-conversion never runs, nothing else consumed
the marker, and it leaked verbatim into the real upstream request body.
OpenAI's own API rejects it with 'Unknown parameter: _omnirouteResponsesStore'.
Confirmed live against the real OpenAI API.

Fix: drop the marker unconditionally at the end of translateRequest once
translation is complete, regardless of destination format. Chat Completions'
own store field means something different (dashboard eval storage, not
Responses-style previous_response_id continuation), so the client's intent
must not be silently remapped onto it either -- it's simply dropped.

Also fixes a real crash discovered while live-testing store-enabled
requests: src/sse/handlers/chat.ts referenced isProviderBreakerFailureStatus
without importing it (only the unused PROVIDER_BREAKER_FAILURE_STATUSES
constant was imported), turning a clean 429/'no credits' response into an
uncaught ReferenceError whenever all provider accounts were rate-limited.
Confirmed live (container logs showed the exact ReferenceError before the
fix, and clean error responses after).

Plus two small unrelated base-red fixes needed to get the test suite
running at all on this branch: a broken relative import in
conol-web/index.ts (one path segment short, pointed at a nonexistent
directory), and a real syntax error in gateways.ts (missing closing brace)
that broke esbuild's TypeScript transform for every test file that
transitively imports it, including the pre-existing combo-breaker-429
suite used to verify the isProviderBreakerFailureStatus fix doesn't
regress breaker classification.

Regression test: tests/unit/responses-store-marker-leak.test.ts (confirmed
failing before the translator/index.ts fix, passing after).

⚠️ base-red inherited: migration 143_job_registry.sql duplicated an
already-existing 146_job_registry.sql (byte-identical migration body,
confirmed via diff); the 143 file is deleted since 146 is canonical per
SCHEMA_VERSION_RENAMES. Needed for translateRequest's DB-backed model
capability lookup to run at all in tests.
2026-08-13 04:02:38 -03:00
Markus Hartung
c9daf99e37 fix(combo): clear LKGP pin when its target fails, not only set it on success (#10034)
setLKGP() was only ever called on success — nothing invalidated a "last
known good provider" pin once that provider started failing, so a
*separate* subsequent request kept re-selecting the same just-failed
target via applyStrategyOrdering.ts's LKGP reordering.

Live incident: an OpenClaw request to combo "default" (routerStrategy:
lkgp) got a real reasoning + apply_patch tool call from
opencode-zen/big-pickle, then 3 separate follow-up requests over the
next ~2 minutes each independently re-selected the same big-pickle
target and each timed out with "504 Stream produced no non-ping SSE
event within 95000ms" before the client gave up — instead of failing
over to any of the combo's other 12 models.

Root cause confirmed via code read: circuit breaker and model lockout
deliberately don't react to this failure class (isStreamReadinessFailureErrorBody
exempts STREAM_READINESS_TIMEOUT/combo_target_timeout 504s from tripping
the provider breaker, and REQUEST_SCOPED_UPSTREAM_ERROR_CODES suppresses
model-lockout recording for the same class — both intentional, to avoid
poisoning a healthy provider on request-specific timing). Nothing else
in the system was clearing the stale LKGP pin, so it kept winning
target-selection ordering for every new top-level request.

Fix: add clearLKGP(comboName, modelId) to src/lib/db/settings/lkgp.ts,
export it through settings.ts/localDb.ts, and call it (mirroring the
existing setLKGP-on-success call pattern exactly, same two keys) in both
combo.ts's per-target failure paths -- handleComboChat's "Done retrying
this model" block and handleRoundRobinCombo's structurally identical
twin -- right where a target is finally given up on and the loop moves
to the next one.

TDD: new regression test in tests/unit/combo-routing-engine.test.ts
("clears LKGP after the last-known-good target fails") reproduces the
exact live scenario -- confirmed failing against the pre-fix code,
passing after. Added direct unit coverage for clearLKGP itself in
tests/unit/db-settings-crud.test.ts (deletes only the targeted key,
sibling keys survive; no-op on an unset key doesn't throw) and
registered the new export in db-settings-split.test.ts's public API
surface characterization test.

Test plan:
- Full combo/LKGP-related suite (combo-routing-engine, db-settings-crud,
  db-settings-split, combo-strategy-fallbacks,
  combo-selected-connection-success,
  delete-provider-connection-invalidates-lkgp-8887, db-read-cache) --
  183/183 passing.
- npx tsc --noEmit -- clean for all changed files (pre-existing unrelated
  errors elsewhere in the same test files confirmed identical against a
  pristine upstream/release/v3.8.50 checkout, zero diff at those lines).
- npm run lint -- clean (new test's any usage properly typed, not left
  to inflate the file's frozen any-budget suppression).

⚠️ base-red inherited: #9985
2026-08-13 04:02:34 -03:00
Markus Hartung
4bda22583e fix(sse): provider-response summary format bugs (dashboard Provider Response panel) (#10037)
* fix(sse): provider-response summary reconstructed from truncated events

The dashboard's "Provider Response" panel showed a stale, incomplete
snapshot for long streamed responses. Root cause: open-sse/utils/stream.ts
reconstructed the summary from
buildStreamSummaryFromEvents(providerPayloadCollector.getEvents(), ...)
-- but getEvents() only returns whatever survived the collector's
maxEvents/maxBytes cap, so once a stream exceeded it (easy with a
reasoning + tool-calling model), everything after the cutoff (final
finish_reason, tool_calls, rest of reasoning_content, usage) was
silently dropped from the reconstruction, even though the client
actually received the correct, complete response.

Fix: streamPayloadCollector.ts's per-format summary builders
(buildOpenAISummary/buildResponsesSummary/buildClaudeSummary/
buildGeminiSummary) are now also available as incremental reducers
(createXReducer: ingest one chunk at a time, finalize at the end).
createStructuredSSECollector accepts a format + fallbackModel and feeds
the reducer on every push() -- including chunks that get dropped from
the retained event array once the cap is hit -- via a new getSummary()
method. stream.ts's error-path call site now uses
collector.getSummary() instead of reconstructing from the (possibly
truncated) getEvents().

Extracted from a squashed commit (originally authored alongside a
conversation-tracking continuation fix in the same commit) -- only the
files relevant to this SSE-summary bug are included here
(stream.ts/streamPayloadCollector.ts + their test); the unrelated
conversationTracker.ts continuation fix stays with the conversation-
tracking PR it belongs to.

Test plan:
- New TDD regression tests in tests/unit/stream-payload-collector.test.ts,
  confirmed failing before the fix and passing after.

* fix(sse): provider-response summary used the client's format, not the provider's

providerPayloadCollector (dashboard "Provider Response" panel) was keyed on
sourceFormat (the CLIENT's wire format) instead of targetFormat (the
PROVIDER's — see createSSEStream's own @param doc: "targetFormat - Provider
format", "sourceFormat - Client format"). Whenever a request translates
between two different formats — e.g. a Responses-API client routed to a
plain-OpenAI-chat-completions upstream, the common OpenClaw/opencode-zen
shape — the reducer picked for sourceFormat could never recognize the
provider's actual raw event shape, so it stayed stuck at its empty initial
state. The dashboard's "Provider Response" panel showed a permanently empty
`output: []` while "Client Response" (built from separately-accumulated
state, unaffected by this bug) correctly showed full content — reading as
if the two panels simply disagreed about the same request.

Confirmed live via a wire-level pcap capture (scripts/sre/tcp-close-
analyzer.py) cross-referenced against the dashboard log
(1786032832181-1c6275): the actual response was complete and correct: this
was purely a logging/summary bug, never a wire-format bug.

Fix is mode-aware: TRANSLATE mode uses targetFormat (the provider's true
format); PASSTHROUGH mode keeps sourceFormat, since passthrough has no
separate provider/client format split — nothing gets translated there, and
real passthrough callers (createPassthroughStreamWithLogger) don't even
pass targetFormat.

New regression test reproduces the exact live scenario (Responses-API
source, OpenAI target, real chat.completion.chunk deltas) and asserts the
provider summary reflects them — confirmed it fails with the old
`sourceFormat`-keyed code (reproducing the live `output: []`-style
symptom) and passes with the fix.

Co-authored-by: Markus Hartung <markus.hartung@gmail.com>

* fix(sse): stamp object: chat.completion on the provider-summary fallback

createSSEStream's providerPayloadCollector.build() falls back to the
synthesized responseBody as the "Provider Response" dashboard summary
whenever sourceFormat/targetFormat isn't OPENAI_RESPONSES (in both the
passthrough and translate branches) -- but responseBody is built purely
for the client and never carries an `object` field at all, so the
summary ended up with `object: undefined` instead of the expected
"chat.completion", even though everything else (choices, usage) was
correct.

Caught by this PR's own new regression test ("createSSEStream translate
mode: providerPayload summary reflects the PROVIDER's format, not the
client's") -- the code itself was unchanged by the rebase (applied
cleanly from the original commit), so this was a latent gap in the
original fix, not a rebase regression.

Fix: stamp `object: "chat.completion"` on a shallow copy used only for
the provider summary in both branches; responseBody itself (sent to the
client elsewhere) stays untouched.

Verified: tests/unit/stream-utils.test.ts 51/52 passing (the one
remaining failure is an unrelated, pre-existing v3.6.6-era test,
confirmed present and failing identically on a pristine
upstream/release/v3.8.50 checkout -- base-red inherited: #9985).
typecheck/lint clean (pre-existing unrelated errors elsewhere in the
file, confirmed identical to upstream).

---------

Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
2026-08-13 04:02:30 -03:00
Markus Hartung
ae54c6221c fix(responses-api): tool call after reasoning collided on the same output_index (#10025)
emitToolCallAdded/closeToolCall used the provider's raw Chat Completions
tool_calls[].index directly as the Responses API output_index. That index
is scoped only to the tool_calls array and legitimately restarts at 0 for
the first tool call, but a reasoning item (and/or a text message) emitted
earlier in the same turn may already have claimed output_index 0 (and 1).
A client that tracks response items by output_index (as the Responses API
spec expects) then sees the tool call's added/delta/done events land on an
index it already marked complete, and silently drops the tool call --
producing an "incomplete turn" that never dispatches it.

Reported live: OpenClaw on combo default -> opencode-zen/big-pickle sent a
reasoning block immediately followed by a function call in the same turn
(no text message in between); the function call's output_index collided
with the reasoning item's.

A similar collision (tool call after a *text message*) was already fixed
in open-sse/translator/response/openai-responses.ts (#9822/#9843), but
that file is only used by the zed-hosted executor -- the general
/v1/responses path (wired via responsesHandler.ts) goes through this file,
which never received the equivalent fix.

Fix: compute the tool call's output_index once (offset past any reasoning/
message item already emitted this turn) and cache it in
state.funcOutputIndex, so every added/delta/done event for that call --
including ones emitted later from the finish_reason handler or flush() --
shares exactly the same output_index.

TDD: new regression tests in
tests/unit/responses-transformer-tool-call-reasoning-collision.test.ts
reproduce the exact live scenario (reasoning immediately followed by a
tool call, and multiple tool calls after reasoning) -- confirmed failing
against the pre-fix code, passing after. Full transformer test suite
(responses-transformer*.test.ts, responses-replay-fixes.test.ts,
responses-api-truncation.test.ts, responses-request-translation.test.ts)
-- 24/24 passing, no regressions.

⚠️ base-red inherited: #9985
2026-08-13 04:02:25 -03:00
Markus Hartung
0a72988bde fix(responses-api): explicit function-tool declaration must win over apply_patch-is-custom fallback (#10041)
open-sse/translator/response/openai-responses.ts's isCustomTool check
unconditionally treats any tool named "apply_patch" as a Codex-style
custom tool: `toolName === "apply_patch" || state.customToolNames?.has?.(toolName)`.
This overrides a client's own explicit declaration whenever it registers
apply_patch as a plain `type:"function"` tool (with its own JSON-schema
parameters) instead of `type:"custom"`.

Live incident: OpenClaw (combo "default" -> opencode-zen/big-pickle)
declared apply_patch as `type:"function"` with `{input:string}`
parameters. The model correctly produced valid JSON matching that
schema (`{"input":"*** Begin Patch..."}`), but OmniRoute unwrapped it
into a custom_tool_call with raw-text `input` instead of the
function_call/`arguments` shape the client actually registered.
OpenClaw's own dispatcher only implements function_call handling for a
name it declared as type:"function", so it silently never recognized
the tool call at all -- no error, no execution, no follow-up request
ever carrying a result back to the model.

Traced the exact live code path (chatCore.ts -> createSSEStream
translate mode -> translator/index.ts's hub-and-spoke openai ->
openai-responses conversion) to confirm this file -- not
transformer/responsesTransformer.ts -- is what handles combo-routed
streaming for this client/provider format pair.

PR #7905 ("Restore Responses API custom tool calls") already states
this exact precedence should hold ("...while preserving explicit
function-tool precedence") but its unconditional `toolName ===
"apply_patch"` OR never actually implemented that carve-out for
apply_patch specifically -- this fixes the gap between that PR's
stated intent and its actual behavior.

Fix: state.toolSchemas (populated from body.tools by
extractToolSchemaMap(), already threaded through stream.ts's translate
state for a different purpose -- #6951's stripEmptyOptionalToolArgs)
only contains an entry for a tool name when the client's request
declared it with a `parameters` JSON schema, i.e. as type:"function".
Gate the apply_patch fallback on NOT finding it there: apply_patch
still defaults to custom (native Codex CLI convention -- the model
emits it without the client ever declaring it as a tool) unless the
client explicitly registered it as a function tool, in which case that
explicit declaration wins.

Test plan:
- TDD: two new regression tests in
  tests/unit/translator-openai-responses-custom-tool-1007.test.ts --
  "...with tool defined" (function_call, arguments stay raw JSON) and
  "...without tool defined" (unchanged custom_tool_call fallback,
  mirroring the existing #1007 coverage). The "with" test is confirmed
  failing against the pre-fix code, passing after; the "without" test
  passed before and after (regression guard for the existing fallback
  behavior).
- Full related suite (translator-openai-responses-custom-tool-1007,
  responses-handler, responses-active-stream-custom-tool,
  translator-resp-openai-responses,
  translator-resp-openai-responses-namespace-identity,
  translator-openai-responses-image-output-8459,
  responses-transformer) -- 64/64 passing, no regressions to PR #7905's
  own custom-tool coverage.
- npx tsc --noEmit -- clean (pre-existing loose-typing errors in this
  test file confirmed identical on a pristine upstream checkout).
- npm run lint -- clean.

⚠️ base-red inherited: #9985
2026-08-13 04:02:21 -03:00
zannen7
cc41503c4a fix(chatgpt-web): preserve native max thinking effort (#10077)
* fix(chatgpt-web): preserve max thinking effort

* fix(chatgpt-web): allow native max effort

* test(chatgpt-web): cover native max effort

* docs(changelog): record ChatGPT Web max effort fix
2026-08-13 01:01:52 -03:00