diff --git a/README.md b/README.md
index 24a28b5655..72d30374fb 100644
--- a/README.md
+++ b/README.md
@@ -424,7 +424,7 @@ All **19** strategies — mix & match per combo step:
| 16 |
lkgp |
- Last-Known-Good Path — sticky to the last successful target |
+ Last-Known-Good Path — pins to the last successful provider, then falls back to rules |
| 17 |
diff --git a/changelog.d/maintenance/11555-lkgp-doc-provider-stickiness.md b/changelog.d/maintenance/11555-lkgp-doc-provider-stickiness.md
new file mode 100644
index 0000000000..9c0c300554
--- /dev/null
+++ b/changelog.d/maintenance/11555-lkgp-doc-provider-stickiness.md
@@ -0,0 +1 @@
+- **docs(routing):** LKGP strategy copy now states provider-level stickiness and the explicit rules fallback instead of promising per-target pinning ([#11555](https://github.com/diegosouzapw/OmniRoute/pull/11555))
diff --git a/docs/guides/USER_GUIDE.md b/docs/guides/USER_GUIDE.md
index 9f5c79ee4a..516fd75dc2 100644
--- a/docs/guides/USER_GUIDE.md
+++ b/docs/guides/USER_GUIDE.md
@@ -827,7 +827,7 @@ Configure via **Dashboard → Settings → Routing**. The dashboard exposes the
- `p2c` (Power of Two Choices)
- `least-used` and `cost-optimized`
- `auto` — score-driven across all candidates
-- `lkgp` (Last Known Good Provider) — sticks to the last successful model per session
+- `lkgp` (Last Known Good Provider) — pins to the last successful provider, then falls back to rules
- `context-optimized` — picks the model with the largest free context window
- `context-relay` — chains long-context models for follow-up turns
@@ -1088,15 +1088,15 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
OmniRoute ships with a **score-driven auto-router** that picks the best model for each request across every connected provider — no combo to maintain. Just send the request with one of the `auto/*` prefixes and OmniRoute will assemble a virtual combo on the fly, scoring candidates on latency, cost, success rate, context fit, model fitness for the task, recent failures, quota, and circuit-breaker state.
-| Prefix | Optimizes for |
-| -------------- | ----------------------------------------------------------------------------- |
-| `auto` | Balanced default (latency × cost × success rate) |
-| `auto/coding` | Coding tasks: prefers Claude, GPT-5, GLM, Kimi, Qwen Coder, DeepSeek coders |
-| `auto/cheap` | Lowest $/token, accepts higher latency |
-| `auto/fast` | Lowest latency, ignores cost |
-| `auto/offline` | Local-only providers (Ollama, vLLM, llama.cpp) — useful for air-gapped setups |
-| `auto/smart` | Reasoning quality first (Opus, GPT-5 xhigh, R1, GLM 5.1 reasoning) |
-| `auto/lkgp` | "Last Known Good Provider" — sticky to the most recently successful target |
+| Prefix | Optimizes for |
+| -------------- | ------------------------------------------------------------------------------------------- |
+| `auto` | Balanced default (latency × cost × success rate) |
+| `auto/coding` | Coding tasks: prefers Claude, GPT-5, GLM, Kimi, Qwen Coder, DeepSeek coders |
+| `auto/cheap` | Lowest $/token, accepts higher latency |
+| `auto/fast` | Lowest latency, ignores cost |
+| `auto/offline` | Local-only providers (Ollama, vLLM, llama.cpp) — useful for air-gapped setups |
+| `auto/smart` | Reasoning quality first (Opus, GPT-5 xhigh, R1, GLM 5.1 reasoning) |
+| `auto/lkgp` | "Last Known Good Provider" — pins to the last successful provider, then falls back to rules |
Example:
diff --git a/docs/routing/AUTO-COMBO.md b/docs/routing/AUTO-COMBO.md
index 6e3d598181..84d6a8e190 100644
--- a/docs/routing/AUTO-COMBO.md
+++ b/docs/routing/AUTO-COMBO.md
@@ -280,7 +280,7 @@ OmniRoute's combo engine supports **19 routing strategies** (declared in `src/sh
| `headroom` | Pick the target with the most remaining quota headroom |
| `strict-random` | Random without deduplication of repeats |
| `auto` | Use Auto Combo scoring (15-factor) — **recommended** |
-| `lkgp` | Last-Known-Good Path (sticky route to last successful target) |
+| `lkgp` | Last-Known-Good Path (pins to the last successful provider, then falls back to rules) |
| `context-optimized` | Pick target with best fit for current context size |
| `cache-optimized` | Reorder targets by prompt-cache affinity — the connection likeliest to already hold this request's cached prefix is tried first (`open-sse/services/combo/promptCacheAffinity.ts`, #8008) |
| `fusion` 🧬 | Fan out to a panel of models in parallel, then synthesize one answer via a judge (see below) |
diff --git a/tests/unit/router-strategies.test.ts b/tests/unit/router-strategies.test.ts
index 2a162c35ed..db88737ba6 100644
--- a/tests/unit/router-strategies.test.ts
+++ b/tests/unit/router-strategies.test.ts
@@ -262,6 +262,34 @@ test("lkgp — lkgpEnabled:false delegates to rules", () => {
assert.equal(d.strategy, "rules");
});
+test("lkgp — falls back to rules when the pool lacks the last known good provider", () => {
+ const pool = [cand({ provider: "x" })];
+ const context: RoutingContext = {
+ taskType: "default",
+ lastKnownGoodProvider: "y",
+ };
+ const d = getStrategy("lkgp").select(pool, context);
+ const expected = getStrategy("rules").select(pool, context);
+ assert.equal(d.strategy, expected.strategy);
+ assert.equal(d.provider, expected.provider);
+ assert.equal(d.model, expected.model);
+});
+
+test("lkgp — keeps the first candidate among several targets of the pinned provider", () => {
+ const pool = [
+ cand({ provider: "shared", model: "shared/m1", connectionId: "conn-1" }),
+ cand({ provider: "shared", model: "shared/m2", connectionId: "conn-2" }),
+ ];
+ const d = getStrategy("lkgp").select(pool, {
+ taskType: "default",
+ lastKnownGoodProvider: "shared",
+ });
+ assert.equal(d.provider, "shared");
+ assert.equal(d.model, "shared/m1");
+ assert.equal(d.connectionId, "conn-1");
+ assert.equal(d.candidatesConsidered, 1);
+});
+
// ── selectWithStrategy + registry ─────────────────────────────────────────────
test("selectWithStrategy — dispatches by name", () => {
const pool = [