chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,827 @@
|
||||
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
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user