Commit Graph

5622 Commits

Author SHA1 Message Date
Markus Hartung
6e1e5c9a45 fix(sse): tool-incapable provider handling (AI Horde + Responses content-collapse scoping) (#8212)
* fix(sse): collapse single-text-part Responses-API content to a plain string

Every /v1/responses request — even the simplest single-string input —
got 500'd by AI Horde's Aphrodite-backed facade. Root cause:
normalizeResponsesInputForChat() always wraps a plain string input as
`content: [{ type: "input_text", text: value }]` (a one-element array),
and openaiResponsesToOpenAIRequest() mapped that straight through to
`content: [{ type: "text", text: value }]` on the Chat Completions side
— an array. That's spec-valid (OpenAI's own API accepts both shapes),
but strict/naive OpenAI-compatible backends like AI Horde's only
implement the plain-string form and reject the array form outright.

A single-text-part array and a plain string are semantically
identical, so collapse is safe. Real multi-part messages (text+image,
text+file) are left untouched.

Regression test: tests/unit/openai-responses-single-text-content-string.test.ts
(RED before the fix — every collapsed-content assertion failed with an
object instead of a string; GREEN after).

Also adds a deeper AI Horde load-test suite (sequential/concurrent/
cross-model/sustained-throughput/new-capable-model-candidates) that
surfaced this bug via real live traffic after Behemoth-X-123B was
temporarily added to the "default" combo for evaluation.

Co-Authored-By: Markus Hartung <markus.hartung@gmail.com>

* fix(sse): unsupportedParams provider-level fallback for aihorde's live-discovered models

Real OpenClaw traffic against the newly-added Behemoth-X-123B combo
target kept 500ing on every attempt even after the Responses-API
content-array fix landed. The pipeline artifact showed why: `tools`
was still present, unstripped, in the request actually sent to AI
Horde's Aphrodite backend.

Root cause: `unsupportedParams: ["tools", "tool_choice",
"parallel_tool_calls"]` was only declared on the 3 models statically
listed in the aihorde registry entry (Cydonia-24B, Skyfall-31B,
google/gemma-4-31b). AI Horde uses `passthroughModels: true` — its
live worker roster changes constantly — so Behemoth-X-123B, like every
other dynamically-discovered aihorde model, had no model-specific
unsupportedParams entry, and getUnsupportedParams() returned [] for
it. But "the workers run raw text-completion backends" (no tool
calling) is true of every model AI Horde serves, not just the 3
catalogued ones.

Adds a provider-level `unsupportedParams` fallback on RegistryEntry,
checked by getUnsupportedParams() after the per-model lookup misses.
Set on the aihorde entry so it covers its entire live-discovered
roster, present and future, without needing a static per-model catalog
entry for each one.

