Skip to content

Pr 3211 - #3239

Closed
diegosouzapw wants to merge 2 commits into
release/v3.8.11from
pr-3211
Closed

Pr 3211#3239
diegosouzapw wants to merge 2 commits into
release/v3.8.11from
pr-3211

Conversation

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Summary

  • Describe the user-facing or operational change.

Related Issues

  • Closes #
  • Related to #

Validation

  • npm run lint
  • npm run test:unit
  • npm run test:coverage
  • Coverage is still >= 60% for statements, lines, functions, and branches
  • SonarQube PR analysis is green or any remaining issues are explicitly documented below

Tests Added Or Updated

  • List every changed or added automated test file.
  • If no production code changed, state that here.

Coverage Notes

  • If this PR changes src/, open-sse/, electron/, or bin/, explain which tests cover the change.
  • If coverage moved down in any touched file, explain why and what follow-up task will recover it.

Reviewer Notes

  • Call out any risky areas, migrations, feature flags, or manual validation that reviewers should know about.

diegosouzapw and others added 2 commits June 5, 2026 02:50
…eep it deleted (#3199)

Synced (fetched) models were authoritative with no per-model delete, so
individual llama-cpp models couldn't be removed (or re-appeared on re-fetch).
Add removeSyncedAvailableModel + mark the id hidden so re-import skips it;
the DELETE route now removes from both custom and synced sets.

Co-authored-by: tjengbudi <tjengbudi@users.noreply.github.com>
# Conflicts:
#	src/app/api/provider-models/route.ts
#	src/lib/db/models.ts

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses issue #3199 by ensuring that deleted provider models are marked as hidden and excluded from synced lists, preventing them from reappearing during subsequent auto-fetches. It updates the DELETE and GET API routes, refactors removeSyncedAvailableModel in the database layer to support connection-scoped deletions, and adds comprehensive unit tests. The review feedback highlights two performance and atomicity concerns: an N+1 query issue in the GET route due to calling getModelIsHidden inside a filter loop, and the lack of an explicit transaction when executing multiple database write operations in removeSyncedAvailableModel. Batch-fetching hidden IDs and wrapping database updates in a transaction are recommended to resolve these issues.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 810 to +814
const allSynced = await getSyncedAvailableModels(provider);
if (Array.isArray(allSynced) && allSynced.length > 0) {
providerSyncedModels = allSynced.map((m) => ({
// #3199 — a model the operator deleted is flagged hidden; drop it from the
// authoritative synced list so the deletion sticks across re-fetch even when
// excludeHidden is not requested by the caller.
const visibleSynced = allSynced.filter((m) => !getModelIsHidden(provider, m.id));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Calling getModelIsHidden inside a filter loop triggers two database queries per model (one for custom models and one for compat overrides). For providers with many models, this results in an N+1 query problem that severely degrades performance. Fetching the hidden model IDs in batch once per request avoids this overhead.

      const allSynced = await getSyncedAvailableModels(provider);
      // #3199 — a model the operator deleted is flagged hidden; drop it from the
      // authoritative synced list so the deletion sticks across re-fetch even when
      // excludeHidden is not requested by the caller.
      const { getCustomModels, getModelCompatOverrides } = await import("@/lib/localDb");
      const [customModels, compatOverrides] = await Promise.all([
        getCustomModels(provider),
        getModelCompatOverrides(provider),
      ]);
      const hiddenIds = new Set<string>();
      for (const m of customModels || []) {
        if (m?.isHidden) hiddenIds.add(m.id);
      }
      for (const co of compatOverrides || []) {
        if (co?.isHidden) hiddenIds.add(co.id);
      }
      const visibleSynced = allSynced.filter((m) => !hiddenIds.has(m.id));

Comment on lines +940 to +942
freshSynced = (await getSyncedAvailableModels(provider)).filter(
(m) => !getModelIsHidden(provider, m.id)
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Calling getModelIsHidden inside a filter loop triggers two database queries per model. For providers with many models, this results in an N+1 query problem that severely degrades performance. Fetching the hidden model IDs in batch once per request avoids this overhead.

        const allSynced = await getSyncedAvailableModels(provider);
        const { getCustomModels, getModelCompatOverrides } = await import("@/lib/localDb");
        const [customModels, compatOverrides] = await Promise.all([
          getCustomModels(provider),
          getModelCompatOverrides(provider),
        ]);
        const hiddenIds = new Set<string>();
        for (const m of customModels || []) {
          if (m?.isHidden) hiddenIds.add(m.id);
        }
        for (const co of compatOverrides || []) {
          if (co?.isHidden) hiddenIds.add(co.id);
        }
        freshSynced = allSynced.filter((m) => !hiddenIds.has(m.id));

Comment thread src/lib/db/models.ts
Comment on lines +808 to +831
let changed = false;
for (const { key, value } of rows) {
let models: SyncedAvailableModel[];
try {
models = normalizeSyncedAvailableModels(JSON.parse(value));
} catch {
continue;
}
const filtered = models.filter((m) => m.id !== targetId);
if (filtered.length === models.length) continue;
changed = true;
if (filtered.length === 0) {
db.prepare("DELETE FROM key_value WHERE namespace = 'syncedAvailableModels' AND key = ?").run(
key
);
} else {
db.prepare(
"UPDATE key_value SET value = ? WHERE namespace = 'syncedAvailableModels' AND key = ?"
).run(JSON.stringify(filtered), key);
}
}

if (changed) backupDbFile("pre-write");
return changed;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Executing multiple database write operations inside a loop without an explicit transaction can lead to performance degradation (due to multiple disk syncs) and a lack of atomicity. Wrapping the updates in a transaction ensures they are executed efficiently and atomically.

  let changed = false;
  const runUpdates = db.transaction(() => {
    for (const { key, value } of rows) {
      let models: SyncedAvailableModel[];
      try {
        models = normalizeSyncedAvailableModels(JSON.parse(value));
      } catch {
        continue;
      }
      const filtered = models.filter((m) => m.id !== targetId);
      if (filtered.length === models.length) continue;
      changed = true;
      if (filtered.length === 0) {
        db.prepare("DELETE FROM key_value WHERE namespace = 'syncedAvailableModels' AND key = ?").run(
          key
        );
      } else {
        db.prepare(
          "UPDATE key_value SET value = ? WHERE namespace = 'syncedAvailableModels' AND key = ?"
        ).run(JSON.stringify(filtered), key);
      }
    }
    if (changed) backupDbFile("pre-write");
  });

  runUpdates();
  return changed;

@diegosouzapw

Copy link
Copy Markdown
Collaborator Author

Closing as a duplicate: this branch (pr-3211) carries the same synced-model delete-persistence work as #3236, which is itself already in release/v3.8.11 via commits 6fe7c6b (#3212) and 7b4bda1 (#3204). Superseded. Deleting the branch.

@diegosouzapw
diegosouzapw deleted the pr-3211 branch June 5, 2026 16:13
@kilo-code-bot

kilo-code-bot Bot commented Jun 5, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 0
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
src/lib/db/models.ts 808-828 Missing transaction wrapper in removeSyncedAvailableModel — the removed old implementation wrapped all DB operations in db.transaction() for atomicity, but the new implementation performs multiple UPDATE/DELETE operations without a transaction. This could leave the database in an inconsistent state if an error occurs mid-operation.
Files Reviewed (4 files)
  • src/app/api/provider-models/route.ts - Changes look correct (filtering hidden models, calling removeSyncedAvailableModel)
  • src/app/api/providers/[id]/models/route.ts - Changes look correct (filtering hidden models from synced lists)
  • src/lib/db/models.ts - Critical: Missing transaction in removeSyncedAvailableModel
  • tests/unit/synced-model-delete-3199.test.ts - Test coverage looks comprehensive

Note: The existing inline comments for lines 814, 942, and 831 in the PR were noted but not duplicated.

Fix Link: https://app.kilo.ai/cloud-agent-fork/review/ed01ad7e-d6c3-4132-a909-a11dd12f5200


Reviewed by laguna-m.1-20260312:free · 1,042,027 tokens

Comment thread src/lib/db/models.ts
}
}

let changed = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Missing transaction wrapper — the old implementation wrapped all DB operations in db.transaction() for atomicity, but this implementation performs multiple UPDATE/DELETE operations without a transaction. This could leave the database in an inconsistent state if an error occurs mid-operation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant