@@ -16,6 +16,7 @@ export const VENDOR_ENTRY_TEMPLATE_ID = 'vendor.entry.template';
1616const wordFilter = or ( matchesBaseContiguousSubString , matchesWords ) ;
1717const CAPABILITY_REGEX = / @ c a p a b i l i t y : \s * ( [ ^ \s ] + ) / gi;
1818const VISIBLE_REGEX = / @ v i s i b l e : \s * ( t r u e | f a l s e ) / i;
19+ const PROVIDER_REGEX = / @ p r o v i d e r : \s * ( ( " .+ ?" ) | ( [ ^ \s ] + ) ) / gi;
1920
2021export const SEARCH_SUGGESTIONS = {
2122 FILTER_TYPES : [
@@ -54,6 +55,7 @@ export interface IModelItemEntry {
5455 templateId : string ;
5556 providerMatches ?: IMatch [ ] ;
5657 modelNameMatches ?: IMatch [ ] ;
58+ modelIdMatches ?: IMatch [ ] ;
5759 capabilityMatches ?: string [ ] ;
5860}
5961
@@ -111,92 +113,154 @@ export class ChatModelsViewModel extends EditorModel {
111113
112114 filter ( searchValue : string ) : readonly IViewModelEntry [ ] {
113115 this . searchValue = searchValue ;
116+ const filtered = this . filterModels ( this . modelEntries , searchValue ) ;
117+ this . splice ( 0 , this . _viewModelEntries . length , filtered ) ;
118+ return this . viewModelEntries ;
119+ }
114120
115- let modelEntries = this . modelEntries ;
116- const capabilityMatchesMap = new Map < string , string [ ] > ( ) ;
121+ private filterModels ( modelEntries : IModelEntry [ ] , searchValue : string ) : ( IVendorItemEntry | IModelItemEntry ) [ ] {
122+ let visible : boolean | undefined ;
117123
118124 const visibleMatches = VISIBLE_REGEX . exec ( searchValue ) ;
119125 if ( visibleMatches && visibleMatches [ 1 ] ) {
120- const visible = visibleMatches [ 1 ] . toLowerCase ( ) === 'true' ;
121- modelEntries = this . filterByVisible ( modelEntries , visible ) ;
126+ visible = visibleMatches [ 1 ] . toLowerCase ( ) === 'true' ;
122127 searchValue = searchValue . replace ( VISIBLE_REGEX , '' ) ;
123128 }
124129
125130 const providerNames : string [ ] = [ ] ;
126- let match : RegExpExecArray | null ;
127-
128- const providerRegexGlobal = / @ p r o v i d e r : \s * ( ( " .+ ?" ) | ( [ ^ \s ] + ) ) / gi;
129- while ( ( match = providerRegexGlobal . exec ( searchValue ) ) !== null ) {
130- const providerName = match [ 2 ] ? match [ 2 ] . substring ( 1 , match [ 2 ] . length - 1 ) : match [ 3 ] ;
131+ let providerMatch : RegExpExecArray | null ;
132+ PROVIDER_REGEX . lastIndex = 0 ;
133+ while ( ( providerMatch = PROVIDER_REGEX . exec ( searchValue ) ) !== null ) {
134+ const providerName = providerMatch [ 2 ] ? providerMatch [ 2 ] . substring ( 1 , providerMatch [ 2 ] . length - 1 ) : providerMatch [ 3 ] ;
131135 providerNames . push ( providerName ) ;
132136 }
133-
134- // Apply provider filter with OR logic if multiple providers
135137 if ( providerNames . length > 0 ) {
136- modelEntries = this . filterByProviders ( modelEntries , providerNames ) ;
137- searchValue = searchValue . replace ( / @ p r o v i d e r : \s * ( ( " .+ ?" ) | ( [ ^ \s ] + ) ) / gi, '' ) . replace ( / @ v e n d o r : \s * ( ( " .+ ?" ) | ( [ ^ \s ] + ) ) / gi, '' ) ;
138+ searchValue = searchValue . replace ( PROVIDER_REGEX , '' ) ;
138139 }
139140
140- // Apply capability filters with AND logic if multiple capabilities
141- const capabilityNames : string [ ] = [ ] ;
141+ const capabilities : string [ ] = [ ] ;
142142 let capabilityMatch : RegExpExecArray | null ;
143-
143+ CAPABILITY_REGEX . lastIndex = 0 ;
144144 while ( ( capabilityMatch = CAPABILITY_REGEX . exec ( searchValue ) ) !== null ) {
145- capabilityNames . push ( capabilityMatch [ 1 ] . toLowerCase ( ) ) ;
145+ capabilities . push ( capabilityMatch [ 1 ] . toLowerCase ( ) ) ;
146146 }
147-
148- if ( capabilityNames . length > 0 ) {
149- const filteredEntries = this . filterByCapabilities ( modelEntries , capabilityNames ) ;
150- modelEntries = [ ] ;
151- for ( const { entry, matchedCapabilities } of filteredEntries ) {
152- modelEntries . push ( entry ) ;
153- capabilityMatchesMap . set ( ChatModelsViewModel . getId ( entry ) , matchedCapabilities ) ;
154- }
155- searchValue = searchValue . replace ( / @ c a p a b i l i t y : \s * ( [ ^ \s ] + ) / gi, '' ) ;
147+ if ( capabilities . length > 0 ) {
148+ searchValue = searchValue . replace ( CAPABILITY_REGEX , '' ) ;
156149 }
157150
151+ const quoteAtFirstChar = searchValue . charAt ( 0 ) === '"' ;
152+ const quoteAtLastChar = searchValue . charAt ( searchValue . length - 1 ) === '"' ;
153+ const completeMatch = quoteAtFirstChar && quoteAtLastChar ;
154+ if ( quoteAtFirstChar ) {
155+ searchValue = searchValue . substring ( 1 ) ;
156+ }
157+ if ( quoteAtLastChar ) {
158+ searchValue = searchValue . substring ( 0 , searchValue . length - 1 ) ;
159+ }
158160 searchValue = searchValue . trim ( ) ;
159- const filtered = searchValue ? this . filterByText ( modelEntries , searchValue , capabilityMatchesMap ) : this . toEntries ( modelEntries , capabilityMatchesMap ) ;
160161
161- this . splice ( 0 , this . _viewModelEntries . length , filtered ) ;
162- return this . viewModelEntries ;
163- }
162+ const isFiltering = searchValue !== '' || capabilities . length > 0 || providerNames . length > 0 || visible !== undefined ;
164163
165- private filterByProviders ( modelEntries : IModelEntry [ ] , providers : string [ ] ) : IModelEntry [ ] {
166- const lowerProviders = providers . map ( p => p . toLowerCase ( ) . trim ( ) ) ;
167- return modelEntries . filter ( m =>
168- lowerProviders . some ( provider =>
169- m . vendor . toLowerCase ( ) === provider ||
170- m . vendorDisplayName . toLowerCase ( ) === provider
171- )
172- ) ;
173- }
174-
175- private filterByVisible ( modelEntries : IModelEntry [ ] , visible : boolean ) : IModelEntry [ ] {
176- return modelEntries . filter ( m => ( m . metadata . isUserSelectable ?? false ) === visible ) ;
177- }
164+ const result : ( IVendorItemEntry | IModelItemEntry ) [ ] = [ ] ;
165+ const words = searchValue . split ( ' ' ) ;
166+ const allVendors = new Set ( this . modelEntries . map ( m => m . vendor ) ) ;
167+ const showHeaders = allVendors . size > 1 ;
168+ const addedVendors = new Set < string > ( ) ;
169+ const lowerProviders = providerNames . map ( p => p . toLowerCase ( ) . trim ( ) ) ;
178170
179- private filterByCapabilities ( modelEntries : IModelEntry [ ] , capabilities : string [ ] ) : { entry : IModelEntry ; matchedCapabilities : string [ ] } [ ] {
180- const result : { entry : IModelEntry ; matchedCapabilities : string [ ] } [ ] = [ ] ;
181- for ( const m of modelEntries ) {
182- if ( ! m . metadata . capabilities ) {
171+ for ( const modelEntry of modelEntries ) {
172+ if ( ! isFiltering && showHeaders && this . collapsedVendors . has ( modelEntry . vendor ) ) {
173+ if ( ! addedVendors . has ( modelEntry . vendor ) ) {
174+ const vendorInfo = this . languageModelsService . getVendors ( ) . find ( v => v . vendor === modelEntry . vendor ) ;
175+ result . push ( {
176+ type : 'vendor' ,
177+ id : `vendor-${ modelEntry . vendor } ` ,
178+ vendorEntry : {
179+ vendor : modelEntry . vendor ,
180+ vendorDisplayName : modelEntry . vendorDisplayName ,
181+ managementCommand : vendorInfo ?. managementCommand
182+ } ,
183+ templateId : VENDOR_ENTRY_TEMPLATE_ID ,
184+ collapsed : true
185+ } ) ;
186+ addedVendors . add ( modelEntry . vendor ) ;
187+ }
183188 continue ;
184189 }
185- const allMatchedCapabilities : string [ ] = [ ] ;
186- let matchesAll = true ;
187-
188- for ( const capability of capabilities ) {
189- const matchedForThisCapability = this . getMatchingCapabilities ( m , capability ) ;
190- if ( matchedForThisCapability . length === 0 ) {
191- matchesAll = false ;
192- break ;
190+
191+ if ( visible !== undefined ) {
192+ if ( ( modelEntry . metadata . isUserSelectable ?? false ) !== visible ) {
193+ continue ;
194+ }
195+ }
196+
197+ if ( lowerProviders . length > 0 ) {
198+ const matchesProvider = lowerProviders . some ( provider =>
199+ modelEntry . vendor . toLowerCase ( ) === provider ||
200+ modelEntry . vendorDisplayName . toLowerCase ( ) === provider
201+ ) ;
202+ if ( ! matchesProvider ) {
203+ continue ;
193204 }
194- allMatchedCapabilities . push ( ...matchedForThisCapability ) ;
195205 }
196206
197- if ( matchesAll ) {
198- result . push ( { entry : m , matchedCapabilities : distinct ( allMatchedCapabilities ) } ) ;
207+ // Filter by capabilities
208+ let matchedCapabilities : string [ ] = [ ] ;
209+ if ( capabilities . length > 0 ) {
210+ if ( ! modelEntry . metadata . capabilities ) {
211+ continue ;
212+ }
213+ let matchesAll = true ;
214+ for ( const capability of capabilities ) {
215+ const matchedForThisCapability = this . getMatchingCapabilities ( modelEntry , capability ) ;
216+ if ( matchedForThisCapability . length === 0 ) {
217+ matchesAll = false ;
218+ break ;
219+ }
220+ matchedCapabilities . push ( ...matchedForThisCapability ) ;
221+ }
222+ if ( ! matchesAll ) {
223+ continue ;
224+ }
225+ matchedCapabilities = distinct ( matchedCapabilities ) ;
226+ }
227+
228+ // Filter by text
229+ let modelMatches : ModelItemMatches | undefined ;
230+ if ( searchValue ) {
231+ modelMatches = new ModelItemMatches ( modelEntry , searchValue , words , completeMatch ) ;
232+ if ( ! modelMatches . modelNameMatches && ! modelMatches . modelIdMatches && ! modelMatches . providerMatches && ! modelMatches . capabilityMatches ) {
233+ continue ;
234+ }
235+ }
236+
237+ if ( showHeaders && ! addedVendors . has ( modelEntry . vendor ) ) {
238+ const vendorInfo = this . languageModelsService . getVendors ( ) . find ( v => v . vendor === modelEntry . vendor ) ;
239+ result . push ( {
240+ type : 'vendor' ,
241+ id : `vendor-${ modelEntry . vendor } ` ,
242+ vendorEntry : {
243+ vendor : modelEntry . vendor ,
244+ vendorDisplayName : modelEntry . vendorDisplayName ,
245+ managementCommand : vendorInfo ?. managementCommand
246+ } ,
247+ templateId : VENDOR_ENTRY_TEMPLATE_ID ,
248+ collapsed : false
249+ } ) ;
250+ addedVendors . add ( modelEntry . vendor ) ;
199251 }
252+
253+ const modelId = ChatModelsViewModel . getId ( modelEntry ) ;
254+ result . push ( {
255+ type : 'model' ,
256+ id : modelId ,
257+ templateId : MODEL_ENTRY_TEMPLATE_ID ,
258+ modelEntry,
259+ modelNameMatches : modelMatches ?. modelNameMatches || undefined ,
260+ modelIdMatches : modelMatches ?. modelIdMatches || undefined ,
261+ providerMatches : modelMatches ?. providerMatches || undefined ,
262+ capabilityMatches : matchedCapabilities . length ? matchedCapabilities : undefined ,
263+ } ) ;
200264 }
201265 return result ;
202266 }
@@ -239,42 +303,6 @@ export class ChatModelsViewModel extends EditorModel {
239303 return matchedCapabilities ;
240304 }
241305
242- private filterByText ( modelEntries : IModelEntry [ ] , searchValue : string , capabilityMatchesMap : Map < string , string [ ] > ) : IModelItemEntry [ ] {
243- const quoteAtFirstChar = searchValue . charAt ( 0 ) === '"' ;
244- const quoteAtLastChar = searchValue . charAt ( searchValue . length - 1 ) === '"' ;
245- const completeMatch = quoteAtFirstChar && quoteAtLastChar ;
246- if ( quoteAtFirstChar ) {
247- searchValue = searchValue . substring ( 1 ) ;
248- }
249- if ( quoteAtLastChar ) {
250- searchValue = searchValue . substring ( 0 , searchValue . length - 1 ) ;
251- }
252- searchValue = searchValue . trim ( ) ;
253-
254- const result : IModelItemEntry [ ] = [ ] ;
255- const words = searchValue . split ( ' ' ) ;
256-
257- for ( const modelEntry of modelEntries ) {
258- const modelMatches = new ModelItemMatches ( modelEntry , searchValue , words , completeMatch ) ;
259- if ( modelMatches . modelNameMatches
260- || modelMatches . providerMatches
261- || modelMatches . capabilityMatches
262- ) {
263- const modelId = ChatModelsViewModel . getId ( modelEntry ) ;
264- result . push ( {
265- type : 'model' ,
266- id : modelId ,
267- templateId : MODEL_ENTRY_TEMPLATE_ID ,
268- modelEntry,
269- modelNameMatches : modelMatches . modelNameMatches || undefined ,
270- providerMatches : modelMatches . providerMatches || undefined ,
271- capabilityMatches : capabilityMatchesMap . get ( modelId ) ,
272- } ) ;
273- }
274- }
275- return result ;
276- }
277-
278306 getVendors ( ) : IUserFriendlyLanguageModel [ ] {
279307 return [ ...this . languageModelsService . getVendors ( ) ] . sort ( ( a , b ) => {
280308 if ( a . vendor === 'copilot' ) { return - 1 ; }
@@ -342,62 +370,28 @@ export class ChatModelsViewModel extends EditorModel {
342370 this . filter ( this . searchValue ) ;
343371 }
344372
345- getConfiguredVendors ( ) : IVendorItemEntry [ ] {
346- return this . toEntries ( this . modelEntries , new Map ( ) , true ) as IVendorItemEntry [ ] ;
347- }
348-
349- private toEntries ( modelEntries : IModelEntry [ ] , capabilityMatchesMap : Map < string , string [ ] > , excludeModels ?: boolean ) : ( IVendorItemEntry | IModelItemEntry ) [ ] {
350- const result : ( IVendorItemEntry | IModelItemEntry ) [ ] = [ ] ;
351- const vendorMap = new Map < string , IModelEntry [ ] > ( ) ;
352-
353- for ( const modelEntry of modelEntries ) {
354- const models = vendorMap . get ( modelEntry . vendor ) || [ ] ;
355- models . push ( modelEntry ) ;
356- vendorMap . set ( modelEntry . vendor , models ) ;
357- }
358-
359- const showVendorHeaders = vendorMap . size > 1 ;
360-
361- for ( const [ vendor , models ] of vendorMap ) {
362- const firstModel = models [ 0 ] ;
363- const isCollapsed = this . collapsedVendors . has ( vendor ) ;
364- const vendorInfo = this . languageModelsService . getVendors ( ) . find ( v => v . vendor === vendor ) ;
365-
366- if ( showVendorHeaders ) {
373+ getConfiguredVendors ( ) : IVendorEntry [ ] {
374+ const result : IVendorEntry [ ] = [ ] ;
375+ const seenVendors = new Set < string > ( ) ;
376+ for ( const modelEntry of this . modelEntries ) {
377+ if ( ! seenVendors . has ( modelEntry . vendor ) ) {
378+ seenVendors . add ( modelEntry . vendor ) ;
379+ const vendorInfo = this . languageModelsService . getVendors ( ) . find ( v => v . vendor === modelEntry . vendor ) ;
367380 result . push ( {
368- type : 'vendor' ,
369- id : `vendor-${ vendor } ` ,
370- vendorEntry : {
371- vendor : firstModel . vendor ,
372- vendorDisplayName : firstModel . vendorDisplayName ,
373- managementCommand : vendorInfo ?. managementCommand
374- } ,
375- templateId : VENDOR_ENTRY_TEMPLATE_ID ,
376- collapsed : isCollapsed
381+ vendor : modelEntry . vendor ,
382+ vendorDisplayName : modelEntry . vendorDisplayName ,
383+ managementCommand : vendorInfo ?. managementCommand
377384 } ) ;
378385 }
379-
380- if ( ! excludeModels && ( ! isCollapsed || ! showVendorHeaders ) ) {
381- for ( const modelEntry of models ) {
382- const modelId = ChatModelsViewModel . getId ( modelEntry ) ;
383- result . push ( {
384- type : 'model' ,
385- id : modelId ,
386- modelEntry,
387- templateId : MODEL_ENTRY_TEMPLATE_ID ,
388- capabilityMatches : capabilityMatchesMap . get ( modelId ) ,
389- } ) ;
390- }
391- }
392386 }
393-
394387 return result ;
395388 }
396389}
397390
398391class ModelItemMatches {
399392
400393 readonly modelNameMatches : IMatch [ ] | null = null ;
394+ readonly modelIdMatches : IMatch [ ] | null = null ;
401395 readonly providerMatches : IMatch [ ] | null = null ;
402396 readonly capabilityMatches : IMatch [ ] | null = null ;
403397
@@ -408,10 +402,7 @@ class ModelItemMatches {
408402 this . matches ( searchValue , modelEntry . metadata . name , ( word , wordToMatchAgainst ) => matchesWords ( word , wordToMatchAgainst , true ) , words ) :
409403 null ;
410404
411- // Match against model identifier
412- if ( ! this . modelNameMatches ) {
413- this . modelNameMatches = this . matches ( searchValue , modelEntry . identifier , or ( matchesWords , matchesCamelCase ) , words ) ;
414- }
405+ this . modelIdMatches = this . matches ( searchValue , modelEntry . identifier , or ( matchesWords , matchesCamelCase ) , words ) ;
415406
416407 // Match against vendor display name
417408 this . providerMatches = this . matches ( searchValue , modelEntry . vendorDisplayName , ( word , wordToMatchAgainst ) => matchesWords ( word , wordToMatchAgainst , true ) , words ) ;
0 commit comments