Compare commits

..

1 Commits

Author SHA1 Message Date
Xiangzhe
268d97cd43 fix(providers): hidden models leak into GET /v1/models (#11300)
The visibility toggle on a provider's dashboard page (PATCH
/api/provider-models) persists the hidden-model override under whatever key
the page's [id] route param happened to be — an alias (cc/gh/cx/ag/xao), a
canonical provider id, a compatible-provider node UUID, or its configured
prefix. catalog.ts's isModelHiddenBulk() only ever did a single-key lookup,
so a hidden model stayed listed in GET /v1/models whenever the write key and
the loop's read key diverged.

Make isModelHiddenBulk multi-key aware: given a provider key and an optional
already-resolved canonical id, it now checks the raw key, its canonical
provider id, that canonical id's alias, and the compatible-provider-node
prefix for either — covering every key the dashboard could plausibly have
written under. Updated every catalog loop call site (static PROVIDER_MODELS,
Codex-native-unprefixed, synced-discovery, custom models, alias-backed
models, managed-fallback) to pass along whichever raw/canonical pair it
already has in scope.
2026-08-23 22:16:47 -03:00
14 changed files with 46 additions and 383 deletions

View File

@@ -12,21 +12,7 @@
* `sanitizeReasoningEffortForProvider` in `executors/base/reasoningEffort.ts`)
* so the 4xx→retry round-trip is paid at most once per process per provider+model.
*
* `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.
* `clampToLearned` implements downgrade-only clamping: greatest accepted <= demand.
*
* 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
@@ -146,39 +132,25 @@ export function recordLearnedReasoningEffort(
}
/**
* 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.
* Return the greatest accepted value <= effortStr (downgrade only), or null
* if effortStr is already accepted, below the minimum, or not in ORDER.
*/
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;
let nearestAbove: string | null = null;
let nearestAboveRank = Infinity;
let highest: string | null = null;
let highestRank = -1;
const minRank = Math.min(...[...accepted].map((v) => rankOf(v)));
if (rank < minRank) return null;
let best: string | null = null;
let bestRank = -1;
for (const v of accepted) {
const r = rankOf(v);
if (r < 0) continue;
if (r >= rank && r < nearestAboveRank) {
nearestAboveRank = r;
nearestAbove = v;
}
if (r > highestRank) {
highestRank = r;
highest = v;
if (r <= rank && r > bestRank) {
bestRank = r;
best = v;
}
}
return nearestAbove ?? highest;
return best;
}
// Matches prose shapes: OVH's "@ai-sdk/openai-compatible" deserializer

View File

@@ -7,10 +7,6 @@ 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");
@@ -33,7 +29,7 @@ export function AdaptaTutorialModal({ isOpen, onClose }: AdaptaTutorialModalProp
<p className="text-text-muted mt-0.5">
{t("step1DescPrefix")}{" "}
<a
href="https://link.omniroute.online/adapta"
href="https://agent.adapta.one/agentic-chat"
target="_blank"
rel="noopener noreferrer"
className="underline text-primary"

View File

@@ -439,22 +439,13 @@ type ProviderConnectionLike = {
* whose stored `providerSpecificData.profileArn` matches the given ARN.
* Returns null when profileArn is undefined/null or no match is found.
*
* #10815 hardened `findKiroConnectionByIdentity` to require an account-level
* identifier (email or clientId) alongside a matching profileArn before
* trusting the match — distinct Builder ID accounts (Google/GitHub social
* login) can share the same CodeWhisperer profile ARN, and matching on ARN
* alone let a second social login silently overwrite the first connection.
* `email`/`clientId` here let a caller supply that account identifier; the
* real `saveAndRespond()` call sites already do (see below).
*
* Exported for unit tests (#3615).
*/
export function findKiroConnectionByProfileArn(
connections: ProviderConnectionLike[],
profileArn: string | undefined,
accountIdentity?: { email?: string | null; clientId?: string | null }
profileArn: string | undefined
): ProviderConnectionLike | null {
return findKiroConnectionByIdentity(connections, { profileArn, ...accountIdentity });
return findKiroConnectionByIdentity(connections, { profileArn });
}
// ── Save to OmniRoute DB ──────────────────────────────────────────────────────

View File

@@ -1267,7 +1267,6 @@
"agentBridgeSubtitle": "Interceptar tráfego de agentes IDE",
"trafficInspector": "Inspector de Tráfego",
"trafficInspectorSubtitle": "Monitorar chamadas LLM + debugar tráfego HTTPS",
"trafficInspectorPurpose": "Veja exatamente o que sua aplicação envia e recebe dos provedores de IA. Funciona com qualquer cliente compatível com OpenAI.",
"cliCode": "CLI Code's",
"cliCodeSubtitle": "Ferramentas de código que apontam para o OmniRoute",
"cliAgents": "CLI Agents",
@@ -1869,16 +1868,7 @@
"directDownloadHint": "Ou baixe o formato do instalador respectivo diretamente:",
"releaseNotes": "Notas de Lançamento",
"readMore": "Leia Mais",
"noAuthLabel": "Sem Autenticação",
"readinessEyebrow": "Prepare-se para rotear",
"readinessTitle": "Envie sua primeira requisição",
"readinessSubtitle": "Quatro pequenos passos. O OmniRoute verifica a prontidão conforme você avança.",
"readinessStep1": "Conecte um provedor",
"readinessStep2": "Configure a autenticação do endpoint",
"readinessStep3": "Copie seu endpoint",
"readinessStep4": "Envie uma requisição de teste",
"readinessContinue": "Continuar configuração",
"readinessDismiss": "Dispensar por agora"
"noAuthLabel": "Sem Autenticação"
},
"analytics": {
"title": "Análises",
@@ -6710,18 +6700,6 @@
"sidebarVisibility": "Hide sidebar items",
"sidebarVisibilityDesc": "Hide any sidebar navigation entry to reduce visual clutter.",
"sidebarVisibilityHint": "Any sidebar section is hidden automatically when a...",
"presetAll": "Tudo",
"presetAllDesc": "Mostrar tudo",
"presetEssentials": "Essenciais",
"presetEssentialsDesc": "Caminho para iniciantes - Ferramentas avançadas continuam pesquisáveis",
"presetMinimal": "Mínimo",
"presetMinimalDesc": "Apenas páginas principais",
"presetDeveloper": "Desenvolvedor",
"presetDeveloperDesc": "Ferramentas de dev & proxy",
"presetAdmin": "Admin",
"presetAdminDesc": "Monitoramento & auditoria",
"settingsSidebarTitle": "Personalização da Barra Lateral",
"settingsSidebarDesc": "Escolha quais itens da barra lateral exibir. Essenciais mantém as ferramentas avançadas pesquisáveis.",
"hideHealthLogs": "Ocultar Logs de Health Check",
"hideHealthLogsDesc": "Quando ATIVADO, suprime mensagens [HealthCheck] no console do servidor",
"themeAccent": "Cor do tema",

View File

@@ -1267,7 +1267,6 @@
"agentBridgeSubtitle": "Chặn lưu lượng agent IDE",
"trafficInspector": "Traffic Inspector",
"trafficInspectorSubtitle": "Giám sát lệnh gọi LLM + gỡ lỗi mọi lưu lượng HTTPS",
"trafficInspectorPurpose": "Xem chính xác những gì ứng dụng của bạn gửi đến và nhận từ các nhà cung cấp AI. Hoạt động với bất kỳ ứng dụng khách nào tương thích với OpenAI.",
"cliCode": "CLI Code",
"cliCodeSubtitle": "Các công cụ lập trình trỏ đến OmniRoute",
"cliAgents": "CLI Agents",
@@ -1869,16 +1868,7 @@
"directDownloadHint": "Hoặc tải trực tiếp định dạng trình cài đặt phù hợp:",
"releaseNotes": "Ghi chú phát hành",
"readMore": "Đọc thêm",
"noAuthLabel": "Không xác thực",
"readinessEyebrow": "Chuẩn bị định tuyến",
"readinessTitle": "Gửi yêu cầu đầu tiên của bạn",
"readinessSubtitle": "Bốn bước nhỏ. OmniRoute kiểm tra mức độ sẵn sàng khi bạn thực hiện.",
"readinessStep1": "Kết nối một nhà cung cấp",
"readinessStep2": "Định cấu hình xác thực endpoint",
"readinessStep3": "Sao chép endpoint của bạn",
"readinessStep4": "Gửi một yêu cầu thử nghiệm",
"readinessContinue": "Tiếp tục thiết lập",
"readinessDismiss": "Bỏ qua lúc này"
"noAuthLabel": "Không xác thực"
},
"analytics": {
"title": "Phân tích",
@@ -6710,18 +6700,6 @@
"sidebarVisibility": "Ẩn các mục trên thanh bên",
"sidebarVisibilityDesc": "Ẩn bất kỳ mục điều hướng nào trên thanh bên để giảm bớt sự lộn xộn về mặt trực quan mà không vô hiệu hóa bất kỳ tính năng nào",
"sidebarVisibilityHint": "Bất kỳ phần nào trên thanh bên sẽ tự động bị ẩn khi tất cả các mục bên trong nó đều bị ẩn",
"presetAll": "Tất cả",
"presetAllDesc": "Hiển thị mọi thứ",
"presetEssentials": "Thiết yếu",
"presetEssentialsDesc": "Lộ trình cho người mới bắt đầu - Công cụ nâng cao vẫn có thể tìm kiếm",
"presetMinimal": "Tối giản",
"presetMinimalDesc": "Chỉ các trang cốt lõi",
"presetDeveloper": "Nhà phát triển",
"presetDeveloperDesc": "Công cụ dev & proxy",
"presetAdmin": "Quản trị",
"presetAdminDesc": "Giám sát & kiểm toán",
"settingsSidebarTitle": "Tùy chỉnh thanh bên",
"settingsSidebarDesc": "Chọn các mục trên thanh bên sẽ hiển thị. Thiết yếu giữ cho các công cụ nâng cao vẫn có thể tìm kiếm.",
"hideHealthLogs": "Ẩn nhật ký kiểm tra sức khỏe",
"hideHealthLogsDesc": "Khi BẬT, sẽ chặn các thông báo [HealthCheck] trong bảng điều khiển máy chủ",
"themeAccent": "Màu chủ đề",

View File

@@ -499,25 +499,6 @@ 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

@@ -2,15 +2,9 @@ import test from "node:test";
import assert from "node:assert/strict";
// Repro for #6571 — REST-fallback path of `omniroute compression` (hit only when
// the MCP surface is not mounted, i.e. mcpCall()'s 404/501 branch) uses the
// /api/mcp/tools/call is not mounted, i.e. mcpCall()'s 404/501 branch) uses the
// nonexistent `engine` field instead of the canonical `defaultMode` field, and
// the table renderer prints "[object Object]" for nested object cells.
//
// #10960 moved the MCP transport from the never-mounted `/api/mcp/tools/call`
// to the real Streamable HTTP endpoint `/api/mcp/stream` (mcpClient.mjs ->
// callMcpEndpoint()). The REST-fallback trigger in these mocks must match
// that endpoint, not the retired one, or mcpCallTool() throws on an
// unmocked fetch instead of exercising the fallback path this test targets.
type MockResponse = Pick<Response, "ok" | "status" | "headers" | "json" | "text">;
@@ -51,7 +45,7 @@ test("restCompressionStatus (via runCompressionStatus REST fallback) should surf
const origFetch = globalThis.fetch;
globalThis.fetch = (async (url: string | URL | Request) => {
const u = String(url);
if (u.includes("/api/mcp/stream")) return makeResp({ error: "not mounted" }, 404);
if (u.includes("/api/mcp/tools/call")) return makeResp({ error: "not mounted" }, 404);
if (u.includes("/api/settings/compression")) {
// Canonical server payload — NOTE: field is `defaultMode`, there is no `engine` key.
// src/lib/db/compression.ts COMPRESSION_MODES / GET route just returns getCompressionSettings().
@@ -91,7 +85,7 @@ test("restSetEngine (via runCompressionEngineSet REST fallback) should PUT `defa
const putBodies: Record<string, unknown>[] = [];
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
const u = String(url);
if (u.includes("/api/mcp/stream")) return makeResp({ error: "not mounted" }, 404);
if (u.includes("/api/mcp/tools/call")) return makeResp({ error: "not mounted" }, 404);
if (u.includes("/api/settings/compression") && init?.method === "PUT") {
const body = init?.body ? JSON.parse(String(init.body)) : {};
putBodies.push(body);

View File

@@ -113,18 +113,12 @@ test("derived name is never empty or null", () => {
const FAKE_PROFILE_ARN = "arn:aws:iam::123456789012:user/sso-user";
const FAKE_CLIENT_ID = "client-abc";
const fakeConnectionWithArn = {
id: "conn-abc",
provider: "kiro",
authType: "oauth",
email: null,
providerSpecificData: {
profileArn: FAKE_PROFILE_ARN,
region: "us-east-1",
clientId: FAKE_CLIENT_ID,
},
providerSpecificData: { profileArn: FAKE_PROFILE_ARN, region: "us-east-1" },
};
const fakeConnectionNoArn = {
@@ -135,29 +129,13 @@ const fakeConnectionNoArn = {
providerSpecificData: { region: "us-east-1" },
};
test("findKiroConnectionByProfileArn returns the matching connection when an account identifier agrees", async () => {
// #10815 — matching on profileArn alone is unsafe (distinct Builder ID
// accounts can share a profile ARN), so the caller must also supply an
// account-level identifier (email or clientId) that does not contradict
// the stored connection, exactly like saveAndRespond()'s real call sites do.
const result = await findKiroConnectionByProfileArn(
[fakeConnectionWithArn, fakeConnectionNoArn],
FAKE_PROFILE_ARN,
{ clientId: FAKE_CLIENT_ID }
);
assert.deepEqual(result, fakeConnectionWithArn);
});
test("findKiroConnectionByProfileArn returns null for a profileArn-only match with no account identifier (#10815)", async () => {
// Guards the #10815 fix: two different Builder ID accounts (Google/GitHub
// social login) can share the same CodeWhisperer profile ARN, so trusting
// an ARN match without any account identifier would let a second social
// login silently overwrite the first connection.
test("findKiroConnectionByProfileArn returns the matching connection", async () => {
// The function should scan existing kiro connections and match by profileArn.
const result = await findKiroConnectionByProfileArn(
[fakeConnectionWithArn, fakeConnectionNoArn],
FAKE_PROFILE_ARN
);
assert.equal(result, null);
assert.deepEqual(result, fakeConnectionWithArn);
});
test("findKiroConnectionByProfileArn returns null when no match exists", async () => {

View File

@@ -130,18 +130,13 @@ test("a later, lower accepted-list does ratchet the cap down", () => {
assert.equal((getLearnedReasoningEffort("acme", "model-x") as unknown as Set<string>).size, 2);
});
// #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 () => {
test("clampToLearned medium→low when accepted is low,high,max", async () => {
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
assert.equal(clampToLearned("medium", new Set(["low", "high", "max"])), "high");
assert.equal(clampToLearned("medium", new Set(["low", "high", "max"])), "low");
});
// #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 () => {
test("clampToLearned xhigh→high when accepted is low,high,max", async () => {
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
assert.equal(clampToLearned("xhigh", new Set(["low", "high", "max"])), "max");
assert.equal(clampToLearned("xhigh", new Set(["low", "high", "max"])), "high");
});
test("clampToLearned ultra→max when accepted is low,high,max", async () => {
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
@@ -159,25 +154,17 @@ 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);
});
// #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 () => {
test("clampToLearned returns null when effort < min (no upgrade)", async () => {
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
assert.equal(clampToLearned("low", new Set(["high", "max"])), "high");
assert.equal(clampToLearned("low", new Set(["high", "max"])), null);
});
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);
});
// #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 () => {
test("clampToLearned returns null when effort is none but accepted is low,high,max", async () => {
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
assert.equal(clampToLearned("none", new Set(["low", "high", "max"])), "low");
assert.equal(clampToLearned("none", new Set(["low", "high", "max"])), null);
});
test("recordLearned stores Set and getLearned returns Set", () => {
const s = recordLearnedReasoningEffort("acme", "m1", ["low", "high", "max"]);

View File

@@ -111,13 +111,7 @@ describe("injectMemory system-must-be-first (#6135)", () => {
it("regression: a NON-flagged provider keeps the existing cache-safe placement", () => {
const req = multiTurn();
// #11290/#11303 added a Claude-family-specific reroute to injectSystemFirst()
// for the mid-array splice (a system message right after a plain-text
// assistant turn is rejected by Claude Opus 5), so "anthropic" no longer
// exercises the plain cache-safe splice path this test targets. Use a
// provider outside both the strict-system-first set AND the Claude family
// to keep testing the original (still-current) cache-safe behavior.
const out = injectMemory(req, [mem("dark mode")], "openai", { cacheSafe: true });
const out = injectMemory(req, [mem("dark mode")], "anthropic", { cacheSafe: true });
// Existing behavior: memory inserted just before the last user message (index 3).
assert.equal(out.messages[3].role, "system");
assert.ok(out.messages[3].content.includes("Memory context"));

View File

@@ -83,25 +83,7 @@ test.after(async () => {
});
test("successful GLM quota refresh clears transient rate-limit state", async () => {
// 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 connection = await createGlmConnectionWithTransientCooldown();
const connectionId = (connection as { id: string }).id;
await withMockedFetch((() => glmQuotaResponse()) as typeof fetch, async () => {
@@ -119,39 +101,6 @@ 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",
@@ -385,52 +334,6 @@ 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 (nearest-tier: medium -> high, #11295)", async () => {
test("400 please use low, high, or max clamps and retries once", async () => {
const executor = new SimpleExecutor();
const originalFetch = globalThis.fetch;
const capturedBodies: Record<string, unknown>[] = [];
@@ -140,10 +140,7 @@ test("400 please use low, high, or max clamps and retries once (nearest-tier: me
});
assert.equal(capturedBodies.length, 2);
assert.equal(capturedBodies[0].reasoning_effort, "medium");
// #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");
assert.equal(capturedBodies[1].reasoning_effort, "low");
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"));
@@ -193,7 +190,7 @@ test("400 please use low, medium with ultra retries to medium", async () => {
}
});
test("sub-floor clamp now retries: learned {high,max} with low request clamps up to high (#11295)", async () => {
test("no-op clamp does not retry: learned {high,max} with low request stays single-fetch", async () => {
const executor = new SimpleExecutor();
const originalFetch = globalThis.fetch;
const capturedBodies: Record<string, unknown>[] = [];
@@ -217,20 +214,17 @@ test("sub-floor clamp now retries: learned {high,max} with low request clamps up
};
try {
// #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.
// low is below the learned minimum {high,max}: downgrade-only passthrough,
// sanitizer leaves the body unchanged -> no identical-body retry.
const result = await executor.execute({
model: "x-preview-f-free-3",
body: { reasoning_effort: "low" },
stream: false,
credentials: {},
});
assert.equal(capturedBodies.length, 2);
assert.equal(capturedBodies.length, 1);
assert.equal(capturedBodies[0].reasoning_effort, "low");
assert.equal(capturedBodies[1].reasoning_effort, "high");
assert.equal(result.response.status, 200);
assert.equal(result.response.status, 400);
} finally {
globalThis.fetch = originalFetch;
}

View File

@@ -1,79 +0,0 @@
// #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,25 +90,23 @@ test("deepseek's non-ordinal max<->xhigh translation is untouched by the learned
assert.equal(result.reasoning_effort, "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)", () => {
test("proactive clamp: medium→low for learned {low,high,max}", () => {
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, "high");
assert.equal(out.reasoning_effort, "low");
});
test("proactive clamp: xhigh→max for learned {low,high,max} (nearest-tier, #11295)", () => {
test("proactive clamp: xhigh→high for learned {low,high,max}", () => {
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, "max");
assert.equal(out.reasoning_effort, "high");
});
test("proactive clamp: ultra→max for learned {low,high,max}", () => {
recordLearnedReasoningEffort("opencode-zen-direct", "x-preview-f-free-3", ["low", "high", "max"]);
@@ -137,16 +135,14 @@ test("proactive clamp: high→medium for learned {low,medium}", () => {
) as { reasoning_effort: string };
assert.equal(out.reasoning_effort, "medium");
});
// #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)", () => {
test("no upgrade: low stays low for learned {high,max}", () => {
recordLearnedReasoningEffort("acme", "m3", ["high", "max"]);
const out = sanitizeReasoningEffortForProvider(
{ reasoning_effort: "low", model: "m3" },
"acme",
"m3"
) as { reasoning_effort: string };
assert.equal(out.reasoning_effort, "high");
assert.equal(out.reasoning_effort, "low");
});
test("custom model ultra→medium for learned {low,medium}", () => {
recordLearnedReasoningEffort("openai-compatible-chat-eaff6869", "qwen3-coder-30b-a3b-instruct-2", ["low", "medium"]);