d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
828 lines
24 KiB
TypeScript
828 lines
24 KiB
TypeScript
import type { ComponentType } from 'react'
|
|
import { type ModelCapabilities, PROVIDER_DEFINITIONS } from '@/providers/models'
|
|
|
|
const PROVIDER_PREFIXES: Record<string, string[]> = {
|
|
'azure-openai': ['azure/'],
|
|
'azure-anthropic': ['azure-anthropic/'],
|
|
vertex: ['vertex/'],
|
|
bedrock: ['bedrock/'],
|
|
cerebras: ['cerebras/'],
|
|
fireworks: ['fireworks/'],
|
|
together: ['together/'],
|
|
baseten: ['baseten/'],
|
|
'ollama-cloud': ['ollama-cloud/'],
|
|
groq: ['groq/'],
|
|
openrouter: ['openrouter/'],
|
|
vllm: ['vllm/'],
|
|
}
|
|
|
|
const TOKEN_REPLACEMENTS: Record<string, string> = {
|
|
ai: 'AI',
|
|
aws: 'AWS',
|
|
gpt: 'GPT',
|
|
oss: 'OSS',
|
|
llm: 'LLM',
|
|
xai: 'xAI',
|
|
openai: 'OpenAI',
|
|
anthropic: 'Anthropic',
|
|
azure: 'Azure',
|
|
gemini: 'Gemini',
|
|
vertex: 'Vertex',
|
|
groq: 'Groq',
|
|
mistral: 'Mistral',
|
|
deepseek: 'DeepSeek',
|
|
cerebras: 'Cerebras',
|
|
ollama: 'Ollama',
|
|
bedrock: 'Bedrock',
|
|
google: 'Google',
|
|
moonshotai: 'Moonshot AI',
|
|
qwen: 'Qwen',
|
|
glm: 'GLM',
|
|
kimi: 'Kimi',
|
|
nova: 'Nova',
|
|
llama: 'Llama',
|
|
meta: 'Meta',
|
|
cohere: 'Cohere',
|
|
amazon: 'Amazon',
|
|
opus: 'Opus',
|
|
sonnet: 'Sonnet',
|
|
haiku: 'Haiku',
|
|
flash: 'Flash',
|
|
preview: 'Preview',
|
|
latest: 'Latest',
|
|
mini: 'Mini',
|
|
nano: 'Nano',
|
|
pro: 'Pro',
|
|
plus: 'Plus',
|
|
plusplus: 'PlusPlus',
|
|
code: 'Code',
|
|
codex: 'Codex',
|
|
instant: 'Instant',
|
|
versatile: 'Versatile',
|
|
instruct: 'Instruct',
|
|
guard: 'Guard',
|
|
safeguard: 'Safeguard',
|
|
medium: 'Medium',
|
|
small: 'Small',
|
|
large: 'Large',
|
|
lite: 'Lite',
|
|
premier: 'Premier',
|
|
premierer: 'Premier',
|
|
micro: 'Micro',
|
|
reasoning: 'Reasoning',
|
|
non: 'Non',
|
|
distill: 'Distill',
|
|
chat: 'Chat',
|
|
text: 'Text',
|
|
embedding: 'Embedding',
|
|
router: 'Router',
|
|
}
|
|
|
|
const PRICE_NUMBER_FORMAT_3 = new Intl.NumberFormat('en-US', {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 3,
|
|
})
|
|
|
|
const PRICE_NUMBER_FORMAT_4 = new Intl.NumberFormat('en-US', {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 4,
|
|
})
|
|
|
|
const UPDATED_AT_DATE_FORMAT = new Intl.DateTimeFormat('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
})
|
|
|
|
export interface PricingInfo {
|
|
input: number
|
|
cachedInput?: number
|
|
output: number
|
|
updatedAt: string
|
|
}
|
|
|
|
export interface CatalogFaq {
|
|
question: string
|
|
answer: string
|
|
}
|
|
|
|
export interface CapabilityFact {
|
|
label: string
|
|
value: string
|
|
}
|
|
|
|
export interface CatalogModel {
|
|
id: string
|
|
slug: string
|
|
href: string
|
|
displayName: string
|
|
shortId: string
|
|
providerId: string
|
|
providerName: string
|
|
providerSlug: string
|
|
contextWindow: number | null
|
|
releaseDate: string | null
|
|
deprecated: boolean
|
|
pricing: PricingInfo
|
|
capabilities: ModelCapabilities
|
|
capabilityTags: string[]
|
|
summary: string
|
|
bestFor?: string
|
|
searchText: string
|
|
}
|
|
|
|
export interface CatalogProvider {
|
|
id: string
|
|
slug: string
|
|
href: string
|
|
name: string
|
|
description: string
|
|
summary: string
|
|
defaultModel: string
|
|
defaultModelDisplayName: string
|
|
icon?: ComponentType<{ className?: string }>
|
|
color?: string
|
|
isReseller: boolean
|
|
contextInformationAvailable: boolean
|
|
/** Max agent-block file attachment size in bytes when the provider exceeds the default. */
|
|
maxFileAttachmentBytes: number | null
|
|
providerCapabilityTags: string[]
|
|
modelCount: number
|
|
models: CatalogModel[]
|
|
featuredModels: CatalogModel[]
|
|
searchText: string
|
|
}
|
|
|
|
export function formatTokenCount(value?: number | null): string {
|
|
if (value == null) {
|
|
return 'Unknown'
|
|
}
|
|
|
|
if (value >= 1000000) {
|
|
return `${trimTrailingZeros((value / 1000000).toFixed(2))}M`
|
|
}
|
|
|
|
if (value >= 1000) {
|
|
return `${trimTrailingZeros((value / 1000).toFixed(0))}k`
|
|
}
|
|
|
|
return value.toLocaleString('en-US')
|
|
}
|
|
|
|
export function formatFileSize(bytes?: number | null): string {
|
|
if (bytes == null) {
|
|
return 'Unknown'
|
|
}
|
|
|
|
const gb = bytes / (1024 * 1024 * 1024)
|
|
if (gb >= 1) {
|
|
return `${trimTrailingZeros(gb.toFixed(1))}GB`
|
|
}
|
|
return `${Math.round(bytes / (1024 * 1024))}MB`
|
|
}
|
|
|
|
export function formatPrice(price?: number | null): string {
|
|
if (price === undefined || price === null) {
|
|
return 'N/A'
|
|
}
|
|
|
|
const formatter = price > 0 && price < 0.001 ? PRICE_NUMBER_FORMAT_4 : PRICE_NUMBER_FORMAT_3
|
|
|
|
return `$${trimTrailingZeros(formatter.format(price))}`
|
|
}
|
|
|
|
export function formatUpdatedAt(date: string): string {
|
|
try {
|
|
return UPDATED_AT_DATE_FORMAT.format(new Date(date))
|
|
} catch {
|
|
return date
|
|
}
|
|
}
|
|
|
|
export function formatCapabilityBoolean(
|
|
value: boolean | undefined,
|
|
{
|
|
positive = 'Supported',
|
|
negative = 'Not supported',
|
|
}: {
|
|
positive?: string
|
|
negative?: string
|
|
} = {}
|
|
): string {
|
|
return value ? positive : negative
|
|
}
|
|
|
|
function supportsCatalogStructuredOutputs(capabilities: ModelCapabilities): boolean {
|
|
return !capabilities.deepResearch
|
|
}
|
|
|
|
export function getEffectiveMaxOutputTokens(capabilities: ModelCapabilities): number | null {
|
|
return capabilities.maxOutputTokens ?? null
|
|
}
|
|
|
|
function trimTrailingZeros(value: string): string {
|
|
return value.replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1')
|
|
}
|
|
|
|
function slugify(value: string): string {
|
|
return value
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '')
|
|
.replace(/--+/g, '-')
|
|
}
|
|
|
|
function getProviderPrefixes(providerId: string): string[] {
|
|
return PROVIDER_PREFIXES[providerId] ?? [`${providerId}/`]
|
|
}
|
|
|
|
function stripProviderPrefix(providerId: string, modelId: string): string {
|
|
for (const prefix of getProviderPrefixes(providerId)) {
|
|
if (modelId.startsWith(prefix)) {
|
|
return modelId.slice(prefix.length)
|
|
}
|
|
}
|
|
|
|
return modelId
|
|
}
|
|
|
|
function stripTechnicalSuffixes(value: string): string {
|
|
return value
|
|
.replace(/-\d{8}-v\d+:\d+$/i, '')
|
|
.replace(/-v\d+:\d+$/i, '')
|
|
.replace(/-\d{8}$/i, '')
|
|
}
|
|
|
|
function tokenizeModelName(value: string): string[] {
|
|
return value
|
|
.replace(/[./:_]+/g, '-')
|
|
.split('-')
|
|
.filter(Boolean)
|
|
}
|
|
|
|
function mergeVersionTokens(tokens: string[]): string[] {
|
|
const merged: string[] = []
|
|
|
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
const current = tokens[index]
|
|
const next = tokens[index + 1]
|
|
|
|
if (/^\d{1,2}$/.test(current) && /^\d{1,2}$/.test(next)) {
|
|
merged.push(`${current}.${next}`)
|
|
index += 1
|
|
continue
|
|
}
|
|
|
|
merged.push(current)
|
|
}
|
|
|
|
return merged
|
|
}
|
|
|
|
function formatModelToken(token: string): string {
|
|
const normalized = token.toLowerCase()
|
|
|
|
if (TOKEN_REPLACEMENTS[normalized]) {
|
|
return TOKEN_REPLACEMENTS[normalized]
|
|
}
|
|
|
|
if (/^\d+b$/i.test(token)) {
|
|
return `${token.slice(0, -1)}B`
|
|
}
|
|
|
|
if (/^\d+e$/i.test(token)) {
|
|
return `${token.slice(0, -1)}E`
|
|
}
|
|
|
|
if (/^o\d+$/i.test(token)) {
|
|
return token.toLowerCase()
|
|
}
|
|
|
|
if (/^r\d+$/i.test(token)) {
|
|
return token.toUpperCase()
|
|
}
|
|
|
|
if (/^v\d+$/i.test(token)) {
|
|
return token.toUpperCase()
|
|
}
|
|
|
|
if (/^\d+\.\d+$/.test(token)) {
|
|
return token
|
|
}
|
|
|
|
if (/^[a-z]{3,}\d+$/i.test(token)) {
|
|
const [, prefix, version] = token.match(/^([a-z]{3,})(\d+)$/i) ?? []
|
|
if (prefix && version) {
|
|
return `${formatModelToken(prefix)} ${version}`
|
|
}
|
|
}
|
|
|
|
if (/^[a-z]\d+[a-z]$/i.test(token)) {
|
|
return token.toUpperCase()
|
|
}
|
|
|
|
if (/^\d+$/.test(token)) {
|
|
return token
|
|
}
|
|
|
|
return token.charAt(0).toUpperCase() + token.slice(1)
|
|
}
|
|
|
|
function formatModelDisplayName(providerId: string, modelId: string): string {
|
|
const shortId = stripProviderPrefix(providerId, modelId)
|
|
const normalized = stripTechnicalSuffixes(shortId)
|
|
const tokens = mergeVersionTokens(tokenizeModelName(normalized))
|
|
|
|
const displayName = tokens
|
|
.map(formatModelToken)
|
|
.join(' ')
|
|
.split(/\s+/)
|
|
.filter(
|
|
(word, index, words) => index === 0 || word.toLowerCase() !== words[index - 1].toLowerCase()
|
|
)
|
|
.join(' ')
|
|
|
|
return displayName.replace(/^GPT (\d[\w.]*)/i, 'GPT-$1').replace(/\bGpt\b/g, 'GPT')
|
|
}
|
|
|
|
function buildCapabilityTags(capabilities: ModelCapabilities): string[] {
|
|
const tags: string[] = []
|
|
|
|
if (capabilities.temperature) {
|
|
tags.push(`Temperature ${capabilities.temperature.min}-${capabilities.temperature.max}`)
|
|
}
|
|
|
|
if (capabilities.toolUsageControl) {
|
|
tags.push('Tool choice')
|
|
}
|
|
|
|
if (supportsCatalogStructuredOutputs(capabilities)) {
|
|
tags.push('Structured outputs')
|
|
}
|
|
|
|
if (capabilities.computerUse) {
|
|
tags.push('Computer use')
|
|
}
|
|
|
|
if (capabilities.deepResearch) {
|
|
tags.push('Deep research')
|
|
}
|
|
|
|
if (capabilities.reasoningEffort) {
|
|
tags.push(`Reasoning ${capabilities.reasoningEffort.values.join(', ')}`)
|
|
}
|
|
|
|
if (capabilities.verbosity) {
|
|
tags.push(`Verbosity ${capabilities.verbosity.values.join(', ')}`)
|
|
}
|
|
|
|
if (capabilities.thinking) {
|
|
tags.push(`Thinking ${capabilities.thinking.levels.join(', ')}`)
|
|
}
|
|
|
|
if (capabilities.maxOutputTokens) {
|
|
tags.push(`Max output ${formatTokenCount(capabilities.maxOutputTokens)}`)
|
|
}
|
|
|
|
if (capabilities.memory === false) {
|
|
tags.push('Memory off')
|
|
}
|
|
|
|
return tags
|
|
}
|
|
|
|
function buildBestForLine(model: {
|
|
pricing: PricingInfo
|
|
capabilities: ModelCapabilities
|
|
contextWindow: number | null
|
|
}): string | null {
|
|
const { pricing, capabilities, contextWindow } = model
|
|
|
|
if (capabilities.deepResearch) {
|
|
return 'Best for multi-step research workflows and agent-led web investigation.'
|
|
}
|
|
|
|
if (capabilities.reasoningEffort || capabilities.thinking) {
|
|
return 'Best for reasoning-heavy tasks that need more deliberate model control.'
|
|
}
|
|
|
|
if (contextWindow && contextWindow >= 1000000) {
|
|
return 'Best for long-context retrieval, large documents, and high-memory workflows.'
|
|
}
|
|
|
|
if (capabilities.nativeStructuredOutputs) {
|
|
return 'Best for production workflows that need reliable typed outputs.'
|
|
}
|
|
|
|
if (pricing.input <= 0.2 && pricing.output <= 1.25) {
|
|
return 'Best for cost-sensitive automations, background tasks, and high-volume workloads.'
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function buildModelSummary(
|
|
providerName: string,
|
|
displayName: string,
|
|
pricing: PricingInfo,
|
|
contextWindow: number | null,
|
|
capabilityTags: string[]
|
|
): string {
|
|
const parts = [
|
|
`${displayName} is a ${providerName} model tracked in Sim.`,
|
|
contextWindow ? `It supports a ${formatTokenCount(contextWindow)} token context window.` : null,
|
|
`Pricing starts at ${formatPrice(pricing.input)}/1M input tokens and ${formatPrice(pricing.output)}/1M output tokens.`,
|
|
capabilityTags.length > 0
|
|
? `Key capabilities include ${capabilityTags.slice(0, 3).join(', ')}.`
|
|
: null,
|
|
]
|
|
|
|
return parts.filter(Boolean).join(' ')
|
|
}
|
|
|
|
function computeModelRelevanceScore(model: CatalogModel): number {
|
|
return (
|
|
(model.capabilities.reasoningEffort ? 10 : 0) +
|
|
(model.capabilities.thinking ? 10 : 0) +
|
|
(model.capabilities.deepResearch ? 8 : 0) +
|
|
(model.capabilities.nativeStructuredOutputs ? 4 : 0) +
|
|
(model.contextWindow ?? 0) / 100000
|
|
)
|
|
}
|
|
|
|
function compareModelsByRelevance(a: CatalogModel, b: CatalogModel): number {
|
|
return computeModelRelevanceScore(b) - computeModelRelevanceScore(a)
|
|
}
|
|
|
|
const rawProviders = Object.values(PROVIDER_DEFINITIONS).map((provider) => {
|
|
const providerSlug = slugify(provider.id)
|
|
const providerDisplayName = provider.name
|
|
const providerCapabilityTags = buildCapabilityTags(provider.capabilities ?? {})
|
|
|
|
const models: CatalogModel[] = provider.models.map((model) => {
|
|
const shortId = stripProviderPrefix(provider.id, model.id)
|
|
const mergedCapabilities = { ...provider.capabilities, ...model.capabilities }
|
|
const capabilityTags = buildCapabilityTags(mergedCapabilities)
|
|
const bestFor = buildBestForLine({
|
|
pricing: model.pricing,
|
|
capabilities: mergedCapabilities,
|
|
contextWindow: model.contextWindow ?? null,
|
|
})
|
|
const displayName = formatModelDisplayName(provider.id, model.id)
|
|
const modelSlug = slugify(shortId)
|
|
const href = `/models/${providerSlug}/${modelSlug}`
|
|
|
|
return {
|
|
id: model.id,
|
|
slug: modelSlug,
|
|
href,
|
|
displayName,
|
|
shortId,
|
|
providerId: provider.id,
|
|
providerName: providerDisplayName,
|
|
providerSlug,
|
|
contextWindow: model.contextWindow ?? null,
|
|
releaseDate: model.releaseDate ?? null,
|
|
deprecated: model.deprecated ?? false,
|
|
pricing: model.pricing,
|
|
capabilities: mergedCapabilities,
|
|
capabilityTags,
|
|
summary: buildModelSummary(
|
|
providerDisplayName,
|
|
displayName,
|
|
model.pricing,
|
|
model.contextWindow ?? null,
|
|
capabilityTags
|
|
),
|
|
...(bestFor ? { bestFor } : {}),
|
|
searchText: [
|
|
provider.name,
|
|
providerDisplayName,
|
|
provider.id,
|
|
provider.description,
|
|
model.id,
|
|
shortId,
|
|
displayName,
|
|
capabilityTags.join(' '),
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ')
|
|
.toLowerCase(),
|
|
}
|
|
})
|
|
|
|
const defaultModelDisplayName =
|
|
models.find((model) => model.id === provider.defaultModel)?.displayName ||
|
|
(provider.defaultModel ? formatModelDisplayName(provider.id, provider.defaultModel) : 'Dynamic')
|
|
|
|
const featuredModels = [...models].sort(compareModelsByRelevance).slice(0, 6)
|
|
|
|
return {
|
|
id: provider.id,
|
|
slug: providerSlug,
|
|
href: `/models/${providerSlug}`,
|
|
name: providerDisplayName,
|
|
description: provider.description,
|
|
summary: `${providerDisplayName} has ${models.length} tracked model${models.length === 1 ? '' : 's'} in Sim with pricing, context window, and capability metadata.`,
|
|
defaultModel: provider.defaultModel,
|
|
defaultModelDisplayName,
|
|
icon: provider.icon,
|
|
color: provider.color,
|
|
isReseller: provider.isReseller ?? false,
|
|
contextInformationAvailable: provider.contextInformationAvailable !== false,
|
|
maxFileAttachmentBytes: provider.fileAttachment?.maxBytes ?? null,
|
|
providerCapabilityTags,
|
|
modelCount: models.length,
|
|
models,
|
|
featuredModels,
|
|
searchText: [
|
|
provider.name,
|
|
provider.id,
|
|
provider.description,
|
|
provider.defaultModel,
|
|
defaultModelDisplayName,
|
|
providerCapabilityTags.join(' '),
|
|
models.map((model) => model.displayName).join(' '),
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ')
|
|
.toLowerCase(),
|
|
} satisfies CatalogProvider
|
|
})
|
|
|
|
function assertUniqueGeneratedRoutes(providers: CatalogProvider[]): void {
|
|
const seenProviderHrefs = new Map<string, string>()
|
|
const seenModelHrefs = new Map<string, string>()
|
|
|
|
for (const provider of providers) {
|
|
const existingProvider = seenProviderHrefs.get(provider.href)
|
|
if (existingProvider) {
|
|
throw new Error(
|
|
`Duplicate provider route detected: ${provider.href} for ${provider.id} and ${existingProvider}`
|
|
)
|
|
}
|
|
seenProviderHrefs.set(provider.href, provider.id)
|
|
|
|
for (const model of provider.models) {
|
|
const existingModel = seenModelHrefs.get(model.href)
|
|
if (existingModel) {
|
|
throw new Error(
|
|
`Duplicate model route detected: ${model.href} for ${model.id} and ${existingModel}`
|
|
)
|
|
}
|
|
seenModelHrefs.set(model.href, model.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
assertUniqueGeneratedRoutes(rawProviders)
|
|
|
|
export const MODEL_CATALOG_PROVIDERS: CatalogProvider[] = rawProviders
|
|
export const MODEL_PROVIDERS_WITH_CATALOGS = MODEL_CATALOG_PROVIDERS.filter(
|
|
(provider) => provider.models.length > 0 && !provider.isReseller
|
|
)
|
|
export const MODEL_PROVIDERS_WITH_DYNAMIC_CATALOGS = MODEL_CATALOG_PROVIDERS.filter(
|
|
(provider) => provider.models.length === 0
|
|
)
|
|
export const ALL_CATALOG_MODELS = MODEL_PROVIDERS_WITH_CATALOGS.flatMap(
|
|
(provider) => provider.models
|
|
)
|
|
export const TOTAL_MODEL_PROVIDERS = MODEL_CATALOG_PROVIDERS.length
|
|
export const TOTAL_MODELS = ALL_CATALOG_MODELS.length
|
|
export const MAX_CONTEXT_WINDOW = Math.max(
|
|
...ALL_CATALOG_MODELS.map((model) => model.contextWindow ?? 0)
|
|
)
|
|
export const TOP_MODEL_PROVIDERS = MODEL_PROVIDERS_WITH_CATALOGS.slice(0, 8).map(
|
|
(provider) => provider.name
|
|
)
|
|
|
|
export function getPricingBounds(pricing: PricingInfo): { lowPrice: number; highPrice: number } {
|
|
return {
|
|
lowPrice: Math.min(
|
|
pricing.input,
|
|
pricing.output,
|
|
...(pricing.cachedInput !== undefined ? [pricing.cachedInput] : [])
|
|
),
|
|
highPrice: Math.max(pricing.input, pricing.output),
|
|
}
|
|
}
|
|
|
|
export function getProviderBySlug(providerSlug: string): CatalogProvider | null {
|
|
return MODEL_CATALOG_PROVIDERS.find((provider) => provider.slug === providerSlug) ?? null
|
|
}
|
|
|
|
export function getModelBySlug(providerSlug: string, modelSlug: string): CatalogModel | null {
|
|
const provider = getProviderBySlug(providerSlug)
|
|
if (!provider) {
|
|
return null
|
|
}
|
|
|
|
return provider.models.find((model) => model.slug === modelSlug) ?? null
|
|
}
|
|
|
|
export function getRelatedModels(targetModel: CatalogModel, limit = 6): CatalogModel[] {
|
|
const provider = MODEL_PROVIDERS_WITH_CATALOGS.find(
|
|
(entry) => entry.id === targetModel.providerId
|
|
)
|
|
if (!provider) {
|
|
return []
|
|
}
|
|
|
|
const targetTokens = new Set(tokenizeModelName(stripTechnicalSuffixes(targetModel.shortId)))
|
|
|
|
const scored = provider.models.reduce<Array<{ model: CatalogModel; score: number }>>(
|
|
(acc, model) => {
|
|
if (model.id === targetModel.id) {
|
|
return acc
|
|
}
|
|
|
|
const modelTokens = tokenizeModelName(stripTechnicalSuffixes(model.shortId))
|
|
const sharedTokenCount = modelTokens.filter((token) => targetTokens.has(token)).length
|
|
const sharedCapabilityCount = model.capabilityTags.filter((tag) =>
|
|
targetModel.capabilityTags.includes(tag)
|
|
).length
|
|
|
|
acc.push({
|
|
model,
|
|
score: sharedTokenCount * 2 + sharedCapabilityCount + (model.contextWindow ?? 0) / 1000000,
|
|
})
|
|
return acc
|
|
},
|
|
[]
|
|
)
|
|
|
|
return scored
|
|
.sort((a, b) => b.score - a.score)
|
|
.slice(0, limit)
|
|
.map(({ model }) => model)
|
|
}
|
|
|
|
export function buildProviderFaqs(provider: CatalogProvider): CatalogFaq[] {
|
|
const cheapestModel = getCheapestProviderModel(provider)
|
|
const largestContextModel = getLargestContextProviderModel(provider)
|
|
|
|
const toolUseModels = provider.models.filter(
|
|
(m) =>
|
|
m.capabilities.toolUsageControl !== undefined ||
|
|
provider.providerCapabilityTags.includes('Tool Use')
|
|
)
|
|
|
|
const faqs: CatalogFaq[] = [
|
|
{
|
|
question: `What ${provider.name} models are available in Sim?`,
|
|
answer: `Sim currently tracks ${provider.modelCount} ${provider.name} model${provider.modelCount === 1 ? '' : 's'} including ${provider.models
|
|
.slice(0, 6)
|
|
.map((model) => model.displayName)
|
|
.join(', ')}${provider.modelCount > 6 ? ', and more' : ''}.`,
|
|
},
|
|
{
|
|
question: `What is the default ${provider.name} model in Sim?`,
|
|
answer: provider.defaultModel
|
|
? `${provider.defaultModelDisplayName} is the default ${provider.name} model in Sim.`
|
|
: `${provider.name} does not have a fixed default model in the public catalog because models are loaded dynamically.`,
|
|
},
|
|
{
|
|
question: `What is the cheapest ${provider.name} model tracked in Sim?`,
|
|
answer: cheapestModel
|
|
? `${cheapestModel.displayName} currently has the lowest listed input price at ${formatPrice(
|
|
cheapestModel.pricing.input
|
|
)}/1M tokens.`
|
|
: `Sim does not currently expose a fixed public pricing table for ${provider.name} models on this page.`,
|
|
},
|
|
{
|
|
question: `Which ${provider.name} model has the largest context window?`,
|
|
answer: largestContextModel?.contextWindow
|
|
? `${largestContextModel.displayName} currently has the largest listed context window at ${formatTokenCount(
|
|
largestContextModel.contextWindow
|
|
)} tokens.`
|
|
: `Context window details are not fully available for every ${provider.name} model in the public catalog.`,
|
|
},
|
|
]
|
|
|
|
if (toolUseModels.length > 0) {
|
|
faqs.push({
|
|
question: `Which ${provider.name} models support tool use and function calling in Sim?`,
|
|
answer:
|
|
toolUseModels.length === provider.modelCount
|
|
? `All ${provider.name} models in Sim support tool use and function calling, allowing agents to invoke external APIs, query databases, and run custom actions.`
|
|
: `${toolUseModels
|
|
.slice(0, 5)
|
|
.map((m) => m.displayName)
|
|
.join(
|
|
', '
|
|
)}${toolUseModels.length > 5 ? ', and others' : ''} support tool use and function calling in Sim, enabling agents to invoke external APIs and run custom actions.`,
|
|
})
|
|
}
|
|
|
|
return faqs
|
|
}
|
|
|
|
export function buildModelFaqs(provider: CatalogProvider, model: CatalogModel): CatalogFaq[] {
|
|
const faqs: CatalogFaq[] = [
|
|
{
|
|
question: `What is ${model.displayName}?`,
|
|
answer: `${model.displayName} is a ${provider.name} model available in Sim. ${model.summary}`,
|
|
},
|
|
{
|
|
question: `How much does ${model.displayName} cost?`,
|
|
answer: `${model.displayName} is listed at ${formatPrice(model.pricing.input)}/1M input tokens${model.pricing.cachedInput !== undefined ? `, ${formatPrice(model.pricing.cachedInput)}/1M cached input tokens` : ''}, and ${formatPrice(model.pricing.output)}/1M output tokens.`,
|
|
},
|
|
{
|
|
question: `What is the context window for ${model.displayName}?`,
|
|
answer: model.contextWindow
|
|
? `${model.displayName} supports a context window of ${formatTokenCount(model.contextWindow)} tokens in Sim. In an agent, this determines how much conversation history, tool outputs, and retrieved documents the model can hold in a single call.`
|
|
: `A public context window value is not currently tracked for ${model.displayName}.`,
|
|
},
|
|
{
|
|
question: `What capabilities does ${model.displayName} support?`,
|
|
answer:
|
|
model.capabilityTags.length > 0
|
|
? `${model.displayName} supports the following capabilities in Sim: ${model.capabilityTags.join(', ')}.`
|
|
: `${model.displayName} supports standard text generation in Sim. No additional capability flags such as tool use or structured outputs are currently tracked for this model.`,
|
|
},
|
|
]
|
|
|
|
if (model.bestFor) {
|
|
faqs.push({
|
|
question: `What is ${model.displayName} best used for?`,
|
|
answer: `${model.bestFor} When used in a Sim workflow, it can be selected in any Agent block from the model picker.`,
|
|
})
|
|
}
|
|
|
|
return faqs
|
|
}
|
|
|
|
export function buildModelCapabilityFacts(model: CatalogModel): CapabilityFact[] {
|
|
const { capabilities } = model
|
|
const supportsStructuredOutputs = supportsCatalogStructuredOutputs(capabilities)
|
|
|
|
return [
|
|
{
|
|
label: 'Temperature',
|
|
value: capabilities.temperature
|
|
? `${capabilities.temperature.min} to ${capabilities.temperature.max}`
|
|
: 'Not configurable',
|
|
},
|
|
{
|
|
label: 'Reasoning effort',
|
|
value: capabilities.reasoningEffort
|
|
? capabilities.reasoningEffort.values.join(', ')
|
|
: 'Not supported',
|
|
},
|
|
{
|
|
label: 'Verbosity',
|
|
value: capabilities.verbosity ? capabilities.verbosity.values.join(', ') : 'Not supported',
|
|
},
|
|
{
|
|
label: 'Thinking levels',
|
|
value: capabilities.thinking
|
|
? `${capabilities.thinking.levels.join(', ')}${
|
|
capabilities.thinking.default ? ` (default: ${capabilities.thinking.default})` : ''
|
|
}`
|
|
: 'Not supported',
|
|
},
|
|
{
|
|
label: 'Structured outputs',
|
|
value: supportsStructuredOutputs
|
|
? capabilities.nativeStructuredOutputs
|
|
? 'Supported (native)'
|
|
: 'Supported'
|
|
: 'Not supported',
|
|
},
|
|
{
|
|
label: 'Tool choice',
|
|
value: formatCapabilityBoolean(capabilities.toolUsageControl),
|
|
},
|
|
{
|
|
label: 'Computer use',
|
|
value: formatCapabilityBoolean(capabilities.computerUse),
|
|
},
|
|
{
|
|
label: 'Deep research',
|
|
value: formatCapabilityBoolean(capabilities.deepResearch),
|
|
},
|
|
{
|
|
label: 'Memory support',
|
|
value: capabilities.memory === false ? 'Disabled' : 'Supported',
|
|
},
|
|
{
|
|
label: 'Max output tokens',
|
|
value: capabilities.maxOutputTokens
|
|
? formatTokenCount(getEffectiveMaxOutputTokens(capabilities))
|
|
: 'Not published',
|
|
},
|
|
]
|
|
}
|
|
|
|
export function getCheapestProviderModel(provider: CatalogProvider): CatalogModel | null {
|
|
if (provider.models.length === 0) return null
|
|
return provider.models.reduce((min, m) => (m.pricing.input < min.pricing.input ? m : min))
|
|
}
|
|
|
|
export function getLargestContextProviderModel(provider: CatalogProvider): CatalogModel | null {
|
|
if (provider.models.length === 0) return null
|
|
return provider.models.reduce((max, m) =>
|
|
(m.contextWindow ?? 0) > (max.contextWindow ?? 0) ? m : max
|
|
)
|
|
}
|