@@ -258,16 +258,8 @@ export function getProviderName(ownedBy: string): string {
258258 return PROVIDER_NAMES [ ownedBy ] ?? ownedBy ;
259259}
260260
261- // Sort key for ordering models oldest-to-newest in pickers. The model menu
262- // opens upward (side="top"), so the last item sits closest to the trigger —
263- // sorting ascending by this key puts the newest model right under the user's
264- // cursor. The key is the version embedded in the model id, e.g.
265- // "claude-sonnet-4-6" -> 4006, "claude-opus-4-8" -> 4008, "claude-fable-5" ->
266- // 5000; a higher number means a newer model. An id with no recognisable
267- // version (a brand-new or unexpected release) ranks as newest so it still
268- // surfaces at the end rather than at an arbitrary gateway-determined position.
269- // Only the first version group is read, so a trailing date suffix (e.g.
270- // "-20251001") is ignored; the minor component is assumed to be < 1000.
261+ // Version embedded in the model id, e.g. "claude-opus-4-8" -> 4008. Ids with no
262+ // recognisable version rank newest. A trailing date suffix is ignored.
271263export function getClaudeModelRecency ( modelId : string ) : number {
272264 const match = modelId . toLowerCase ( ) . match ( / - ( \d + ) (?: [ - . ] ( \d + ) ) ? / ) ;
273265 if ( ! match ) return Number . MAX_SAFE_INTEGER ;
@@ -276,15 +268,48 @@ export function getClaudeModelRecency(modelId: string): number {
276268 return major * 1000 + minor ;
277269}
278270
271+ // Families ordered most-capable first; unknown families sort last.
272+ const MODEL_FAMILY_ORDER = [ "fable" , "opus" , "sonnet" , "haiku" ] ;
273+
274+ function getModelFamilyRank ( modelId : string ) : number {
275+ const id = modelId . toLowerCase ( ) ;
276+ const index = MODEL_FAMILY_ORDER . findIndex ( ( family ) => id . includes ( family ) ) ;
277+ return index === - 1 ? MODEL_FAMILY_ORDER . length : index ;
278+ }
279+
280+ // Group by family, then newest version first within each family.
281+ export function compareModelsForPicker ( a : string , b : string ) : number {
282+ const familyDiff = getModelFamilyRank ( a ) - getModelFamilyRank ( b ) ;
283+ if ( familyDiff !== 0 ) return familyDiff ;
284+ return getClaudeModelRecency ( b ) - getClaudeModelRecency ( a ) ;
285+ }
286+
279287const PROVIDER_PREFIXES = [ "anthropic/" , "openai/" , "google-vertex/" ] ;
280288
289+ const KNOWN_ACRONYMS = new Set ( [ "gpt" , "glm" ] ) ;
290+
291+ // For a known acronym, uppercase it, keep the version attached, and title-case
292+ // any suffix: "gpt-5.6-sol" -> "GPT-5.6 Sol", "glm-5.2" -> "GLM-5.2". Other ids
293+ // stay lowercase to avoid mangling ordinary names (e.g. "llama-3.1-8b").
294+ function formatProviderModelName ( modelId : string ) : string {
295+ const [ acronym , version , ...suffix ] = modelId . split ( "-" ) ;
296+ if ( ! KNOWN_ACRONYMS . has ( acronym . toLowerCase ( ) ) ) return modelId . toLowerCase ( ) ;
297+ const head = version
298+ ? `${ acronym . toUpperCase ( ) } -${ version } `
299+ : acronym . toUpperCase ( ) ;
300+ const tail = suffix . map (
301+ ( word ) => word . charAt ( 0 ) . toUpperCase ( ) + word . slice ( 1 ) . toLowerCase ( ) ,
302+ ) ;
303+ return [ head , ...tail ] . join ( " " ) ;
304+ }
305+
281306export function formatGatewayModelName ( model : GatewayModel ) : string {
282307 if ( isCloudflareModel ( model ) ) {
283- return ( model . id . split ( "/" ) . pop ( ) ?? model . id ) . toLowerCase ( ) ;
308+ return formatProviderModelName ( model . id . split ( "/" ) . pop ( ) ?? model . id ) ;
284309 }
285310
286311 if ( isOpenAIModel ( model ) ) {
287- return stripProviderPrefix ( model . id ) . toLowerCase ( ) ;
312+ return formatProviderModelName ( stripProviderPrefix ( model . id ) ) ;
288313 }
289314
290315 return formatModelId ( model . id ) ;
0 commit comments