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

This commit is contained in:
Chris Estreich 2025-03-28 09:59:58 -07:00 committed by GitHub
parent e7e5511b65
commit dbe0ede9a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 112 additions and 99 deletions

View file

@ -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<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)

View file

@ -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}`)
}

View file

@ -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

View file

@ -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

View file

@ -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(),

View file

@ -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

View file

@ -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]

View file

@ -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>
)
}

View file

@ -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>
)
}