Files
OmniRoute/docs/routing/AUTO-COMBO.md
Diego Rodrigues de Sa e Souza 555b21d296 Release v3.8.37 (#5053)
* chore(release): open v3.8.37 development cycle

* chore(ci): harden release flow — ratchet decoupling, fast-path drift gates, build-scope guard, heap default (#5054)

Implements improvements 1-4 from the v3.8.36 release benchmark (_tasks/release-bench/v3.8.36/PLANO-MELHORIA.md):

1. Quality Ratchet decoupled from flaky coverage (ci.yml): the shard→coverage→ratchet
   chain meant a single flaky Coverage Shard SKIPPED the whole Quality Ratchet on the
   release PR (v3.8.36 #4854), so cycle drift only surfaced post-merge in #5029. The job
   now runs on !cancelled(); coverage download is continue-on-error and the ratchet runs
   --allow-missing, so the DETERMINISTIC gates (eslint/complexity/cognitive/duplication/
   codeql) stay blocking even when coverage is unavailable.

2. Fast-path drift gates (quality.yml PR→release): added check:complexity,
   check:cognitive-complexity, and a new lightweight check:pack-policy (pack-artifact
   unexpected-files check WITHOUT a build, via --policy-only) so drift + stray-tarball-file
   regressions are caught/rebaselined PER-PR instead of cascading onto the release PR.

3. Build heap default 4096→8192 MB (build-next-isolated.mjs): the clean graph peaks
   ~3.9 GB and brushed the old 4 GB ceiling; 8 GB gives headroom. Comment notes heap is
   NOT the fix for a poisoned scope (run check:build-scope instead).

4. check:build-scope gate (new): fails if .ts/.tsx/.js/.jsx files in the tsconfig scope
   exceed a threshold — catches worktrees/cruft leaking into the build scope (the v3.8.36
   OOM root cause: 355,215 vs 4,547 files) BEFORE it detonates next build. Wired into the
   fast-path.

* fix(auth): only trust forwarding headers from loopback TCP peers (#4689)

Integrated into release/v3.8.37 — loopback-gated forwarding headers (IP spoofing fix). Cherry-picked onto current release tip; ipUtils.test.ts 9/9 green.

* fix(codex): treat OAuth 401 as unrecoverable refresh failure (#4686)

Integrated into release/v3.8.37 — codex OAuth 401 treated as unrecoverable refresh. Cherry-picked onto release tip; token-refresh-service.test.ts 38/38 green.

* fix(translator): preserve reasoning_effort for non-Copilot Responses clients (#4688)

Integrated into release/v3.8.37 — preserve reasoning_effort for non-Copilot Responses clients. Cherry-picked onto release tip; tests 47/47 green.

* fix(translator): coerce tool descriptions to strings in OpenAI normalization (#4675)

Integrated into release/v3.8.37 — coerce tool descriptions to strings in OpenAI normalization. Cherry-picked onto release tip; tests 3/3 green.

* feat(sse): x-omniroute-strip-reasoning header to drop reasoning_content (#4678)

Integrated into release/v3.8.37 — x-omniroute-strip-reasoning header. Cherry-picked onto release tip (resolved chatCore.ts/headers.ts adjacency conflict, kept resolveCompressionHeader + isStripReasoningRequested); tests 8/8 green.

* fix(combo): flatten Anthropic tool messages + tool history to prevent upstream 503 (#4648)

Integrated into release/v3.8.37 — flattenToolHistory helper (combo anti-503). Cherry-picked onto release tip; tests 9/9 green.

* feat(headroom): proxy lifecycle management + dashboard UI (Docker sidecar supported) (#4649)

Integrated into release/v3.8.37 — headroom proxy lifecycle (status/start/stop, local-only + spawn-capable per Rules #15/#17). Cherry-picked onto release tip; lifecycle 7/7 + route-guard 43/43 + check:cycles green.

* feat(cli): multi-model support for Factory Droid CLI (#4682)

Integrated into release/v3.8.37 — Factory Droid multi-model support. Cherry-picked onto release tip (kept readJsoncConfig + droidCustomModels imports); droid-custom-models 11/11 green.

* fix(providers): require Default Model in compatible-provider API-key setup (#4641)

Integrated into release/v3.8.37 — require Default Model in compatible-provider API-key setup. Cherry-picked fix + test-move onto release tip (kept release providerSpecificData + QuotaScrapingFields; fixed moved-test import path; baseline rebaseline unneeded, 865<866); UI test 2/2 green.

* fix(dashboard): stop double-masking already-masked API key in list (E2E 3/9 regression) (#4671)

Integrated into release/v3.8.37 — render server-masked key verbatim (drop redundant maskKey call). Note: release's maskKey already guards '****' (since v3.8.34), so this is a safe simplification; added a contract test pinning the **** passthrough invariant (2/2 green, would fail against the pre-guard maskKey = the historical double-mask bug).

* chore(quality): rebaseline file-size for rc17 PR batch drift

Own growth from the merged rc17 PRs (#4678/#4686/#4688) at existing chokepoints —
cohesive, not extractable:
- open-sse/handlers/responseSanitizer.ts 1103->1122 (SanitizeOpenAIResponseOptions + stripReasoning, #4678)
- open-sse/services/tokenRefresh.ts 2070->2090 (codex 401 unrecoverable-refresh guard, #4686)
- tests/unit/token-refresh-service.test.ts 1322->1353 (401 regression case, #4686)
- tests/unit/translator-openai-responses-req.test.ts 1047->1050 (reasoning_effort assertion, #4688)

* docs(env): document HEADROOM_URL in .env.example + ENVIRONMENT.md

The headroom proxy lifecycle (#4649) reads HEADROOM_URL (src/lib/headroom/detect.ts,
default http://localhost:8787) but it was missing from the env contract, tripping
check:env-doc-sync. Adds the var to both .env.example (commented, has a default) and
the Proxy Health table in ENVIRONMENT.md.

* fix(sse): stream writer mock abort() returns a Promise (#4788)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(cli): fall back to default data dir when DATA_DIR is not writable (#4767)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(oauth): verify Cursor installation on Linux before auto-import (#4770)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): track Ollama streaming usage from raw NDJSON chunks (#4754)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): strip enumDescriptions from antigravity tool schema (#4740)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): include low-level cause details in formatProviderError (#4741)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(translator): strip x-anthropic-billing-header in claude-to-openai (#4728)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): gate Kiro image attachments behind a Claude-capability check (#4763)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): read Antigravity usage from the response.usageMetadata envelope (#4785)

Integrated into release/v3.8.37 — Antigravity response.usageMetadata envelope. Cherry-picked onto release tip (resolved test-tail adjacency with #4754 Ollama block); usage-extractor 23/23 green.

* fix(api): fall back to existing access token for any OAuth provider on refresh failure (#4786)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(cli): verify launchd registration + skip self-SIGTERM in macOS autostart (#4765)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(executors): anthropic-compatible-* gateways get Bearer alongside x-api-key (#4729)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): json_schema fallback for OpenAI-compatible providers (#4766)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): use workos auth token shape for cline (#4787)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* feat(sse): parse Gemini CLI 429 retryDelay from structured RetryInfo (#4738)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): finalize tool_calls finish_reason on early stream end in OpenAI Responses translator (#4764)

Integrated into release/v3.8.37 — computeFinishReason finalizes tool_calls on early stream end (Responses translator). Cherry-picked onto release tip; responses-translation-fixes 29/29 green.

* test(sse): golden-lock provider.ts translate-path across all providers (#4734)

Integrated into release/v3.8.37 — golden-lock for provider.ts translate-path. Cherry-picked onto release tip; snapshot regenerated against the current provider set (UPDATE_GOLDEN=1, 167 entries); golden test 3/3 deterministic.

* chore(quality): rebaseline file-size for rc17 leva2 PR batch drift

Own growth from the merged leva2 PRs (cohesive, not extractable):
- src/lib/usage/providerLimits.ts 950->955 (#4786)
- open-sse/executors/default.ts NEW frozen @828 (#4729 + #4766 + #4787 header branches)
- open-sse/translator/request/openai-to-kiro.ts 807->814 (#4763)
- open-sse/translator/response/openai-responses.ts 923->937 (#4764)
- tests/unit/executor-default-base.test.ts 1339->1440 (#4766)
- tests/unit/translator-openai-to-kiro.test.ts 918->980 (#4763)

* fix(dashboard): align Engine Combos editor engines with API schema (#4955) (#5062)

The named-combos pipeline dropdown offered four engines (headroom,
session-dedup, ccr, llmlingua) that stackedPipelineStepSchema rejects, so
selecting one made PUT /api/context/combos/[id] return HTTP 400 while
saveCombo swallowed the non-OK response (if (!res.ok) return). Editing the
default 'Standard Savings' combo and changing an engine reproduced the 400.

- Add canonical STACKED_PIPELINE_ENGINE_INTENSITIES next to the schema as the
  single source of truth; the client dropdown imports it so it can never drift
  from the discriminated union the API validates against.
- Surface save errors and empty-name/empty-pipeline validation in the editor
  instead of failing silently.
- Add a parity unit test asserting the UI engine map equals the schema union
  and that every (engine, intensity) the UI emits is accepted.

* fix(sse): filter nameless hosted tools when converting Responses API to Chat format (#4789)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(dashboard): keep desktop sidebar visible via explicit CSS class (#4812)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): strip enumDescriptions from Antigravity tool schemas (#4813)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(dashboard): resolve passthrough model aliases by providerId in ModelSelectModal (#4815)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(oauth): allow per-connection refresh lead-time override via providerSpecificData.refreshLeadMs (#4818)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): strip X-Stainless-* headers and normalize SDK User-Agent for OpenAI-compatible endpoints (#4820)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): strip Gemini built-in tools when functionDeclarations present in Antigravity envelope (#4821)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(api): surface a Docker-localhost hint on provider-node validation connection errors (#4822)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): resolve bare model names to connection defaultModel before upstream calls (#4825)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(build): trace-include sql.js sql-wasm.wasm in standalone bundle (#4839)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): strip Composer <|final|> sentinel markers leaking after Composer reasoning (#4842)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(config): sync full SiliconFlow model list into registry (#4844)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): close reasoning before message content in Responses stream (#4848)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): reject unsupported Kiro [1m] context suffix (#4816)

Integrated into release/v3.8.37 — cherry-picked onto release tip; test-tail conflict with #4763 resolved (kept both image + [1m] test blocks); CHANGELOG re-merged; 29/29 green.

* fix(db): validate HuggingFace tokens via whoami-v2 auth probe (#4819)

Integrated into release/v3.8.37 — defining commit re-homed onto the god-file-split validation module (validateHuggingFaceProvider in validation/openaiFormat.ts + map wiring); 115/115 green.

* fix(sse): make anthropic-version default-guard case-insensitive (#4823)

Integrated into release/v3.8.37 — conflict with #4729 Bearer-fallback resolved (kept both Bearer fallback + case-insensitive anthropic-version guard); 48/48 green.

* fix(sse): sanitize Kiro tool schemas to avoid 400 "Improperly formed request" (#4847)

Integrated into release/v3.8.37 — conflict in kiro-to-openai.ts resolved (kept release fallbackToolCallId + adopted #1375 toolNameMap remap); 7/7 green.

* feat(sse): add GPT-4 to the GitHub Copilot provider (#4798)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(sse): add GPT-4o mini to GitHub Copilot provider (#4797)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(api): add MiniMax-M3 pricing row (#4814)

Integrated into release/v3.8.37 — pricing row re-homed onto god-file-split pricing/regional.ts (pricing.ts is now a barrel); 4/4 green.

* fix(cli): save runtime deps with --save-exact so a sibling install can't prune them (#4841)

Integrated into release/v3.8.37 — trayRuntime conflict resolved (kept release SYSTRAY_SPEC + added --save-exact); 2/2 green.

* fix(sse): preserve required fields in antigravity tool schemas (#4843)

Integrated into release/v3.8.37 — conflict resolved (kept #4740/#4813 enumDescriptions strip + typed normalizeSchemaTypes, added required-preservation helpers; test-tail merged keeping both enumDescriptions + required tests); 7/7 green.

* chore(quality): rebaseline file-size for rc17b leva3 PR batch drift

* fix(sse): strip reasoning blobs from agentic context to prevent O(n^2) token growth (#4849)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): unwrap Qoder HTTP 200 SSE error envelope so fallback can trigger (#4850)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): strip temperature for Claude models with extended thinking (#4853)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): emit valid concatenable kiro tool_calls.arguments deltas (#4855)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(sse): add toggleable tool-source diagnostics (#4856)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): redact api key from the AUTH debug log in the chat handler (#4858)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): forward AI SDK image parts in Responses translator (#4859)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): resolve custom combos by id and case-insensitive name (#4446) (#4869)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): exclude WS bridge controller-closed error from provider breaker (#4602) (#4870)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(providers): add xAI Grok inbound translators and thinking patcher (#4910)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(embeddings): add dimensions override field to embedding combos (#4913)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(oauth): Codex bulk-import endpoint — POST /api/oauth/codex/import (#4914)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(antigravity): retry transient upstream failures (#4941)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): surface malformed HTTP-200 upstream responses (#4942)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): normalize Codex custom tools (apply_patch) to { input: string } schema (#4862)

Integrated into release/v3.8.37 — conflict in request/openai-responses.ts resolved (kept #4789 nameless-tool skip + added #1007 custom-tool {input:string} normalization); 48/48 green incl. #4789/#4859 regression.

* fix(sse): dense, deterministic output ordering in Responses API response.completed (#4906)

Integrated into release/v3.8.37 — manual integration with #4862 in response/openai-responses.ts (custom-tool funcItem + dense recordCompletedItem). Fixed a latent #4848 interaction: the close-reasoning-before-message guard force-closed <think>-tag reasoning prematurely, which dense output (#4906) then snapshotted as a partial buffer ("plan" vs "planning") — scoped the guard to native reasoning_content (!inThinking) in BOTH transformer + translator paths. Full Responses suite 203/203 green incl. #4848/#4862 regression.

* feat(sse): auto-promote successful combo model to position #1 (#4852)

Integrated into release/v3.8.37 — dropped the stale file-size-baseline.json hunk (re-derived against the rc17b rebaseline); code+test applied clean; 13/13 green.

* feat(providers): add Pioneer AI (Fastino Labs) provider (#4909)

Integrated into release/v3.8.37 — providers.ts apikey block re-homed onto god-file-split src/shared/constants/providers/apikey/frontier-labs.ts (inline APIKEY_PROVIDERS no longer exists); registry/pioneer + providers/index.ts applied clean; 6/6 green.

* add DGrid AI gateway provider (#4931)

Integrated into release/v3.8.37 — rebased the contributor's commit onto the release tip; providers.ts god-file-split conflict resolved by relocating the dgrid APIKEY_PROVIDERS entry into apikey/gateways.ts; CHANGELOG added. 7/7 green. Thanks @dgridOP!

* chore(quality): rebaseline file-size for rc17b leva4 PR batch drift

* docs(routing): sync combo strategy docs for Fusion (17 strategies) (#5067)

Fusion (16th strategy, panel fan-out + judge synthesis) and headroom
shipped but the strategy-count docs were stale (14/15) and omitted both.
Update every combo-strategy reference to the canonical 17, add fusion +
headroom to all strategy tables, and add a dedicated Fusion section to
AUTO-COMBO.md documenting judgeModel / fusionTuning config + an example.

- CLAUDE.md, README.md, FEATURES.md, RESILIENCE_GUIDE.md,
  ARCHITECTURE.md, OPEN_SSE_ARCHITECTURE.md, OMNIROUTE_VS_ALTERNATIVES.md,
  docs/README.md, request-pipeline.mmd: 14/15 -> 17, list fusion + headroom
- docs/routing/AUTO-COMBO.md: strategy table + new Fusion strategy section
- docs/openapi.yaml: add reset-window, headroom, fusion to the strategy enum

* fix(oauth): classify /api/oauth/cursor/auto-import as local-only (route-guard) (#5070)

The Cursor auto-import route runs execFile("which", ["cursor"]) to verify a
local Cursor install before importing credentials — a child-process spawn. The
check:route-guard-membership gate (Hard Rules #15/#17) flagged it as an
unclassified spawn-capable route: reachable past the loopback gate, an
RCE-via-tunnel surface (a leaked JWT over a tunnel could trigger the spawn).

Classify the specific path in LOCAL_ONLY_API_PREFIXES so loopback enforcement
runs unconditionally before any auth check. Scoped to the exact path — the rest
of /api/oauth/ (browser redirect/callback flows) stays remote-reachable.

TDD: added a failing-then-passing assertion in route-guard-local-prefix.test.ts
(classification + an over-broadening guard proving sibling OAuth paths stay
remote). check:route-guard-membership now reports 0 new gaps.

* chore(release): v3.8.37 — 2026-06-26

---------

Co-authored-by: dgridOP <dgrid_op@outlook.com>
2026-06-26 02:51:06 -03:00

28 KiB
Raw Blame History

title, version, lastUpdated
title version lastUpdated
OmniRoute Auto-Combo Engine 3.8.31 2026-06-20

OmniRoute Auto-Combo Engine

For Users: Looking for a quick start? See the Auto-Combo User Guide for simple explanations and examples.

Self-managing model chains with adaptive scoring + zero-config auto-routing

Zero-Config Auto-Routing (auto/ prefix)

NEW: No combo creation required. Use auto/ prefix directly in any client.

Quick Examples

Model ID Variant Behavior
auto default All connected providers, LKGP strategy, balanced weights
auto/coding coding Quality-first weights, suitable for code generation
auto/fast fast Low-latency weighted selection
auto/cheap cheap Cost-optimized routing (lowest cost first)
auto/offline offline Favors providers with highest quota availability
auto/smart smart Quality-first + higher exploration rate (10%) for better model discovery
auto/lkgp lkgp Explicit LKGP (same as default auto)

Category × Tier Composition (auto/<category>:<tier>)

OpenRouter-style suffixes separate what kind of route (category) from how to optimize it (tier), so you can compose them freely (#4235 Phase B, open-sse/services/autoCombo/suffixComposition.ts):

  • Categories (filter the candidate pool by capability): coding · reasoning · vision · chat · multimodal. vision/multimodal keep vision-capable models; reasoning keeps reasoning/thinking models.
  • Tiers (pick the scoring weights / pool filter): fast (ship-fast) · cheap (alias floor, cost-saver) · reliable (circuit-breaker health + latency stability) · free / pro (filter the pool by model tier via classifyTier — free-tier vs. premium).
Example Resolves to
auto/coding:fast coding pool, low-latency weights
auto/coding:cheap coding pool, cost-optimized (alias auto/coding:floor)
auto/reasoning:pro reasoning/thinking models only, premium tier
auto/vision vision-capable models (no tier → balanced weights)
auto/multimodal:free multimodal-capable models, free tier only

Any valid auto/<category>[:<tier>] resolves on demand; a curated subset is advertised in /v1/models and the dashboard (AUTO_SUFFIX_VARIANTS in open-sse/services/autoCombo/builtinCatalog.ts). Filtering is fail-open — if a constraint matches no connected models, the full pool is used so routing never breaks. The core scorer (combo.ts) is unchanged; the category/tier filter is applied in buildAutoCandidates.

Live model intelligence: auto-routing fitness is informed by live Arena ELO rankings + models.dev tier data when the ARENA_ELO_SYNC_ENABLED flag is on (falls back to the static fitness map otherwise).

How to use:

# Any IDE or CLI tool that supports OpenAI format
Base URL: http://localhost:20128/v1
API Key:  <your-endpoint-key>

# In your code/config, set model to:
model: "auto"                 # balanced default
model: "auto/coding"          # best for coding tasks
model: "auto/fast"            # fastest available
model: "auto/cheap"           # cheapest per token

What happens:

  1. OmniRoute detects auto/ prefix in src/sse/handlers/chat.ts
  2. Queries all active provider connections from the database
  3. Filters to those with valid credentials (API key or OAuth token)
  4. Determines the model per connection (connection.defaultModel or provider's first model)
  5. Builds a virtual combo in-memory (not stored in DB)
  6. Routes using the selected variant's weight profile + LKGP strategy

Key properties:

  • Always-on: No toggle, no combo creation, no configuration needed
  • Dynamic: Reflects current connected providers automatically
  • Session stickiness: LKGP ensures last successful provider is prioritized
  • Multi-account aware: Each provider connection becomes a separate candidate
  • No DB writes: Virtual combo exists only for the request, zero persistence overhead

Behind the scenes:

Request: { model: "auto/coding" }
   ↓
src/sse/handlers/chat.ts detects prefix
   ↓
createVirtualAutoCombo('coding') → candidatePool from active connections
   ↓
handleComboChat (same engine as persisted combos)
   ↓
Auto-scoring selects best provider/model per request

Implementation files:

File Purpose
open-sse/services/autoCombo/autoPrefix.ts Prefix parser (parseAutoPrefix)
open-sse/services/autoCombo/virtualFactory.ts Creates virtual AutoComboConfig objects
open-sse/services/autoCombo/providerRegistryAccessor.ts Test hook for mocking provider registry
src/sse/handlers/chat.ts Integration: auto prefix short-circuit
src/shared/constants/providers.ts SYSTEM_PROVIDERS.auto system entry

How It Works (Persisted Auto-Combos)

The Auto-Combo Engine dynamically selects the best provider/model for each request using a 9-factor scoring function (defined in open-sse/services/autoCombo/scoring.tsDEFAULT_WEIGHTS). All weights sum to 1.0.

Auto-Combo 9-factor scoring

Source: diagrams/auto-combo-9factor.mmd

Factor Default Weight Description
health 0.22 Health score from circuit breaker (CLOSED=1.0, HALF_OPEN=0.5, OPEN=0.0)
quota 0.17 Remaining quota / rate-limit headroom [0..1]
costInv 0.17 Inverse blended cost (60% input + 40% output token price, normalized) — cheaper = higher score
latencyInv 0.13 Inverse p95 latency normalized to pool — faster = higher score
taskFit 0.08 Task-type fitness (coding, review, planning, analysis, debugging, docs)
specificityMatch 0.08 Match between request specificity (manifest hint) and model tier
stability 0.05 Variance-based stability (low latency stdDev / error rate)
tierPriority 0.05 Account-tier priority — Ultra=1.0, Pro=0.67, Standard=0.33, Free=0.0
tierAffinity 0.05 Affinity between the candidate's tier and the manifest-recommended tier

Sum: 0.22 + 0.17 + 0.17 + 0.13 + 0.08 + 0.08 + 0.05 + 0.05 + 0.05 = 1.0 (validated by validateWeights()).

Mode Packs

Four pre-defined weight profiles in open-sse/services/autoCombo/modePacks.ts. Each pack overrides the default weights to bias selection toward a specific goal. Below are the full weight tables per pack (each row sums to 1.0).

Factor ship-fast cost-saver quality-first offline-friendly
quota 0.14 0.14 0.10 0.37
health 0.28 0.19 0.18 0.28
costInv 0.05 0.37 0.05 0.10
latencyInv 0.32 0.05 0.05 0.05
taskFit 0.10 0.10 0.37 0.00
stability 0.00 0.05 0.15 0.10
tierPriority 0.05 0.05 0.05 0.05

Notes:

  • tierAffinity and specificityMatch are not set in mode packs — calculateScore() treats them as ?? 0 when absent.
  • Each pack's emphasis at a glance:
    • ship-fast → latencyInv 0.32 + health 0.28 (low-latency, healthy connections)
    • cost-saver → costInv 0.37 (cheapest tokens win)
    • quality-first → taskFit 0.37 + stability 0.15 (best model for the task, consistent)
    • offline-friendly → quota 0.37 + health 0.28 (max headroom regardless of speed/cost)

All Routing Strategies

OmniRoute's combo engine supports 17 routing strategies (declared in src/shared/constants/routingStrategies.tsROUTING_STRATEGY_VALUES). The Auto Combo engine itself is exposed under the auto strategy; the others are available for persisted combos.

Strategy Description
priority First-target ordered list with explicit priority
weighted Weighted random by per-target weight
round-robin Cycle through targets in order
context-relay Hand off context across targets (long conversations)
fill-first Fill each target's quota before moving to next
p2c Power-of-2-choices random load balancing
random Uniform random selection
least-used Pick target with lowest current load
cost-optimized Minimize $ per request given catalog pricing
reset-aware Prioritize by quota reset time — short reset windows ranked higher
reset-window Prefer targets whose quota window resets soonest
headroom Pick the target with the most remaining quota headroom
strict-random Random without deduplication of repeats
auto Use Auto Combo scoring (9-factor) — recommended
lkgp Last-Known-Good Path (sticky route to last successful target)
context-optimized Pick target with best fit for current context size
fusion 🧬 Fan out to a panel of models in parallel, then synthesize one answer via a judge (see below)

= New in v3.8.0 · 🧬 = New in v3.8.36

Fusion Strategy

fusion is the one strategy that does not pick a single target. It fans the prompt out to every panel model in parallel, then a configurable judge model synthesizes a single final answer from all panel responses. Ported from upstream decolua/9router (OpenRouter's Fusion design); implementation in open-sse/services/fusion.ts.

How it works:

  1. Fan-out — the prompt is sent to every panel model at once, forced non-streaming with tools stripped (the judge needs complete prose to synthesize).
  2. Quorum-grace collection — as soon as minPanel answers arrive, a short grace timer starts for the stragglers, then fusion proceeds with whatever was collected. This caps the slowest model's penalty on wall time, bounded by a hard timeout.
  3. Judge synthesis — panel answers are anonymized (Source 1, Source 2, … — so the judge weighs substance, not model brand) and handed to the judge, which analyzes consensus / contradictions / partial coverage / unique insights / blind spots, then writes one authoritative answer. The judge call keeps the client's original stream flag + tools, so streaming and downstream tool use still work.
  4. Graceful degradation — 0 panel answers → 503; exactly 1 survivor → that answer is returned directly (nothing to fuse); a single-model panel answers directly.

Configuration

Configured on the combo's config blob (no schema migration — it reuses the existing combos table):

Field Type Default Purpose
config.judgeModel string first panel model Model that synthesizes the final answer
config.fusionTuning.minPanel number 2 Successful answers required before the grace timer starts (clamped to [2, panelSize])
config.fusionTuning.stragglerGraceMs number 8000 How long to wait for laggards once quorum is reached
config.fusionTuning.panelHardTimeoutMs number 90000 Absolute cap so one hung model can't stall the request

Defaults live in FUSION_DEFAULTS (open-sse/services/fusion.ts).

Example

curl -X POST http://localhost:20128/api/combos \
  -H "Authorization: Bearer <key>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "fusion-panel",
    "strategy": "fusion",
    "targets": [
      { "model": "cc/claude-opus-4-7" },
      { "model": "cx/gpt-5.5" },
      { "model": "glm/glm-5.1" }
    ],
    "config": {
      "judgeModel": "cc/claude-opus-4-7",
      "fusionTuning": { "minPanel": 2, "stragglerGraceMs": 8000, "panelHardTimeoutMs": 90000 }
    }
  }'

Then call it like any combo: {"model":"fusion-panel","messages":[...]}.

Virtual Auto-Combo Factory

The Auto Combo engine doesn't require pre-defined combos. Instead, open-sse/services/autoCombo/virtualFactory.ts builds candidates on-the-fly:

  1. Pulls getProviderConnections({ isActive: true }) (all enabled connections)
  2. Filters to those with valid credentials (API key or non-expired OAuth token via hasUsableOAuthToken())
  3. Cross-references with getProviderRegistry() for model availability + pricing
  4. For each tuple (provider, model, connection), builds a VirtualAutoComboCandidate
  5. Picks connection.defaultModel (or the registry's first model) as the dispatch target
  6. Scores each candidate using the 9-factor scorePool() and the variant's weight pack
  7. Returns the resulting in-memory AutoComboConfig for handleComboChat() — never persisted to DB

This means adding a new provider with auto/* enabled automatically expands the candidate pool — no manual combo editing needed. The virtual combo is rebuilt per request, so newly-added or newly-healthy connections are picked up immediately.

Self-Healing

  • Temporary exclusion: Score < 0.2 → excluded for 5 min (progressive backoff, max 30 min)
  • Circuit breaker awareness: OPEN → auto-excluded; HALF_OPEN → probe requests
  • Incident mode: >50% OPEN → disable exploration, maximize stability
  • Cooldown recovery: After exclusion, first request is a "probe" with reduced timeout

Bandit Exploration

5% of requests (configurable) are routed to random providers for exploration. Disabled in incident mode.

API

There is no dedicated POST /api/combos/auto endpoint — Auto-Combo is consumed in two ways:

  1. Zero-config (recommended): Send any chat completion request with model: "auto" or model: "auto/<variant>". The virtual factory builds the combo per request — no persistence, no API calls needed.

  2. Persisted combo with strategy: "auto": Create a regular combo via POST /api/combos and set strategy: "auto" plus config.auto.weights / config.auto.candidatePool. The same scoring engine is used; the combo is stored in combos and reusable by ID.

For discovery, GET /api/combos/auto lists every variant with its resolved candidate pool plus context_length / max_output_tokens — the MAX across the candidate pool's windows. Clients (e.g. the opencode plugin) must advertise these values instead of 0: a zero context disables opencode's auto-compaction entirely, letting sessions grow until the gateway's history purge destroys context. MAX is safe to advertise because the auto-combo context pre-filter routes oversized requests to large-window candidates.

# Zero-config usage (no combo creation)
curl -X POST http://localhost:20128/v1/chat/completions \
  -H "Authorization: Bearer <key>" \
  -H "Content-Type: application/json" \
  -d '{"model":"auto/coding","messages":[{"role":"user","content":"Hello"}]}'

# Persisted auto combo via the regular combos endpoint
curl -X POST http://localhost:20128/api/combos \
  -H "Content-Type: application/json" \
  -d '{"id":"my-auto","name":"Auto Coder","strategy":"auto","config":{"auto":{"candidatePool":["anthropic","google","openai"],"weights":{"quota":0.15,"health":0.3,"costInv":0.05,"latencyInv":0.35,"taskFit":0.1,"stability":0,"tierPriority":0.05}}}}'

Auto router strategies

Persisted strategy: "auto" combos can set config.routerStrategy (or legacy config.auto.routerStrategy) to one of:

  • rules — default weighted scoring
  • cost / eco — cheapest healthy provider
  • latency / fast — lowest p95 latency with reliability penalty
  • sla-aware / sla — prefer candidates that satisfy p95 latency, error-rate, and optional cost SLOs
  • lkgp — last known good provider first

Router strategies in detail

The auto-combo engine exposes 5 pluggable RouterStrategy implementations that you can swap via config.routerStrategy (or the legacy config.auto.routerStrategy). Each strategy picks one provider from the candidate pool, given a RoutingContext (task type, tool/vision hints, token estimate, optional SLA policy, optional last-known-good provider).

1. rules (default) — 6-factor weighted scoring

Wraps the existing scoring engine. Filters out OPEN circuit-breaker candidates, then runs scorePool() with the current task type and getTaskFitness(), picking the top-scoring provider.

class RulesStrategyImpl implements RouterStrategy {
  readonly name = "rules";
  readonly description =
    "6-factor weighted scoring: quota, health, cost, latency, taskFit, stability";

  select(pool, context) {
    const eligible = pool.filter((c) => c.circuitBreakerState !== "OPEN");
    const ranked = scorePool(eligible.length > 0 ? eligible : pool, context.taskType, undefined, getTaskFitness);
    return { provider: ranked[0].provider, /* ... */ };
  }
}

When to use: Default. Use when you want a balanced trade-off across all signals.

Alias: rules (no alias)


2. cost / eco — cheapest healthy provider

Sorts the candidate pool by costPer1MTokens (ascending) and picks the cheapest. Filters out OPEN candidates first.

class CostStrategyImpl implements RouterStrategy {
  readonly name = "cost";
  readonly description = "Always selects cheapest available provider";

  select(pool, context) {
    const healthy = pool.filter((c) => c.circuitBreakerState !== "OPEN");
    const sorted = [...healthy].sort((a, b) => a.costPer1MTokens - b.costPer1MTokens);
    return { provider: sorted[0].provider, /* ... */ };
  }
}

When to use: Cost-sensitive workloads, batch processing, or background jobs.

Aliases: cost, eco


3. latency / fast — lowest p95 latency with reliability penalty

Sorts by p95LatencyMs + (errorRate * 1000). The error-rate penalty ensures unreliable providers are ranked lower even if their nominal latency is low.

class LatencyStrategyImpl implements RouterStrategy {
  readonly name = "latency";
  readonly description = "Prioritizes lowest p95 latency with reliability weighting";

  select(pool, context) {
    const healthy = pool.filter((c) => c.circuitBreakerState !== "OPEN");
    const sorted = [...healthy].sort((a, b) =>
      (a.p95LatencyMs + a.errorRate * 1000) - (b.p95LatencyMs + b.errorRate * 1000)
    );
    return { provider: sorted[0].provider, /* ... */ };
  }
}

When to use: Latency-sensitive workloads like real-time chat, autocomplete, or interactive coding assistants.

Aliases: latency, fast


4. sla-aware / sla — latency/error/cost SLO compliance

Scores each candidate by how well it satisfies the configured SLO policy:

Factor Weight Formula
Latency score 35% threshold / max(value, ε)
Error score 35% threshold / max(value, ε)
Health score 15% 1.0 (CLOSED) / 0.5 (HALF_OPEN) / 0.0 (OPEN)
Cost score 10% threshold / max(value, ε) or inverse normalized
Stability score 5% inverse normalized latency stddev

When hardConstraints: true, candidates are sorted primarily by violation score (how far they exceed any SLO), then by composite score. Otherwise it's just the composite score.

class SLAStrategyImpl implements RouterStrategy {
  readonly name = "sla-aware";
  readonly description = "Selects the provider most likely to satisfy latency, error-rate, and cost SLOs";

  select(pool, context) {
    // ... scores each candidate against policy: { targetP95Ms, maxErrorRate, maxCostPer1MTokens, hardConstraints }
  }
}

SLA fields (set on the combo config):

{
  "strategy": "auto",
  "config": {
    "routerStrategy": "sla-aware",
    "slaTargetP95Ms": 1500,
    "slaMaxErrorRate": 0.05,
    "slaMaxCostPer1MTokens": 5,
    "slaHardConstraints": true
  }
}

When to use: Production workloads with strict latency, error-rate, or cost budgets.

Aliases: sla-aware, sla


5. lkgp — last known good provider first

Tries the last known good provider (if set) first, then falls back to the rules strategy. Useful for session stickiness — the same provider handles follow-up requests in a conversation.

class LKGPStrategyImpl implements RouterStrategy {
  readonly name = "lkgp";
  readonly description = "Tries last known good provider first, then falls back to rules";

  select(pool, context) {
    if (context.lkgpEnabled === false) {
      return getStrategy("rules").select(pool, context);
    }

    if (context.lastKnownGoodProvider) {
      const candidates = pool.filter(
        (c) => c.provider === context.lastKnownGoodProvider && c.circuitBreakerState !== "OPEN"
      );
      if (candidates.length > 0) {
        return { provider: candidates[0].provider, /* ... */ };
      }
    }

    // Fallback to rules strategy
    return getStrategy("rules").select(pool, context);
  }
}

When to use: Multi-turn conversations where you want the same provider to handle follow-up requests (e.g., for caching, context continuity, or pricing consistency).

Alias: lkgp (no alias)


Custom router strategies

You can register your own RouterStrategy implementation via the public API:

import { registerStrategy, type RouterStrategy } from "@omniroute/open-sse/services/autoCombo/routerStrategy";

class MyCustomStrategy implements RouterStrategy {
  readonly name = "my-custom";
  readonly description = "My custom routing strategy";

  select(pool, context) {
    // Your routing logic here
    return {
      provider: pool[0].provider,
      model: pool[0].model,
      strategy: this.name,
      reason: "MyCustomStrategy: ...",
      candidatesConsidered: pool.length,
      finalScore: 1.0,
    };
  }
}

registerStrategy("my-custom", new MyCustomStrategy());

Then use it:

{
  "strategy": "auto",
  "config": {
    "routerStrategy": "my-custom"
  }
}

Router strategy selection guide

Use case Strategy Reason
Balanced workload rules Default — considers all factors
Minimize cost cost Always picks cheapest
Minimize latency latency Picks fastest reliable provider
Strict SLOs sla-aware Filters by p95/error/cost thresholds
Multi-turn chat lkgp Session stickiness

SLA-aware fields:

{
  "strategy": "auto",
  "config": {
    "routerStrategy": "sla-aware",
    "slaTargetP95Ms": 1500,
    "slaMaxErrorRate": 0.05,
    "slaMaxCostPer1MTokens": 5,
    "slaHardConstraints": true
  }
}

Task Fitness

30+ models scored across 6 task types (coding, review, planning, analysis, debugging, documentation). Supports wildcard patterns (e.g., *-coder → high coding score).

Auto Variants Recap

Including the bare auto (default) plus the 6 AutoVariant values declared in autoPrefix.ts, there are 7 invokable model IDs:

auto, auto/coding, auto/fast, auto/cheap, auto/offline, auto/smart, auto/lkgp

(AutoVariant itself enumerates 6 values; the 7th option is "no variant" — bare auto — handled by parseAutoPrefix() as variant: undefined.)

How tiers fit Auto-Combo

The 9-factor scoring function (open-sse/services/autoCombo/scoring.ts) treats tier membership as one signal via the tierPriority weight. Default weights (from DEFAULT_WEIGHTS):

Factor Default weight Notes
Tier priority 0.05 Tier 1 premium → higher score
Latency (p50 inverse) 0.35 Fastest wins
Cost ($/1M inverse) 0.20 Cheapest blended price wins (60% input + 40% output ratio)
Recent health/error rate 0.15 Unhealthy deprioritized
Quota remaining 0.10 Near-exhausted deprioritized
Context window match 0.08 Penalizes short windows
Task fitness 0.10 Coding → coding-specialist models
Stability 0.00 Disabled by default

Tier alone does not force Tier 1 first — if Tier 1 latency is bad or cost-vs-quality is suboptimal, Tier 2 wins. To force tier ordering, use combo strategy priority and arrange providers by tier.

To strongly favor Tier 1 (subscription), increase tierPriority weight:

{
  "strategy": "auto",
  "config": { "auto": { "weights": { "tierPriority": 0.3, "costInv": 0.05 } } }
}

See docs/marketing/TIERS.md for tier definitions and provider classification.

Files

File Purpose
open-sse/services/autoCombo/scoring.ts 9-factor scoring function, DEFAULT_WEIGHTS, pool norm
open-sse/services/autoCombo/taskFitness.ts Model × task fitness lookup
open-sse/services/autoCombo/engine.ts Selection logic, bandit, budget cap
open-sse/services/autoCombo/selfHealing.ts Exclusion, probes, incident mode
open-sse/services/autoCombo/modePacks.ts 4 weight profiles (ship-fast, cost-saver, quality-first, offline-friendly)
open-sse/services/autoCombo/autoPrefix.ts auto/ prefix parser + 6 variants
open-sse/services/autoCombo/virtualFactory.ts Builds in-memory AutoComboConfig from live connections
open-sse/services/autoCombo/providerRegistryAccessor.ts Test hook for mocking provider registry
src/shared/constants/routingStrategies.ts ROUTING_STRATEGY_VALUES (17 strategies)
src/sse/handlers/chat.ts Integration: auto-prefix short-circuit