Six declarations spell a byte buffer as bare `Buffer` / `Uint8Array`. Without
its type argument that widens to `ArrayBufferLike`, which also admits
`SharedArrayBuffer` — so the value is rejected at every Web API boundary it is
actually passed to: `BodyInit` for `new Response(...)` and `BlobPart` for
`new Blob([...])`.
Every one of them is already ArrayBuffer-backed at runtime. `hexToBytes()`
allocates with `new Uint8Array(len)`; `synthesizeGtts()` and `pcmToWav()` return
`Buffer.concat(...)`; `fetchRemoteImage()` returns
`Buffer.from(await response.arrayBuffer())`; `readPageResponseBody()` returns
`Buffer.from(body)`, which copies. The declarations were simply less specific
than the values, so this states what the code already guarantees.
Same fix#8533 applied to the multipart and gRPC-web bodies.
Fixes 5 of the 208 `tsc -p open-sse/tsconfig.json` diagnostics with no new ones:
3 in audioSpeech.ts (MiniMax hex, gTTS, Vertex Gemini TTS), 1 in
imageGeneration.ts (Topaz Blob upload) and 1 in browserBackedChat.ts.
Refs #8484
Sort static fitness table patterns by descending length before matching. This prevents shorter substrings like 'gpt-4o' from incorrectly matching longer model IDs like 'gpt-4o-mini' when 'gpt-4o' happens to be listed earlier in JavaScript Object key iteration order.
Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
When looking up models_dev_tier or capability scores for -free alias models (e.g. deepseek-v4-flash-free), strip the -free suffix to query the underlying model's intelligence data if direct lookup yields null.
Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
`kieExecutor.createTask()` returns `JsonObject` (`Record<string, unknown>`), so
`createData.data` is `unknown` and the `createData?.data?.taskId` read that
image, video and music generation each duplicated could not compile. The same
three-line expression appeared verbatim in all three handlers.
`open-sse/utils/kieTask.ts` already holds two helpers with exactly this shape —
`normalizeKieTaskState()` and `parseKieResultJson()` both take `unknown`, guard
with `isJsonObject()` and return a declared type. `getKieTaskId()` follows them,
so the three handlers now share one guarded read instead of three unguarded ones.
`getKieCallbackUrl()` took `KieCallbackBody`, a weak type (all properties
optional). Passing a request body whose declared keys are `prompt` /
`timeout_ms` / `poll_interval_ms` tripped TS2559 "no properties in common" at
both music call sites. It receives arbitrary upstream request bodies, so it now
takes `unknown` and guards the same way its neighbours do; `KieCallbackBody`
had no other reference and is gone.
Behaviour is unchanged. `isJsonObject()` rejects arrays and null exactly where
optional chaining already yielded `undefined`, and the callers' `String(taskId)`
coercion moved inside the helper, so a numeric id still reaches `pollTask()` as
a string and a falsy id still takes the 502 branch.
Fixes 5 of the 208 `tsc -p open-sse/tsconfig.json` diagnostics with no new ones:
3 x TS2339 `taskId` on `unknown`, 2 x TS2559 on `KieCallbackBody`.
Refs #8484
Consolidates PR #8660 (matchesAnyToken with full-match priority over
token-level OR fallback) with the existing Turkish search normalization
shipped on release/v3.8.49.
The PR adds a new matchesAnyToken helper used by the providers search
to allow space-separated queries (e.g. 'pollinations sambanova') to
match when any of the tokens is present. Full query match takes
priority so that an exact 'pollinations sambanova' query still hits
even if a partial token would have been ambiguous.
The unit test file gains 12 PR-side matchesAnyToken tests on top of
the 6 release-side Turkish-normalization tests covering the same
function, totaling 27 tests.
Co-authored-by: maxmad64bis <maxmad64bis@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
buildUnifiedModelsResponseCore builds the entire catalog first — every provider's
models, the auto/* combos with their candidate scoring, the embedding/image/rerank/
audio/moderation/video/music registries, the OpenRouter catalog, custom models —
and only at the very end, once the caller turns out to be scoped to a quota pool
(allowedQuotas non-empty), discards all of it and returns the pool's qtSd/* combos
instead.
Measured on a 1 vCPU shared-quota host: ~1.2s of CPU per cold build to return 10
models, and 4.4-7.1s under contention. Claude Code's gateway model discovery aborts
at 3s, so on a busy host the discovery silently falls back.
Everything the quota path needs (`combos`, `timestamp`, `buildComboCatalogMetadata`)
already exists before the expensive loops start, so resolve the key metadata there
and return early.
Extracts two helpers into ./catalogResponse so the short-circuit and the full build
share one implementation instead of two copies that drift:
- applyCatalogPostFilters — the chain that runs AFTER the API-key filter
(configuredOnly, claude effort variants, no-thinking variants, cc-discovery
mirrors, synced effort variants, dedupe)
- finalizeCatalogResponse — enrichment + the codex `models: []` compatibility
field + the response envelope
That chain is not optional for the quota path: the cc-discovery mirrors are exactly
what lets Claude Code see a quota pool's models, and a first draft of this change
dropped them by returning before it. The regression is now pinned by a test that
was verified to fail without the call.
catalog.ts 1615 -> 1480 lines.
TDD: tests/unit/quota-exclusive-catalog-short-circuit.test.ts uses the OpenRouter
catalog fetch as the observable — it is part of the full build and reaches the
network, so a request that performs it did the whole build. The quota request runs
first, while OpenRouter's 24h cache is still cold, otherwise a cached second call
would make the assertion vacuous. Red before (1 fetch), green after (0), and a
normal key still fetches.
Behaviour guards green: quota-exclusive-catalog-4806 (2), quota-key-models-route
(18), cc-discovery-aliases-catalog (9), catalog-helpers-extraction (12),
cc-compatible-model-catalog (1), instrumentation-warm-catalog-cache (3),
catalog-pricing-surface-8018 (3), apikeypolicy-quota-only (6). typecheck, lint and
the file-size gate clean.
Two defects shipped with #8770, both red on release/v3.8.49.
1. `OMNIROUTE_EXTRA_MIGRATIONS_DIRS` was documented in ENVIRONMENT.md but never
added to .env.example, so the env/docs contract gate and its two tests
(check-env-doc-sync, issue-7793-env-doc-sync-repro) failed. Added, with the
same explanation the docs carry.
2. tests/unit/db-migration-runner-extra-dirs.test.ts passed locally and failed on
CI, for two reasons that only appear under the CI invocation:
- It re-imported migrationRunner.ts under a cache-busting query string to pick
up a fresh `MIGRATIONS_DIR` per test. That is not reliable under the loader
chain CI uses (`--import tsx/esm --import setupPolyfill --import
isolateDataDir`): the second test got a runner still pointing at the REAL
migrations directory and tried to apply migration 127 to an empty in-memory
DB. The core directory is now fixed once, before the first import; only the
extra directories vary per test, and those are resolved at call time by
design.
- Tests were registered with top-level `await`. With synchronous bodies that
drains the event loop between tests and `--test-force-exit` — used by every
CI test script — cancels the remainder of the file.
Both constraints are now written down in the file header so the next edit does
not reintroduce them.
Validated with the exact CI invocation, not the bare runner: 11/11, 0 cancelled.
Neighbouring suites green under the same flags: db-migration-runner (26),
check-migration-numbering (15), db-migrationrunner-constants-split (7),
db-migration-version-uniqueness (2), check-env-doc-sync (13),
issue-7793-env-doc-sync-repro (1).
The runner reads exactly one directory and records the bare numeric prefix as the
version, so the numeric slots are a single global namespace. Any distribution that
ships its own migrations next to the upstream set has to draw from that same range
while upstream keeps appending to it — and when both sides claim a number, the
runner records one name for it and treats the other as already applied. That
migration then never runs, silently, on every already-provisioned database.
OMNIROUTE_EXTRA_MIGRATIONS_DIRS registers additional directories as
`namespace=dir` entries separated by the platform path delimiter. Files found
there are recorded as `<namespace>-<number>` (e.g. `ee-134`), a version space that
cannot collide with the upstream numeric one, and they are applied after the core
set. Unset — the default, and the only case for a plain install — nothing changes:
no filesystem access, identical behaviour.
Misconfiguration throws instead of being skipped. A malformed entry, a namespace
outside [a-z][a-z0-9]*, a duplicate namespace, or a directory that does not exist
aborts startup, because silently missing schema is the exact failure this exists to
prevent. Two files sharing a number inside the SAME namespace still collide and
throw, mirroring the runner's own guard.
Also fixes an inconsistency the tests surfaced: a missing core directory returned
early and took the extra directories down with it. They are an independent set.
The version-namespaced strings need no further plumbing — the applied set, the gap
reconciliation and the name-mismatch check all key on the version string, and the
numeric-only paths (`Number.parseInt`, the "032"/"041"/"042" special cases,
`isSchemaAlreadyApplied`) ignore them by construction.
11 new tests; the 7 neighbouring migration suites stay green (62 tests).
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
The discovery-alias info button explains the gate and links to the flag, but the
operator still had to assemble the Claude Code config by hand from the guide.
Render it instead, on the same Claude tool card, from the base URL the card has
already resolved (custom override included, normalized — no /v1, no trailing
slash), with a copy button.
The key slot holds a placeholder, never the real key: this card renders before a
key is necessarily selected, and a real key in copyable text is an easy way to
leak one into a screenshot or a pasted snippet.
buildClaudeDiscoverySettingsSnippet is a pure builder in claudeCliConfig so the
shape is unit-tested (7 cases, including that no `sk-` ever reaches the output
and that an invalid CLAUDE_CODE_AUTO_COMPACT_WINDOW is dropped rather than
emitted). Guide gains the matching section; the five new strings are translated
for pt-BR and vi (the locales with strict parity/marker tests) and marked in the
rest, per the repo's convention.
* feat(db): cc discovery alias gate storage + EXPOSE_CC_DISCOVERY_ALIASES flag
Adds the gate for claude/<provider>/<model> discovery-alias mirror ids on
the /v1/models catalog: a new runtime feature flag (env forces on and wins
over the dashboard DB override), per-provider and per-model "on"/"off"/null
overrides stored in key_value under the ccDiscoveryAliases namespace, and a
pure precedence resolver (model > provider > global). Catalog wiring is a
separate follow-up task; this only lands the gate + storage.
* feat(sse): synthesize claude/ discovery aliases for the model catalog
* feat(api): advertise cc discovery aliases on /v1/models behind the 3-level gate
* feat(sse): resolve claude/ discovery aliases on the request path
* fix(sse): import getComboByName from db/combos, not the localDb barrel
* fix(sse): cover custom-node prefixes and the Codex WS bridge in cc discovery alias resolution
* feat(dashboard): cc discovery alias toggles + flag-screen env warning
Adds the operator-facing UI/API layer for the Claude Code discovery-alias
gate (claude/<provider>/<model> mirror ids on /v1/models): REST endpoint
for provider/model overrides, a provider-detail card with 3-state
(inherit/on/off) toggles, an info button on the Claude Code tool card
linking to Feature Flags, and an env-source warning on the
EXPOSE_CC_DISCOVERY_ALIASES flag card when it's forced on via env.
* feat(api): cc discovery usage metrics
* fix(api): record cc alias metric in the production wrapper + atomic counter upsert
* docs: document cc discovery aliases (Claude Code guide + feature flag catalog)
* fix(sse): don't mirror built-in auto/* combos as discovery aliases (advertised-but-unroutable)
* i18n(vi): translate the discovery-alias strings instead of shipping placeholders
vi is the one locale with a strict "no internal missing markers" test, so the 17
__MISSING__ entries this branch added (the provider ccAlias panel, the info
button, the feature-flag description and the env warning) would have turned that
test red the moment the base itself was repaired. Translated, keeping every ICU
placeholder ({modelId}, {error}) and the literal claude/<provider>/<model> id
shape intact.
* chore(quality): raise the frozen caps this feature legitimately grows
catalog.ts 1615 -> 1639: the alias synthesis is wired into the catalog builder,
which is where the per-key-filtered list is assembled — the only place the mirror
entries can be appended after model hiding has been applied.
localDb.ts 808 -> 810: two re-export lines for the new ccDiscoveryAliases db
module, which is exactly what the "Adding a New DB Module" recipe prescribes.
* refactor(dashboard,api): keep the complexity ratchets flat
The feature added four cyclomatic violations and one cognitive one, which the
ratchets reject — the baseline only moves when a metric improves. Split the new
code instead:
- appendCcDiscoveryAliases: the four skip-guards become isMirrorableId().
- resolveCcDiscoveryAliasStripWith: alias parsing and gate resolution become
parseCcAliasTarget() and resolveGateFor(), replacing a chain of ternaries that
each re-tested isComboAlias.
- FeatureFlagCard: the env-precedence warning becomes its own component instead
of a conditional branch inside an already-large render.
- ProviderCcAliasSection: the loader moves to useCcAliasData(), and the override
list and add-row become ModelOverrideList / AddOverrideRow, bringing both
oversized functions back under the 80-line rule.
Behavior unchanged — the 74 discovery-alias tests pass untouched. Both ratchets
now sit exactly at baseline (2188 / 971).
* fix(api): serve the model catalog stale-first and sanitize its error body
A client with a short discovery timeout (Claude Code allows 3s) hit a full
catalog rebuild — 290 providers plus SQLite reads — every time the memoized
entry expired, and got an empty model picker with no error. Serve an expired
entry immediately and revalidate in the background, bounded by a staleness
window so a permanently failing refresh cannot pin an old catalog forever.
Only a cached 200 is eligible; a state change still drops the cache outright.
The builder's catch block also returned the raw error message in the response
body. Route it through the shared sanitizer (hard rule #12).
* fix(api): reject a failed catalog refresh instead of resolving it stale
catalogInFlight is shared with the cold path, so resolving the background
refresh with the stale entry handed it to callers that had already aged past
CATALOG_STALE_WHILE_REVALIDATE_MS — a stale 200 they were no longer entitled to,
with a build failure disguised as success. The refresh now rejects; the stale
path never awaits it (the rejection is pre-handled, so it can never surface as
an unhandledRejection) and a cold-path caller that joins it gets the sanitized
500. A failed refresh still leaves the cached entry untouched.
Also sanitize the core builder's own catch — that is the realistically reachable
500 for this endpoint, and it still returned the raw error message (hard rule
#12); keep the cache-key format private to the module by having the two test
hooks take the Request and derive the key themselves.
* refactor(api): extract the model-catalog response cache into its own module
The stale-while-revalidate work pushed catalog.ts from 1615 to 1745 lines, past
its frozen size cap. Raising the cap on a file already flagged as too large is
the wrong answer: the caching layer is a self-contained concern (coalescing,
TTL memoization, staleness window, background refresh) that only needs a builder
callback from the catalog module.
catalogCache.ts now owns the maps, the cache key, the state-change invalidation,
the header merge, the background refresh and the test hooks; catalog.ts keeps
auth, the builder, and the error shape, and re-exports the hooks so the existing
tests keep importing them from where they always did.
Net effect: catalog.ts 1745 -> 1513, i.e. 102 lines below the cap it was frozen
at, and the test-only surface no longer sits in the production catalog module.
No behavior change — all 281 tests across every suite importing catalog.ts pass,
including the #6408 one-builder-run guard.
* fix(sse): clamp max_tokens to the model output cap on every path
enforceOutputTokenBudget only capped the three output-token fields against the
remaining context window, so a request whose max_tokens exceeded the model's own
output ceiling reached the upstream unchanged on the single-model path (the
reasoning-token buffer covers only thinking models inside combo routing).
Pass the model's explicit output cap into the budget check and use it as an extra
upper bound when adjusting the fields. The reject decision stays tied to the
context window: an output cap smaller than the default output budget must not
turn a valid request into a 400.
* fix(sse): key the output-cap lookup by provider + model
The bare-string form of getExplicitModelOutputCap resolves to `provider: null`,
which skips the registry cap and the operator's `max_token` capability override
(#6524) — the documented escape hatch for a wrong synced `limit_output`. Clamping
against a stale static spec while the operator had raised the ceiling would
silently truncate output.
Matches the { provider, model } form already used by the sibling capability
lookups in this file (getResolvedModelCapabilities, supportsMaxTokens).
* test(sse): cover the output-cap callsite; harden the sub-token cap guard
The unit tests drive enforceOutputTokenBudget() directly, so dropping the cap
argument at the handleChatCore callsite left every one of them green. Add a
wiring test that runs handleChatCore end to end against a stubbed fetch and
asserts the body actually dispatched upstream.
The cap comes from an operator `max_token` capability override rather than a
catalog model: the override table is keyed by provider, so the test also pins
the { provider, model } lookup — both the missing argument and the bare-string
form fail it (verified by mutating each in turn).
Also floor `maxOutputTokenCap` before the positivity test. A fractional cap
below 1 previously passed `> 0` and floored to an effective cap of 0, clamping
every field to zero; sub-token caps are meaningless and now read as absent.
Unreachable through the callsite (toPositiveInteger filters it) but the exported
contract was wrong.
The adjustment log now states the output ceiling in effect instead of claiming
the cap caused the adjustment — a field can also be adjusted by removal of an
invalid value, which the cap did not cause.
* feat(sse): selective upstream 4xx error passthrough util
* feat(sse): relay upstream 4xx error bodies verbatim on the Anthropic request path
createErrorResult() gains an opt-in 7th param opts.passthrough; when set and
the upstream body is eligible (per shouldPassthroughUpstreamError), the
returned response body is the upstream 4xx body verbatim instead of the
sanitized wrapper. Internal classification fields (error/rawMessage/
errorType/errorCode) are never affected.
Wired into chatCore.ts's 7 upstream-error createErrorResult call sites
(model_unavailable / context_overflow / generic upstream-error branches),
gated on sourceFormat === FORMATS.CLAUDE so only /v1/messages requests get
the verbatim body — OpenAI-format paths are unchanged.
* test(quality): register upstream-error-passthrough in stryker tap.testFiles
`tests/unit/upstream-error-passthrough.test.ts` covers `open-sse/utils/error.ts`,
an instrumented module, so `check:mutation-test-coverage --strict` requires it in
`tap.testFiles` — the gate flagged the drift on this PR.
* fix: repair five base-red failures on release/v3.8.49
Every PR cut from this branch fails CI on the branch's own breakage. Five
distinct causes, none introduced by the PRs that trip over them:
1. dast-smoke / Turbopack build — src/sse/handlers/chat.ts imported
PROVIDER_BREAKER_FAILURE_STATUSES twice in one statement. A duplicate import
specifier is an ECMAScript syntax error, so the production build never
compiled. Introduced by #8258, whose export fix landed on top of an import
that already existed.
2. Unit Tests — the #8393 verified-cooldown bypass was renamed
exactCooldownVerified -> exactCooldownIsUpstreamReset during the #8254
conflict resolution, which also dropped the flag at the markAccountUnavailable
call site entirely. The rename left the test passing the old key (so the flag
was silently ignored and a verified upstream reset got clamped back to
maxCooldownMs), and the dropped call site meant no real caller set it at all.
Align the test on the surviving name, restore the call site, and restore the
doc comment explaining #6863 vs #7940.
3. Unit Tests — #8526 added four common.* keys to en.json only, breaking the
strict key-parity tests for pt-BR and vi. Translated into all 42 locales.
4. Unit Tests — vi carried 17 __MISSING__ placeholders from #8354 and #8463, and
vi is the one locale with a no-placeholder test. Translated.
5. No new ESLint warnings — four suppressed `any`s in
tests/unit/combo-routing-engine.test.ts no longer exist, and ESLint exits 2 on
stale suppressions. Pruned (271 -> 267); no other entry moved.
Also regenerates skills/cli-backup-sync/SKILL.md, which still documented the
`backup status` flags #8512 removed — the merge-integrity gate compares the
generated output against the tree.
Not fixed here: the env/docs contract (NEXT_PUBLIC_OMNIROUTE_BASE_PATH and
OMNIROUTE_BACKUP_SCHEDULE_JOB_INTERVAL_MS missing from .env.example), which #8690
already covers, and the quality baselines, which #8686 covers.
* fix(quality): keep prettier off the generated SKILL.md files
check:agent-skills-sync diffs the generator's output against the tree byte for
byte, but lint-staged runs prettier over any staged *.md — and prettier inserts a
blank line after the frontmatter that the generator does not emit. Committing a
regenerated skill therefore made the gate fail again on the very file that was
just brought back in sync. The 44 untouched skills only escape this because they
never pass through lint-staged.
The generator is the formatter of record for these files, so ignore them.
* fix(i18n,quality): drop the stale zh-TW key; raise the auth.ts frozen cap
#8463 renamed `oauthModal.googleOAuthWarning` away but left the old key behind in
zh-TW, so the "the stale googleOAuthWarning key is GONE from every locale" guard
fails on the branch. Removed it.
The auth.ts frozen line cap goes 2486 -> 2492. Restoring the dropped
exactCooldownIsUpstreamReset call site costs 7 lines, and staging the file makes
lint-staged reformat four pre-existing over-100-column lines to prettier's rule —
unavoidable without bypassing the hook, which hard rule #10 forbids. The file
still sits 12 lines below where the cap was set relative to its actual size.
* fix(sse): export PROVIDER_BREAKER_FAILURE_STATUSES so chat.ts stops throwing ReferenceError
Base-red slice 4 (single root cause across the whole handleChat cluster).
src/sse/handlers/chat.ts:1273 references PROVIDER_BREAKER_FAILURE_STATUSES to decide
whether an all-rate-limited provider result should trip the provider breaker, but the
constant was only a FILE-LOCAL const in chatPredicates.ts (a refactor extracted it out
of chat.ts and never re-exported it). Every request that reached that branch threw
`ReferenceError: PROVIDER_BREAKER_FAILURE_STATUSES is not defined`, so the global
fallback and breaker-gate paths blew up — surfacing as "All models failed |
PROVIDER_BREAKER_FAILURE_STATUSES is not defined" and breaking the handleChat coverage
tests (combo-error passthrough, 503 for cooled-down/open-breaker, budget-error, model
cooldown, body-derived retry-after, non-JSON rate-limit bodies).
Fix: export the const from chatPredicates.ts and import it in chat.ts (one canonical
definition, restoring the pre-refactor behavior).
Validated: chat-route-coverage 15/0 (was 12/3), chat-cooldown-aware-retry 6/0,
chat-rate-limit-body-lock 2/0; breaker guards (7907, combo-breaker-429, openrouter-6842)
unchanged; typecheck:core clean.
* test(nvidia): read PROVIDER_BREAKER_FAILURE_STATUSES from chatPredicates.ts
Same root cause as the chat.ts import fix in this PR: the const was extracted out
of chat.ts into chatPredicates.ts, so the nvidia-quota Phase-1 guard (which greps
the source for the `= new Set([...])` declaration to prove 429 was not added to the
breaker classification) must read chatPredicates.ts, not chat.ts. Now 13/0.
---------
Co-authored-by: Probe Test <probe@example.com>
* test(antigravity): align catalog tests with the #8013/#8123 model realignment
Base-red slice 3. Two test files referenced antigravity model IDs that
models) intentionally renamed/retired — the candidate builder and static catalog
are correct; the tests were stale.
- auto-combo-credentialed-model-pool: claude-sonnet-5 -> claude-sonnet-4-6 and the
gemini-3.5-flash-{low,medium,high} tier -> gemini-3.6-flash-{low,medium,high}
(verified against the live createVirtualAutoCombo candidate set); the exclusion
wildcard and per-account transparency assertions are unchanged in intent.
- T31 static-catalog: gemini-3-pro-preview was retired by #8013, so assert the
current client-visible top flash tier (gemini-3.6-flash-high) plus its absence.
Test-only. Validated: auto-combo-credentialed-model-pool 4/4, the T31/T33/T34/T38
model-specs file 8/8.
* test(model-alias-seed): expect canonical antigravity provider for the agy alias
Same #8013 realignment as this PR: `getModelInfo` resolves the stored `agy/…` alias
target to its canonical provider id `antigravity` (ALIAS_TO_PROVIDER_ID). The stored
alias STRING stays `agy/gemini-pro-agent`; only the resolved `provider` is now
`antigravity`, not `agy`. Test updated to match. 6/0.
---------
Co-authored-by: Probe Test <probe@example.com>
* fix(chatcore): use combo-resolved context limit in enforceOutputTokenBudget
Line 1801 called getTokenLimit() directly, ignoring the contextLimit
variable that was already resolved with combo overrides (e.g. user-set
201320 for nvidia/z-ai/glm-5.2). This caused enforceOutputTokenBudget
to use the fallback 128K default, capping max_tokens to near-zero and
silently truncating responses.
Fix: use the existing contextLimit variable instead of re-resolving.
* fix(chatcore): hoist combo-resolved contextLimit so the output-token budget honors it
contextLimit (including the combo override from resolveComboContextLimit())
was declared inside the proactive-compression `if` block and never survived
to the final enforceOutputTokenBudget() call further down in
handleChatCore(), which referenced an out-of-scope `contextLimit` — a
ReferenceError on every request. Hoist the declaration to function scope so
the combo-resolved context limit is what the output-token budget actually
enforces.
Adds a regression test that drives handleChatCore() end-to-end with a combo
whose resolved context limit differs from the plain per-target
getTokenLimit() lookup, since output-token-budget.test.ts only exercises
enforceOutputTokenBudget() directly and cannot catch this class of bug.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: TonPro <hello@tonpro.fu>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
* fix(sse): label HTTP 499 disconnects as client_disconnected
Preserve caller-supplied error type/code in buildErrorBody so stream
abort classification is not overwritten by the status-code table.
* docs(security): document buildErrorBody classification arg
---------
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
* fix(guardrails): Vision Bridge describe-fallback ignores unreachable candidates
getVisionCapableModels() scanned the entire static PROVIDER_MODELS catalog
for a describe-fallback model without checking whether the provider has a
usable active connection on this instance. On an instance with no `openai`
provider connected, this let the hardcoded default `openai/gpt-4o-mini` win
selection every time, the describe call would fail, and
replaceImageParts()'s describe-failure fallback (#4012) intentionally
preserves the raw image part — which then reaches a non-vision backend and
gets rejected with an opaque upstream error (e.g. "unknown variant
`image_url`, expected `text`").
Extract the credential-usability check already used by the whole-request
reroute path (visionBridge.ts's hasUsableCredentialsForModel /
isProviderConnectionUsable) into a shared visionBridgeCredentials.ts module,
and apply the same check to the describe-fallback candidate list in
visionBridgeRouter.ts. A confirmed-unusable connection (`false`) excludes a
candidate; an indeterminate result (`null`, e.g. no DB) fails open to
preserve existing behavior.
getVisionCapableModels/getBestVisionModel/getFallbackModels become async to
support the credential lookup; call sites and the existing unit test suite
are updated accordingly, plus new coverage for the exclusion behavior.
* fix(guardrails): fix flaky credential-mock race and move Vision Bridge router tests to a CI-blocking runner
The two new assertions added in this PR (excludes a candidate with no
usable active connection / selects a credentialed candidate over an
uncredentialed one) were correct — the failure was a genuine Vitest
race: getVisionCapableModels() fans out to hasUsableCredentialsForModel()
once per catalog entry via Promise.all, and dozens of concurrent
first-load `await import("@/lib/db/providers")` calls for the same
specifier under vi.mock() nondeterministically resolved against the real
module instead of the mock for some callers. Memoize the dynamic import
in visionBridgeCredentials.ts so it resolves exactly once and is reused,
which removes the race entirely (and is cheaper at runtime too).
Also: tests/unit/guardrails/visionBridgeRouter.test.tsx was never
collected by any CI-blocking gate — `test:unit`'s guardrails glob is
`*.test.ts` only, and `test:vitest` (vitest.mcp.config.ts) doesn't
include this directory; only the advisory `test:vitest:ui` picked it up.
Moved the suite to visionBridgeRouter.test.ts under node:test, threading
an optional `deps.hasUsableCredentials` injection point through
getBestVisionModel()/getFallbackModels() (consistent with the existing
deps pattern in visionBridge.ts) since this project's native test runner
has no supported ESM module-mocking mechanism.
Running the full guardrails suite together also surfaced that this PR's
own credential-exclusion feature silently broke the pre-existing
vision-bridge-callmodel.test.ts fallback-retry test: with zero seeded
provider connections in that test's isolated DATA_DIR, every fallback
candidate is now confirmed-unusable and excluded, leaving no fallback to
retry. Seeded one credentialed connection there so the fallback-retry
mechanics stay independent of the (unrelated) credential filter.
Refs #8433
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
* fix(oauth): explain the LAN-IP loopback mismatch with an actionable panel
#8046 already stops the doomed login when a PKCE_CALLBACK_SERVER_PROVIDERS
provider (codex / xai-oauth / grok-cli) is connected from a LAN IP, but it
explained itself as one long English sentence rendered in the generic red
"Connection failed" step. Two concrete problems with that:
- the operator had to parse prose to work out WHICH ports to forward, and the
command shipped with `<port>` / `<omniroute-host>` placeholders to resolve
by hand;
- it forwarded a single port. Both are required: the dashboard port is what
makes the origin true-localhost (a LAN origin never reaches the
callback-server branch at all), and the provider's fixed callback port is
where the browser is actually sent back to. Forwarding either one alone
still fails.
buildPkceLoopbackMismatchHint() now returns the diagnosis as structured data
with the detected host and both ports already filled in, and a dedicated
OAuthLoopbackMismatchPanel renders it as: what happened -> how to fix, in
three numbered steps with copy-to-clipboard fields. No "Try again" button —
retrying the same origin fails identically. The panel yields to the
paste-token tab so grok-cli (which is in both provider sets) never stacks the
two views.
The flat warning string stays exported for non-UI callers.
docs: REMOTE-MODE.md gains a "Connecting Codex / Grok on a remote install"
section with the fixed-callback table and the two-port tunnel, mirroring the
existing Antigravity section.
i18n: 9 new oauthModal keys, hand-written for en + pt-BR and propagated to the
remaining 40 locales as `__MISSING__:` sentinels (runtime falls back to the
clean English value per #7258).
* fix(oauth): correct the Antigravity remote-login guidance and drop the stale i18n copy
Same LAN-origin family as the codex fix in this branch, different mechanism and a
worse failure mode.
Google providers (antigravity / agy) have no fixed foreign port: OAuthModal builds
`http://127.0.0.1:<dashboardPort>/callback`. On a LAN origin that 127.0.0.1 is the
BROWSER's machine, and Google's firstparty/nativeapp consent only releases the code
once the loopback is reachable from the approving browser. When it is not, the consent
never redirects at all — it hangs. So unlike an ordinary provider there is no error
page and no callback URL in the address bar.
That made the existing copy actively wrong. `googleOAuthWarning` was corrected when
the login helper shipped (#5203), but a changed English value does not invalidate
existing translations and `i18n:sync-ui` only fills keys that are ABSENT, never ones
that are STALE — so 39 of 43 locales (pt-BR, pt, es, de, fr, ja, zh-CN, …) kept the
original "wait for the redirect, copy the full URL and paste it below", instructing a
flow that cannot complete. The drift gate that should have caught this is a no-op:
`check-translation-drift.mjs` needs `.i18n-state.json`, which is not in the repo, and
it runs `--warn`.
Because the key's MEANING changed, it is renamed rather than edited — a new key cannot
inherit a stale translation. `googleOAuthWarning` is removed from all 43 locales and
replaced by 7 `googleLoopback*` keys, hand-written for en + pt-BR and marked
`__MISSING__:` elsewhere so the runtime falls back to correct English (#7258).
UI: `OAuthGoogleLoopbackNotice` states what is happening and surfaces both real
remedies with the detected host and port filled in — the local login helper
(recommended; its blob is what the Step 2 field accepts) and a single-port SSH forward.
It also REPLACES `remoteAccessInfo` for this family instead of stacking on top of it:
that notice promises an error page whose URL you copy, true for ordinary providers and
false here.
`agy` deliberately gets no helper command. bin/cli/commands/login.mjs pins
PROVIDER = "antigravity" and parsePastedCredentials() rejects a blob whose embedded
provider does not match the route provider, so advertising the helper there would send
the operator to a blob guaranteed to be refused. It keeps the tunnel path.
Refactor: the shared `resolveDashboardPort` / `buildSshLocalForward` helpers move to
`loopbackTunnel.ts`, used by both hint builders. The codex builder's behaviour is
unchanged (its 11 tests still pass untouched).
docs: REMOTE-MODE.md notes that the dashboard now surfaces the remedies, states that
one forward is enough for Antigravity (contrasting the two-port codex case), and aligns
Option B's command on 127.0.0.1 to match what the UI generates.
---------
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
* fix(backend): fail closed when capability filters empty the combo pool
Tools/vision/structured_output filters no longer re-admit the full pool when
every target is incompatible. Opt-in via combo config compatFilterFailOpen.
Closes#8488
* fix(backend): keep tool-emulation providers under fail-closed filters
Carve out providers with toolCalling:"emulated" (#5240) from tools
capability_mismatch so chatgpt-web combos still reach the prompt shim.
Align round-robin compatFilterFailOpen with settings fallback and drop
new any-typed params from the #8488 combo-routing tests.
* chore(lint): prune stale combo-routing-engine any suppressions
Test cleanup in #8488 dropped four no-explicit-any hits; sync the freeze file.
* chore(quality): rebaseline combo.ts + freeze new-above-cap comboStructure.ts
check:file-size was red for this PR's own growth: combo.ts grew 3640->3693
(+53, the capability-filter fail-closed guard + compatFilterFailOpen escape
hatch at both call sites) and combo/comboStructure.ts crossed the 800-line
new-file cap at 918 (describeCapabilityFilterExhaustion +
providerSupportsEmulatedToolCalling for the #5240 emulated-tool-calling
exemption). Both are irreducible orchestration wiring at the existing combo
filter chokepoint (same precedent as #7301's cooldown-retry generalization).
Companion test tests/unit/combo-routing-engine.test.ts frozen at its own
grown size (3409->3449). No logic change; 95/95 tests pass.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
---------
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
Co-authored-by: Prudhvivuda <Prudhvivuda@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(log): Added new visual scrolling log page
* chore(quality): rebaseline sections.ts own-growth for #8354 (logs-timeline sidebar item)
* feat(log): direct-link a request from the scrolling timeline
Clicking a request bar now sets ?id= on the URL (matching the regular
request log page), and the timeline opens the deep-linked request on
mount. The open/close handlers arm the same guard so a stale
initialSelectedId (router.replace() commits the URL after the render it
triggers) can never reopen the modal right after the user closes it.
* fix(dashboard): propagate logsTimelineSubtitle across all 43 locales
en.json was missing the logsTimelineSubtitle key entirely, breaking the
default locale for the new /dashboard/logs/timeline sidebar entry. Add
the real English string to en.json, add __MISSING__: placeholders to
the 11 locales that lacked the key outright, and convert the 30 locales
that had copied the English text literally to the repo's __MISSING__:
convention for untranslated strings.
Also pause the RequestTimeline 2s poll while the tab is backgrounded
(document.visibilityState), matching the existing pattern in
RequestLoggerV2 and UsageStats.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(i18n,sidebar): add missing logsTimelineSubtitle + update sidebar tests
Address maintainer feedback on #8354:
- Add logsTimelineSubtitle key to en.json + 11 locales (ar, az, bg, bn,
cs, da, de, es, fa, fi, fr) that were missing it
- Add logs-timeline to sidebar-visibility.test.ts expected arrays
- Add logs-timeline to sidebar-monitoring-reorg.test.ts logs group
- Add compression-exclusions to sidebar-visibility.test.ts (pre-existing)
* feat: add touch support for mobile panning and pinch-to-zoom on timeline
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(combos): add select all / unselect all in Browse Catalog
* fix(dashboard): guard combo Select all + test the real batch handlers (#8526)
Select all had no cap — with "Show configured only" off, or a large
provider catalog, one click could add hundreds of models to a combo.
ModelSelectModal now confirms above SELECT_ALL_CONFIRM_THRESHOLD (20)
before batch-adding, matching the native confirm() pattern already
used for bulk/destructive actions elsewhere in the dashboard.
Also extracts ComboFormModal's handleAddModels/handleDeselectModels
batching logic into computeBatchAddModelSteps/computeBatchDeselectModelSteps
(src/lib/combos/builderDraft.ts) so unit tests exercise the real
implementation instead of a hand-maintained mirror that could drift
from the component and stay green while production code broke.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
* fix(chatgpt-web): recover async images via conversation-poll fallback (#7357)
chatgpt-web generates images upstream but frequently fails to return them:
register-websocket is Cloudflare-sensitive and the plain WebSocket used to
receive the async image event lacks the browser TLS fingerprint the HTTP
client (tlsFetchChatGpt) uses, so pollForAsyncImage errors or times out with
no frames — even though the image is already in the conversation.
When the websocket yields nothing, poll GET /backend-api/conversation/{id}
over the same authenticated HTTP path and read the image_asset_pointer
directly (newest message wins, so a reused conversation can't surface a stale
image). The existing makeImageResolver then downloads it via the files API.
This is the durable fallback suggested in #7357.
Verified against a live ChatGPT Plus session: with the websocket capped short,
the fallback recovers the image and returns a real PNG.
* test(chatgpt-web): cover conversation-poll fallback for async images
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: sadruzzahan <istykhan.ik@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
* feat(cli-tools): add all Hermes Agent auxiliary model roles
Extend HERMES_AGENT_ROLES from 7 to 18 slots to match the full
auxiliary.* set in Hermes Agent config.yaml:
- add: mcp, title_generation, memory_query_rewrite, tts_audio_tags,
triage_specifier, kanban_decomposer, profile_describer, goal_judge,
curator, monitor, background_review
- reorder: web_extract before compression (match upstream docs)
Backend generator/reader are generic on auxiliary.<role> — only the
role catalog, UI card, and i18n (en + pl) needed updating.
* fix(i18n): translate the 12 new Hermes auxiliary roles into vi and pt-BR
The 22 new en.json keys landed only in pl.json, breaking the vi and pt-BR
key-parity guards. Adds the same keys with real translations (no placeholder
strings), keeping both parity assertions exact.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* test(cli-helper): guard HERMES role catalog parity between backend and UI
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* test(tail): retire stale i18n __MISSING__ repro + fix qianfan website URL
Base-red slice 6, rebased onto the advanced release/v3.8.49 (91fd5f9). The oauth
grok-cli #7610 guard was already fixed on the base by #8027 (it reads the warning
from grokCliAuthJson.ts) — dropped from this slice to avoid a conflicting duplicate.
Remaining two, still red on the current base:
- i18n #7258: the "focused repro" asserted zh-TW.json STILL carries raw __MISSING__:
placeholders. That backlog was filled (the "no locale has a raw __MISSING__: leaf"
invariant is the durable guard); retired the now-inverted repro.
- qianfan: Baidu renamed the product page (product/wenxinworkshop -> product-s/
qianfan_home); updated the expected website URL.
Validated (clean env): i18n 4/0, qianfan 5/0; oauth-modal-grok 2/0 already green on base.
* fix(resilience): short-circuit combo on input-bound failures (context_length_exceeded) (#8375)
isInputBoundRequestFailure() predicate detects deterministic input-bound
errors (context_length_exceeded/context_window_exceeded). The combo loop
propagates the original 400 immediately instead of burning MAX_GLOBAL_ATTEMPTS
retrying identical oversized inputs against every account.
Test: combo-input-bound-failure-8375.test.ts (1 test, 2 assertions)
* fix(resilience): add early-exit in combo dispatcher for input-bound failures (#8375)
When isInputBoundRequestFailure detects context_length_exceeded,
the combo loop returns {ok:false, response} immediately instead of
re-dispatching the oversized request.
Test: node --import tsx/esm --test tests/unit/combo-input-bound-failure-8375.test.ts
- 1 test, 2 assertions, 0 fail
* fix(translator): strip input_image from tool outputs in Responses->Chat downgrade (#8459)
toolOutputContentToString() extracts input_text/output_text parts and
replaces input_image with a placeholder instead of JSON.stringify'ing
the content-part array (which embedded raw ~52KB base64 as inert text).
Applied to both function_call_output and custom_tool_call_output branches.
Existing translator tests: 88/88 pass.
New tests: 4/4 pass.
* fix(providers): set qwen-web toolCalling to false — web-cookie provider has no native function calling (#8437)
qwen-web is a web-cookie provider that emulates tools via synthetic system
prompt text and <tool> XML parsing, never sending a native tools[] field
upstream. The filterTargetsByRequestCompatibility gate filters out non-tool-
calling targets when the request carries tools, but qwen-web's registry entry
had toolCalling=true, so the filter let it through and a tool-using session
failing over to qwen-web would silently degrade to text-only chat with
'Tool X does not exists' errors.
Sibling web-cookie providers (chatgpt-web, yuanbao-web, claude-web, etc.)
all correctly set toolCalling: false — qwen-web was an outlier introduced
in PR #7874.
Verification:
- LSP diagnostics: clean
- Pattern matches chatgpt-web, yuanbao-web, and other web-cookie providers
* fix(backend): empty upstream response mislabeled as exhausted_connection (#8397)
isEmptyContentFailure guard only matched '/empty content/i' but the actual
error text from detectMalformedNonStream is 'returned an empty response
(no usable choices/output)' — which lacks the word 'content'. Expanded
regex to also match '/empty response/i' so these transient upstream glitches
don't get classified as connection-level exhaustion in combo diagnostics.
Test: 28 existing combo-target-exhaustion tests pass (no new test needed)
* test(#8397): add regression test for empty-response 502 not marking provider/connection exhausted
* test(qwen-web): align registry snapshot with toolCalling:false
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(resilience): scope #8375 input-bound short-circuit to homogeneous remainders
The isInputBoundFailure short-circuit (context_length_exceeded /
context_window_exceeded) fired unconditionally on the first target, aborting
the whole combo even when later targets are a different model with a larger
context window — regressing the intentional heterogeneous-combo fallback that
isContextOverflow400 (#6637) protects. Reproduced with a 2-target combo
(small-context model fails, larger-context model would have succeeded): the
combo never reached target 2.
Scope the short-circuit to remainders where every remaining target shares the
same modelStr as the one that just failed — the "retrying will fail
identically" premise for context_length_exceeded only holds within a
homogeneous same-model pool.
Rebaselines open-sse/services/combo.ts's frozen file-size cap (3642->3679)
for this PR's own combo.ts growth (config/quality/file-size-baseline.json).
Refs #8375
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
---------
Co-authored-by: Probe Test <probe@example.com>
Co-authored-by: herjarsa <herjarsa@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* feat: read INITIAL_PASSWORD env var during setup
Allow users to set the admin password via the INITIAL_PASSWORD
environment variable instead of requiring the --password CLI flag
or interactive prompt. Falls between --password flag and interactive
prompt in resolution priority.
* test(cli): cover INITIAL_PASSWORD env var in setup resolvePassword
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: linh.doan <linh.doan@be.com.vn>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(providers): resolve native vision for path-shaped multimodal model ids
Leaf-id static/registry metadata now wins over synced attachment=false without
modalities, so cp/cline-pass/kimi-k3 forwards images natively.
Closes#8032
* fix(providers): scope path-shaped leaf lookup to vision only
Move leaf MODEL_SPECS fallback out of shared getStaticSpec() so
aihorde/deepseek/deepseek-v4-flash no longer inherits DeepSeek tool-calling
metadata (#8212). Vision still resolves via getVisionStaticSpec().
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
---------
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(providers): honor Extra API Keys rotation in OpencodeExecutor
OpencodeExecutor.buildHeaders bypassed resolveEffectiveKey, so extraApiKeys
never rotated and an empty primary with populated extras sent no auth header.
Closes#8467
* fix(executors): gate the Authorization header write on the resolved effective key
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Sticky root keys keyed only on the first user message caused Claude Code
"New session" + "hi" to reuse a confirmed prior Notion thread (forking history).
Prefer exact conversation-prefix match for multi-turn; keep sticky root for
UREW multi-turn and failed-first-request retries; mint createThread:true when
the sticky root is already confirmed and the request has no assistant history.
* feat(adobe-firefly): reference image attach + /v1/images/edits (follow-up #8006)
Upload source images to Firefly storage (POST /v2/storage/image) and attach
them as referenceBlobs on generate-async, matching live firefly.adobe.com
captures (usage:general for nano multi-ref; usage:subject for gpt-image).
Also wire built-in adobe-firefly through OpenAI-compatible POST /v1/images/edits
(multipart or JSON data URLs, up to 4 refs) so Media edit-with-references
and Open WebUI image-edit hit the same path as image2image generate.
Unit suite: tests/unit/adobe-firefly.test.ts 41/41.
* test(api): add route-level coverage for Adobe Firefly /v1/images/edits + fix typecheck/file-size drift
Covers the referenceBlobs upload path, the 4-reference cap error, and the
credentials/rate-limit branches added to the /v1/images/edits route for
adobe-firefly (#8510). Also fixes a Buffer/BodyInit typecheck mismatch in
uploadAdobeFireflyImage and corrects the adobeFireflyClient.ts file-size
baseline entry to match the gate's actual LOC count (it counts the trailing
newline, so the frozen value is 2317, not 2316), plus a testFrozen entry for
adobe-firefly.test.ts's own +159 line growth from this PR. Moves the
handleAdobeFireflyImageGeneration re-export out of the middle of the import
block in imageGeneration.ts for readability.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(security): restore bounded JWT regex quantifiers dropped by edit-route refactor
The edits-route extraction (test commit 62785e972f) had accidentally
reverted 4 JWT-extraction regexes in adobeFireflyClient.ts from the
bounded {1,4096} quantifier back to unbounded +, undoing the ReDoS fix
from #8173 (CodeQL #754/#755/#756). Restored the bounded quantifiers;
behavior is unchanged (45/45 adobe-firefly tests still pass), only the
backtracking bound is back in place.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
---------
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(hyperagent): default 1M context for fable/opus/sonnet
HyperAgent Claude-family models (fable, opus, sonnet) were falling through
getTokenLimit to the generic 128k default. Agentic tool-loop prompts with
large catalogs then failed with context_length_exceeded (~137k tokens).
- defaultContextLength + per-model contextLength = 1_000_000 on hyperagent registry
- DEFAULT_LIMITS.hyperagent / ha = 1M
- Resolve hyperagent/ha (and fable/opus wire ids) before models.dev DB fallback
Verified: getTokenLimit('hyperagent','fable-latest') === 1000000; context-manager tests 30/30.
* fix(sse): scope hyperagent 1M context fix to the registry, drop unscoped model-name match
The step-1b branch in resolveTokenLimit() matched fable/opus/sonnet model
name substrings for ANY provider, before the models.dev DB lookup. That
collided with anthropic/claude, kiro, windsurf and bluesminds registries,
which serve the same Claude model ids (e.g. claude-opus-4.7-max,
claude-sonnet-5) with their own accurate per-model contextLength — those
were being clobbered to 1M instead of their real (often 200k) limit.
The registry-level defaultContextLength added on the hyperagent provider
entry already fixes the reported bug (getTokenLimit('hyperagent', ...) ===
1_000_000) on its own, scoped correctly by provider. Remove the redundant,
unscoped substring branch and add regression coverage for every hyperagent
fallback model id plus the cross-provider collision.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>