Compare commits

..

5 Commits

Author SHA1 Message Date
Xiangzhe
65dd425fc9 fix(dashboard): route the Adapta tutorial CTA through the branded shortener
The Adapta tutorial modal's CTA linked straight to agent.adapta.one, so those
clicks never reached our metrics. Points the href at
https://link.omniroute.online/adapta (the `adapta` slug on our own shortener)
while keeping the visible link text as the real domain, so users still see
where they are going.

Completes the shortener rollout started in #11196 — that PR created the
`adapta` slug but did not wire this callsite.

Provider baseUrl configs that also mention agent.adapta.one are API endpoints
and are deliberately untouched.
2026-08-24 01:03:24 -03:00
Diego Rodrigues de Sa e Souza
ac02c5b42f fix(resilience): don't clear an active rate-limit cooldown for non-quota_exhausted errors (#11277) (#11310)
Merging --admin: only fails are ESLint warnings ratchet drift (inherited base-red) and dast-smoke (advisory, isRequired:null). Zero overlap with this PR's scope (src/lib/usage/providerLimits.ts).
2026-08-23 22:55:33 -03:00
Diego Rodrigues de Sa e Souza
07d1816a45 fix(providers): hidden models leak into GET /v1/models (#11300) (#11309)
Merging --admin: only fails are ESLint warnings ratchet drift (inherited) and dast-smoke (advisory, isRequired:null). Zero overlap with this PR's file scope (src/app/api/v1/models/catalog.ts).
2026-08-23 22:35:29 -03:00
Diego Rodrigues de Sa e Souza
3192eb88d5 fix(providers): unify learned and declared reasoning-effort clamps on nearest-tier semantics (#11295) (#11305)
Merging --admin with red discrimination (merge-gates §4). 5/6 failing tests are the compression/i18n-vi/kiro/memory mini-cluster fixed by #11306 (this branch predates it). The 6th (token-health-check-kimi, 'jittered expiration window') is a timing-sensitive test — reproduces GREEN locally, name itself indicates randomized jitter, not a deterministic regression from this PR's reasoning-effort clamp change (zero file overlap). ESLint warnings + dast-smoke (advisory, isRequired:null) also inherited.
2026-08-23 22:17:12 -03:00
Diego Rodrigues de Sa e Souza
8969526437 fix(tests): drain compression/kiro/memory/i18n base-red mini-cluster (2026-08-24) (#11306)
Merging --admin: this is itself the base-red drain PR (compression/i18n-vi/i18n-pt-br/kiro/memory mini-cluster). Remaining fails (ESLint warnings ratchet drift, dast-smoke advisory) are not blocking; isRequired:null on dast-smoke.
2026-08-23 22:17:09 -03:00
10 changed files with 504 additions and 44 deletions

View File

@@ -12,7 +12,21 @@
* `sanitizeReasoningEffortForProvider` in `executors/base/reasoningEffort.ts`)
* so the 4xx→retry round-trip is paid at most once per process per provider+model.
*
* `clampToLearned` implements downgrade-only clamping: greatest accepted <= demand.
* `clampToLearned` implements nearest-tier clamping: smallest accepted >= demand,
* falling back to the greatest accepted when demand exceeds every accepted value.
* (#11295 — unified with the static "declared" clamp in
* `executors/base/reasoningEffort.ts`, which already used nearest-tier semantics.
* Before #11295, this learned clamp was downgrade-only — greatest accepted <=
* demand — so the SAME accepted set {low,high,max} produced medium→low here but
* medium→high via the declared path: identical inputs, opposite outputs,
* depending only on whether the model had a static registry entry. #11274's
* DeepSeek native mapping is the precedent for nearest-tier. This also fixes a
* standalone bug: a request BELOW the learned floor (e.g. none/minimal on a
* model that only ever advertised {low,high,max}) used to return null — no
* clamp — so the too-low value passed straight through to the upstream, which
* 400'd again on every subsequent request without ever learning a lower floor.
* Nearest-tier naturally fixes this too: the smallest accepted value is always
* >= any demand below the floor, so it is returned instead of null.
*
* In-memory only (same operator-accepted tradeoff as the thinking-budget cache):
* restart resets, the first request after a restart may re-learn at the cost of
@@ -132,25 +146,39 @@ export function recordLearnedReasoningEffort(
}
/**
* Return the greatest accepted value <= effortStr (downgrade only), or null
* if effortStr is already accepted, below the minimum, or not in ORDER.
* Return the nearest-tier accepted value for effortStr: the smallest accepted
* value with rank >= effortStr's rank, or — when effortStr's rank exceeds every
* accepted value (demand above the learned ceiling) — the greatest accepted
* value. Returns null only when effortStr is already accepted (no clamp
* needed), empty, or not a recognized member of REASONING_EFFORT_ORDER.
*
* Mirrors the declared-capability clamp in `executors/base/reasoningEffort.ts`
* (#11295): both now use nearest-tier semantics so the same accepted set
* produces the same mapping regardless of whether the model has a static
* registry entry or was only learned reactively from an upstream 4xx.
*/
export function clampToLearned(effortStr: string, accepted: Set<string>): string | null {
if (!effortStr || accepted.has(effortStr)) return null;
const rank = rankOf(effortStr);
if (rank === -1) return null;
const minRank = Math.min(...[...accepted].map((v) => rankOf(v)));
if (rank < minRank) return null;
let best: string | null = null;
let bestRank = -1;
let nearestAbove: string | null = null;
let nearestAboveRank = Infinity;
let highest: string | null = null;
let highestRank = -1;
for (const v of accepted) {
const r = rankOf(v);
if (r <= rank && r > bestRank) {
bestRank = r;
best = v;
if (r < 0) continue;
if (r >= rank && r < nearestAboveRank) {
nearestAboveRank = r;
nearestAbove = v;
}
if (r > highestRank) {
highestRank = r;
highest = v;
}
}
return best;
return nearestAbove ?? highest;
}
// Matches prose shapes: OVH's "@ai-sdk/openai-compatible" deserializer

View File

@@ -7,6 +7,10 @@ type AdaptaTutorialModalProps = {
onClose: () => void;
};
// The Adapta CTA href points at https://link.omniroute.online/adapta (our own
// shortener, the `adapta` slug) so the click lands in our Kutt metrics. The visible
// link text intentionally stays the real domain (agent.adapta.one/agentic-chat) so
// users still see where they are going.
export function AdaptaTutorialModal({ isOpen, onClose }: AdaptaTutorialModalProps) {
const t = useTranslations("providers.adaptaTutorial");
@@ -29,7 +33,7 @@ export function AdaptaTutorialModal({ isOpen, onClose }: AdaptaTutorialModalProp
<p className="text-text-muted mt-0.5">
{t("step1DescPrefix")}{" "}
<a
href="https://agent.adapta.one/agentic-chat"
href="https://link.omniroute.online/adapta"
target="_blank"
rel="noopener noreferrer"
className="underline text-primary"

View File

@@ -265,10 +265,6 @@ async function buildUnifiedModelsResponseCore(
// try would let a crash here propagate as an unhandled rejection instead
// (catalogCache.ts's in-flight coalescing does not fully consume rejections).
const hiddenModelsByProvider = getHiddenModelsByProvider();
const isModelHiddenBulk = (providerId: string, modelId: string): boolean => {
const hiddenSet = hiddenModelsByProvider.get(providerId);
return hiddenSet ? hiddenSet.has(modelId) : false;
};
let settings: Record<string, any> = {};
try {
settings = await getSettings();
@@ -377,6 +373,35 @@ async function buildUnifiedModelsResponseCore(
const resolvePublicOwnerId = (providerId: string, canonicalProviderId: string): string =>
providerIdToPrefix[providerId] || canonicalProviderId;
// #11300: the visibility toggle on a provider's dashboard page persists the
// hidden-model row under whatever key the route's `[id]` param happened to be
// (a node UUID, an alias like `cc`/`gh`/`cx`, or a canonical provider id) —
// see `PATCH /api/provider-models`. The catalog loops below each key their own
// lookup differently (raw connection provider, canonical id, or alias), so a
// single-key lookup missed the override whenever the write key and the read key
// diverged. Check every key a model could plausibly have been hidden under:
// the raw key passed in, its resolved canonical provider id, that canonical id's
// alias, and the compatible-provider-node prefix for either.
const isModelHiddenBulk = (
providerKey: string | null | undefined,
modelId: string,
canonicalProviderId?: string | null
): boolean => {
if (!providerKey || !modelId) return false;
const canonical = canonicalProviderId || resolveCanonicalProviderId(providerKey);
const alias =
providerIdToAlias[canonical] || providerIdToAlias[providerKey] || undefined;
const nodePrefix = providerIdToPrefix[providerKey] || providerIdToPrefix[canonical];
const keysToCheck = [providerKey, canonical, alias, nodePrefix].filter(
(k): k is string => Boolean(k)
);
for (const key of keysToCheck) {
const hiddenSet = hiddenModelsByProvider.get(key);
if (hiddenSet?.has(modelId)) return true;
}
return false;
};
// Get combos
let combos = [];
await yieldCatalogBuildTurn();
@@ -955,7 +980,7 @@ async function buildUnifiedModelsResponseCore(
if (!isModelSelectable(canonicalProviderId, model.id)) continue;
if (!providerSupportsModel(canonicalProviderId, model.id)) continue;
const aliasId = `${alias}/${model.id}`;
if (isModelHiddenBulk(canonicalProviderId, model.id)) continue;
if (isModelHiddenBulk(alias, model.id, canonicalProviderId)) continue;
if (isExcludedByProviderConnections(canonicalProviderId, model.id)) continue;
if (shouldHidePaid(canonicalProviderId, model.id, (model as { pricing?: unknown }).pricing))
continue;
@@ -1018,7 +1043,15 @@ async function buildUnifiedModelsResponseCore(
for (const modelId of CODEX_NATIVE_UNPREFIXED_MODELS) {
if (!providerSupportsModel("codex", modelId)) continue;
if (isModelHiddenBulk("codex", modelId)) continue;
// #11300: a codex-native unprefixed model can also be hidden via the
// `openai` provider page (codex runs on the openai-compatible connection)
// or via the `cx` alias — check all three so a hide from any of them
// suppresses the bare model id here.
if (
isModelHiddenBulk("codex", modelId) ||
isModelHiddenBulk("openai", modelId)
)
continue;
const alias = providerIdToAlias.codex || "cx";
const aliasId = `${alias}/${modelId}`;
@@ -1079,7 +1112,7 @@ async function buildUnifiedModelsResponseCore(
if (canonicalProviderId === "codex" && isCodexDiscoveryModelExcluded(sm)) {
continue;
}
if (isModelHiddenBulk(providerId, sm.id)) continue;
if (isModelHiddenBulk(providerId, sm.id, canonicalProviderId)) continue;
if (isExcludedByProviderConnections(canonicalProviderId, sm.id)) continue;
// #6457: some upstream discovery catalogs (e.g. HuggingFace's live
// `/v1/models`) return image/diffusion models with no modality info,
@@ -1498,7 +1531,7 @@ async function buildUnifiedModelsResponseCore(
if (!isUnifiedChatSourceModelSelectable(canonicalProviderId, { ...model, id: modelId }))
continue;
if (model.isHidden === true) continue;
if (isModelHiddenBulk(canonicalProviderId, modelId)) continue;
if (isModelHiddenBulk(providerId, modelId, canonicalProviderId)) continue;
if (isExcludedByProviderConnections(canonicalProviderId, modelId)) continue;
// #6328: apply hidePaidModels to user-defined custom rows too.
// Custom entries do not carry pricing, so shouldHidePaid() decides
@@ -1682,7 +1715,7 @@ async function buildUnifiedModelsResponseCore(
continue;
}
if (isModelHiddenBulk(canonicalProviderId, modelId)) continue;
if (isModelHiddenBulk(providerKey, modelId, canonicalProviderId)) continue;
if (isExcludedByProviderConnections(canonicalProviderId, modelId)) continue;
// #6328: apply hidePaidModels to alias-backed rows too. Alias mappings
// point at providerKey/modelId with no pricing, so shouldHidePaid()
@@ -1756,7 +1789,7 @@ async function buildUnifiedModelsResponseCore(
for (const model of fallbackModels) {
const modelId = typeof model.id === "string" ? model.id : null;
if (!modelId) continue;
if (isModelHiddenBulk(canonicalProviderId, modelId)) continue;
if (isModelHiddenBulk(providerId, modelId, canonicalProviderId)) continue;
if (isExcludedByProviderConnections(canonicalProviderId, modelId)) continue;
// #6328: apply hidePaidModels to managed-fallback rows too. Compatible
// provider fallbacks lack pricing; shouldHidePaid() decides via the

View File

@@ -499,6 +499,25 @@ export async function maybeClearRecoveredQuotaState(
// the previous synthetic-cooldown guard.
return connection;
}
} else if (
connection.rateLimitedUntil &&
new Date(connection.rateLimitedUntil).getTime() > Date.now()
) {
// Universal fallback guard for every lastErrorType other than
// "quota_exhausted" (which gets the more precise per-window check above,
// and may legitimately release early once the REAL window has reset even
// while a synthetic rateLimitedUntil is still in the future). A future
// rateLimitedUntil is a hard statement made by the 429/error handler that
// persisted it (src/sse/services/auth.ts, src/app/api/providers/[id]/test/
// route.ts) — no quota poll finding *some* usable window elsewhere should
// be able to overrule it. Before this fix, ANY lastErrorType other than
// "quota_exhausted" skipped straight to hasTransientState/
// clearRecoveredProviderState() below with no rateLimitedUntil check at
// all, so a multi-day cooldown (observed: 146h, Z.AI weekly quota) got
// cleared on the very next quota sync a few minutes later — a
// self-restart/burn loop that kept burning real upstream calls against a
// known-exhausted connection (#11277).
return connection;
}
const hasTransientState =

View File

@@ -0,0 +1,177 @@
/**
* #11300 — Models toggled to "Hidden" on Provider pages are still listed in
* `GET /v1/models`.
*
* `PATCH /api/provider-models?provider=<key>&modelId=<id>` persists the hidden
* override under whatever key the dashboard's `[id]` route param happened to be
* (an alias like `cc`/`gh`/`cx`, a canonical provider id, a compatible-provider
* node UUID, or its configured prefix). `catalog.ts`'s `isModelHiddenBulk()` did
* a single-key lookup, so a model stayed listed in `/v1/models` whenever the key
* used to READ diverged from the key used to WRITE:
*
* - Static `PROVIDER_MODELS` loop checked only `canonicalProviderId` — a model
* hidden under the alias (e.g. `cc` for Claude Code) never matched.
* - The Codex-native-unprefixed loop checked only `"codex"` — a model hidden
* via the `openai` provider page (codex often shares the openai-compatible
* connection) never matched.
* - The synced-discovery loop checked only the raw connection `providerId` —
* a model hidden via the compatible-provider node's configured *prefix*
* (the identifier the operator actually sees/uses on that node's page)
* never matched.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-11300-hidden-leak-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const modelsDb = await import("../../src/lib/db/models.ts");
const { mergeModelCompatOverride } = await import("../../src/lib/localDb.ts");
const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
v1ModelsCatalog.__resetCatalogBuilderRunsForTest();
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(async () => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
async function fetchCatalogIds(): Promise<string[]> {
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
assert.equal(response.status, 200);
const body = (await response.json()) as { data: Array<{ id: string }> };
assert.ok(Array.isArray(body.data), "response has data array");
return body.data.map((m) => m.id);
}
test("#11300 A: hiding a static model under its ALIAS (cc) excludes it under both cc/ and claude/ ids", async () => {
await providersDb.createProviderConnection({
provider: "claude",
authType: "apikey",
name: "claude-main",
apiKey: "sk-test-11300a",
isActive: true,
testStatus: "active",
providerSpecificData: {},
});
// Sanity: before hiding, the model is advertised.
let ids = await fetchCatalogIds();
assert.ok(
ids.includes("cc/claude-opus-5"),
`expected cc/claude-opus-5 to be listed before hiding — got ${JSON.stringify(ids.filter((i) => i.includes("claude-opus-5")))}`
);
// Operator hides the model on the provider page, whose route param is the
// alias "cc" (not the canonical "claude").
mergeModelCompatOverride("cc", "claude-opus-5", { isHidden: true });
ids = await fetchCatalogIds();
assert.ok(
!ids.includes("cc/claude-opus-5"),
`#11300 RED: cc/claude-opus-5 hidden under alias "cc" must not appear — got ${JSON.stringify(ids.filter((i) => i.includes("claude-opus-5")))}`
);
assert.ok(
!ids.includes("claude/claude-opus-5"),
`#11300 RED: claude/claude-opus-5 hidden under alias "cc" must not appear either`
);
});
test("#11300 B: hiding a codex-native unprefixed model under \"openai\" excludes the bare model id", async () => {
await providersDb.createProviderConnection({
provider: "codex",
authType: "oauth",
name: "codex-main",
apiKey: "sk-test-11300b",
isActive: true,
testStatus: "active",
providerSpecificData: {},
});
const nativeModelId = "gpt-5.6-sol";
let ids = await fetchCatalogIds();
assert.ok(
ids.includes(nativeModelId),
`expected bare "${nativeModelId}" to be listed before hiding — got ${JSON.stringify(ids.filter((i) => i.includes("gpt-5.6-sol")))}`
);
// Hidden via the "openai" provider page (codex native models are commonly
// reached through the shared openai-compatible connection).
mergeModelCompatOverride("openai", nativeModelId, { isHidden: true });
ids = await fetchCatalogIds();
assert.ok(
!ids.includes(nativeModelId),
`#11300 RED: bare "${nativeModelId}" hidden under "openai" must not appear — got ${JSON.stringify(ids.filter((i) => i.includes("gpt-5.6-sol")))}`
);
});
test("#11300 C: hiding a compatible-node synced model under its configured PREFIX excludes prefix/<model>", async () => {
const NODE_ID = "openai-compatible-chat-11300-c0ffee00-0000-4000-8000-000000000000";
const PREFIX = "deepseek-node-11300";
await providersDb.createProviderNode({
id: NODE_ID,
type: "openai-compatible",
name: "Deepseek Node (11300 probe)",
prefix: PREFIX,
baseUrl: "https://proxy.example.com",
chatPath: "/v1/chat/completions",
modelsPath: "/v1/models",
});
const connection = await providersDb.createProviderConnection({
provider: NODE_ID,
authType: "apikey",
name: "deepseek-node-conn",
apiKey: "sk-test-11300c",
isActive: true,
testStatus: "active",
providerSpecificData: {
baseUrl: "https://proxy.example.com",
chatPath: "/v1/chat/completions",
modelsPath: "/v1/models",
},
});
const modelId = "deepseek-v4-flash-0731";
await modelsDb.replaceSyncedAvailableModelsForConnection(NODE_ID, (connection as { id: string }).id, [
{ id: modelId, name: "DeepSeek V4 Flash", source: "imported", supportedEndpoints: ["chat"] },
]);
let ids = await fetchCatalogIds();
assert.ok(
ids.includes(`${PREFIX}/${modelId}`),
`expected ${PREFIX}/${modelId} to be listed before hiding — got ${JSON.stringify(ids.filter((i) => i.includes(modelId)))}`
);
// Operator hides the model via the node's page, which is keyed by the
// configured prefix rather than the internal node UUID.
mergeModelCompatOverride(PREFIX, modelId, { isHidden: true });
ids = await fetchCatalogIds();
assert.ok(
!ids.includes(`${PREFIX}/${modelId}`),
`#11300 RED: ${PREFIX}/${modelId} hidden under prefix "${PREFIX}" must not appear — got ${JSON.stringify(ids.filter((i) => i.includes(modelId)))}`
);
assert.ok(
!ids.includes(`${NODE_ID}/${modelId}`),
`#11300 RED: ${NODE_ID}/${modelId} hidden under prefix "${PREFIX}" must not appear either`
);
});

View File

@@ -130,13 +130,18 @@ test("a later, lower accepted-list does ratchet the cap down", () => {
assert.equal((getLearnedReasoningEffort("acme", "model-x") as unknown as Set<string>).size, 2);
});
test("clampToLearned medium→low when accepted is low,high,max", async () => {
// #11295: nearest-tier semantics (smallest accepted >= demand) — unified with
// the declared/static clamp. Was downgrade-only (greatest accepted <= demand,
// medium→low) before #11295.
test("clampToLearned medium→high when accepted is low,high,max (nearest-tier, #11295)", async () => {
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
assert.equal(clampToLearned("medium", new Set(["low", "high", "max"])), "low");
assert.equal(clampToLearned("medium", new Set(["low", "high", "max"])), "high");
});
test("clampToLearned xhigh→high when accepted is low,high,max", async () => {
// #11295: xhigh(rank 5) has no accepted tier >= it among {low,high,max}
// (max=6 IS >= 5, so nearest-tier picks max) — was downgrade-only high before.
test("clampToLearned xhigh→max when accepted is low,high,max (nearest-tier, #11295)", async () => {
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
assert.equal(clampToLearned("xhigh", new Set(["low", "high", "max"])), "high");
assert.equal(clampToLearned("xhigh", new Set(["low", "high", "max"])), "max");
});
test("clampToLearned ultra→max when accepted is low,high,max", async () => {
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
@@ -154,17 +159,25 @@ test("clampToLearned returns null when already accepted", async () => {
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
assert.equal(clampToLearned("low", new Set(["low", "high", "max"])), null);
});
test("clampToLearned returns null when effort < min (no upgrade)", async () => {
// #11295: a sub-floor demand (below every accepted value) now maps to the
// accepted floor instead of returning null. Pre-#11295 this returned null —
// no clamp — so the too-low value passed straight through to the upstream,
// which 400'd again on every subsequent request without ever learning a
// lower floor.
test("clampToLearned maps sub-floor demand to the accepted floor instead of null (#11295)", async () => {
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
assert.equal(clampToLearned("low", new Set(["high", "max"])), null);
assert.equal(clampToLearned("low", new Set(["high", "max"])), "high");
});
test("clampToLearned returns null for turbo (not in ORDER)", async () => {
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
assert.equal(clampToLearned("turbo", new Set(["low", "high", "max"])), null);
});
test("clampToLearned returns null when effort is none but accepted is low,high,max", async () => {
// #11295: none is below the learned floor {low,high,max} — nearest-tier maps
// it to the floor (low) instead of returning null (no clamp, upstream 400s
// again with no chance to ever learn a lower floor).
test("clampToLearned maps none to the floor (low) when accepted is low,high,max (#11295)", async () => {
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
assert.equal(clampToLearned("none", new Set(["low", "high", "max"])), null);
assert.equal(clampToLearned("none", new Set(["low", "high", "max"])), "low");
});
test("recordLearned stores Set and getLearned returns Set", () => {
const s = recordLearnedReasoningEffort("acme", "m1", ["low", "high", "max"]);

View File

@@ -83,7 +83,25 @@ test.after(async () => {
});
test("successful GLM quota refresh clears transient rate-limit state", async () => {
const connection = await createGlmConnectionWithTransientCooldown();
// The cooldown must already be EXPIRED for a successful refresh to clear it
// (#11277: a rateLimitedUntil still in the future is a hard statement from
// the error handler that persisted it — no quota poll may overrule it,
// regardless of lastErrorType). Before #11277's fix this test used a
// still-future rateLimitedUntil and asserted it got cleared anyway, which
// was the same defect class as the reported bug, just a shorter window.
const connection = await providersDb.createProviderConnection({
provider: "glm",
authType: "apikey",
name: `GLM Recovery ${Date.now()}`,
apiKey: "glm-test-key",
testStatus: "unavailable",
rateLimitedUntil: new Date(Date.now() - 60_000).toISOString(),
lastError: "rate limit exceeded",
lastErrorType: "rate_limited",
lastErrorSource: "executor",
errorCode: 429,
backoffLevel: 2,
});
const connectionId = (connection as { id: string }).id;
await withMockedFetch((() => glmQuotaResponse()) as typeof fetch, async () => {
@@ -101,6 +119,39 @@ test("successful GLM quota refresh clears transient rate-limit state", async ()
assert.equal(updated.backoffLevel, 0, "backoffLevel should be reset to 0");
});
test("a still-future rateLimitedUntil is not cleared by a successful quota refresh, regardless of lastErrorType (#11277)", async () => {
const stillFutureRateLimitedUntil = new Date(Date.now() + 60_000).toISOString();
const connection = await providersDb.createProviderConnection({
provider: "glm",
authType: "apikey",
name: `GLM Still Cooling ${Date.now()}`,
apiKey: "glm-test-key",
testStatus: "unavailable",
rateLimitedUntil: stillFutureRateLimitedUntil,
lastError: "rate limit exceeded",
lastErrorType: "rate_limited",
lastErrorSource: "executor",
errorCode: 429,
backoffLevel: 2,
});
const connectionId = (connection as { id: string }).id;
await withMockedFetch((() => glmQuotaResponse()) as typeof fetch, async () => {
await providerLimits.fetchAndPersistProviderLimits(connectionId, "manual");
});
const updated = (await providersDb.getProviderConnectionById(connectionId)) as Record<
string,
unknown
>;
assert.equal(
updated.testStatus,
"unavailable",
"an active cooldown must stay locked even though the quota fetch succeeded"
);
assert.equal(updated.rateLimitedUntil, stillFutureRateLimitedUntil);
});
async function createGlmConnectionWithStatus(status: string) {
return providersDb.createProviderConnection({
provider: "glm",
@@ -334,6 +385,52 @@ test("Claude subscription quota still exhausted keeps the connection locked (no
assert.equal(after.rateLimitedUntil, syntheticRateLimitedUntil);
});
test("rate_limit_exceeded cooldown is not cleared early by an unrelated quota window looking usable (#11277)", async () => {
// Reproduces #11277: a connection-scoped cooldown persisted with
// lastErrorType "rate_limit_exceeded" (RateLimitReason.RATE_LIMIT_EXCEEDED)
// and a long rateLimitedUntil (derived from an upstream reset hint — the
// reported production case was ~146h) must NOT be cleared just because the
// next scheduled quota sync reports hasUsableQuota()===true from some
// unrelated window. Before the fix, only lastErrorType==="quota_exhausted"
// reached the rateLimitedUntil guard, so every other reason (including
// rate_limit_exceeded) skipped straight to clearRecoveredProviderState(),
// producing a self-restart/burn loop on a multi-day cooldown.
const farFutureRateLimitedUntil = new Date(Date.now() + 146 * 60 * 60 * 1000).toISOString();
const created = await providersDb.createProviderConnection({
provider: "opencode",
authType: "apikey",
name: `OpenCode RateLimitExceeded ${Date.now()}`,
apiKey: "opencode-test-key",
testStatus: "unavailable",
isActive: true,
lastError: "Account quota exhausted (opencode)",
lastErrorType: "rate_limit_exceeded",
errorCode: 429,
rateLimitedUntil: farFutureRateLimitedUntil,
backoffLevel: 1,
});
const connectionId = (created as { id: string }).id;
const connection = await providersDb.getProviderConnectionById(connectionId);
// No `quotas` object at all (degraded/partial fetch shape) — this is the
// exact shape that, pre-fix, fell straight through to hasTransientState
// and cleared the cooldown for any lastErrorType other than quota_exhausted.
const result = await providerLimits.maybeClearRecoveredQuotaState(connection, {
quotas: { unrelated: { unlimited: true } },
});
assert.equal(
result.testStatus,
"unavailable",
"an active rate_limit_exceeded cooldown must stay locked"
);
const after = await providersDb.getProviderConnectionById(connectionId);
assert.equal(after.testStatus, "unavailable");
assert.equal(after.lastErrorType, "rate_limit_exceeded");
assert.equal(after.rateLimitedUntil, farFutureRateLimitedUntil);
});
test("CAS primitive clears when expected state matches", async () => {
const created = await createGlmConnectionWithTransientCooldown();
const connectionId = (created as { id: string }).id;

View File

@@ -108,7 +108,7 @@ test("a second request for the same provider+model sends the learned value on th
}
});
test("400 please use low, high, or max clamps and retries once", async () => {
test("400 please use low, high, or max clamps and retries once (nearest-tier: medium -> high, #11295)", async () => {
const executor = new SimpleExecutor();
const originalFetch = globalThis.fetch;
const capturedBodies: Record<string, unknown>[] = [];
@@ -140,7 +140,10 @@ test("400 please use low, high, or max clamps and retries once", async () => {
});
assert.equal(capturedBodies.length, 2);
assert.equal(capturedBodies[0].reasoning_effort, "medium");
assert.equal(capturedBodies[1].reasoning_effort, "low");
// #11295: nearest-tier — smallest accepted >= demand — maps medium(3) to
// high(4), the smallest accepted rank at or above it (was "low" under the
// old downgrade-only direction).
assert.equal(capturedBodies[1].reasoning_effort, "high");
const learned = getLearnedReasoningEffort("openai-compatible-chat-eaff6869", "x-preview-f-free") as unknown as Set<string>;
assert.ok(learned instanceof Set);
assert.ok(learned.has("low"));
@@ -190,7 +193,7 @@ test("400 please use low, medium with ultra retries to medium", async () => {
}
});
test("no-op clamp does not retry: learned {high,max} with low request stays single-fetch", async () => {
test("sub-floor clamp now retries: learned {high,max} with low request clamps up to high (#11295)", async () => {
const executor = new SimpleExecutor();
const originalFetch = globalThis.fetch;
const capturedBodies: Record<string, unknown>[] = [];
@@ -214,17 +217,20 @@ test("no-op clamp does not retry: learned {high,max} with low request stays sing
};
try {
// low is below the learned minimum {high,max}: downgrade-only passthrough,
// sanitizer leaves the body unchanged -> no identical-body retry.
// #11295: low is below the learned minimum {high,max}. Pre-#11295 this was
// a downgrade-only passthrough (no clamp, no retry, upstream stayed 400
// forever). Nearest-tier now clamps up to the accepted floor (high) and
// retries once, succeeding.
const result = await executor.execute({
model: "x-preview-f-free-3",
body: { reasoning_effort: "low" },
stream: false,
credentials: {},
});
assert.equal(capturedBodies.length, 1);
assert.equal(capturedBodies.length, 2);
assert.equal(capturedBodies[0].reasoning_effort, "low");
assert.equal(result.response.status, 400);
assert.equal(capturedBodies[1].reasoning_effort, "high");
assert.equal(result.response.status, 200);
} finally {
globalThis.fetch = originalFetch;
}

View File

@@ -0,0 +1,79 @@
// #11295 — the learned clamp (reactive, from upstream 4xx) and the declared
// clamp (static registry `supportedThinkingEfforts`) used to disagree on
// direction for the identical accepted set {low,high,max}: the learned path
// was downgrade-only (medium -> low) while the declared path was already
// nearest-tier (medium -> high). Same inputs, opposite outputs, depending only
// on whether the model happened to have a static registry entry. This test
// proves the two paths now agree, and that a request below the learned floor
// (previously silently passed through unmapped, returning null from
// clampToLearned) is now mapped up to the nearest accepted tier instead.
import { test, after, beforeEach } from "node:test";
import assert from "node:assert/strict";
import { clampToLearned } from "../../open-sse/services/learnedReasoningEffortCaps.ts";
import { sanitizeReasoningEffortForProvider } from "../../open-sse/executors/base/reasoningEffort.ts";
import {
recordLearnedReasoningEffort,
__test_resetLearnedReasoningEffortCaps,
} from "../../open-sse/services/learnedReasoningEffortCaps.ts";
beforeEach(() => {
__test_resetLearnedReasoningEffortCaps();
});
after(() => {
__test_resetLearnedReasoningEffortCaps();
});
test("clampToLearned: nearest-tier medium -> high when accepted is {low,high,max} (was low pre-#11295)", () => {
assert.equal(clampToLearned("medium", new Set(["low", "high", "max"])), "high");
});
test("sanitizeReasoningEffortForProvider maps medium identically for a LEARNED-only model and a DECLARED model with the same {low,high,max} accepted set", () => {
// Learned side: a custom OpenAI-compatible connection that has no static
// registry entry — the only source of truth is the reactively-learned set.
recordLearnedReasoningEffort("acme-oai-compatible", "custom-reasoner", [
"low",
"high",
"max",
]);
const learnedResult = sanitizeReasoningEffortForProvider(
{ reasoning_effort: "medium" },
"acme-oai-compatible",
"custom-reasoner"
) as Record<string, unknown>;
// Declared side: opencode-go/ox-alpha-free, whose registry entry declares
// supportedThinkingEfforts: ["low", "high", "max"] (see reasoningEffort.ts
// comment referencing the Console Go 400 case).
const declaredResult = sanitizeReasoningEffortForProvider(
{ reasoning_effort: "medium" },
"opencode-go",
"ox-alpha-free"
) as Record<string, unknown>;
assert.equal(learnedResult.reasoning_effort, "high");
assert.equal(declaredResult.reasoning_effort, "high");
assert.equal(learnedResult.reasoning_effort, declaredResult.reasoning_effort);
});
test("sub-floor request (none) on a learned-only model with floor {low,high,max} maps to low, not a pass-through null-clamp", () => {
recordLearnedReasoningEffort("acme-oai-compatible", "custom-reasoner-2", [
"low",
"high",
"max",
]);
const result = sanitizeReasoningEffortForProvider(
{ reasoning_effort: "none" },
"acme-oai-compatible",
"custom-reasoner-2"
) as Record<string, unknown>;
assert.equal(result.reasoning_effort, "low");
});
test("clampToLearned: sub-floor demand (none) below accepted {low,high,max} maps to the accepted floor (low), not null", () => {
assert.equal(clampToLearned("none", new Set(["low", "high", "max"])), "low");
});
test("clampToLearned: sub-floor demand (low) below accepted {high,max} maps to the accepted floor (high), not null", () => {
assert.equal(clampToLearned("low", new Set(["high", "max"])), "high");
});

View File

@@ -90,23 +90,25 @@ test("deepseek's non-ordinal max<->xhigh translation is untouched by the learned
assert.equal(result.reasoning_effort, "max");
});
test("proactive clamp: medium→low for learned {low,high,max}", () => {
// #11295: nearest-tier — smallest accepted >= demand — replaces the old
// downgrade-only (greatest accepted <= demand) direction.
test("proactive clamp: medium→high for learned {low,high,max} (nearest-tier, #11295)", () => {
recordLearnedReasoningEffort("opencode-zen-direct", "x-preview-f-free", ["low", "high", "max"]);
const out = sanitizeReasoningEffortForProvider(
{ reasoning_effort: "medium", model: "x-preview-f-free" },
"opencode-zen-direct",
"x-preview-f-free"
) as { reasoning_effort: string };
assert.equal(out.reasoning_effort, "low");
assert.equal(out.reasoning_effort, "high");
});
test("proactive clamp: xhigh→high for learned {low,high,max}", () => {
test("proactive clamp: xhigh→max for learned {low,high,max} (nearest-tier, #11295)", () => {
recordLearnedReasoningEffort("opencode-zen-direct", "x-preview-f-free-2", ["low", "high", "max"]);
const out = sanitizeReasoningEffortForProvider(
{ reasoning_effort: "xhigh", model: "x-preview-f-free-2" },
"opencode-zen-direct",
"x-preview-f-free-2"
) as { reasoning_effort: string };
assert.equal(out.reasoning_effort, "high");
assert.equal(out.reasoning_effort, "max");
});
test("proactive clamp: ultra→max for learned {low,high,max}", () => {
recordLearnedReasoningEffort("opencode-zen-direct", "x-preview-f-free-3", ["low", "high", "max"]);
@@ -135,14 +137,16 @@ test("proactive clamp: high→medium for learned {low,medium}", () => {
) as { reasoning_effort: string };
assert.equal(out.reasoning_effort, "medium");
});
test("no upgrade: low stays low for learned {high,max}", () => {
// #11295: sub-floor demand (low, below the learned floor {high,max}) now
// clamps up to the floor instead of passing through unchanged.
test("sub-floor clamp: low→high for learned {high,max} (#11295)", () => {
recordLearnedReasoningEffort("acme", "m3", ["high", "max"]);
const out = sanitizeReasoningEffortForProvider(
{ reasoning_effort: "low", model: "m3" },
"acme",
"m3"
) as { reasoning_effort: string };
assert.equal(out.reasoning_effort, "low");
assert.equal(out.reasoning_effort, "high");
});
test("custom model ultra→medium for learned {low,medium}", () => {
recordLearnedReasoningEffort("openai-compatible-chat-eaff6869", "qwen3-coder-30b-a3b-instruct-2", ["low", "medium"]);