فهرست منبع

Handle null [x]ModelInfo types, recover from settings schema parse errors (#2064)

Chris Estreich 9 ماه پیش
والد
کامیت
dbe0ede9a7

+ 25 - 9
src/core/config/ContextProxy.ts

@@ -2,16 +2,17 @@ import * as vscode from "vscode"
 
 import {
 	PROVIDER_SETTINGS_KEYS,
+	GLOBAL_SETTINGS_KEYS,
+	SECRET_STATE_KEYS,
+	GLOBAL_STATE_KEYS,
 	ProviderSettings,
-	providerSettingsSchema,
 	GlobalSettings,
-	globalSettingsSchema,
-	RooCodeSettings,
-	SECRET_STATE_KEYS,
 	SecretState,
-	isSecretStateKey,
-	GLOBAL_STATE_KEYS,
 	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<GlobalSettings | undefined> {
 		try {
 			const globalSettings = globalSettingsExportSchema.parse(this.getValues())
-
 			return Object.fromEntries(Object.entries(globalSettings).filter(([_, value]) => value !== undefined))
 		} catch (error) {
 			console.log(error.message)

+ 1 - 13
src/core/config/ProviderSettingsManager.ts

@@ -16,18 +16,6 @@ export const providerProfilesSchema = z.object({
 
 export type ProviderProfiles = z.infer<typeof providerProfilesSchema>
 
-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}`)
 		}

+ 10 - 10
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

+ 10 - 10
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

+ 5 - 5
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(),

+ 5 - 40
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 (
-		<VSCodeLink href="https://openrouter.ai/settings/keys" className="text-vscode-foreground hover:underline">
-			${formattedBalance}
-		</VSCodeLink>
-	)
-}
-
-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 (
-		<VSCodeLink href="https://app.requesty.ai/settings" className="text-vscode-foreground hover:underline">
-			${formattedBalance}
-		</VSCodeLink>
-	)
-}
+import { OpenRouterBalanceDisplay } from "./OpenRouterBalanceDisplay"
+import { RequestyBalanceDisplay } from "./RequestyBalanceDisplay"
 
 interface ApiOptionsProps {
 	uriScheme: string | undefined

+ 15 - 11
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<T> = NonNullable<
-	{ [K in keyof ApiConfiguration]: Required<ApiConfiguration>[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<string, ModelInfo> | null
-	modelIdKey: ModelIdKeys
-	modelInfoKey: ExtractType<ModelInfo>
+	modelIdKey: ModelIdKey
+	modelInfoKey: ModelInfoKey
 	serviceName: string
 	serviceUrl: string
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: <K extends keyof ProviderSettings>(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]

+ 19 - 0
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 (
+		<VSCodeLink href="https://openrouter.ai/settings/keys" className="text-vscode-foreground hover:underline">
+			${formattedBalance}
+		</VSCodeLink>
+	)
+}

+ 21 - 0
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 (
+		<VSCodeLink href="https://app.requesty.ai/settings" className="text-vscode-foreground hover:underline">
+			${formattedBalance}
+		</VSCodeLink>
+	)
+}