Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@
"open-sse/mcp-server/server.ts": 1555,
"open-sse/mcp-server/tools/advancedTools.ts": 1120,
"_rebaseline_2026_06_27_5193_antigravity_basered": "Base-red (pre-existing release drift, fast-gate PR->release skips check:file-size): accountFallback.ts 1773->1777 and src/app/api/providers/[id]/test/route.ts 924->940 were already over their frozen caps on release/v3.8.39 independent of any antigravity change. Owner chose to rebaseline (keep the documented issue-reference comments #1846/#1449/#347 etc.) rather than accept the contributor comment-stripping in #5200/#5198. Reverted #5200 to restore the comments; bumped these two frozen caps to the actual base sizes. No logic change.",
"open-sse/services/accountFallback.ts": 1864,
"open-sse/services/accountFallback.ts": 1892,
"open-sse/services/adobeFireflyClient.ts": 1958,
"open-sse/services/batchProcessor.ts": 915,
"open-sse/services/browserBackedChat.ts": 850,
Expand Down Expand Up @@ -317,7 +317,8 @@
"_rebaseline_2026_07_21_8034_compression_exclusions_sidebar": "#8034 (compression exclusions dashboard tab) own growth: sections.ts 796->806 (+10, one new COMPRESSION_CONTEXT_GROUP sidebar item linking /dashboard/compression/exclusions). The file was already 796/800 before this PR (organic growth from prior sidebar entries), so a single new nav item pushed it 6 lines over cap. Freezing at 806 (cannot grow further); the sidebar item array is data, not extractable logic.",
"src/shared/constants/sidebarVisibility/sections.ts": 806,
"_rebaseline_2026_07_22_8056_headroom_minrows": "#8056 (@RaviTharuma, persist Headroom minRows) own growth: src/lib/db/compression.ts 850->866 (+16 HeadroomConfig+DEFAULT_HEADROOM_CONFIG+normalize/store in get/updateCompressionSettings) and open-sse/services/compression/strategySelector.ts 1054->1060 (+6 merge settings.headroom into stacked stepConfig). Cohesive settings-persistence + stacked-merge wiring at existing chokepoints, frozen at new size.",
"_rebaseline_2026_07_22_8081_reasoning_placeholder_guard": "#8081 (@Dingding-leo) own growth: openai-responses.ts 1125->1137 (+12) restructuring the reasoning-placeholder guard so it skips only the empty content block and still emits finish_reason/tool_calls in the same chunk. Cohesive translator wiring; frozen at new size."
"_rebaseline_2026_07_22_8081_reasoning_placeholder_guard": "#8081 (@Dingding-leo) own growth: openai-responses.ts 1125->1137 (+12) restructuring the reasoning-placeholder guard so it skips only the empty content block and still emits finish_reason/tool_calls in the same chunk. Cohesive translator wiring; frozen at new size.",
"_rebaseline_2026_07_22_8050_model_lockout_exact_family": "#8050 (@AndrianBalanescu) own growth: accountFallback.ts 1864->1892 (+28) — exact-vs-family model-lockout scoping (getModelLockKey/isModelLocked/clearModelLock/getModelLockoutInfo) so an Antigravity 404 for one bare model no longer hijacks the whole family cooldown. Cohesive lockout logic; frozen at new size."
},
"testCap": 800,
"testFrozen": {
Expand Down
82 changes: 58 additions & 24 deletions open-sse/services/accountFallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,12 +410,20 @@ function getCanonicalLockProvider(provider: string): string {
return canonical;
}

function getModelLockKey(provider: string, connectionId: string, model: string) {
function getModelLockKey(
provider: string,
connectionId: string,
model: string,
reason?: string | null,
status?: number | null
) {
const canonicalProvider = getCanonicalLockProvider(provider);
const lockModel =
canonicalProvider === "codex"
? getCodexModelScope(model)
: getQuotaScopedModelForProvider(canonicalProvider, model) || model;
reason === "not_found" || status === 404
? model
: canonicalProvider === "codex"
? getCodexModelScope(model)
: getQuotaScopedModelForProvider(canonicalProvider, model) || model;
return `${canonicalProvider}:${connectionId}:${lockModel}`;
}

Expand Down Expand Up @@ -509,7 +517,7 @@ export function lockModel(
): void {
if (!model) return; // No model → skip model-level locking
ensureCleanupTimer();
const key = getModelLockKey(provider, connectionId, model);
const key = getModelLockKey(provider, connectionId, model, reason);
cleanupModelLockKey(key);
const newUntil = Date.now() + cooldownMs;
// Preserve the longer cooldown if an existing lock has more time remaining.
Expand Down Expand Up @@ -568,7 +576,7 @@ export function recordModelLockoutFailure(
options: { exactCooldownMs?: number | null; maxCooldownMs?: number } = {}
) {
ensureCleanupTimer();
const key = getModelLockKey(provider, connectionId, model);
const key = getModelLockKey(provider, connectionId, model, reason, status);
const now = Date.now();
cleanupModelLockKey(key, now);

Expand Down Expand Up @@ -634,10 +642,16 @@ export function clearModelLock(
model: string | null | undefined
): boolean {
if (!model) return false;
const key = getModelLockKey(provider, connectionId, model);
const hadLock = modelLockouts.delete(key);
const hadFailureState = modelFailureState.delete(key);
return hadLock || hadFailureState;
const familyKey = getModelLockKey(provider, connectionId, model);
const exactKey = `${getCanonicalLockProvider(provider)}:${connectionId}:${model}`;

const hadLock1 = modelLockouts.delete(familyKey);
const hadFailure1 = modelFailureState.delete(familyKey);

const hadLock2 = modelLockouts.delete(exactKey);
const hadFailure2 = modelFailureState.delete(exactKey);

return hadLock1 || hadFailure1 || hadLock2 || hadFailure2;
}

/**
Expand Down Expand Up @@ -753,10 +767,14 @@ export function isModelLocked(
model: string | null | undefined
): boolean {
if (!model) return false;
const key = getModelLockKey(provider, connectionId, model);
cleanupModelLockKey(key);
const entry = modelLockouts.get(key);
return Boolean(entry);

const exactKey = `${getCanonicalLockProvider(provider)}:${connectionId}:${model}`;
cleanupModelLockKey(exactKey);
if (modelLockouts.has(exactKey)) return true;

const familyKey = getModelLockKey(provider, connectionId, model);
cleanupModelLockKey(familyKey);
return modelLockouts.has(familyKey);
}

/**
Expand All @@ -768,16 +786,32 @@ export function getModelLockoutInfo(
model: string | null | undefined
) {
if (!model) return null;
const key = getModelLockKey(provider, connectionId, model);
cleanupModelLockKey(key);
const entry = modelLockouts.get(key);
if (!entry) return null;
return {
reason: entry.reason,
remainingMs: entry.until - Date.now(),
lockedAt: new Date(entry.lockedAt).toISOString(),
failureCount: entry.failureCount,
};

const exactKey = `${getCanonicalLockProvider(provider)}:${connectionId}:${model}`;
cleanupModelLockKey(exactKey);
const exactEntry = modelLockouts.get(exactKey);
if (exactEntry) {
return {
reason: exactEntry.reason,
remainingMs: exactEntry.until - Date.now(),
lockedAt: new Date(exactEntry.lockedAt).toISOString(),
failureCount: exactEntry.failureCount,
};
}

const familyKey = getModelLockKey(provider, connectionId, model);
cleanupModelLockKey(familyKey);
const familyEntry = modelLockouts.get(familyKey);
if (familyEntry) {
return {
reason: familyEntry.reason,
remainingMs: familyEntry.until - Date.now(),
lockedAt: new Date(familyEntry.lockedAt).toISOString(),
failureCount: familyEntry.failureCount,
};
}

return null;
}

export type ModelLockoutInfo = {
Expand Down
28 changes: 24 additions & 4 deletions open-sse/services/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ ALIAS_TO_PROVIDER_ID["xiaomi"] = "xiaomi-mimo";
// prefix is "llamacpp". Register it so parseModel("llamacpp/<model>") resolves
// provider = "llama-cpp" instead of the identity fallback ("llamacpp").
ALIAS_TO_PROVIDER_ID["llamacpp"] = "llama-cpp";
// agy/ is the short alias for antigravity provider.
ALIAS_TO_PROVIDER_ID["agy"] = "antigravity";

// Provider-scoped legacy model aliases. Used to normalize provider/model inputs
// and keep backward compatibility when upstream IDs change.
Expand Down Expand Up @@ -599,10 +601,28 @@ async function resolveModelByProviderInference(modelId: string, extendedContext:
};
}

if (candidatesToUse.length === 1) {
const provider = candidatesToUse[0];
const canonicalModel = resolveInferredProviderModel(provider, modelId);
return { provider, model: canonicalModel, extendedContext };
// Canonicalize candidates (deduplicate alias providers pointing to the same provider ID)
const canonicalCandidates = Array.from(
new Set(candidatesToUse.map((p) => resolveProviderAlias(p)).filter((p): p is string => p !== null))
);

// Filter candidates by active connections configured in the database
let activeCandidates: string[] = [];
if (activeProviders && activeProviders.size > 0) {
activeCandidates = canonicalCandidates.filter((p) => activeProviders.has(p));
}

// Auto-pick:
// 1. If active providers match, pick from active candidates (first active provider).
// 2. If no active providers filter applied, but canonical candidates deduplicate to 1 provider, pick it.
const effectiveCandidates = activeCandidates.length > 0 ? activeCandidates : canonicalCandidates;

if (effectiveCandidates.length >= 1) {
if (activeCandidates.length > 0 || effectiveCandidates.length === 1) {
const provider = effectiveCandidates[0];
const canonicalModel = resolveInferredProviderModel(provider, modelId);
return { provider, model: canonicalModel, extendedContext };
}
}

if (candidatesToUse.length > 1) {
Expand Down
21 changes: 21 additions & 0 deletions tests/unit/bare-model-autopick.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import test from "node:test";
import assert from "node:assert/strict";

import { getModelInfoCore } from "../../open-sse/services/model.ts";

test("gpt-oss-120b-medium auto-picks antigravity provider via canonical deduplication", async () => {
const info = await getModelInfoCore("gpt-oss-120b-medium", null);

assert.equal(info.provider, "antigravity");
assert.equal(info.model, "gpt-oss-120b-medium");
assert.equal((info as Record<string, unknown>).errorType, undefined);
});

test("unprefixed model with no active providers falls back to ambiguous_model when multiple distinct providers exist", async () => {
const info = await getModelInfoCore("gpt-oss-120b", null);

assert.equal(info.provider, null);
assert.equal((info as Record<string, unknown>).errorType, "ambiguous_model");
assert.ok(Array.isArray((info as Record<string, unknown>).candidateProviders));
assert.ok((info as Record<string, unknown>).candidateProviders.length > 1);
});
66 changes: 66 additions & 0 deletions tests/unit/repro-antigravity-404-family-cooldown-hijack.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import test from "node:test";
import assert from "node:assert/strict";

import {
recordModelLockoutFailure,
isModelLocked,
clearAllModelLockouts,
} from "../../open-sse/services/accountFallback.ts";

test("Antigravity 404 Model Not Found locks exact model ONLY, not entire family", () => {
clearAllModelLockouts();

// 1. Simulate a 404 error on a non-existent model "gemini-3.6"
recordModelLockoutFailure(
"antigravity",
"test_connection_id",
"gemini-3.6",
"not_found",
404,
5000
);

// 2. Exact non-existent model should block
assert.equal(
isModelLocked("antigravity", "test_connection_id", "gemini-3.6"),
true,
"Requested non-existent model gemini-3.6 should be locked"
);

// 3. Valids in the same family should NOT be blocked
assert.equal(
isModelLocked("antigravity", "test_connection_id", "gemini-3.1-flash-lite"),
false,
"Valid model gemini-3.1-flash-lite should not be locked by a different model's 404 error"
);

assert.equal(
isModelLocked("antigravity", "test_connection_id", "gemini-1.5-flash"),
false,
"Valid model gemini-1.5-flash should not be locked by a different model's 404 error"
);
});

test("Antigravity 429 Rate Limit locks whole family", () => {
clearAllModelLockouts();

// 1. Simulate a 429 rate limit on "gemini-3.1-flash-lite"
recordModelLockoutFailure(
"antigravity",
"test_connection_id",
"gemini-3.1-flash-lite",
"rate_limited",
429,
60000
);

// 2. The rate-limited model should be locked
assert.equal(isModelLocked("antigravity", "test_connection_id", "gemini-3.1-flash-lite"), true);

// 3. Sibling models of the same family should also be locked
assert.equal(
isModelLocked("antigravity", "test_connection_id", "gemini-1.5-flash"),
true,
"Sibling model should be locked by the family quota exhaustion"
);
});
Loading