From dbe0ede9a7148abd8b37a36aca3c356678a71ddb Mon Sep 17 00:00:00 2001 From: Chris Estreich Date: Fri, 28 Mar 2025 09:59:58 -0700 Subject: [PATCH] Handle null [x]ModelInfo types, recover from settings schema parse errors (#2064) --- src/core/config/ContextProxy.ts | 36 ++++++++++----- src/core/config/ProviderSettingsManager.ts | 14 +----- src/exports/roo-code.d.ts | 20 ++++----- src/exports/types.ts | 20 ++++----- src/schemas/index.ts | 10 ++--- .../src/components/settings/ApiOptions.tsx | 45 +++---------------- .../src/components/settings/ModelPicker.tsx | 26 ++++++----- .../settings/OpenRouterBalanceDisplay.tsx | 19 ++++++++ .../settings/RequestyBalanceDisplay.tsx | 21 +++++++++ 9 files changed, 112 insertions(+), 99 deletions(-) create mode 100644 webview-ui/src/components/settings/OpenRouterBalanceDisplay.tsx create mode 100644 webview-ui/src/components/settings/RequestyBalanceDisplay.tsx diff --git a/src/core/config/ContextProxy.ts b/src/core/config/ContextProxy.ts index 48c9846856..835ca214f6 100644 --- a/src/core/config/ContextProxy.ts +++ b/src/core/config/ContextProxy.ts @@ -2,16 +2,17 @@ import * as vscode from "vscode" import { PROVIDER_SETTINGS_KEYS, - ProviderSettings, - providerSettingsSchema, - GlobalSettings, - globalSettingsSchema, - RooCodeSettings, + GLOBAL_SETTINGS_KEYS, SECRET_STATE_KEYS, - SecretState, - isSecretStateKey, GLOBAL_STATE_KEYS, + ProviderSettings, + GlobalSettings, + SecretState, GlobalState, + RooCodeSettings, + providerSettingsSchema, + globalSettingsSchema, + isSecretStateKey, } from "../../schemas" import { logger } from "../../utils/logging" @@ -151,7 +152,15 @@ export class ContextProxy { */ public getGlobalSettings(): GlobalSettings { - return globalSettingsSchema.parse({ ...this.stateCache }) + const values = this.getValues() + + try { + return globalSettingsSchema.parse(values) + } catch (error) { + // Log to Posthog? + // We'll want to know about bad type assumptions or bad ExtensionState data. + return GLOBAL_SETTINGS_KEYS.reduce((acc, key) => ({ ...acc, [key]: values[key] }), {} as GlobalSettings) + } } /** @@ -159,7 +168,15 @@ export class ContextProxy { */ public getProviderSettings(): ProviderSettings { - return providerSettingsSchema.parse(this.getValues()) + const values = this.getValues() + + try { + return providerSettingsSchema.parse(values) + } catch (error) { + // Log to Posthog? + // We'll want to know about bad type assumptions or bad ExtensionState data. + return PROVIDER_SETTINGS_KEYS.reduce((acc, key) => ({ ...acc, [key]: values[key] }), {} as ProviderSettings) + } } public async setProviderSettings(values: ProviderSettings) { @@ -206,7 +223,6 @@ export class ContextProxy { public async export(): Promise { try { const globalSettings = globalSettingsExportSchema.parse(this.getValues()) - return Object.fromEntries(Object.entries(globalSettings).filter(([_, value]) => value !== undefined)) } catch (error) { console.log(error.message) diff --git a/src/core/config/ProviderSettingsManager.ts b/src/core/config/ProviderSettingsManager.ts index 471abe7e18..80904b401e 100644 --- a/src/core/config/ProviderSettingsManager.ts +++ b/src/core/config/ProviderSettingsManager.ts @@ -16,18 +16,6 @@ export const providerProfilesSchema = z.object({ export type ProviderProfiles = z.infer -const providerProfilesExportSchema = providerProfilesSchema.extend({ - apiConfigs: z.record( - z.string(), - providerSettingsWithIdSchema.omit({ - glamaModelInfo: true, - openRouterModelInfo: true, - unboundModelInfo: true, - requestyModelInfo: true, - }), - ), -}) - export class ProviderSettingsManager { private static readonly SCOPE_PREFIX = "roo_cline_config_" @@ -246,7 +234,7 @@ export class ProviderSettingsManager { public async export() { try { - return await this.lock(async () => providerProfilesExportSchema.parse(await this.load())) + return await this.lock(async () => providerProfilesSchema.parse(await this.load())) } catch (error) { throw new Error(`Failed to export provider profiles: ${error}`) } diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index 12e86be8fd..87ac4306e8 100644 --- a/src/exports/roo-code.d.ts +++ b/src/exports/roo-code.d.ts @@ -27,7 +27,7 @@ type ProviderSettings = { anthropicBaseUrl?: string | undefined glamaModelId?: string | undefined glamaModelInfo?: - | { + | ({ maxTokens?: number | undefined contextWindow: number supportsImages?: boolean | undefined @@ -40,13 +40,13 @@ type ProviderSettings = { description?: string | undefined reasoningEffort?: ("low" | "medium" | "high") | undefined thinking?: boolean | undefined - } + } | null) | undefined glamaApiKey?: string | undefined openRouterApiKey?: string | undefined openRouterModelId?: string | undefined openRouterModelInfo?: - | { + | ({ maxTokens?: number | undefined contextWindow: number supportsImages?: boolean | undefined @@ -59,7 +59,7 @@ type ProviderSettings = { description?: string | undefined reasoningEffort?: ("low" | "medium" | "high") | undefined thinking?: boolean | undefined - } + } | null) | undefined openRouterBaseUrl?: string | undefined openRouterSpecificProvider?: string | undefined @@ -83,7 +83,7 @@ type ProviderSettings = { openAiR1FormatEnabled?: boolean | undefined openAiModelId?: string | undefined openAiCustomModelInfo?: - | { + | ({ maxTokens?: number | undefined contextWindow: number supportsImages?: boolean | undefined @@ -96,7 +96,7 @@ type ProviderSettings = { description?: string | undefined reasoningEffort?: ("low" | "medium" | "high") | undefined thinking?: boolean | undefined - } + } | null) | undefined openAiUseAzure?: boolean | undefined azureApiVersion?: string | undefined @@ -125,7 +125,7 @@ type ProviderSettings = { unboundApiKey?: string | undefined unboundModelId?: string | undefined unboundModelInfo?: - | { + | ({ maxTokens?: number | undefined contextWindow: number supportsImages?: boolean | undefined @@ -138,12 +138,12 @@ type ProviderSettings = { description?: string | undefined reasoningEffort?: ("low" | "medium" | "high") | undefined thinking?: boolean | undefined - } + } | null) | undefined requestyApiKey?: string | undefined requestyModelId?: string | undefined requestyModelInfo?: - | { + | ({ maxTokens?: number | undefined contextWindow: number supportsImages?: boolean | undefined @@ -156,7 +156,7 @@ type ProviderSettings = { description?: string | undefined reasoningEffort?: ("low" | "medium" | "high") | undefined thinking?: boolean | undefined - } + } | null) | undefined modelTemperature?: (number | null) | undefined modelMaxTokens?: number | undefined diff --git a/src/exports/types.ts b/src/exports/types.ts index 9432537718..812a12b243 100644 --- a/src/exports/types.ts +++ b/src/exports/types.ts @@ -28,7 +28,7 @@ type ProviderSettings = { anthropicBaseUrl?: string | undefined glamaModelId?: string | undefined glamaModelInfo?: - | { + | ({ maxTokens?: number | undefined contextWindow: number supportsImages?: boolean | undefined @@ -41,13 +41,13 @@ type ProviderSettings = { description?: string | undefined reasoningEffort?: ("low" | "medium" | "high") | undefined thinking?: boolean | undefined - } + } | null) | undefined glamaApiKey?: string | undefined openRouterApiKey?: string | undefined openRouterModelId?: string | undefined openRouterModelInfo?: - | { + | ({ maxTokens?: number | undefined contextWindow: number supportsImages?: boolean | undefined @@ -60,7 +60,7 @@ type ProviderSettings = { description?: string | undefined reasoningEffort?: ("low" | "medium" | "high") | undefined thinking?: boolean | undefined - } + } | null) | undefined openRouterBaseUrl?: string | undefined openRouterSpecificProvider?: string | undefined @@ -84,7 +84,7 @@ type ProviderSettings = { openAiR1FormatEnabled?: boolean | undefined openAiModelId?: string | undefined openAiCustomModelInfo?: - | { + | ({ maxTokens?: number | undefined contextWindow: number supportsImages?: boolean | undefined @@ -97,7 +97,7 @@ type ProviderSettings = { description?: string | undefined reasoningEffort?: ("low" | "medium" | "high") | undefined thinking?: boolean | undefined - } + } | null) | undefined openAiUseAzure?: boolean | undefined azureApiVersion?: string | undefined @@ -126,7 +126,7 @@ type ProviderSettings = { unboundApiKey?: string | undefined unboundModelId?: string | undefined unboundModelInfo?: - | { + | ({ maxTokens?: number | undefined contextWindow: number supportsImages?: boolean | undefined @@ -139,12 +139,12 @@ type ProviderSettings = { description?: string | undefined reasoningEffort?: ("low" | "medium" | "high") | undefined thinking?: boolean | undefined - } + } | null) | undefined requestyApiKey?: string | undefined requestyModelId?: string | undefined requestyModelInfo?: - | { + | ({ maxTokens?: number | undefined contextWindow: number supportsImages?: boolean | undefined @@ -157,7 +157,7 @@ type ProviderSettings = { description?: string | undefined reasoningEffort?: ("low" | "medium" | "high") | undefined thinking?: boolean | undefined - } + } | null) | undefined modelTemperature?: (number | null) | undefined modelMaxTokens?: number | undefined diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 932fd490c9..72c02c61fc 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -314,12 +314,12 @@ export const providerSettingsSchema = z.object({ anthropicBaseUrl: z.string().optional(), // Glama glamaModelId: z.string().optional(), - glamaModelInfo: modelInfoSchema.optional(), + glamaModelInfo: modelInfoSchema.nullish(), glamaApiKey: z.string().optional(), // OpenRouter openRouterApiKey: z.string().optional(), openRouterModelId: z.string().optional(), - openRouterModelInfo: modelInfoSchema.optional(), + openRouterModelInfo: modelInfoSchema.nullish(), openRouterBaseUrl: z.string().optional(), openRouterSpecificProvider: z.string().optional(), openRouterUseMiddleOutTransform: z.boolean().optional(), @@ -344,7 +344,7 @@ export const providerSettingsSchema = z.object({ openAiApiKey: z.string().optional(), openAiR1FormatEnabled: z.boolean().optional(), openAiModelId: z.string().optional(), - openAiCustomModelInfo: modelInfoSchema.optional(), + openAiCustomModelInfo: modelInfoSchema.nullish(), openAiUseAzure: z.boolean().optional(), azureApiVersion: z.string().optional(), openAiStreamingEnabled: z.boolean().optional(), @@ -379,11 +379,11 @@ export const providerSettingsSchema = z.object({ // Unbound unboundApiKey: z.string().optional(), unboundModelId: z.string().optional(), - unboundModelInfo: modelInfoSchema.optional(), + unboundModelInfo: modelInfoSchema.nullish(), // Requesty requestyApiKey: z.string().optional(), requestyModelId: z.string().optional(), - requestyModelInfo: modelInfoSchema.optional(), + requestyModelInfo: modelInfoSchema.nullish(), // Claude 3.7 Sonnet Thinking modelTemperature: z.number().nullish(), modelMaxTokens: z.number().optional(), diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 40e329fca3..3ed1158f54 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -8,8 +8,6 @@ import { Checkbox } from "vscrui" import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { ExternalLinkIcon } from "@radix-ui/react-icons" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SelectSeparator, Button } from "@/components/ui" - import { ApiConfiguration, ModelInfo, @@ -42,56 +40,23 @@ import { import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage" import { vscode } from "@/utils/vscode" +import { validateApiConfiguration, validateModelId, validateBedrockArn } from "@/utils/validate" import { useOpenRouterModelProviders, OPENROUTER_DEFAULT_PROVIDER_NAME, } from "@/components/ui/hooks/useOpenRouterModelProviders" -import { useOpenRouterKeyInfo } from "@/components/ui/hooks/useOpenRouterKeyInfo" -import { useRequestyKeyInfo } from "@/components/ui/hooks/useRequestyKeyInfo" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SelectSeparator, Button } from "@/components/ui" + import { MODELS_BY_PROVIDER, PROVIDERS, AWS_REGIONS, VERTEX_REGIONS } from "./constants" import { VSCodeButtonLink } from "../common/VSCodeButtonLink" import { ModelInfoView } from "./ModelInfoView" import { ModelPicker } from "./ModelPicker" import { TemperatureControl } from "./TemperatureControl" -import { validateApiConfiguration, validateModelId, validateBedrockArn } from "@/utils/validate" import { ApiErrorMessage } from "./ApiErrorMessage" import { ThinkingBudget } from "./ThinkingBudget" import { R1FormatSetting } from "./R1FormatSetting" - -// Component to display OpenRouter API key balance -const OpenRouterBalanceDisplay = ({ apiKey, baseUrl }: { apiKey: string; baseUrl?: string }) => { - const { data: keyInfo } = useOpenRouterKeyInfo(apiKey, baseUrl) - - if (!keyInfo || !keyInfo.limit) { - return null - } - - const formattedBalance = (keyInfo.limit - keyInfo.usage).toFixed(2) - - return ( - - ${formattedBalance} - - ) -} - -const RequestyBalanceDisplay = ({ apiKey }: { apiKey: string }) => { - const { data: keyInfo } = useRequestyKeyInfo(apiKey) - - if (!keyInfo) { - return null - } - - // Parse the balance to a number and format it to 2 decimal places - const balance = parseFloat(keyInfo.org_balance) - const formattedBalance = balance.toFixed(2) - - return ( - - ${formattedBalance} - - ) -} +import { OpenRouterBalanceDisplay } from "./OpenRouterBalanceDisplay" +import { RequestyBalanceDisplay } from "./RequestyBalanceDisplay" interface ApiOptionsProps { uriScheme: string | undefined diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index e4e848478c..c8d8ed7a44 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -3,6 +3,8 @@ import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" import { Trans } from "react-i18next" import { ChevronsUpDown, Check, X } from "lucide-react" +import { ProviderSettings, ModelInfo } from "../../../../src/schemas" + import { useAppTranslation } from "@/i18n/TranslationContext" import { cn } from "@/lib/utils" import { @@ -18,30 +20,30 @@ import { Button, } from "@/components/ui" -import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api" - import { normalizeApiConfiguration } from "./ApiOptions" import { ThinkingBudget } from "./ThinkingBudget" import { ModelInfoView } from "./ModelInfoView" -type ExtractType = NonNullable< - { [K in keyof ApiConfiguration]: Required[K] extends T ? K : never }[keyof ApiConfiguration] +type ModelIdKey = keyof Pick< + ProviderSettings, + "glamaModelId" | "openRouterModelId" | "unboundModelId" | "requestyModelId" | "openAiModelId" > -type ModelIdKeys = NonNullable< - { [K in keyof ApiConfiguration]: K extends `${string}ModelId` ? K : never }[keyof ApiConfiguration] +type ModelInfoKey = keyof Pick< + ProviderSettings, + "glamaModelInfo" | "openRouterModelInfo" | "unboundModelInfo" | "requestyModelInfo" | "openAiCustomModelInfo" > interface ModelPickerProps { defaultModelId: string defaultModelInfo?: ModelInfo models: Record | null - modelIdKey: ModelIdKeys - modelInfoKey: ExtractType + modelIdKey: ModelIdKey + modelInfoKey: ModelInfoKey serviceName: string serviceUrl: string - apiConfiguration: ApiConfiguration - setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: K, value: ProviderSettings[K]) => void } export const ModelPicker = ({ @@ -72,7 +74,9 @@ export const ModelPicker = ({ const onSelect = useCallback( (modelId: string) => { - if (!modelId) return + if (!modelId) { + return + } setOpen(false) const modelInfo = models?.[modelId] diff --git a/webview-ui/src/components/settings/OpenRouterBalanceDisplay.tsx b/webview-ui/src/components/settings/OpenRouterBalanceDisplay.tsx new file mode 100644 index 0000000000..fd081d1c56 --- /dev/null +++ b/webview-ui/src/components/settings/OpenRouterBalanceDisplay.tsx @@ -0,0 +1,19 @@ +import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" + +import { useOpenRouterKeyInfo } from "@/components/ui/hooks/useOpenRouterKeyInfo" + +export const OpenRouterBalanceDisplay = ({ apiKey, baseUrl }: { apiKey: string; baseUrl?: string }) => { + const { data: keyInfo } = useOpenRouterKeyInfo(apiKey, baseUrl) + + if (!keyInfo || !keyInfo.limit) { + return null + } + + const formattedBalance = (keyInfo.limit - keyInfo.usage).toFixed(2) + + return ( + + ${formattedBalance} + + ) +} diff --git a/webview-ui/src/components/settings/RequestyBalanceDisplay.tsx b/webview-ui/src/components/settings/RequestyBalanceDisplay.tsx new file mode 100644 index 0000000000..9eb9734499 --- /dev/null +++ b/webview-ui/src/components/settings/RequestyBalanceDisplay.tsx @@ -0,0 +1,21 @@ +import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" + +import { useRequestyKeyInfo } from "@/components/ui/hooks/useRequestyKeyInfo" + +export const RequestyBalanceDisplay = ({ apiKey }: { apiKey: string }) => { + const { data: keyInfo } = useRequestyKeyInfo(apiKey) + + if (!keyInfo) { + return null + } + + // Parse the balance to a number and format it to 2 decimal places. + const balance = parseFloat(keyInfo.org_balance) + const formattedBalance = balance.toFixed(2) + + return ( + + ${formattedBalance} + + ) +}