Skip to content

Commit a4f6f53

Browse files
committed
Parse providers individually
1 parent 45fb958 commit a4f6f53

File tree

2 files changed

+97
-11
lines changed

2 files changed

+97
-11
lines changed

src/core/config/ProviderSettingsManager.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ExtensionContext } from "vscode"
22
import { z, ZodError } from "zod"
33

4-
import { providerSettingsSchema, ApiConfigMeta } from "../../schemas"
4+
import { providerSettingsSchema, ApiConfigMeta, ProviderSettings } from "../../schemas"
55
import { Mode, modes } from "../../shared/modes"
66
import { telemetryService } from "../../services/telemetry/TelemetryService"
77

@@ -321,7 +321,32 @@ export class ProviderSettingsManager {
321321
private async load(): Promise<ProviderProfiles> {
322322
try {
323323
const content = await this.context.secrets.get(this.secretsKey)
324-
return content ? providerProfilesSchema.parse(JSON.parse(content)) : this.defaultProviderProfiles
324+
325+
if (!content) {
326+
return this.defaultProviderProfiles
327+
}
328+
329+
const providerProfiles = providerProfilesSchema
330+
.extend({
331+
apiConfigs: z.record(z.string(), z.any()),
332+
})
333+
.parse(JSON.parse(content))
334+
335+
const apiConfigs = Object.entries(providerProfiles.apiConfigs).reduce(
336+
(acc, [key, apiConfig]) => {
337+
// Use .strict() to ensure only defined fields are allowed
338+
const result = providerSettingsWithIdSchema.strict().safeParse(apiConfig)
339+
return result.success ? { ...acc, [key]: result.data } : acc
340+
},
341+
{} as Record<string, ProviderSettingsWithId>,
342+
)
343+
344+
return {
345+
...providerProfiles,
346+
apiConfigs: Object.fromEntries(
347+
Object.entries(apiConfigs).filter(([_, apiConfig]) => apiConfig !== null),
348+
),
349+
}
325350
} catch (error) {
326351
if (error instanceof ZodError) {
327352
telemetryService.captureSchemaValidationError({ schemaName: "ProviderProfiles", error })

src/core/config/__tests__/ProviderSettingsManager.test.ts

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,17 @@ describe("ProviderSettingsManager", () => {
6868
JSON.stringify({
6969
currentApiConfigName: "default",
7070
apiConfigs: {
71+
// Provide a minimal valid structure (missing ID)
7172
default: {
72-
config: {},
73+
apiProvider: "openai", // Example provider
74+
openAiModelId: "gpt-4", // Corresponding model field
75+
rateLimitSeconds: 0,
7376
},
77+
// Provide a minimal valid structure (missing ID)
7478
test: {
7579
apiProvider: "anthropic",
80+
apiModelId: "claude-instant-1", // Corresponding model field
81+
rateLimitSeconds: 0,
7682
},
7783
},
7884
}),
@@ -94,19 +100,24 @@ describe("ProviderSettingsManager", () => {
94100
JSON.stringify({
95101
currentApiConfigName: "default",
96102
apiConfigs: {
103+
// Valid structure, missing rateLimitSeconds
97104
default: {
98-
config: {},
99-
id: "default",
105+
apiProvider: "openai",
106+
openAiModelId: "gpt-4",
107+
id: "default-id", // Needs an ID for migration test structure
100108
rateLimitSeconds: undefined,
101109
},
110+
// Valid structure, missing rateLimitSeconds and ID (ID will be generated)
102111
test: {
103112
apiProvider: "anthropic",
113+
apiModelId: "claude-instant-1",
104114
rateLimitSeconds: undefined,
105115
},
116+
// Valid structure, existing rateLimitSeconds
106117
existing: {
107118
apiProvider: "anthropic",
108-
// this should not really be possible, unless someone has loaded a hand edited config,
109-
// but we don't overwrite so we'll check that
119+
apiModelId: "claude-instant-1",
120+
id: "existing-id", // Needs an ID
110121
rateLimitSeconds: 43,
111122
},
112123
},
@@ -422,11 +433,13 @@ describe("ProviderSettingsManager", () => {
422433
JSON.stringify({
423434
currentApiConfigName: "default",
424435
apiConfigs: {
436+
// Provide a valid structure for 'test'
425437
test: {
426-
config: {
427-
apiProvider: "anthropic",
428-
},
429-
id: "test-id",
438+
apiProvider: "anthropic",
439+
apiModelId: "claude-instant-1",
440+
apiKey: "test-key",
441+
rateLimitSeconds: 0,
442+
id: "test-id", // Keep ID as loadConfig uses it
430443
},
431444
},
432445
}),
@@ -437,6 +450,54 @@ describe("ProviderSettingsManager", () => {
437450
"Failed to load config: Error: Failed to write provider profiles to secrets: Error: Storage failed",
438451
)
439452
})
453+
454+
it("should remove invalid profiles during load", async () => {
455+
const invalidConfig = {
456+
currentApiConfigName: "valid",
457+
apiConfigs: {
458+
valid: {
459+
apiProvider: "anthropic",
460+
apiKey: "valid-key",
461+
// id: "valid-id", // ID is added by the manager, not part of the schema input
462+
apiModelId: "claude-3-opus-20240229", // Correct field name for Anthropic model
463+
rateLimitSeconds: 0, // Generic field, should be fine
464+
},
465+
invalid: {
466+
// Missing required fields like apiProvider or apiKey
467+
id: "invalid-id",
468+
someOtherField: "value",
469+
},
470+
anotherInvalid: "not an object", // Incorrect type
471+
},
472+
migrations: {
473+
rateLimitSecondsMigrated: true,
474+
},
475+
}
476+
477+
mockSecrets.get.mockResolvedValue(JSON.stringify(invalidConfig))
478+
479+
// Load a valid config - this should trigger the cleanup
480+
// Note: loadConfig itself doesn't clean, initialization does.
481+
// We need to call initialize to trigger the load and cleanup.
482+
await providerSettingsManager.initialize()
483+
484+
// Check that the stored config was updated and invalid profiles removed
485+
// Initialize calls store multiple times potentially (migration, id generation, cleanup)
486+
// We need to find the call that reflects the final cleaned state.
487+
// The last call to store should contain the cleaned config.
488+
const storeCalls = mockSecrets.store.mock.calls
489+
expect(storeCalls.length).toBeGreaterThan(0) // Ensure store was called at least once
490+
const finalStoredConfigJson = storeCalls[storeCalls.length - 1][1]
491+
const storedConfig = JSON.parse(finalStoredConfigJson)
492+
493+
expect(storedConfig.apiConfigs.valid).toBeDefined()
494+
expect(storedConfig.apiConfigs.invalid).toBeUndefined()
495+
expect(storedConfig.apiConfigs.anotherInvalid).toBeUndefined()
496+
expect(Object.keys(storedConfig.apiConfigs)).toEqual(["valid"])
497+
// Initialize might reset currentApiConfigName if the original was invalid,
498+
// or keep it if it was valid. Let's check it's still 'valid'.
499+
expect(storedConfig.currentApiConfigName).toBe("valid")
500+
})
440501
})
441502

442503
describe("ResetAllConfigs", () => {

0 commit comments

Comments
 (0)