Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/sim/blocks/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getHostedModels,
getProviderIcon,
getProviderModels,
orderModelIdsByReleaseDate,
} from '@/providers/models'
import { useProvidersStore } from '@/stores/providers/store'

Expand Down Expand Up @@ -48,7 +49,7 @@ export const SERVICE_ACCOUNT_SUBBLOCKS: SubBlockConfig[] = [
*/
export function getModelOptions() {
const providersState = useProvidersStore.getState()
const baseModels = providersState.providers.base.models
const baseModels = orderModelIdsByReleaseDate(providersState.providers.base.models)
const ollamaModels = providersState.providers.ollama.models
const ollamaCloudModels = providersState.providers['ollama-cloud'].models
const vllmModels = providersState.providers.vllm.models
Expand Down
104 changes: 104 additions & 0 deletions apps/sim/providers/models.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import {
getBaseModelProviders,
orderModelIdsByReleaseDate,
PROVIDER_DEFINITIONS,
} from '@/providers/models'

/** Maps a lowercased model ID to its provider's index in the catalog. */
const PROVIDER_INDEX_BY_MODEL = new Map<string, number>()
/** Maps a lowercased model ID to its release time (ms), or null when undated. */
const RELEASE_TIME_BY_MODEL = new Map<string, number | null>()
for (const [providerIndex, provider] of Object.values(PROVIDER_DEFINITIONS).entries()) {
for (const model of provider.models) {
const id = model.id.toLowerCase()
PROVIDER_INDEX_BY_MODEL.set(id, providerIndex)
RELEASE_TIME_BY_MODEL.set(id, model.releaseDate ? Date.parse(model.releaseDate) : null)
}
}

describe('orderModelIdsByReleaseDate', () => {
it('keeps provider grouping order intact', () => {
const ordered = orderModelIdsByReleaseDate(Object.keys(getBaseModelProviders()))
let lastProviderIndex = -1
const seenProviders = new Set<number>()
for (const id of ordered) {
const providerIndex = PROVIDER_INDEX_BY_MODEL.get(id.toLowerCase())
expect(providerIndex).toBeDefined()
// A provider's models must form one contiguous run: once we leave a provider
// we never return to it.
if (providerIndex !== lastProviderIndex) {
expect(seenProviders.has(providerIndex as number)).toBe(false)
seenProviders.add(providerIndex as number)
lastProviderIndex = providerIndex as number
}
}
})

it('sorts models within a provider newest-first by release date', () => {
const ordered = orderModelIdsByReleaseDate(Object.keys(getBaseModelProviders()))
for (let i = 1; i < ordered.length; i++) {
const prev = ordered[i - 1].toLowerCase()
const curr = ordered[i].toLowerCase()
if (PROVIDER_INDEX_BY_MODEL.get(prev) !== PROVIDER_INDEX_BY_MODEL.get(curr)) continue

const prevTime = RELEASE_TIME_BY_MODEL.get(prev)
const currTime = RELEASE_TIME_BY_MODEL.get(curr)
// Dated models precede undated ones; among dated models, newer precedes older.
if (prevTime == null) {
expect(currTime).toBeNull()
} else if (currTime != null) {
expect(prevTime).toBeGreaterThanOrEqual(currTime)
}
}
})

it('preserves the cross-provider grouping order given in the input', () => {
// Pick the first model of two different providers and feed the second provider
// first; the helper must keep that provider's group ahead of the other.
const byProvider = new Map<number, string[]>()
for (const id of Object.keys(getBaseModelProviders())) {
const providerIndex = PROVIDER_INDEX_BY_MODEL.get(id.toLowerCase()) as number
const bucket = byProvider.get(providerIndex) ?? []
bucket.push(id)
byProvider.set(providerIndex, bucket)
}
const providerIndexes = [...byProvider.keys()]
expect(providerIndexes.length).toBeGreaterThanOrEqual(2)
const [firstProvider, secondProvider] = providerIndexes
const fromFirst = byProvider.get(firstProvider) as string[]
const fromSecond = byProvider.get(secondProvider) as string[]

// Input order intentionally leads with the second provider.
const input = [fromSecond[0], fromFirst[0]]
const ordered = orderModelIdsByReleaseDate(input)
expect(PROVIDER_INDEX_BY_MODEL.get(ordered[0].toLowerCase())).toBe(secondProvider)
expect(PROVIDER_INDEX_BY_MODEL.get(ordered[1].toLowerCase())).toBe(firstProvider)
})

it('places unknown model IDs last, preserving their input order', () => {
const known = Object.keys(getBaseModelProviders())[0]
const ordered = orderModelIdsByReleaseDate(['mystery-a', known, 'mystery-b'])
expect(ordered[0]).toBe(known)
expect(ordered.slice(1)).toEqual(['mystery-a', 'mystery-b'])
})

it('is case-insensitive when matching catalog IDs', () => {
const id = Object.keys(getBaseModelProviders())[0]
const ordered = orderModelIdsByReleaseDate([id.toUpperCase()])
expect(ordered).toEqual([id.toUpperCase()])
})

it('returns an empty array for empty input', () => {
expect(orderModelIdsByReleaseDate([])).toEqual([])
})

it('does not add or drop any IDs', () => {
const input = Object.keys(getBaseModelProviders())
const ordered = orderModelIdsByReleaseDate(input)
expect([...ordered].sort()).toEqual([...input].sort())
})
})
67 changes: 67 additions & 0 deletions apps/sim/providers/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3047,6 +3047,73 @@ export function getProviderModels(providerId: string): string[] {
return PROVIDER_DEFINITIONS[providerId]?.models.map((m) => m.id) || []
}

interface ModelCatalogEntry {
providerId: string
declIndex: number
releaseTime: number
}

/**
* Lowercased model ID → catalog position metadata, built once from the static
* provider catalog. Dynamic providers contribute nothing here because their model
* lists are populated at runtime (not at module load), and only catalog models are
* ever reordered by release date.
*/
const MODEL_CATALOG_INDEX: Map<string, ModelCatalogEntry> = new Map(
Object.entries(PROVIDER_DEFINITIONS).flatMap(([providerId, provider]) =>
provider.models.map((model, declIndex): [string, ModelCatalogEntry] => {
const parsed = model.releaseDate ? Date.parse(model.releaseDate) : Number.NaN
return [
model.id.toLowerCase(),
{
providerId,
declIndex,
releaseTime: Number.isNaN(parsed) ? Number.NEGATIVE_INFINITY : parsed,
},
]
})
)
)

/**
* Reorders model IDs so that, within each provider, newer models (by release date)
* come first — while preserving the caller's existing provider grouping order. The
* relative order of providers is taken from the order they first appear in `modelIds`,
* so the cross-provider layout the user already sees is never reshuffled.
*
* Models without a known release date keep their declaration order and sort after
* dated models within the same provider. IDs not found in the catalog (e.g.
* dynamically-discovered provider models) are left in their original order at the end.
*/
export function orderModelIdsByReleaseDate(modelIds: string[]): string[] {
const groups = new Map<string, string[]>()
const unknown: string[] = []

for (const id of modelIds) {
const meta = MODEL_CATALOG_INDEX.get(id.toLowerCase())
if (!meta) {
unknown.push(id)
continue
}
const bucket = groups.get(meta.providerId)
if (bucket) bucket.push(id)
else groups.set(meta.providerId, [id])
}

const ordered: string[] = []
for (const bucket of groups.values()) {
bucket.sort((a, b) => {
const ma = MODEL_CATALOG_INDEX.get(a.toLowerCase())!
const mb = MODEL_CATALOG_INDEX.get(b.toLowerCase())!
if (ma.releaseTime !== mb.releaseTime) return mb.releaseTime - ma.releaseTime
return ma.declIndex - mb.declIndex
})
ordered.push(...bucket)
}
ordered.push(...unknown)
return ordered
}

export const DYNAMIC_MODEL_PROVIDERS = [
'ollama',
'ollama-cloud',
Expand Down
Loading