| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489 |
- import { ExtensionContext } from "vscode"
- import { z, ZodError } from "zod"
- import {
- type ProviderSettingsEntry,
- providerSettingsSchema,
- providerSettingsSchemaDiscriminated,
- } from "@roo-code/types"
- import { TelemetryService } from "@roo-code/telemetry"
- import { Mode, modes } from "../../shared/modes"
- const providerSettingsWithIdSchema = providerSettingsSchema.extend({ id: z.string().optional() })
- const discriminatedProviderSettingsWithIdSchema = providerSettingsSchemaDiscriminated.and(
- z.object({ id: z.string().optional() }),
- )
- type ProviderSettingsWithId = z.infer<typeof providerSettingsWithIdSchema>
- export const providerProfilesSchema = z.object({
- currentApiConfigName: z.string(),
- apiConfigs: z.record(z.string(), providerSettingsWithIdSchema),
- modeApiConfigs: z.record(z.string(), z.string()).optional(),
- migrations: z
- .object({
- rateLimitSecondsMigrated: z.boolean().optional(),
- diffSettingsMigrated: z.boolean().optional(),
- openAiHeadersMigrated: z.boolean().optional(),
- })
- .optional(),
- })
- export type ProviderProfiles = z.infer<typeof providerProfilesSchema>
- export class ProviderSettingsManager {
- private static readonly SCOPE_PREFIX = "roo_cline_config_"
- private readonly defaultConfigId = this.generateId()
- private readonly defaultModeApiConfigs: Record<string, string> = Object.fromEntries(
- modes.map((mode) => [mode.slug, this.defaultConfigId]),
- )
- private readonly defaultProviderProfiles: ProviderProfiles = {
- currentApiConfigName: "default",
- apiConfigs: { default: { id: this.defaultConfigId } },
- modeApiConfigs: this.defaultModeApiConfigs,
- migrations: {
- rateLimitSecondsMigrated: true, // Mark as migrated on fresh installs
- diffSettingsMigrated: true, // Mark as migrated on fresh installs
- openAiHeadersMigrated: true, // Mark as migrated on fresh installs
- },
- }
- private readonly context: ExtensionContext
- constructor(context: ExtensionContext) {
- this.context = context
- // TODO: We really shouldn't have async methods in the constructor.
- this.initialize().catch(console.error)
- }
- public generateId() {
- return Math.random().toString(36).substring(2, 15)
- }
- // Synchronize readConfig/writeConfig operations to avoid data loss.
- private _lock = Promise.resolve()
- private lock<T>(cb: () => Promise<T>) {
- const next = this._lock.then(cb)
- this._lock = next.catch(() => {}) as Promise<void>
- return next
- }
- /**
- * Initialize config if it doesn't exist and run migrations.
- */
- public async initialize() {
- try {
- return await this.lock(async () => {
- const providerProfiles = await this.load()
- if (!providerProfiles) {
- await this.store(this.defaultProviderProfiles)
- return
- }
- let isDirty = false
- // Migrate existing installs to have per-mode API config map
- if (!providerProfiles.modeApiConfigs) {
- // Use the currently selected config for all modes initially
- const currentName = providerProfiles.currentApiConfigName
- const seedId =
- providerProfiles.apiConfigs[currentName]?.id ??
- Object.values(providerProfiles.apiConfigs)[0]?.id ??
- this.defaultConfigId
- providerProfiles.modeApiConfigs = Object.fromEntries(modes.map((m) => [m.slug, seedId]))
- isDirty = true
- }
- // Ensure all configs have IDs.
- for (const [_name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {
- if (!apiConfig.id) {
- apiConfig.id = this.generateId()
- isDirty = true
- }
- }
- // Ensure migrations field exists
- if (!providerProfiles.migrations) {
- providerProfiles.migrations = {
- rateLimitSecondsMigrated: false,
- diffSettingsMigrated: false,
- openAiHeadersMigrated: false,
- } // Initialize with default values
- isDirty = true
- }
- if (!providerProfiles.migrations.rateLimitSecondsMigrated) {
- await this.migrateRateLimitSeconds(providerProfiles)
- providerProfiles.migrations.rateLimitSecondsMigrated = true
- isDirty = true
- }
- if (!providerProfiles.migrations.diffSettingsMigrated) {
- await this.migrateDiffSettings(providerProfiles)
- providerProfiles.migrations.diffSettingsMigrated = true
- isDirty = true
- }
- if (!providerProfiles.migrations.openAiHeadersMigrated) {
- await this.migrateOpenAiHeaders(providerProfiles)
- providerProfiles.migrations.openAiHeadersMigrated = true
- isDirty = true
- }
- if (isDirty) {
- await this.store(providerProfiles)
- }
- })
- } catch (error) {
- throw new Error(`Failed to initialize config: ${error}`)
- }
- }
- private async migrateRateLimitSeconds(providerProfiles: ProviderProfiles) {
- try {
- let rateLimitSeconds: number | undefined
- try {
- rateLimitSeconds = await this.context.globalState.get<number>("rateLimitSeconds")
- } catch (error) {
- console.error("[MigrateRateLimitSeconds] Error getting global rate limit:", error)
- }
- if (rateLimitSeconds === undefined) {
- // Failed to get the existing value, use the default.
- rateLimitSeconds = 0
- }
- for (const [_name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {
- if (apiConfig.rateLimitSeconds === undefined) {
- apiConfig.rateLimitSeconds = rateLimitSeconds
- }
- }
- } catch (error) {
- console.error(`[MigrateRateLimitSeconds] Failed to migrate rate limit settings:`, error)
- }
- }
- private async migrateDiffSettings(providerProfiles: ProviderProfiles) {
- try {
- let diffEnabled: boolean | undefined
- let fuzzyMatchThreshold: number | undefined
- try {
- diffEnabled = await this.context.globalState.get<boolean>("diffEnabled")
- fuzzyMatchThreshold = await this.context.globalState.get<number>("fuzzyMatchThreshold")
- } catch (error) {
- console.error("[MigrateDiffSettings] Error getting global diff settings:", error)
- }
- if (diffEnabled === undefined) {
- // Failed to get the existing value, use the default.
- diffEnabled = true
- }
- if (fuzzyMatchThreshold === undefined) {
- // Failed to get the existing value, use the default.
- fuzzyMatchThreshold = 1.0
- }
- for (const [_name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {
- if (apiConfig.diffEnabled === undefined) {
- apiConfig.diffEnabled = diffEnabled
- }
- if (apiConfig.fuzzyMatchThreshold === undefined) {
- apiConfig.fuzzyMatchThreshold = fuzzyMatchThreshold
- }
- }
- } catch (error) {
- console.error(`[MigrateDiffSettings] Failed to migrate diff settings:`, error)
- }
- }
- private async migrateOpenAiHeaders(providerProfiles: ProviderProfiles) {
- try {
- for (const [_name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {
- // Use type assertion to access the deprecated property safely
- const configAny = apiConfig as any
- // Check if openAiHostHeader exists but openAiHeaders doesn't
- if (
- configAny.openAiHostHeader &&
- (!apiConfig.openAiHeaders || Object.keys(apiConfig.openAiHeaders || {}).length === 0)
- ) {
- // Create the headers object with the Host value
- apiConfig.openAiHeaders = { Host: configAny.openAiHostHeader }
- // Delete the old property to prevent re-migration
- // This prevents the header from reappearing after deletion
- configAny.openAiHostHeader = undefined
- }
- }
- } catch (error) {
- console.error(`[MigrateOpenAiHeaders] Failed to migrate OpenAI headers:`, error)
- }
- }
- /**
- * List all available configs with metadata.
- */
- public async listConfig(): Promise<ProviderSettingsEntry[]> {
- try {
- return await this.lock(async () => {
- const providerProfiles = await this.load()
- return Object.entries(providerProfiles.apiConfigs).map(([name, apiConfig]) => ({
- name,
- id: apiConfig.id || "",
- apiProvider: apiConfig.apiProvider,
- }))
- })
- } catch (error) {
- throw new Error(`Failed to list configs: ${error}`)
- }
- }
- /**
- * Save a config with the given name.
- * Preserves the ID from the input 'config' object if it exists,
- * otherwise generates a new one (for creation scenarios).
- */
- public async saveConfig(name: string, config: ProviderSettingsWithId): Promise<string> {
- try {
- return await this.lock(async () => {
- const providerProfiles = await this.load()
- // Preserve the existing ID if this is an update to an existing config.
- const existingId = providerProfiles.apiConfigs[name]?.id
- const id = config.id || existingId || this.generateId()
- // Filter out settings from other providers.
- const filteredConfig = providerSettingsSchemaDiscriminated.parse(config)
- providerProfiles.apiConfigs[name] = { ...filteredConfig, id }
- await this.store(providerProfiles)
- return id
- })
- } catch (error) {
- throw new Error(`Failed to save config: ${error}`)
- }
- }
- public async getProfile(
- params: { name: string } | { id: string },
- ): Promise<ProviderSettingsWithId & { name: string }> {
- try {
- return await this.lock(async () => {
- const providerProfiles = await this.load()
- let name: string
- let providerSettings: ProviderSettingsWithId
- if ("name" in params) {
- name = params.name
- if (!providerProfiles.apiConfigs[name]) {
- throw new Error(`Config with name '${name}' not found`)
- }
- providerSettings = providerProfiles.apiConfigs[name]
- } else {
- const id = params.id
- const entry = Object.entries(providerProfiles.apiConfigs).find(
- ([_, apiConfig]) => apiConfig.id === id,
- )
- if (!entry) {
- throw new Error(`Config with ID '${id}' not found`)
- }
- name = entry[0]
- providerSettings = entry[1]
- }
- return { name, ...providerSettings }
- })
- } catch (error) {
- throw new Error(`Failed to get profile: ${error instanceof Error ? error.message : error}`)
- }
- }
- /**
- * Activate a profile by name or ID.
- */
- public async activateProfile(
- params: { name: string } | { id: string },
- ): Promise<ProviderSettingsWithId & { name: string }> {
- const { name, ...providerSettings } = await this.getProfile(params)
- try {
- return await this.lock(async () => {
- const providerProfiles = await this.load()
- providerProfiles.currentApiConfigName = name
- await this.store(providerProfiles)
- return { name, ...providerSettings }
- })
- } catch (error) {
- throw new Error(`Failed to activate profile: ${error instanceof Error ? error.message : error}`)
- }
- }
- /**
- * Delete a config by name.
- */
- public async deleteConfig(name: string) {
- try {
- return await this.lock(async () => {
- const providerProfiles = await this.load()
- if (!providerProfiles.apiConfigs[name]) {
- throw new Error(`Config '${name}' not found`)
- }
- if (Object.keys(providerProfiles.apiConfigs).length === 1) {
- throw new Error(`Cannot delete the last remaining configuration`)
- }
- delete providerProfiles.apiConfigs[name]
- await this.store(providerProfiles)
- })
- } catch (error) {
- throw new Error(`Failed to delete config: ${error}`)
- }
- }
- /**
- * Check if a config exists by name.
- */
- public async hasConfig(name: string) {
- try {
- return await this.lock(async () => {
- const providerProfiles = await this.load()
- return name in providerProfiles.apiConfigs
- })
- } catch (error) {
- throw new Error(`Failed to check config existence: ${error}`)
- }
- }
- /**
- * Set the API config for a specific mode.
- */
- public async setModeConfig(mode: Mode, configId: string) {
- try {
- return await this.lock(async () => {
- const providerProfiles = await this.load()
- // Ensure the per-mode config map exists
- if (!providerProfiles.modeApiConfigs) {
- providerProfiles.modeApiConfigs = {}
- }
- // Assign the chosen config ID to this mode
- providerProfiles.modeApiConfigs[mode] = configId
- await this.store(providerProfiles)
- })
- } catch (error) {
- throw new Error(`Failed to set mode config: ${error}`)
- }
- }
- /**
- * Get the API config ID for a specific mode.
- */
- public async getModeConfigId(mode: Mode) {
- try {
- return await this.lock(async () => {
- const { modeApiConfigs } = await this.load()
- return modeApiConfigs?.[mode]
- })
- } catch (error) {
- throw new Error(`Failed to get mode config: ${error}`)
- }
- }
- public async export() {
- try {
- return await this.lock(async () => {
- const profiles = providerProfilesSchema.parse(await this.load())
- const configs = profiles.apiConfigs
- for (const name in configs) {
- // Avoid leaking properties from other providers.
- configs[name] = discriminatedProviderSettingsWithIdSchema.parse(configs[name])
- }
- return profiles
- })
- } catch (error) {
- throw new Error(`Failed to export provider profiles: ${error}`)
- }
- }
- public async import(providerProfiles: ProviderProfiles) {
- try {
- return await this.lock(() => this.store(providerProfiles))
- } catch (error) {
- throw new Error(`Failed to import provider profiles: ${error}`)
- }
- }
- /**
- * Reset provider profiles by deleting them from secrets.
- */
- public async resetAllConfigs() {
- return await this.lock(async () => {
- await this.context.secrets.delete(this.secretsKey)
- })
- }
- private get secretsKey() {
- return `${ProviderSettingsManager.SCOPE_PREFIX}api_config`
- }
- private async load(): Promise<ProviderProfiles> {
- try {
- const content = await this.context.secrets.get(this.secretsKey)
- if (!content) {
- return this.defaultProviderProfiles
- }
- const providerProfiles = providerProfilesSchema
- .extend({
- apiConfigs: z.record(z.string(), z.any()),
- })
- .parse(JSON.parse(content))
- const apiConfigs = Object.entries(providerProfiles.apiConfigs).reduce(
- (acc, [key, apiConfig]) => {
- const result = providerSettingsWithIdSchema.safeParse(apiConfig)
- return result.success ? { ...acc, [key]: result.data } : acc
- },
- {} as Record<string, ProviderSettingsWithId>,
- )
- return {
- ...providerProfiles,
- apiConfigs: Object.fromEntries(
- Object.entries(apiConfigs).filter(([_, apiConfig]) => apiConfig !== null),
- ),
- }
- } catch (error) {
- if (error instanceof ZodError) {
- TelemetryService.instance.captureSchemaValidationError({
- schemaName: "ProviderProfiles",
- error,
- })
- }
- throw new Error(`Failed to read provider profiles from secrets: ${error}`)
- }
- }
- private async store(providerProfiles: ProviderProfiles) {
- try {
- await this.context.secrets.store(this.secretsKey, JSON.stringify(providerProfiles, null, 2))
- } catch (error) {
- throw new Error(`Failed to write provider profiles to secrets: ${error}`)
- }
- }
- }
|