Files
simstudioai--sim/apps/sim/app/(landing)/models/utils.ts
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

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
)
}