Skip to content

Commit c864a92

Browse files
authored
improvement(models): sort model dropdown by latest release date within each provider (#5099)
* improvement(models): sort model dropdown by latest release date within each provider * fix(models): preserve input provider order and build catalog index once
1 parent 73c73ff commit c864a92

3 files changed

Lines changed: 173 additions & 1 deletion

File tree

apps/sim/blocks/utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
getHostedModels,
1414
getProviderIcon,
1515
getProviderModels,
16+
orderModelIdsByReleaseDate,
1617
} from '@/providers/models'
1718
import { useProvidersStore } from '@/stores/providers/store'
1819

@@ -48,7 +49,7 @@ export const SERVICE_ACCOUNT_SUBBLOCKS: SubBlockConfig[] = [
4849
*/
4950
export function getModelOptions() {
5051
const providersState = useProvidersStore.getState()
51-
const baseModels = providersState.providers.base.models
52+
const baseModels = orderModelIdsByReleaseDate(providersState.providers.base.models)
5253
const ollamaModels = providersState.providers.ollama.models
5354
const ollamaCloudModels = providersState.providers['ollama-cloud'].models
5455
const vllmModels = providersState.providers.vllm.models

apps/sim/providers/models.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import {
6+
getBaseModelProviders,
7+
orderModelIdsByReleaseDate,
8+
PROVIDER_DEFINITIONS,
9+
} from '@/providers/models'
10+
11+
/** Maps a lowercased model ID to its provider's index in the catalog. */
12+
const PROVIDER_INDEX_BY_MODEL = new Map<string, number>()
13+
/** Maps a lowercased model ID to its release time (ms), or null when undated. */
14+
const RELEASE_TIME_BY_MODEL = new Map<string, number | null>()
15+
for (const [providerIndex, provider] of Object.values(PROVIDER_DEFINITIONS).entries()) {
16+
for (const model of provider.models) {
17+
const id = model.id.toLowerCase()
18+
PROVIDER_INDEX_BY_MODEL.set(id, providerIndex)
19+
RELEASE_TIME_BY_MODEL.set(id, model.releaseDate ? Date.parse(model.releaseDate) : null)
20+
}
21+
}
22+
23+
describe('orderModelIdsByReleaseDate', () => {
24+
it('keeps provider grouping order intact', () => {
25+
const ordered = orderModelIdsByReleaseDate(Object.keys(getBaseModelProviders()))
26+
let lastProviderIndex = -1
27+
const seenProviders = new Set<number>()
28+
for (const id of ordered) {
29+
const providerIndex = PROVIDER_INDEX_BY_MODEL.get(id.toLowerCase())
30+
expect(providerIndex).toBeDefined()
31+
// A provider's models must form one contiguous run: once we leave a provider
32+
// we never return to it.
33+
if (providerIndex !== lastProviderIndex) {
34+
expect(seenProviders.has(providerIndex as number)).toBe(false)
35+
seenProviders.add(providerIndex as number)
36+
lastProviderIndex = providerIndex as number
37+
}
38+
}
39+
})
40+
41+
it('sorts models within a provider newest-first by release date', () => {
42+
const ordered = orderModelIdsByReleaseDate(Object.keys(getBaseModelProviders()))
43+
for (let i = 1; i < ordered.length; i++) {
44+
const prev = ordered[i - 1].toLowerCase()
45+
const curr = ordered[i].toLowerCase()
46+
if (PROVIDER_INDEX_BY_MODEL.get(prev) !== PROVIDER_INDEX_BY_MODEL.get(curr)) continue
47+
48+
const prevTime = RELEASE_TIME_BY_MODEL.get(prev)
49+
const currTime = RELEASE_TIME_BY_MODEL.get(curr)
50+
// Dated models precede undated ones; among dated models, newer precedes older.
51+
if (prevTime == null) {
52+
expect(currTime).toBeNull()
53+
} else if (currTime != null) {
54+
expect(prevTime).toBeGreaterThanOrEqual(currTime)
55+
}
56+
}
57+
})
58+
59+
it('preserves the cross-provider grouping order given in the input', () => {
60+
// Pick the first model of two different providers and feed the second provider
61+
// first; the helper must keep that provider's group ahead of the other.
62+
const byProvider = new Map<number, string[]>()
63+
for (const id of Object.keys(getBaseModelProviders())) {
64+
const providerIndex = PROVIDER_INDEX_BY_MODEL.get(id.toLowerCase()) as number
65+
const bucket = byProvider.get(providerIndex) ?? []
66+
bucket.push(id)
67+
byProvider.set(providerIndex, bucket)
68+
}
69+
const providerIndexes = [...byProvider.keys()]
70+
expect(providerIndexes.length).toBeGreaterThanOrEqual(2)
71+
const [firstProvider, secondProvider] = providerIndexes
72+
const fromFirst = byProvider.get(firstProvider) as string[]
73+
const fromSecond = byProvider.get(secondProvider) as string[]
74+
75+
// Input order intentionally leads with the second provider.
76+
const input = [fromSecond[0], fromFirst[0]]
77+
const ordered = orderModelIdsByReleaseDate(input)
78+
expect(PROVIDER_INDEX_BY_MODEL.get(ordered[0].toLowerCase())).toBe(secondProvider)
79+
expect(PROVIDER_INDEX_BY_MODEL.get(ordered[1].toLowerCase())).toBe(firstProvider)
80+
})
81+
82+
it('places unknown model IDs last, preserving their input order', () => {
83+
const known = Object.keys(getBaseModelProviders())[0]
84+
const ordered = orderModelIdsByReleaseDate(['mystery-a', known, 'mystery-b'])
85+
expect(ordered[0]).toBe(known)
86+
expect(ordered.slice(1)).toEqual(['mystery-a', 'mystery-b'])
87+
})
88+
89+
it('is case-insensitive when matching catalog IDs', () => {
90+
const id = Object.keys(getBaseModelProviders())[0]
91+
const ordered = orderModelIdsByReleaseDate([id.toUpperCase()])
92+
expect(ordered).toEqual([id.toUpperCase()])
93+
})
94+
95+
it('returns an empty array for empty input', () => {
96+
expect(orderModelIdsByReleaseDate([])).toEqual([])
97+
})
98+
99+
it('does not add or drop any IDs', () => {
100+
const input = Object.keys(getBaseModelProviders())
101+
const ordered = orderModelIdsByReleaseDate(input)
102+
expect([...ordered].sort()).toEqual([...input].sort())
103+
})
104+
})

apps/sim/providers/models.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3047,6 +3047,73 @@ export function getProviderModels(providerId: string): string[] {
30473047
return PROVIDER_DEFINITIONS[providerId]?.models.map((m) => m.id) || []
30483048
}
30493049

3050+
interface ModelCatalogEntry {
3051+
providerId: string
3052+
declIndex: number
3053+
releaseTime: number
3054+
}
3055+
3056+
/**
3057+
* Lowercased model ID → catalog position metadata, built once from the static
3058+
* provider catalog. Dynamic providers contribute nothing here because their model
3059+
* lists are populated at runtime (not at module load), and only catalog models are
3060+
* ever reordered by release date.
3061+
*/
3062+
const MODEL_CATALOG_INDEX: Map<string, ModelCatalogEntry> = new Map(
3063+
Object.entries(PROVIDER_DEFINITIONS).flatMap(([providerId, provider]) =>
3064+
provider.models.map((model, declIndex): [string, ModelCatalogEntry] => {
3065+
const parsed = model.releaseDate ? Date.parse(model.releaseDate) : Number.NaN
3066+
return [
3067+
model.id.toLowerCase(),
3068+
{
3069+
providerId,
3070+
declIndex,
3071+
releaseTime: Number.isNaN(parsed) ? Number.NEGATIVE_INFINITY : parsed,
3072+
},
3073+
]
3074+
})
3075+
)
3076+
)
3077+
3078+
/**
3079+
* Reorders model IDs so that, within each provider, newer models (by release date)
3080+
* come first — while preserving the caller's existing provider grouping order. The
3081+
* relative order of providers is taken from the order they first appear in `modelIds`,
3082+
* so the cross-provider layout the user already sees is never reshuffled.
3083+
*
3084+
* Models without a known release date keep their declaration order and sort after
3085+
* dated models within the same provider. IDs not found in the catalog (e.g.
3086+
* dynamically-discovered provider models) are left in their original order at the end.
3087+
*/
3088+
export function orderModelIdsByReleaseDate(modelIds: string[]): string[] {
3089+
const groups = new Map<string, string[]>()
3090+
const unknown: string[] = []
3091+
3092+
for (const id of modelIds) {
3093+
const meta = MODEL_CATALOG_INDEX.get(id.toLowerCase())
3094+
if (!meta) {
3095+
unknown.push(id)
3096+
continue
3097+
}
3098+
const bucket = groups.get(meta.providerId)
3099+
if (bucket) bucket.push(id)
3100+
else groups.set(meta.providerId, [id])
3101+
}
3102+
3103+
const ordered: string[] = []
3104+
for (const bucket of groups.values()) {
3105+
bucket.sort((a, b) => {
3106+
const ma = MODEL_CATALOG_INDEX.get(a.toLowerCase())!
3107+
const mb = MODEL_CATALOG_INDEX.get(b.toLowerCase())!
3108+
if (ma.releaseTime !== mb.releaseTime) return mb.releaseTime - ma.releaseTime
3109+
return ma.declIndex - mb.declIndex
3110+
})
3111+
ordered.push(...bucket)
3112+
}
3113+
ordered.push(...unknown)
3114+
return ordered
3115+
}
3116+
30503117
export const DYNAMIC_MODEL_PROVIDERS = [
30513118
'ollama',
30523119
'ollama-cloud',

0 commit comments

Comments
 (0)