Regression test: tests/unit/aihorde-tools-unsupported-provider-fallback.test.ts
(RED before the fix — Behemoth-X and deepseek-v4-flash both returned
[] instead of the stripped param list; GREEN after, with a control
case confirming the fallback doesn't leak to unrelated providers).

Co-Authored-By: Markus Hartung <markus.hartung@gmail.com>

* fix(sse): flatten leftover tool-call history when stripping unsupported tools

Third bug in the same AI Horde/Behemoth-X saga: even after tools/
tool_choice were correctly stripped from the live request (previous
fix), real combo traffic still 500'd. The conversation history itself
carried a prior turn's role:"assistant" tool_calls and role:"tool"
result messages, left over from before the combo failed over from a
tool-capable model (Gemini) to a non-tool-capable one (AI Horde). Its
raw completion backend doesn't understand those message shapes at all,
independent of whether live `tools` is present — confirmed by
reproducing with a role:"tool" message and NO tools param at all.

flattenToolHistory() (open-sse/utils/flattenToolHistory.ts) already
existed for exactly this, fully unit-tested — it just had zero call
sites anywhere in the request pipeline. Extracts the unsupported-params
strip into a small testable module
(open-sse/handlers/chatCore/unsupportedParamsStrip.ts, following the
existing chatCore god-file decomposition pattern e.g.
executorClientHeaders.ts) that now also flattens tool-call history
whenever "tools" was among the stripped params.

Regression test: tests/unit/chatcore-unsupported-params-strip.test.ts
(RED before the fix — the flattening test failed with the raw
tool_calls array still present; GREEN after). All 434 existing
chatcore-*.test.ts tests still pass.

Co-Authored-By: Markus Hartung <markus.hartung@gmail.com>

* fix(sse): gate tool-history flattening on unsupported, not on stripped-this-request

The previous commit's flattening only fired when "tools" was actually
present-and-stripped on THIS request. A second live reproduction
against AI Horde had no live `tools` param at all — only stale
tool_calls/tool-result messages inherited from before a combo
failover — and still 500'd, because that condition never triggered.

A model that can't do tool calling can't do it whether or not the
current request happens to carry a `tools` array. Gate on the
unsupported-params list itself (unsupported.includes("tools")) instead
of the subset that was actually present-and-deleted this time.

Regression test added to the same file (RED before — the no-live-tools
case left tool_calls/role:"tool" untouched; GREEN after). All 435
chatcore-*.test.ts still pass.

Co-Authored-By: Markus Hartung <markus.hartung@gmail.com>

* fix(sse): skip tool-incapable combo targets, error clearly on direct requests

Two complementary fixes for a model that structurally can't do tool
calling at all (e.g. AI Horde's raw completion backends) rather than
silently degrading — following up on the earlier strip/flatten fix,
which stopped the crashes but let a tool-incapable target still get
selected and return a 200 that narrates a fake tool call in prose
instead of erroring or being skipped.

1. Root cause, combo routing: getResolvedModelCapabilities()'s
   `supportsTools` resolution only checked per-model registry entries,
   synced capabilities, and static specs — none of which exist for a
   dynamically-discovered model (AI Horde's passthroughModels roster
   changes as workers come and go). It fell through to
   heuristicToolCalling(), which optimistically defaults to `true` for
   any unrecognized model (TOOL_CALLING_UNSUPPORTED_PATTERNS is empty).
   Added a provider-level fallback reusing the same unsupportedParams
   signal the request-time strip already relies on. This makes the
   EXISTING filterTargetsByRequestCompatibility (comboStructure.ts) —
   which already correctly excludes non-tool-capable targets when a
   request requires tools — actually work for these models; no combo.ts
   changes were needed, it was only ever fed bad capability data.

2. Direct/pinned requests: filterTargetsByRequestCompatibility only
   protects combo routing. A direct request naming an exact
   tool-incapable model has no other target to fail over to — added
   checkToolCallingRequiredButUnsupported (chatCore/toolCallingRequiredCheck.ts),
   gated on isCombo: false, returning a clear 400 instead of a 200 that
   silently can't do what was asked.

Regression tests (both RED before, GREEN after):
- tests/unit/model-capabilities-provider-unsupported-tools.test.ts
- tests/unit/chatcore-tool-calling-required-check.test.ts
All 463 chatcore-*/model-capabilities-*.test.ts and 31 combo
compatibility-filter tests still pass.

Co-Authored-By: Markus Hartung <markus.hartung@gmail.com>

* fix(sse): correct handleChatCore return shape for the tool-calling-blocked error

handleChatCore's documented contract is `{ success, response, status,
error }`, not a raw Response — returning `new Response(...)` directly
(copied from a different early-return whose surrounding context turned
out not to share this function's top-level contract) produced "No
response is returned from route handler ... Expected a Response object
but received 'undefined'" and a bare 500 with an empty body, caught
immediately when verifying the previous commit live.

Uses createErrorResult() (already used by the adjacent
translation-failure branch a few lines up) instead of hand-building the
Response, matching the same pattern already established in this
function for early error returns.

Co-Authored-By: Markus Hartung <markus.hartung@gmail.com>

* fix(sse): scope Responses single-text-content collapse to providers that need it

The single-text-part content array -> plain string collapse (added for AI
Horde's Aphrodite facade, which 500s on the array form) was applied
unconditionally to every provider, silently breaking the standard OpenAI
array-shaped content contract that other providers and existing tests
depend on. Added RegistryEntry.requiresPlainStringContent, gated the
collapse on it (true only for aihorde), and threaded modelInfo.provider
through responsesHandler -> responsesApiHelper -> the translator so the
real /v1/responses call site can identify the provider.

Co-Authored-By: Markus Hartung <markus.hartream@gmail.com>

---------

Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
Co-authored-by: Markus Hartung <markus.hartream@gmail.com>
2026-07-23 05:03:53 -03:00
Markus Hartung
4ea08f520b fix(sse): Gemini malformed function-call handling + tool_choice translation (#8211)
* fix(sse): synthesize tool_calls for Gemini's malformed function-call abort reasons

Live incident (dashboard log id 1784489701456-d8c0e9): Gemini terminates a
stream with finishReason MALFORMED_FUNCTION_CALL/UNEXPECTED_TOOL_CALL when its
own parser rejects an attempted tool call — there's no real functionCall part,
only a human-readable finishMessage. gemini-to-openai.ts passed this through
raw as finish_reason (9router#2462's fix, correctly keeping it off a clean
"stop"/Claude end_turn), but a raw "malformed_function_call" isn't one of
OpenAI's 5 documented finish_reason values, so a real OpenAI-format client
(OpenClaw) has no handling for it at all and silently never notices the turn
failed — confirmed live via tests/integration/live-gemini-workload.test.ts's
[28] streaming case after the Gemini TPM/rebase work on this branch.

Fix: synthesize a tool_calls entry (arguments carry the error code + Gemini's
finishMessage, valid JSON) and finish_reason: "tool_calls" instead, routing
the failure into the ordinary "tool call arguments didn't parse" path every
OpenAI-compatible agent loop already handles. Defers to a real tool call if
one already completed earlier in the same turn — the real call wins, no
synthetic entry piles on top of it.

Tests (TDD, each confirmed red-before-green):
- 5 new unit tests in the existing 9router#2462 regression file, covering the
  synthesis itself, UNEXPECTED_TOOL_CALL, the real-tool-call-wins edge case,
  and no-regression on a clean STOP.
- New fixture (tests/fixtures/translation/gemini-malformed-function-call-stream.json):
  the real 6-chunk event series from the live incident, sanitized (personal
  paths/URLs replaced with generic placeholders, structure preserved exactly).
- New integration test chains the real translator into the real Responses API
  transformer using that same fixture, proving correct behavior on BOTH
  /v1/chat/completions and /v1/responses from one shared ground-truth event
  series.

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

* fix(sse): don't drop a malformed tool-call failure when it lands beside a real one

Live incident (dashboard log id 1784589106014-2a42f8), analyzing why the
prior malformed-function-call fix (3568c7259) still wasn't reaching the
client in this case: Gemini can emit a REAL, valid functionCall AND finish
the SAME candidate with MALFORMED_FUNCTION_CALL — the model attempted
multiple tool calls in one turn (here: a real status-check call plus a
malformed "exec"+"cron" multi-call attempt), one parsed cleanly and the
other didn't.

The first fix version skipped synthesizing a failure signal whenever a real
tool call already existed (state.toolCalls.size > 0), on the assumption
that meant the model was retrying a LATER, separate attempt after an
earlier one already succeeded. That's indistinguishable, from the
translator's state, from this same-turn case — so it silently discarded
the malformed attempt's information entirely: the client saw the real call
succeed and never learned the other tool calls were attempted and rejected.

Fix: always synthesize the failure entry when a malformed abort reason is
seen, appending it alongside any real tool call rather than skipping it.
Multiple tool_calls in one response is normal, well-supported OpenAI
behavior (parallel tool calls), so this adds the failure as an additional
entry instead of replacing or hiding the real one.

Tests (TDD, confirmed red-before-green):
- Rewrote the unit test that encoded the old (wrong) assumption to assert
  both the real and synthesized calls are present.
- New fixture (gemini-malformed-function-call-parallel-real-call-stream.json):
  the real event series from this incident, sanitized.
- New integration tests (same file as 3568c7259's) prove both
  /v1/chat/completions and /v1/responses surface both tool calls correctly
  from this fixture.

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

* feat(sse): honor tool_choice when translating OpenAI requests to Gemini

Investigating a live report that gemini-3.1-flash-lite frequently narrates
an intended tool call in plain text instead of actually emitting one
(dashboard log id 1784591483850-49c408 — 9 raw provider chunks, all plain
text, zero functionCall parts, clean finishReason STOP): body.tool_choice
was never read anywhere in the OpenAI->Gemini request translator.
result.toolConfig was unconditionally hardcoded to
{ functionCallingConfig: { mode: "VALIDATED" } } whenever tools were
present, regardless of what the caller sent. VALIDATED lets the model
respond with plain text OR a schema-validated function call at its own
discretion — it never forces a call the way OpenAI's tool_choice:
"required" (Gemini's ANY mode) does, so a caller had no way to compel a
tool call even when explicitly requesting one.

Added convertOpenAIToolChoiceToGemini(), mirroring the existing
convertOpenAIToolChoice() in openai-to-claude.ts for the same OpenAI
tool_choice shapes (string "auto"/"none"/"required", or
{type:"function",function:{name}} to force one specific tool):
  - unset/"auto"        -> VALIDATED (unchanged default, no regression)
  - "required"/"any"    -> ANY (forces a call)
  - "none"              -> NONE (disables function calling)
  - {type:"function",...} -> ANY + allowedFunctionNames: [name]

Wired into both Gemini request paths: the direct/base translator
(openaiToGeminiBase) and the Antigravity/Cloud Code envelope
(wrapInCloudCodeEnvelope), which now reuses the base translator's already-
computed toolConfig instead of re-deriving its own hardcoded VALIDATED.

This unblocks (but does not itself resolve) the live question — a
tool_choice: "required" A/B test against gemini-3.1-flash-lite follows to
confirm ANY mode actually changes the narrate-vs-act behavior in practice.

Also updates the T11 any-budget allowlist for this file: the "any" string
comparisons (tool_choice value "any", not a TypeScript type) are the same
documented false-positive pattern already carved out for executors/base.ts.

Tests (TDD, confirmed red-before-green): 9 new unit tests covering all
tool_choice shapes on both the direct and Antigravity/Cloud Code paths,
plus the no-tools and unset-default no-regression cases.

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

* chore(quality): file-size baseline for own-growth (#8211)

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

---------

Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-23 05:03:44 -03:00
Markus Hartung
40eb5a87a7 chore(quality): fix 2 pre-existing lint/suppression drift issues (#8209)
* chore(quality): refresh stale any-suppression count for combo-routing-engine.test.ts

Rebasing onto release/v3.8.49 pulled in upstream's #8008 (prompt-cache
affinity), which added 2 more `any` usages to this test file (269 ->
271). ESLint's suppressions mechanism requires an exact count match —
any drift makes the whole file's suppression stale and reports every
violation as new. Not a violation to fix (pre-existing test-mock any
usage in an upstream commit), just an allowlist count refresh.

Co-Authored-By: Markus Hartung <markus.hartung@gmail.com>

* chore(quality): type the oauth-refresh-dedup test's connection filter instead of any

Upstream #8062 introduced this test file with an untyped `any` filter
callback param, which the strict any-budget lint rule flags as a new
violation (not a pre-existing one to allowlist). Derives the element type
from getProviderConnections' own return type instead of importing/hand-
writing it.

Co-Authored-By: Markus Hartung <markus.hartream@gmail.com>

---------

Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
Co-authored-by: Markus Hartung <markus.hartream@gmail.com>
2026-07-23 05:03:35 -03:00
Markus Hartung
40ee0847d9 feat(sre): add tcp-close-analyzer.py for debugging client-vs-server TCP close order (#8208)
Dependency-free (stdlib-only) libpcap/Ethernet/IPv4/TCP parser that answers
one question: does OmniRoute or the far end (Caddy, on behalf of whichever
client it's proxying) close the TCP connection first? Dashboard-level 499s
only tell us OmniRoute detected a dropped connection, not which side's FIN/
RST actually landed first -- this settles it from the raw packets.

Handles classic Ethernet and both "Linux cooked" linktypes (SLL/SLL2, what
`tcpdump -i any` produces) since rootless Podman has no host-visible bridge
interface to capture on directly -- the capture instructions in the script
document the nsenter-into-container-netns workaround.

Adds --find to grep every reassembled stream for a literal marker string --
in practice the reliable way to locate one specific request (the
x-correlation-id header isn't echoed on every hop) is dropping a fresh UUID
into an actual chat message and searching for it, then cross-referencing
the matched stream's timing against data/call_logs/<date>/*.json.

Co-authored-by: Markus Hartung <markus.hartream@gmail.com>
2026-07-23 05:03:26 -03:00
Roberto
4ecca379fc fix(providers): fix Azure AI Foundry multi-model discovery and per-deployment connection testing (#8174) (#8206)
Co-authored-by: not-knope <185121404+not-knope@users.noreply.github.com>
2026-07-23 05:03:17 -03:00
Chirag Singhal
8565954e65 fix(stream): add logging to empty catch blocks in stream error handling (#8143)
* fix(combo,model-fallback,sqljs): three stream-reliability fixes

- targetExhaustion: skip remaining same-provider models on 401 auth failure
  (prevents opencode-zen noauth cascade wasting retry attempts) (#8133)
- modelFamilyFallback: skip unsupported models in T5 fallback chain
  (prevents GitHub provider trying deprecated claude-opus-4.8/4.7) (#8134)
- sqljsAdapter: split package.json resolve string to suppress Next.js
  Can't resolve warning at build time (#8135)

* fix(stream): add logging to empty catch blocks in stream error handling

- stream.ts: Log errors in onComplete/onFailure callbacks (lines 929, 1112, 2451, 2536, 2561, 2717)
- streamHandler.ts: Log errors in stall watchdog and trackPendingRequest (lines 249, 334, 657, 663, 667)
- cursor.ts: Add comments to intentional H2 lifecycle catches, log KV/exec errors
- next.config.mjs: Externalize sql.js to suppress build warnings

Closes #8138, #8139, #8140, #8141, #8142

* refactor(stream): scope PR to logging hygiene, drop out-of-scope exhaustion/fallback hunks

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

* chore(quality): rebaseline stream.ts for #8143 empty-catch logging own-growth

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

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: rafaumeu <rafael.zendron22@gmail.com>
Co-authored-by: chirag127 <chirag127@users.noreply.github.com>
2026-07-23 04:50:51 -03:00
Ravi Tharuma
4fd1f0f15c fix(guardrails): align INPUT_SANITIZER request masking gate (#8093) (#8124)
Scoped to guardrails/security: dropped the unrelated js-yaml/tar/shell-quote/
brace-expansion override bumps, and isolated sanitizer-residual-policy.test.ts
to a tmp DATA_DIR so it no longer touches the real storage.sqlite.

Co-authored-by: RaviTharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: rafaumeu <rafael.zendron22@gmail.com>
2026-07-23 04:50:43 -03:00
backryun
f664af8f36 feat(github): refresh Copilot model catalog (#8226)
* feat(github): refresh Copilot model catalog

* feat(github): refresh Copilot model catalog (gpt-5.6 family)

Dropped the claude-opus-4.6 reinstatement (contradicts #7223/#2821 with no new
evidence; risks a production 400 on /v1/messages). Kept the gpt-5.6-sol/terra/luna
additions, which already exist on the Codex provider.

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

---------

Co-authored-by: backryun <backryun@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-23 04:34:38 -03:00
backryun
ed8755a6d0 feat(github-models): refresh catalog and compatibility (#8225) 2026-07-23 04:34:30 -03:00
WITALO ROCHA
2357590a62 fix(gemini): strip OpenAI "strict" tool-schema keyword for Antigravity (#7901)
RubyLLM (and other OpenAI-convention clients) embed strict:true/false directly
inside a function tools parameters JSON schema. Gemini/Antigravity rejects the
unrecognized keyword with a 400 ("Unknown name strict ... Cannot find field"),
the same failure class as the existing multipleOf entry. Broke every Chatwit
Captain tool-calling call routed through witdev_antigravity/gemini-*.

Reconstructed onto current release/v3.8.49 tip (preserves #8231 CIVIC_INTEGRITY
exclusion; the author's stale base showed it as a spurious revert).

Co-authored-by: Witroch4 <wital@example.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-23 04:34:09 -03:00
Rafael Dias Zendron
ef708e1236 docs(security): correct prompt-injection severity table + heuristic-limitations disclaimer (#8097) (#8113)
Scoped to the docs-truthfulness fix: reverted the erroneous INPUT_SANITIZER_ENABLED
default flip (flag is intentionally on-by-default per the #8093 ruling) and dropped
5 unrelated bundled changes. Keeps only the accurate SECURITY.md correction plus the
sanitizerFixtures / security-docs-truthfulness test.

Co-authored-by: rafaumeu <rafaumeu@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-23 04:33:48 -03:00
Diego Rodrigues de Sa e Souza
9a3b605f34 feat: classify grok-web Cloudflare anti-bot blocks + gated browser-backed cf_clearance path (#8019) (#8241)
Co-authored-by: Probe Test <probe@example.com>
2026-07-23 01:33:32 -03:00
Diego Rodrigues de Sa e Souza
d2c35e8d58 docs(readme): re-audit numbers, fix table scroll, refresh contributors (#8243)
Numbers sweep (text + SVGs now consistent):
- Routing strategies 18 -> 19 (add cache-optimized row; hero/tier-cascade/
  strategies-grid alts + comparison table)
- Coding agents 26 -> 33 in promise-pillars alt
- "10 engines above" -> 12 (codex-responses)
- Re-render the 5 hand-authored SVGs to match the vetted text: readme-hero
  (268->278, ~1.4B->~1.53B, 18->19), free-tier-budget (~1.4B->~1.53B,
  ~2.0B->~2.15B, 39->43 pools), promise-pillars + cli-terminal (268->278),
  compression-pipeline (11->12 engine cells, re-laid out)

Contributors:
- Headline 350+ -> 500+ (507 real human identities)
- Card commit counts via GitHub contributions API: oyi77 213, JxnLexn 58,
  backryun 53, herjarsa 25; reorder backryun above chirag127 (53 > 46)

Table horizontal-scroll fixes:
- Screenshots: markdown images -> HTML table with width=400 (was unbounded)
- "Every major lab" icons width 98 -> 80; "Free Forever" cards 127 -> 104
- MCP/A2A endpoints: full URLs -> paths (host stated once above)
- Trim over-long Headroom cell; comparison "Routing strategies" cell
- Video thumbs 280 -> 264

Verbosity/naturalness:
- Dedup strategies-grid alt (was relisting the table above)
- Trim Quota-Share What's-New bullet; fix negative-parallelism in CLI intro

check:docs-counts STRICT green; docs-sync PASS; all SVGs well-formed.

Co-authored-by: Probe Test <probe@example.com>
2026-07-23 01:33:22 -03:00
Diego Rodrigues de Sa e Souza
08129cfb0c fix(ci): merge-train --fast mirrors test:unit subdir allowlist (#7688)
The fast bucket fed every changed tests/unit file to node:test; vitest-only
subdirs (autoCombo) always fail under that runner and redden the train. The
classifier now carries the same {api,auth,…} allowlist as package.json's
test:unit (guarded by a sync test) and stops excluding ui/*.test.ts, which
test:unit does run.
2026-07-23 01:33:13 -03:00
Diego Rodrigues de Sa e Souza
5fdbd7f326 chore: add K3banner-1.png banner asset (#8242)
Co-authored-by: Probe Test <probe@example.com>
2026-07-23 01:24:06 -03:00
Diego Rodrigues de Sa e Souza
3f8280b85f fix(providers): filter unsupported family-fallback candidates against the provider catalog (#8134) (#8240)
Co-authored-by: Probe Test <probe@example.com>
2026-07-23 00:46:05 -03:00
Diego Rodrigues de Sa e Souza
a37a2fe8c3 fix(backend): word-boundary-safe tool-result truncation in lite compression mode (#8169) (#8239)
Co-authored-by: Probe Test <probe@example.com>
2026-07-23 00:46:01 -03:00
Diego Rodrigues de Sa e Souza
917314156e fix(gemini): drop HARM_CATEGORY_CIVIC_INTEGRITY from the default Gemini safety settings (#8231) (#8238)
Co-authored-by: Probe Test <probe@example.com>
2026-07-23 00:45:56 -03:00
Diego Rodrigues de Sa e Souza
b0704752d9 fix(api): narrow claudeClassifierCompat auto trigger so stop_sequences alone no longer short-circuits (#8189) (#8236)
Co-authored-by: Probe Test <probe@example.com>
2026-07-23 00:45:52 -03:00
Diego Rodrigues de Sa e Souza
38fd4d34d9 feat(sse): restrict auto-combo no-auth pool to allowlist (opencode, felo) + docs (#8183)
Restrict the auto-combo no-auth (keyless) candidate pool to an allowlist —
opencode + felo — the only keyless backends verified to answer without any
credential on the reference egress (VPS .15). Excluded no-auth providers stay
usable via direct <alias>/<model> calls; they are just no longer auto-routed to.
Guard: tests/unit/noauth-autocombo-allowlist.test.ts.

Docs: add a "works the second you install it" free section near the top of the
README; sync the compression stack count 11 → 12 engines; document 13 env vars
(VNC browser-login knobs + VIBEPROXY_DATA_DIR) in .env.example / ENVIRONMENT.md.
2026-07-23 00:31:15 -03:00
adevwithpurpose
2d4db0157e fix(providers): discover live AGY models (#8123)
Live AGY model discovery (isDiscoverableAgyModelId + filterUserCallableAntigravityModels)
composing with the #8013 antigravity discovery rewrite. Reconstructed onto the current
release tip (branch was ~977 commits behind); updated the test to the renamed version-cache
API (seedAntigravityVersionCache -> seedAntigravityIde/CliVersionCache) after the fusion.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-22 20:47:56 -03:00
backryun
888c872459 refactor(antigravity): align official clients and callable catalog (#8013)
* fix(antigravity): preserve protocol fidelity and fail closed

* chore: add PR-numbered changelog fragment

* test: split oversized Antigravity suites

* refactor(antigravity): align official IDE and CLI identities

* fix(antigravity): align catalog with callable models

* test(antigravity): update 2 test files to renamed version-cache API (#8013 fix)

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

---------

Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com>
Co-authored-by: backryun <backryun@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: nguyenha935 <nguyenha935@users.noreply.github.com>
Co-authored-by: Probe Test <probe@example.com>
2026-07-22 20:41:19 -03:00
diegosouzapw
c2acaf287b refactor(compression): extract resolveHeadroomDetail to keep dispatchCompression under the complexity gate (#8058) 2026-07-22 20:29:21 -03:00
Andrew B.
53f435da8d fix(antigravity): scope 404 model-not-found lockout to exact model + bare-model autopick (#8050)
Scopes the Antigravity 404 model-not-found lockout to the exact model (not the whole
family) so one missing bare model no longer hijacks the family cooldown, plus bare-model
autopick via resolveModelByProviderInference dedup. The thinking-signature-recovery portion
was dropped — #7899 is already fixed and merged on the release via #7906.

Co-authored-by: AndrianBalanescu <AndrianBalanescu@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-22 20:25:37 -03:00
Austin Liu
07dada6b81 fix: strip internal reasoning placeholder from user-visible content (#8081) (#8162)
* fix: strip internal reasoning placeholder from user-visible content (#8081)

The internal reasoning replay sentinel '(prior reasoning summary unavailable)'
can leak into user-visible assistant content when a model echoes it through
ordinary message.content / delta.content. Existing suppression only checked
reasoning_content fields and reasoning-specific events.

Changes:
- Add stripInternalReasoningPlaceholder() to reasoningPlaceholder.ts —
  removes all occurrences of the sentinel and trims; returns '' when
  nothing meaningful remains
- Streaming: strip in responsesTransformer.ts, openai-responses.ts, and
  openai-to-claude.ts at the delta.content entry point; skip emission
  entirely when only the placeholder was present
- Non-streaming: strip in responseSanitizer.ts sanitizeMessageContent()
  and sanitizeResponsesMessageContent() (all three text paths)

translateText is unaffected (uses mode='translate' via plain newsClient).
The per-provider reasoning_content check remains as defense-in-depth.

* fix: skip only empty content block on reasoning-placeholder, keep finish_reason/tool_calls (#8081)

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

* chore(quality): rebaseline openai-responses.ts own-growth (#8081 guard)

---------

Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local>
Co-authored-by: Probe Test <probe@example.com>
Co-authored-by: Dingding-leo <Dingding-leo@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-22 19:04:51 -03:00
Ravi Tharuma
066e9275c4 fix(models): drop generic catalog siblings of specialty surfaces (#8015) (#8021)
Final catalog dedupe pass drops a generic/untyped chat-like sibling row
when a typed non-chat specialty row (audio/video/moderation/...) exists
for the same public id — closing the #4424 follow-up (whisper-1, tts-1,
omni-moderation-latest, elevenlabs/*, veo-free/*). Pure, I/O-free,
order-preserving; also removes a stray raw NUL byte that was embedded in
the dedupe key template literal (which made git render the file binary).

Restores test coverage for relative-order preservation across distinct
ids and for two distinct id-less entries never being grouped, keeping the
suite's assertion count at parity with the pre-fix baseline.

Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
2026-07-22 19:00:05 -03:00
Austin Liu
5660bdefbd fix(windows): add windowsHide to all child process spawns (#8131) (#8167)
* fix(windows): add windowsHide to all child process spawns (#8131)

On Windows, child processes spawned without windowsHide: true cause
transient conhost.exe/cmd console windows to flash open. Audited all
spawn/exec/execFile/execSync/execFileSync call sites and added
windowsHide: true where missing.

Files patched:
- src/mitm/manager.ts (MITM server spawn)
- src/mitm/systemCommands.ts (sudo/system command spawn)
- src/mitm/inspector/systemProxyConfig.ts (execFile wrapper)
- src/shared/services/cliRuntime.ts (CLI spawn + npm execFileSync)
- src/lib/plugins/loader.ts (plugin host spawn)
- src/lib/providerModels/cursorAgent.ts (cursor binary spawn)
- src/lib/cloudflaredTunnel.ts (cloudflared spawn)

Unix-only call sites (shell: /bin/bash, which) are unaffected.
electron/main.js already had windowsHide: true.

* fix(windows): cover remaining spawn sites missed by #8131 windowsHide sweep

Extends the #8131 windowsHide audit to the three call sites the original
sweep missed: ServiceSupervisor.start() and processManager.startProcess()
(both spawn() embedded-service child processes), and
installers/utils.ts::buildNpmExecOptions() (the execFile() options runNpm()
uses to install services). All three now always set windowsHide: true so
no transient conhost.exe/cmd console window flashes open on Windows.

The two spawn() options objects are factored into small, pure, exported
builder functions (buildServiceSpawnOptions, buildCliproxyapiSpawnOptions)
so the regression test can assert on the constructed options directly,
since both call sites use a bare named `import { spawn } from
"node:child_process"` that ESM live-binding semantics make unmockable
without --experimental-test-module-mocks (not currently enabled repo-wide).

Bumps config/quality/file-size-baseline.json for cloudflaredTunnel.ts
934->935 (the PR's own +1 windowsHide line at the existing spawn options
object).

Co-Authored-By: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local>
Co-authored-by: Probe Test <probe@example.com>
Co-authored-by: Dingding-leo <Dingding-leo@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-22 18:08:15 -03:00
Austin Liu
13a7a32ca2 fix(combos): expose synced reasoning-effort variants in Combo Builder model picker (#8072) (#8165)
* fix(combos): expose synced reasoning-effort variants in Combo Builder model picker (#8072)

Synced reasoning-effort aliases (e.g. GLM-5.2-high, GLM-5.2-medium)
appear in the catalog and Playground but were missing from the Combo
Builder's inline model picker. buildModelOptions() added base synced
records but never ran appendSyncedEffortVariants().

Convert synced models with non-empty supportedThinkingEfforts into
catalog-shaped entries, run the shared appendSyncedEffortVariants
utility (preserving its effort normalization, provider exclusions,
suffix-collision handling, and naming behavior), and add any new
variant ids to the builder model map. Variants inherit the base
model's endpoints, context length, output limit, and thinking support.

* fix(combos): correct baseId derivation for synced effort variants (#8072)

appendSyncedEffortVariants sets a variant's own root field to
${baseRoot}-${tier} (still tier-suffixed), not the true base model id.
buildModelOptions() was deriving baseId from variant.root, so the lookup
into modelMap never matched and every <model>-<tier> variant silently
fell back to bare defaults instead of inheriting contextLength,
outputTokenLimit, supportedEndpoints, and supportsThinking from its
base model.

Track each variant's true base raw id directly while iterating tiers
during catalogShaped construction instead of re-deriving it from
variant.root.

Adds a regression test seeding a synced model with
supportedThinkingEfforts via replaceSyncedAvailableModelsForConnection
and asserting the resulting <model>-<tier> variants both appear and
inherit the base entry's metadata through getComboBuilderOptions().

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

---------

Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local>
Co-authored-by: Probe Test <probe@example.com>
Co-authored-by: Dingding-leo <Dingding-leo@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-22 18:08:08 -03:00
Ravi Tharuma
e7f965c9cc fix(compression): persist Headroom minRows (set 5 and reload keeps 5) (#8058)
* fix(compression): persist Headroom minRows (set 5 and reload keeps 5)

Fixes diegosouzapw/OmniRoute#8056.

Headroom detail settings had a Save-looking form but EngineConfigPage only
persisted aggressive/ultra via SETTINGS_SUBOBJECT, so minRows always reseeded
to the schema default (8) after reload.

- Add HeadroomConfig + DEFAULT_HEADROOM_CONFIG (minRows: 8)
- Accept headroom in compressionSettingsUpdateSchema (minRows 2..10000)
- Normalize/store headroom in get/updateCompressionSettings
- Register headroom in EngineConfigPage SETTINGS_SUBOBJECT so Save works
- Merge settings.headroom into stacked stepConfig for runtime apply
- Thread minRows through preview API + EngineConfigPage preview payload
- Tests: schema/DB round-trip, engine apply, stacked merge, UI Save→PUT 5

* chore(quality): rebaseline compression.ts + strategySelector.ts own-growth (#8056 headroom minRows)

---------

Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
2026-07-22 17:44:20 -03:00
RCrushMe
89bad0fa52 fix(responses): close namespace round-trip for Responses-Chat translation (#7936) (#8151)
* fix(responses): close namespace round-trip for Responses-Chat translation (#7936)

The #7905 custom-tool-call path landed in release/v3.8.49 but left #7936
open: Responses namespace sub-tools were flattened to a bare leaf on the
Chat wire with no response-side closure, so Codex's adjudicator rejected
every namespace sub-tool call with `unsupported call` -- it only has a
dispatch entry for the header bits (namespace+name), no entry for the
bare leaf.

This patch closes the round-trip without mutating the Chat wire name
(alignment with #7905's bare-leaf contract and with the issue author's
proposed fix):

* request side (openai-responses.ts): keep tool.function.name as the bare
  leaf, populate a side-band namespaceToolIdentityMap keyed on that leaf,
  and thread it through translatedBody._toolNameMap.
* request -> response seam (chatCore.ts): extract the identity map before
  dispatch and pass it through to the non-stream completion path and to
  all three stream pipelines (translate openai-responses, translate
  other, passthrough).
* response translator (response/openai-responses.ts): in emitToolCall
  (response.output_item.added) and closeToolCall (custom_tool_call /
  function_call output_item.done), call resolveRequestToolIdentity() to
  rewrite the bare leaf back to {namespace,name} and emit codex-compatible
  independent fields.
* passthrough (utils/stream.ts): add a response passthrough rewriter
  restoreResponsesPassthroughFunctionCallIdentity that intercepts
  response.output_item.added, response.output_item.done, and
  response.completed and stamps the same {namespace,name} tuple.
* helper (requestToolIdentity.ts): a 20-line stateless resolver; never
  parses a name.

The wire-visible Chat tool.function.name stays the bare leaf -- non-OpenAI
providers (NVIDIA, GLM, Kimi, Gemini, ...) frequently truncate or rewrite
long __-dotted names; bare leaves avoid that failure mode entirely. The
codex ResponseItem::FunctionCall schema (models.rs) declares an
independent namespace: Option<String> field and has a
function_call_deserializes_optional_namespace round-trip test, so
emitting it separately matches the codex adjudicator dispatch.

Includes 19 new test cases across 4 files:
- request-side bare-leaf wire + side-band ledger construction
- response-side tuple emit + unmapped passthrough + apply_patch exclusion
- ambiguous-leaf collision safety (entry dropped, leaf emits verbatim)
- per-request isolation between concurrent streams
- a precompiled Atlassian-style nested namespace override

* fix(responses): skip tool_search_call input items instead of 400 (#7936 addendum)

Codex 0.42+ emits `tool_search_call` (and later `tool_search_result`) input
items when the model uses the dynamic tool-search optimization. They are
metadata-only: they record that the model queried a subset of the
available tools, and carry nothing that OpenAI Chat Completions can
represent.

Without an explicit skip in openai-responses.ts, the input loop threw
Unsupported Responses API feature: input item type 'tool_search_call'
cannot be represented in Chat Completions -- and because these items
stay in the Responses API `input` for every follow-up turn, the whole
server returned 400 on EVERY subsequent /v1/responses in the same
session until the user cleared history.

Observed in the wild:
  /v1/responses 400 "Unsupported Responses API feature: input item
  type 'tool_search_call' cannot be represented in Chat Completions
  [longcat/LongCat-2.0 (400), longcat/LongCat-2.0 (400)]"

The meituan combo (longcat fallback) was the most visible victim, but
the underlying throw is source-format-side and hits any Responses-API
consumer whose upstream does not natively support Responses.

Fix: stop on the item type the same way `reasoning` is skipped --
display-only metadata, no chat side-effect. Covers both
`tool_search_call` and the follow-up `tool_search_result` shapes.

Adds 3 unit tests:
  - tool_search_call is silently skipped (no 400)
  - tool_search_result is silently skipped
  - tool_search_call items interspersed with real messages are skipped
    in order; real messages survive

* chore(quality): rebaseline openai-responses.ts + stream.ts own-growth (#7936 namespace round-trip)

---------

Co-authored-by: TonPro <hello@tonpro.fu>
Co-authored-by: RCrushMe <RCrushMe@users.noreply.github.com>
2026-07-22 17:38:30 -03:00
Ravi Tharuma
7ae17168db fix(security): decouple request PII redaction from injection mode (#8102)
* fix(security): decouple request PII redaction from injection mode

PII_REDACTION_ENABLED now rewrites request PII independently of
INPUT_SANITIZER_MODE, so the enterprise recipe (MODE=block + PII on)
actually redacts. Also cover Responses API string input/prompt shapes
and correct docs that claimed MODE=redact strips injection text.

Refs: #8092 #8093 #8094 #8096 #8097

* test(security): drop no-explicit-any in sanitizer unit tests

Unblocks CI lint/quality ratchet on the PII redaction PR by typing
chat-like payloads instead of casting to any.

---------

Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
2026-07-22 17:38:20 -03:00
Makcim Ivanov
ffb37f99a5 fix(cursor): bridge native tools to client calls (#8171)
Co-authored-by: Makcim Ivanov <10184529+makcimbx@users.noreply.github.com>
2026-07-22 17:38:10 -03:00
Rafael Dias Zendron
e31c4d31e2 fix: classify Google quota exhaustion responses (#8071)
Recognize Google RESOURCE_EXHAUSTED responses that include a billing-period reset window while preserving transient rate-limit classification for generic exhaustion messages.

Fixes #8060
2026-07-22 17:38:02 -03:00
Innokentiy Solntsev
f17d23bf0b fix(oauth): honor connectionId on token refresh so email-less providers don't duplicate (#8062)
persistOAuthConnection gated its whole dedup step behind if(tokenData.email).
The matcher (findExistingOAuthConnectionMatch) already matches by explicit
connectionId first, but it was never reached when the payload had no top-level
email. GitHub Copilot's device-code flow keeps identity under
providerSpecificData.githubEmail, so tokenData.email is undefined — a refresh
(which passes the existing connectionId) skipped the match and fell through to
createProviderConnection, producing a duplicate connection.

- Widen the gate to if(connectionId || tokenData.email) so an explicit
  connectionId is honored regardless of email.
- Guard the matcher's email branch with if(!tokenData.email) return false, so a
  widened gate can't false-match an email-less connection via
  safeEqual(undefined, undefined).

Fixes #8059.
2026-07-22 17:37:54 -03:00
Innokentiy Solntsev
1a076464f0 feat(dashboard): make Codex quota card windows reflect reality (#8054)
Two accuracy problems in buildCodexUsageQuotas (open-sse/services/codexUsageQuotas.ts):

1. ChatGPT Codex's /wham/usage advertises a latent per-feature ceiling for the
   spark feature (metered_feature codex_bengalfox) to accounts by default. A
   never-used bucket is unanchored (used_percent 0, reset_after_seconds ==
   limit_window_seconds), so it recomputes its reset as now + full_window on
   every fetch and was rendered as a permanent GPT-5.3-Codex-Spark row at 100%
   for a model the operator never used. Skip latent windows (isLatentWindow);
   they reappear once the feature is actually used. The label now comes from the
   payload's own limit_name, falling back to the constant.

2. primary_window/secondary_window were labeled session/weekly purely by
   position, ignoring limit_window_seconds, so a 7-day primary_window showed
   'Session'. The session/weekly keys (routing semantics) stay unchanged; only
   the display label is corrected from the real window duration
   (windowDurationLabel), so a 7-day window shows 'Weekly'.

Fixes #8051.
2026-07-22 17:37:46 -03:00
TrackCrewGalore
b8ec0aa218 fix(providers): refresh Baidu ERNIE and Qianfan website URLs (#6271) (#8128)
Point dashboard provider cards at current Baidu developer landings instead of the deprecated yiyan nag page and the 301ing wenxinworkshop path.

Co-authored-by: LandLord64 <ulofeuduokhai@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 17:37:38 -03:00
Ulofe Uduokhai
f1d38527ce docs(ops): publish public branching and release model (#7627) (#8129)
Explain release/* as the active cycle, main as the published line, and tags as ship markers so contributors know where to aim PRs.

Co-authored-by: Ulofe Uduokhai <179733406+c4usal@users.noreply.github.com>
2026-07-22 17:37:30 -03:00
Lenine Júnior
24634bfb65 docs: add AgentRouter multi-provider routing troubleshooting (#8049)
Document the failure mode where a leftover hand-made anthropic-compatible /
openai-compatible-chat provider owns the agentrouter/<model> IDs (or is
referenced by a combo), so requests route to it instead of the built-in
agentrouter provider and get rejected with 'unauthorized client detected' or
an HTML error page. Point users at the ROUTING log line to diagnose and steer
them back to the native provider.
2026-07-22 17:37:22 -03:00
Diego Rodrigues de Sa e Souza
dbdc7daade feat(compression): per-model/endpoint compression exclusion filter (#8034) (#8064)
* feat(compression): per-model/endpoint compression exclusion filter (#8034)

* chore(quality): rebaseline compression.ts own-growth 845->850 (#8034 exclusions persistence)

---------

Co-authored-by: Probe Test <probe@example.com>
2026-07-22 16:15:57 -03:00
Diego Rodrigues de Sa e Souza
f23d7770ec feat: zh-CN terminology glossary + consistency gate + normalization pass (#8038) (#8166)
One-shot 提供商->提供者 normalization across src/i18n/messages/zh-CN.json
(679 substitutions) and bin/cli/locales/zh-CN.json (54), mirroring #8024's
zh-TW pass. Adds a versioned terminology glossary
(scripts/i18n/glossary/zh-CN.json), a protected-names list
(scripts/i18n/glossary/protected-terms.json), and a pure-function
consistency check (scripts/i18n/check-glossary-consistency.mjs,
npm run i18n:check-glossary) wired into CI as the i18n-glossary-zhcn job.
zh-CN added to the visual-QA harness default locales. Complements the
existing parity (check-ui-keys-coverage.mjs) and ICU (validate_translation.py)
gates without replacing them.
2026-07-22 15:54:26 -03:00
Diego Rodrigues de Sa e Souza
e392a39047 feat: native Fish Audio TTS provider on /v1/audio/speech (#8099) (#8164) 2026-07-22 15:54:01 -03:00
Diego Rodrigues de Sa e Souza
0f2d9abba7 feat(compression): teach the model the CCR retrieve protocol on first marker (#8033) (#8063) 2026-07-22 15:53:22 -03:00
Diego Rodrigues de Sa e Souza
910463a0f3 fix(security): bound JWT-extraction regexes to prevent polynomial ReDoS (CodeQL #754/#755/#756) (#8173) 2026-07-22 12:25:38 -03:00
Diego Rodrigues de Sa e Souza
40bd70c9cd fix(cli): surface the real spawn error in process supervisor (#8091) (#8158) 2026-07-22 11:28:20 -03:00
Diego Rodrigues de Sa e Souza
9d0bdb871d fix(sse): stop Codex/Responses sanitizer turning system image_url into output_text (#8089) (#8147) 2026-07-22 11:28:16 -03:00
Diego Rodrigues de Sa e Souza
e6087c62b2 fix(sse): anonymous fingerprint fallback for keyless Pollinations image gen (#8085) (#8157)
Pollinations image requests with no configured apiKey/accessToken (the
common free case) were sent with no Authorization header AND no
fingerprint headers, so Pollinations' own upstream legitimately
rejected them with a real 401 even for a valid OmniRoute key. The
chat path already has an anonymous fingerprint-pool fallback
(PollinationsExecutor.execute()'s isAnonymous branch); the image
path never reused it.

Adds open-sse/handlers/imageGeneration/pollinationsAnonAuth.ts,
mirroring the chat executor's anonymous session-pool fallback for
handleOpenAIImageGeneration, and fixes the pre-existing bug where
Authorization was set to the literal string "Bearer undefined"
when no token was configured (now correctly gated by if (token)).

Regression test: tests/unit/pollinations-image-anon-fallback-8085.test.ts
2026-07-22 11:28:12 -03:00
Diego Rodrigues de Sa e Souza
c252c9d885 fix(providers): add missing poe registry baseUrl entry (#8082) (#8149)
* fix(providers): add missing poe registry baseUrl entry (#8082)

The built-in poe provider (passthroughModels:true, NAMED_OPENAI_STYLE_PROVIDERS)
had no open-sse/config/providers/ REGISTRY entry, so model discovery's
getRegistryEntry("poe")?.baseUrl resolved to undefined and GET
/api/providers/[id]/models always failed with {"error":"No base URL
configured for provider"} even though credentials and inference worked fine
(the validation/inference path already had a hardcoded https://api.poe.com/v1
fallback). Adds a real REGISTRY entry mirroring moonshot/byteplus, and points
the audioMiscProviders.ts hardcoded fallback at the same POE_DEFAULT_BASE_URL
constant so both paths agree going forward.

* test: regenerate provider translate-path golden for poe (#8082)
2026-07-22 11:28:07 -03:00
Diego Rodrigues de Sa e Souza
1503044055 fix(routing): anchor quota cache on globalThis for cross-chunk consistency (#8065) (#8150)
src/domain/quotaCache.ts kept its quota state (cache Map, refreshingSet,
refreshTimer, tickRunning) in bare module-scope variables. In a Next.js 16
`output: "standalone"` build, code reachable only from instrumentation-node.ts
(providerLimitsSyncScheduler's write path) and code reachable from an
API-route/SSE-handler chunk (auth.ts::evaluateQuotaLimitPolicy()'s read path)
can be compiled into separate server chunks, each independently instantiating
this module's top-level state. A quota renewal written by the sync scheduler
was invisible to the routing read path, leaving accounts stuck exhausted until
a full process restart.

Anchors all quota-cache state on a single globalThis-held object, following
the same pattern already used in src/lib/credentialHealth/cache.ts and
src/lib/db/core.ts, and the identical fix already shipped for this exact
failure mode in src/lib/pricingSync.ts (#6325 / commit de9d748dac).

Regression test: tests/unit/repro-8065-quota-cache-cross-instance.test.ts
imports the module twice under distinct query-string specifiers to force two
separate module instances, proving a write from one instance is now visible
to a read from the other.
2026-07-22 11:28:03 -03:00
Diego Rodrigues de Sa e Souza
98b1aa34b5 fix(sse): run compression pipeline per turn in Codex Responses WS bridge (#8052) (#8154)
The Codex Responses-over-WebSocket bridge bypassed the whole prompt-compression
pipeline (and its analytics writes) that the HTTP/SSE path (chatCore.ts) runs on
every request, via two gaps:

1. prepare() in codex-responses-ws/route.ts never called anything from
   open-sse/services/compression/* — it authenticated, injected memory, applied
   reasoning-routing, then went straight to executor.transformRequest().
2. scripts/dev/responses-ws-proxy.mjs memoized the upstream connection in
   ensureUpstream() and only called the internal "prepare" action on the FIRST
   response.create of a WS session — every subsequent turn on a reused
   connection bypassed prepare() (and therefore compression) entirely.

Fix: a new compression.ts module wires the core compression pipeline (settings
resolution -> selectCompressionStrategy -> applyCompressionAsync ->
compression_analytics/compression_engine_breakdown writes, reusing
adaptBodyForCompression's existing Responses-API input[] adapter) into
prepare(); responses-ws-proxy.mjs now re-runs prepare() (via a new shared
runPrepare() helper) for every logical response.create turn on a reused
connection, not just the first, without recreating the upstream socket.

Regression test: tests/unit/responses-ws-proxy-compression-parity.test.ts
proves the reused-connection bypass by execution (RED: 1 prepare call for 2
turns; GREEN after the fix: 2 prepare calls for 2 turns).
2026-07-22 11:27:57 -03:00
Diego Rodrigues de Sa e Souza
b954a3a60f fix(oauth): warn instead of silently opening unreachable localhost redirect for LAN-IP Codex/xAI/Grok OAuth (#8046) (#8152) 2026-07-22 11:27:53 -03:00