Skip to content

feat(routing): LGKP remembers last good account per provider - #2338

Merged
diegosouzapw merged 4 commits into
cheaper-inference:release/v3.8.0from
oyi77:feat/lgkp-last-good-account
May 17, 2026
Merged

diegosouzapw merged 4 commits into
cheaper-inference:release/v3.8.0from
oyi77:feat/lgkp-last-good-account

Conversation

@oyi77

@oyi77 oyi77 commented May 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • LGKP mode now tracks the specific connection/account ID alongside the provider, not just the provider name
  • On subsequent requests, the exact same account that last succeeded is prioritized first — no more falling back to provider-level matching and retrying bad accounts
  • Backward-compatible: existing plain-string LKGP records in the DB are auto-upgraded to the new { provider, connectionId } shape on read

Changes

File What changed
src/lib/db/settings.ts getLKGP() returns LKGPRecord { provider, connectionId? }, setLKGP() accepts optional connectionId, backward-compatible parsing of old plain-string values
src/lib/db/readCache.ts Cache layer updated for LKGPRecord shape, setCachedLKGP() passes through connectionId
open-sse/services/combo.ts Both write paths (handleComboChat + handleRoundRobinCombo) pass target.connectionId to setLKGP(); both read paths (lkgp strategy + auto strategy) extract connectionId from the record and match on it first before falling back to provider-level
open-sse/services/autoCombo/routerStrategy.ts RoutingContext gains lastKnownGoodConnectionId?, LKGPStrategyImpl logs account info in decision reason

How it works

Before: LGKP stored "openai" → next request tries all openai accounts in order until one works.

After: LGKP stores { provider: "openai", connectionId: "conn-abc123" } → next request prioritizes conn-abc123 directly, skipping accounts that previously failed.

Test plan

  • TypeScript typecheck passes (tsc --noEmit)
  • ESLint passes on all changed files
  • Pre-commit hooks pass (prettier, eslint, docs-sync, env contract, i18n)
  • LKGP-related unit tests pass (db-settings-crud, db-read-cache, combo-routing-engine)
  • Manual verification: enable LGKP strategy on a combo, send request through provider A/account 1, verify next request routes to same account first

Related Issues

Closes #2262

🤖 Generated with OpenClaude

Previously LGKP only tracked which provider last succeeded. Now it also
tracks the specific connection/account ID, so the same account is reused
on the next request instead of falling back to provider-level matching.

Changes:
- settings.ts: getLKGP returns LKGPRecord { provider, connectionId },
  setLKGP accepts optional connectionId, backward-compatible parsing
- readCache.ts: cache layer updated for LKGPRecord shape
- combo.ts: both write paths (handleComboChat + handleRoundRobinCombo)
  pass target.connectionId to setLKGP; both read paths (lkgp strategy
  + auto strategy) extract connectionId and match on it first
- routerStrategy.ts: RoutingContext gains lastKnownGoodConnectionId,
  LKGPStrategyImpl includes account info in decision reason

Closes cheaper-inference#2262

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
@oyi77
oyi77 requested a review from diegosouzapw as a code owner May 17, 2026 17:18

@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 account-level tracking for the Last Known Good Provider (LKGP) by incorporating a connectionId into the LKGP records, database schema, and routing context. While the logic in combo.ts correctly prioritizes matches using both provider and connection ID, the implementation in LKGPStrategyImpl within routerStrategy.ts currently only filters by provider and defaults to the first available candidate. This oversight prevents true account-level prioritization in that strategy and renders the updated decision reason string misleading, as it may display an account ID that was not strictly enforced during selection.

Comment on lines +134 to +138
const candidates = pool.filter(
(c) => c.provider === context.lastKnownGoodProvider && c.circuitBreakerState !== "OPEN"
);
if (best) {
if (candidates.length > 0) {
const best = candidates[0];

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

The LKGPStrategyImpl does not actually prioritize the specific account identified by lastKnownGoodConnectionId. It filters the pool by provider and simply selects the first available candidate (candidates[0]). If multiple accounts exist for the same provider, this logic will not necessarily pick the one that previously succeeded, which contradicts the PR's goal of account-level LGKP for the auto strategy. To fix this, ProviderCandidate should include the connectionId (populated in buildAutoCandidates), and this logic should attempt to find a matching candidate before defaulting to the first one in the pool.

model: best.model,
strategy: this.name,
reason: `LKGP: using last known good provider ${best.provider}`,
reason: `LKGP: using last known good provider ${best.provider}${context.lastKnownGoodConnectionId ? ` (account ${context.lastKnownGoodConnectionId})` : ""}`,

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

The decision reason string is misleading because it includes the account ID even though the selection logic above (line 138) doesn't actually guarantee that the specific account was chosen. The selection logic should be updated to match the account ID before this reason string can be considered accurate.

Address Gemini code review: auto strategy candidates are provider-level
only (ProviderCandidate has no connectionId), so passing connectionId
through RoutingContext was misleading. Account-level matching only works
in the standalone lkgp strategy path which uses ResolvedComboTarget.

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
@oyi77

oyi77 commented May 17, 2026

Copy link
Copy Markdown
Contributor Author

@ gemini-code-assist Good catch. Fixed in 81b6b40:

  • Removed lastKnownGoodConnectionId from RoutingContext — auto strategy candidates (ProviderCandidate) are provider-level only, no connectionId field
  • Removed misleading account mention from LKGPStrategyImpl reason string
  • Removed unused variable in combo.ts auto strategy path

Account-level matching is correctly handled only in the standalone lkgp strategy path (combo.ts lines 1820-1856) which operates on ResolvedComboTarget[] that carries connectionId.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Thank you for the update, @oyi77. These changes look correct and improve the maintainability of the routing logic by removing the unused variable and cleaning up the RoutingContext and log messages. The implementation now correctly isolates account-level matching to the appropriate strategy path.

@diegosouzapw
diegosouzapw merged commit 193e7a6 into cheaper-inference:release/v3.8.0 May 17, 2026
1 of 3 checks passed
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thank you @oyi77 for this improvement! Your LKGP enhancement — storing the last known good account (connectionId) alongside the provider — has been merged into release/v3.8.0. We added unit tests for the new connectionId-aware getLKGP/setLKGP signatures and backward-compat parsing of old plain-string values. This will ship in the upcoming v3.8.0 release. Great contribution!

diegosouzapw added a commit that referenced this pull request May 18, 2026
…th (#2359)

Combo dispatch and the combo test button used to crash with
'TypeError: e.startsWith is not a function' when a step's modelStr
failed to resolve (regression after #2338 added per-account LKGP
routing for local/Docker providers). The TypeScript annotation says
the field is always a string, but malformed combo rows leaked through
to the dispatch path.

Two defensive boundary guards:

1. open-sse/services/combo.ts LKGP fallback findIndex — type-check
   target.modelStr before calling startsWith.

2. src/app/api/combos/test/route.ts testComboTarget — coerce
   target.modelStr at the entry, surface a clean 'Combo step is
   missing a model id' error instead of crashing.

Regression test parses the source and asserts no unguarded
target.modelStr.<string-method> usages remain in combo.ts, so a
future refactor that reintroduces the pattern fails loudly.
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…-inference#2338)

Integrated into release/v3.8.0 — added unit tests for connectionId-aware getLKGP/setLKGP
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…th (cheaper-inference#2359)

Combo dispatch and the combo test button used to crash with
'TypeError: e.startsWith is not a function' when a step's modelStr
failed to resolve (regression after cheaper-inference#2338 added per-account LKGP
routing for local/Docker providers). The TypeScript annotation says
the field is always a string, but malformed combo rows leaked through
to the dispatch path.

Two defensive boundary guards:

1. open-sse/services/combo.ts LKGP fallback findIndex — type-check
   target.modelStr before calling startsWith.

2. src/app/api/combos/test/route.ts testComboTarget — coerce
   target.modelStr at the entry, surface a clean 'Combo step is
   missing a model id' error instead of crashing.

Regression test parses the source and asserts no unguarded
target.modelStr.<string-method> usages remain in combo.ts, so a
future refactor that reintroduces the pattern fails loudly.
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
…-inference#2338)

Integrated into release/v3.8.0 — added unit tests for connectionId-aware getLKGP/setLKGP
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
…th (cheaper-inference#2359)

Combo dispatch and the combo test button used to crash with
'TypeError: e.startsWith is not a function' when a step's modelStr
failed to resolve (regression after cheaper-inference#2338 added per-account LKGP
routing for local/Docker providers). The TypeScript annotation says
the field is always a string, but malformed combo rows leaked through
to the dispatch path.

Two defensive boundary guards:

1. open-sse/services/combo.ts LKGP fallback findIndex — type-check
   target.modelStr before calling startsWith.

2. src/app/api/combos/test/route.ts testComboTarget — coerce
   target.modelStr at the entry, surface a clean 'Combo step is
   missing a model id' error instead of crashing.

Regression test parses the source and asserts no unguarded
target.modelStr.<string-method> usages remain in combo.ts, so a
future refactor that reintroduces the pattern fails loudly.
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…-inference#2338)

Integrated into release/v3.8.0 — added unit tests for connectionId-aware getLKGP/setLKGP
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…th (cheaper-inference#2359)

Combo dispatch and the combo test button used to crash with
'TypeError: e.startsWith is not a function' when a step's modelStr
failed to resolve (regression after cheaper-inference#2338 added per-account LKGP
routing for local/Docker providers). The TypeScript annotation says
the field is always a string, but malformed combo rows leaked through
to the dispatch path.

Two defensive boundary guards:

1. open-sse/services/combo.ts LKGP fallback findIndex — type-check
   target.modelStr before calling startsWith.

2. src/app/api/combos/test/route.ts testComboTarget — coerce
   target.modelStr at the entry, surface a clean 'Combo step is
   missing a model id' error instead of crashing.

Regression test parses the source and asserts no unguarded
target.modelStr.<string-method> usages remain in combo.ts, so a
future refactor that reintroduces the pattern fails loudly.
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