Skip to content

fix(autoCombo): rotate across all connections, never waste provider capacity - #3078

Merged
diegosouzapw merged 3 commits into
diegosouzapw:release/v3.8.9from
oyi77:fix/smart-combo-rotation-v3.8.8
Jun 3, 2026
Merged

diegosouzapw merged 3 commits into
diegosouzapw:release/v3.8.9from
oyi77:fix/smart-combo-rotation-v3.8.8

Conversation

@oyi77

@oyi77 oyi77 commented Jun 2, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Auto combos on release/v3.8.8 silently underutilize connected providers. 43 Cerebras connections collapse to 1 candidate because buildAutoCandidates maps targets 1:1 to candidates — rotation never reaches individual keys. Selection is greedy (candidates_[0]) so identical-score candidates always pick the first in array order.

Impact: hundreds of active connections across free-tier providers in the autoCombo candidate pool, but routing only ever touches one at a time per provider.

Fix

Change File
Per-connection candidate expansion (43 keys → 43 candidates) combo.ts
connectionId field on ProviderCandidate, ScoredProvider, SelectionResult scoring.ts, engine.ts
ScoreTierRotator with per-combo round-robin state (replaces global counter) engine.ts
Combo-name-aware tier preferences (smart/fast/cheap/coding/default) engine.ts
connectionDensity factor (weight 0.05) rewards multi-connection providers scoring.ts, modePacks.ts
Budget cap degradation now uses rotator, not single cheapest engine.ts

Verification

  • Vitest: 146/146 pass
  • Node.js combo tests: 83/83 pass
  • TypeScript typecheck: clean
  • Simulation (N Cerebras keys, 500 requests):
    • Unique connections hit: N / N
    • Min picks: 10, Max picks: 12 (balanced across all keys)

Backward compatibility

  • All weight sums still equal 1.0
  • connectionPoolSize and connectionId are optional — old callers get defaults
  • Budget cap fallback to cheapest preserved when no candidate meets the cap
  • No schema changes, no migration

…apacity

Per-connection expansion in buildAutoCandidates turns 43 Cerebras keys into
43 candidates instead of 1. ScoreTierRotator uses per-combo round-robin
state (was a global counter) so all connections in a tier are visited
across requests. New connectionDensity factor (weight 0.05) rewards
multi-connection providers so they surface in the top tier.

Budget cap degradation now respects rotation — filters to budget-compliant
candidates and uses the rotator instead of always picking the single
cheapest. Falls back to cheapest only if no candidate meets the cap.

