diff --git a/README.md b/README.md index 81c955da3a..82fcc0c17c 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ The [website](https://maka.apache.org/en/) walks through one turn of the log and **Apache Releases**: Maka has not made an Apache release yet. When one exists, the signed source archive will be the official release; packages distributed elsewhere are convenience artifacts. See the [downloads page](https://maka.apache.org/en/downloads/) and [`.github/ASF_SOURCE_RELEASE.md`](./.github/ASF_SOURCE_RELEASE.md) for candidate criteria, signing procedures, and verification steps. -**Desktop Nightly**: Built daily from `main` for developers and testers, for macOS on Apple Silicon and Intel, Windows x64 and Linux x64 and arm64; the Windows and Linux builds are unsigned previews. It is not an ASF release and is not intended for production use. The [downloads page](https://maka.apache.org/en/downloads/) has the installers and the platform status. +**Development testing**: To contribute or help test unreleased changes, see [CONTRIBUTING](./CONTRIBUTING.md) and the [development mailing list](https://lists.apache.org/list.html?dev@maka.apache.org). Development builds are not approved Apache releases. **Build from source**: To compile and run Desktop, the TUI, or the CLI directly from a source checkout, see the [Build from source](#build-from-source) section below. diff --git a/README.zh-CN.md b/README.zh-CN.md index 473f9b86ed..fc8a4bfdd9 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -56,7 +56,7 @@ Agent harness 的本职就是把任务做完。衡量它的标准只有一条: **Apache Releases**:Maka 尚未发布过 Apache release。发布之后,带签名的源码包才是正式 release,其他渠道分发的包属于便利构建。候选版本的准入标准、签名路径与验包步骤见[下载页面](https://maka.apache.org/zh-CN/downloads/)与 [`.github/ASF_SOURCE_RELEASE.md`](./.github/ASF_SOURCE_RELEASE.md)。 -**Desktop Nightly**:每天从 `main` 构建,面向开发者和测试者,覆盖 macOS 的 Apple Silicon 与 Intel、Windows x64、Linux x64 与 arm64;Windows 和 Linux 构建是未签名预览。它不是 ASF release,不适合生产使用。安装包与平台状态见[下载页面](https://maka.apache.org/zh-CN/downloads/)。 +**开发测试**:如果希望贡献代码或协助测试尚未发布的改动,请阅读[贡献指南](./CONTRIBUTING.zh-CN.md)并关注[开发邮件列表](https://lists.apache.org/list.html?dev@maka.apache.org)。开发构建不是获批的 Apache 正式版本。 **从源码构建**:要从源码 checkout 直接构建并运行 Desktop、TUI 或 CLI,见下方的[从源码构建](#从源码构建)一节。 diff --git a/apps/desktop/src/main/__tests__/model-catalog-choices.test.ts b/apps/desktop/src/main/__tests__/model-catalog-choices.test.ts index 8f2deeba3b..827756db06 100644 --- a/apps/desktop/src/main/__tests__/model-catalog-choices.test.ts +++ b/apps/desktop/src/main/__tests__/model-catalog-choices.test.ts @@ -74,7 +74,7 @@ describe('model catalog picker helpers', () => { { connectionId: 'connection-ready', connectionSlug: 'ready-second', - providerType: 'opencode-free', + providerType: 'opencode-go', providerLabel: 'OpenCode Zen', model: 'ready-model', label: 'Ready', diff --git a/apps/desktop/src/main/__tests__/runtime-host-connections-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-connections-ipc-main.test.ts index 69495893ac..9d4b91f138 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-connections-ipc-main.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-connections-ipc-main.test.ts @@ -27,7 +27,6 @@ import { openInteractiveRuntimePolicyStoresForWrite } from '@maka/storage/runtim import { resolveConnectionModelCatalog } from '@maka/core/model-catalog'; import type { UpdateCatalogConnectionInput } from '@maka/core/runtime-policy'; import { createDesktopConnectionSettingsServices } from '../../renderer/platform/desktop/create-connection-settings-services.js'; -import { defaultEnabledModelIdsWhenOmitted } from '@maka/core/llm-connections'; import type { RuntimeHostConnectionCatalogEntry as ConnectionCatalogEntry, RuntimeHostConnectionCatalogSnapshot as ConnectionCatalogSnapshot, @@ -43,12 +42,7 @@ import { } from '../runtime-host-connections-ipc-main.js'; import { normalizeCreateConnectionInputForIpc } from '../connections-ipc-validation.js'; -const OPENCODE_FREE_ENABLED_MODEL_IDS: readonly string[] = - defaultEnabledModelIdsWhenOmitted('opencode-free') ?? []; -// `providerType in PROVIDER_REGISTRY` traverses the prototype chain, so an -// inherited member named a provider the build does not register. The renderer -// reaches this boundary, and what it admits is persisted. test('refuses a prototype member posing as a provider type', () => { for (const providerType of ['__proto__', 'toString', 'constructor', 'hasOwnProperty']) { assert.throws( @@ -513,7 +507,7 @@ test('keeps saved custom header values out of the renderer and preserves them by ]); }); -test('preserves the provider default inventory beside the recommended model', async () => { +test('creates a connection with only the explicitly selected model', async () => { const handlers = new Map unknown>(); let createdModels: readonly string[] = []; const emptyCatalog: ConnectionCatalogSnapshot = { @@ -536,11 +530,11 @@ test('preserves the provider default inventory beside the recommended model', as defaultTarget: null, connections: [ { - connectionId: 'connection-free', + connectionId: 'connection-go', revision: 1, - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', + slug: 'opencode-go', + name: 'OpenCode Go', + providerType: 'opencode-go', enabled: true, enabledModelIds: createdModels, catalogEntries: [], @@ -555,7 +549,7 @@ test('preserves the provider default inventory beside the recommended model', as createdModels = draft.enabledModelIds; return { kind: 'committed', - connection: { connectionId: 'connection-free', revision: 1 }, + connection: { connectionId: 'connection-go', revision: 1 }, }; }, } as never, @@ -563,14 +557,13 @@ test('preserves the provider default inventory beside the recommended model', as }); await handlers.get('connections:create')?.({}, { - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', - defaultModel: 'nemotron-3-ultra-free', + slug: 'opencode-go', + name: 'OpenCode Go', + providerType: 'opencode-go', + defaultModel: 'minimax-m3', }); - // Snapshot-derived set; assert the contract, not today's ids. - assert.deepEqual(createdModels, [...OPENCODE_FREE_ENABLED_MODEL_IDS]); + assert.deepEqual(createdModels, ['minimax-m3']); }); test('projects the Host default target without inventing a second Connection authority', () => { diff --git a/apps/desktop/src/main/runtime-host-connections-ipc-main.ts b/apps/desktop/src/main/runtime-host-connections-ipc-main.ts index a1f88ec12b..1a31becefa 100644 --- a/apps/desktop/src/main/runtime-host-connections-ipc-main.ts +++ b/apps/desktop/src/main/runtime-host-connections-ipc-main.ts @@ -29,7 +29,6 @@ import { buildChatModelChoices } from '@maka/core/chat-model-choice'; import type { ProjectedLlmConnection } from '@maka/core/llm-connections'; import { connectionEnabledModelIds, - defaultEnabledModelIdsWhenOmitted, PROVIDER_REGISTRY, providerAuthRequiresSecret, } from '@maka/core/llm-connections'; @@ -222,7 +221,6 @@ export function registerRuntimeHostConnectionsIpc( enabled: true, enabledModelIds: connectionEnabledModelIds({ defaultModel: input.defaultModel, - enabledModelIds: defaultEnabledModelIdsWhenOmitted(input.providerType), }), ...(modelOverrides === undefined ? {} : { modelOverrides }), ...(input.requestBodyOverlay === undefined diff --git a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts index 0661ccc60a..66f940d647 100644 --- a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts +++ b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts @@ -146,9 +146,9 @@ const zhCopy = { oauthLoadingDetail: '正在读取本机 OAuth 登录状态,读取完成前不会把未知状态显示成未登录。', oauthUnknownDetail: '暂时无法读取本机 OAuth 登录状态;请刷新页面或重新打开设置。', oauthWaitingDetail: '请到账号连接完成登录;登录成功后会自动出现在模型连接里。', - oauthRetired: '此登录方式已停用', - oauthRetiredDetail: - '这条连接使用的登录方式已从 Maka 移除,无法再登录,也无法用于对话。改用 Anthropic API Key 连接即可继续使用 Claude 模型;删除这条连接会一并清除本机保存的登录凭据。', + providerRetired: '此模型服务已停用', + providerRetiredDetail: + '这条连接已无法用于对话。请添加其他模型连接,并选择新的默认模型;原有对话记录会保留。', credentialLoadingDetail: '正在读取模型凭据状态,读取完成前暂不测试连接或刷新模型。', credentialUnknownDetail: '模型凭据状态暂时没刷新成功,已避免把未知状态显示成未登录或未配置。', testConnection: '测试连接', @@ -372,9 +372,9 @@ const zhTwCopy = { oauthLoadingDetail: '正在讀取本機 OAuth 登入狀態,讀取完成前不會把未知狀態顯示成未登入。', oauthUnknownDetail: '暫時無法讀取本機 OAuth 登入狀態;請重新整理頁面或重新開啟設定。', oauthWaitingDetail: '請到帳號連線完成登入;登入成功後會自動出現在模型連線裡。', - oauthRetired: '此登入方式已停用', - oauthRetiredDetail: - '這條連線使用的登入方式已從 Maka 移除,無法再登入,也無法用於對話。改用 Anthropic API Key 連線即可繼續使用 Claude 模型;刪除這條連線會一併清除本機儲存的登入憑據。', + providerRetired: '此模型服務已停用', + providerRetiredDetail: + '這條連線已無法用於對話。請新增其他模型連線,並選擇新的預設模型;原有對話記錄會保留。', credentialLoadingDetail: '正在讀取模型憑據狀態,讀取完成前暫不測試連線或重新整理模型。', credentialUnknownDetail: '模型憑據狀態暫時沒重新整理成功,已避免把未知狀態顯示成未登入或未設定。', testConnection: '測試連線', @@ -598,9 +598,9 @@ const enCopy: ProviderSettingsCopy = { oauthLoadingDetail: 'Reading the local OAuth status. An unknown state will not be shown as signed out.', oauthUnknownDetail: 'The local OAuth status is temporarily unavailable. Refresh the page or reopen Settings.', oauthWaitingDetail: 'Complete sign-in under account connections. The model connection appears automatically afterward.', - oauthRetired: 'This sign-in path is retired', - oauthRetiredDetail: - 'The sign-in this connection uses was removed from Maka. It can no longer be signed into or used in a conversation. Add an Anthropic API key connection to keep using Claude models; deleting this connection also clears the sign-in credential stored on this machine.', + providerRetired: 'This provider is retired', + providerRetiredDetail: + 'This connection can no longer be used for conversations. Add another provider and choose a new default model. Existing conversations are kept.', credentialLoadingDetail: 'Reading model credential status. Connection tests and model refresh are paused until it finishes.', credentialUnknownDetail: 'Model credential status could not be refreshed, so the connection is not being mislabeled as signed out or unconfigured.', testConnection: 'Test connection', diff --git a/apps/desktop/src/renderer/locales/onboarding-copy.ts b/apps/desktop/src/renderer/locales/onboarding-copy.ts index f408b0ab3b..fb4fd82958 100644 --- a/apps/desktop/src/renderer/locales/onboarding-copy.ts +++ b/apps/desktop/src/renderer/locales/onboarding-copy.ts @@ -80,8 +80,8 @@ const ONBOARDING_COPY_BY_LOCALE: UiCatalog = { }, 'blocked:all_connections_retired': { eyebrow: '连接需要处理', - title: '现有连接的登录方式已停用。', - body: '这些连接使用的登录方式已从 Maka 移除,无法再登录,也无法用于对话。添加一个新的模型连接即可继续。', + title: '现有连接的模型服务已停用。', + body: '这些连接已无法用于对话。添加一个新的模型连接即可继续。', cta: { label: '添加模型连接' }, tone: 'destructive', }, @@ -129,8 +129,8 @@ const ONBOARDING_COPY_BY_LOCALE: UiCatalog = { }, 'blocked:all_connections_retired': { eyebrow: '連線需要處理', - title: '現有連線的登入方式已停用。', - body: '這些連線使用的登入方式已從 Maka 移除,無法再登入,也無法用於對話。新增一個新的模型連線即可繼續。', + title: '現有連線的模型服務已停用。', + body: '這些連線已無法用於對話。新增一個模型連線即可繼續。', cta: { label: '新增模型連線' }, tone: 'destructive', }, @@ -178,8 +178,8 @@ const ONBOARDING_COPY_BY_LOCALE: UiCatalog = { }, 'blocked:all_connections_retired': { eyebrow: 'Connection needs attention', - title: 'The sign-in your connections use is retired.', - body: 'The sign-in these connections use was removed from Maka. They can no longer be signed into or used in a conversation. Add a new model connection to continue.', + title: 'Your model providers are retired.', + body: 'These connections can no longer be used for conversations. Add a new model connection to continue.', cta: { label: 'Add a model connection' }, tone: 'destructive', }, diff --git a/apps/desktop/src/renderer/onboarding-provider-types.ts b/apps/desktop/src/renderer/onboarding-provider-types.ts index b669728e36..711974f6c3 100644 --- a/apps/desktop/src/renderer/onboarding-provider-types.ts +++ b/apps/desktop/src/renderer/onboarding-provider-types.ts @@ -20,7 +20,6 @@ import type { ProviderType } from '@maka/core/llm-connections'; export const FIRST_RUN_PROVIDER_TYPES = [ - 'opencode-free', 'opencode-go', 'openai', 'anthropic', diff --git a/apps/desktop/src/renderer/settings/provider-connection-detail.tsx b/apps/desktop/src/renderer/settings/provider-connection-detail.tsx index 76657e7ede..f79878cf8e 100644 --- a/apps/desktop/src/renderer/settings/provider-connection-detail.tsx +++ b/apps/desktop/src/renderer/settings/provider-connection-detail.tsx @@ -435,10 +435,11 @@ function ConnectionDetailInner(props: ConnectionDetailProps) { return ( - {needsOAuth && ( - retired ? ( - - ) : oauthLoginService ? ( + {retired && ( + + )} + {needsOAuth && !retired && ( + oauthLoginService ? ( {/* The name row is outside the key/endpoint guard below: a connection with neither — an OAuth subscription, say — still has a name, and diff --git a/apps/desktop/src/renderer/settings/provider-display-copy.ts b/apps/desktop/src/renderer/settings/provider-display-copy.ts index 25cfb4db61..f7e6b1fc74 100644 --- a/apps/desktop/src/renderer/settings/provider-display-copy.ts +++ b/apps/desktop/src/renderer/settings/provider-display-copy.ts @@ -295,9 +295,9 @@ export const PROVIDER_DISPLAY_COPY = { en: { name: 'OpenCode Go', description: 'Low-cost subscription to curated open coding models.', badge: 'Plan' }, }, 'opencode-free': { - 'zh-CN': { name: 'OpenCode Free', description: '免费匿名 OpenCode Zen 模型,无需密钥,按 IP 限速', badge: 'Free' }, - 'zh-TW': { name: 'OpenCode Free', description: '免費匿名 OpenCode Zen 模型,無需金鑰,按 IP 限速', badge: 'Free' }, - en: { name: 'OpenCode Free', description: 'Free anonymous OpenCode Zen models — no API key, IP-limited.', badge: 'Free' }, + 'zh-CN': { name: 'OpenCode Free', description: '已停用:OpenCode 免费服务仅限其官方客户端使用。', badge: 'Free' }, + 'zh-TW': { name: 'OpenCode Free', description: '已停用:OpenCode 免費服務僅限其官方用戶端使用。', badge: 'Free' }, + en: { name: 'OpenCode Free', description: 'Retired: OpenCode restricts its free tier to its own client.', badge: 'Free' }, }, commandcode: { 'zh-CN': { name: 'Command Code', description: '使用 Command Code 套餐额度,连接后自动获取模型。', badge: 'Coding' }, diff --git a/apps/desktop/src/renderer/settings/use-connection-detail.ts b/apps/desktop/src/renderer/settings/use-connection-detail.ts index d40257f0e4..8c57144ffe 100644 --- a/apps/desktop/src/renderer/settings/use-connection-detail.ts +++ b/apps/desktop/src/renderer/settings/use-connection-detail.ts @@ -516,7 +516,7 @@ export function useConnectionDetail(props: ConnectionDetailProps) { if (!isConnectionDetailCurrent(lifecycle)) return; if (result.ok) { // The backend probes the enabled models first, then the provider - // fallbacks (opencode-free tries each in turn until one answers). When + // fallback. When // the model that actually answered isn't one the user enabled, a plain // "connection succeeded · " reads as if their selection never // took — and hides that their chosen model is currently down. Name both diff --git a/apps/desktop/stories/settings/provider-settings.stories.tsx b/apps/desktop/stories/settings/provider-settings.stories.tsx index 0819df57cf..0ba93cdd38 100644 --- a/apps/desktop/stories/settings/provider-settings.stories.tsx +++ b/apps/desktop/stories/settings/provider-settings.stories.tsx @@ -62,6 +62,7 @@ type AutoOpenTarget = | 'detail-static' | 'detail-relay' | 'detail-large' + | 'detail-retired' | 'add' | 'catalog' | 'oauth' @@ -700,12 +701,15 @@ function clickAutoOpenTarget(root: HTMLElement, target: AutoOpenTarget): boolean || target === 'detail-static' || target === 'detail-relay' || target === 'detail-large' + || target === 'detail-retired' ) { // ListItem's clickable surface is an invisible button inside the row, so // the row is located by its slug hook and the button taken from within it. const slug = target === 'detail' ? 'zai-live' + : target === 'detail-retired' + ? 'opencode-free' : target === 'detail-alibaba' ? 'alibaba-token-plan-cn' : target === 'detail-static' @@ -790,7 +794,8 @@ export const EmptyProviders: Story = { render: () => , play: async ({ canvasElement }) => { await waitFor(() => { - expect(canvasElement.querySelector('.providerCatalogRow[data-provider="opencode-free"]')).not.toBeNull(); + expect(canvasElement.querySelector('.providerCatalogRow[data-provider="opencode-go"]')).not.toBeNull(); + expect(canvasElement.querySelector('.providerCatalogRow[data-provider="opencode-free"]')).toBeNull(); }, { timeout: 5_000 }); expect(canvasElement.querySelector('[data-maka-contract="provider-catalog"]')).toBeNull(); }, @@ -806,6 +811,25 @@ export const ConnectionDetailPage: Story = { ), }; +// Real path: after upgrading, open the retained OpenCode Free connection in Settings. +export const RetiredFreeConnection: Story = { + render: () => ( + + ), + play: async ({ canvasElement }) => { + await waitFor(() => { + expect(within(canvasElement).getByRole('alert')).toHaveTextContent(/已停用|retired/); + }); + expect(within(canvasElement).queryByRole('button', { name: /测试连接|測試連線|Test connection/ })).toBeNull(); + }, +}; + // Real path: 设置 → 模型 → an OpenRouter connection after fetching hundreds of models. // The fetched snapshot is local; browsing it never needs an API key. export const LargeConnectionDetail: Story = { diff --git a/packages/cli/src/__tests__/acp-child-process.test.ts b/packages/cli/src/__tests__/acp-child-process.test.ts index ceaa5ec70f..616ba06cc0 100644 --- a/packages/cli/src/__tests__/acp-child-process.test.ts +++ b/packages/cli/src/__tests__/acp-child-process.test.ts @@ -334,7 +334,10 @@ describe('Maka ACP child process', () => { assert.deepEqual(await harness.waitForExit(), { code: 0, signal: null }); assert.equal(harness.stderr, ''); }, - { startRuntimeHost: true }, + { + startRuntimeHost: true, + model: { id: 'acp-session-fixture', thinkingLevels: [] }, + }, ); }); diff --git a/packages/cli/src/runtime-host-tui-command.ts b/packages/cli/src/runtime-host-tui-command.ts index 84546337a3..09fde11ea2 100644 --- a/packages/cli/src/runtime-host-tui-command.ts +++ b/packages/cli/src/runtime-host-tui-command.ts @@ -73,7 +73,7 @@ export async function runRuntimeHostTui(input: RunRuntimeHostTuiInput): Promise< input.locale, input.hostProfileId, ); - if (!configured) throw error; + if (!configured) return 0; context = await createRuntimeHostTuiContext(contextInput); } let ownerAction: (() => Promise) | undefined; diff --git a/packages/core/src/__tests__/llm-connections.test.ts b/packages/core/src/__tests__/llm-connections.test.ts index 207903aad4..80235c1fcd 100644 --- a/packages/core/src/__tests__/llm-connections.test.ts +++ b/packages/core/src/__tests__/llm-connections.test.ts @@ -26,7 +26,6 @@ import { } from '../model-metadata.js'; import { PROVIDER_REGISTRY, providerFallbackModelIds } from '../provider-registry.js'; import { - authorizeConnectionModel, effectiveBaseUrl, normalizeConnectionBaseUrl, providerAuthRequiresSecret, @@ -415,41 +414,18 @@ test('provider recognition does not resolve inherited object members', () => { } }); -test('a quarantined model id is vetoed even when enabled and present in the inventory', () => { +test('stored OpenCode Free models cannot be selected', () => { const connection = { - providerType: 'opencode-free' as ProviderType, - enabledModelIds: ['nemotron-3-ultra-free', 'muse-spark-1.2-contributor-free'], - models: [{ id: 'nemotron-3-ultra-free' }, { id: 'muse-spark-1.2-contributor-free' }], - }; - assert.equal(authorizeConnectionModel(connection, 'muse-spark-1.2-contributor-free'), undefined); - assert.deepEqual(authorizeConnectionModel(connection, 'nemotron-3-ultra-free'), { - id: 'nemotron-3-ultra-free', - }); -}); - -test('a quarantined stored default is dropped from the picker, not re-added as a missing-default row', () => { - // The retired `x-preview-f-free` was picker-visible before the quarantine, so - // an upgrade connection can carry it as `defaultModel` and enabled. `models` - // and `enabledModelIds` are filtered against `brokenModelIds`, but the raw - // `defaultModel` used to pass through unfiltered and `makeMissingDefaultEntry` - // re-added it as a selectable `provider_default` row — visible and pickable - // while `authorizeConnectionModel` vetoed the same id. The picker and the send - // authority must agree: neither offers it, and the live model still renders. - const connection = { - connectionId: 'connection-opencode-free', + connectionId: 'stored-free', slug: 'opencode-free', name: 'OpenCode Free', providerType: 'opencode-free' as ProviderType, enabled: true, - defaultModel: 'x-preview-f-free', - enabledModelIds: ['x-preview-f-free', 'nemotron-3-ultra-free'], - models: [{ id: 'x-preview-f-free' }, { id: 'nemotron-3-ultra-free' }], - modelSource: 'fetched' as const, + defaultModel: 'nemotron-3-ultra-free', + enabledModelIds: ['nemotron-3-ultra-free'], + models: [{ id: 'nemotron-3-ultra-free' }], createdAt: 1, updatedAt: 1, }; - const models = chatModelChoicesFor([connection]).map(({ model }) => model); - assert.ok(!models.includes('x-preview-f-free'), 'quarantined default must not be offered'); - assert.ok(models.includes('nemotron-3-ultra-free'), 'live enabled model still renders'); - assert.equal(authorizeConnectionModel(connection, 'x-preview-f-free'), undefined); + assert.deepEqual(chatModelChoicesFor([connection]), []); }); diff --git a/packages/core/src/__tests__/model-catalog.test.ts b/packages/core/src/__tests__/model-catalog.test.ts index 193171fba2..eaf3d3c563 100644 --- a/packages/core/src/__tests__/model-catalog.test.ts +++ b/packages/core/src/__tests__/model-catalog.test.ts @@ -332,31 +332,31 @@ test('catalog provenance follows the projected model facts marker used in produc test('fallback provider catalogs include projected facts-backed models', () => { const entries = buildConnectionModelCatalogEntries({ connection: { - slug: 'opencode-free-facts', - providerType: 'opencode-free', - defaultModel: 'custom-free-model', + slug: 'ark-facts', + providerType: 'volcengine-ark', + defaultModel: 'custom-model', models: [ { - id: 'custom-free-model', + id: 'custom-model', contextWindow: 128_000, }, ], modelSource: 'fallback', }, }); - const entry = entries.find((candidate) => candidate.id === 'custom-free-model'); + const entry = entries.find((candidate) => candidate.id === 'custom-model'); assert.equal(entry?.contextWindow, 128_000); }); test('fallback provider catalogs apply facts to known fallback models', () => { const entries = buildConnectionModelCatalogEntries({ connection: { - slug: 'opencode-free-known-facts', - providerType: 'opencode-free', - defaultModel: 'nemotron-3-ultra-free', + slug: 'ark-known-facts', + providerType: 'volcengine-ark', + defaultModel: 'doubao-seed-2-0-pro-260215', models: [ { - id: 'nemotron-3-ultra-free', + id: 'doubao-seed-2-0-pro-260215', contextWindow: 200_000, inputLimit: 200_000, capabilities: { chat: true }, @@ -365,7 +365,7 @@ test('fallback provider catalogs apply facts to known fallback models', () => { modelSource: 'fallback', }, }); - const entry = entries.find((candidate) => candidate.id === 'nemotron-3-ultra-free'); + const entry = entries.find((candidate) => candidate.id === 'doubao-seed-2-0-pro-260215'); assert.equal(entry?.contextWindow, 200_000); }); diff --git a/packages/core/src/__tests__/provider-catalog-contract.test.ts b/packages/core/src/__tests__/provider-catalog-contract.test.ts index fef332b3bc..a8ef42552f 100644 --- a/packages/core/src/__tests__/provider-catalog-contract.test.ts +++ b/packages/core/src/__tests__/provider-catalog-contract.test.ts @@ -20,7 +20,6 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { - defaultEnabledModelIdsWhenOmitted, deriveConnectionSlug, validateConnectionBaseUrl, validateSlug, @@ -229,7 +228,7 @@ describe('retired provider contract', () => { ); it('pins the entries this catalog retires', () => { - assert.deepEqual(retired, ['claude-subscription']); + assert.deepEqual(retired, ['opencode-free', 'claude-subscription']); }); it('keeps a retired provider registered but unwired', () => { @@ -283,8 +282,9 @@ describe('retired provider contract', () => { connection: { slug: `${type}-stored`, providerType: type, - defaultModel: PROVIDER_REGISTRY[type].fallbackModels[0] ?? '', - models: undefined, + defaultModel: 'stored-model', + enabledModelIds: ['stored-model'], + models: [{ id: 'stored-model' }], modelSource: 'fallback', }, }); @@ -331,34 +331,3 @@ describe('provider catalog contract — fallback lifecycle', () => { assert.deepEqual(regressed, []); }); }); - -describe('opencode-free retired-model quarantine', () => { - const opencodeFree = PROVIDER_REGISTRY['opencode-free']; - - // Ox Alpha Free (x-preview-f-free) was retired upstream on OpenCode Zen while - // models.dev still snapshots it free+active, so the derivation would keep - // offering it as a default-enabled, picker-visible row. Remove this assertion - // together with the OPENCODE_FREE_BROKEN_MODEL_IDS entry once the snapshot - // marks it deprecated (or upstream serves it again). - it('quarantines x-preview-f-free out of the offered free models', () => { - assert.ok(opencodeFree.brokenModelIds?.includes('x-preview-f-free')); - assert.ok(!providerFallbackModelIds(opencodeFree).includes('x-preview-f-free')); - assert.ok( - !(defaultEnabledModelIdsWhenOmitted('opencode-free') ?? []).includes('x-preview-f-free'), - ); - }); - - // Mechanism guard, independent of which ids the deny-list holds: a quarantined - // id must never leak back into the offered candidates through either path. - it('never offers a quarantined broken id as a free candidate', () => { - const broken = new Set(opencodeFree.brokenModelIds ?? []); - const offered = [ - ...providerFallbackModelIds(opencodeFree), - ...(defaultEnabledModelIdsWhenOmitted('opencode-free') ?? []), - ]; - assert.deepEqual( - offered.filter((id) => broken.has(id)), - [], - ); - }); -}); diff --git a/packages/core/src/llm-connections.ts b/packages/core/src/llm-connections.ts index f5a4a697eb..035228a5fc 100644 --- a/packages/core/src/llm-connections.ts +++ b/packages/core/src/llm-connections.ts @@ -37,7 +37,6 @@ import type { import { CODEX_SUBSCRIPTION_UNSUPPORTED_CHATGPT_MODELS } from './codex-model-compatibility.js'; import { CATALOG_PROVIDER_TYPES, - OPENCODE_FREE_DEFAULT_MODEL, PROVIDER_REGISTRY, RECOMMENDED_PROVIDER_TYPES, providerDefaultsOf, @@ -56,7 +55,6 @@ import { export { CODEX_SUBSCRIPTION_UNSUPPORTED_CHATGPT_MODELS }; export { CATALOG_PROVIDER_TYPES, - OPENCODE_FREE_DEFAULT_MODEL, PROVIDER_REGISTRY, RECOMMENDED_PROVIDER_TYPES, providerDefaultsOf, @@ -245,7 +243,7 @@ export function connectionEnabledModelIds(connection: { * 3. the Host's entry says the connection can hold a chat on it. * * (3) already subsumes what clients used to re-derive locally: a retired - * provider, a quarantined `brokenModelIds` id, and a model whose metadata says + * provider and a model whose metadata says * it cannot chat are all non-offerable before a client sees them. A client * re-testing any of those against its OWN registry answers for a build that is * not the one running the send. @@ -336,12 +334,6 @@ export function authorizeConnectionModel( ): ModelInfo | undefined { const model = modelId.trim(); if (!model || !connectionEnabledModelIds(connection).includes(model)) return undefined; - // The one veto: quarantined ids fail in a shape the send cannot surface - // (e.g. a billed 200 with an empty completion), so the request settling it - // is not available as the arbiter. See ProviderDefaults.brokenModelIds. - if (providerDefaultsOf(connection.providerType)?.brokenModelIds?.includes(model)) { - return undefined; - } // The observed row wins wherever it exists: it carries wire metadata such as // `apiProtocol`, and capabilities, which a synthesized entry cannot. Absent // capabilities already mean "unknown", not "unsupported". @@ -531,20 +523,6 @@ export interface ConnectionTestResult { errorClass?: ConnectionTestErrorClass; } -/** - * The models a connection created without an explicit selection starts with, - * or undefined when the provider seeds nothing. Derived from the provider's - * shipped baseline rather than listed a second time: the two can then never - * disagree about what "all of them" means. - */ -export function defaultEnabledModelIdsWhenOmitted( - providerType: ProviderType, -): readonly string[] | undefined { - const defaults = providerDefaultsOf(providerType); - if (!defaults?.enableShippedModelsByDefault) return undefined; - return providerFallbackModelIds(defaults); -} - export function providerAuthRequiresSecret(providerType: ProviderType): boolean { const authKind = providerDefaultsOf(providerType)?.authKind; return authKind === 'api_key' || authKind === 'oauth_token'; diff --git a/packages/core/src/model-catalog.ts b/packages/core/src/model-catalog.ts index 09a1e11fba..5a1026a3f2 100644 --- a/packages/core/src/model-catalog.ts +++ b/packages/core/src/model-catalog.ts @@ -204,32 +204,16 @@ export function buildConnectionModelCatalogEntries( // Mirrors `isRealConnection` in connection-readiness.ts. if (!defaults) return []; const supportsModelDiscovery = providerSupportsModelDiscovery(connection.providerType); - // Quarantined ids never surface as offerable entries — from any source, - // including inventories stored or selections made before the quarantine — - // mirroring the `authorizeConnectionModel` veto. - const broken = new Set(defaults.brokenModelIds ?? []); const fallbackModels = providerFallbackModelIds(defaults); - // A quarantined id persisted as this connection's `defaultModel` must not - // re-enter the catalog either. `models` and `enabledModelIds` are filtered - // below, but a broken default reaches `makeMissingDefaultEntry` unfiltered and - // would be re-added as a selectable `provider_default` row — picker-visible - // and default-capable while `authorizeConnectionModel` vetoes the same id. A - // reachable persisted state: the id was picker-visible before the quarantine. - // Dropping it leaves the connection with no valid default (readiness reports - // `missing_model`), which is what a model that can no longer send warrants. - const defaultModel = broken.has((connection.defaultModel ?? '').trim()) - ? undefined - : connection.defaultModel; + const defaultModel = connection.defaultModel; const fallbackModelIds = new Set(fallbackModels); - const projectedModelsById = new Map( - (connection.models ?? []).filter(({ id }) => !broken.has(id)).map((model) => [model.id, model]), - ); + const projectedModelsById = new Map((connection.models ?? []).map((model) => [model.id, model])); // Fallback providers have no live inventory, but a projected connection can // still carry enabled model-facts entries that are absent from the static // list. Keep both sets in the catalog so those user-declared models retain // their metadata. const models = supportsModelDiscovery - ? connection.models?.filter(({ id }) => !broken.has(id)) + ? connection.models : [ ...fallbackModels.map( (id) => @@ -238,9 +222,7 @@ export function buildConnectionModelCatalogEntries( ...displayNameForKnownModel(connection.providerType, id), }, ), - ...(connection.models ?? []).filter( - (model) => !broken.has(model.id) && !fallbackModelIds.has(model.id), - ), + ...(connection.models ?? []).filter((model) => !fallbackModelIds.has(model.id)), ]; return buildModelCatalogEntries({ providerType: connection.providerType, @@ -259,7 +241,7 @@ export function buildConnectionModelCatalogEntries( savedModelIds: [ ...(connection.enabledModelIds ?? []), ...Object.keys(connection.modelOverrides ?? {}), - ].filter((id) => !broken.has(id)), + ], }); } diff --git a/packages/core/src/model-metadata.ts b/packages/core/src/model-metadata.ts index b3a1ebe11e..aa13094d64 100644 --- a/packages/core/src/model-metadata.ts +++ b/packages/core/src/model-metadata.ts @@ -34,12 +34,6 @@ export interface ModelMetadata { knowledgeCutoff?: string; structuredOutput?: boolean; lastUpdated?: string; - /** - * models.dev prices the model at zero input cost. Marks free-tier - * candidates (e.g. opencode-free); display names are not a contract for - * this, several free models carry no "Free" suffix. - */ - isFree?: boolean; capabilities?: ModelInfo['capabilities']; modalities?: ModelInfo['modalities']; /** diff --git a/packages/core/src/models-dev-projection.ts b/packages/core/src/models-dev-projection.ts index 5a6d1bee9b..a2dc05e981 100644 --- a/packages/core/src/models-dev-projection.ts +++ b/packages/core/src/models-dev-projection.ts @@ -278,7 +278,6 @@ export function projectModelsDevModel( ...(model.knowledge !== undefined ? { knowledgeCutoff: model.knowledge } : {}), ...(model.structured_output !== undefined ? { structuredOutput: model.structured_output } : {}), ...(model.last_updated !== undefined ? { lastUpdated: model.last_updated } : {}), - ...(model.cost?.input === 0 ? { isFree: true } : {}), capabilities: { ...(modalities ? { vision: modalities.input.includes('image') } : {}), reasoning: model.reasoning === true, diff --git a/packages/core/src/provider-registry.ts b/packages/core/src/provider-registry.ts index 1fa3d8ce71..d3a6d3005f 100644 --- a/packages/core/src/provider-registry.ts +++ b/packages/core/src/provider-registry.ts @@ -23,8 +23,6 @@ import { GENERATED_MODELS_DEV_PROVIDER_FACTS, } from './model-metadata.generated.js'; -export const OPENCODE_FREE_DEFAULT_MODEL = 'nemotron-3-ultra-free'; - export type ProviderCategory = 'oauth' | 'domestic' | 'overseas' | 'local' | 'custom'; export type ProviderCatalogGroup = 'recommended' | 'plans' | 'api' | 'aggregators' | 'local'; @@ -133,16 +131,9 @@ export interface ProviderDefaults { authKind: 'api_key' | 'optional_api_key' | 'oauth_token' | 'none'; /** * The baseline this provider ships: what it offers with no live list to go - * on. Read it through `providerFallbackModelIds`, never directly — the - * accessor subtracts `brokenModelIds`. + * on. */ fallbackModels: string[]; - /** - * A new connection to this provider starts with its whole shipped baseline - * enabled instead of nothing. Set where a provider costs the user nothing to - * call, so the models are on the moment the connection exists. - */ - enableShippedModelsByDefault?: true; status: 'ready' | 'phase3-experimental'; runtimeAdapter: ProviderRuntimeAdapter; /** Additional request protocols; omitted models still use runtimeAdapter. */ @@ -154,13 +145,6 @@ export interface ProviderDefaults { * registered so stored connections still decode; it just cannot be used. */ retired?: true; - /** - * Models with dated evidence of persistent breakage whose failure shape the - * send itself cannot surface (e.g. empty completions that still bill). - * Vetoed in `authorizeConnectionModel` and omitted from catalog offers — - * the one exception to "the user's selection is the authorization". - */ - brokenModelIds?: readonly string[]; modelDiscovery: ProviderModelDiscovery; category: ProviderCategory; catalogGroup?: ProviderCatalogGroup; @@ -695,48 +679,6 @@ const opencodeGoModelIds = toolCallingModelIds( GENERATED_MODELS_DEV_METADATA['opencode-go'], ['minimax-m3'], ).filter((id) => GENERATED_MODELS_DEV_METADATA['opencode-go'][id]?.lifecycle !== 'deprecated'); -// opencode-free is Maka's first-class free anonymous default. It shares the -// OpenCode Zen endpoint and model ids, exposing the active tool-capable -// models the models.dev snapshot marks `isFree` (zero input cost). Deriving -// the set from the snapshot lets routine metadata refreshes rotate free -// models in and out instead of letting a hardcoded pin rot (#3409). -// -// Persistently broken free models, excluded with dated evidence. Deny-only: -// a stale entry hides at most one healthy model, the opposite failure mode of -// the allow-list pin this replaced. Entries should be re-probed on snapshot -// refreshes and removed once the model produces content again. -// 2026-08-21 muse-spark-1.2-contributor-free: anonymous completions return -// 200 with an empty message and bill the full token budget (4 consecutive -// probes, max_tokens 8–200) — a failure shape that even "the send settles it" -// cannot surface, which is why these ids are also vetoed in -// `authorizeConnectionModel` rather than merely dropped from this derivation. -// 2026-08-30 x-preview-f-free (Ox Alpha Free): retired upstream — dropped from -// the anonymous /models listing and every completion returns HTTP 401 -// {"type":"ModelError","message":"Model x-preview-f-free is not supported"}. -// models.dev still snapshots it as free+active, so the derivation kept offering -// it as a default-enabled, picker-visible row until this quarantine. Remove -// once the snapshot marks it deprecated (or upstream serves it again). -const OPENCODE_FREE_BROKEN_MODEL_IDS = new Set([ - 'muse-spark-1.2-contributor-free', - 'x-preview-f-free', -]); -const opencodeFreeModelIds = toolCallingModelIds( - 'OpenCode Free', - Object.fromEntries( - Object.entries(GENERATED_MODELS_DEV_METADATA.opencode).filter( - ([id, model]) => - model.isFree === true && - model.lifecycle !== 'deprecated' && - !OPENCODE_FREE_BROKEN_MODEL_IDS.has(id), - ), - ), - [OPENCODE_FREE_DEFAULT_MODEL], -); -if (opencodeFreeModelIds[0] !== OPENCODE_FREE_DEFAULT_MODEL) { - throw new Error( - `models.dev opencode snapshot no longer serves ${OPENCODE_FREE_DEFAULT_MODEL} as an active tool-capable free model; pick a new OPENCODE_FREE_DEFAULT_MODEL`, - ); -} const githubCopilot = GENERATED_MODELS_DEV_PROVIDER_FACTS['github-copilot']; if (githubCopilot.id !== 'github-copilot') { throw new Error('models.dev GitHub Copilot provider facts are missing stable id github-copilot'); @@ -1264,23 +1206,15 @@ const providerRegistry = { label: 'OpenCode Free', baseUrl: opencode.api, authKind: 'none', - fallbackModels: [...opencodeFreeModelIds], - // Free and keyless: nothing is spent by having every shipped model on, and - // a user who just added the connection can send immediately. - enableShippedModelsByDefault: true, - brokenModelIds: [...OPENCODE_FREE_BROKEN_MODEL_IDS], - status: 'ready', - runtimeAdapter: { kind: 'openai-compatible', name: 'provider' }, + fallbackModels: [], + status: 'phase3-experimental', + runtimeAdapter: { kind: 'unavailable' }, + retired: true, modelDiscovery: { kind: 'fallback', - reason: - 'The anonymous /models listing describes the full Zen catalog with no cost facts; which models are FREE is a models.dev fact, so the derived candidates are the inventory and the send settles availability.', + reason: 'OpenCode restricts its free tier to the OpenCode client.', }, category: 'overseas', - catalogGroup: 'plans', - signupUrl: 'https://opencode.ai/zen', - catalogOrder: 0, - recommendedOrder: 0, }, togetherai: { label: together.name, @@ -1775,18 +1709,13 @@ export function providerDefaultsOf(providerType: string): ProviderDefaults | und /** * The models a provider offers with no live list to go on: the baseline it - * ships, minus anything quarantined. This is the only reader of + * ships. This is the only reader of * `fallbackModels` — a provider's offline offer has exactly one authority. - * - * `brokenModelIds` subtracts here rather than being pruned from the baseline at - * the source because the ids it names are ones a stored connection may still - * carry from an older shipped list. */ export function providerFallbackModelIds( - defaults: Pick, + defaults: Pick, ): string[] { - const broken = new Set(defaults.brokenModelIds ?? []); - return defaults.fallbackModels.filter((id) => !broken.has(id)); + return [...defaults.fallbackModels]; } /** diff --git a/packages/core/src/runtime-policy.ts b/packages/core/src/runtime-policy.ts index dddae35461..196a0387b8 100644 --- a/packages/core/src/runtime-policy.ts +++ b/packages/core/src/runtime-policy.ts @@ -377,22 +377,6 @@ export interface RemoveCatalogConnectionInput { readonly expected: ConnectionVersionBasis; } -/** - * Built-in seed evolution as one atomic catalog mutation: a row still exactly - * matching a historical system seed follows the current seed — enabled ids AND - * the static inventory — and a system default the migration removes is - * retargeted in the same document write. Any other inventory is a user - * selection and is never touched; an already-null default stays null. - */ -export interface MigrateSystemSeedInput { - readonly slug: string; - readonly providerType: ProviderType; - readonly legacyEnabledModelIds: readonly (readonly string[])[]; - readonly enabledModelIds: readonly string[]; - readonly defaultModelId: string; - readonly retiredModelIds: readonly string[]; -} - export interface SetDefaultConnectionTargetInput { readonly expectedCatalogRevision: Revision; readonly target: ConnectionTarget | null; diff --git a/packages/runtime-host/src/__tests__/authenticated-websocket.test.ts b/packages/runtime-host/src/__tests__/authenticated-websocket.test.ts index dfc022e4b2..df347063cf 100644 --- a/packages/runtime-host/src/__tests__/authenticated-websocket.test.ts +++ b/packages/runtime-host/src/__tests__/authenticated-websocket.test.ts @@ -60,6 +60,36 @@ const KNOWN_EMPTY_LIVE_RUN_STATE = { runningTurnIds: [], } as const; +async function configureTestModel(local: RuntimeHostConnection): Promise { + const catalog = await local.request('connection.catalog.query', { kind: 'start' }); + assert.equal(catalog.kind, 'page'); + if (catalog.kind !== 'page') assert.fail('Expected the initial catalog page'); + const created = await local.request('connection.catalog.create', { + expectedCatalogRevision: catalog.revision, + connection: { + slug: 'websocket-fixture', + name: 'WebSocket fixture', + providerType: 'openai-compatible', + baseUrl: 'https://websocket-model.invalid/v1', + enabled: true, + enabledModelIds: ['websocket-test-model'], + }, + }); + if (created.kind !== 'committed') assert.fail('Test connection must commit'); + const connectionId = created.connection.connectionId; + const credential = await local.request('credential.vault.set', { + locator: { scope: 'connection', connectionId, kind: 'api_key' }, + expected: null, + secret: 'websocket-fixture-key', + }); + assert.equal(credential.kind, 'committed'); + const selected = await local.request('connection.catalog.set-default-target', { + expectedCatalogRevision: created.catalogRevision, + target: { connectionId, modelId: 'websocket-test-model' }, + }); + assert.equal(selected.kind, 'committed'); +} + test('one Local IPC owner and one authenticated WebSocket Client control the same Session', { timeout: 120_000, }, async () => { @@ -75,6 +105,7 @@ test('one Local IPC owner and one authenticated WebSocket Client control the sam let guest: RuntimeHostConnection | undefined; try { local = requireConnection(await connectRuntimeHost({ rootPath: root, protocol: PROTOCOL })); + await configureTestModel(local); const issued = await local.request('access.credential.issue', { principalKind: 'remote_owner', principalId: 'remote-device', @@ -513,6 +544,7 @@ test('an authenticated WebSocket Client reconnects after service restart to cano let remote: Awaited> | undefined; try { local = requireConnection(await connectRuntimeHost({ rootPath: root, protocol: PROTOCOL })); + await configureTestModel(local); const issued = await local.request('access.credential.issue', { principalKind: 'remote_owner', principalId: 'restart-client', diff --git a/packages/runtime-host/src/__tests__/bootstrap-runtime-policy.test.ts b/packages/runtime-host/src/__tests__/bootstrap-runtime-policy.test.ts index 96d072d059..1de3e62bdb 100644 --- a/packages/runtime-host/src/__tests__/bootstrap-runtime-policy.test.ts +++ b/packages/runtime-host/src/__tests__/bootstrap-runtime-policy.test.ts @@ -22,10 +22,6 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { test } from 'node:test'; -import { - OPENCODE_FREE_DEFAULT_MODEL, - defaultEnabledModelIdsWhenOmitted, -} from '@maka/core/llm-connections'; import { openInteractiveRuntimePolicyStoresForWrite, type RuntimePolicyStoresWriter, @@ -33,9 +29,6 @@ import { import { resolveStorageRoot, tryAcquireInteractiveRootOwner } from '@maka/storage/root-authority'; import { ensureBootstrapRuntimePolicy } from '../server/bootstrap-runtime-policy.js'; -const OPENCODE_FREE_ENABLED_MODEL_IDS: readonly string[] = - defaultEnabledModelIdsWhenOmitted('opencode-free') ?? []; - test('hosted initialization retries invalid input and rotates proxy credentials through the store', async () => { await withFixture(async ({ root, stores }) => { const initialize = (proxyUrl: string) => @@ -65,122 +58,69 @@ test('hosted initialization retries invalid input and rotates proxy credentials }); }); -test('a fresh Host starts with one anonymous runnable target', async () => { +test('a fresh Host without a user API key leaves provider selection to onboarding', async () => { await withFixture(async ({ root, stores }) => { await ensureBootstrapRuntimePolicy({ workspaceRoot: root, stores, environment: {} }); - const catalog = await stores.connectionCatalog.getSnapshot(); - assert.equal(catalog.connections.length, 1); - const free = catalog.connections[0]; - assert.equal(free?.slug, 'opencode-free'); - assert.equal(free?.enabled, true); - // The free set is derived from the models.dev snapshot and rotates with - // refreshes; assert the structural contract, not today's ids. - assert.deepEqual(free?.enabledModelIds, [...OPENCODE_FREE_ENABLED_MODEL_IDS]); - assert.ok(free.enabledModelIds.length > 0); - assert.equal(free.enabledModelIds[0], OPENCODE_FREE_DEFAULT_MODEL); - assert.deepEqual(catalog.defaultTarget, { - connectionId: free?.connectionId, - modelId: OPENCODE_FREE_DEFAULT_MODEL, - }); - assert.deepEqual( - catalog.connections.map(({ slug }) => slug), - ['opencode-free'], - ); - }); -}); - -test('reconciles retired OpenCode Free models without removing user models', async () => { - await withFixture(async ({ root, stores }) => { - const created = await stores.connectionCatalog.create({ - expectedCatalogRevision: 0, - connection: { - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', - enabled: true, - enabledModelIds: ['nemotron-3-ultra-free', 'deepseek-v4-flash-free', 'user-model'], - }, - }); - assert.equal(created.kind, 'committed'); - const connection = created.snapshot.connections[0]!; - const updated = await stores.connectionCatalog.update({ - expected: { connectionId: connection.connectionId, revision: connection.revision }, - changes: { - name: connection.name, - enabled: true, - enabledModelIds: connection.enabledModelIds, - }, - }); - assert.equal(updated.kind, 'committed'); - const updatedConnection = updated.snapshot.connections[0]!; - const defaulted = await stores.connectionCatalog.setDefaultTarget({ - expectedCatalogRevision: updated.snapshot.revision, - target: { connectionId: updatedConnection.connectionId, modelId: 'deepseek-v4-flash-free' }, - }); - assert.equal(defaulted.kind, 'committed'); - - await ensureBootstrapRuntimePolicy({ workspaceRoot: root, stores, environment: {} }); - - const migrated = (await stores.connectionCatalog.getSnapshot()).connections.find( - ({ slug }) => slug === 'opencode-free', - ); - assert.deepEqual(migrated?.enabledModelIds, ['nemotron-3-ultra-free', 'user-model']); - // The row stores no inventory of its own: this provider ships one, and the - // resolver prepends the current build's list to whatever the row holds. - assert.deepEqual(migrated?.models, []); - assert.deepEqual((await stores.connectionCatalog.getSnapshot()).defaultTarget, { - connectionId: migrated?.connectionId, - modelId: 'nemotron-3-ultra-free', - }); + assert.deepEqual(catalog.connections, []); + assert.equal(catalog.defaultTarget, null); }); }); -test('bootstrap resumes after interruption and prefers the supported environment key', async () => { +test('an interrupted environment import resumes credential setup before selecting a default', async () => { await withFixture(async ({ root, stores }) => { const created = await stores.connectionCatalog.create({ expectedCatalogRevision: 0, connection: { - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', + slug: 'env-anthropic', + name: 'Anthropic (env)', + providerType: 'anthropic', enabled: true, - enabledModelIds: ['nemotron-3-ultra-free'], + enabledModelIds: ['claude-sonnet-4-5-20250929'], }, }); assert.equal(created.kind, 'committed'); await writeFile( join(root, '.runtime-host-bootstrap.json'), '{"version":1,"state":"initializing"}\n', - 'utf8', ); - + await assert.rejects( + ensureBootstrapRuntimePolicy({ + workspaceRoot: root, + environment: { ANTHROPIC_API_KEY: 'anthropic-secret' }, + stores: { + ...stores, + connectionCatalog: { + ...stores.connectionCatalog, + setDefaultTarget: async () => { + throw new Error('Interrupted default write'); + }, + }, + }, + }), + /Interrupted default write/, + ); await ensureBootstrapRuntimePolicy({ workspaceRoot: root, stores, - environment: { - ANTHROPIC_API_KEY: 'anthropic-secret', - OPENAI_API_KEY: 'openai-secret', - }, + environment: { ANTHROPIC_API_KEY: 'anthropic-secret', OPENAI_API_KEY: 'unused' }, }); - const catalog = await stores.connectionCatalog.getSnapshot(); - assert.deepEqual( - catalog.connections.map(({ slug }) => slug), - ['opencode-free', 'env-anthropic'], - ); - const anthropic = catalog.connections[1]; + assert.equal(catalog.connections.length, 1); + const connection = catalog.connections[0]!; assert.deepEqual(catalog.defaultTarget, { - connectionId: anthropic?.connectionId, + connectionId: connection.connectionId, modelId: 'claude-sonnet-4-5-20250929', }); const status = await stores.credentialVault.getStatus({ scope: 'connection', - connectionId: anthropic!.connectionId, + connectionId: connection.connectionId, kind: 'api_key', }); assert.equal(status.kind, 'status'); if (status.kind === 'status') assert.equal(status.status.configured, true); + await ensureBootstrapRuntimePolicy({ workspaceRoot: root, stores, environment: {} }); + assert.deepEqual(await stores.connectionCatalog.getSnapshot(), catalog); }); }); @@ -233,7 +173,7 @@ test('bootstrap does not alter an existing user catalog', async () => { }); }); -test('an invalid optional environment credential does not keep bootstrap active', async () => { +test('a failed environment credential leaves no unusable default and can be retried', async () => { await withFixture(async ({ root, stores }) => { const errors: unknown[] = []; await ensureBootstrapRuntimePolicy({ @@ -242,143 +182,70 @@ test('an invalid optional environment credential does not keep bootstrap active' environment: { OPENAI_API_KEY: 'x'.repeat(64 * 1024 + 1) }, onDeferredError: (error) => errors.push(error), }); - assert.equal(errors.length, 1); - const catalog = await stores.connectionCatalog.getSnapshot(); - const free = catalog.connections.find(({ slug }) => slug === 'opencode-free'); - assert.deepEqual(catalog.defaultTarget, { - connectionId: free?.connectionId, - modelId: 'nemotron-3-ultra-free', - }); + const failed = await stores.connectionCatalog.getSnapshot(); + assert.deepEqual(failed.connections, []); + assert.equal(failed.defaultTarget, null); await ensureBootstrapRuntimePolicy({ workspaceRoot: root, stores, - environment: { OPENAI_API_KEY: 'x'.repeat(64 * 1024 + 1) }, - onDeferredError: (error) => errors.push(error), + environment: { OPENAI_API_KEY: 'valid-key' }, + }); + const catalog = await stores.connectionCatalog.getSnapshot(); + assert.deepEqual(catalog.defaultTarget, { + connectionId: catalog.connections[0]!.connectionId, + modelId: 'gpt-4o-mini', }); - assert.equal(errors.length, 1); }); }); -test('a historical persisted seed migrates atomically, inventory and default included', async () => { +test('an old Free default is released without deleting the connection or importing a paid replacement', async () => { await withFixture(async ({ root, stores }) => { - // An actual pre-#3409 persisted document: the three-model seed, the - // pinned four-model fallback inventory of that build, and a default - // target on a model the migration removes. const connectionId = '00000000-0000-4000-8000-000000000001'; + const connection = { + connectionId, + revision: 3, + slug: 'opencode-free', + name: 'OpenCode Free', + providerType: 'opencode-free', + enabled: true, + enabledModelIds: ['nemotron-3-ultra-free'], + models: [{ id: 'nemotron-3-ultra-free' }], + modelSource: 'fallback', + modelsFetchedAt: 0, + }; await writeFile( join(root, 'connection-catalog.json'), - `${JSON.stringify({ + JSON.stringify({ schemaVersion: 1, revision: 7, - defaultTarget: { connectionId, modelId: 'deepseek-v4-flash-free' }, - connections: [ - { - connectionId, - revision: 3, - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', - enabled: true, - enabledModelIds: ['nemotron-3-ultra-free', 'mimo-v2.5-free', 'deepseek-v4-flash-free'], - models: [ - { id: 'nemotron-3-ultra-free' }, - { id: 'mimo-v2.5-free' }, - { id: 'big-pickle' }, - { id: 'deepseek-v4-flash-free' }, - ], - modelSource: 'fallback', - modelsFetchedAt: 0, - }, - ], - })}\n`, + defaultTarget: { connectionId, modelId: 'nemotron-3-ultra-free' }, + connections: [connection], + }), ); - - await ensureBootstrapRuntimePolicy({ workspaceRoot: root, stores, environment: {} }); - - // One document write carried all three: enabled ids, the dropped static - // inventory, and the retargeted default. - const catalog = await stores.connectionCatalog.getSnapshot(); - const migrated = catalog.connections.find(({ slug }) => slug === 'opencode-free'); - assert.deepEqual(migrated?.enabledModelIds, [...OPENCODE_FREE_ENABLED_MODEL_IDS]); - // The pinned copy goes rather than being re-pinned to this build's list: - // the resolver prepends the shipped inventory on every read, so a row that - // stores one can only go stale again. - assert.deepEqual(migrated?.models, []); - assert.deepEqual(catalog.defaultTarget, { - connectionId, - modelId: OPENCODE_FREE_DEFAULT_MODEL, - }); - - // Restart on the far side of the commit boundary: a second bootstrap is a - // byte-identical no-op. (On the near side the single write never happened, - // so the original document simply migrates on the next start.) - await ensureBootstrapRuntimePolicy({ workspaceRoot: root, stores, environment: {} }); - assert.deepEqual(await stores.connectionCatalog.getSnapshot(), catalog); - }); -}); - -test('a historical seed with a user-cleared default migrates without inventing one', async () => { - await withFixture(async ({ root, stores }) => { - const connectionId = '00000000-0000-4000-8000-000000000002'; await writeFile( - join(root, 'connection-catalog.json'), - `${JSON.stringify({ - schemaVersion: 1, - revision: 4, - defaultTarget: null, - connections: [ - { - connectionId, - revision: 2, - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', - enabled: true, - enabledModelIds: ['nemotron-3-ultra-free'], - models: [{ id: 'nemotron-3-ultra-free' }], - modelSource: 'fallback', - modelsFetchedAt: 0, - }, - ], - })}\n`, + join(root, '.runtime-host-bootstrap.json'), + '{"version":1,"state":"initializing"}\n', ); - - await ensureBootstrapRuntimePolicy({ workspaceRoot: root, stores, environment: {} }); - + const initialize = () => + ensureBootstrapRuntimePolicy({ + workspaceRoot: root, + stores, + environment: { OPENAI_API_KEY: 'must-not-be-imported' }, + }); + await initialize(); const catalog = await stores.connectionCatalog.getSnapshot(); - const migrated = catalog.connections.find(({ slug }) => slug === 'opencode-free'); - assert.deepEqual(migrated?.enabledModelIds, [...OPENCODE_FREE_ENABLED_MODEL_IDS]); assert.equal(catalog.defaultTarget, null); - }); -}); - -test('a user-modified opencode-free inventory is never migrated', async () => { - // A reordered seed counts as user-modified too: exact sequence equality is - // the (documented, lossy) proof a row is still system-owned. - for (const enabledModelIds of [ - ['nemotron-3-ultra-free', 'big-pickle'], - ['mimo-v2.5-free', 'nemotron-3-ultra-free', 'user-model'], - ]) { - await withFixture(async ({ root, stores }) => { - const created = await stores.connectionCatalog.create({ - expectedCatalogRevision: 0, - connection: { - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', - enabled: true, - enabledModelIds, - }, - }); - assert.equal(created.kind, 'committed'); - const before = await stores.connectionCatalog.getSnapshot(); - - await ensureBootstrapRuntimePolicy({ workspaceRoot: root, stores, environment: {} }); - - assert.deepEqual(await stores.connectionCatalog.getSnapshot(), before); + assert.deepEqual(catalog.connections, [connection]); + assert.deepEqual((await stores.credentialVault.getSnapshot()).entries, []); + const admission = await stores.operations.resolveExecutionConnection({ + kind: 'catalog_slug', + connectionSlug: 'opencode-free', }); - } + assert.equal(admission.kind, 'provider_retired'); + await initialize(); + assert.deepEqual(await stores.connectionCatalog.getSnapshot(), catalog); + }); }); async function withFixture( diff --git a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts index 21949b0bfc..662afd5eb9 100644 --- a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts +++ b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts @@ -915,12 +915,7 @@ test('backend creation admits an enabled model a live list omits', async () => { }); test('backend creation admits an enabled model a snapshot never listed', async () => { - // `opencode-free` has no model-list endpoint, so its discovery run replays - // the array this build shipped and records `modelSource: 'fallback'`. The - // user enabled this id; a release snapshot cannot rule on what an account - // serves (#1584). Until now the only id that could get through an absent - // inventory was a hardcoded `deepseek` / `deepseek-v4-flash` pair (#2896) — - // the same situation, conceded for one provider. + // User-selected models remain usable when a fallback catalog has not listed them. const modelId = 'claude-opus-5'; const backend = await createHostAiSdkBackend( backendCreationFixture({ @@ -930,10 +925,10 @@ test('backend creation admits an enabled model a snapshot never listed', async ( kind: 'ready', connection: { slug: 'backend-creation-connection', - providerType: 'opencode-free', + providerType: 'volcengine-ark', enabledModelIds: [modelId], models: [{ id: 'grok-code' }], - modelSource: 'fetched' as const, + modelSource: 'fallback' as const, }, networkProxy: { enabled: false }, secretMaterial: {}, diff --git a/packages/runtime-host/src/server/bootstrap-runtime-policy.ts b/packages/runtime-host/src/server/bootstrap-runtime-policy.ts index 7aff9c4bec..991c8a9f13 100644 --- a/packages/runtime-host/src/server/bootstrap-runtime-policy.ts +++ b/packages/runtime-host/src/server/bootstrap-runtime-policy.ts @@ -20,11 +20,7 @@ import { randomUUID } from 'node:crypto'; import { readFile, rename, rm, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { - OPENCODE_FREE_DEFAULT_MODEL, - defaultEnabledModelIdsWhenOmitted, - type ProviderType, -} from '@maka/core/llm-connections'; +import type { ProviderType } from '@maka/core/llm-connections'; import type { ConnectionCatalogEntry } from '@maka/core/runtime-policy'; import type { RuntimePolicyStoresWriter } from '@maka/storage/runtime-policy-stores'; @@ -42,23 +38,15 @@ interface BootstrapSeed { readonly providerType: ProviderType; readonly enabledModelIds: readonly string[]; readonly baseUrl?: string; - readonly secret?: string; + readonly secret: string; } -/** - * What the seeded OpenCode Free connection starts with — the provider's own - * declaration of the models a fresh connection to it enables, so the seed and - * a hand-added connection cannot drift apart. - */ -const OPENCODE_FREE_SEED_MODEL_IDS: readonly string[] = - defaultEnabledModelIdsWhenOmitted('opencode-free') ?? []; - interface BootstrapJournal { readonly version: 1; readonly state: 'initializing'; } -/** Establishes a usable first target before Runtime Host accepts clients. */ +/** Imports a user's environment credential on first start, before accepting clients. */ export async function ensureBootstrapRuntimePolicy(input: { readonly workspaceRoot: string; readonly stores: RuntimePolicyStoresWriter; @@ -70,52 +58,30 @@ export async function ensureBootstrapRuntimePolicy(input: { const journalPath = join(input.workspaceRoot, JOURNAL_FILE); const resuming = await readJournal(journalPath); const initialCatalog = await input.stores.connectionCatalog.getSnapshot(); - if (initialCatalog.connections.length > 0) { - // One atomic catalog mutation: enabled ids, static inventory, and a - // retarget of a system default the migration removes all land in the same - // document write, so no restart can observe a half-migrated row. - try { - await input.stores.connectionCatalog.migrateSystemSeed({ - slug: 'opencode-free', - providerType: 'opencode-free', - legacyEnabledModelIds: LEGACY_OPENCODE_FREE_SEEDS, - enabledModelIds: OPENCODE_FREE_SEED_MODEL_IDS, - defaultModelId: OPENCODE_FREE_DEFAULT_MODEL, - retiredModelIds: retiredOpencodeFreeModelIds(), - }); - } catch (error) { - input.onDeferredError?.(error); - } - } - if (!resuming) { - if (initialCatalog.connections.length > 0) return; - await writeJournal(journalPath); + if (!resuming && initialCatalog.connections.length > 0) return; + const seed = bootstrapSeed(input.environment ?? process.env); + if (!seed || initialCatalog.connections.some((connection) => connection.slug !== seed.slug)) { + await rm(journalPath, { force: true }); + return; } - - const seeds = bootstrapSeeds(input.environment ?? process.env); - const { connection: free } = await ensureConnection(input.stores, seeds[0]!); - await setDefaultIfMissing(input.stores, free); - await rm(journalPath, { force: true }); - + if (!resuming) await writeJournal(journalPath); + let connection: ConnectionCatalogEntry; try { - let preferred = free; - for (const seed of seeds.slice(1)) { - const ensured = await ensureConnection(input.stores, seed); - const connection = ensured.connection; - if (seed.secret) { - try { - await ensureCredential(input.stores, connection, seed.secret); - } catch (error) { - if (ensured.created) await removeFailedBootstrapConnection(input.stores, connection); - throw error; - } - } - preferred = connection; + const ensured = await ensureConnection(input.stores, seed); + connection = ensured.connection; + try { + await ensureCredential(input.stores, connection, seed.secret); + } catch (error) { + if (ensured.created) await removeFailedBootstrapConnection(input.stores, ensured.connection); + throw error; } - await replaceBootstrapDefault(input.stores, free, preferred); } catch (error) { input.onDeferredError?.(error); + await rm(journalPath, { force: true }); + return; } + await setDefaultIfMissing(input.stores, connection); + await rm(journalPath, { force: true }); } async function initializeHostedPolicy( @@ -182,45 +148,37 @@ async function initializeHostedPolicy( if (result.kind !== 'committed') throw new Error('Hosted proxy initialization failed'); } -function bootstrapSeeds(environment: BootstrapEnvironment): readonly BootstrapSeed[] { - const seeds: BootstrapSeed[] = [ - { - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', - enabledModelIds: OPENCODE_FREE_SEED_MODEL_IDS, - }, - ]; +function bootstrapSeed(environment: BootstrapEnvironment): BootstrapSeed | undefined { const deepseek = environment.DEEPSEEK_API_KEY?.trim(); const anthropic = environment.ANTHROPIC_API_KEY?.trim(); const openai = environment.OPENAI_API_KEY?.trim(); if (deepseek) { - seeds.push({ + return { slug: 'env-deepseek', name: 'DeepSeek (env)', providerType: 'deepseek', enabledModelIds: ['deepseek-v4-flash'], baseUrl: environment.DEEPSEEK_BASE_URL?.trim() || 'https://api.deepseek.com', secret: deepseek, - }); + }; } else if (anthropic) { - seeds.push({ + return { slug: 'env-anthropic', name: 'Anthropic (env)', providerType: 'anthropic', enabledModelIds: ['claude-sonnet-4-5-20250929'], secret: anthropic, - }); + }; } else if (openai) { - seeds.push({ + return { slug: 'env-openai', name: 'OpenAI (env)', providerType: 'openai', enabledModelIds: ['gpt-4o-mini'], secret: openai, - }); + }; } - return seeds; + return undefined; } async function ensureConnection( @@ -256,26 +214,6 @@ async function ensureConnection( throw new Error(`Bootstrap Connection could not be created: ${seed.slug}`); } -/** - * Every opencode-free inventory a past release seeded, verbatim. A row still - * equal to one of these is provably system-owned and may follow the current - * seed; any other value is a user selection and is never touched. Every - * release that changes the derived seed must append the previous value here, - * or rows it planted read as user selections forever. (This exact-match - * enumeration is deliberately lossy; the versioned seed policy discussed in - * #3354 is the durable replacement.) - */ -const LEGACY_OPENCODE_FREE_SEEDS: readonly (readonly string[])[] = [ - ['big-pickle'], - ['nemotron-3-ultra-free'], - ['nemotron-3-ultra-free', 'mimo-v2.5-free', 'deepseek-v4-flash-free'], -]; - -function retiredOpencodeFreeModelIds(): readonly string[] { - const current = new Set(OPENCODE_FREE_SEED_MODEL_IDS); - return [...new Set(LEGACY_OPENCODE_FREE_SEEDS.flat())].filter((id) => !current.has(id)); -} - async function removeFailedBootstrapConnection( stores: RuntimePolicyStoresWriter, connection: ConnectionCatalogEntry, @@ -319,7 +257,7 @@ async function setDefaultIfMissing( expectedCatalogRevision: catalog.revision, target: { connectionId: connection.connectionId, - modelId: OPENCODE_FREE_DEFAULT_MODEL, + modelId: connection.enabledModelIds[0]!, }, }); if (committed.kind !== 'committed') { @@ -327,31 +265,6 @@ async function setDefaultIfMissing( } } -async function replaceBootstrapDefault( - stores: RuntimePolicyStoresWriter, - free: ConnectionCatalogEntry, - preferred: ConnectionCatalogEntry, -): Promise { - if (preferred.connectionId === free.connectionId || !preferred.enabled) return; - const catalog = await stores.connectionCatalog.getSnapshot(); - const current = catalog.defaultTarget; - if ( - current !== null && - (current.connectionId !== free.connectionId || current.modelId !== OPENCODE_FREE_DEFAULT_MODEL) - ) { - return; - } - const modelId = preferred.enabledModelIds[0]; - if (!modelId) return; - const committed = await stores.connectionCatalog.setDefaultTarget({ - expectedCatalogRevision: catalog.revision, - target: { connectionId: preferred.connectionId, modelId }, - }); - if (committed.kind !== 'committed') { - throw new Error(`Bootstrap preferred target could not be stored: ${committed.kind}`); - } -} - async function readJournal(path: string): Promise { let contents: string; try { diff --git a/packages/runtime/src/__tests__/opencode-free-anonymous.test.ts b/packages/runtime/src/__tests__/opencode-free-anonymous.test.ts deleted file mode 100644 index 2f5e9ff1c9..0000000000 --- a/packages/runtime/src/__tests__/opencode-free-anonymous.test.ts +++ /dev/null @@ -1,384 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * opencode-free anonymous runtime — the invariant the free tier depends on. - * - * opencode-free is Maka's zero-credential default. Its free-tier access works - * ONLY because the request carries no Authorization header and the OpenCode - * Zen server treats that as anonymous (per-IP rate-limited). This invariant is - * the whole point of the provider, so it must be locked directly against - * opencode-free, not inferred from "the code path looks like Ollama's". - * - * If @ai-sdk/openai-compatible ever stops omitting the header on an empty key, - * or someone later "improves" opencode-free by injecting a placeholder key, - * this test must fail before the anonymous free path silently breaks. - */ -import assert from 'node:assert/strict'; -import { describe, test } from 'node:test'; -import { generateText } from 'ai'; -import type { LlmConnection } from '@maka/core/llm-connections'; -import { PROVIDER_REGISTRY } from '@maka/core/provider-registry'; -import { getAIModel } from '@maka/runtime/model-factory'; - -import { testConnection } from '@maka/runtime/test-connection'; - -describe('opencode-free anonymous runtime', () => { - test('omits Authorization and posts to zen/v1 with the model id (empty key, no secret)', async () => { - const baseUrl = 'https://opencode.ai/zen/v1'; - const connection: LlmConnection = { - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', - baseUrl, - defaultModel: 'big-pickle', - enabled: true, - createdAt: 0, - updatedAt: 0, - }; - - let captured: { url: string; authorization: string | null; model: unknown } | undefined; - const fakeFetch: typeof globalThis.fetch = async (input, init) => { - const request = new Request(input, init); - const body = JSON.parse(await request.text()) as Record; - captured = { - url: request.url, - authorization: request.headers.get('authorization'), - model: body.model, - }; - return new Response( - JSON.stringify({ - id: 'chatcmpl-opencode-free', - object: 'chat.completion', - created: 1, - model: 'big-pickle', - choices: [ - { - index: 0, - message: { role: 'assistant', content: 'ok' }, - finish_reason: 'stop', - }, - ], - usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, - }), - { status: 200, headers: { 'content-type': 'application/json' } }, - ); - }; - - const model = getAIModel({ - connection, - apiKey: '', - modelId: 'big-pickle', - fetch: fakeFetch, - }); - - const result = await generateText({ model, prompt: 'hi' }); - - assert.ok(captured, 'opencode-free request was issued'); - assert.equal( - captured?.authorization, - null, - 'opencode-free must send NO Authorization header (anonymous free tier)', - ); - assert.equal(captured?.url, 'https://opencode.ai/zen/v1/chat/completions'); - assert.equal(captured?.model, 'big-pickle'); - assert.equal(result.text, 'ok'); - }); - - test('rejects a 200 error envelope and probes the next free model', async () => { - const requestedModels: string[] = []; - const connection: LlmConnection = { - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', - defaultModel: 'big-pickle', - enabledModelIds: ['big-pickle'], - enabled: true, - createdAt: 0, - updatedAt: 0, - }; - const fakeFetch: typeof globalThis.fetch = async (_input, init) => { - const body = JSON.parse(String(init?.body)) as { model: string }; - requestedModels.push(body.model); - if (body.model === 'big-pickle') { - return new Response(JSON.stringify({ error: { type: 'server_error' } }), { - status: 200, - headers: { 'content-type': 'application/json' }, - }); - } - return new Response( - JSON.stringify({ - choices: [{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], - }), - { status: 200, headers: { 'content-type': 'application/json' } }, - ); - }; - - const result = await testConnection(connection, '', undefined, { fetch: fakeFetch }); - - assert.equal(result.ok, true); - assert.equal(result.modelTested, 'nemotron-3-ultra-free'); - assert.deepEqual(requestedModels, ['big-pickle', 'nemotron-3-ultra-free']); - }); - - test('probes a healthy fallback for a customized connection without hiding the model tested', async () => { - const requestedModels: string[] = []; - const connection: LlmConnection = { - slug: 'my-opencode-free', - name: 'My free connection', - providerType: 'opencode-free', - baseUrl: 'https://custom.example/v1', - defaultModel: 'custom-broken-free', - enabledModelIds: ['custom-broken-free'], - enabled: true, - createdAt: 0, - updatedAt: 0, - }; - const fakeFetch: typeof globalThis.fetch = async (_input, init) => { - const body = JSON.parse(String(init?.body)) as { model: string }; - requestedModels.push(body.model); - return new Response( - JSON.stringify( - body.model === 'custom-broken-free' - ? { error: { type: 'server_error' } } - : { choices: [{ message: { role: 'assistant', content: 'ok' } }] }, - ), - { status: 200, headers: { 'content-type': 'application/json' } }, - ); - }; - - const result = await testConnection(connection, '', undefined, { fetch: fakeFetch }); - - assert.equal(result.ok, true); - assert.equal(result.modelTested, 'nemotron-3-ultra-free'); - assert.deepEqual(requestedModels, ['custom-broken-free', 'nemotron-3-ultra-free']); - }); - - test('does not probe a quarantined enabled model', async () => { - const requestedModels: string[] = []; - const connection: LlmConnection = { - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', - defaultModel: 'muse-spark-1.2-contributor-free', - enabledModelIds: ['muse-spark-1.2-contributor-free'], - enabled: true, - createdAt: 0, - updatedAt: 0, - }; - const fakeFetch: typeof globalThis.fetch = async (_input, init) => { - const body = JSON.parse(String(init?.body)) as { model: string }; - requestedModels.push(body.model); - return new Response( - JSON.stringify({ choices: [{ message: { role: 'assistant', content: 'ok' } }] }), - { status: 200, headers: { 'content-type': 'application/json' } }, - ); - }; - - const result = await testConnection(connection, '', undefined, { fetch: fakeFetch }); - - assert.equal(result.ok, true); - assert.equal(result.modelTested, 'nemotron-3-ultra-free'); - assert.deepEqual(requestedModels, ['nemotron-3-ultra-free']); - }); - - test('keeps a quarantined default from blocking the healthy fallback', async () => { - const requestedModels: string[] = []; - const connection: LlmConnection = { - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', - defaultModel: 'x-preview-f-free', - enabledModelIds: ['x-preview-f-free'], - enabled: true, - createdAt: 0, - updatedAt: 0, - }; - const fakeFetch: typeof globalThis.fetch = async (_input, init) => { - const body = JSON.parse(String(init?.body)) as { model: string }; - requestedModels.push(body.model); - return new Response( - JSON.stringify({ choices: [{ message: { role: 'assistant', content: 'ok' } }] }), - { status: 200, headers: { 'content-type': 'application/json' } }, - ); - }; - - const result = await testConnection(connection, '', undefined, { fetch: fakeFetch }); - - assert.equal(result.ok, true); - assert.equal(result.modelTested, 'nemotron-3-ultra-free'); - assert.deepEqual(requestedModels, ['nemotron-3-ultra-free']); - }); - - test('returns no model to test when every candidate is quarantined', async () => { - const defaults = PROVIDER_REGISTRY['opencode-free']; - const originalFallbackModels = defaults.fallbackModels; - const originalBrokenModelIds = defaults.brokenModelIds; - defaults.fallbackModels = []; - defaults.brokenModelIds = ['muse-spark-1.2-contributor-free']; - - try { - const connection: LlmConnection = { - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', - defaultModel: 'muse-spark-1.2-contributor-free', - enabledModelIds: ['muse-spark-1.2-contributor-free'], - enabled: true, - createdAt: 0, - updatedAt: 0, - }; - const fakeFetch: typeof globalThis.fetch = async () => { - throw new Error('no request expected'); - }; - - const result = await testConnection(connection, '', undefined, { fetch: fakeFetch }); - - assert.equal(result.ok, false); - assert.equal(result.errorMessage, 'No model to test'); - } finally { - defaults.fallbackModels = originalFallbackModels; - defaults.brokenModelIds = originalBrokenModelIds; - } - }); - - test('tests an explicit model once without probing fallbacks', async () => { - const requestedModels: string[] = []; - const connection: LlmConnection = { - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', - defaultModel: 'nemotron-3-ultra-free', - enabled: true, - createdAt: 0, - updatedAt: 0, - }; - const fakeFetch: typeof globalThis.fetch = async (_input, init) => { - const body = JSON.parse(String(init?.body)) as { model: string }; - requestedModels.push(body.model); - return new Response(JSON.stringify({ error: { type: 'server_error' } }), { - status: 200, - headers: { 'content-type': 'application/json' }, - }); - }; - - const result = await testConnection(connection, '', 'explicit-free', { fetch: fakeFetch }); - - assert.equal(result.ok, false); - assert.equal(result.modelTested, 'explicit-free'); - assert.deepEqual(requestedModels, ['explicit-free']); - }); - - test('rejects malformed completion choices before accepting a fallback', async () => { - const requestedModels: string[] = []; - const connection: LlmConnection = { - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', - defaultModel: 'big-pickle', - enabledModelIds: ['big-pickle'], - enabled: true, - createdAt: 0, - updatedAt: 0, - }; - const fakeFetch: typeof globalThis.fetch = async (_input, init) => { - const body = JSON.parse(String(init?.body)) as { model: string }; - requestedModels.push(body.model); - return new Response( - JSON.stringify( - body.model === 'big-pickle' - ? { choices: [{}] } - : { choices: [{ message: { role: 'assistant', content: 'ok' } }] }, - ), - { status: 200, headers: { 'content-type': 'application/json' } }, - ); - }; - - const result = await testConnection(connection, '', undefined, { fetch: fakeFetch }); - - assert.equal(result.ok, true); - assert.equal(result.modelTested, 'nemotron-3-ultra-free'); - assert.deepEqual(requestedModels, ['big-pickle', 'nemotron-3-ultra-free']); - }); - - test('accepts a reasoning completion whose final content is null', async () => { - const connection: LlmConnection = { - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', - defaultModel: 'mimo-v2.5-free', - enabled: true, - createdAt: 0, - updatedAt: 0, - }; - const fakeFetch: typeof globalThis.fetch = async () => - new Response( - JSON.stringify({ - choices: [ - { - message: { - role: 'assistant', - content: null, - reasoning_content: 'The answer was truncated before final text.', - }, - finish_reason: 'length', - }, - ], - }), - { status: 200, headers: { 'content-type': 'application/json' } }, - ); - - const result = await testConnection(connection, '', 'mimo-v2.5-free', { fetch: fakeFetch }); - - assert.equal(result.ok, true); - assert.equal(result.modelTested, 'mimo-v2.5-free'); - }); - - test('bounds all fallback probes by one shared deadline', async () => { - const connection: LlmConnection = { - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', - defaultModel: 'big-pickle', - enabledModelIds: ['big-pickle'], - enabled: true, - createdAt: 0, - updatedAt: 0, - }; - const fakeFetch: typeof globalThis.fetch = async (_input, init) => { - return await new Promise((_resolve, reject) => { - const signal = init?.signal; - assert.ok(signal); - const rejectFromAbort = () => reject(signal.reason); - if (signal.aborted) rejectFromAbort(); - else signal.addEventListener('abort', rejectFromAbort, { once: true }); - }); - }; - const startedAt = Date.now(); - - const result = await testConnection(connection, '', undefined, { - fetch: fakeFetch, - timeoutMs: 40, - }); - - assert.equal(result.ok, false); - assert.equal(result.errorClass, 'timeout'); - assert.ok(Date.now() - startedAt < 1_000); - }); -}); diff --git a/packages/runtime/src/__tests__/opencode-free-retired.test.ts b/packages/runtime/src/__tests__/opencode-free-retired.test.ts new file mode 100644 index 0000000000..ce20006838 --- /dev/null +++ b/packages/runtime/src/__tests__/opencode-free-retired.test.ts @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import type { LlmConnection } from '@maka/core/llm-connections'; +import { getAIModel } from '../model-factory.js'; +import { testConnection } from '../test-connection.js'; + +test('a stored OpenCode Free connection cannot probe or create a model', async () => { + const connection: LlmConnection = { + slug: 'opencode-free', + name: 'OpenCode Free', + providerType: 'opencode-free', + defaultModel: 'nemotron-3-ultra-free', + enabledModelIds: ['nemotron-3-ultra-free'], + enabled: true, + createdAt: 1, + updatedAt: 1, + }; + const fetchFn = async () => { + throw new Error('Retired provider must not make requests'); + }; + const result = await testConnection(connection, '', undefined, { fetch: fetchFn }); + assert.equal(result.ok, false); + assert.match(result.errorMessage ?? '', /no longer supported|retired|not available/i); + assert.throws( + () => getAIModel({ connection, apiKey: '', modelId: 'nemotron-3-ultra-free', fetch: fetchFn }), + /retired and can no longer resolve/, + ); +}); diff --git a/packages/runtime/src/__tests__/opencode-session-header.test.ts b/packages/runtime/src/__tests__/opencode-session-header.test.ts index 2ce11a8e5b..3d3ac1f12b 100644 --- a/packages/runtime/src/__tests__/opencode-session-header.test.ts +++ b/packages/runtime/src/__tests__/opencode-session-header.test.ts @@ -22,15 +22,13 @@ import { describe, test } from 'node:test'; import { OPENCODE_SESSION_HEADER, withOpenCodeSessionHeader } from '../opencode-session-header.js'; describe('OpenCode session header', () => { - test('adds a trimmed stable session identity for OpenCode Go and Free', () => { + test('adds a trimmed stable session identity for OpenCode Go', () => { assert.deepEqual(withOpenCodeSessionHeader('opencode-go', ' session-a '), { [OPENCODE_SESSION_HEADER]: 'session-a', }); assert.equal(withOpenCodeSessionHeader('opencode', 'session-a'), undefined); assert.equal(withOpenCodeSessionHeader('openai', 'session-a'), undefined); - assert.deepEqual(withOpenCodeSessionHeader('opencode-free', ' session-a '), { - [OPENCODE_SESSION_HEADER]: 'session-a', - }); + assert.equal(withOpenCodeSessionHeader('opencode-free', 'session-a'), undefined); }); test('preserves an explicitly configured header case-insensitively', () => { diff --git a/packages/runtime/src/__tests__/provider-conformance.test.ts b/packages/runtime/src/__tests__/provider-conformance.test.ts index d29f32033d..d91f9ff219 100644 --- a/packages/runtime/src/__tests__/provider-conformance.test.ts +++ b/packages/runtime/src/__tests__/provider-conformance.test.ts @@ -1419,36 +1419,6 @@ describe('models.dev provider conformance', () => { assert.deepEqual(requests[2]?.body.contents, [{ role: 'user', parts: [{ text: 'Hi' }] }]); }); - test('OpenCode Free probes reuse identity across candidates and renew it per operation', async () => { - const sessions: Array = []; - const server = await startJsonServer(async (request, response) => { - sessions.push(request.headers['x-opencode-session'] as string | undefined); - respondJson(response, sessions.length % 2 === 1 ? 429 : 200, { - choices: [{ message: { role: 'assistant', content: 'ok' } }], - }); - }); - const connection: LlmConnection = { - slug: 'opencode-free', - name: 'OpenCode Free', - providerType: 'opencode-free', - baseUrl: `${server.url}/zen/v1`, - defaultModel: 'nemotron-3-ultra-free', - enabledModelIds: ['nemotron-3-ultra-free', 'mimo-v2.5-free'], - enabled: true, - createdAt: 1, - updatedAt: 1, - }; - assert.equal((await testConnection(connection, '')).ok, true); - assert.equal((await testConnection(connection, '')).ok, true); - assert.equal(sessions.length, 4); - for (const session of sessions) { - assert.match(session ?? '', /^[0-9a-f-]{36}$/); - } - assert.equal(sessions[0], sessions[1]); - assert.equal(sessions[2], sessions[3]); - assert.notEqual(sessions[0], sessions[2]); - }); - test('OpenCode Go connection probes identify every supported wire request', async () => { const requests: Array<{ url: string; diff --git a/packages/runtime/src/__tests__/responses-wire-contract.test.ts b/packages/runtime/src/__tests__/responses-wire-contract.test.ts index bdf98b796f..3c7cbf5287 100644 --- a/packages/runtime/src/__tests__/responses-wire-contract.test.ts +++ b/packages/runtime/src/__tests__/responses-wire-contract.test.ts @@ -192,7 +192,7 @@ describe('responses wire contract', () => { assert.deepEqual(sessionHeaders, ['session-opencode-go']); }); - test('sends OpenCode Go and Free session identities through their model adapters', async () => { + test('sends OpenCode Go session identities through their model adapters', async () => { const requests: Array<{ url: string; sessionHeader: string | null }> = []; const fetch = (async (url: string | URL | Request, init?: RequestInit) => { const request = new Request(url, init); @@ -231,8 +231,6 @@ describe('responses wire contract', () => { for (const [providerType, modelId] of [ ['opencode-go', 'kimi-k2.7-code'], ['opencode-go', 'minimax-m3'], - ['opencode-free', 'nemotron-3-ultra-free'], - ['opencode-free', 'nemotron-3-ultra-free'], ] as const) { const model = getAIModel({ connection: conn(providerType), @@ -255,14 +253,6 @@ describe('responses wire contract', () => { url: 'https://opencode.ai/zen/go/v1/messages', sessionHeader: 'session-opencode-go', }, - { - url: 'https://opencode.ai/zen/v1/chat/completions', - sessionHeader: 'session-opencode-free', - }, - { - url: 'https://opencode.ai/zen/v1/chat/completions', - sessionHeader: 'session-opencode-free', - }, ]); }); diff --git a/packages/runtime/src/opencode-session-header.ts b/packages/runtime/src/opencode-session-header.ts index 8ae20f3f2b..360aa4cbfe 100644 --- a/packages/runtime/src/opencode-session-header.ts +++ b/packages/runtime/src/opencode-session-header.ts @@ -21,17 +21,14 @@ import type { ProviderType } from '@maka/core/llm-connections'; export const OPENCODE_SESSION_HEADER = 'x-opencode-session'; -/** Adds OpenCode Go/Free session identity without overriding an explicit header. */ +/** Adds OpenCode Go session identity without overriding an explicit header. */ export function withOpenCodeSessionHeader( providerType: ProviderType, sessionId: string | undefined, headers?: Readonly>, ): Readonly> | undefined { const normalizedSessionId = sessionId?.trim(); - if ( - (providerType !== 'opencode-go' && providerType !== 'opencode-free') || - !normalizedSessionId - ) { + if (providerType !== 'opencode-go' || !normalizedSessionId) { return headers; } if (Object.keys(headers ?? {}).some((name) => name.toLowerCase() === OPENCODE_SESSION_HEADER)) { diff --git a/packages/runtime/src/test-connection.ts b/packages/runtime/src/test-connection.ts index cba9a7f207..29519d0c59 100644 --- a/packages/runtime/src/test-connection.ts +++ b/packages/runtime/src/test-connection.ts @@ -167,10 +167,8 @@ async function testConnectionStrict( if (!defaults) { return { ok: false, errorMessage: `Unknown provider type "${connection.providerType}"` }; } - const sessionId = - connection.providerType === 'opencode-go' || connection.providerType === 'opencode-free' - ? randomUUID() - : undefined; + if (defaults.retired) return retiredProviderTestResult(connection.providerType); + const sessionId = connection.providerType === 'opencode-go' ? randomUUID() : undefined; const auth = defaults.authKind; const secret = auth === 'none' ? '' : apiKey; const testModel = resolveConnectionTestModel( @@ -182,44 +180,6 @@ async function testConnectionStrict( if (!testModel) { return { ok: false, errorMessage: 'No model to test' }; } - if (connection.providerType === 'opencode-free' && !model?.trim()) { - const brokenModelIds = new Set(defaults.brokenModelIds ?? []); - const candidates = [ - ...new Set([ - ...connectionEnabledModelIds(connection).filter((id) => !brokenModelIds.has(id)), - ...providerFallbackModelIds(defaults), - ]), - ]; - if (candidates.length === 0) { - return { ok: false, errorMessage: 'No model to test' }; - } - let lastFailure: ConnectionTestResult | undefined; - for (let index = 0; index < candidates.length; index += 1) { - const candidate = candidates[index]!; - const remainingMs = timeoutMs - (Date.now() - t0); - if (remainingMs <= 0) { - return connectionTestFailure(new ConnectionEffectFetchError('timeout'), t0); - } - const remainingCandidates = candidates.length - index; - const attemptTimeoutMs = Math.max(1, Math.floor(remainingMs / remainingCandidates)); - try { - const result = await testConnectionModel( - connection, - secret, - candidate, - fetchFn, - t0, - attemptTimeoutMs, - sessionId, - ); - if (result.ok) return result; - lastFailure = result; - } catch (error) { - lastFailure = connectionTestFailure(error, t0, true); - } - } - return lastFailure ?? connectionTestFailure(new ConnectionEffectFetchError('timeout'), t0); - } return await testConnectionModel( connection, @@ -503,48 +463,10 @@ async function probeOpenAI( timeoutMs, }); if (!r.ok) return httpFailure(r, t0); - if (connection.providerType === 'opencode-free') { - const body = await r.readJson(); - if (!isOpenAIChatCompletion(body)) { - return { - ok: false, - errorMessage: 'OpenCode Free returned no valid chat completion', - errorClass: 'provider_unavailable', - latencyMs: Date.now() - t0, - modelTested: model, - }; - } - return { ok: true, latencyMs: Date.now() - t0, modelTested: model }; - } await r.cancel(); return { ok: true, latencyMs: Date.now() - t0, modelTested: model }; } -function isOpenAIChatCompletion(value: unknown): boolean { - if (!value || typeof value !== 'object') return false; - const choices = (value as { choices?: unknown }).choices; - return ( - Array.isArray(choices) && - choices.some((choice) => { - if (!choice || typeof choice !== 'object') return false; - const message = (choice as { message?: unknown }).message; - if (!message || typeof message !== 'object') return false; - const completion = message as { - content?: unknown; - reasoning?: unknown; - reasoning_content?: unknown; - tool_calls?: unknown; - }; - return ( - typeof completion.content === 'string' || - typeof completion.reasoning === 'string' || - typeof completion.reasoning_content === 'string' || - (Array.isArray(completion.tool_calls) && completion.tool_calls.length > 0) - ); - }) - ); -} - async function probeGoogle( baseUrl: string, apiKey: string, diff --git a/packages/storage/src/runtime-policy-stores.ts b/packages/storage/src/runtime-policy-stores.ts index 1d6cc77bac..0f8303d61e 100644 --- a/packages/storage/src/runtime-policy-stores.ts +++ b/packages/storage/src/runtime-policy-stores.ts @@ -31,7 +31,6 @@ import type { RemoveCatalogConnectionInput, RuntimePolicySnapshot, SetCredentialInput, - MigrateSystemSeedInput, SetDefaultConnectionTargetInput, UpdateCatalogConnectionInput, } from '@maka/core/runtime-policy'; @@ -116,7 +115,6 @@ export interface ConnectionCatalogWriter extends ConnectionCatalogReader { setDefaultTarget( input: SetDefaultConnectionTargetInput, ): Promise; - migrateSystemSeed(input: MigrateSystemSeedInput): Promise; } export interface CredentialVaultReader { @@ -230,7 +228,6 @@ function createWriterFacade(coordinator: RuntimePolicyCoordinator): RuntimePolic update: (input) => coordinator.updateConnection(input), remove: (input) => coordinator.removeConnection(input), setDefaultTarget: (input) => coordinator.setDefaultTarget(input), - migrateSystemSeed: (input) => coordinator.migrateSystemSeed(input), }, credentialVault: { getSnapshot: () => coordinator.getVaultSnapshot(), diff --git a/packages/storage/src/runtime-policy/connection-catalog-document.ts b/packages/storage/src/runtime-policy/connection-catalog-document.ts index d26ffd4863..7a4b1dea84 100644 --- a/packages/storage/src/runtime-policy/connection-catalog-document.ts +++ b/packages/storage/src/runtime-policy/connection-catalog-document.ts @@ -49,7 +49,6 @@ import { type CreateCatalogConnectionInput, type RemoveCatalogConnectionInput, type SetDefaultConnectionTargetInput, - type MigrateSystemSeedInput, type UpdateCatalogConnectionInput, } from '@maka/core/runtime-policy'; import { PROVIDER_REGISTRY, reconcileConnectionAfterModelFetch } from '@maka/core/llm-connections'; @@ -412,83 +411,6 @@ export class ConnectionCatalogDocumentOwner { return committed(next); } - /** - * Built-in seed evolution as ONE atomic catalog mutation. A row whose - * `enabledModelIds` still exactly match a historical system seed is provably - * system-owned: it follows the current seed, its static inventory is - * re-derived from the current build, and a default target the migration - * removes is retargeted inside the same document write — so no restart can - * observe enabled ids without their inventory, or a nulled default awaiting - * a second write. Any other inventory (including a reordering) is a user - * selection and is never touched; an already-null default stays null. - */ - async migrateSystemSeed( - root: string, - input: MigrateSystemSeedInput, - ): Promise { - if (!input.enabledModelIds.includes(input.defaultModelId)) { - throw codecError('invalid_connection_input', 'Seed default must be in the seed selection'); - } - const current = await this.read(root); - const index = current.connections.findIndex( - (item) => item.slug === input.slug && item.providerType === input.providerType, - ); - const previous = current.connections[index]; - const retired = new Set(input.retiredModelIds); - const sameIds = (left: readonly string[], right: readonly string[]) => - left.length === right.length && left.every((id, position) => id === right[position]); - const isLegacySeed = previous - ? input.legacyEnabledModelIds.some((seed) => sameIds(previous.enabledModelIds, seed)) - : false; - const hasRetiredModels = previous - ? previous.enabledModelIds.some((id) => retired.has(id)) || - previous.models.some((model) => retired.has(model.id)) - : false; - if (!previous || (!isLegacySeed && !hasRetiredModels)) { - return committed(current); - } - // A legacy seed's stored inventory was the registry's shipped list copied - // in at write time. Clearing it is the migration: the resolver prepends - // that list from the current build, so the row stops carrying a stale - // second copy of it. Any other row keeps its own inventory, minus the - // retired ids. - const models = isLegacySeed ? [] : previous.models.filter((model) => !retired.has(model.id)); - const { - lastTest: _lastTest, - modelSource: _modelSource, - modelsFetchedAt: _modelsFetchedAt, - ...retained - } = previous; - const migratedEnabledModelIds = isLegacySeed - ? [...input.enabledModelIds] - : previous.enabledModelIds.filter((id) => !retired.has(id)); - const connections = [...current.connections]; - const modelOverrides = pruneModelOverrides( - previous.modelOverrides, - Object.keys(previous.modelOverrides ?? {}).filter((id) => !retired.has(id)), - ); - connections[index] = { - ...retained, - revision: nextRevision(previous.revision), - enabledModelIds: migratedEnabledModelIds, - models, - ...(isLegacySeed || previous.modelSource === undefined - ? {} - : { modelSource: previous.modelSource, modelsFetchedAt: previous.modelsFetchedAt }), - ...(modelOverrides === undefined ? {} : { modelOverrides }), - }; - const target = current.defaultTarget; - const defaultTarget = - target !== null && - target.connectionId === previous.connectionId && - !migratedEnabledModelIds.includes(target.modelId) - ? { connectionId: previous.connectionId, modelId: input.defaultModelId } - : target; - const next = this.nextDocument(current, connections, defaultTarget); - await this.write(root, next); - return committed(next); - } - async setDefaultTarget( root: string, rawInput: SetDefaultConnectionTargetInput, diff --git a/packages/storage/src/runtime-policy/coordinator.ts b/packages/storage/src/runtime-policy/coordinator.ts index cd1841598b..5269c08320 100644 --- a/packages/storage/src/runtime-policy/coordinator.ts +++ b/packages/storage/src/runtime-policy/coordinator.ts @@ -58,7 +58,6 @@ import { type RequestHeaderUpdate, type SavedRequestHeaders, type SetCredentialInput, - type MigrateSystemSeedInput, type SetDefaultConnectionTargetInput, type UpdateCatalogConnectionInput, type UpdateNetworkProxyInput, @@ -476,10 +475,6 @@ export class RuntimePolicyCoordinator { ); } - migrateSystemSeed(input: MigrateSystemSeedInput) { - return this.inLane((root) => this.catalog.migrateSystemSeed(root, input)); - } - setCredential(rawInput: SetCredentialInput) { return this.setCredentialWithAuthority(rawInput, 'client'); } diff --git a/scripts/model-metadata/models-dev-api.snapshot.json b/scripts/model-metadata/models-dev-api.snapshot.json index ea9518ee54..6bc4b3f7c9 100644 --- a/scripts/model-metadata/models-dev-api.snapshot.json +++ b/scripts/model-metadata/models-dev-api.snapshot.json @@ -7,7 +7,7 @@ "etag": "W/\"153362e16f62e2c0b4bbb4d66949e9c5\"", "responseSha256": "153362e16f62e2c0b4bbb4d66949e9c5ccc050e0dd7dbd325bbf434f2a8fcbf1" }, - "projectionSha256": "be5b78501eb764e135c9a7c66b609ccb366fb383d5b4f7f532e78974c2ff0e51", + "projectionSha256": "da65200d9c31e4883f25bd3e4e6fe5fabe15b803472e85662761ba6950ef7ae8", "projection": { "metadata": { "anthropic": { @@ -1437,7 +1437,6 @@ "contextWindow": 32768, "maxOutputTokens": 16384, "lastUpdated": "2025-01-01", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -1455,7 +1454,6 @@ "contextWindow": 32768, "maxOutputTokens": 16384, "lastUpdated": "2025-01-01", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -3040,7 +3038,6 @@ "maxOutputTokens": 16384, "knowledgeCutoff": "2025-04", "lastUpdated": "2025-12-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -3061,7 +3058,6 @@ "contextWindow": 202752, "maxOutputTokens": 16384, "lastUpdated": "2026-02-11", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -3083,7 +3079,6 @@ "maxOutputTokens": 32768, "knowledgeCutoff": "2025-01", "lastUpdated": "2026-01-27", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -3104,7 +3099,6 @@ "contextWindow": 196608, "maxOutputTokens": 24576, "lastUpdated": "2026-02-12", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -3123,7 +3117,6 @@ "maxOutputTokens": 65536, "structuredOutput": true, "lastUpdated": "2026-02-03", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -3142,7 +3135,6 @@ "maxOutputTokens": 65536, "knowledgeCutoff": "2025-04", "lastUpdated": "2025-07-23", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -3161,7 +3153,6 @@ "maxOutputTokens": 32768, "knowledgeCutoff": "2025-04", "lastUpdated": "2026-01-23", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -3180,7 +3171,6 @@ "maxOutputTokens": 65536, "knowledgeCutoff": "2025-04", "lastUpdated": "2026-02-16", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -3223,7 +3213,6 @@ "maxOutputTokens": 65536, "knowledgeCutoff": "2025-04", "lastUpdated": "2026-04-02", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -3265,7 +3254,6 @@ "maxOutputTokens": 64000, "knowledgeCutoff": "2025-04", "lastUpdated": "2026-06-02", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -3289,7 +3277,6 @@ "maxOutputTokens": 16384, "knowledgeCutoff": "2025-04", "lastUpdated": "2025-12-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -3310,7 +3297,6 @@ "contextWindow": 202752, "maxOutputTokens": 16384, "lastUpdated": "2026-02-11", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -3332,7 +3318,6 @@ "maxOutputTokens": 32768, "knowledgeCutoff": "2025-01", "lastUpdated": "2026-01-27", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -3354,7 +3339,6 @@ "inputLimit": 196601, "maxOutputTokens": 24576, "lastUpdated": "2026-02-12", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -3373,7 +3357,6 @@ "maxOutputTokens": 65536, "structuredOutput": true, "lastUpdated": "2026-02-03", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -3392,7 +3375,6 @@ "maxOutputTokens": 65536, "knowledgeCutoff": "2025-04", "lastUpdated": "2025-07-23", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -3411,7 +3393,6 @@ "maxOutputTokens": 32768, "knowledgeCutoff": "2025-04", "lastUpdated": "2026-01-23", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -3430,7 +3411,6 @@ "maxOutputTokens": 65536, "knowledgeCutoff": "2025-04", "lastUpdated": "2026-02-16", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -3473,7 +3453,6 @@ "maxOutputTokens": 65536, "knowledgeCutoff": "2025-04", "lastUpdated": "2026-04-02", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -3515,7 +3494,6 @@ "maxOutputTokens": 64000, "knowledgeCutoff": "2025-04", "lastUpdated": "2026-06-02", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -3540,7 +3518,6 @@ "knowledgeCutoff": "2025-01", "structuredOutput": true, "lastUpdated": "2025-12-05", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -3563,7 +3540,6 @@ "knowledgeCutoff": "2025-05", "structuredOutput": true, "lastUpdated": "2026-04-24", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -3587,7 +3563,6 @@ "knowledgeCutoff": "2025-05", "structuredOutput": true, "lastUpdated": "2026-07-31", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -3611,7 +3586,6 @@ "knowledgeCutoff": "2025-05", "structuredOutput": true, "lastUpdated": "2026-04-24", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -3634,7 +3608,6 @@ "maxOutputTokens": 384000, "structuredOutput": true, "lastUpdated": "2026-08-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -3658,7 +3631,6 @@ "knowledgeCutoff": "2025-05", "structuredOutput": true, "lastUpdated": "2026-09-10", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -3681,7 +3653,6 @@ "maxOutputTokens": 16384, "structuredOutput": true, "lastUpdated": "2026-02-12", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -3703,7 +3674,6 @@ "maxOutputTokens": 128000, "structuredOutput": true, "lastUpdated": "2026-04-07", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -3725,7 +3695,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-06-13", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -3744,7 +3713,6 @@ "description": "Video model for image-to-video generation", "lifecycle": "active", "lastUpdated": "2026-07-17", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -3760,7 +3728,6 @@ "description": "Video model for reference-guided video generation", "lifecycle": "active", "lastUpdated": "2026-07-17", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -3776,7 +3743,6 @@ "description": "Video model for prompt-driven text-to-video generation", "lifecycle": "active", "lastUpdated": "2026-07-17", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -3796,7 +3762,6 @@ "knowledgeCutoff": "2025-01", "structuredOutput": true, "lastUpdated": "2026-01", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -3819,7 +3784,6 @@ "knowledgeCutoff": "2025-01", "structuredOutput": true, "lastUpdated": "2026-04-21", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -3842,7 +3806,6 @@ "knowledgeCutoff": "2025-01", "structuredOutput": true, "lastUpdated": "2026-06-12", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -3864,7 +3827,6 @@ "inputLimit": 196601, "maxOutputTokens": 32768, "lastUpdated": "2026-02-12", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -3881,7 +3843,6 @@ "lifecycle": "active", "contextWindow": 8192, "lastUpdated": "2026-03-03", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -3898,7 +3859,6 @@ "lifecycle": "active", "contextWindow": 8192, "lastUpdated": "2026-03-03", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -3917,7 +3877,6 @@ "maxOutputTokens": 65536, "structuredOutput": true, "lastUpdated": "2026-04-27", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -3940,7 +3899,6 @@ "knowledgeCutoff": "2025-04", "structuredOutput": true, "lastUpdated": "2026-04-02", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -3962,7 +3920,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-05-21", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -3985,7 +3942,6 @@ "knowledgeCutoff": "2025-04", "structuredOutput": true, "lastUpdated": "2026-06-02", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -4007,7 +3963,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-08-26", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -4030,7 +3985,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-08-03", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -4053,7 +4007,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-07-19", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -4073,7 +4026,6 @@ "lifecycle": "active", "contextWindow": 8192, "lastUpdated": "2026-05-29", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -4090,7 +4042,6 @@ "lifecycle": "active", "contextWindow": 8192, "lastUpdated": "2026-05-29", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -4112,7 +4063,6 @@ "knowledgeCutoff": "2025-01", "structuredOutput": true, "lastUpdated": "2025-12-05", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -4135,7 +4085,6 @@ "knowledgeCutoff": "2025-05", "structuredOutput": true, "lastUpdated": "2026-04-24", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -4159,7 +4108,6 @@ "knowledgeCutoff": "2025-05", "structuredOutput": true, "lastUpdated": "2026-07-31", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -4183,7 +4131,6 @@ "knowledgeCutoff": "2025-05", "structuredOutput": true, "lastUpdated": "2026-04-24", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -4206,7 +4153,6 @@ "maxOutputTokens": 384000, "structuredOutput": true, "lastUpdated": "2026-08-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -4229,7 +4175,6 @@ "maxOutputTokens": 16384, "structuredOutput": true, "lastUpdated": "2026-02-12", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -4251,7 +4196,6 @@ "maxOutputTokens": 128000, "structuredOutput": true, "lastUpdated": "2026-04-07", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -4273,7 +4217,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-06-13", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -4292,7 +4235,6 @@ "description": "Video model for image-to-video generation", "lifecycle": "active", "lastUpdated": "2026-07-17", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -4308,7 +4250,6 @@ "description": "Video model for reference-guided video generation", "lifecycle": "active", "lastUpdated": "2026-07-17", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -4324,7 +4265,6 @@ "description": "Video model for prompt-driven text-to-video generation", "lifecycle": "active", "lastUpdated": "2026-07-17", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -4344,7 +4284,6 @@ "knowledgeCutoff": "2025-01", "structuredOutput": true, "lastUpdated": "2026-01", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -4367,7 +4306,6 @@ "knowledgeCutoff": "2025-01", "structuredOutput": true, "lastUpdated": "2026-04-21", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -4390,7 +4328,6 @@ "knowledgeCutoff": "2025-01", "structuredOutput": true, "lastUpdated": "2026-06-12", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -4412,7 +4349,6 @@ "inputLimit": 196601, "maxOutputTokens": 32768, "lastUpdated": "2026-02-12", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -4429,7 +4365,6 @@ "lifecycle": "active", "contextWindow": 8192, "lastUpdated": "2026-03-03", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -4446,7 +4381,6 @@ "lifecycle": "active", "contextWindow": 8192, "lastUpdated": "2026-03-03", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -4465,7 +4399,6 @@ "maxOutputTokens": 65536, "structuredOutput": true, "lastUpdated": "2026-04-27", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -4488,7 +4421,6 @@ "knowledgeCutoff": "2025-04", "structuredOutput": true, "lastUpdated": "2026-04-02", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -4510,7 +4442,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-05-21", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -4533,7 +4464,6 @@ "knowledgeCutoff": "2025-04", "structuredOutput": true, "lastUpdated": "2026-06-02", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -4555,7 +4485,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-08-26", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -4578,7 +4507,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-08-03", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -4601,7 +4529,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-07-19", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -4621,7 +4548,6 @@ "lifecycle": "active", "contextWindow": 8192, "lastUpdated": "2026-05-29", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -4638,7 +4564,6 @@ "lifecycle": "active", "contextWindow": 8192, "lastUpdated": "2026-05-29", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -4941,7 +4866,6 @@ "knowledgeCutoff": "2025-09-23", "structuredOutput": true, "lastUpdated": "2026-06-09", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -8739,7 +8663,6 @@ "maxOutputTokens": 65536, "structuredOutput": false, "lastUpdated": "2026-03-25", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -8758,7 +8681,6 @@ "maxOutputTokens": 65536, "structuredOutput": false, "lastUpdated": "2026-03-25", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -8829,7 +8751,6 @@ "contextWindow": 4096, "maxOutputTokens": 4096, "lastUpdated": "2025-01-23", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -10472,7 +10393,6 @@ "maxOutputTokens": 128000, "knowledgeCutoff": "2025-04", "lastUpdated": "2025-08-08", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -10587,7 +10507,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-07-16", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -10610,7 +10529,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-07-16", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -10631,7 +10549,6 @@ "contextWindow": 1048576, "maxOutputTokens": 32768, "lastUpdated": "2026-09-11", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -10655,7 +10572,6 @@ "knowledgeCutoff": "2025-01", "structuredOutput": true, "lastUpdated": "2026-06-12", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -10923,7 +10839,6 @@ "contextWindow": 204800, "maxOutputTokens": 131072, "lastUpdated": "2025-10-27", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -10941,7 +10856,6 @@ "contextWindow": 204800, "maxOutputTokens": 131072, "lastUpdated": "2025-12-23", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -10959,7 +10873,6 @@ "contextWindow": 204800, "maxOutputTokens": 131072, "lastUpdated": "2026-02-12", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -10977,7 +10890,6 @@ "contextWindow": 204800, "maxOutputTokens": 131072, "lastUpdated": "2026-02-13", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -10995,7 +10907,6 @@ "contextWindow": 204800, "maxOutputTokens": 131072, "lastUpdated": "2026-03-18", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -11013,7 +10924,6 @@ "contextWindow": 204800, "maxOutputTokens": 131072, "lastUpdated": "2026-03-18", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -11031,7 +10941,6 @@ "contextWindow": 1048576, "maxOutputTokens": 512000, "lastUpdated": "2026-06-25", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -11181,7 +11090,6 @@ "maxOutputTokens": 256000, "knowledgeCutoff": "2025-12", "lastUpdated": "2025-12-09", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -11844,7 +11752,6 @@ "contextWindow": 128000, "maxOutputTokens": 8192, "lastUpdated": "2025-05-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -11862,7 +11769,6 @@ "contextWindow": 8192, "maxOutputTokens": 1024, "lastUpdated": "2026-04-30", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -11880,7 +11786,6 @@ "contextWindow": 40960, "maxOutputTokens": 40960, "lastUpdated": "2025-08-12", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -11900,7 +11805,6 @@ "knowledgeCutoff": "2024-07", "structuredOutput": false, "lastUpdated": "2026-02-04", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -11919,7 +11823,6 @@ "maxOutputTokens": 40960, "knowledgeCutoff": "2025-06", "lastUpdated": "2026-01-31", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -11937,7 +11840,6 @@ "contextWindow": 4096, "knowledgeCutoff": "2024-08", "lastUpdated": "2025-09-05", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -11956,7 +11858,6 @@ "maxOutputTokens": 262000, "structuredOutput": true, "lastUpdated": "2025-11-25", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -11998,7 +11899,6 @@ "knowledgeCutoff": "2025-05", "structuredOutput": true, "lastUpdated": "2026-07-31", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -12042,7 +11942,6 @@ "maxOutputTokens": 384000, "structuredOutput": true, "lastUpdated": "2026-08-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -12064,7 +11963,6 @@ "maxOutputTokens": 4096, "structuredOutput": true, "lastUpdated": "2024-07-16", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12082,7 +11980,6 @@ "contextWindow": 131072, "maxOutputTokens": 16384, "lastUpdated": "2025-03-12", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -12100,7 +11997,6 @@ "contextWindow": 131072, "maxOutputTokens": 16384, "lastUpdated": "2025-03-12", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -12120,7 +12016,6 @@ "knowledgeCutoff": "2024-06", "structuredOutput": true, "lastUpdated": "2025-06-12", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -12140,7 +12035,6 @@ "knowledgeCutoff": "2024-06", "structuredOutput": true, "lastUpdated": "2025-06-03", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -12159,7 +12053,6 @@ "maxOutputTokens": 16384, "knowledgeCutoff": "2025-01", "lastUpdated": "2026-04-02", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -12180,7 +12073,6 @@ "contextWindow": 128000, "maxOutputTokens": 8192, "lastUpdated": "2024-08-26", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -12198,7 +12090,6 @@ "contextWindow": 128000, "maxOutputTokens": 8192, "lastUpdated": "2025-03-10", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12216,7 +12107,6 @@ "contextWindow": 128000, "maxOutputTokens": 8192, "lastUpdated": "2025-06-12", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12235,7 +12125,6 @@ "maxOutputTokens": 4096, "structuredOutput": true, "lastUpdated": "2024-07-16", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12254,7 +12143,6 @@ "maxOutputTokens": 4096, "knowledgeCutoff": "2023-12", "lastUpdated": "2025-01-01", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12274,7 +12162,6 @@ "knowledgeCutoff": "2023-12", "structuredOutput": true, "lastUpdated": "2024-09-18", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -12294,7 +12181,6 @@ "knowledgeCutoff": "2023-12", "structuredOutput": true, "lastUpdated": "2024-09-18", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12313,7 +12199,6 @@ "maxOutputTokens": 32000, "structuredOutput": true, "lastUpdated": "2024-09-18", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12332,7 +12217,6 @@ "maxOutputTokens": 8192, "knowledgeCutoff": "2023-12", "lastUpdated": "2024-09-25", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -12351,7 +12235,6 @@ "maxOutputTokens": 4096, "structuredOutput": true, "lastUpdated": "2024-11-26", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12371,7 +12254,6 @@ "knowledgeCutoff": "2024-02", "structuredOutput": true, "lastUpdated": "2025-04-01", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -12389,7 +12271,6 @@ "contextWindow": 128000, "maxOutputTokens": 16384, "lastUpdated": "2026-04-30", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -12409,7 +12290,6 @@ "knowledgeCutoff": "2026-01-04", "structuredOutput": true, "lastUpdated": "2026-08-10", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -12431,7 +12311,6 @@ "maxOutputTokens": 8192, "knowledgeCutoff": "2024-12", "lastUpdated": "2025-09-05", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12451,7 +12330,6 @@ "maxOutputTokens": 16384, "structuredOutput": false, "lastUpdated": "2025-07-26", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12469,7 +12347,6 @@ "contextWindow": 204800, "maxOutputTokens": 131072, "lastUpdated": "2026-04-11", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -12487,7 +12364,6 @@ "contextWindow": 1000000, "maxOutputTokens": 16384, "lastUpdated": "2026-06-01", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -12510,7 +12386,6 @@ "maxOutputTokens": 32768, "structuredOutput": false, "lastUpdated": "2025-09-25", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12528,7 +12403,6 @@ "contextWindow": 262144, "maxOutputTokens": 16384, "lastUpdated": "2025-12-02", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -12547,7 +12421,6 @@ "maxOutputTokens": 65536, "structuredOutput": true, "lastUpdated": "2025-04-01", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12567,7 +12440,6 @@ "knowledgeCutoff": "2025-01", "structuredOutput": true, "lastUpdated": "2025-12-02", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -12587,7 +12459,6 @@ "maxOutputTokens": 32768, "structuredOutput": false, "lastUpdated": "2025-09-25", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -12606,7 +12477,6 @@ "maxOutputTokens": 32768, "structuredOutput": true, "lastUpdated": "2026-04-29", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -12627,7 +12497,6 @@ "contextWindow": 128000, "maxOutputTokens": 8192, "lastUpdated": "2025-06-12", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12646,7 +12515,6 @@ "maxOutputTokens": 8192, "structuredOutput": true, "lastUpdated": "2026-03-16", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -12667,7 +12535,6 @@ "contextWindow": 65536, "maxOutputTokens": 13108, "lastUpdated": "2024-04-17", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12685,7 +12552,6 @@ "contextWindow": 32768, "maxOutputTokens": 16384, "lastUpdated": "2026-03-15", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12704,7 +12570,6 @@ "maxOutputTokens": 262144, "knowledgeCutoff": "2024-10", "lastUpdated": "2025-09-05", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12724,7 +12589,6 @@ "knowledgeCutoff": "2025-01", "structuredOutput": true, "lastUpdated": "2026-04-21", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -12746,7 +12610,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-07-16", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -12767,7 +12630,6 @@ "lifecycle": "active", "maxOutputTokens": 4096, "lastUpdated": "2026-04-16", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12785,7 +12647,6 @@ "contextWindow": 128000, "maxOutputTokens": 8192, "lastUpdated": "2025-07-20", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12802,7 +12663,6 @@ "lifecycle": "active", "maxOutputTokens": 4096, "lastUpdated": "2025-03-18", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -12820,7 +12680,6 @@ "contextWindow": 131072, "maxOutputTokens": 16384, "lastUpdated": "2025-12-01", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -12837,7 +12696,6 @@ "lifecycle": "active", "maxOutputTokens": 4096, "lastUpdated": "2025-06-30", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -12854,7 +12712,6 @@ "lifecycle": "active", "maxOutputTokens": 4096, "lastUpdated": "2026-02-26", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -12872,7 +12729,6 @@ "contextWindow": 128000, "maxOutputTokens": 4096, "lastUpdated": "2026-03-03", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12890,7 +12746,6 @@ "contextWindow": 32768, "maxOutputTokens": 2048, "lastUpdated": "2025-07-24", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12908,7 +12763,6 @@ "contextWindow": 128000, "maxOutputTokens": 8192, "lastUpdated": "2025-04-15", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12926,7 +12780,6 @@ "contextWindow": 131072, "maxOutputTokens": 16384, "lastUpdated": "2025-03-18", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -12947,7 +12800,6 @@ "contextWindow": 32768, "maxOutputTokens": 16384, "lastUpdated": "2025-04-10", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -12965,7 +12817,6 @@ "contextWindow": 128000, "maxOutputTokens": 4096, "lastUpdated": "2025-10-28", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -12983,7 +12834,6 @@ "contextWindow": 128000, "maxOutputTokens": 16384, "lastUpdated": "2025-04-07", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -13004,7 +12854,6 @@ "contextWindow": 131072, "maxOutputTokens": 65536, "lastUpdated": "2025-04-07", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -13025,7 +12874,6 @@ "contextWindow": 131072, "maxOutputTokens": 65536, "lastUpdated": "2025-07-25", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -13046,7 +12894,6 @@ "contextWindow": 32768, "maxOutputTokens": 2048, "lastUpdated": "2026-02-10", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -13064,7 +12911,6 @@ "contextWindow": 128000, "maxOutputTokens": 4096, "lastUpdated": "2026-03-31", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -13081,7 +12927,6 @@ "lifecycle": "active", "maxOutputTokens": 4096, "lastUpdated": "2025-06-12", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13099,7 +12944,6 @@ "contextWindow": 128000, "maxOutputTokens": 4096, "lastUpdated": "2026-04-16", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13118,7 +12962,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2024-09", "lastUpdated": "2024-12", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -13140,7 +12983,6 @@ "maxOutputTokens": 65536, "structuredOutput": true, "lastUpdated": "2026-04-28", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -13204,7 +13046,6 @@ "maxOutputTokens": 262144, "structuredOutput": true, "lastUpdated": "2026-08-11", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -13225,7 +13066,6 @@ "contextWindow": 128000, "maxOutputTokens": 4096, "lastUpdated": "2026-01-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -13243,7 +13083,6 @@ "contextWindow": 128000, "maxOutputTokens": 8192, "lastUpdated": "2024-08-26", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13261,7 +13100,6 @@ "contextWindow": 128000, "maxOutputTokens": 128000, "lastUpdated": "2025-10-28", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -13279,7 +13117,6 @@ "contextWindow": 128000, "maxOutputTokens": 8192, "lastUpdated": "2026-03-16", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13297,7 +13134,6 @@ "contextWindow": 32768, "maxOutputTokens": 2048, "lastUpdated": "2025-07-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13315,7 +13151,6 @@ "contextWindow": 32768, "maxOutputTokens": 2048, "lastUpdated": "2025-05-29", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13334,7 +13169,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2024-09", "lastUpdated": "2025-08-18", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -13355,7 +13189,6 @@ "contextWindow": 128000, "maxOutputTokens": 4096, "lastUpdated": "2025-01-17", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13373,7 +13206,6 @@ "contextWindow": 128000, "maxOutputTokens": 4096, "lastUpdated": "2025-12-12", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13391,7 +13223,6 @@ "contextWindow": 128000, "maxOutputTokens": 8192, "lastUpdated": "2025-07-20", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13409,7 +13240,6 @@ "contextWindow": 128000, "maxOutputTokens": 8192, "lastUpdated": "2025-11-13", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13427,7 +13257,6 @@ "contextWindow": 128000, "maxOutputTokens": 8192, "lastUpdated": "2025-06-13", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13444,7 +13273,6 @@ "lifecycle": "active", "maxOutputTokens": 4096, "lastUpdated": "2026-04-16", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13462,7 +13290,6 @@ "contextWindow": 128000, "maxOutputTokens": 4096, "lastUpdated": "2026-01-01", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13479,7 +13306,6 @@ "lifecycle": "active", "maxOutputTokens": 4096, "lastUpdated": "2025-01-08", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13499,7 +13325,6 @@ "knowledgeCutoff": "2025-08", "structuredOutput": true, "lastUpdated": "2025-08-14", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -13521,7 +13346,6 @@ "maxOutputTokens": 32768, "structuredOutput": true, "lastUpdated": "2025-08-05", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -13542,7 +13366,6 @@ "maxOutputTokens": 4096, "knowledgeCutoff": "2023-09", "lastUpdated": "2025-09-05", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13561,7 +13384,6 @@ "maxOutputTokens": 16384, "structuredOutput": false, "lastUpdated": "2026-07-02", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -13578,7 +13400,6 @@ "lifecycle": "active", "structuredOutput": false, "lastUpdated": "2025-08-07", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -13595,7 +13416,6 @@ "lifecycle": "active", "structuredOutput": false, "lastUpdated": "2025-08-19", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -13614,7 +13434,6 @@ "maxOutputTokens": 4096, "structuredOutput": true, "lastUpdated": "2024-11-06", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13633,7 +13452,6 @@ "maxOutputTokens": 66536, "knowledgeCutoff": "2025-04", "lastUpdated": "2025-07-23", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13652,7 +13470,6 @@ "maxOutputTokens": 16384, "knowledgeCutoff": "2024-12", "lastUpdated": "2025-09-05", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13671,7 +13488,6 @@ "maxOutputTokens": 65536, "structuredOutput": true, "lastUpdated": "2026-02-23", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -13694,7 +13510,6 @@ "knowledgeCutoff": "2026-01", "structuredOutput": true, "lastUpdated": "2026-02-16", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -13715,7 +13530,6 @@ "contextWindow": 128000, "maxOutputTokens": 8192, "lastUpdated": "2025-07-25", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13733,7 +13547,6 @@ "contextWindow": 256000, "maxOutputTokens": 16384, "lastUpdated": "2026-02-02", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -13754,7 +13567,6 @@ "contextWindow": 256000, "maxOutputTokens": 16384, "lastUpdated": "2026-05-28", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -13775,7 +13587,6 @@ "contextWindow": 1048576, "maxOutputTokens": 16384, "lastUpdated": "2026-07-15", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -13793,7 +13604,6 @@ "contextWindow": 128000, "maxOutputTokens": 8192, "lastUpdated": "2025-04-10", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -13812,7 +13622,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-06-13", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -13834,7 +13643,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-08-26", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -15353,7 +15161,6 @@ "knowledgeCutoff": "2025-01", "structuredOutput": true, "lastUpdated": "2025-10-17", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -15675,7 +15482,6 @@ "knowledgeCutoff": "2025-05", "structuredOutput": true, "lastUpdated": "2026-07-31", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -15959,7 +15765,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2025-04", "lastUpdated": "2025-12-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -16002,7 +15807,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2025-04", "lastUpdated": "2026-02-11", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -16651,7 +16455,6 @@ "contextWindow": 256000, "maxOutputTokens": 256000, "lastUpdated": "2025-08-20", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -16671,7 +16474,6 @@ "maxOutputTokens": 64000, "structuredOutput": true, "lastUpdated": "2026-07-06", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -16694,7 +16496,6 @@ "maxOutputTokens": 64000, "knowledgeCutoff": "2025-06", "lastUpdated": "2026-04-20", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -16770,7 +16571,6 @@ "maxOutputTokens": 262144, "knowledgeCutoff": "2024-10", "lastUpdated": "2026-01-27", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -16853,7 +16653,6 @@ "maxOutputTokens": 32000, "structuredOutput": false, "lastUpdated": "2026-07-21", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -16875,7 +16674,6 @@ "maxOutputTokens": 32800, "knowledgeCutoff": "2025-06", "lastUpdated": "2026-04-21", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -16894,7 +16692,6 @@ "maxOutputTokens": 32768, "structuredOutput": false, "lastUpdated": "2026-08-27", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -16916,7 +16713,6 @@ "maxOutputTokens": 32768, "structuredOutput": false, "lastUpdated": "2026-07-23", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -16938,7 +16734,6 @@ "maxOutputTokens": 32768, "structuredOutput": false, "lastUpdated": "2026-08-06", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -16956,7 +16751,6 @@ "contextWindow": 1000000, "maxOutputTokens": 131072, "lastUpdated": "2026-06-30", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -16978,7 +16772,6 @@ "maxOutputTokens": 65536, "knowledgeCutoff": "2024-12", "lastUpdated": "2025-12-16", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -16997,7 +16790,6 @@ "maxOutputTokens": 64000, "knowledgeCutoff": "2024-12", "lastUpdated": "2026-03-18", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -17016,7 +16808,6 @@ "maxOutputTokens": 64000, "knowledgeCutoff": "2024-12", "lastUpdated": "2026-03-18", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -17035,7 +16826,6 @@ "maxOutputTokens": 32000, "knowledgeCutoff": "2024-12", "lastUpdated": "2026-04-24", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -17072,7 +16862,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2025-01", "lastUpdated": "2025-12-23", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -17109,7 +16898,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2025-01", "lastUpdated": "2026-02-12", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -17163,7 +16951,6 @@ "maxOutputTokens": 32000, "knowledgeCutoff": "2025-01", "lastUpdated": "2026-05-31", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -17206,7 +16993,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-08-05", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -17249,7 +17035,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-09-02", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -17271,7 +17056,6 @@ "maxOutputTokens": 128000, "knowledgeCutoff": "2026-02", "lastUpdated": "2026-03-11", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -17290,7 +17074,6 @@ "maxOutputTokens": 128000, "knowledgeCutoff": "2026-02", "lastUpdated": "2026-06-04", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -17309,7 +17092,6 @@ "maxOutputTokens": 262144, "structuredOutput": true, "lastUpdated": "2026-08-11", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -17329,7 +17111,6 @@ "knowledgeCutoff": "2025-09-23", "structuredOutput": true, "lastUpdated": "2026-06-09", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -17411,7 +17192,6 @@ "maxOutputTokens": 65536, "knowledgeCutoff": "2025-04", "lastUpdated": "2026-04-02", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -17433,7 +17213,6 @@ "maxOutputTokens": 66000, "knowledgeCutoff": "2025-06", "lastUpdated": "2026-05-08", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -17452,7 +17231,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2025-06", "lastUpdated": "2026-01-28", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -17471,7 +17249,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-08-21", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -18077,7 +17854,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-08-21", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -19422,7 +19198,6 @@ "maxOutputTokens": 64000, "structuredOutput": false, "lastUpdated": "2026-06-17", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -19766,7 +19541,6 @@ "maxOutputTokens": 460800, "structuredOutput": true, "lastUpdated": "2026-08-14", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -20315,7 +20089,6 @@ "maxOutputTokens": 32768, "structuredOutput": false, "lastUpdated": "2026-04-02", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -20358,7 +20131,6 @@ "maxOutputTokens": 32768, "structuredOutput": false, "lastUpdated": "2026-04-02", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -20380,7 +20152,6 @@ "maxOutputTokens": 65536, "structuredOutput": false, "lastUpdated": "2026-03-25", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -20399,7 +20170,6 @@ "maxOutputTokens": 65536, "structuredOutput": false, "lastUpdated": "2026-03-25", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -20560,7 +20330,6 @@ "maxOutputTokens": 32768, "structuredOutput": false, "lastUpdated": "2026-08-27", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -20582,7 +20351,6 @@ "maxOutputTokens": 32768, "structuredOutput": false, "lastUpdated": "2026-09-04", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -20625,7 +20393,6 @@ "maxOutputTokens": 32768, "structuredOutput": false, "lastUpdated": "2026-09-10", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -20719,7 +20486,6 @@ "maxOutputTokens": 8192, "structuredOutput": true, "lastUpdated": "2026-08-11", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -21787,7 +21553,6 @@ "maxOutputTokens": 235929, "structuredOutput": true, "lastUpdated": "2026-09-08", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -21809,7 +21574,6 @@ "maxOutputTokens": 235929, "structuredOutput": true, "lastUpdated": "2026-09-08", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -21912,7 +21676,6 @@ "maxOutputTokens": 65536, "structuredOutput": false, "lastUpdated": "2026-04-28", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -21956,7 +21719,6 @@ "maxOutputTokens": 235929, "structuredOutput": true, "lastUpdated": "2026-03-11", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -22001,7 +21763,6 @@ "maxOutputTokens": 65536, "structuredOutput": false, "lastUpdated": "2026-06-04", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -22045,7 +21806,6 @@ "maxOutputTokens": 8192, "structuredOutput": false, "lastUpdated": "2026-06-04", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -22088,7 +21848,6 @@ "maxOutputTokens": 65536, "structuredOutput": false, "lastUpdated": "2026-08-11", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -23399,7 +23158,6 @@ "maxOutputTokens": 8000, "structuredOutput": true, "lastUpdated": "2026-02-01", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -23592,7 +23350,6 @@ "maxOutputTokens": 32768, "structuredOutput": false, "lastUpdated": "2026-07-21", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -23635,7 +23392,6 @@ "maxOutputTokens": 32768, "structuredOutput": false, "lastUpdated": "2026-07-02", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -25181,7 +24937,6 @@ "maxOutputTokens": 262144, "structuredOutput": false, "lastUpdated": "2026-07-30", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -25203,7 +24958,6 @@ "maxOutputTokens": 262144, "structuredOutput": false, "lastUpdated": "2026-07-15", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -27916,7 +27670,6 @@ "contextWindow": 202752, "maxOutputTokens": 16384, "lastUpdated": "2026-02-11", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -27937,7 +27690,6 @@ "contextWindow": 131072, "maxOutputTokens": 16384, "lastUpdated": "2026-03-08", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -27955,7 +27707,6 @@ "contextWindow": 131072, "maxOutputTokens": 16384, "lastUpdated": "2026-03-08", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -27973,7 +27724,6 @@ "contextWindow": 131072, "maxOutputTokens": 16384, "lastUpdated": "2026-03-08", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -27991,7 +27741,6 @@ "contextWindow": 131072, "maxOutputTokens": 16384, "lastUpdated": "2026-03-08", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -28010,7 +27759,6 @@ "maxOutputTokens": 32768, "knowledgeCutoff": "2025-01", "lastUpdated": "2026-01-27", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -28031,7 +27779,6 @@ "contextWindow": 204800, "maxOutputTokens": 32768, "lastUpdated": "2026-02-12", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -28049,7 +27796,6 @@ "contextWindow": 131072, "maxOutputTokens": 16384, "lastUpdated": "2026-03-08", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -28070,7 +27816,6 @@ "inputLimit": 192000, "maxOutputTokens": 128000, "lastUpdated": "2026-07-06", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -28116,7 +27861,6 @@ "inputLimit": 192000, "maxOutputTokens": 128000, "lastUpdated": "2026-07-06", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -28138,7 +27882,6 @@ "contextWindow": 256000, "maxOutputTokens": 64000, "lastUpdated": "2026-04-20", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -30877,7 +30620,6 @@ "contextWindow": 256000, "maxOutputTokens": 32000, "lastUpdated": "2026-08-27", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -30898,7 +30640,6 @@ "contextWindow": 256000, "maxOutputTokens": 32000, "lastUpdated": "2026-08-27", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -30919,7 +30660,6 @@ "contextWindow": 256000, "maxOutputTokens": 32000, "lastUpdated": "2026-09-04", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -30937,7 +30677,6 @@ "contextWindow": 256000, "maxOutputTokens": 32000, "lastUpdated": "2026-09-04", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -30955,7 +30694,6 @@ "contextWindow": 256000, "maxOutputTokens": 32000, "lastUpdated": "2026-09-08", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -30976,7 +30714,6 @@ "contextWindow": 256000, "maxOutputTokens": 32000, "lastUpdated": "2026-09-08", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -31243,7 +30980,6 @@ "maxOutputTokens": 4096, "knowledgeCutoff": "2023-12", "lastUpdated": "2024-12-06", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -31262,7 +30998,6 @@ "maxOutputTokens": 4096, "knowledgeCutoff": "2024-08", "lastUpdated": "2025-04-05", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -31281,7 +31016,6 @@ "maxOutputTokens": 4096, "knowledgeCutoff": "2024-08", "lastUpdated": "2025-04-05", - "isFree": true, "capabilities": { "vision": true, "reasoning": false, @@ -33861,7 +33595,6 @@ "maxOutputTokens": 32768, "structuredOutput": false, "lastUpdated": "2026-07-21", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -35556,7 +35289,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2024-12", "lastUpdated": "2026-03-18", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -35577,7 +35309,6 @@ "contextWindow": 8192, "maxOutputTokens": 8192, "lastUpdated": "2026-03-18", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -35596,7 +35327,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2024-12", "lastUpdated": "2026-04-22", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -35618,7 +35348,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2024-12", "lastUpdated": "2026-04-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -35639,7 +35368,6 @@ "contextWindow": 8192, "maxOutputTokens": 8192, "lastUpdated": "2026-04-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -35657,7 +35385,6 @@ "contextWindow": 8192, "maxOutputTokens": 8192, "lastUpdated": "2026-04-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -35675,7 +35402,6 @@ "contextWindow": 8192, "maxOutputTokens": 8192, "lastUpdated": "2026-04-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -35696,7 +35422,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2024-12", "lastUpdated": "2026-03-18", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -35717,7 +35442,6 @@ "contextWindow": 8192, "maxOutputTokens": 8192, "lastUpdated": "2026-03-18", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -35736,7 +35460,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2024-12", "lastUpdated": "2026-04-22", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -35758,7 +35481,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2024-12", "lastUpdated": "2026-04-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -35779,7 +35501,6 @@ "contextWindow": 8192, "maxOutputTokens": 8192, "lastUpdated": "2026-04-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -35797,7 +35518,6 @@ "contextWindow": 8192, "maxOutputTokens": 8192, "lastUpdated": "2026-04-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -35815,7 +35535,6 @@ "contextWindow": 8192, "maxOutputTokens": 8192, "lastUpdated": "2026-04-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -35836,7 +35555,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2024-12", "lastUpdated": "2026-03-18", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -35857,7 +35575,6 @@ "contextWindow": 8192, "maxOutputTokens": 8192, "lastUpdated": "2026-03-18", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -35876,7 +35593,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2024-12", "lastUpdated": "2026-04-22", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -35898,7 +35614,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2024-12", "lastUpdated": "2026-04-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -35919,7 +35634,6 @@ "contextWindow": 8192, "maxOutputTokens": 8192, "lastUpdated": "2026-04-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -35937,7 +35651,6 @@ "contextWindow": 8192, "maxOutputTokens": 8192, "lastUpdated": "2026-04-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -35955,7 +35668,6 @@ "contextWindow": 8192, "maxOutputTokens": 8192, "lastUpdated": "2026-04-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": false, @@ -36018,7 +35730,6 @@ "maxOutputTokens": 98304, "knowledgeCutoff": "2025-04", "lastUpdated": "2025-07-28", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -36124,7 +35835,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2025-04", "lastUpdated": "2026-01-19", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -36314,7 +36024,6 @@ "maxOutputTokens": 131072, "knowledgeCutoff": "2025-04", "lastUpdated": "2025-12-22", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -36336,7 +36045,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-03-16", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -36358,7 +36066,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-06-13", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -36380,7 +36087,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-06-13", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -36402,7 +36108,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-08-14", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -36424,7 +36129,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-08-26", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -36446,7 +36150,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-08-14", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -36758,7 +36461,6 @@ "maxOutputTokens": 128000, "knowledgeCutoff": "2026-01-31", "lastUpdated": "2026-06-30", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -37382,7 +37084,6 @@ "knowledgeCutoff": "2025-01", "structuredOutput": true, "lastUpdated": "2026-06-12", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -37423,7 +37124,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-07-16", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -38116,7 +37816,6 @@ "maxOutputTokens": 256000, "knowledgeCutoff": "2026-03-01", "lastUpdated": "2026-05-29", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -38674,7 +38373,6 @@ "maxOutputTokens": 64000, "knowledgeCutoff": "2025-01-01", "lastUpdated": "2025-12-08", - "isFree": true, "capabilities": { "vision": true, "reasoning": true, @@ -38711,7 +38409,6 @@ "maxOutputTokens": 64000, "knowledgeCutoff": "2025-01-01", "lastUpdated": "2026-01-19", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, @@ -38826,7 +38523,6 @@ "maxOutputTokens": 131072, "structuredOutput": true, "lastUpdated": "2026-06-13", - "isFree": true, "capabilities": { "vision": false, "reasoning": true, diff --git a/scripts/smoke-release-cli-package.mjs b/scripts/smoke-release-cli-package.mjs index 7b342c8732..c275b4a8a1 100644 --- a/scripts/smoke-release-cli-package.mjs +++ b/scripts/smoke-release-cli-package.mjs @@ -739,6 +739,9 @@ async function smokeInteractiveTui({ packageRoot, cliEntrypoint, ptySpawn, root const workspace = join(root, 'workspace'); mkdirSync(workspace, { recursive: true }); const environment = isolatedEnvironment(home); + for (const key of ['DEEPSEEK_API_KEY', 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY']) { + delete environment[key]; + } const dataRoots = await resolveInstalledDataRoots(packageRoot, environment, home); await withCleanup( async () => { @@ -760,7 +763,9 @@ async function smokeInteractiveTui({ packageRoot, cliEntrypoint, ptySpawn, root }, timeoutMs: PROCESS_TIMEOUT_MS, }); - if (result.exitCode !== 0) throw new Error(`Interactive TUI exited with ${result.exitCode}`); + if (result.exitCode !== 0) { + throw new Error(`Interactive TUI exited with ${result.exitCode}: ${result.output}`); + } }, (completed) => settleRuntimeHost(packageRoot, dataRoots.workspaceRoot, completed), ); @@ -844,6 +849,9 @@ async function smokeNpxScheduleRecovery({ packageRoot, cliEntrypoint, ptySpawn, // or a symlink that resolves back to the persistent installation. cpSync(packageRoot, temporaryPackage, { recursive: true, dereference: true }); const environment = { ...isolatedEnvironment(home), npm_config_cache: cache }; + for (const key of ['DEEPSEEK_API_KEY', 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY']) { + delete environment[key]; + } const dataRoots = await resolveInstalledDataRoots(packageRoot, environment, home); const installation = await importInstalled(packageRoot, 'dist/runtime-host-cli-installation.js'); if ( @@ -905,12 +913,13 @@ async function smokeNpxScheduleRecovery({ packageRoot, cliEntrypoint, ptySpawn, args: [entrypoint], cwd: workspace, environment, - marker: '/setup', + marker: 'Set Up Provider', onMarker: async (terminal) => { await prepare(); await observer.close(); observer = undefined; - terminal.write('/exit\r'); + terminal.write('\x03'); + setTimeout(() => terminal.write('\x04'), 250); }, timeoutMs: PROCESS_TIMEOUT_MS, }); diff --git a/scripts/sync-model-metadata.mjs b/scripts/sync-model-metadata.mjs index 1045b5764e..7f65d646f9 100644 --- a/scripts/sync-model-metadata.mjs +++ b/scripts/sync-model-metadata.mjs @@ -53,7 +53,6 @@ export const PRICING_EXCLUDED_PROVIDER_TYPES = new Set([ 'kimi-coding-plan', 'minimax-coding-plan', 'MiniMax-cn', - 'opencode-free', 'opencode-go', 'stepfun-ai-step-plan', 'stepfun-step-plan', diff --git a/website/README.md b/website/README.md index b2b88ff786..71499993fa 100644 --- a/website/README.md +++ b/website/README.md @@ -29,7 +29,7 @@ npm --workspace @maka/website run test:dist ## Content -- The positioning sentence, the page structure and the three download paths follow the consensus in [#4307](https://github.com/apache/maka/discussions/4307). The homepage direction (Astryx Centered Hero) was chosen by vote in the same thread. +- The positioning sentence and homepage direction (Astryx Centered Hero) follow the consensus in [#4307](https://github.com/apache/maka/discussions/4307). Public download calls to action lead to approved releases, or the current release status until one is available. Contributor setup and testing instructions live in CONTRIBUTING; the site does not promote Nightly installers to general users. See the [ASF publication policy](https://www.apache.org/legal/release-policy.html#publication). - English and Chinese are one page each in `src/copy/`. Both share the `Copy` type in `src/copy/types.ts`, so a section, claim or link added to one language fails to type-check until the other has it too, and `test/site.test.mjs` asserts the built pages link the same documents. Yuhan Lei (@Astro-Han) keeps the two in sync. - Numbers on the homepage are drawn from the reports in [`docs/eval/`](../docs/eval/) and link to them. The reports own the numbers. - Fact-check cadence: the homepage is re-read against the product at every release, and whenever the positioning, the primary journey, platform support or the trust boundary changes. The README's *Get Maka* section, `SECURITY.md` and `docs/eval/` are the sources to check against. diff --git a/website/src/components/Downloads.astro b/website/src/components/Downloads.astro index 2441a7d099..b1e6cd31dd 100644 --- a/website/src/components/Downloads.astro +++ b/website/src/components/Downloads.astro @@ -41,14 +41,11 @@ const steps = { `gpg --verify ${archive}.asc ${archive}`, ], checksum: [`curl -O ${dist}${archive}.sha512`, `shasum -a 512 -c ${archive}.sha512`], - clone: ['git clone https://github.com/apache/maka.git', 'cd maka'], - build: ['npm ci', 'npm run build'], }; const sections = [ ['apache-releases', d.releases.h2], ['verify', d.verify.h2], - ['nightly', d.nightly.h2], - ['source', d.source.h2], + ['development', d.development.h2], ] as const; --- @@ -61,8 +58,6 @@ const sections = [
@@ -87,25 +82,10 @@ const sections = [
-
-

{d.nightly.h2}

- {d.nightly.note} -

{d.nightly.p} {d.nightly.windows}

- {copy.hero.nightly} -
- -
- -
-

{d.source.h2}

-
    {d.source.prerequisites.map((item) =>
  • {item}
  • )}
- {(['clone', 'build'] as const).map((step) => ( - <> - {d.source[step]} -
{steps[step].join('\n')}
- - ))} -

{d.source.after}

+
+

{d.development.h2}

+

{d.development.p}

+

{d.development.contribute} · {d.development.discuss}

@@ -119,7 +99,7 @@ const sections = [ // Copy buttons and the outline's current-section marker. (() => { for (const button of document.querySelectorAll('.copy')) { - // Each button is named after its step, so five of them stay distinguishable to a screen reader. + // Name each button after its step so screen readers can distinguish them. const show = (text) => { button.textContent = text; button.setAttribute('aria-label', `${text}: ${button.dataset.label}`); diff --git a/website/src/components/Home.astro b/website/src/components/Home.astro index b38d40fc02..41a126eed8 100644 --- a/website/src/components/Home.astro +++ b/website/src/components/Home.astro @@ -56,8 +56,8 @@ const reads = [

{copy.hero.headline[0]}{copy.hero.headline[1]}{copy.hero.headline[2]}

{copy.hero.lede}

{copy.hero.fine} · {copy.hero.architecture}
@@ -126,19 +126,15 @@ const reads = [ diff --git a/website/src/copy/en.ts b/website/src/copy/en.ts index e0810ddd6c..c6647346a9 100644 --- a/website/src/copy/en.ts +++ b/website/src/copy/en.ts @@ -41,7 +41,7 @@ export const en: Copy = { community: 'Community', security: 'Security', asf: 'ASF', - getMaka: 'Get Maka', + getMaka: 'Release status', menu: 'Menu', }, hero: { @@ -51,9 +51,9 @@ export const en: Copy = { ' of everything it did.', ], lede: 'An agent harness exists to finish tasks. We hold it to one measure: how many it completes and at what cost. We publish every run: same model, same official verifier, full per-task record.', - nightly: 'Try Desktop Nightly', - source: 'Build from source', - fine: 'Nightly is a developer build, not an ASF release', + releases: 'Release status', + contribute: 'Contribute', + fine: 'The first Apache release is in preparation', architecture: 'Read the architecture', }, scene: { @@ -116,17 +116,12 @@ export const en: Copy = { more: 'Log Is the Runtime', }, get: { - h3: 'Get Maka', - p: 'Three paths, kept separate on purpose.', - nightly: { - title: 'Try Desktop Nightly', - body: 'Daily builds from main for developers and testers, published on GitHub Releases. macOS on Apple Silicon and Intel; Windows and Linux are unsigned previews.', - note: 'NOT AN ASF RELEASE · MAY BE UNSTABLE', - }, - source: { - title: 'Build from source', - body: 'Clone apache/maka, then npm ci and npm run build. Desktop, TUI and CLI share one Runtime Host.', - note: 'APACHE-2.0', + h3: 'Releases and development', + p: 'Follow release progress or take part in development.', + contribute: { + title: 'Contribute to Maka', + body: 'The contributor guide covers development setup, testing and submitting changes.', + note: 'FOR CONTRIBUTORS', }, releases: { title: 'Apache Releases', @@ -178,7 +173,7 @@ export const en: Copy = { }, downloads: { title: 'Downloads', - lede: 'The signed source archive is the release. Everything else on this page is a convenience build, and says so.', + lede: 'Apache Maka has not made its first Apache release yet. Approved releases and verification instructions will be listed here.', onThisPage: 'On this page', copy: 'Copy', copied: 'Copied', @@ -189,49 +184,25 @@ export const en: Copy = { value: 'None yet. The first one appears here after its vote.', note: 'NOT YET', }, - nightly: { - label: 'Desktop Nightly', - value: - 'Daily from main. macOS arm64 and x64; Windows x64 and Linux x64 and arm64 as unsigned previews.', - note: 'NOT AN ASF RELEASE', - }, - source: { - label: 'Source', - value: 'apache/maka on GitHub, Apache License 2.0.', - note: 'APACHE-2.0', - }, }, releases: { h2: 'Apache releases', note: 'NO APACHE RELEASE YET', p: 'Apache Maka (Incubating) has not made an Apache release. When the first one passes its vote, this section will list it: the source archive, its SHA-512 checksum and detached GPG signature from the ASF distribution directory, and the KEYS file the signature verifies against.', - distNote: 'Until then the distribution directory does not exist:', + distNote: 'Approved releases will be available from:', }, verify: { h2: 'Verify a release', - p: 'Every Apache release is verified the same way, and every reviewer on the vote does this before voting.', + p: 'After a release is available, replace with its version in the commands below.', keys: 'Step 1: Import the release managers’ keys', signature: 'Step 2: Check the signature', checksum: 'Step 3: Check the checksum', }, - nightly: { - h2: 'Desktop Nightly', - note: 'NOT AN ASF RELEASE', - p: 'Desktop Nightly is built daily from main for developers and testers and published as a GitHub prerelease. Choose the newest Maka Desktop Nightly; after installation the app updates itself on the Nightly channel. It is not an ASF release and is not intended for production use. It ships for macOS on Apple Silicon and Intel, Windows x64, and Linux x64 and arm64.', - windows: 'The Windows and Linux builds are unsigned previews, not a supported release tier.', - }, - source: { - h2: 'Build from source', - prerequisites: [ - 'Node.js 22.19 or newer', - 'npm 11', - 'Git', - 'ripgrep, which the Grep tool shells out to', - ], - clone: 'Step 1: Clone the repository', - build: 'Step 2: Install and build every workspace', - after: - 'CONTRIBUTING covers the workspace layout and how to start Desktop, the TUI and the CLI from that build.', + development: { + h2: 'Participate in development', + p: 'To contribute code or help with testing, read the contributor guide and follow the development mailing list. Development builds are not approved Apache releases.', + contribute: 'Contributor guide', + discuss: 'Development mailing list', }, }, }; diff --git a/website/src/copy/links.ts b/website/src/copy/links.ts index 529718e62d..cc33e0162b 100644 --- a/website/src/copy/links.ts +++ b/website/src/copy/links.ts @@ -24,7 +24,6 @@ const main = `${repo}/blob/main`; export const links = { repo, - releases: `${repo}/releases`, discussions: `${repo}/discussions`, devList: 'https://lists.apache.org/list.html?dev@maka.apache.org', docs: `${main}/docs/README.md`, @@ -32,7 +31,6 @@ export const links = { nineArm: `${main}/docs/eval/terminal-bench-2.1-deepseek-v4-flash-nine-arm.md`, paired: `${main}/docs/eval/terminal-bench-2.1-deepseek-v4-flash-maka-vs-opencode.md`, security: `${main}/SECURITY.md`, - windows: `${main}/docs/windows-support.md`, asf: 'https://www.apache.org/', incubator: 'https://incubator.apache.org/', conduct: 'https://www.apache.org/foundation/policies/conduct.html', diff --git a/website/src/copy/types.ts b/website/src/copy/types.ts index 9ff5442596..8175a8c52d 100644 --- a/website/src/copy/types.ts +++ b/website/src/copy/types.ts @@ -44,8 +44,8 @@ export interface Copy { hero: { headline: [string, string, string]; lede: string; - nightly: string; - source: string; + releases: string; + contribute: string; fine: string; architecture: string; }; @@ -70,8 +70,7 @@ export interface Copy { get: { h3: string; p: string; - nightly: { title: string; body: string; note: string }; - source: { title: string; body: string; note: string }; + contribute: { title: string; body: string; note: string }; releases: { title: string; body: string; note: string }; }; reads: { @@ -104,12 +103,9 @@ export interface Copy { status: { h3: string; release: { label: string; value: string; note: string }; - nightly: { label: string; value: string; note: string }; - source: { label: string; value: string; note: string }; }; releases: { h2: string; note: string; p: string; distNote: string }; verify: { h2: string; p: string; keys: string; signature: string; checksum: string }; - nightly: { h2: string; note: string; p: string; windows: string }; - source: { h2: string; prerequisites: string[]; clone: string; build: string; after: string }; + development: { h2: string; p: string; contribute: string; discuss: string }; }; } diff --git a/website/src/copy/zh-CN.ts b/website/src/copy/zh-CN.ts index d6ed0fde7c..ad292ecdae 100644 --- a/website/src/copy/zh-CN.ts +++ b/website/src/copy/zh-CN.ts @@ -35,15 +35,15 @@ export const zhCN: Copy = { community: '社区', security: '安全', asf: 'ASF', - getMaka: '获取 Maka', + getMaka: '发布进展', menu: '菜单', }, hero: { headline: ['一个高性能的 Agent 工作台,', '并完整记录', '它做过的每一件事。'], lede: 'Agent harness 的本职就是把任务做完。衡量它的标准只有一条:完成了多少,花了多少。我们公开每一次运行:同一个模型,同一个官方验证器,逐任务的完整记录。', - nightly: '体验 Desktop Nightly', - source: '从源码构建', - fine: 'Nightly 是开发者构建,不是 ASF release', + releases: '查看发布进展', + contribute: '参与开发', + fine: '首个 Apache 正式版本正在准备中', architecture: '阅读架构文档', }, scene: { @@ -96,17 +96,12 @@ export const zhCN: Copy = { more: 'Log Is the Runtime', }, get: { - h3: '获取 Maka', - p: '三条路径,边界分明。', - nightly: { - title: '体验 Desktop Nightly', - body: '每天基于 main 构建,面向开发者和测试者,发布在 GitHub Releases。macOS 覆盖 Apple Silicon 与 Intel;Windows 和 Linux 是未签名预览。', - note: '不是 ASF RELEASE · 可能不稳定', - }, - source: { - title: '从源码构建', - body: '克隆 apache/maka,然后 npm ci 和 npm run build。Desktop、TUI 和 CLI 共用同一个运行时宿主。', - note: 'APACHE-2.0', + h3: '发布与开发', + p: '关注正式版本的发布进展,或参与项目开发。', + contribute: { + title: '参与 Maka 开发', + body: '贡献指南介绍了开发环境搭建、测试和提交改动的方法。', + note: '面向贡献者', }, releases: { title: 'Apache Releases', @@ -157,7 +152,7 @@ export const zhCN: Copy = { }, downloads: { title: '下载', - lede: '带签名的源码包才是正式 release。本页其余内容都是便利构建,并且都明确标注。', + lede: 'Apache Maka 尚未发布首个 Apache 正式版本。获批后,本页会提供发布文件和验证说明。', onThisPage: '本页目录', copy: '复制', copied: '已复制', @@ -168,44 +163,25 @@ export const zhCN: Copy = { value: '暂未发布。首个 release 投票通过后会列在这里。', note: '暂无', }, - nightly: { - label: 'Desktop Nightly', - value: - '每天基于 main 构建。macOS arm64 与 x64;Windows x64、Linux x64 与 arm64 为未签名预览。', - note: '不是 ASF RELEASE', - }, - source: { - label: '源码', - value: 'GitHub 上的 apache/maka,Apache License 2.0。', - note: 'APACHE-2.0', - }, }, releases: { h2: 'Apache releases', note: '暂无 APACHE RELEASE', p: 'Apache Maka (Incubating) 尚未发布过 Apache release。首个 release 投票通过后会列在这里:源码包、ASF 分发目录中的 SHA-512 校验和与独立的 GPG 签名,以及签名对应的 KEYS 文件。', - distNote: '在此之前,分发目录尚未创建:', + distNote: '获批的正式版本将发布到:', }, verify: { h2: '验证 release', - p: '所有 Apache release 的验证方式都一样,参与投票的每位 reviewer 在表决前都会走一遍这几步。', + p: '正式版本发布后,将以下命令中的 替换为对应版本号。', keys: '第 1 步:导入 release manager 的公钥', signature: '第 2 步:校验签名', checksum: '第 3 步:核对校验和', }, - nightly: { - h2: 'Desktop Nightly', - note: '不是 ASF RELEASE', - p: 'Desktop Nightly 每天基于 main 构建,面向开发者和测试者,以 GitHub prerelease 形式发布。选择最新的 Maka Desktop Nightly;安装后应用会在 Nightly 渠道自动更新。它不是 ASF release,不适合生产环境。提供 macOS 的 Apple Silicon 与 Intel、Windows x64、Linux x64 与 arm64 构建。', - windows: 'Windows 和 Linux 构建是未签名预览,不属于受支持的发布层级。', - }, - source: { - h2: '从源码构建', - prerequisites: ['Node.js 22.19 或更高版本', 'npm 11', 'Git', 'ripgrep,供 Grep 工具调用'], - clone: '第 1 步:克隆仓库', - build: '第 2 步:安装依赖并构建全部 workspace', - after: - 'CONTRIBUTING 介绍了 workspace 的目录结构,以及如何从这份构建启动 Desktop、TUI 和 CLI。', + development: { + h2: '参与开发', + p: '如果希望贡献代码或协助测试,请阅读贡献指南并关注开发邮件列表。开发构建不是获批的 Apache 正式版本。', + contribute: '贡献指南', + discuss: '开发邮件列表', }, }, }; diff --git a/website/src/styles/site.css b/website/src/styles/site.css index 1ea8e2273c..70b21cbc69 100644 --- a/website/src/styles/site.css +++ b/website/src/styles/site.css @@ -733,7 +733,6 @@ h2 { } } -/* Get Maka: three paths */ .tile.get { flex-direction: row; gap: 40px; @@ -745,10 +744,10 @@ h2 { flex-direction: column; gap: 8px; } -.paths3 { +.paths { flex: 1; display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(2, 1fr); gap: 24px; } .path { @@ -1111,7 +1110,7 @@ h2 { .get-h { flex-basis: auto; } - .paths3 { + .paths { grid-template-columns: 1fr; } .reads { diff --git a/website/test/site.test.mjs b/website/test/site.test.mjs index 8f11997bcb..30989cd2bc 100644 --- a/website/test/site.test.mjs +++ b/website/test/site.test.mjs @@ -119,7 +119,7 @@ test('every copy button on the downloads page has its own accessible name', () = /