From 68050290ad1f58404709524990295b1e6c8fa761 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 16 Jun 2026 11:05:36 -0700 Subject: [PATCH 1/2] improvement(models): sort model dropdown by latest release date within each provider --- apps/sim/blocks/utils.ts | 3 +- apps/sim/providers/models.test.ts | 81 +++++++++++++++++++++++++++++++ apps/sim/providers/models.ts | 42 ++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 apps/sim/providers/models.test.ts diff --git a/apps/sim/blocks/utils.ts b/apps/sim/blocks/utils.ts index 861c0b12de..8fc80b1009 100644 --- a/apps/sim/blocks/utils.ts +++ b/apps/sim/blocks/utils.ts @@ -13,6 +13,7 @@ import { getHostedModels, getProviderIcon, getProviderModels, + orderModelIdsByReleaseDate, } from '@/providers/models' import { useProvidersStore } from '@/stores/providers/store' @@ -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 diff --git a/apps/sim/providers/models.test.ts b/apps/sim/providers/models.test.ts new file mode 100644 index 0000000000..fe64fda0b1 --- /dev/null +++ b/apps/sim/providers/models.test.ts @@ -0,0 +1,81 @@ +/** + * @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() +/** Maps a lowercased model ID to its release time (ms), or null when undated. */ +const RELEASE_TIME_BY_MODEL = new Map() +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() + 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('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()) + }) +}) diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index a827c32fe8..0323ca5f7e 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -3047,6 +3047,48 @@ export function getProviderModels(providerId: string): string[] { return PROVIDER_DEFINITIONS[providerId]?.models.map((m) => m.id) || [] } +/** + * Reorders catalog model IDs so that, within each provider, newer models (by + * release date) come first, while preserving the existing provider grouping order. + * + * 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 catalogIndex = new Map< + string, + { providerIndex: number; declIndex: number; releaseTime: number } + >() + Object.values(PROVIDER_DEFINITIONS).forEach((provider, providerIndex) => { + provider.models.forEach((model, declIndex) => { + const parsed = model.releaseDate ? Date.parse(model.releaseDate) : Number.NaN + catalogIndex.set(model.id.toLowerCase(), { + providerIndex, + declIndex, + releaseTime: Number.isNaN(parsed) ? Number.NEGATIVE_INFINITY : parsed, + }) + }) + }) + + return modelIds + .map((id, inputIndex) => ({ id, inputIndex, meta: catalogIndex.get(id.toLowerCase()) })) + .sort((a, b) => { + if (!a.meta || !b.meta) { + if (!a.meta && !b.meta) return a.inputIndex - b.inputIndex + return a.meta ? -1 : 1 + } + if (a.meta.providerIndex !== b.meta.providerIndex) { + return a.meta.providerIndex - b.meta.providerIndex + } + if (a.meta.releaseTime !== b.meta.releaseTime) { + return b.meta.releaseTime - a.meta.releaseTime + } + return a.meta.declIndex - b.meta.declIndex + }) + .map((entry) => entry.id) +} + export const DYNAMIC_MODEL_PROVIDERS = [ 'ollama', 'ollama-cloud', From 7ee4d9765a5954e9b4d7b4f635ead13c0150fbfd Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 16 Jun 2026 11:14:35 -0700 Subject: [PATCH 2/2] fix(models): preserve input provider order and build catalog index once --- apps/sim/providers/models.test.ts | 23 ++++++++ apps/sim/providers/models.ts | 89 ++++++++++++++++++++----------- 2 files changed, 80 insertions(+), 32 deletions(-) diff --git a/apps/sim/providers/models.test.ts b/apps/sim/providers/models.test.ts index fe64fda0b1..ca9af8a07c 100644 --- a/apps/sim/providers/models.test.ts +++ b/apps/sim/providers/models.test.ts @@ -56,6 +56,29 @@ describe('orderModelIdsByReleaseDate', () => { } }) + 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() + 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']) diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index 0323ca5f7e..b8fa2a2bae 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -3047,46 +3047,71 @@ 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 = 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 catalog model IDs so that, within each provider, newer models (by - * release date) come first, while preserving the existing provider grouping order. + * 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 catalogIndex = new Map< - string, - { providerIndex: number; declIndex: number; releaseTime: number } - >() - Object.values(PROVIDER_DEFINITIONS).forEach((provider, providerIndex) => { - provider.models.forEach((model, declIndex) => { - const parsed = model.releaseDate ? Date.parse(model.releaseDate) : Number.NaN - catalogIndex.set(model.id.toLowerCase(), { - providerIndex, - declIndex, - releaseTime: Number.isNaN(parsed) ? Number.NEGATIVE_INFINITY : parsed, - }) - }) - }) - - return modelIds - .map((id, inputIndex) => ({ id, inputIndex, meta: catalogIndex.get(id.toLowerCase()) })) - .sort((a, b) => { - if (!a.meta || !b.meta) { - if (!a.meta && !b.meta) return a.inputIndex - b.inputIndex - return a.meta ? -1 : 1 - } - if (a.meta.providerIndex !== b.meta.providerIndex) { - return a.meta.providerIndex - b.meta.providerIndex - } - if (a.meta.releaseTime !== b.meta.releaseTime) { - return b.meta.releaseTime - a.meta.releaseTime - } - return a.meta.declIndex - b.meta.declIndex + const groups = new Map() + 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 }) - .map((entry) => entry.id) + ordered.push(...bucket) + } + ordered.push(...unknown) + return ordered } export const DYNAMIC_MODEL_PROVIDERS = [