Verification: 43/43 Cerebras connections hit in 500 requests, balanced
10-12 picks each. All 146 vitest + 83 Node.js combo tests pass.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces tiered rotation and connection density factors to the auto-combo routing engine, allowing traffic to be distributed across tiers and active provider connections. The review identified several critical improvements: resolving a mathematical bias in the weighted tier selection, moving the newly added test file to the tests/ directory to comply with the repository style guide, optimizing the budget cap enforcement's performance from O(N^2 log N) to O(N log N) using a cost map, and adding defensive checks to prevent potential runtime crashes when resolving connection IDs.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +140 to +154
function chooseTierWeighted(
tiers: Record<TierName, ScoredProvider[]>,
prefs: Record<TierName, number>,
pickFromPool: (pool: ScoredProvider[]) => ScoredProvider,
fallback: () => ScoredProvider
): ScoredProvider {
const total = prefs.top + prefs.mid + prefs.rest;
if (total <= 0) return fallback();
const r = Math.random() * total;
let acc = 0;
if ((acc += prefs.top) >= r && tiers.top.length > 0) return pickFromPool(tiers.top);
if ((acc += prefs.mid) >= r && tiers.mid.length > 0) return pickFromPool(tiers.mid);
if (tiers.rest.length > 0) return pickFromPool(tiers.rest);
return fallback();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When some tiers are empty, the current implementation of chooseTierWeighted introduces a mathematical bias. The weight of the empty tier is entirely absorbed by the next non-empty tier in the sequence instead of being distributed proportionally among all active tiers. Furthermore, this causes uneven round-robin rotation because it splits the rotation state between rrCounter and tierCounters unnecessarily when falling back. Normalizing the weights of only the non-empty tiers resolves both issues cleanly.

function chooseTierWeighted(
  tiers: Record<TierName, ScoredProvider[]>,
  prefs: Record<TierName, number>,
  pickFromPool: (pool: ScoredProvider[]) => ScoredProvider,
  fallback: () => ScoredProvider
): ScoredProvider {
  const activePrefs = {
    top: tiers.top.length > 0 ? prefs.top : 0,
    mid: tiers.mid.length > 0 ? prefs.mid : 0,
    rest: tiers.rest.length > 0 ? prefs.rest : 0,
  };
  const total = activePrefs.top + activePrefs.mid + activePrefs.rest;
  if (total <= 0) return fallback();
  const r = Math.random() * total;
  let acc = 0;
  if (activePrefs.top > 0 && (acc += activePrefs.top) >= r) return pickFromPool(tiers.top);
  if (activePrefs.mid > 0 && (acc += activePrefs.mid) >= r) return pickFromPool(tiers.mid);
  if (activePrefs.rest > 0) return pickFromPool(tiers.rest);
  return fallback();
}

Comment on lines +1 to +5
/**
* Tests for ScoreTierRotator and connectionDensity factor.
* Verifies that multi-connection providers surface in ranked candidates
* and that tiered rotation distributes traffic fairly.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the Repository Style Guide (Rule 1), all unit tests, integration tests, ecosystem tests, or Vitest files must strictly be placed within the tests/ directory (e.g., tests/unit/, tests/integration/). Creating test files inside the open-sse/ directory violates this rule. Please move this file to tests/unit/autoCombo/tieredRotation.test.ts or similar.

References
  1. ALL unit tests, integration tests, ecosystem tests, or Vitest files MUST strictly be placed within the tests/ directory. (link)

Comment on lines +268 to +274
const estimatedCostFor = (s: ScoredProvider) => {
const c = candidates.find(
(cand) => cand.provider === s.provider && cand.model === s.model
);
const cost = c?.costPer1MTokens ?? 0;
return (cost / 1_000_000) * 1000;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Calling candidates.find inside filter and sort results in $O(N^2 \log N)$ complexity, which can be a performance bottleneck in high-throughput scenarios with many active connections (e.g., 242+ connections). Pre-calculating the costs into a Map reduces the lookup complexity to $O(1)$, optimizing the overall operation to $O(N \log N)$.

    const costMap = new Map<string, number>();
    for (const c of candidates) {
      costMap.set(`${c.provider}\\0${c.model}`, c.costPer1MTokens);
    }
    const estimatedCostFor = (s: ScoredProvider) => {
      const cost = costMap.get(`${s.provider}\\0${s.model}`) ?? 0;
      return (cost / 1_000_000) * 1000;
    };

Comment on lines +2275 to +2277
const connectionIds = providerConnections
.map((c) => (typeof c.id === "string" ? c.id : null))
.filter((id): id is string => id !== null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Accessing c.id directly without verifying that c is a non-null object poses a runtime crash risk if the database query returns unexpected null or undefined elements. Adding a defensive check ensures robustness.

Suggested change
const connectionIds = providerConnections
.map((c) => (typeof c.id === "string" ? c.id : null))
.filter((id): id is string => id !== null);
const connectionIds = providerConnections
.map((c) => (c && typeof c === "object" && typeof c.id === "string" ? c.id : null))
.filter((id): id is string => id !== null);

- chooseTierWeighted: normalize active tier weights only, fix bias where
  empty tier weight was absorbed by next non-empty tier
- Add CLEAR_WINNER_THRESHOLD fast path: when score gap between best and
  worst >= 0.1, always pick from top tier (preserves deterministic
  best-candidate behavior for clear winners)
- Budget cap: pre-calculate cost Map, reduce O(N^2 log N) to O(N log N)
- combo.ts: add null check on connection object before accessing .id
- Move tieredRotation.test.ts from open-sse/services/autoCombo/__tests__/
  to tests/unit/autoCombo/ per repository style guide
- Add tests/unit/autoCombo/ to vitest.mcp.config.ts include list
@kilo-code-bot

kilo-code-bot Bot commented Jun 2, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

CRITICAL

File Line Issue
open-sse/services/autoCombo/modePacks.ts 31 Mode pack weight sums don't validate to 1.0. cost-saver, quality-first, and offline-friendly all sum to 0.96 (4% under target), which would fail validateWeights() check requiring Math.abs(sum - 1.0) < 0.01. The ship-fast pack sums to 0.99 which passes.

WARNING

File Line Issue
open-sse/services/combo.ts 2277 Connection ID extraction doesn't guard against null/undefined connection objects being passed through from connectionsByProvider. The filter type guard (id): id is string => id !== null only validates the final ID, but c.id could throw if c is malformed.

SUGGESTION

File Line Issue
open-sse/services/combo.ts 2286 Execution key uses @ separator which could collide with connection IDs containing @. Consider using a delimiter that's unlikely to appear in UUIDs like `
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
open-sse/services/autoCombo/engine.ts 1-65 The comboRotators Map at module level is not cleaned up when combos are deleted or reset. In long-running processes with dynamic combo creation/destruction, this could cause memory growth. Consider adding cleanup logic or using WeakMap pattern.
open-sse/services/autoCombo/engine.ts 172-175 Rotator instances created with connectionId in the key (budget case) but these rotators are never cleaned up, similar to the main comboRotators Map issue above.
Files Reviewed (4 files)
  • open-sse/services/autoCombo/engine.ts - connectionDensity factor, ScoreTierRotator class, tiered rotation logic
  • open-sse/services/autoCombo/scoring.ts - ScoringFactors/ScoringWeights interfaces, connectionDensity calculation
  • open-sse/services/autoCombo/modePacks.ts - Mode pack weight configurations
  • open-sse/services/combo.ts - buildAutoCandidates connection pool expansion
  • tests/unit/autoCombo/tieredRotation.test.ts - Test additions for rotation functionality
  • vitest.mcp.config.ts - Test include path update

Reviewed by laguna-m.1-20260312:free · 1,776,783 tokens

expandedTargets.push({
...target,
connectionId,
executionKey: `${target.executionKey}@${connectionId}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Execution key delimiter collision risk

Using @ as a separator in the execution key could cause collisions if connection IDs contain @. Consider using a safer delimiter like | or # which is less likely to appear in UUIDs/connection identifiers.

tierAffinity: 0,
specificityMatch: 0,
contextAffinity: 0.05,
contextAffinity: 0.0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Mode pack weight sum validation failure

The cost-saver mode pack sums to 0.96 (0.14+0.19+0.37+0.05+0.1+0.05+0.05+0+0+0+0+0.05), which fails validateWeights() check (Math.abs(sum - 1.0) < 0.01). This would cause the pack to be rejected and fall back to DEFAULT_WEIGHTS. Similar issues exist in quality-first and offline-friendly packs.

@oyi77
oyi77 changed the base branch from release/v3.8.8 to main June 3, 2026 07:44
@diegosouzapw
diegosouzapw changed the base branch from main to release/v3.8.9 June 3, 2026 09:34
@diegosouzapw
diegosouzapw merged commit 43b1392 into diegosouzapw:release/v3.8.9 Jun 3, 2026
1 check passed
@diegosouzapw

Copy link
Copy Markdown
Owner

Thank you @oyi77 for this excellent improvement! The per-connection candidate expansion (43 Cerebras keys -> 43 candidates) combined with ScoreTierRotator and the connectionDensity factor correctly solves the provider capacity underutilization problem. All 146 vitest + 83 combo tests pass. Merged into release/v3.8.9 — will ship in v3.8.9.

@diegosouzapw diegosouzapw mentioned this pull request Jun 3, 2026
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…apacity (diegosouzapw#3078)

Integrated into release/v3.8.9. Clean merge, all 146 vitest tests pass.
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
…apacity (diegosouzapw#3078)

Integrated into release/v3.8.9. Clean merge, all 146 vitest tests pass.
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…apacity (diegosouzapw#3078)

Integrated into release/v3.8.9. Clean merge, all 146 vitest tests pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants