fix(models): preserve eye-hidden models across auto-sync (#3782) (#3816)

* fix(models): preserve eye-hidden models across auto-sync (#3782)

* chore(quality): re-baseline file-size for models.ts growth (#3782)
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-14 01:38:38 -03:00
committed by GitHub
parent e952ae1406
commit ef324cd00e
7 changed files with 224 additions and 19 deletions

View File

@@ -400,10 +400,13 @@ export async function DELETE(request) {
const removedCustom = await removeCustomModel(provider, modelId);
const removedSynced = await removeSyncedAvailableModel(provider, modelId);
if (removedSynced) {
// #3199: mark the deleted synced model hidden so a later auto-fetch
// re-import (replaceSyncedAvailableModelsForConnection) does not re-add it
// — otherwise the model reappears and the delete looks like it did nothing.
mergeModelCompatOverride(provider, modelId, { isHidden: true });
// #3199 + #3782: mark the deleted synced model with the DISTINCT `isDeleted`
// marker so a later auto-fetch re-import does not re-add it. We also keep
// `isHidden:true` so existing UI/visibility behavior is unchanged. The sync
// filter keys on `isDeleted` (not `isHidden`), which is what lets an
// eye/visibility-hidden model (`isHidden` only) survive a re-sync while a
// deleted one stays dropped.
mergeModelCompatOverride(provider, modelId, { isDeleted: true, isHidden: true });
}
const removed = removedCustom || removedSynced;
const removedAliases = await deleteManagedAvailableModelAliases(provider, [modelId]);

View File

@@ -111,6 +111,16 @@ export type ModelCompatOverride = {
compatByProtocol?: CompatByProtocolMap;
upstreamHeaders?: Record<string, string>;
isHidden?: boolean;
/**
* #3782 — distinct "deleted" marker, separate from {@link isHidden}.
*
* `isHidden` is set by the EYE/visibility toggle and must be PRESERVED across a
* re-sync (the model stays listed-but-hidden). `isDeleted` is set by the trash/
* DELETE route and means "drop this id on every re-import" (#3199). Keeping the
* two flags distinct is what lets {@link replaceSyncedAvailableModelsForConnection}
* preserve eye-hidden models while still dropping deleted ones.
*/
isDeleted?: boolean;
};
function readCompatList(providerId: string): ModelCompatOverride[] {
@@ -156,6 +166,8 @@ export type ModelCompatPatch = {
/** Replace top-level extra headers for override-only rows; omit to leave unchanged. */
upstreamHeaders?: Record<string, string> | null;
isHidden?: boolean | null;
/** #3782 — distinct delete marker; set by the DELETE route, never by the eye toggle. */
isDeleted?: boolean | null;
};
function compatByProtocolHasEntries(map: CompatByProtocolMap | undefined): boolean {
@@ -210,11 +222,20 @@ export function mergeModelCompatOverride(
next.isHidden = Boolean(patch.isHidden);
}
}
if ("isDeleted" in patch) {
if (patch.isDeleted === null || patch.isDeleted === false) {
delete next.isDeleted;
} else {
next.isDeleted = Boolean(patch.isDeleted);
}
}
const hasHiddenFlag = Object.prototype.hasOwnProperty.call(next, "isHidden");
const hasDeletedFlag = Object.prototype.hasOwnProperty.call(next, "isDeleted");
if (
next.normalizeToolCallId ||
hasPreserveFlag ||
hasHiddenFlag ||
hasDeletedFlag ||
compatByProtocolHasEntries(next.compatByProtocol) ||
hasTopUpstream
) {
@@ -714,10 +735,15 @@ export async function replaceSyncedAvailableModelsForConnection(
): Promise<SyncedAvailableModel[]> {
const db = getDbInstance();
const key = `${providerId}:${connectionId}`;
// #3199: drop ids the operator has deleted/hidden so a re-fetch does not
// re-import a model that was explicitly removed.
// #3199: drop ids the operator DELETED (trash) so a re-fetch does not re-import
// a model that was explicitly removed.
// #3782: key ONLY on the distinct `isDeleted` marker — NOT on `isHidden`.
// Eye/visibility-hidden models (`isHidden:true`, no `isDeleted`) must stay in
// the synced store so they remain listed-but-hidden across re-syncs instead of
// churning back on through the managed-alias path ("Auto Sync Enabling all
// Models"). See getModelIsDeleted for the legacy-row caveat.
const normalizedModels = normalizeSyncedAvailableModels(models).filter(
(m) => !getModelIsHidden(providerId, m.id)
(m) => !getModelIsDeleted(providerId, m.id)
);
if (normalizedModels.length === 0) {
db.prepare("DELETE FROM key_value WHERE namespace = 'syncedAvailableModels' AND key = ?").run(
@@ -1034,6 +1060,28 @@ export function getModelIsHidden(providerId: string, modelId: string): boolean {
return Boolean(co?.isHidden);
}
/**
* #3782 — Check if a model was DELETED (trash) rather than merely eye-hidden.
*
* Only the DELETE route sets `isDeleted`. The sync re-import filter keys on this
* (not on `isHidden`) so eye-hidden models survive a re-sync while deleted ones
* stay dropped.
*
* Legacy caveat: rows written by the DELETE route BEFORE this change carry only
* `isHidden:true` (no `isDeleted`). Treating bare legacy `isHidden:true` as
* deleted here would resurrect the #3782 bug for eye-hidden models; treating it
* as "kept" would resurrect previously-deleted models. Resurrecting a deleted
* model is the less-surprising, recoverable outcome (the operator can re-hide or
* re-delete it), whereas silently dropping an eye-hidden model is the reported
* regression — so we deliberately key ONLY on the explicit `isDeleted` flag and
* accept that a handful of pre-existing deleted rows may reappear once after the
* upgrade. Going forward both paths write the correct distinct markers.
*/
export function getModelIsDeleted(providerId: string, modelId: string): boolean {
const co = readCompatList(providerId).find((e) => e.id === modelId);
return Boolean(co?.isDeleted);
}
/**
* Persist the hidden flag for a model. Stores the override on the custom-model
* row when one exists, otherwise on the compat-override list. Setting

View File

@@ -1,5 +1,6 @@
import {
getCustomModels,
getModelIsHidden,
getSyncedAvailableModelsForConnection,
mergeModelCompatOverride,
replaceCustomModels,
@@ -271,7 +272,10 @@ export async function importManagedModels({
current = (ANTIGRAVITY_MODEL_ALIASES as any)[current];
continue;
}
if (ANTIGRAVITY_REVERSE_MODEL_ALIASES && (ANTIGRAVITY_REVERSE_MODEL_ALIASES as any)[current]) {
if (
ANTIGRAVITY_REVERSE_MODEL_ALIASES &&
(ANTIGRAVITY_REVERSE_MODEL_ALIASES as any)[current]
) {
current = (ANTIGRAVITY_REVERSE_MODEL_ALIASES as any)[current];
continue;
}
@@ -306,13 +310,17 @@ export async function importManagedModels({
await setMitmAliasAll("antigravity", mappings);
}
let syncedAliases = 0;
if (usesManagedAvailableModels(providerId) && (mode === "merge" || discoveredModels.length > 0)) {
const aliasModelIds = mode === "sync" ? syncedAvailableModels : discoveredModels;
// #3782: eye-hidden models now survive in `syncedAvailableModels` (so they stay
// listed-but-hidden), but they must NOT be re-assigned a routable managed alias
// — otherwise auto-sync silently re-enables routing for a model the operator hid.
// Exclude `isHidden` ids from the alias assignment. `pruneMissing` (sync mode)
// then drops any stale alias an eye-hidden model previously held.
const aliasSync = await syncManagedAvailableModelAliases(
providerId,
aliasModelIds.map((model) => model.id),
aliasModelIds.map((model) => model.id).filter((id) => !getModelIsHidden(providerId, id)),
{ pruneMissing: mode === "sync" }
);
syncedAliases = aliasSync.assignedAliases.length;