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
57 changes: 57 additions & 0 deletions apps/desktop/test/model-config-import.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,60 @@ test("scanModelConfigs keeps same-endpoint CC Switch profiles with different key
assert.equal(drafts.find((d) => d.externalId === "claude:packy")?.secretValue, "sk-cc");
assert.equal(drafts.find((d) => d.externalId === "claude:other")?.secretValue, "sk-other");
});

// Regression: issue #588. Reproduces the reporter's scenario — a stale
// cc-switch v18 pi snapshot missing one model, plus a newer ~/.pi/agent/models.json
// that lists all ten. The old code returned the snapshot under the "cc-switch"
// source and silently dropped the tenth model. The fix routes pi rows through
// the "pi" scanner so the authoritative file wins.
test("scanModelConfigs keeps the pi native config authoritative when a cc-switch snapshot exists", async () => {
const home = await mkdtemp(join(tmpdir(), "pi-cc-switch-pi-"));
await mkdir(join(home, ".pi", "agent"), { recursive: true });
await mkdir(join(home, ".cc-switch"), { recursive: true });

// Newer, authoritative pi config with ten models.
await writeFile(
join(home, ".pi", "agent", "models.json"),
JSON.stringify({
providers: {
"opencode-go": {
name: "opencode-go",
baseUrl: "https://api.oj.ink/v1",
api: "openai-completions",
apiKey: "sk-pi",
models: Array.from({ length: 10 }, (_, i) => ({ id: `m${i + 1}` })),
},
},
}),
);

// Stale cc-switch snapshot: same endpoint + key, only nine models, and a
// renamed label that used to leak into the UI.
const { DatabaseSync } = await import("node:sqlite");
const db = new DatabaseSync(join(home, ".cc-switch", "cc-switch.db"));
db.exec(
"CREATE TABLE providers (id TEXT, app_type TEXT, name TEXT, settings_config TEXT)",
);
db.prepare("INSERT INTO providers VALUES (?, ?, ?, ?)").run(
"opencode-go",
"pi",
"OpenCode Zen Go",
JSON.stringify({
baseUrl: "https://api.oj.ink/v1",
api: "openai-completions",
apiKey: "sk-pi",
models: Array.from({ length: 9 }, (_, i) => ({ id: `m${i + 1}` })),
}),
);
db.close();

const drafts = await scanModelConfigs({ homeDir: home, env: {} });
const piDrafts = drafts.filter((d) => d.source === "pi");
const ccDrafts = drafts.filter((d) => d.source === "cc-switch");
assert.equal(ccDrafts.length, 0, "no pi rows should surface under the cc-switch source");
assert.equal(piDrafts.length, 1);
assert.equal(piDrafts[0].externalId, "opencode-go");
assert.equal(piDrafts[0].name, "opencode-go");
assert.equal(piDrafts[0].modelIds.length, 10);
assert.ok(piDrafts[0].modelIds.includes("m10"));
});
32 changes: 32 additions & 0 deletions packages/shared/src/model-config-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,38 @@ describe("parseCcSwitchProviders", () => {
expect(rows).toHaveLength(1);
expect(parseCcSwitchProviders(rows)[0]?.source).toBe("cc-switch");
});

// Regression: issue #588 — a stale cc-switch snapshot of ~/.pi/agent/models.json
// used to silently outrank the pi source and drop any models added after the
// one-shot sync. Rows with app_type='pi' must be ignored here so the "pi"
// scanner keeps ownership of the authoritative file.
it("skips app_type='pi' rows so the pi native config stays authoritative", () => {
const drafts = parseCcSwitchProviders([
{
id: "opencode-go",
appType: "pi",
name: "OpenCode Zen Go",
settingsConfig: {
baseUrl: "https://api.oj.ink/v1",
api: "openai-completions",
apiKey: "sk-pi",
// Stale snapshot: only nine of ten models — missing `deepseek-flash`.
models: [
{ id: "m1" },
{ id: "m2" },
{ id: "m3" },
{ id: "m4" },
{ id: "m5" },
{ id: "m6" },
{ id: "m7" },
{ id: "m8" },
{ id: "m9" },
],
},
},
]);
expect(drafts).toEqual([]);
});
});

describe("parseJsonDocument / parseTomlSubset", () => {
Expand Down
11 changes: 7 additions & 4 deletions packages/shared/src/model-config-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,10 +538,13 @@ function parseCcSwitchProvider(
);
}
if (appType === "pi") {
return retagCcSwitch(
row,
parsePiModelConfig({ providers: { [row.id]: row.settingsConfig } }, env),
);
// Pi providers have an authoritative native config (`~/.pi/agent/models.json`).
// CC Switch only stores a one-shot v18 migration snapshot of that file; models
// added afterwards never make it back in, and our dedupe key ignores `models`
// coverage. Trusting the snapshot silently drops the newer entries and lands
// pi providers under the "CC Switch" group with a renamed label. Let the `pi`
// scanner own these rows so the source of truth wins. See issue #588.
return [];
}
if (appType === "codex" || appType === "grokbuild") {
return parseCcSwitchTomlApp(row, env, appType === "codex" ? "responses" : "chat_completions");
Expand Down
Loading