Repository navigation
fix(proxy): probe a proxy's assigned provider host instead of a generic target - #10664
Merged
diegosouzapw merged 147 commits intoAug 20, 2026
Conversation
…ic target The sweep and the "Test All" endpoint only ever probed the generic PR cheaper-inference#8411 target (httpbin.org/ip by default). cheaper-inference#10420 established the same principle for per-connection "Test Connection" checks: a generic probe can stay green while the real model surface fails. This generalizes that to proxy health. src/lib/proxyHealth/providerProbeTarget.ts resolves, for a given proxy, the real host of the provider it is assigned to (getProxyWhereUsed), then a GET /models probe through the existing url helpers (getRegistryEntry + addModelsSuffix) — no new URL construction. Eligibility is deliberately narrow: only providers with a registry entry carrying a singular `baseUrl` (or `testKeyBaseUrl`) qualify. This excludes, by construction and without any special-casing: - antigravity/agy: its registry entry only has `baseUrls[]` (a custom urlBuilder), no singular `baseUrl` to read. Its "Test Connection" probes by a real generation call (streamGenerateContent) — reusing that for a periodic proxy sweep would bill a generation per proxy per sweep, which this change never does. - self-hosted / custom-baseUrl providers: their real target lives in a connection's providerSpecificData, not the provider itself. A proxy is assigned to a provider, never to one specific connection, so there is no single answer to reuse. The guard on the resolved URL is https-only (no registry entry uses plain http today) and rejects a query string, split out as isProbeSafeBaseUrl so it is testable without a real registry entry — same principle as resolveWebCookieProbe's guard. A resolved target can still be one nobody can reach: some registry baseUrls are placeholders (databricks's default is a 16-zero azuredatabricks.net host) that fail DNS/TLS for every proxy, not just a broken one. testOneProxy and testSingleProxy already treat a 5xx from the target as inconclusive rather than a proxy fault; a connection-level failure against a provider-resolved target gets the same treatment now — only the operator-configured generic target is trusted enough for "can't connect" to mean "proxy is at fault". Target resolution itself degrades to the generic target on any error too, so a DB hiccup can never make a proxy silently drop out of a sweep's results. The generic target's HEAD request works for httpbin.org/ip but not necessarily for a real provider's /models endpoint (many reject HEAD). The probe method is now GET for a provider-resolved target and stays HEAD for the generic one — the generic path is otherwise untouched. PROXY_HEALTH_USE_PROVIDER_TARGET (default enabled) restores the prior behavior when set to "false". Default-on is deliberate: with the safeguard above, a proxy is never penalized for a provider's target being unreachable, so there is nothing left for an opt-in default to protect against. Only the registry-backed proxy_assignments table is read. The legacy per-provider proxy config has no reverse (proxy -> provider) lookup; building one would be new machinery. A legacy-only assignment degrades safely to the generic target, same as no assignment at all. Refs cheaper-inference#8411
* Update SETUP_GUIDE.md * docs: correct Windows data directory note Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…er-inference#10491) * fix(cli): recognize {connections} envelope from /api/providers GET /api/providers returns {connections, total} (src/app/api/providers/ route.ts:78), but `omniroute test --all-providers` and `omniroute oauth providers` both parsed the response as `data.providers ?? data.items ?? data` -- an object, not an array -- so `.filter` threw "(data.providers ?? data.items ?? data).filter is not a function" on every call. keys.mjs already had the correct fallback chain (`data.keys || data.connections || data.items || data`); apply the same `connections` field to both remaining call sites. * test(cli): cover provider connections envelope Add regression coverage for both CLI consumers of the /api/providers connections envelope. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…aper-inference#10498) * fix(compression): add i18n support for less-code and terse-prose Translates less-code output style to pt-BR, vi, ja, and id. Adds missing vi translation to terse-prose caveman mode. Removes less-code from English-only allowlist and updates matrix tests. Fixes cheaper-inference#10426 * docs(compression): add output styles coverage table Adds the requested Output Styles matrix to the compression guide covering styles, supported languages, and intensity levels. Fixes cheaper-inference#10426
…ab Duo direct_access 401 (cheaper-inference#10365) (cheaper-inference#10499) * fix(providers): fall back to public Code Suggestions endpoint on GitLab Duo direct_access 401 (cheaper-inference#10365) * fix(providers): extend GitLab Duo 401 fallback to the connection-test path (cheaper-inference#10365) The chat-completion path (open-sse/executors/gitlab.ts) already falls back to the public Code Suggestions completions endpoint when the direct_access exchange is rejected with 401, but testOAuthConnection() / the dashboard Retest button still reported the connection unhealthy on the same 401 — even though a real chat request through that connection would have succeeded via the fallback. Apply the identical fallback contract to the connection-test path (first attempt and the post-refresh retry), sharing the predicate with the executor via shouldFallbackToPublicCodeSuggestions. --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
…act connection labels in terminal errors (cheaper-inference#10314) (cheaper-inference#10501) * fix(resilience): keep combo quality and auth reasons separate and redact connection labels in terminal errors (cheaper-inference#10314) * fix(resilience): sanitize identifiers in error text, add explicit terminal-status policy, fix classifier ordering (cheaper-inference#10314) Four gaps in the prior combo-error-aggregation fix: - formatComboOutcomes() only redacted connection identifiers in the model label, never in the raw upstream error TEXT — a proxy echoing a connection/account id back in its error body leaked it into the client-facing terminal message. Redact both. - The terminal HTTP status was still `lastStatus` — whichever target happened to fail last, independent of the other targets' reasons. Add resolveComboTerminalStatus(): preserve a 4xx only when every eligible target's failure is genuinely "the request is invalid" (model-class); a heterogeneous mix (e.g. a quality failure + a sibling's 401) now normalizes to a 5xx-class status reflecting an infra/provider problem, never a misleading client error borrowed from an unrelated target. - classifyComboOutcome()'s ordering had `status === 408 || status >= 499` checked before `status >= 500`, making the provider branch permanently unreachable — every real 5xx (500/502/503/504) was silently mislabeled as "timeout". Fixed to an exact match (408/499) and gave 429 its own explicit `rate_limit` kind instead of falling into the generic "model" (request-invalid) bucket by accident. - Added an integration-level regression driving the real handleComboChat wiring end-to-end (quality failure + sibling 401, and a success-after- quality-failure case), not just the pure aggregation helpers. Updated three pre-existing tests whose assertions encoded the OLD last-writer-wins contract this fix intentionally supersedes (cheaper-inference#8486 Part B antigravity retryAfter tests, two combo-routing-engine status/message tests) to the new, more precise contract; verified the underlying cheaper-inference#8486 concern (wrong target's retryAfter header) is still honored under the new status policy. --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
…requests fail fast with a clear error (cheaper-inference#10225) (cheaper-inference#10503) * fix(combo): surface context-overflow before compression so oversized requests fail fast with a clear error (cheaper-inference#10225) * fix(combo): make context-overflow deferral target-aware for native Codex passthrough (cheaper-inference#10225) The deferral added by the prior commit checked only operator-named compression exclusions when deciding whether at least one target "can compress" — it never accounted for native Codex Responses passthrough targets, which chatCore.ts unconditionally excludes from compression (compressionExcluded = nativeCodexPassthrough || ...). Deferring on such a target's account let an oversized request skip both the combo preflight AND compression, reaching fetch() uncompressed. Thread the same request-shape facts chatCore.ts uses (shouldUseNativeCodexPassthrough: provider/sourceFormat/endpointPath/body/ headers) down into getKnownContextOverflow so the deferral decision can never drift from chatCore's own — a native-codex-passthrough target now never counts as "compressible", so a pool made only of such targets keeps the fast local 400 instead of a wasted round trip. Adds regression coverage: the pure getKnownContextOverflow target-aware check, an end-to-end handleComboChat proof that a native-codex-only pool fails fast with zero dispatches, and two real handleChatCore-path tests proving compression actually reduces the dispatched body when eligible, and that a still-too-large-after-compression request is rejected locally without an upstream call. --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
… month' (cheaper-inference#10381) (cheaper-inference#10509) * fix(dashboard): count live usage_history rows in Free Tier 'used this month' (cheaper-inference#10381) * fix(dashboard): use an indexable UTC month-range predicate for used-this-month (cheaper-inference#10381) sumUsageTokensThisMonth() filtered usage_history with substr(timestamp, 1, 7) = strftime('%Y-%m', 'now') — a substr() expression SQLite cannot use a range index on, and fragile against any timestamp that isn't exactly ISO-shaped. Replace with an indexable inclusive-start/ exclusive-end UTC range: timestamp >= <month start> AND timestamp < <next month start>, matching the ISO 8601 format saveRequestUsage() already writes. Adds a boundary regression test: the first instant of the current month is included, the last instant of the previous month is excluded, and a next-month row is excluded too (covers the upper bound substr() could never express). --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
* fix(providers): test token-backed web sessions * fix(providers): restrict token-web-session test dispatch to validated providers Narrow shouldUseApiKeyConnectionTest to the token-kind web-session providers that actually have a token-aware connection validator (deepseek-web, kimi-web, tinycms-web, copilot-m365-web, copilot-web, zai-web). WEB_SESSION_CREDENTIAL_REQUIREMENTS marks more providers as kind: "token" than have a matching validator in SPECIALTY_VALIDATORS (hailuo-web, microsoft-designer-web, t3-chat-web, promptql) — those were falling through to the generic cookie-based validateWebCookieProvider probe, which sends the stored credential as a Cookie header and treats most non-401/403 responses as valid, so an invalid token could be reported as a healthy connection. Add regression coverage for hailuo-web and promptql (plus microsoft-designer-web and t3-chat-web) proving they stay off the API-key test path, and for every currently validated token-kind provider proving they still use it. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…ion on model-unsupported 400 (cheaper-inference#10525) * fix(settings,auth): default debugMode to false and skip account rotation on model-unsupported 400 * fix(auth): disambiguate model-unsupported from auth-credential 400 The model-unsupported guard used MODEL_ACCESS_DENIED_PATTERNS directly, which also matches auth-credential errors like 'invalid api key for model X'. Add the AUTH_CREDENTIAL_ERROR_PATTERNS exclusion (same as checkFallbackError) and use provider_model_unsupported log reason. Addresses maintainer feedback on PR cheaper-inference#10525 Signed-off-by: Minxi Hou <houminxi@gmail.com> * fix(auth): narrow model-unsupported guard to avoid misclassifying account-scoped entitlement 400s The cheaper-inference#10460 guard reused MODEL_ACCESS_DENIED_PATTERNS directly, which also matches ambiguous "access"/"permission" phrasing (e.g. "does not have permission to access this model") that commonly signals an ACCOUNT-scoped entitlement gap (PRO vs free tier) rather than a genuinely provider-wide unsupported model — a different account of the same provider may still have access, so those must keep rotating normally instead of being short-circuited. Extract isProviderModelUnsupported400() in accountFallback.ts: reuses the same AUTH_CREDENTIAL_ERROR_PATTERNS exclusion checkFallbackError's 400 branch already applies, narrowed to a strict subset of unambiguous "provider does not serve this model at all" phrasings. auth.ts now calls this shared helper instead of testing the broader patterns in isolation, and exposes the sanitized reason ("provider_model_unsupported") on the returned result, not just in the log line. Also fix DATA_DIR test-isolation ordering in account-fallback-service.test.ts: it was assigned after the first dynamic import of accountFallback.ts, which transitively imports src/lib/db/core.ts (DATA_DIR is captured once at module-load time), so the intended isolated test directory was silently never used. Move the assignment before any transitive DB import, and add regression tests for the 3-account rotation contract: exactly one upstream call for an unambiguous provider-wide 400 with the combo advancing to the next target, continued rotation for account-scoped 401/403/429 and for the permission/entitlement 400 case that motivated this narrowing. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Signed-off-by: Minxi Hou <houminxi@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…r-inference#10533) * fix(models): honor Codex combo context overrides * test(codex): align discovery context expectation * test(models): align Codex route limits * test(models): align remaining Codex route limits
…heaper-inference#10534) * fix(sse): clear quota_exhausted cooldown when real window recovers The claude-token-fallback combo was not auto-returning to Sonnet/Opus after a subscription 429 recovered. maybeClearRecoveredQuotaState() was honoring the synthetic 1h cooldown (SUBSCRIPTION_QUOTA_COOLDOWN_MS, persisted when no upstream reset was parseable) instead of the REAL per-window resetAt returned by the scheduled quota poller, so the connection stayed locked long past the actual quota reset. Add windowStillExhaustedAfterRealReset() and use it to decide recovery per-quota-window: a quota_exhausted connection now clears as soon as no governing window is still exhausted with a future-or-unknown real reset, instead of waiting out the synthetic cooldown. Falls back to the previous synthetic-cooldown guard when the fetch has no quota object at all (degraded/failed shape) so existing behavior is unchanged there. Preserves the existing kimi-coding partial-refresh semantics: an exhausted window with no parseable resetAt still blocks recovery. * fix(sse): preserve Claude extra-usage block from general quota recovery maybeClearRecoveredQuotaState()'s new per-window recovery check (added in this branch) only inspected usage.quotas, so a Claude connection blocked by the extra-usage guard (lastErrorSource: "extra_usage") could be released just because the session/weekly quota windows looked recovered, even while extraUsage.queued was still true. Extra-usage blocking is orthogonal to quota-window exhaustion and must only be released by syncClaudeExtraUsageStateIfNeeded (buildClaudeExtraUsageConnectionUpdate). Add a guard that keeps the connection locked when lastErrorSource is "extra_usage", the blockExtraUsage policy is still enabled, and the fresh usage snapshot still reports extraUsage.queued === true. Add an integration test walking the real fetchLiveProviderLimitsWithOptions -> syncClaudeExtraUsageStateIfNeeded -> maybeClearRecoveredQuotaState call chain with recovered quota windows but extraUsage.queued=true, asserting the connection stays unavailable with lastErrorSource still "extra_usage". Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…e Go (cheaper-inference#10540) * fix(deepseek): align V4 reasoning efforts * docs(changelog): note DeepSeek effort fix * fix(deepseek): align OpenCode V4 effort aliases * test(deepseek): align effort alias expectation * fix(deepseek): scope low effort to v4 * fix(opencode-go): route DeepSeek V4 through Responses
…eaper-inference#10542) * feat(providers): optional AI Horde API key and live image catalog Allow a registered Horde key on the no-auth connection and send it for chat and image jobs. List only image models that currently have workers, and generate through Horde's native async API. # Conflicts: # open-sse/config/imageRegistry.ts # src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx # src/shared/constants/providers.ts # src/sse/services/auth.ts * fix(providers): validate AI Horde keys against find_user The OpenAI-compatible /v1/models probe returns 200 for any Bearer token on oai.aihorde.net, so Check always succeeded. Use Horde's /v2/find_user lookup instead; an empty key still counts as the optional anonymous path. * chore(changelog): name the AI Horde fragment for cheaper-inference#10542 * fix(images): harden AI Horde optional-key selection and outbound fetches - Optional-key selection now honors connection health (rate-limit cooldown and terminal/unavailable test status) before handing a stored key back, rotating to the next healthy key or falling back to the anonymous no-auth path instead of using an unhealthy stored key. - Route the Horde submit/check/status/cancel and catalog calls through the repository's bounded outbound-fetch helper (timeout, no more bare fetch()) and route R2 image downloads through the established bounded remote-image fetch (SSRF host guard, DNS-rebinding pin, streaming byte cap, redirect limit) instead of an unbounded fetch(). - Extend the generation deadline to cover the full request lifecycle (catalog freshness check, submit, polling, and image download), and add a regression test proving that exceeding the deadline issues a DELETE cancel to Horde's API rather than only timing out locally. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: pqr <pqr@soraka.ititti.es> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…er-inference#10544) * fix(a2a): use a constant-time bearer compare in /api/a2a/tasks * fix(a2a): drop new Function from tasks-auth test in favor of dynamic import The regression test for the constant-time bearer compare loaded tokensMatch and authenticateA2A by regex-extracting their source and eval'ing it via new Function, which trips the repo's no-new-func/no-implied-eval ESLint rules (error-level everywhere, including tests). Export both helpers as a test seam from the route module (mirrors the existing bridgeSecretMatches/authRouteInternals pattern) and import them directly in the test instead. Also drops the now-unused eslint-disable directives. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…e#10554) * fix(vision): preserve high detail for inline images * fix(vision): scope high-detail image default to OpenCode clients defaultImageDetail() was applied at prepareUpstreamBody, the shared upstream-body prep path for every provider and format, not just the OpenCode path the fix targets. Gate it on isOpencodeClient (the existing User-Agent/x-opencode-* header signal already used for bypassDefaultToolLimit at this call site) so non-OpenCode callers keep the provider's own image detail default. Adds a regression test covering a non-OpenCode caller against the same opencode-zen provider. * fix(vision): document and test the global vs OpenCode-only detail scope The OpenCode-only high-detail default in chatCore/upstreamBody.ts (defaultImageDetail, gated on isOpencodeClient) forwards the caller's own image_url.detail and was already correctly scoped in a prior commit on this branch. The internal vision-bridge describe self-loop (visionBridgeHelpers.ts) is architecturally global: VisionBridgeGuardrail runs for every caller/provider whenever the target model lacks vision support, and there is no client-identity signal at that layer to gate on. Its describe prompt explicitly asks the vision model to transcribe visible text, so requesting "high" detail unconditionally is justified on its own merits (OCR accuracy), independent of the OpenCode motivation. Adds a compatibility assertion proving the Anthropic wire-format branch of the same describe self-loop carries no `detail` field (it has no such concept) and is therefore unaffected by this default, and documents the split (OpenCode-only forwarding vs. global describe default) in docs/security/GUARDRAILS.md. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: rinseaid <rinseaid@rinseaid.net> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…sing (cheaper-inference#10565) Native gemini-embedding-2 400s with a dead-end credentials error even though openrouter/google/gemini-embedding-2 already serves 3072-d vectors. Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…ference#10566) GET /v1/models listed google/gemini-embedding-001 but omitted google/gemini-embedding-2 even though that id already returns 3072-d vectors. Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…-inference#10568) Jina-compatible clients POST /v1/multimodal-embeddings and currently get HTTP 404 unknown_route from the catch-all. Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…er-inference#10569) Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…cheaper-inference#10571) * fix(opencode): session stability, free-tier routing, and CLI defaults - Wire generateSessionId() into opencodeHeaders so x-opencode-session is a deterministic fingerprint instead of randomUUID() per request, enabling upstream prompt caching across a conversation - Thread request body through buildHeaders() so session fingerprint has access to model, system, messages, and tools - Default CLI header synthesis to ON (opt-out via false), align values with 9router proven defaults (opencode/desktop/global) - Auto-echo listing-valid model names for noAuth providers so response.model matches /v1/models listing - Short-circuit free-tier model resolution to opencode provider first to prevent prefix inference misrouting when catalog is unreachable * fix(opencode): make free-tier default flip self-consistent + add coverage PR cheaper-inference#10571 flipped OPENCODE_SYNTHESIZE_CLI_HEADERS to on-by-default and changed the synthesized UA/client/project default values, but shipped with 2 broken assertions in the existing cheaper-inference#5997 regression test and no coverage for the new session-fingerprinting, free-tier routing, or noAuth echoModel logic (Hard Rule cheaper-inference#18). - Update tests/unit/opencode-cli-headers-synthesis-5997.test.ts to match the new on-by-default behavior and new default values; add an explicit opt-out coverage test so the forward-only path is still guarded. - Fix 20 further test failures in tests/unit/opencode-executor.test.ts and tests/unit/refactor-buildHeaders-opencode.test.ts caused by the same default flip (pin OPENCODE_SYNTHESIZE_CLI_HEADERS=false for the characterization suites that predate cheaper-inference#10571; use a genuinely CLI-looking UA where the preserved-UA test requires one). - Fix a real bug found via TDD while adding the mandated free-tier routing regression test: the big-pickle/*-free short-circuit in open-sse/services/model.ts checked activeProviders?.has("opencode") literally, but getActiveProviderSet() canonicalizes every connection's provider id through resolveProviderAlias(), which rewrites "opencode" to "opencode-zen" via a manual override — so an active no-auth opencode connection could never satisfy the check. Now checks both opencode-family candidate ids. Proven with a test that fails on the original code and passes with the fix (both connections active with a stale synced catalog omitting big-pickle). - Extract the noAuth-provider echoModel aliasing in chatCore.ts into a pure, directly-testable helper (open-sse/handlers/chatCore/noAuthEchoModel.ts), matching the existing chatCore god-file decomposition pattern. - Add regression tests for generateSessionId()-based x-opencode-session fingerprinting (stable within a conversation, changes on model/message changes), the free-tier routing short-circuit, and the noAuth echoModel aliasing. - Add the changelog.d/ fragment and sync docs/reference/ENVIRONMENT.md's OPENCODE_SYNTHESIZE_CLI_HEADERS/OPENCODE_USER_AGENT/OPENCODE_CLIENT/ OPENCODE_PROJECT rows to the new defaults. Does NOT resolve whether flipping OPENCODE_SYNTHESIZE_CLI_HEADERS's default was the right call, and does NOT touch the separate open PR cheaper-inference#10357 which flips the same flag with a different literal default value - that decision is left to the maintainer at merge time. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…_search (cheaper-inference#10575) Add githubSkillTools to getAllToolDefinitions() so the searchable MCP catalog matches TOTAL_MCP_TOOL_COUNT, which already counts them. The GitHub skill tools were registered and counted but missing from the catalog, so omniroute_tool_search could not surface them. Adds regression tests at both layers: catalog aggregation and client-visible discovery via the MCP client. Co-authored-by: Brandon Bennett <brandonbennett@macbookair.myfiosgateway.com>
…niRoute (cheaper-inference#10581) * feat(providers): complete Jina AI via OmniRoute including Omni multimodal Dashboard and env keys share one Jina credential pool, native v5 Omni {text}/{image}/{content} docs pass through /v1/embeddings intact, and classify/segment/search are proxied without a third unused Jina card. * chore(changelog): name Jina complete-provider fragment for cheaper-inference#10581 * feat(providers): make Gemini Embedding 2 multimodal work via OmniRoute Route gemini-embedding-2 through embedContent/batchEmbedContents so N OpenAI input items become N vectors, pass through native multimodal parts, and use dashboard Gemini keys (GEMINI_API_KEY only as fallback). * fix(providers): resolve rebase fallout for Jina/Gemini embeddings - narrow the two new no-explicit-any violations introduced by this PR (validateJinaFoundationProvider's params + catch, search.ts's normalizeJinaSearchResponse data param) - cast credentials to Record<string, unknown> at the two quota-preflight call sites in src/sse/services/auth.ts so the new JinaEnvCredentials / GeminiEnvCredentials union members type-check without loosening the allRateLimited narrowing used elsewhere in the same function Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…no credentials (cheaper-inference#10584) * fix(audio): fall back nested STT models when the prefix provider has no credentials Bare ids such as deepgram/nova-3 prefix-match the native provider and 400 when that key is missing, even if OpenRouter lists the same model. Retry the gateway and mention qualified catalog ids in the error. Closes cheaper-inference#10583 * test(audio): scope whisper-1 fallback test to a 2-provider registry nanogpt was added to AUDIO_TRANSCRIPTION_PROVIDERS (already merged, unrelated to this fix) with a bare "whisper-1" model id, which now intercepts findAlternateAudioProvider's first candidate before the qualified-alias branch this test exists to cover. Scope the test to a local {openai, openrouter} registry subset so it deterministically exercises the qualified `${provider}/${model}` fallback regardless of future providers that also list a bare "whisper-1" id. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…e#10601) * fix(xai): cap chat history at xAI 800-message limit xAI returns 413 when messages/input exceed 800 items. Token compression never fires on a long tool loop that still fits the context window, so trim at the executor edge after Responses expansion and drop orphaned tool pairs from the cut. * chore(changelog): attach PR number to xAI 800-message fragment * fix(xai): resolve TS2339 generic assignment in capXaiRequestHistory Drop the T extends Record<string, unknown> generic on capXaiRequestHistory and type it directly as Record<string, unknown> -> Record<string, unknown>. Assigning next.messages / next.input onto a generic T was rejected by TypeScript even though every call site already passes/consumes a JsonRecord (= Record<string, unknown>), so no caller relied on the generic preserving a narrower type. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: mikolaj92 <mikolaj92@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…nference#10607) Whisper-compatible upstreams pick the decoder from the multipart filename against an allow-list (flac, m4a, mp3, mp4, mpeg, mpga, oga, ogg, wav, webm) that has no `opus`, and OmniRoute forwarded the client's filename verbatim. The same bytes transcribed as `note.ogg` and 400'd as `note.opus`. Since /v1/audio/speech emits audio/opus for `response_format=opus`, clients re-uploading their own voice notes hit this on every round trip. A `.opus` file is Opus in an Ogg container (RFC 7845), so `.ogg` is a truthful relabel and is already on the allow-list. Rewrite the extension in getUploadedFileName, the single choke point feeding buildMultipartBody. The OpenRouter STT path had the same root cause with a quieter symptom: `.opus` matched neither its extension list nor its MIME map, so it fell through to the "wav" default and announced Opus bytes as WAV. Map both the extension and audio/opus to its already-supported ogg container. Fixes cheaper-inference#10588
…cheaper-inference#10781) Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
…-inference#10782) Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
…eaper-inference#10783) Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
…ocks (cheaper-inference#9692) (cheaper-inference#10791) Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
…e account (cheaper-inference#9708) (cheaper-inference#10792) Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
…heaper-inference#10734) (cheaper-inference#10793) Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
…heaper-inference#10017 control-line rules (cheaper-inference#10796) Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
…llision cheaper-inference#10722 (Token Kiosk) and cheaper-inference#10729 (Cursor) each independently bumped the gateways/specialty-media APIKEY_PROVIDERS count by one in the same merge-train batch; neither PR could see the other's bump, so the merged tree landed at 230 while the surviving hardcoded assertion still said 229. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…tics (cheaper-inference#10797) Merged — the 5 pre-existing tests that broke from this PR's intentional 404→401 remap (single-model no-credentials) are now realigned to the new contract. Thanks!
…fallback and combo hops cheaper-inference#10792 (cheaper-inference#9708) added a same-account retry for retryable 502/503/504/507 transport failures, applied uniformly inside handleSingleModelChat. Two other paths call into the same function recursively/iteratively and each carries its own documented single-call guarantee that the retry silently broke: - Emergency fallback (cheaper-inference#1731): exactly one hop to the free fallback model, no extra calls against an already-exhausted provider. The retry was doubling that call whenever the fallback model itself returned a transient-looking status. - Combo routing: target-level fallback is the combo's own policy (next target, not same-account retry). The retry delayed that policy and could surface the wrong terminal status when a later combo/global-fallback hop threw. Both regressions were already covered by existing tests in chat-route-coverage.test.ts (asserting exact call counts / preserved status) — confirmed red on the release tip before this fix, green after. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…inference#10826) Passo 10 of the Radar go-live: publish the OmniRoute catalog export the Radar server consumes to a stable URL, so the 1 GB-RAM private server pulls it via RADAR_EXPORT_URL instead of depending on the deploy-time snapshot. - scripts/release/radar-export.mjs: emits {geradoEm, budgets, totais, registry, provenance} from the catalog config modules. Provenance (sourceCommit, sourceRef, runUrl, generatedBy) is never fabricated — unknown fields stay null. - .github/workflows/radar-export.yml: on main catalog changes / manual dispatch / weekly, generates the export and clobbers the stable 'radar-export-latest' release asset (gh release, GH_TOKEN — checkout persist-credentials:false). - tests/unit/radar-export.test.mjs: consumer contract (budgets[] non-empty) + provenance null-when-unknown + GitHub-env reflection. Stable URL for RADAR_EXPORT_URL: https://github.com/diegosouzapw/OmniRoute/releases/download/radar-export-latest/export-omniroute.json Re-baselines zizmorFindings 190->192 (+2 unpinned-uses @vn, the repo-wide deliberate convention; artipacked auto-fixed). Co-authored-by: Xiangzhe <bakryun0718@proton.me>
Today's merge-train batch1 (cheaper-inference#10722 Token Kiosk, cheaper-inference#10729 Cursor) each added one new APIKEY_PROVIDERS entry, bringing the live provider count to 343 — the hardcoded '342' in README.md, AGENTS.md, llm.txt, package.json's description, PROVIDER_REFERENCE.md, and 4 hero/comparison SVGs went stale as a result. check:docs-counts (STRICT) now passes; regenerated PROVIDER_REFERENCE.md via npm run gen:provider-reference. The v3.8.50 growth-log table row in README.md (line 66) is left as-is — it's a historical point-in-time snapshot, not a live claim. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…ference#10723) Merged — locally validated (72/72 focused tests, typecheck:core clean, all static gates green) after resolving base-drift conflicts (catalog.ts cooperative-yield insertion point, modelMetadataRegistry.ts snapshot-param signature). CI's red checks (Unit Tests fast-path shards, Fast Quality Gates, Docs Gates) are confirmed PRE-EXISTING base-red on the pure release tip — reproduced tests/unit/db-driver-bundling-externals.test.ts, tests/unit/model-catalog-runtime-invalidation.test.ts and others failing identically against origin/release/v3.8.50 with zero PR content, unrelated to this change. Thanks for the design and for absorbing cheaper-inference#10724's value here — great work on both review rounds!
…ence#8367) Merged — locally validated (30/30 focused tests: chatcore-codex-account-pool, codex-account-cooldown-write, codex-account-pool, providers-route-codex-account-pool, resilience-explain-codex-account, sse-auth-codex-account-pool; typecheck:core clean; file-size/complexity/cognitive-complexity/changelog gates all green). Merges clean against the current release tip with zero conflicts. Great refactor — extracting persistCodexQuotaState out of chatCore.ts into a proper codexAccount/ module with virtual quota pool isolation is a solid improvement. Thanks!
…eaper-inference#10162) Merged — per owner decision on RFC cheaper-inference#10141: gateway token estimates (chars/4) become advisory-only for combo routing/context checks, never a hard pre-dispatch 400. Locally validated (31/31 focused tests across capability filtering, target resolution, and the cheaper-inference#8841 repro), file-size/changelog gates clean, merges conflict-free against the current release tip. Thanks for the well-scoped fix and for flagging this as an RFC first!
…nce#10806) Merged — locally validated (61/61 focused tests: early-stream-keepalive, chat-body-admission, responses-parse-once-4041, responses-route-early-keepalive-wiring; file-size/changelog gates clean, merges conflict-free against the current release tip). Good catch replacing the synthetic reasoning placeholder with a real response.in_progress bookkeeping event — keeps event-level watchdogs (Codex etc.) happy without any replayable fake reasoning content. Thanks!
…h check (cheaper-inference#10652) Merged — validated together with a batch of related maxmad64bis PRs in one combined worktree (typecheck:core clean, complexity/cognitive-complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
…r-inference#10662) Merged — validated together with a batch of related maxmad64bis PRs in one combined worktree (typecheck:core clean, complexity/cognitive-complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
…reakdown (issue cheaper-inference#10670) (cheaper-inference#10679) Merged — validated together with a batch of related maxmad64bis PRs in one combined worktree (typecheck:core clean, complexity/cognitive-complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
…kInterval (cheaper-inference#8443) (cheaper-inference#10687) Merged — validated together with a batch of related maxmad64bis PRs in one combined worktree (typecheck:core clean, complexity/cognitive-complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
…per-inference#10780) Merged — validated together with a batch of related maxmad64bis PRs in one combined worktree (typecheck:core clean, complexity/cognitive-complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
…cheaper-inference#10694) Merged — locally validated (23/23 focused probe-isolation tests, typecheck:core clean, file-size/changelog gates green). Reconciled with today's cheaper-inference#8367 (codexAccount module extraction, merged earlier): the persistCodexQuotaState closure this PR touched had been extracted into persistCodexChildQuotaResponse — applied the same probe-origin isolation guard (!shouldIsolateProbeFailures()) at its new call site instead of reintroducing the old inline closure. Thanks for closing this real gap!
…aper-inference#10654) Merged — locally validated in a combined batch worktree with the related proxy-health-probe PRs (typecheck:core clean, gates green). Thanks!
… checks (cheaper-inference#10657) Merged — locally validated (15/15 focused tests, typecheck:core clean, file-size/changelog gates green) after resolving base-drift against cheaper-inference#10654 (both landed today, same file — combined import block, no logical conflict). Thanks!
…w/OmniRoute into HEAD # Conflicts: # .env.example # docs/reference/ENVIRONMENT.md # src/app/api/settings/proxies/auto-test/route.ts # src/lib/proxyHealth/scheduler.ts
diegosouzapw
merged commit Aug 20, 2026
4821f9f
into
cheaper-inference:release/v3.8.50
2 of 4 checks passed
3 tasks done
3 of 5 tasks
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
…ic target (cheaper-inference#10664) Merged — locally validated (16/16 focused tests, typecheck:core clean, file-size/changelog gates green) after resolving base-drift against cheaper-inference#10654/cheaper-inference#10657 (both landed earlier today, same file chain — combined additively, no logical conflict). Nice chain of proxy-probe-target fixes, thanks!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
(
httpbin.org/ipby default). fix(antigravity): classify geo-blocked egress, exclude account, real connection probe #10420 established the same principle for per-connection"Test Connection" checks: a generic probe can stay green while the real model surface fails.
This generalizes that to proxy health.
src/lib/proxyHealth/providerProbeTarget.tsresolves, for a given proxy, the real host of theprovider it is assigned to (
getProxyWhereUsed), then probesGET {baseUrl}/modelsthroughthe existing url helpers (
getRegistryEntry+addModelsSuffix) — no new URL construction.baseUrl(ortestKeyBaseUrl) qualify.antigravity/agyonly hasbaseUrls[](a customurlBuilder), so it is excluded by construction, without any special-casing — its"Test Connection" flow probes by a real generation call, and this change never bills one.
Self-hosted / custom-baseUrl providers are excluded too: their real target lives in a
connection's
providerSpecificData, not the provider itself, and a proxy is assigned to aprovider, never to one specific connection.
baseUrls are placeholders(
databricks's default is a 16-zeroazuredatabricks.nethost).testOneProxyandtestSingleProxyalready treat a 5xx from the target as inconclusive rather than a proxyfault; a connection-level failure against a provider-resolved target now gets the same
treatment — only the operator-configured generic target is trusted enough for "can't connect"
to mean "proxy is at fault". Target resolution itself falls back to the generic target on any
error, so a DB hiccup can never make a proxy silently drop out of a sweep's results.
HEADworks forhttpbin.org/ipbut not necessarily for a realprovider's
/modelsendpoint (many rejectHEAD). The probe method isGETfor aprovider-resolved target and stays
HEADfor the generic one — the generic path is otherwiseuntouched.
Related Issues
blocked-outcome half is fix(api): a target refusing the egress IP is not a healthy proxy #10654)
Validation
npm run lintTests Added Or Updated
tests/unit/provider-probe-target.test.ts(new, 16 tests):isProbeSafeBaseUrltested directly (https-only, query-string rejection, non-http(s)schemes, empty input) — no registry entry needed, so this guard's own correctness doesn't
depend on the registry never drifting.
resolveEligibleProviderUrlchecked against two real registry entries:groq(eligible,plain
baseUrl) andantigravity(ineligible —baseUrls[]only, no singularbaseUrl).resolveProviderProbeTargetchecked against a real database (assignProxyToScope): noassignment, provider assignment, ineligible-provider assignment, the kill switch off, a
global-scope assignment (must not be mistaken for a provider one), and multiple assignments
(first eligible one wins).
Coverage Notes
src/lib/proxyHealth/providerProbeTarget.tsis new and fully covered by the 16 tests above.scheduler.tsandauto-test/route.tseach gain a few lines of wiring (target/methodselection); covered indirectly through the existing
proxy-health-*suites, which still passunchanged.
Reviewer Notes
PROXY_HEALTH_USE_PROVIDER_TARGET(default enabled) restores the prior behavior when set to"false". Default-on is deliberate: with the inconclusive-on-connection-failure safeguardabove, a proxy is never penalized for a provider's target being unreachable, so there is
nothing left for an opt-in default to protect against — happy to flip it if you'd rather ship
this opt-in regardless.
proxy_assignmentstable is read. The legacy per-provider proxyconfig (
/api/settings/proxy?level=provider&id=...) has no proxy→provider reverse lookup;building one would be new machinery rather than reuse. A legacy-only assignment degrades
safely to the generic target, same as no assignment at all.
/modelsmeasures much (some web-cookie hosts return 401/404for an unauthenticated GET) — that's fine for a reachability check, since the proxy is still
correctly classified as "ok" (it relayed), just not a signal that the provider account itself
is healthy. That's out of scope here; account/credential health is a separate, existing
mechanism.