fix(coding-agent): handle invalid models json during migration

closes #5418
This commit is contained in:
Mario Zechner
2026-06-09 13:14:24 +02:00
parent e4907b3b09
commit 1906074369
3 changed files with 60 additions and 35 deletions

View File

@@ -8,6 +8,7 @@
### Fixed ### Fixed
- Fixed invalid `models.json` syntax to skip startup config migrations and report the normal file-path-aware models error instead of a raw JSON parse stack trace ([#5418](https://github.com/earendil-works/pi/issues/5418)).
- Fixed GitHub release notes and interactive changelog links to resolve package-relative documentation URLs correctly ([#5516](https://github.com/earendil-works/pi/issues/5516)). - Fixed GitHub release notes and interactive changelog links to resolve package-relative documentation URLs correctly ([#5516](https://github.com/earendil-works/pi/issues/5516)).
## [0.79.0] - 2026-06-08 ## [0.79.0] - 2026-06-08

View File

@@ -144,6 +144,7 @@ function migrateModelsJsonConfigValues(agentDir: string): ConfigValueMigration[]
const modelsPath = join(agentDir, "models.json"); const modelsPath = join(agentDir, "models.json");
if (!existsSync(modelsPath)) return []; if (!existsSync(modelsPath)) return [];
try {
const parsed = JSON.parse(stripJsonComments(readFileSync(modelsPath, "utf-8"))) as unknown; const parsed = JSON.parse(stripJsonComments(readFileSync(modelsPath, "utf-8"))) as unknown;
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return []; if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return [];
const modelsData = parsed as Record<string, unknown>; const modelsData = parsed as Record<string, unknown>;
@@ -171,7 +172,8 @@ function migrateModelsJsonConfigValues(agentDir: string): ConfigValueMigration[]
const modelOverrides = providerRecord.modelOverrides; const modelOverrides = providerRecord.modelOverrides;
if (typeof modelOverrides === "object" && modelOverrides !== null && !Array.isArray(modelOverrides)) { if (typeof modelOverrides === "object" && modelOverrides !== null && !Array.isArray(modelOverrides)) {
for (const [modelId, modelOverride] of Object.entries(modelOverrides)) { for (const [modelId, modelOverride] of Object.entries(modelOverrides)) {
if (typeof modelOverride !== "object" || modelOverride === null || Array.isArray(modelOverride)) continue; if (typeof modelOverride !== "object" || modelOverride === null || Array.isArray(modelOverride))
continue;
const modelOverrideRecord = modelOverride as Record<string, unknown>; const modelOverrideRecord = modelOverride as Record<string, unknown>;
migrateHeadersConfig( migrateHeadersConfig(
modelOverrideRecord.headers, modelOverrideRecord.headers,
@@ -185,6 +187,9 @@ function migrateModelsJsonConfigValues(agentDir: string): ConfigValueMigration[]
if (migrations.length === 0) return []; if (migrations.length === 0) return [];
writeFileSync(modelsPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf-8"); writeFileSync(modelsPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf-8");
return migrations; return migrations;
} catch {
return [];
}
} }
function migrateExplicitEnvVarConfigValues(): void { function migrateExplicitEnvVarConfigValues(): void {

View File

@@ -3,6 +3,8 @@ import * as os from "node:os";
import * as path from "node:path"; import * as path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { ENV_AGENT_DIR } from "../src/config.ts"; import { ENV_AGENT_DIR } from "../src/config.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { ModelRegistry } from "../src/core/model-registry.ts";
import { runMigrations } from "../src/migrations.ts"; import { runMigrations } from "../src/migrations.ts";
describe("config value env var syntax migration", () => { describe("config value env var syntax migration", () => {
@@ -68,6 +70,23 @@ describe("config value env var syntax migration", () => {
expect(logMessage).toContain('auth.json["anthropic"].key: ANTHROPIC_API_KEY -> $ANTHROPIC_API_KEY'); expect(logMessage).toContain('auth.json["anthropic"].key: ANTHROPIC_API_KEY -> $ANTHROPIC_API_KEY');
}); });
it.each([
["malformed", '{\n "providers": {\n'],
["blank", ""],
])("does not throw on %s models.json during config migration", (_name, content) => {
const agentDir = createAgentDir();
const modelsPath = path.join(agentDir, "models.json");
fs.writeFileSync(modelsPath, content, "utf-8");
withAgentDir(agentDir, () => expect(() => runMigrations(agentDir)).not.toThrow());
expect(fs.readFileSync(modelsPath, "utf-8")).toBe(content);
const registry = ModelRegistry.create(AuthStorage.create(path.join(agentDir, "auth.json")), modelsPath);
const loadError = registry.getError();
expect(loadError).toContain("Failed to parse models.json");
expect(loadError).toContain(`File: ${modelsPath}`);
});
it("rewrites legacy uppercase models.json API key and header values", () => { it("rewrites legacy uppercase models.json API key and header values", () => {
const agentDir = createAgentDir(); const agentDir = createAgentDir();
fs.writeFileSync( fs.writeFileSync(