* build: update dependencies Update the AI SDK providers, Vue, Reka UI, Valibot, Zod, es-toolkit, CodeMirror, Storybook, Playwright, Hono and other dependencies to their current releases, consistently across workspaces. The Tauri plugin packages must match their Rust crates, and the new plugin crates require Tauri 2.12, so Cargo.lock, @tauri-apps/api and the Tauri CLI move to 2.12 as well. * build(harness): update the AI SDK harness packages @ai-sdk/harness 1.0.74 pinned ai 7.0.67, so the workspace carried a second copy of ai next to the root one; 1.0.138 depends on the same ai release. The Pi adapter no longer takes a model: HarnessAgent does. The settings were spread from untyped records, so the compiler could not reject the stale key and the chosen model would have been dropped; they are plain literals now. PiAuthOptions is now PiAuthenticationMode, and auth accepts an environment record. Pass the gateway key that way instead of writing it into the process-wide environment while a session is created. Derive the thinking level from the adapter's settings, which adds 'max'. * build: hold vue-tsc at 3.3.11 vue-tsc 3.3.12 no longer sees a v-slot binding inside a component that also has an event listener, so check:vue reports "Cannot find name 'control'" in MCPConnectionEditor and ProfileEditor. 3.3.11 checks them cleanly. * fix(ai): keep retryability for provider errors reported mid-stream From ai 7.0.80 a provider error after the response stream starts is a StreamProviderError rather than an APICallError, so classifyAIChatError lost its isRetryable. * feat(desktop): accept updates only when signed for their version Tauri CLI 2.12 records the app version in each updater signature, and updater 2.13 checks it against the version latest.json announces. With requireSignedVersion it also rejects signatures that carry no version, so a tampered manifest cannot pair a newer version number with an older, still validly signed bundle. Release assembly now fails when a signature does not name the version being released, instead of shipping one that installed apps would reject. * docs: note the dependency update's security fixes in the changelog * feat(ai): recommend the latest models The provider packages now know Claude Sonnet 5.5 and Opus 5.5 and the GPT-6 series. Make Sonnet 5.5 and GPT-6.1 Sol the defaults, list Opus 5.5, Fable 5.1, GPT-6 Astra and GPT-6 Luna, and replace the two free OpenRouter models that OpenRouter no longer serves. * build: align the fig package's valibot with the workspace
147 lines
4.9 KiB
TypeScript
147 lines
4.9 KiB
TypeScript
import { afterEach, describe, expect, test } from 'bun:test'
|
|
|
|
import {
|
|
listCatalogModels,
|
|
resetModelsDevCatalogForTests,
|
|
resolveModelsDevModel
|
|
} from '@/app/ai/models/catalog'
|
|
|
|
function catalogResponse(body: unknown): typeof fetch {
|
|
return (async () => new Response(JSON.stringify(body), { status: 200 })) as typeof fetch
|
|
}
|
|
|
|
afterEach(() => resetModelsDevCatalogForTests())
|
|
|
|
describe('models.dev catalog', () => {
|
|
test('resolves provider model capabilities and output limits', async () => {
|
|
const model = await resolveModelsDevModel(
|
|
'anthropic',
|
|
'claude-sonnet-4-6-20260301',
|
|
catalogResponse({
|
|
anthropic: {
|
|
models: {
|
|
'claude-sonnet-4-6': {
|
|
name: 'Claude Sonnet 4.6',
|
|
tool_call: true,
|
|
attachment: true,
|
|
limit: { output: 64_000 }
|
|
}
|
|
}
|
|
}
|
|
})
|
|
)
|
|
|
|
expect(model).toEqual({
|
|
id: 'claude-sonnet-4-6-20260301',
|
|
name: 'Claude Sonnet 4.6',
|
|
capabilities: ['tools', 'vision'],
|
|
recommendedMaxOutputTokens: 64_000,
|
|
status: 'active'
|
|
})
|
|
})
|
|
|
|
test('lists compatible models with curated recommendations first and latest models next', async () => {
|
|
const models = await listCatalogModels(
|
|
'openai',
|
|
catalogResponse({
|
|
openai: {
|
|
models: {
|
|
'gpt-6.1-sol': {
|
|
name: 'GPT-6.1 Sol from catalog',
|
|
tool_call: true,
|
|
attachment: true,
|
|
release_date: '2026-07-09',
|
|
modalities: { output: ['text'] },
|
|
limit: { output: 128_000 }
|
|
},
|
|
'gpt-6-preview': {
|
|
name: 'GPT-6 Preview',
|
|
tool_call: true,
|
|
release_date: '2026-09-01',
|
|
modalities: { output: ['text'] }
|
|
},
|
|
'gpt-image': {
|
|
name: 'GPT Image',
|
|
tool_call: false,
|
|
release_date: '2026-09-02',
|
|
modalities: { output: ['image'] }
|
|
},
|
|
missingOutput: { tool_call: true },
|
|
malformedOutput: { tool_call: true, modalities: { output: 'text' } },
|
|
imageOutput: { tool_call: true, modalities: { output: ['image'] } },
|
|
legacy: {
|
|
name: 'Legacy',
|
|
tool_call: true,
|
|
status: 'deprecated',
|
|
release_date: '2025-01-01'
|
|
}
|
|
}
|
|
}
|
|
})
|
|
)
|
|
|
|
expect(models[0]).toMatchObject({
|
|
id: 'gpt-6.1-sol',
|
|
name: 'GPT-6.1 Sol',
|
|
tag: 'Best',
|
|
capabilities: ['tools', 'vision'],
|
|
releaseDate: '2026-07-09'
|
|
})
|
|
expect(models.findIndex((model) => model.id === 'gpt-6-preview')).toBeGreaterThan(0)
|
|
expect(models.some((model) => model.id === 'gpt-image')).toBeFalse()
|
|
expect(models.some((model) => model.id === 'legacy')).toBeFalse()
|
|
for (const id of ['missingOutput', 'malformedOutput', 'imageOutput']) {
|
|
expect(models.some((model) => model.id === id)).toBeFalse()
|
|
}
|
|
})
|
|
|
|
test('falls back to curated models when the catalog request fails', async () => {
|
|
const failingFetch = (async () => new Response(null, { status: 503 })) as typeof fetch
|
|
const models = await listCatalogModels('anthropic', failingFetch)
|
|
|
|
expect(models[0]).toMatchObject({ id: 'claude-sonnet-5-5', tag: 'Best for design' })
|
|
})
|
|
|
|
test('retries a failed shared request and shares a successful request', async () => {
|
|
const originalFetch = globalThis.fetch
|
|
let requests = 0
|
|
globalThis.fetch = (async () => {
|
|
requests++
|
|
if (requests === 1) return new Response(null, { status: 503 })
|
|
return new Response(
|
|
JSON.stringify({
|
|
openai: { models: { retryModel: { name: 'Retry model', tool_call: true } } }
|
|
})
|
|
)
|
|
}) as typeof fetch
|
|
try {
|
|
expect(await resolveModelsDevModel('openai', 'retryModel')).toBeNull()
|
|
const [first, second] = await Promise.all([
|
|
resolveModelsDevModel('openai', 'retryModel'),
|
|
resolveModelsDevModel('openai', 'retryModel')
|
|
])
|
|
expect(first?.name).toBe('Retry model')
|
|
expect(second).toEqual(first)
|
|
expect(requests).toBe(2)
|
|
} finally {
|
|
globalThis.fetch = originalFetch
|
|
}
|
|
})
|
|
|
|
test('preserves vision support in offline curated models', async () => {
|
|
const failingFetch = (async () => new Response(null, { status: 503 })) as typeof fetch
|
|
const openai = await listCatalogModels('openai', failingFetch)
|
|
for (const id of ['gpt-6.1-sol', 'gpt-6-astra', 'gpt-6-luna']) {
|
|
expect(openai.find((model) => model.id === id)?.capabilities).toContain('vision')
|
|
}
|
|
const zai = await listCatalogModels('zai', failingFetch)
|
|
expect(zai.find((model) => model.id === 'glm-5v-turbo')?.capabilities).toContain('vision')
|
|
})
|
|
|
|
test('returns null when the provider or model is unknown', async () => {
|
|
expect(
|
|
await resolveModelsDevModel('openai-compatible', 'local', catalogResponse({}))
|
|
).toBeNull()
|
|
})
|
|
})
|