mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 14:52:09 +03:00
2d48bb6dcaabf0972f01b0c348bb290208a8563d
121 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1bd1af2f48 |
refactor(sse): narrow three result unions via type predicates (#8499)
* refactor(sse): narrow three result unions via type predicates Eight diagnostics across three executors, all the same root cause already recorded in #8483: this workspace compiles with `strictNullChecks: false`, where a boolean-literal discriminant narrows the positive branch but not the negative one. Reading a failure-only field after `!result.ok` therefore leaves the full union. auggie.ts 2 .error on AuggieModelResolution muse-spark-web 4 .error on GraphqlResult notion-web 2 .retryable / .errorResult on the runOnce union #8483 retagged its union with a string discriminant. That is the better shape when the union is module-private and small, but it does not fit here: `resolveAuggieModel` is exported and its tests deep-equal the literal `{ ok: true, model }` object, so retagging would churn public API and assertions to fix a checker limitation. Each union instead gets an explicit type predicate, which narrows correctly under these compiler settings while leaving the shape, every call site, and the tests untouched. notion-web's inline union is named `NotionAttempt` first so it has something to `Extract` from. Validation: full tsc error-set diff against the base config — 335 -> 327, zero new errors (line-number-agnostic). `typecheck:core` clean; the 6 existing test files importing a touched executor pass. Coverage: each predicate is a one-liner whose control flow inverts on a stray `!`, and all three failure branches already have behavioral guards — `auggie-executor.test.ts` (400 + /Unknown Auggie model/), `muse-spark-web-continuation.test.ts` ("Warmup failed: …"), and `executor-notion-web.test.ts` (nested temporarily-unavailable → retried). The two assertions added here pin the arm that the predicate unlocks on the one union that is exported and directly reachable. * chore(quality): rebaseline muse-spark-web.ts for #8499 own growth The new isGraphqlFailure() type-predicate helper (TS7 strictNullChecks:false narrowing fix) grows the frozen file 1396->1405 (+9), irreducible per the justification recorded in config/quality/file-size-baseline.json. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> --------- Co-authored-by: ikelvingo <im.kelvinwong@gmail.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> |
||
|
|
f60c9fe542 | chore(quality): rebaseline complexity/cognitive for v3.8.49 merge-train own-growth | ||
|
|
1d7878d857 | chore(quality): rebaseline complexity/cognitive to v3.8.49 tip drift | ||
|
|
4053e2314a |
chore(quality): rebaseline file-size for inherited base growth (#8561)
check:file-size fails on the pristine release/v3.8.49 tip ( |
||
|
|
278640b438 |
chore(ci): resync stale no-explicit-any suppression count for proxy-registry.test.ts (#8544)
* chore(ci): resync stale no-explicit-any suppression count for proxy-registry.test.ts
tests/unit/proxy-registry.test.ts is frozen at 55 no-explicit-any
violations but only has 54 since #8447 (
|
||
|
|
7a8f9156da |
test(sse): repair two base-red gates on release/v3.8.49 (#8490)
* test(sse): repair two base-red gates on release/v3.8.49 `release/v3.8.49` is red at its own HEAD ( |
||
|
|
909642879f | feat: add Claude Opus 5 support (#8464) | ||
|
|
11f7ba2c72 |
fix(sse): record tool calls into shared state for openai->openai-responses call-log summary (#8462)
The openai-responses translator tracked tool calls in its own funcCallIds/ funcNames/funcArgsBuf bookkeeping without ever writing to the shared state.toolCalls Map that stream.ts's completion-log summary builder reads. Every openai->openai-responses translated stream with a tool call was persisted with finish_reason "stop" and no tool_calls, even though the actual SSE events sent to the client were correct. |
||
|
|
1cafd328c7 | fix(dashboard): persist compression engine detail settings (Headroom / session dedup / CCR) instead of dropping them on save (#8388) (#8404) | ||
|
|
406f41de30 |
fix(translator): cap thinking budget on explicit budget_tokens path (#8312)
* fix(translator): cap thinking budget on explicit budget_tokens path * fix(translator): stop dropping thinkingConfig on cap-0 reasoning_effort path The thinking-budget-cap guard added in this branch skipped thinkingConfig entirely whenever a model's thinkingBudgetCap was 0 (e.g. gemini-3-flash), including on the reasoning_effort/budgetMap path. That regressed the pre-#6943 native-defaults contract (thinkingBudget 0 / includeThoughts false must still be present) and crashed callers that read `.thinkingConfig.thinkingBudget` unconditionally (translator-openai-to-gemini-defaults.test.ts). Also restore includeThoughts:true on the Claude-format explicit thinking.budget_tokens path (openai-to-gemini.ts's Claude-format field and claude-to-gemini.ts's native thinking field): budget_tokens:0 there is the client's dynamic-thinking sentinel (#6813), not an off-switch, and must stay true even after the new capping — the cap must only clamp positive explicit values, never flip the zero sentinel's semantics. Updates two tests this branch added that encoded the incorrect "omit thinkingConfig / includeThoughts:false for the 0 sentinel" behavior, to match the pre-existing, still-required contracts above. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(providers): revert scope-creep flip of Gemini 3.5/3.6 Flash supportsThinking The thinking-budget-cap fix accidentally expanded 5 shorthand modelSpecs entries (gemini-3.5-flash, gemini-3.5-flash-low, gemini-3.6-flash-high/ medium/low) into explicit objects setting supportsThinking:true and thinkingBudgetCap:24576. That flip was unrelated to the two proven test regressions (translator-openai-to-gemini-defaults.test.ts and claude-to-gemini-budget-tokens-zero-6813.test.ts, which only exercise gemini-3-flash-preview, gemini-3.1-pro and gemini-2.5-pro) and reopens a deliberately closed path from #8013: Antigravity still rejects client-supplied thinking params for these Gemini 3.5/3.6 Flash tier ids, so supportsThinking must stay false (inherited from GEMINI_35_FLASH_MODEL_SPEC). Reverted all 5 entries back to the release shorthand `{ ...GEMINI_35_FLASH_MODEL_SPEC }`. gemini-3-flash, gemini-3.1-pro and gemini-2.5-pro (the models the regression tests actually exercise) were already correctly specced in the release baseline and are untouched. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
1e0886cd09 |
test: hermetic notion thread-session mocks + drop duplicated usage-analytics suite (#8392)
* test: hermetic TLS mock for notion thread-session suite + drop duplicated usage-analytics file Root cause A (#8159): sendNotionInferenceRequest() in open-sse/executors/notion-web.ts was migrated from fetch() to tlsFetchNotion() (open-sse/services/notionTlsClient.ts, native tls-client-node binary) to get past Notion's Cloudflare TLS fingerprinting. #8159 updated the mock in the sibling tests/unit/executor-notion-web.test.ts (installNotionTlsMock, wired through __setTlsFetchOverrideForTesting) but never touched tests/unit/executor-notion-web-thread-sessions.test.ts (split out earlier by #7900) — its 3 execute()-driven tests still mocked globalThis.fetch, which tlsFetchNotion() never calls once the native TLS client loads successfully. Confirmed live: all 3 tests hit real https://app.notion.com with a fake cookie and got a real 401 (~8.1-8.5s each here; on a network with blocked/slow egress this would instead hang up to the client's ~190s timeout+grace per test — a CI-hang risk). Fix: replicate installNotionTlsMock verbatim from the sibling file into executor-notion-web-thread-sessions.test.ts so the 3 tests mock the TLS override point instead of global fetch. Suite is now fully hermetic — 8/8 pass, no network I/O, total runtime 31.0s -> 8.0s. Root cause B (#7700): tests/unit/usage-analytics-route-extra.test.ts was created as a byte-for-byte duplicate of 10 of the 22 tests in tests/unit/usage-analytics-route.test.ts. #7300 later fixed a fixture bug in the retention-window boundary test ("does not double-count raw and aggregated rows") in the main file only — reading getUserDatabaseSettings().retention.usageHistory live instead of a hardcoded 30-day cutoff (default retention is 365 days) — leaving the duplicate copy on the stale hardcoded value, which now fails (1 !== 2). Fix: delete the duplicate file. All 10 of its test names exist verbatim in the main file (verified with comm -12) and that file passes 22/22: - does not double-count raw and aggregated rows - does not persist guessed API key attribution - does not throw Unknown named parameter on short range (needsAggregated=false) - does not throw Unknown named parameter with apiKey filter on long range - groups renamed API key usage by stable ID - includes activityMap for heatmap - includes cost by API key - omits global aggregates when filtering by API key - returns 500 on database errors - returns weeklyPattern for the costs dashboard No coverage loss — same production code, same assertions, one fewer redundant file. Validation: - RED executor-notion-web-thread-sessions.test.ts: 5 pass / 3 fail (401 !== 200, real network hit), 31.0s - RED usage-analytics-route-extra.test.ts: 9 pass / 1 fail (1 !== 2), 18.7s - GREEN executor-notion-web-thread-sessions.test.ts: 8/8 pass, 8.0s, hermetic (no network) - GREEN executor-notion-web.test.ts (sibling, untouched): 37/37 pass, byte-identical diff - GREEN usage-analytics-route.test.ts (untouched): 22/22 pass, byte-identical diff - npx eslint on the changed file: clean - npm run typecheck:core: clean (exit 0) Refs #8159 Refs #7300 Refs #7700 * chore(quality): register usage-analytics-route-extra deletion in test-masking allowlist check:test-masking hard-flags any deleted test file without a _deletedWithReplacement entry. The deletion is legitimate (100% duplicate suite, coverage retained verbatim in tests/unit/usage-analytics-route.test.ts) -- same registration pattern as the video-dashscope entry. Refs #7700 Refs #7300 |
||
|
|
fbea867d11 |
test: realign catalog snapshot tests to current deliberate catalog state (#8386)
* test: realign catalog snapshot tests to current deliberate catalog state Six catalog/snapshot tests drifted behind deliberate catalog changes that were already validated by newer sibling tests. No production code touched; every change aligns a stale snapshot to behavior already validated by newer sibling tests. Root causes (all confirmed against the current code before editing): - tests/unit/providers-constants-split.test.ts: APIKEY_PROVIDERS grew from 187 to 195 entries via #8077 (clova-studio/internlm/ant-ling, regional), #8161 (sarvam/plamo → regional, writer → frontier-labs) and #8170 (typhoon → regional, inception → frontier-labs). Family counts verified to sum to 195 (gateways 60, frontier-labs 24, inference-hosts 28, enterprise-cloud 17, regional 40, specialty-media 26) with no duplicates. Updated the two assertions and extended the changelog comment. - tests/unit/qianfan-provider.test.ts: the expected Baidu Qianfan website URL was the pre-#8128 wenxinworkshop path. #8128/#6271 moved it to https://cloud.baidu.com/product-s/qianfan_home, already locked by the sibling regression test tests/unit/baidu-qianfan-website-urls-6271.test.ts. - tests/unit/t31-t33-t34-t38-model-specs.test.ts and tests/unit/auto-combo-credentialed-model-pool.test.ts: the Antigravity catalog refactor (#8013) retired gemini-3-pro-preview/claude-sonnet-5 and renamed the Gemini 3.5 Flash tiers (low/medium/high -> extra-low/low/gemini-3-flash-agent), confirmed against ANTIGRAVITY_PUBLIC_MODELS and tests/unit/antigravity-retired-public-models.test.ts. Swapped the retired IDs for currently-registered ones (gemini-3.6-flash-high, claude-sonnet-4-6, gemini-3-flash-agent, gemini-3.5-flash-low/extra-low) and moved the wildcard-exclusion prefix test from the now-2-tier "gemini-3.5-*" group to "gemini-3.6-*", which has 3 real tiers today (same >=3 semantics, just pointed at a prefix that still has 3 members). - tests/unit/model-alias-seed.test.ts: getModelInfo("gemini-3.1-pro") now canonicalizes through ALIAS_TO_PROVIDER_ID["agy"] = "antigravity" (#8050), the same pattern already applied to opencode -> opencode-zen. Updated the expected provider id. - tests/unit/video-dashscope.test.ts (deleted, 216 lines): #8266 reorganized the Alibaba video catalog so the flat wan2.7-t2v id no longer exists under the plain "alibaba" provider (only the dated wan2.7-t2v-2026-06-12 does); the flat id now lives only under "qwen-cloud". All 6 tests in the file failed because they built requests against alibaba/wan2.7-t2v, which the new allowlist now rejects with 400 ("unsupported alibaba video model") - verified directly against VIDEO_PROVIDERS in open-sse/config/videoRegistry.ts. Coverage already exists and was confirmed passing pre-deletion in tests/unit/alibaba-video-media.test.ts (including an explicit "Alibaba rejects video models outside its own allowlist" case for this exact id) and tests/unit/qwen-cloud-video-media.test.ts (covers the same id under qwen-cloud). Note: the deleted file's DashScope upstream error-path assertions (401 missing credentials, 502 missing task_id, 502 FAILED status, 504 poll timeout) don't have a byte-for-byte equivalent in the two replacement files, though the shared dashscopeHandler.ts code path they exercise remains covered by several sibling *-media.test.ts files for the happy path and local validation. - tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts: #7892 added /api/vnc-session to the SPAWN_CAPABLE_PREFIXES deny-list (Hard Rules #15/#17 hardening). Bumped the expected length 10 -> 11 and added the entry to the test's named list for documentation. Refs #8013, #8050, #8266, #7892, #8128 * chore(quality): allowlist the video-dashscope.test.ts deletion with its replacements check:test-masking (pr-test-policy CI gate) requires a _deletedWithReplacement entry for any deleted test file, even when the deletion is a verified-legitimate supersession. Documents the same #8266 rationale from the prior commit in the machine-checked allowlist so the deletion is not flagged as unexplained masking. Refs #8266 |
||
|
|
7202654f81 | fix(providers): classify per-model-quota 403 and DEGRADED 400 as model-unhealthy in checkFallbackError (#8247, #8248) (#8323) | ||
|
|
2d789424f1 | chore(quality): rebaseline file-size own-growth for merge-train 15 (auth/muse-spark/translator-test) | ||
|
|
869d08ff8b |
Add Alibaba-family media model support (#8266)
* Add Alibaba-family media models * chore(quality): rebaseline imageRegistry+cognitive for #8266 media own-growth --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
91fd5f946a | chore(quality): rebaseline accountFallback+combo for #8252 own-growth (post-merge follow-up) | ||
|
|
f3ed4a49d4 |
feat(providers): add weekly quota tracking for grok-web (#8127)
* feat(providers): add weekly quota fetcher for grok-web (grok.com SSO) Implements a bespoke QuotaFetcher for the grok-web provider that: - Reads OIDC tokens from ~/.grok/auth.json (local Grok CLI login) - Refreshes tokens via auth.x.ai OIDC if expired - Calls https://cli-chat-proxy.grok.com/v1/billing?format=credits - Returns a single 'weekly' window with creditUsagePercent and resetAt - Caches results with 60s TTL (matching codexQuotaFetcher pattern) - Supports GROK_AUTH_PATH env var override for testing - Registers in chat.ts before registerGenericQuotaFetchers Tests cover: missing auth, successful fetch with header verification, 401-triggered token refresh with retry, 60s cache TTL, and preflight integration. Closes #6444 * chore(quality): rebaseline chat.ts for #8127 own-growth --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
2a865aaaa7 |
feat(settings): configurable model catalog cache TTL (#8219)
* feat(settings): configurable model catalog cache TTL Add modelCatalogCacheTtlMs to DatabaseSettings with default 1500ms. Extend cache-config API route to accept the new field. Replace hardcoded catalog cache TTL with dynamic settings value. Add 'Cache' settings tab at /dashboard/settings/cache with sidebar entry, i18n keys, header description, and legacy route redirect. * feat(combo): add model connection filter toggle to ModelSelectModal Adds a 'Show configured only' checkbox below the search bar in ModelSelectModal that filters each provider group's models through hasEligibleConnectionForModel. Toggle state persists in localStorage. - Import hasEligibleConnectionForModel from domain/connectionModelRules - showConfiguredOnly state + localStorage persistence - connectionFilteredGroups memo layered on filteredGroups - Renders both provider section and empty state from connectionFilteredGroups * test(combo): add connection filter toggle tests for ModelSelectModal Three test cases: (1) hide excluded models when toggle on, (2) show empty state when all models excluded, (3) drop provider group when all its models excluded. All 3 tests pass. * fix(settings): correct cache-config route import + add route/tab coverage The cache-config route imported get/update helpers from a nonexistent module (@/lib/localDb/databaseSettings) and called an undefined updateSettings() in PUT, crashing every request. Import the real databaseSettings module (matching the sibling database/route.ts convention) and call updateDatabaseSettings(); idempotencyWindowMs is routed through the flat @/lib/db/settings module instead, since that is where it is actually read at runtime (idempotencyLayer.ts, runtimeSettings.ts) — it was never part of the databaseSettings "cache" section type. Also fixes a dashboard-typecheck regression in ModelSelectModal.tsx: the new connection-filter toggle called hasEligibleConnectionForModel() with activeProviders entries typed too narrowly to include providerSpecificData, which real connection objects carry at runtime. Adds: - tests/unit/cache-config-route-8219.test.ts: GET/PUT resolve without crashing, modelCatalogCacheTtlMs and idempotencyWindowMs round-trip. - tests/unit/ui/cache-settings-tab-bounds-8219.test.tsx: CacheSettingsTab min/max TTL bounds (100ms/60000ms) gate the Save button and surface a validation message; in-bounds values PUT correctly. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(quality): rebaseline sections.ts for #8219 own-growth --------- Co-authored-by: oyi77 <oyi77@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
b640b69078 |
feat(codex): support reference image edits (#8122)
* feat(codex): support reference image edits * docs(changelog): add Codex edit fragment * fix(codex): harden image edit admission * fix(security): redact image error credentials * fix(security): close error redaction bypasses * feat(codex): support multiple image references * fix(codex): preserve reference candidate semantics * chore(quality): rebaseline image-generation-handler.test.ts for #8122 codex image edits own-growth Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: 千乘妍 (Xiaoyaner) <xiaoyaner0201@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
18f1f667bf | fix(claude-web): align session transport and fallback (#8230) | ||
|
|
cc17b304ab |
fix(sse): Gemini TPM/RPD quota classification + combo cooldown-wait resilience (#8213)
* fix(sse): Gemini TPM classification, combo-cooldown-wait for auto/quota-share, and target-timeout floor
Gemini TPM/RPM 429s were misclassified as QUOTA_EXHAUSTED because
sanitizeErrorMessage() truncates to the first line, hiding Google's
metric name and retry hint on lines 2-3. Added a rawMessage field
(internal-only, never reaches the client) and classifyGeminiQuotaMetricFromText()
to classify from the untruncated text, reordered ahead of the generic
credits/daily-quota checks.
Widened comboCooldownWaitEnabled (wait out a short transient cooldown
instead of crystallizing a 429/503) from quota-share-only to also cover
auto-strategy combos, and raised the wait ceiling to 65s/130s-budget/90s-cap
to match Gemini's ~60s TPM/RPM windows.
The per-target timeout (DEFAULT_COMBO_TARGET_TIMEOUT_MS, 120s) was shorter
than the new 130s cooldown-wait budget, so a target could get cut off
mid-wait with a synthetic 524 instead of completing the retry. Added
resolveComboTargetTimeoutMsForCombo()/isComboCooldownWaitEligible() in
comboConfig.ts to raise the per-target floor to budgetMs+buffer only for
wait-eligible strategies (auto/quota-share), verified live: a 12-request
concurrent burst against a TPM-exhausted combo went from 2/12 succeeding
(10 x 524) to 12/12 succeeding with zero 503/524.
Also: liveGeminiShared.ts's sendAndValidate now fails fast on a 503
instead of retrying past it, and the health dashboard + request logger
surface TPM stats alongside RPM/RPD.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): combo-exhausted rejection logs now capture request body + attempted models
recordRejectedRequestUsage() (the fast path for combo requests that never
reach handleChatCore, e.g. all targets locked by resilience cooldown)
hardcoded provider: "-" and never passed a request body to saveCallLog(),
so /dashboard/logs entries for these failures were nearly useless for
debugging: no way to see the client's request or which models were tried.
- recordRejectedRequestUsage() now accepts requestBody and persists it
through the existing saveCallLog() artifact mechanism (same path
handleChatCore's own logging uses).
- Added summarizeComboAttemptedModels(), which reads the combo's own model
list (always available, unlike the response's combo-diagnostics headers —
a model-level resilience-lockout skip never touches the
exhaustedProviders/exhaustedConnections sets those headers are built
from) to populate a real "provider" value instead of "-".
- Wired both into the call site in src/sse/handlers/chat.ts.
NOTE: unrelated to the Gemini TPM/combo-cooldown-wait fix on this branch —
landed here per operator request, to be split into its own branch/PR.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* feat(sse): synthetic streaming keep-alive event + 5-minute Gemini cooldown-wait ceiling
Many clients enforce a first-SSE-byte timeout, which made it unsafe to wait
out a longer upstream rate-limit cooldown on a streaming request — the
client would abandon the connection before any bytes arrived. This landed
in two parts:
1. Synthetic startup "thinking" event (OpenAI chat/completions format):
the already-existing withEarlyStreamKeepalive wrapper (open-sse/utils/
earlyStreamKeepalive.ts, wired into /v1/chat/completions, /v1/messages,
/v1/responses since #2544) opens the SSE stream immediately once a
request runs past its threshold, but only ever sent empty/no-op
keepalive frames. Added a `startupFrame` option (defaults to
`keepaliveFrame` — zero behavior change unless a route opts in) so the
very first frame can carry real content instead. Wired
OPENAI_STARTUP_THINKING_FRAME (a reasoning_content delta: "OmniRoute:
got request, sending to provider") into /v1/chat/completions only —
Claude Messages and Responses API formats both require a preceding
envelope event (message_start / response.created) that a synthetic
pre-dispatch frame can't safely fabricate without risking a duplicate
envelope once the real stream arrives, so those two routes keep their
existing (safe, proven) keepalive frames unchanged.
2. Raised the "wait out a known cooldown, then retry" ceiling to 5 minutes
for both retry mechanisms, now that a client-side first-byte timeout is
no longer a risk on the (opted-in) route:
- comboCooldownWait (auto/quota-share combos, open-sse/services/combo.ts):
maxWaitMs hard clamp raised 90s -> 300s (src/lib/resilience/settings/
normalize.ts); defaults raised to maxWaitMs:90s/maxAttempts:5/
budgetMs:300s. comboConfig.ts's resolveComboTargetTimeoutMsForCombo
already derives the per-target timeout floor from budgetMs, so it
tracks the new ceiling with no further changes.
- waitForCooldown (direct, non-combo model requests, src/sse/handlers/
chat.ts): this mechanism had NO cumulative cap before — only a
per-wait cap (maxRetryWaitMs) and a retry count (maxRetries), so
maxRetries x maxRetryWaitMs could exceed 5 minutes with no ceiling.
Added a budgetMs field (mirrors comboCooldownWait) to
WaitForCooldownSettings/CooldownAwareRetrySettings, threaded a
requestRetryBudgetLeftMs tracker through chat.ts's requestAttemptLoop
(mirrors combo.ts's comboCooldownBudgetLeftMs), and made
getCooldownAwareRetryDecision refuse to wait once the cumulative
budget is exhausted even if the single wait is under maxRetryWaitMs.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): extend the synthetic keep-alive thinking event to /v1/responses
Live incident (OpenClaw, log id 1784407081908-cbc24f): a /v1/responses
request to gemini/gemma-4-31b-it took 56s to produce a first byte and the
client disconnected (499 request_signal_aborted) — the same client-first-byte-
timeout problem the previous commit fixed for /v1/chat/completions, but
/v1/responses only had the generic bare-comment keepalive (no content), so it
wasn't covered.
Added RESPONSES_STARTUP_THINKING_FRAME: a self-contained synthetic reasoning
item (response.output_item.added -> reasoning_summary_part.added ->
reasoning_summary_text.delta -> reasoning_summary_part.done), opened AND
closed within this one frame rather than left dangling — it never carries a
response_id, so it can't collide with the real upstream response's own
independent response.created lifecycle that follows. Mirrors the abbreviated
delta+part.done close pattern open-sse/utils/stream.ts's own
emitSyntheticResponsesReasoningSummary already uses for real mid-stream
reasoning content.
Wired into src/app/api/v1/responses/route.ts via the startupFrame option
added in the previous commit.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): combo cooldown-wait vars reset every setTry, crystallizing a bogus 503 instead of waiting
Live incident (log id 1784416706646-51): a request to the "default" combo
(strategy=auto, maxSetRetries=3) hit a real Gemini TPM 429 on both gemma-4
targets, correctly classified as a short 40s rate_limit lockout — then
crystallized a 503 "all upstream accounts are inactive" in 6.9s instead of
ever reaching the cooldown-aware wait.
Root cause: `lastError`/`earliestRetryAfter`/`lastStatus` were declared with
`let` INSIDE the `for (setTry...)` loop body, so they reset to null at the
start of every set-try. When both targets lock out on setTry 0, every
subsequent setTry (1..maxSetRetries) pre-skips both targets via the
isModelLocked check with no real dispatch — so on the FINAL setTry (the only
one whose values the post-loop decision reads, since it's gated behind
`if (setTry < maxSetRetries) continue`), lastStatus was null, hitting the
"!lastStatus" branch (ALL_ACCOUNTS_INACTIVE 503) and completely bypassing the
comboCooldownWaitEnabled / earliestRetryAfter wait logic — even though a
real 429 with a known ~40s retry-after WAS observed on setTry 0.
This bug predates today's Gemini TPM work (any combo with maxSetRetries > 0
whose targets all lock out on the first pass was affected) but was masked in
existing tests: the "auto strategy (2 models...)" regression test uses
maxSetRetries: 0, so it only ever runs ONE setTry iteration and never
exercises the reset-on-retry path. It also explains why the dedicated
12-concurrent-request burst test passed cleanly — with concurrent requests,
timing variance meant some request's FINAL setTry iteration still had a live
target to dispatch to, giving lastStatus/earliestRetryAfter fresh data. A
single isolated request has no such luck.
Fix: hoist lastError/earliestRetryAfter/lastStatus to just inside
dispatchWithCooldownRetry, before the setTry loop, so they persist across
set-tries (still reset fresh on each recursive dispatchWithCooldownRetry()
call after a wait, which is correct). recordedAttempts/fallbackCount/
exhaustedProviders etc. are intentionally left per-iteration (unrelated to
this bug).
New regression test in tests/unit/combo-quota-share-cooldown-wait.test.ts
reproduces the exact live scenario (2 targets, both lock out on setTry 0,
maxSetRetries: 3) — confirmed red (503) against the pre-fix code, green
(200, waits and retries) against the fix.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* test(sse): extend live Gemini workload to Responses API + add large-context TPM test
Two additions to the live Gemini test suite, both live-verified against the
dev instance:
1. sendAndValidate() (tests/integration/liveGeminiShared.ts) now accepts an
apiFormat: "chat" | "responses" parameter, building the Responses-API
request shape (input array, max_output_tokens) and parsing its SSE events
(response.output_text.delta / response.reasoning_summary_text.delta /
response.completed) via the new readResponsesSSEStream(). Wired into two
new tests in live-gemini-workload.test.ts ([30]/[31]), mirroring the
existing Chat Completions streaming coverage. Verified live: 24/25 + 5/5
payloads succeeded end-to-end through the new code path (the one failure
was a ~300s test-client fetch timeout unrelated to the Responses API code
itself — a separate, not-yet-addressed test-harness limitation).
2. genHugeContextMessage() builds a single message large enough (~4
chars/token estimate) to approach or exceed Gemini's free-tier TPM ceiling
(16000 input tokens/min for gemma-4) by itself. Every other prompt
generator in this file tops out around 1-2k tokens — nowhere near that
ceiling — so none of the existing workload tests ever exercised a REAL TPM
429, only RPM-style rate limiting. tests/integration/gemini-large-context-tpm.test.ts
sends two ~12-13k-token requests back-to-back (comfortably exceeding
16000/min together) to exercise the full path against production Gemini:
TPM classification, the comboCooldownWait retry, and the synthetic
keep-alive frame on a genuinely slow request.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): abandoned combo target dispatch now observes its own per-target timeout, fixing a permanent "pending" dashboard leak
Live incident (dashboard log id 1784418258231-14961a, reported as "an ongoing
request even though there's already a 200"): a combo target dispatch
abandoned by comboTargetTimeoutMs (open-sse/services/combo/targetTimeoutRunner.ts)
left a permanent phantom "pending" entry in the dashboard, even after the
overall combo request had already succeeded via a different retry.
Root cause: chatCore.ts's createStreamController — and everything downstream
that depends on it (withRateLimit's Promise.race against Bottleneck,
acquireAccountSemaphore) — only ever watches clientRawRequest.signal, which
is the ORIGINAL client's request signal (set once via buildClientRawRequest
and reused unchanged across every target dispatch in a combo). It has no
connection to targetTimeoutRunner.ts's OWN AbortController
(target.modelAbortSignal), which is what actually fires when
comboTargetTimeoutMs (300s) elapses. src/sse/handlers/chat.ts's
handleSingleModel bridge between combo.ts and handleSingleModelChat received
`target.modelAbortSignal` but silently dropped it — never forwarded it
anywhere. So when a target got abandoned (e.g. stuck inside a wedged
Bottleneck rate-limiter queue, see the WEDGED force-reset log line from the
same incident), its per-target timeout fired and let the COMBO move on and
retry successfully elsewhere — but the abandoned dispatch's own promise
chain never learned it had been superseded, so it hung forever waiting on a
signal that was never going to fire, and trackPendingRequest(false) (the
finalize call) never ran.
Fix: thread target.modelAbortSignal through as a new modelAbortSignal
runtimeOption, and merge it into clientRawRequest.signal (via the existing
mergeAbortSignals helper from open-sse/executors/base.ts) right before
dispatch, so an abandoned target's own promise chain now observes its abort
and can reach its cleanup path — new resolveDispatchClientRawRequest() makes
this mechanically testable in isolation.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): combo cooldown-wait state recording, rate-limit wedge recovery, OpenAI-format SSE error frames
Five related fixes surfaced by live incidents (dashboard log ids 1784457764961-73,
1784465227489-a2cbc0, 1784504040241-6f8b9a) while validating the Gemini TPM/cooldown-wait
work on this branch against real OpenClaw traffic:
- combo.ts: the model-lockout bail-out branches in dispatchWithCooldownRetry never
recorded lastStatus, so once every target in a set hit an existing lockout the final
check crystallized a bogus ALL_ACCOUNTS_INACTIVE 503 instead of reaching the
cooldown-wait decision, even with a real 429 + short retry-after observed.
- combo.ts/combo/types.ts: the "all credentials cooling down" pre-dispatch rejection
(buildModelCooldownBody) nests its retry hint as error.retry_after/reset_seconds, not
the top-level retryAfter every other 429 shape uses — combo's extraction only read the
latter, so earliestRetryAfter stayed null for this shape even after lastStatus was fixed.
- rateLimitManager.ts: the wedge-recovery watchdog used disconnect(), which releases the
heartbeat timer but never rejects jobs already QUEUED on that instance — orphaned
dispatches hung until the outer ~300s per-target timeout, well past real clients'
patience. Switched to stop({ dropWaitingJobs: true }), safe because the wedge condition
already requires RUNNING===0 && EXECUTING===0.
- earlyStreamKeepalive.ts: the in-band error frame emitted after committing to a 200 SSE
stream was hardcoded to Anthropic's `event: error` convention for every route, including
the OpenAI-format ones (/v1/chat/completions, /v1/responses) where that framing is
either invisible or malformed to a plain data-line parser. Added per-route
OPENAI_CHAT_ERROR_FRAME / OPENAI_RESPONSES_ERROR_FRAME and wired them in.
- chatCore.ts: persisted a synthetic clientResponse error body even when the client had
already disconnected (AbortError) before that body was ever computed — misleading the
dashboard into showing "what the client received" for a response that was never sent.
Also: RequestLoggerDetail.tsx — Provider/Client Event Stream panes lost their collapse
toggle when StreamSection replaced the collapsible PayloadSection (
|
||
|
|
ebe086ebb5 |
fix(sse): surface OpenRouter mid-stream error chunks instead of a false empty success (#8210)
* fix(sse): surface OpenRouter mid-stream error chunks instead of a false empty success OpenRouter (and similar OpenAI-compatible aggregators) can send an HTTP 200 SSE stream whose body carries a chat.completion.chunk with empty choices and a top-level error object instead of any delta -- e.g. the underlying provider hitting its own capacity limit mid-request. The Responses-API response translator's `!chunk.choices?.length` branch treated this exactly like a legitimate trailing-usage/no-op chunk, so the stream silently ended with response.completed / error: null / output: [] -- a false "successful but empty" response masking a real 502 provider_unavailable failure. Live repro: request 1784726796287-a45bb3 (OpenClaw via the default combo, nvidia/nemotron-3-ultra-550b-a55b:free via OpenRouter) got HTTP 200 with 0 tokens in/out after Nvidia returned "Worker local total request limit reached (33/32)" mid-stream; the client saw an empty completed response with no indication anything failed. Mirrors the Gemini mid-stream error fix (#4177): set state.upstreamError from the in-band error chunk so stream.ts's existing upstreamError handling takes over (sendCompleted emits status: "failed" with a real error object). Co-Authored-By: Markus Hartung <markus.hartream@gmail.com> * chore(quality): file-size baseline for own-growth (#8210) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Markus Hartung <markus.hartream@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
813bea4184 |
feat(vnc-session): persistent noVNC browser login for web-cookie providers (#7892)
* chore(ci): add .mergify.yml to main — Mergify only reads config from the default branch (#7168) * feat(vnc-session): persistent noVNC browser login for web cookie/token providers ## Why (the headless-install problem) OmniRoute's web cookie/token providers (ChatGPT Web, Gemini Web, Claude Web, DeepSeek Web, …) need a live browser session, but the gateway normally runs **headless** — as a systemd service, inside Docker, or on a VPS with no display. There is no desktop for the operator to log into the provider in. Today the operator has to obtain the session cookie/token *out of band* (open a real browser elsewhere, export cookies, paste them into the connection row). That is fiddly, breaks on every provider UI change, and is a non-starter on a headless box where you can't open a browser at all. This PR adds an **on-demand interactive login**: OmniRoute boots a containerized browser that exposes a noVNC web UI at the host. The operator opens that URL in *their own* browser, logs in normally, and OmniRoute then harvests the resulting cookies / localStorage back into the provider's `provider_connections` row over the DevTools Protocol. No display required on the host — the headless server renders the login into a container and the human just drives it through a web page. ## How we ran into this - The shipped `dist/` bundle has **no App Router source**, so the only visible seam was `dist/server-ws.mjs`'s `http.createServer` monkeypatch. That seam is **dead**: Next's standalone `startServer` creates its own http server in a way that bypasses the override, so a route registered there never fires (debug logs confirmed: zero requests reached it). The real seam is the Next **App Router** (`src/app/api/...`), which lives in the dev tree, not `dist/`. - **Chromium ≥130 forces the remote-debugging port onto `127.0.0.1`** and ignores `--remote-debugging-address=0.0.0.0`. A plain published port can't reach it, so cookie harvest needs an in-container TCP bridge to republish the loopback CDP onto `0.0.0.0`. We shipped that bridge, but the cleaner default is **Firefox** (`jlesage/firefox`): its debugger binds `0.0.0.0` out of the box, so harvest works with no bridge at all. - The CDP harvester **hung forever** on the first tries: the message handler was defined but never attached to the socket, so every `send()` promise stayed pending. We replaced Playwright's `connectOverCDP` (which stalls through the bridge) with a **raw `ws` client** and wired the handler — now resolves. ## What New management API (scoped like the other admin endpoints via `requireManagementAuth`): | Method | Path | Purpose | | --- | --- | --- | | GET | `/api/vnc-session` | list active sessions + supported providers | | GET | `/api/vnc-session/:provider` | session state | | POST | `/api/vnc-session/:provider/start` | boot browser container → returns `vncUrl` | | POST | `/api/vnc-session/:provider/harvest` | persist cookies into the provider row | | POST | `/api/vnc-session/:provider/touch` | defer idle auto-stop | | DELETE | `/api/vnc-session/:provider` | stop + remove the container | ## Implementation - `src/lib/vncSession/manifest.ts` — provider → login URL + cookie/token map + config - `src/lib/vncSession/harvest.ts` — raw-CDP cookie/localStorage harvester (`ws`) - `src/lib/vncSession/service.ts` — docker lifecycle, port allocation, idle sweep, DB write - `src/app/api/vnc-session/**` — App Router routes - `src/lib/gracefulShutdown.ts` — tears down running login containers on exit ## Browser image choice Default is **`jlesage/firefox`** (0.0.0.0-friendly CDP, no bridge). The Chromium image + in-container bridge lives under `docker/vnc-browser/chromium`, selectable via `OMNIROUTE_VNC_IMAGE`. See `docker/vnc-browser/README.md`. ## Config (env) `OMNIROUTE_VNC_IMAGE`, `OMNIROUTE_VNC_CONTAINER_VNC_PORT`, `OMNIROUTE_VNC_CONTAINER_CDP_PORT`, `OMNIROUTE_VNC_PROFILE_DIR`, `OMNIROUTE_VNC_IDLE_MS`, `OMNIROUTE_VNC_MAX_MS`, `OMNIROUTE_VNC_MAX_SESSIONS`, `OMNIROUTE_DOCKER_BIN` — all documented in the docker README. ## Tests `tests/unit/vnc-session.test.ts` — manifest lookup + credential mapping (cookie / token / whole-jar). All passing via the Node test runner. ## Notes - Docker is the only external dependency; if the `docker` CLI is missing, `start` throws a clear error and shutdown is a no-op. - No secrets are returned by any endpoint — only session metadata + ports. Co-authored-by: Sora <138304505+Capslockb@users.noreply.github.com> Co-authored-by: Bernardo <138304505+Capslockb@users.noreply.github.com> * refactor(vnc-session): derive provider credentials from shared contract * fix(vnc-session): harden CDP harvesting and credential filtering * refactor(vnc-session): scope lifecycle to provider connections * fix(vnc-session): sanitize and scope management routes * fix(vnc-session): use canonical provider list in API * test(vnc-session): align coverage with canonical manifest * docs(vnc-session): align browser setup with current implementation * fix(security): loopback-gate /api/vnc-session (Hard Rule #15/#17) The new /api/vnc-session/* routes spawn Docker containers via child_process.spawn (src/lib/vncSession/service.ts) but were never registered in LOCAL_ONLY_API_PREFIXES or SPAWN_CAPABLE_PREFIXES, so they were reachable from non-loopback callers (any manage-scope API key or dashboard session over a tunnel) - the same CVE class (GHSA-fhh6-4qxv-rpqj) those constants exist to close. Register VNC_ROUTE_PREFIX (already exported but unused in manifest.ts) in both prefix lists, and add a regression test asserting isLocalOnlyPath()/isLocalOnlyBypassableByManageScope() correctly classify the new prefix. Co-authored-by: CAPSLOCKB <138304505+Capslockb@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> |
||
|
|
e86e5bcc51 |
feat(media): Adobe Firefly image + video generation provider (#8006)
* feat(media): Adobe Firefly image + video generation provider
Add unofficial adobe-firefly media provider with full OpenAI-compatible
image and video generation: Nano Banana / GPT Image families, Sora 2,
Veo 3.1 (standard/fast/reference), and Kling 3.0.
Supports browser session cookies (auto IMS token exchange) or direct
IMS access tokens, async submit-and-poll against Firefly 3P endpoints,
aspect-ratio and resolution controls, and multi-account web-session UX.
Chat completions are intentionally rejected (media-only surface).
Includes unit coverage for registry wiring, payload builders, auth
resolution, and mocked generate happy-paths.
* fix(adobe-firefly): clio auth, discovery fallback, credits balance
Root-cause 401 invalid token against live firefly.adobe.com captures:
generate/discovery use x-api-key + IMS client_id clio-playground-web
(not projectx_webapp). Align headers/origin, dual cookie to IMS exchange
(clio first, Express fallback), BKS poll rewrite for /jobs/result.
Models: parse POST /v2/models/discovery + static fallback catalog from
adobe/get_models.txt; expand image/video registries.
Limits: GET firefly.adobe.io/v1/credits/balance (SunbreakWebUI1) with
total/remaining + free/plan detail quotas. Clarify cookie vs JWT UX.
Unit tests: 27/27 pass.
* fix(adobe-firefly): reject guest tokens from page-only cookies
Live repro with firefly.adobe.com Cookie export: IMS check with
guest_allowed=true returns account_type=guest (no AdobeID). That token
fails generate (401 invalid token) and credits/balance (403
ErrMismatchOauthToken). guest_allowed=false needs adobelogin.com IMS
session cookies which are not present in a page-only Cookie paste.
- Detect/reject guest JWTs; clear error tells user to paste Bearer JWT
- Prefer user JWT from HAR/mixed paste; improve credential extraction
- Update web-cookie + credential UX to recommend Authorization Bearer
Unit tests 29/29.
* fix(adobe-firefly): production auth, Limits, and 408 load handling
Live validation against firefly.adobe.com + packaged VibeProxy:
Auth / credentials
- Prefer IMS user JWT (Bearer from firefly-3p); reject guest tokens from
page-only cookies with an actionable error
- Extract JWT from Bearer, access_token=, IMS sessionStorage tokenValue,
and mixed HAR pastes; prefer non-guest tokens
- Strip JWT from Cookie header (undici Headers.append crash on mixed paste)
- Keep sherlockToken → x-arp-session-id + sanitized Cookie for generate
Limits
- credits/balance → Record quotas (firefly_total / free / plan) so
providerLimits caches them (arrays were ignored)
- Allowlist adobe-firefly + firefly in USAGE_SUPPORTED + APIKEY limits
- Live: 10000 plan credits parsed end-to-end after refresh
Generate
- Browser-shaped gpt-image body (size auto, no extra top-level size)
- Exponential 408 "system under load" retries (8 attempts) with clear
client message that 408 is Adobe capacity, not invalid token
- Live: generate returns proper 408 under load; balance/models stay 200
Tests: adobe-firefly unit suite 33/33 pass.
* fix(adobe-firefly): match live capture headers; add gpt-image-2
- Do not send firefly.adobe.com Cookie to firefly-3p (wrong-origin; soft 408)
- Lift sherlockToken only into x-arp-session-id
- Poll headers match status_check.txt (Bearer + accept, no x-api-key)
- Catalog gpt-image-2 alias → upstream modelVersion "2" (GPT Image 2)
- Shorter 408 retry budget so clients fail fast with clear message
- Unit suite 34/34
* fix(adobe-firefly): always send x-arp-session-id on generate (fixes 408)
Root cause of Bearer JWT → HTTP 408 colligo "system under load":
submit only set x-arp-session-id when sherlockToken was present in a
cookie paste. JWT-only credentials never sent the header, and Adobe
soft-blocks those requests with instant 408 (x-colligo-timeout:0.0).
A/B against a real user IMS token:
- det nonce + synthetic ARP → 200
- random nonce + synthetic ARP → 200
- det nonce without ARP → 408
Match adobe2api / GPT2Image-Pro:
- buildAdobeSubmitNonce = sha256(user_id + prompt[:256])
- buildAdobeArpSessionId = base64({sid, ftr}) synthetic session
- buildAdobeSubmitHeaders always sets both headers
Live adobeFireflyGenerateImage end-to-end: submit + poll → S3 presigned URL.
* fix(adobe-firefly): drop literal cred fallbacks + type-clean tests
Addresses pre-merge review feedback on #8006:
- Removes the `|| "literal"` fallback after resolvePublicCred() in
adobeFireflyApiKey()/adobeFireflyExpressClientId()/adobeFireflyBalanceApiKey()
(open-sse/services/adobeFireflyClient.ts). resolvePublicCred() already
always returns the decoded embedded default, so the literal fallback
was dead code that reproduced the exact env-or-literal anti-pattern
docs/security/PUBLIC_CREDS.md documents as BAD (Hard Rule #11).
- Replaces the 11 `@typescript-eslint/no-explicit-any` casts in
tests/unit/adobe-firefly.test.ts with concrete types
(Record<string, unknown>, Headers, Error-narrowing on the
assert.rejects predicate), matching the pattern already used
elsewhere in this suite. `no-explicit-any` is a hard ESLint error
under tests/ in this repo.
- Freezes file-size baseline entries for the new
open-sse/services/adobeFireflyClient.ts (1958 LOC, new-file cap 800,
mirrors the qoderCli.ts precedent for a legitimately large new
provider client), open-sse/config/imageRegistry.ts (800->821, new
adobe-firefly registry entry) and the +3 LOC growth in
src/lib/usage/providerLimits.ts (1000->1003).
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>
Co-authored-by: artickc <artickc@users.noreply.github.com>
|
||
|
|
1b010f6c40 |
feat(sse): add HyperAgent (hyperagent.com) unofficial web provider (#7994)
* feat(sse): add HyperAgent (hyperagent.com) unofficial web provider
Reverse-engineered from live SPA captures (hyperagent/*.txt):
Chat:
- Cookie session auth (full Cookie header)
- New thread via GET /threads/new (or POST /api/threads)
- POST /api/threads/{id}/chat with SPA feature flags + content
- SSE parse of text/session_start/session_end/done events
- Multi-turn sticky threadId + sessionId cache (history prefix + last assistant)
Models:
- Hardcoded catalog from SPA pricing map
- Pretty display names (Claude Fable 5) while wire modelId stays fable etc.
- /v1/models exposes pretty name; chat uses modelId
Limits:
- GET /api/settings/billing/usage → creditBlocks initialUsd/remainingUsd/usedUsd
- USD Credits quota for Limits page
Tests: 15/15 unit/executor-hyperagent
* fix(sse): HyperAgent execution mode + fable-latest wire model (no plan mode)
* fix(sse): document HyperAgent env vars + regenerate golden snapshot
Addresses pre-merge review feedback on #7994:
- Documents HYPERAGENT_USAGE_URL in .env.example and ENVIRONMENT.md
(OMNIROUTE_DATA_DIR was already documented via the sibling PromptQL
provider) so check-env-doc-sync.test.ts passes.
- Regenerates the provider-translate-path golden snapshot to include
the new hyperagent/ha registry entries.
- Swaps the local toNumber() helper in usage/hyperagent.ts for the
canonical @/shared/utils/numeric import (#7879 no-restricted-syntax
rule landed on the release branch after this PR was opened).
- Freezes file-size baseline entries for the new
open-sse/executors/hyperagent.ts (937 LOC, new-file cap 800) and the
+3 LOC growth in src/lib/usage/providerLimits.ts (1000->1003), both
irreducible to this PR's own provider-registration wiring.
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>
|
||
|
|
9020ed53f9 |
fix(grok-cli): require full auth.json on OAuth paste import (#7610) (#8027)
* fix(grok-cli): require full auth.json on OAuth paste import (#7610) The Grok Build paste path told operators to paste only the JWT "key" field, which creates connections with refresh_token=null that can never auto-refresh. Require the full ~/.grok/auth.json object (with refresh_token) in OAuthModal, and reject bare JWT pastes with a clear error. * fix(grok-cli): add behavioral test coverage for auth.json paste-import (#7610) Replace the source-regex-only test for the OAuth paste-import path with a real behavioral suite (bare JWT rejected, auth.json missing refresh_token rejected, multi-entry auth.json accepted, valid auth.json POSTed) using the existing grok-device-oauth-modal.test.tsx jsdom harness. Extract parseGrokCliPasteToken() into its own src/lib/oauth/utils/grokCliAuthJson.ts module so it is directly unit-testable and to keep OAuthModal.tsx's frozen file-size gate from growing (bump 1080->1100, justified in file-size-baseline.json, mirroring the existing extraction precedent on this file). Also fixes two pre-existing "JWT Token" label assertions that this PR's own tab rename ("Import auth.json") had left stale. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
fb6ea295bf |
feat(compression): add Responses tool-output engine (#8010)
* Add Responses tool-output compression engine * fix: enable Codex Responses stacked steps * fix(compression): share Codex tokenizer and rebase UI * fix(compression): sync MCP engine selection * fix(compression): i18n parity for codex-responses mode + rebaseline The codex-responses compression engine already imports the shared countTextTokens/resolveTokenizerEncoding from tiktokenCounter.ts (no duplicate encoder) and CompressionSettingsTab.tsx already threads the new mode through the existing useTranslations()/labelKey pattern - both pre-existing on this branch tip after rebasing onto release/v3.8.49. What was missing after the rebase: the new compressionModeCodexResponses / compressionModeCodexResponsesDesc keys existed only in en.json. Filled en-fallback into all 42 locales via scripts/i18n/fill-missing-from-en.mjs and added real pt-BR/vi translations. Also rebaselined the three files whose own growth (new codex-responses mode wiring) crossed the frozen file-size caps: open-sse/mcp-server/schemas/tools.ts, open-sse/services/ compression/strategySelector.ts, and src/lib/db/compression.ts. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
5dd3c76ad7 | feat: canonical numeric helpers + tier-1 (analytics) migration (#7879) (#7969) | ||
|
|
a48a75ef12 |
chore(quality): bump muse-spark-web file-size baseline 1388→1394 (the 401 ecto_1_sess cookie hint added 6 lines)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
19c3ff51e7 |
chore(quality): rebaseline file-size for own-growth from v3.8.49 merges (OAuthModal/muse-spark/combo + PricingTab/ComboDefaultsTab)
Legitimate own-growth from real features merged this cycle (#7735 grok OAuth chooser, #7528 muse-spark WS rewrite, #7301 combo cooldown-retry, #7972 pricing, #7973/#8008 combo). File-size ratchet is release-captain territory; owner-approved rebaseline. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
287802cf86 |
fix: repair pre-existing red gates on the release/v3.8.49 tip (#8055)
* fix(dashboard): resolve Kimi banner casing collision + shrink frozen test file (release tip) - Rename src/app/(dashboard)/dashboard/kimiSponsorBanner.ts to kimiSponsorBannerGate.ts so it no longer differs from KimiSponsorBanner.tsx only by the first letter's case (breaks next build on case-insensitive filesystems). Updates the sole importer (KimiSponsorBanner.tsx) and the two tests that reference it. - Extract the 8 Kimi/Moonshot featured-ordering tests out of the frozen tests/unit/providers-page-utils.test.ts (grown 3 lines past its 1294 cap by #8039's rebrand-comment update) into a new sibling file tests/unit/providers-page-utils-kimi.test.ts. No assertions dropped; both files pass in full (24 + 8 = 32 tests). * fix(sse): register PromptQlExecutor in the executor registry (release tip) getExecutor("promptql") had no entry in open-sse/executors/index.ts, so it silently fell through to DefaultExecutor's provider fallback, which issues a raw fetch() and returns the bare upstream Response instead of the executor wrapper shape {response, url, headers, transformedBody}. The real PromptQlExecutor class (open-sse/executors/promptql.ts) already honors the contract correctly — it was just never wired into the registry. Fixes tests/unit/executor-web-cookie-sweep.test.ts "promptql executor returns wrapper shape". * fix(i18n): backfill 2220 missing pt-BR keys to restore en.json parity (release tip) pt-BR.json fell behind after #7935 restored +2220 keys into en.json and vi.json but left pt-BR.json unmodified. Translated all missing entries to Brazilian Portuguese, preserving ICU/interpolation placeholders and existing terminology, and merged them mirroring en.json's key order so the diff is additions-only (the small comma-only deletions are pure JSON reformatting from new sibling keys). * fix(providers): repair 4 pre-existing catalog/registry reds on release tip - providers-constants-split.test.ts: APIKEY_PROVIDERS grew 182->187 (PR #7887 added 5 free-tier providers: ainative/aion/sealion/routeway/nara). Verified no dup/loss (6-family partition sums exactly to 187) and updated the stale expected count + comment trail to match. - cline registry: added the missing minimax/minimax-m3 free OpenRouter entry (#3321) and fixed the neighbouring nemotron-3-ultra-550b-a55b entry, which carried a stray ":free" id suffix and an imprecise 1_000_000 contextLength instead of the 1_048_576 the test (and every sibling 1M-context entry in this catalog) expects. - promptqlModels.ts / registry/promptql/index.ts: PROMPTQL_FALLBACK_MODELS's minimax-m3 entry was missing supportsVision, and the registry mapping dropped it entirely (only id/name were passed through) — it was the sole minimax-m3 entry across the whole registry not flagged multimodal, despite every other provider (minimax, minimax-cn, ollama-cloud, trae, bazaarlink, clinepass, codebuddy-cn, opencode-zen/go, synthetic, huggingchat, lmarena) agreeing MiniMax-M3 supports vision. Added the field to the PromptQlModel type and threaded it through. - tests/snapshots/provider/translate-path.json: regenerated the golden via UPDATE_GOLDEN=1. Diffed old vs new — zero providers removed, 5 added (ainative/aion/nara/routeway/sealion, matching #7887), and the only changed entry (cline) reflects the already-merged #7914 ClinePass header protocol change (Cline/<version> User-Agent + X-Task-ID) that a prior narrow golden touch-up missed capturing. * fix(docs): repair docs-sync/env-sync/repo-contract gates (release tip) Six pre-existing reds on release/v3.8.49, all "repo drifted from its own documented contract": - check-docs-counts-sync: free-tier headline was stale (~1.4B/~2.0B) vs the live catalog (~1.53B steady / ~2.15B first month, 43 pools). Updated README.md and docs/reference/FREE_TIERS.md to the live numbers and added a v3.8.49 correction note explaining the pool-count delta (39->43, #7840). Also fixed a soft executors-count drift in ARCHITECTURE.md (84->86, 268->271 providers) while touching that line. - release-green-docs-drift-7253: docs/proxy-subscriptions.md referenced a fabricated migration filename (123_proxy_subscriptions.sql); the real file is 131_proxy_subscriptions.sql. Fixed all 3 occurrences. - check-env-doc-sync + issue-7793-env-doc-sync-repro: OMNIROUTE_DATA_DIR (DATA_DIR fallback alias read by open-sse/executors/promptql/threadSticky.ts) was undocumented. Added to .env.example and docs/reference/ENVIRONMENT.md. - check-db-rules: src/lib/db/proxySubscriptions.ts (#7299) is a db-internal split of proxies.ts (kept under the frozen file-size cap) whose one export is already re-exported via proxies.ts -> localDb.ts. Added it to INTENTIONALLY_INTERNAL with the same db-internal justification used for identical split modules (apiKeyColumnFallbacks, providerNodeSelect, webSessionDedup) rather than a redundant direct re-export from localDb.ts. - mcp-server-hollow-dist-deps: the sanity test expected better-sqlite3 among the MCP bundle's static top-level external imports. That's been stale since the pre-#7878 migration to a cascading SqliteAdapter driver factory (createRequire()-based lazy require, not a static import); better-sqlite3 already has its own native-asset copy guarantee in assembleStandalone.mjs, unrelated to this test's EXTRA_MODULE_ENTRIES concern. Updated the assertion to a still-genuinely-static external (zod) with a comment explaining the change. No production runtime behavior changed — docs, .env.example, and a checker allowlist/test-expectation only. * fix(dashboard): repair stale UI component-shape test assertions (release tip) Two pre-existing reds in the dashboard UI component-contract cluster were caused by test assertions that had gone stale after intentional, correct refactors — not by real defects in the components: - quota-pool-wizard-multi.test.ts: the step-3 preview assertion required the literal single-line substring "connectionIds.map((cid)". Prettier (100-char width, project config) legitimately breaks the connectionIds.map(...).filter(...) chain across lines because of the multi-line callback body, so the literal never matches. PoolWizard.tsx still builds previewByProvider correctly by mapping over connectionIds; updated the assertion to a regex that tolerates the line break. - v388-phase1-screen-fixes.test.ts: the shared Select placeholder-guard assertion required the literal "!children && placeholder". An earlier, intentional i18n commit changed the hardcoded "Select an option" default to a translated fallback (`placeholder ?? t("selectOption")`), which requires parens around the ?? expression for operator precedence. The guard behavior is unchanged (still gated on !children); updated the assertion to match the current, correct guard shape. Both fixes are read-only test-file changes; no production behavior changed. review-reviews-v3814-fixes.test.ts still has one pre-existing, unrelated red (LEDGER-4: minimax-m3 registry entries missing supportsVision) that requires editing the promptql provider registry/catalog — out of this cluster's scope, left untouched and reported separately. * fix(providers): reconcile cline catalog contradictions + deterministic golden (release tip) The first tip-green pass introduced 3 regressions caught by CI on sibling guard tests: - clinepass-provider + cline-catalog-models-3321 encoded OPPOSITE expectations of the same cline model list (minimax presence, nvidia :free suffix). Reference upstream (OpenRouter free lineup) confirms nvidia/nemotron-3-ultra-550b-a55b:free (with :free, 1M ctx) is correct, so restore that id and fix #3321's stale no-:free assertion; add minimax/minimax-m3 (the real #3321 gap) to clinepass-provider's list. - check-db-rules-classification froze INTENTIONALLY_INTERNAL at 35; proxySubscriptions was the intentional 36th entry — add it + bump the count. - provider-translate-path golden stored a LITERAL Cline/3.8.49: clineAuth resolves the version from APP_CONFIG.version (stable), but the golden sanitizer collapsed only process.env.npm_package_version (unset under `node`, set under `npm run`) — so the golden was shard-dependent. Resolve APP_VERSION from APP_CONFIG.version like clineAuth and regenerate; now Cline/<APP> normalizes identically in every shard. * fix(services): type execFile signal/killed in classifyError + ratchet dashboard baseline (release tip) Pre-existing base-red on the tip's Fast Quality Gates (dashboard-typecheck), missed in the first inventory: - src/lib/services/installers/utils.ts TS2339 — `err.signal` was read off a value typed as NodeJS.ErrnoException, which @types/node does not declare `signal`/`killed` on (those belong to execFile's ExecFileException). Widen classifyError's param to type both, and drop the now-redundant `(err as … { killed })` cast. - Ratchet config/quality/dashboard-typecheck-baseline.json down: 5 baselined errors were fixed by already-merged PRs but never ratcheted (OAuthModal TS2769 4→3 / TS2345 4→3, CliproxyModelMappingEditor TS2339, CompressionPreviewAccordion TS4104, MonacoEditor TS2307). Baseline now 254, matching live — gate exits 0. |
||
|
|
577bbf3e47 |
[defer] feat(combo): universal cooldown-aware retry & auto-strategy combo-ref guard (#7301)
* feat(combo): universal cooldown-aware retry & auto-strategy combo-ref guard
Two changes:
1. Universal cooldown-aware retry (combo.ts):
- Remove strategy==="quota-share" gate from comboCooldownWaitEnabled
- Enables all 18 combo strategies (priority, weighted, round-robin, etc.)
to wait out a short transient cooldown and retry the full set
- Non-quota-share strategies use shouldWaitForComboCooldown directly
with earliestRetryAfter and reason="rate_limit" (no per-model lockout)
- quota-share retains its existing per-target model lockout logic
2. Auto-strategy combo-ref guard (autoStrategy.ts):
- expandAutoComboCandidatePool now detects kind==="combo-ref" entries
- When present, returns eligibleTargets without expanding to ALL providers
- Fixes scenario where an "auto" combo with combo-ref delegates to a
sub-combo but pulls in every model from every active provider
* feat(combo): global comboTimeoutMs + aggregated error diagnostics
Adds two features to improve combo resilience and debuggability:
1. Global combo timeout (comboTimeoutMs)
- Configurable per-combo via DEFAULT_COMBO_CONFIG (default 0 = disabled)
- After each target completes, checks if total elapsed time exceeds limit
- When exceeded, stops trying further targets and returns 504 immediately
- Backward-compatible: 0 preserves legacy unlimited-iteration behavior
2. Aggregated error diagnostics
- comboErrors array accumulates per-model failure details (model, status, error)
- On combo timeout or all-models-exhausted, returns a message listing the
first (up to 5) model-level errors with their HTTP status codes
- Enables operators to see WHICH models failed and WHY without digging
through individual server logs
* test(combo): cover universal cooldown-aware retry, comboTimeoutMs, and combo-ref guard
Adds/updates automated coverage for this PR's production changes (PR Test
Policy requires tests alongside src/open-sse/electron/bin changes):
- Update the "preserves the first failure status" expectation in
combo-routing-engine.test.ts: the aggregated per-model error-diagnostics
suffix is new intended output, not a regression.
- Add two new tests for the global comboTimeoutMs feature: the combo stops
dispatching further targets and returns 504/COMBO_TIMEOUT with aggregated
diagnostics once the ceiling trips, and comboTimeoutMs=0 (default) never
trips it.
- Rewrite the non-quota-share (priority) cooldown-wait scenario in
combo-quota-share-cooldown-wait-timing.test.ts: comboCooldownWait is no
longer gated on strategy === "quota-share", so a priority combo now waits
out a short 429 and re-dispatches too (via shouldWaitForComboCooldown with
reason "rate_limit"), instead of propagating immediately as before. Also
adds the disabled-flag counterpart for parity with the quota-share suite.
- Add coverage for the #COMBO-REF guard in expandAutoComboCandidatePool:
a combo whose models array contains a kind:"combo-ref" entry must not be
expanded to every model of every active provider.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* test(combo): keep the new comboTimeoutMs tests free of no-explicit-any
The two new tests initially copied the neighbouring tests' `any`-typed
handleSingleModel params / json() casts. Those neighbours are pre-existing
violations frozen in config/quality/eslint-suppressions.json at a count of 261
for this file, so the 7 new occurrences pushed it to 268 and broke `npm run
lint` (no-explicit-any is an error in tests/ since #6218; new violations must
be fixed, not re-frozen).
Type the new tests properly instead: `unknown`/`string` params, a
ComboErrorPayload interface for the parsed body, and drop the unused
`relayOptions: null as any` (the sibling combo cooldown suites already omit
it). Back to exactly 261 — the suppression file is untouched.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(combo): gate the universal cooldown retry on the REAL lock reason, not a hardcoded "rate_limit"
The universal cooldown-aware retry kept the quota-share path on
resolveComboCooldownWaitDecision but gave every OTHER strategy a shortcut that
hardcoded `reason: "rate_limit"` and fed shouldWaitForComboCooldown the
earliestRetryAfter directly.
comboCooldownRetry.ts documents TWO deliberate barriers ("SECURITY —
quota_exhausted must be excluded"): (1) the reason allow-list, and (2) the
maxWaitMs ceiling, explicitly called the SECOND barrier. Hardcoding the reason
removed barrier 1 for 17 of the 18 strategies and left only the ceiling — which
does NOT cover a quota_exhausted lock whose wait lands under maxWaitMs. In that
case the combo waits, redispatches against a model that is locked until the
quota resets, and burns the retry budget for nothing.
The shortcut's premise ("non-quota-share combos have no per-connection model
lockout tracking") is also false: recordModelLockoutFailure in the target loop
is not gated on quota-share, so every strategy records model lockouts and the
real reason is always available.
Fix: one decision path for every strategy, always through
resolveComboCooldownWaitDecision, so the reason always crosses the allow-list.
The lock lookup is now keyed on each TARGET's own model (via a new third
`target` arg on lookupLock) — quota-share combos are single-model/multi-account
so this is identical to the previous orderedTargets[0] behavior, but
heterogeneous combos (priority, weighted, round-robin, …) carry a different
model per target and would otherwise miss every lock but the first.
Regression guard (tests/unit/serial/combo-quota-share-cooldown-wait-timing.test.ts):
a priority combo where modelLockout.errorCodes=[403] leaves the 403's
quota_exhausted lock as the only one in play while a 429 crystallizes the
status, and the resulting wait is short enough that the ceiling lets it through
— so only the allow-list can stop it. Verified failing-then-passing: with the
hardcoded reason the combo makes 6 dispatches (wait+redispatch x2); with the
real reason it makes 2 and propagates the 429.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* chore(quality): rebaseline file-size for PR #7301 own growth (combo +91, combo-routing-engine test +68)
---------
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
|
||
|
|
4012bac41d |
fix(i18n): preserve remaining Vietnamese localization (#7935)
* fix(i18n): preserve remaining Vietnamese localization * chore(quality): rebaseline file-size cap for 9 dashboard components (i18n wiring) Restoring the Vietnamese localization on 9 dashboard components (useTranslations wiring + t()/tc() call-site swaps for previously hardcoded strings) grows each file by a small, irreducible amount. Bumps the frozen file-size-baseline.json caps to match, with a justification entry per the project's own ratchet policy. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(i18n): wire weekday localization + add missing qwen CLI description Two gaps left by this PR's own new contract tests, caught while reconciling the branch against the release tip: - CostOverviewTab.tsx added formatWeekdayLabel() but never called it; the Weekly Usage Pattern chart still showed raw English day abbreviations regardless of locale. Now maps weeklyPattern rows through it before handing them to WeeklyPatternCard. - cliTools.toolDescriptions was missing an entry for "qwen" (a baseUrlSupport:"full" tool) in both en.json and vi.json, failing the PR's own cli-catalog-display-contract.test.ts. Covered by the PR's existing tests/unit/dashboard-localization-contract.test.ts and tests/unit/cli-catalog-display-contract.test.ts (both now pass). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * i18n(vi): backfill the 4 proxySubscription keys #7299 added to en.json #7299 (proxy subscriptions) merged while this branch was rebasing, adding settings.proxySubscriptionsTab and settings.proxySubscription.error.{LOCAL_CORE_ENDPOINT_INVALID, NEEDS_CORE_NOT_CONFIGURED,NO_USABLE_NODES} to en.json. This PR's own i18n-vi-completeness contract asserts full en↔vi key parity, so the merge of the current release tip surfaced them as missing. Adds the Vietnamese translations, keeping the SS/VMess/Trojan/VLESS/SOCKS5 technical terms verbatim. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: nguyenha935 <nguyenha935@users.noreply.github.com> |
||
|
|
a865fddb26 |
fix(perplexity-web): multi-step empty content + advanced-quota cooldown (#7930)
Perplexity's live multi-step/copilot streams can surface the advanced_models_quota_low upsell instead of any answer text when the account's weekly advanced-model budget is exhausted. Detect it and return HTTP 429 with reset_seconds/Retry-After (mapped to rate_limited_until) instead of a silent empty-content error. Also fixes plan-goal (thinking) extraction for live multi-step streams that deliver the plan as an RFC-6902 diff patch against plan_block instead of a materialized plan_block object — those goals were previously dropped. Reconstructed against release/v3.8.49: most of the original "empty content" fix in this PR was independently and differently addressed on release already (extractAnswerFromFinalText + longestMarkdownAnswer), so only the two non-overlapping pieces above are ported here. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
f909b1d45e |
fix(resilience): don't cool down accounts or trip the breaker on client aborts (#7908)
* fix(resilience): don't cool down accounts or trip the breaker on client aborts When the caller drops the connection mid-stream, the in-flight request surfaces request_signal_aborted, "Client disconnected", or a DOM AbortError with no upstream status code. These shapes were counted as provider failures: the serving connection went into cooldown, the provider circuit breaker accrued failures, and healthy accounts ended up marked unavailable from client-side cancellations alone. Treat client aborts as local stream lifecycle events (#4602 policy): extend isLocalStreamLifecycleError() to recognize abort shapes and skip connection disable and breaker accounting for them. Genuine upstream failures (5xx/429/401) are still counted. Fixes #7907. * fix(resilience): guard the two remaining breaker-trip call sites against client aborts (#7907) PR #7908 correctly wired isLocalStreamLifecycleError() into shouldSkipConnDisable() and chatHelpers.ts's onStreamFailure, but two separate breaker._onFailure()-triggering call sites were purely status-code gated and never checked it, so a client-side abort (no upstream status, defaults to 502, error='request_signal_aborted') still tripped the whole-provider circuit breaker — the highest blast-radius of the 3 resilience mechanisms: - src/sse/handlers/chat.ts: the single-model, non-combo terminal-failure path called breaker._onFailure() directly, bypassing the isFailure option (which only applies inside breaker.execute()). Extracted the predicate into shouldTripProviderBreakerForResult() and added the missing isLocalStreamLifecycleError guard. - open-sse/services/combo/comboPredicates.ts::shouldRecordProviderBreakerFailure(), used by handleComboChat's executeTarget (open-sse/services/combo.ts), gained the same guard via a new optional `error` field. Added tests/unit/circuit-breaker-abort-provider-trip-7907.test.ts exercising both real predicates directly (not just the isolated isLocalStreamLifecycleError() helper) — confirmed red on the unfixed code (missing export) and green after the fix, alongside the existing #4602/#7908/combo-breaker-429 suites (24/24 pass, no regressions). file-size-baseline.json: +1 combo.ts (irreducible call-site wiring for the new `error` field) and +1 chatHelpers.ts (own growth from the PR's already-verified onStreamFailure guard, surfaced only now since fast-gates PR->release skip check:file-size). Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> Co-authored-by: insoln <insoln@ya.ru> * test(quality): register circuit-breaker abort tests in stryker tap.testFiles The two new tests (circuit-breaker-abort-provider-trip-7907, circuit-breaker-client-abort) import mutated modules (circuitBreaker.ts, comboPredicates.ts) but were not listed in stryker.conf.json tap.testFiles, tripping check:mutation-test-coverage --strict. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> |
||
|
|
cec2f58c24 |
chore(quality): prune stale ESLint suppression (db-migration-runner-account-identity)
#7843's test reorganization removed the 2 no-explicit-any usages that this suppression covered, leaving a stale entry that fails lint:json --max-warnings 0 (exit 2, 'suppressions left that do not occur anymore') on the release tip for every fresh PR run. Pruning tightens the gate — no rebaseline. |
||
|
|
65e0aeda79 |
[Part 1/3]refactor(qwen): replace legacy Qwen Code and remove OAuth provider (#7866)
* refactor(cli): remove legacy Qwen Code integration * refactor(qwen): remove deprecated Qwen OAuth provider * feat(cli): rebuild Qwen Code integration for upstream V4 * fix(qwen): clear stale CLI auth on reset * test(qwen): align retired provider coverage * fix(db): renumber qwen-cleanup migration 129 -> 130 release/v3.8.49 tip took slot 129 via #7843 (usage_history_codex_strong_identity, itself renumbered from 128 during the #7838/#7840 base-red cleanup) after this branch forked; renumber remove_unregistered_qwen_data to 130. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
d813bedf5c |
fix(cli): register ESM alias resolver for @/ paths under global install (#7808)
* chore(ci): add .mergify.yml to main — Mergify only reads config from the default branch (#7168) * fix(ci): add the auto-enqueue pull_request_rule to the Mergify config (queue_conditions alone are eligibility-only) (#7179) * fix(ci): migrate Mergify auto-enqueue to merge_protections_settings.auto_merge_conditions (rules-based path is EOL 2026-07-16) (#7216) * fix(ci): drop Mergify batch settings (batching is a paid-tier feature; free plan queue is serial) (#7220) * fix(ci): merge queue tolerates the advisory dast-smoke failure (its GH-hosted build hang dequeued every attempt) (#7225) * test(ci): make the #6634 selfref guard hermetic — main's copy hard-fails every PR (#7341) main's copy of this test still does git I/O inside a unit test: const baseSrc = git(['show', 'origin/main:' + FILE]); Runners check out a shallow single ref, so origin/main does not resolve and the test dies with 'fatal: invalid object name origin/main'. Every PR into main fails Unit Tests (7/8) on it — today that is #7313, #7315, #7316, #7334, #7336 and #7337, six PRs red on a defect none of them introduced. #7313 has no other red at all. release/v3.8.49 already carries a fix ( |
||
|
|
d2ab1893ed |
feat(catalog): map unmapped free tiers, add navy + aihorde, surface keyless providers (#7840)
* feat(catalog): map unmapped free tiers, add navy + aihorde, surface keyless
Seven providers whose free tier was documented upstream but never reached our
catalog. Five of them we could already route — only the quota was missing.
Providers already routable, quota now mapped:
- requesty (200 req/day), ovhcloud (2 req/min per IP, anonymous), agnes
(permanently free), glm (GLM-4.7/4.5-Flash are Free on the official pricing
table). All registered as recurring-uncapped: their free tier is capped in
REQUESTS, not tokens, so inventing a token figure would inflate the headline.
The "~30M/month" that circulates for GLM belongs to BigModel.cn (a separate
Chinese offering) and is deliberately not recorded.
New providers:
- navy: one shared 150K tokens/day pool (~4.5M/month) drained by a per-model
token_multiplier. Registered as a SINGLE pooled row — summing its ~149 free
models would overcount ~149x.
- aihorde: crowdsourced volunteer GPUs, keyless via the documented anonymous
key. No tool calling and a 120s timeout, because requests queue for minutes.
Also:
- kilo-gateway reconciled against its live /models list (7 -> 13 models) and
flagged with the new trainsOnPrompts field: every free Kilo model reports
mayTrainOnYourPrompts: true, so the privacy cost now sits next to the quota.
- Free-tier page gains search, provider/keyless filters, per-row type badges,
a "no API key required" section and a curation-date freshness indicator.
- catalogUpdatedAt comes from an explicit FREE_CATALOG_CURATED_AT constant
rather than the data file's mtime: a standalone build rewrites timestamps on
deploy, which would advertise a months-old catalog as updated today.
Net effect on the headline: 462 -> 484 models but 1.371B -> 1.376B tokens,
because only navy publishes a token quota. That is the point — coverage grows
without the number lying.
* refactor(providers): derive one answer for "does this need an API key?"
"Works without a credential" lived in three registries that disagreed, and only
three providers were classified the same way in all of them:
- NOAUTH_PROVIDERS.noAuth -> whether the connect form hides the field
- RegistryEntry.authType / anonymousApiKey -> what the executor really sends
- FreeModelBudget.freeType === "keyless" -> how the catalog labels it
getCredentialRequirement() now derives the answer from the two sources that
describe real behaviour, returning none | optional | oauth | required. It adds
no list to maintain: registering a provider the usual way is enough. oauth is
deliberately NOT "works without a credential" — there is no key to paste, but
signing in is still a barrier, and calling it keyless would mislead.
anonymousApiKey outranks noAuth: AI Horde ships a documented anonymous key AND
honours a real one for higher queue priority, so it is "optional" rather than
"none" even though the form hides the field.
Fixes one real inconsistency this branch introduced: ovhcloud was catalogued as
keyless while its registry demanded a key. Verified live — the anonymous tier
answers /chat/completions with no Authorization header, and a BAD key returns
403 instead of degrading, so authType is now "optional" and the executor
attaches the header only when a real credential exists.
The 10 pre-existing divergences (agy, blackbox, pollinations, puter, qwen-web,
…) are frozen in KEYLESS_CATALOG_DRIFT with a stale-entry check: the gate blocks
new drift, and fails if a frozen entry stops drifting so the debt list cannot
outlive the debt. Resolving each one means confirming upstream behaviour, not
editing a list.
* fix(dashboard): build "no API key required" from routing, not freeType
Probing all ten providers the catalog labels `keyless` (2026-07-20) showed the
label answers a different question than the UI was asking:
blackbox 401 "No api key passed in."
friendliai 401 "no authorization info provided"
iflytek 401 Unauthorized
sparkdesk 401 Unauthorized
puter 401 "Missing authentication token"
muse-spark-web 403 (authHeader is a session cookie, not a key)
qwen-web 200 but serves the WAF HTML page, not the API
liquid 404 — endpoint moved; needs its own audit
pollinations 200 with real choices <- genuinely key-free
ovhcloud 200, and 403 on a BAD key <- fixed earlier in this branch
`freeType: "keyless"` means "free access not quantifiable in tokens" — it sits
beside `oauth` in FREE_TIERS.md for exactly that reason. The new section was
listing those rows under "No API key required", which would have sent users to
providers that reject them. It now derives from getCredentialRequirement().
pollinations was the one real find: it answers with no credential at all, so its
registry entry moves from apikey to optional and it leaves the recorded list.
The list is computed in the route handler, not the component: deriving it
client-side pulled the whole 201-entry provider REGISTRY into the browser
bundle. The component takes `noCredentialProviders` from the payload and stays
dumb — which is also why the vitest run could not resolve REGISTRY through the
`@omniroute/*` alias and silently classified every provider as credentialed.
* fix(test,providers): resolve open-sse in vitest; point liquid at its live host
vitest.config.ts / vitest.mcp.config.ts had no `@omniroute/open-sse` alias, so
imports from open-sse resolved to undefined instead of throwing. Tests stayed
green while every lookup silently returned a default — that is how the free-tier
card asserted on provider credentials with REGISTRY never loaded. Both configs
now mirror the tsconfig paths, and tests/unit/ui/open-sse-alias.test.tsx pins it
by asserting on values only reachable through REGISTRY (aihorde's anonymous key,
pollinations' optional auth), so a future regression fails loudly.
liquid pointed at api.liquid.ai, which stopped serving the API — every path now
returns a Vercel 404 HTML page, so routing failed with an unparseable body
instead of a clean error. The live OpenAI-compatible host is inference.liquid.ai
(403 {"detail":"Not authenticated"} without a key). Both verified 2026-07-20.
Swept every free-catalog provider for the same failure. Five more looked dead on
a /models probe (agentrouter, coze, kiro, nlpcloud, puter) but answer their chat
endpoint with real API JSON — a 404 on /models only means the path is not
exposed. They are untouched: liquid was the only genuine casualty.
* test(providers): move the APIKEY_PROVIDERS partition count to 180
This PR adds one gateway provider (navy), so the frozen entry-count and the
family-partition sum both shift by one. The assertions are moving targets by
design — they exist to catch a provider silently landing in two families or in
none, not to freeze the catalog size.
|