11import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
22import debounce from "debounce"
3- import { useMemo , useState , useCallback , useEffect } from "react"
3+ import { useMemo , useState , useCallback , useEffect , useRef } from "react"
44import { useMount } from "react-use"
55import { CaretSortIcon , CheckIcon } from "@radix-ui/react-icons"
66
@@ -23,15 +23,24 @@ import { vscode } from "../../utils/vscode"
2323import { normalizeApiConfiguration } from "./ApiOptions"
2424import { ModelInfoView } from "./ModelInfoView"
2525
26- interface ModelPickerProps {
26+ type ModelProvider = "glama" | "openRouter" | "unbound" | "requesty" | "openAi"
27+
28+ type ModelKeys < T extends ModelProvider > = `${T } Models`
29+ type ConfigKeys < T extends ModelProvider > = `${T } ModelId`
30+ type InfoKeys < T extends ModelProvider > = `${T } ModelInfo`
31+ type RefreshMessageType < T extends ModelProvider > = `refresh${Capitalize < T > } Models`
32+
33+ interface ModelPickerProps < T extends ModelProvider = ModelProvider > {
2734 defaultModelId : string
28- modelsKey : "glamaModels" | "openRouterModels" | "unboundModels" | "requestyModels"
29- configKey : "glamaModelId" | "openRouterModelId" | "unboundModelId" | "requestyModelId"
30- infoKey : "glamaModelInfo" | "openRouterModelInfo" | "unboundModelInfo" | "requestyModelInfo"
31- refreshMessageType : "refreshGlamaModels" | "refreshOpenRouterModels" | "refreshUnboundModels" | "refreshRequestyModels"
35+ modelsKey : ModelKeys < T >
36+ configKey : ConfigKeys < T >
37+ infoKey : InfoKeys < T >
38+ refreshMessageType : RefreshMessageType < T >
39+ refreshValues ?: Record < string , any >
3240 serviceName : string
3341 serviceUrl : string
3442 recommendedModel : string
43+ allowCustomModel ?: boolean
3544}
3645
3746export const ModelPicker = ( {
@@ -40,25 +49,51 @@ export const ModelPicker = ({
4049 configKey,
4150 infoKey,
4251 refreshMessageType,
52+ refreshValues,
4353 serviceName,
4454 serviceUrl,
4555 recommendedModel,
56+ allowCustomModel = false ,
4657} : ModelPickerProps ) => {
58+ const [ customModelId , setCustomModelId ] = useState ( "" )
59+ const [ isCustomModel , setIsCustomModel ] = useState ( false )
4760 const [ open , setOpen ] = useState ( false )
4861 const [ value , setValue ] = useState ( defaultModelId )
4962 const [ isDescriptionExpanded , setIsDescriptionExpanded ] = useState ( false )
63+ const prevRefreshValuesRef = useRef < Record < string , any > | undefined > ( )
5064
51- const { apiConfiguration, setApiConfiguration, [ modelsKey ] : models , onUpdateApiConfig } = useExtensionState ( )
52- const modelIds = useMemo ( ( ) => Object . keys ( models ) . sort ( ( a , b ) => a . localeCompare ( b ) ) , [ models ] )
65+ const { apiConfiguration, [ modelsKey ] : models , onUpdateApiConfig, setApiConfiguration } = useExtensionState ( )
66+
67+ const modelIds = useMemo (
68+ ( ) => ( Array . isArray ( models ) ? models : Object . keys ( models ) ) . sort ( ( a , b ) => a . localeCompare ( b ) ) ,
69+ [ models ] ,
70+ )
5371
5472 const { selectedModelId, selectedModelInfo } = useMemo (
5573 ( ) => normalizeApiConfiguration ( apiConfiguration ) ,
5674 [ apiConfiguration ] ,
5775 )
5876
77+ const onSelectCustomModel = useCallback (
78+ ( modelId : string ) => {
79+ setCustomModelId ( modelId )
80+ const modelInfo = { id : modelId }
81+ const apiConfig = { ...apiConfiguration , [ configKey ] : modelId , [ infoKey ] : modelInfo }
82+ setApiConfiguration ( apiConfig )
83+ onUpdateApiConfig ( apiConfig )
84+ setValue ( modelId )
85+ setOpen ( false )
86+ setIsCustomModel ( false )
87+ } ,
88+ [ apiConfiguration , configKey , infoKey , onUpdateApiConfig , setApiConfiguration ] ,
89+ )
90+
5991 const onSelect = useCallback (
6092 ( modelId : string ) => {
61- const apiConfig = { ...apiConfiguration , [ configKey ] : modelId , [ infoKey ] : models [ modelId ] }
93+ const modelInfo = Array . isArray ( models )
94+ ? { id : modelId } // For OpenAI models which are just strings
95+ : models [ modelId ] // For other models that have full info objects
96+ const apiConfig = { ...apiConfiguration , [ configKey ] : modelId , [ infoKey ] : modelInfo }
6297 setApiConfiguration ( apiConfig )
6398 onUpdateApiConfig ( apiConfig )
6499 setValue ( modelId )
@@ -67,16 +102,42 @@ export const ModelPicker = ({
67102 [ apiConfiguration , configKey , infoKey , models , onUpdateApiConfig , setApiConfiguration ] ,
68103 )
69104
70- const debouncedRefreshModels = useMemo (
71- ( ) => debounce ( ( ) => vscode . postMessage ( { type : refreshMessageType } ) , 50 ) ,
72- [ refreshMessageType ] ,
73- )
105+ const debouncedRefreshModels = useMemo ( ( ) => {
106+ return debounce ( ( ) => {
107+ const message = refreshValues
108+ ? { type : refreshMessageType , values : refreshValues }
109+ : { type : refreshMessageType }
110+ vscode . postMessage ( message )
111+ } , 100 )
112+ } , [ refreshMessageType , refreshValues ] )
74113
75114 useMount ( ( ) => {
76115 debouncedRefreshModels ( )
77116 return ( ) => debouncedRefreshModels . clear ( )
78117 } )
79118
119+ useEffect ( ( ) => {
120+ if ( ! refreshValues ) {
121+ prevRefreshValuesRef . current = undefined
122+ return
123+ }
124+
125+ // Check if all values in refreshValues are truthy
126+ if ( Object . values ( refreshValues ) . some ( ( value ) => ! value ) ) {
127+ prevRefreshValuesRef . current = undefined
128+ return
129+ }
130+
131+ // Compare with previous values
132+ const prevValues = prevRefreshValuesRef . current
133+ if ( prevValues && JSON . stringify ( prevValues ) === JSON . stringify ( refreshValues ) ) {
134+ return
135+ }
136+
137+ prevRefreshValuesRef . current = refreshValues
138+ debouncedRefreshModels ( )
139+ } , [ debouncedRefreshModels , refreshValues ] )
140+
80141 useEffect ( ( ) => setValue ( selectedModelId ) , [ selectedModelId ] )
81142
82143 return (
@@ -104,6 +165,17 @@ export const ModelPicker = ({
104165 </ CommandItem >
105166 ) ) }
106167 </ CommandGroup >
168+ { allowCustomModel && (
169+ < CommandGroup heading = "Custom" >
170+ < CommandItem
171+ onSelect = { ( ) => {
172+ setIsCustomModel ( true )
173+ setOpen ( false )
174+ } } >
175+ + Add custom model
176+ </ CommandItem >
177+ </ CommandGroup >
178+ ) }
107179 </ CommandList >
108180 </ Command >
109181 </ PopoverContent >
@@ -125,6 +197,28 @@ export const ModelPicker = ({
125197 < VSCodeLink onClick = { ( ) => onSelect ( recommendedModel ) } > { recommendedModel } .</ VSCodeLink >
126198 You can also try searching "free" for no-cost options currently available.
127199 </ p >
200+ { allowCustomModel && isCustomModel && (
201+ < div className = "fixed inset-0 bg-black/50 flex items-center justify-center z-50" >
202+ < div className = "bg-[var(--vscode-editor-background)] p-6 rounded-lg w-96" >
203+ < h3 className = "text-lg font-semibold mb-4" > Add Custom Model</ h3 >
204+ < input
205+ type = "text"
206+ className = "w-full p-2 mb-4 bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border border-[var(--vscode-input-border)] rounded"
207+ placeholder = "Enter model ID"
208+ value = { customModelId }
209+ onChange = { ( e ) => setCustomModelId ( e . target . value ) }
210+ />
211+ < div className = "flex justify-end gap-2" >
212+ < Button variant = "secondary" onClick = { ( ) => setIsCustomModel ( false ) } >
213+ Cancel
214+ </ Button >
215+ < Button onClick = { ( ) => onSelectCustomModel ( customModelId ) } disabled = { ! customModelId . trim ( ) } >
216+ Add
217+ </ Button >
218+ </ div >
219+ </ div >
220+ </ div >
221+ ) }
128222 </ >
129223 )
130224}
0 commit comments