Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (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 / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

435 lines
16 KiB
TypeScript

/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import {
getBaseModelProviders,
getHostedModels,
getModelPricing,
getModelsWithPromptCaching,
getPromptCachingMinimumTokens,
getProviderModels,
getThinkingStreamVisibility,
isModelDeprecated,
orderModelIdsByReleaseDate,
PROVIDER_DEFINITIONS,
updateFireworksModels,
} from '@/providers/models'
import { supportsPromptCaching } from '@/providers/utils'
describe('Anthropic thinking stream visibility', () => {
it('classifies visible Claude thinking as summarized rather than raw', () => {
for (const providerId of ['anthropic', 'azure-anthropic'] as const) {
for (const model of PROVIDER_DEFINITIONS[providerId].models) {
if (model.capabilities.thinking) {
expect(getThinkingStreamVisibility(model.id)).toBe('summary')
}
}
}
})
})
describe('Meta thinking stream visibility', () => {
it('classifies private Muse reasoning as not streamed', () => {
expect(getThinkingStreamVisibility('muse-spark-1.1')).toBe('none')
})
})
describe('prompt caching capability', () => {
const cachingModels = new Set(getModelsWithPromptCaching())
it('covers every Claude model on both Anthropic surfaces', () => {
for (const providerId of ['anthropic', 'azure-anthropic'] as const) {
for (const model of PROVIDER_DEFINITIONS[providerId].models) {
expect(cachingModels.has(model.id)).toBe(true)
}
}
})
/**
* OpenAI and Gemini cache automatically with no caller control, so declaring
* the capability would put a switch in the UI that does nothing.
*/
it('excludes providers whose caching is automatic', () => {
for (const providerId of ['openai', 'google'] as const) {
for (const model of PROVIDER_DEFINITIONS[providerId].models) {
expect(cachingModels.has(model.id)).toBe(false)
}
}
expect(supportsPromptCaching('gpt-5.5')).toBe(false)
})
it('reports the vendor minimum prefix, raised for Haiku', () => {
expect(getPromptCachingMinimumTokens('claude-sonnet-5')).toBe(1024)
expect(getPromptCachingMinimumTokens('claude-haiku-4-5')).toBe(2048)
expect(getPromptCachingMinimumTokens('azure-anthropic/claude-haiku-4-5')).toBe(2048)
expect(getPromptCachingMinimumTokens('gpt-5.5')).toBeNull()
})
})
const DYNAMIC_PROVIDERS = new Set([
'ollama',
'ollama-cloud',
'vllm',
'litellm',
'openrouter',
'fireworks',
'together',
'baseten',
])
function firstDeprecatedModelId(): string | undefined {
for (const [providerId, provider] of Object.entries(PROVIDER_DEFINITIONS)) {
if (DYNAMIC_PROVIDERS.has(providerId)) continue
const dep = provider.models.find((m) => m.sunset)
if (dep) return dep.id
}
return undefined
}
/** Maps a lowercased model ID to its provider's index in the catalog. */
const PROVIDER_INDEX_BY_MODEL = new Map<string, number>()
/** Maps a lowercased model ID to its release time (ms), or null when undated. */
const RELEASE_TIME_BY_MODEL = new Map<string, number | null>()
for (const [providerIndex, provider] of Object.values(PROVIDER_DEFINITIONS).entries()) {
for (const model of provider.models) {
const id = model.id.toLowerCase()
PROVIDER_INDEX_BY_MODEL.set(id, providerIndex)
RELEASE_TIME_BY_MODEL.set(id, model.releaseDate ? Date.parse(model.releaseDate) : null)
}
}
describe('orderModelIdsByReleaseDate', () => {
it('keeps provider grouping order intact', () => {
const ordered = orderModelIdsByReleaseDate(Object.keys(getBaseModelProviders()))
let lastProviderIndex = -1
const seenProviders = new Set<number>()
for (const id of ordered) {
const providerIndex = PROVIDER_INDEX_BY_MODEL.get(id.toLowerCase())
expect(providerIndex).toBeDefined()
// A provider's models must form one contiguous run: once we leave a provider
// we never return to it.
if (providerIndex !== lastProviderIndex) {
expect(seenProviders.has(providerIndex as number)).toBe(false)
seenProviders.add(providerIndex as number)
lastProviderIndex = providerIndex as number
}
}
})
it('sorts models within a provider newest-first by release date', () => {
const ordered = orderModelIdsByReleaseDate(Object.keys(getBaseModelProviders()))
for (let i = 1; i < ordered.length; i++) {
const prev = ordered[i - 1].toLowerCase()
const curr = ordered[i].toLowerCase()
if (PROVIDER_INDEX_BY_MODEL.get(prev) !== PROVIDER_INDEX_BY_MODEL.get(curr)) continue
const prevTime = RELEASE_TIME_BY_MODEL.get(prev)
const currTime = RELEASE_TIME_BY_MODEL.get(curr)
// Dated models precede undated ones; among dated models, newer precedes older.
if (prevTime == null) {
expect(currTime).toBeNull()
} else if (currTime != null) {
expect(prevTime).toBeGreaterThanOrEqual(currTime)
}
}
})
it('preserves the cross-provider grouping order given in the input', () => {
// Pick the first model of two different providers and feed the second provider
// first; the helper must keep that provider's group ahead of the other.
const byProvider = new Map<number, string[]>()
for (const id of Object.keys(getBaseModelProviders())) {
const providerIndex = PROVIDER_INDEX_BY_MODEL.get(id.toLowerCase()) as number
const bucket = byProvider.get(providerIndex) ?? []
bucket.push(id)
byProvider.set(providerIndex, bucket)
}
const providerIndexes = [...byProvider.keys()]
expect(providerIndexes.length).toBeGreaterThanOrEqual(2)
const [firstProvider, secondProvider] = providerIndexes
const fromFirst = byProvider.get(firstProvider) as string[]
const fromSecond = byProvider.get(secondProvider) as string[]
// Input order intentionally leads with the second provider.
const input = [fromSecond[0], fromFirst[0]]
const ordered = orderModelIdsByReleaseDate(input)
expect(PROVIDER_INDEX_BY_MODEL.get(ordered[0].toLowerCase())).toBe(secondProvider)
expect(PROVIDER_INDEX_BY_MODEL.get(ordered[1].toLowerCase())).toBe(firstProvider)
})
it('places unknown model IDs last, preserving their input order', () => {
const known = Object.keys(getBaseModelProviders())[0]
const ordered = orderModelIdsByReleaseDate(['mystery-a', known, 'mystery-b'])
expect(ordered[0]).toBe(known)
expect(ordered.slice(1)).toEqual(['mystery-a', 'mystery-b'])
})
it('is case-insensitive when matching catalog IDs', () => {
const id = Object.keys(getBaseModelProviders())[0]
const ordered = orderModelIdsByReleaseDate([id.toUpperCase()])
expect(ordered).toEqual([id.toUpperCase()])
})
it('returns an empty array for empty input', () => {
expect(orderModelIdsByReleaseDate([])).toEqual([])
})
it('does not add or drop any IDs', () => {
const input = Object.keys(getBaseModelProviders())
const ordered = orderModelIdsByReleaseDate(input)
expect([...ordered].sort()).toEqual([...input].sort())
})
})
describe('sakana provider definition', () => {
const sakana = PROVIDER_DEFINITIONS.sakana
it('is registered with fugu as the default model', () => {
expect(sakana).toBeDefined()
expect(sakana.id).toBe('sakana')
expect(sakana.defaultModel).toBe('fugu')
expect(sakana.modelPatterns).toEqual([/^fugu/])
})
it('exposes fugu and fugu-ultra with a 1M context window', () => {
expect(sakana.models.map((m) => m.id)).toEqual(['fugu', 'fugu-ultra'])
for (const model of sakana.models) {
expect(model.contextWindow).toBe(1000000)
}
})
it('prices both models at the documented fugu-ultra ceiling', () => {
for (const model of sakana.models) {
expect(model.pricing.input).toBe(5)
expect(model.pricing.output).toBe(30)
expect(model.pricing.cachedInput).toBe(0.5)
}
})
it('routes bare fugu model IDs to the sakana provider', () => {
const baseModels = getBaseModelProviders()
expect(baseModels.fugu).toBe('sakana')
expect(baseModels['fugu-ultra']).toBe('sakana')
})
})
describe('nvidia provider definition', () => {
const nvidia = PROVIDER_DEFINITIONS.nvidia
const expectedModels = [
{ id: 'nvidia/llama-3.1-nemotron-70b-instruct', contextWindow: 128000 },
{ id: 'nvidia/llama-3.1-nemotron-ultra-253b-v1', contextWindow: 131072 },
{ id: 'nvidia/llama-3.3-nemotron-super-49b-v1.5', contextWindow: 131072 },
{ id: 'nvidia/nemotron-3-nano-30b-a3b', contextWindow: 262144 },
{ id: 'nvidia/nemotron-3-super-120b-a12b', contextWindow: 1048576 },
{ id: 'nvidia/nemotron-3-ultra-550b-a55b', contextWindow: 1048576 },
]
it('is registered with the current-gen Super model as the default', () => {
expect(nvidia).toBeDefined()
expect(nvidia.id).toBe('nvidia')
expect(nvidia.defaultModel).toBe('nvidia/nemotron-3-super-120b-a12b')
expect(nvidia.modelPatterns).toEqual([/^nvidia\//])
})
it('exposes all six Nemotron models with the documented context windows', () => {
expect(nvidia.models.map((m) => m.id)).toEqual(expectedModels.map((m) => m.id))
for (const expected of expectedModels) {
const model = nvidia.models.find((m) => m.id === expected.id)
expect(model?.contextWindow).toBe(expected.contextWindow)
}
})
it('routes every nvidia model ID to the nvidia provider', () => {
const baseModels = getBaseModelProviders()
for (const expected of expectedModels) {
expect(baseModels[expected.id]).toBe('nvidia')
}
})
})
describe('zai provider definition', () => {
const zai = PROVIDER_DEFINITIONS.zai
const expectedModels = [
{ id: 'glm-5.2', contextWindow: 1000000 },
{ id: 'glm-5.1', contextWindow: 200000 },
{ id: 'glm-5', contextWindow: 200000 },
{ id: 'glm-5-turbo', contextWindow: 200000 },
{ id: 'glm-4.7', contextWindow: 200000 },
{ id: 'glm-4.7-flashx', contextWindow: 200000 },
{ id: 'glm-4.6', contextWindow: 200000 },
{ id: 'glm-4.5', contextWindow: 128000 },
{ id: 'glm-4.5-air', contextWindow: 128000 },
{ id: 'glm-4.5-x', contextWindow: 128000 },
{ id: 'glm-4.5-airx', contextWindow: 128000 },
{ id: 'glm-4-32b-0414-128k', contextWindow: 128000 },
]
it('is registered with a bare glm-4.6 as the default model', () => {
expect(zai).toBeDefined()
expect(zai.id).toBe('zai')
expect(zai.defaultModel).toBe('glm-4.6')
expect(zai.defaultModel.startsWith('zai/')).toBe(false)
// No fallback pattern — an unscoped `/^glm/` would overmatch unrelated self-hosted
// "glm-*" models and misroute them to Z.ai's hosted billing.
expect(zai.modelPatterns).toEqual([])
})
it('exposes every GLM model with the documented context window', () => {
expect(zai.models.map((m) => m.id)).toEqual(expectedModels.map((m) => m.id))
for (const expected of expectedModels) {
const model = zai.models.find((m) => m.id === expected.id)
expect(model?.contextWindow).toBe(expected.contextWindow)
}
})
it('routes every bare glm-* model ID to the zai provider', () => {
const baseModels = getBaseModelProviders()
for (const expected of expectedModels) {
expect(baseModels[expected.id]).toBe('zai')
}
})
it('is included in getHostedModels since Sim provides the Z.ai key server-side', () => {
expect(getHostedModels()).toContain('glm-4.6')
})
})
describe('kimi provider definition', () => {
const kimi = PROVIDER_DEFINITIONS.kimi
const expectedModels = [
{ id: 'kimi-k3', contextWindow: 1048576 },
{ id: 'kimi-k2.7-code', contextWindow: 262144 },
{ id: 'kimi-k2.7-code-highspeed', contextWindow: 262144 },
{ id: 'kimi-k2.6', contextWindow: 262144 },
]
it('is registered with kimi-k2.6 as the default model', () => {
expect(kimi).toBeDefined()
expect(kimi.id).toBe('kimi')
// kimi-k2.6 (not the flagship kimi-k3) — k3 access is tier-gated on Moonshot accounts,
// and the default must be a model every account can serve.
expect(kimi.defaultModel).toBe('kimi-k2.6')
// No fallback pattern — an unscoped `/^kimi/` would overmatch Kimi weights re-hosted by
// other providers and misroute them to Moonshot's hosted billing.
expect(kimi.modelPatterns).toEqual([])
})
it('exposes every Kimi model with the documented context window', () => {
expect(kimi.models.map((m) => m.id)).toEqual(expectedModels.map((m) => m.id))
for (const expected of expectedModels) {
const model = kimi.models.find((m) => m.id === expected.id)
expect(model?.contextWindow).toBe(expected.contextWindow)
}
})
it('declares no temperature capability since every current Kimi model pins it server-side', () => {
expect(kimi.capabilities?.temperature).toBeUndefined()
for (const model of kimi.models) {
expect(model.capabilities.temperature).toBeUndefined()
}
})
it('exposes the thinking toggle only on kimi-k2.6', () => {
for (const model of kimi.models) {
const hasToggle = model.id === 'kimi-k2.6'
if (hasToggle) {
expect(model.capabilities.thinking).toEqual({
levels: ['disabled', 'enabled'],
default: 'enabled',
})
} else {
expect(model.capabilities.thinking).toBeUndefined()
}
}
})
it('routes every kimi model ID to the kimi provider', () => {
const baseModels = getBaseModelProviders()
for (const expected of expectedModels) {
expect(baseModels[expected.id]).toBe('kimi')
}
})
it('is included in getHostedModels since Sim provides the Kimi key server-side', () => {
expect(getHostedModels()).toContain('kimi-k3')
})
})
describe('xai provider definition', () => {
const xai = PROVIDER_DEFINITIONS.xai
it('is registered with grok-4.5 as the default model', () => {
expect(xai).toBeDefined()
expect(xai.id).toBe('xai')
expect(xai.defaultModel).toBe('grok-4.5')
})
it('is included in getHostedModels since Sim provides the xAI key server-side', () => {
expect(getHostedModels()).toContain('grok-4.5')
})
})
describe('fireworks static catalog (the sim-auto pool)', () => {
const poolModels = ['fireworks/glm-5.2', 'fireworks/kimi-k3']
it('is included in getHostedModels since Sim provides the Fireworks key server-side', () => {
for (const model of poolModels) {
expect(getHostedModels()).toContain(model)
}
})
it('prices every pool model so hosted usage is billable', () => {
for (const model of poolModels) {
const pricing = getModelPricing(model)
expect(pricing?.input).toBeGreaterThan(0)
expect(pricing?.output).toBeGreaterThan(0)
}
})
it('survives a dynamic model sync, which merges rather than replaces', () => {
updateFireworksModels(['fireworks/accounts/acme/models/custom'])
for (const model of poolModels) {
expect(getHostedModels()).toContain(model)
expect(getModelPricing(model)?.input).toBeGreaterThan(0)
}
expect(getProviderModels('fireworks')).toContain('fireworks/accounts/acme/models/custom')
})
it('does not duplicate a pool model the dynamic listing also returns', () => {
updateFireworksModels(['fireworks/glm-5.2'])
expect(getProviderModels('fireworks').filter((id) => id === 'fireworks/glm-5.2')).toHaveLength(
1
)
expect(getModelPricing('fireworks/glm-5.2')?.input).toBeGreaterThan(0)
})
})
describe('isModelDeprecated', () => {
it('returns true for a catalogued deprecated model (case-insensitive)', () => {
const id = firstDeprecatedModelId()
expect(id).toBeDefined()
expect(isModelDeprecated(id!)).toBe(true)
expect(isModelDeprecated(id!.toUpperCase())).toBe(true)
})
it('returns false for the default model of every provider', () => {
for (const provider of Object.values(PROVIDER_DEFINITIONS)) {
if (provider.defaultModel) expect(isModelDeprecated(provider.defaultModel)).toBe(false)
}
})
it('returns false for empty, unknown, and dynamic-provider ids', () => {
expect(isModelDeprecated('')).toBe(false)
expect(isModelDeprecated(undefined)).toBe(false)
expect(isModelDeprecated(null)).toBe(false)
expect(isModelDeprecated('not-a-real-model')).toBe(false)
expect(isModelDeprecated('openrouter/some/model')).toBe(false)
})
})