Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -951,9 +951,9 @@
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {
"./application/contracts/provider-retirement.js": 1,
"./locales/shell-copy.js": 1,
"./settings/settings-nav.js": 1,
"@maka/core/provider-registry": 1,
"@maka/ui/icons": 1
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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.
*/

// Keep the command palette's retirement query on the public application boundary
// without loading the metadata-built provider registry.
export { isRetiredProvider } from '@maka/core/provider-retirement';
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/command-palette-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {
} from '@maka/ui/icons';
import type { ChatDefaultPermissionMode, SettingsSection, ThemePreference } from '@maka/core/settings';
import type { LlmConnection } from '@maka/core/llm-connections';
import { isRetiredProvider } from '@maka/core/provider-registry';
import { isRetiredProvider } from './application/contracts/provider-retirement.js';
import type { PermissionMode } from '@maka/core/permission';
import type { UiLocale } from '@maka/core/ui-locale';
import type { NavSelection } from '@maka/ui';
Expand Down
25 changes: 25 additions & 0 deletions docs/model-metadata-firstscreen-optimization.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ owners:

# perf(desktop): remove models.dev metadata from the renderer startup path

> **Audit update (2026-09-08, #4711 follow-up):** the `command-palette-commands.ts`
> static chain recorded as accepted debt below is now cut. `isRetiredProvider` was
> extracted into `packages/core/src/provider-retirement.ts` (a metadata-free module
> whose only `provider-registry` import is `import type`), and
> `command-palette-commands.ts` now imports it through
> `application/contracts/provider-retirement.ts`, which re-exports only the predicate
> from `@maka/core/provider-retirement`,
> so the `AppShellOverlays → … → command-palette-commands → provider-registry` hop
> no longer reaches the generated tables. A separate static chain still does —
> `main.tsx → @maka/ui → maka-uri → core/settings → core/onboarding →
> core/llm-connections → provider-registry` — and remains open; the "Desired
> outcome" section already lists the `modelMenuGroups`/`buildChatModelChoices`
> seam it belongs to.

<details open>
<summary><strong>English</strong></summary>

Expand Down Expand Up @@ -116,6 +130,17 @@ Acceptance criteria:
<details>
<summary><strong>简体中文</strong></summary>

> **审计更新(2026-09-08,#4711 后续):** 下面记录为遗留债务的
> `command-palette-commands.ts` 静态链路现已切断。`isRetiredProvider` 被抽取到
> `packages/core/src/provider-retirement.ts`(一个不含元数据的模块,对
> `provider-registry` 仅有 `import type`),`command-palette-commands.ts` 改通过
> `application/contracts/provider-retirement.ts` 引入;该契约仅从
> `@maka/core/provider-retirement` 转导出此谓词,因此
> `AppShellOverlays → … → command-palette-commands → provider-registry` 这一段不再触达
> 生成表。另有一条静态链路仍然存在——`main.tsx → @maka/ui → maka-uri → core/settings →
> core/onboarding → core/llm-connections → provider-registry`——属“期望结果”中已列出的
> `modelMenuGroups`/`buildChatModelChoices` 同一切面,尚未处理。

## 问题

> **状态(2026-09-07 核验):** 该优化已落地——下面列出的五条首屏依赖链在当前源码中均已切断。另有一条静态路径仍存在,现记录为已接受的遗留:`AppShellOverlays`(`app-shell.tsx:170`)→ `useAppShellCommands` → `command-palette-commands.ts:50` 从 `@maka/core/provider-registry` 导入 `isRetiredProvider`,而 `PROVIDER_REGISTRY` 由 `model-metadata.generated` 构建——这是运行时导入而非懒加载的命令面板入口,因此验收标准中"首屏全部静态传递依赖排除 metadata"的绝对表述并未完全满足。Problem 一节保留的是 2026-08-04 优化前状态的记录。
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"./openai-urls": "./dist/openai-urls.js",
"./llm-connections": "./dist/llm-connections.js",
"./provider-registry": "./dist/provider-registry.js",
"./provider-retirement": "./dist/provider-retirement.js",
"./model-catalog": "./dist/model-catalog.js",
"./model-facts": "./dist/model-facts.js",
"./model-metadata": "./dist/model-metadata.js",
Expand Down
17 changes: 15 additions & 2 deletions packages/core/src/__tests__/provider-catalog-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
} from '../provider-registry.js';
import { buildConnectionModelCatalogEntries } from '../model-catalog.js';
import { PROVIDER_AUTH_ACTIONS, deriveProviderAuthContract } from '../provider-auth.js';
import { RETIRED_PROVIDER_TYPES } from '../provider-retirement.js';
import type { ProviderType } from '../llm-connections.js';

describe('provider connection slug derivation contract', () => {
Expand Down Expand Up @@ -228,8 +229,20 @@ describe('retired provider contract', () => {
isRetiredProvider(type),
);

it('pins the entries this catalog retires', () => {
assert.deepEqual(retired, ['opencode-free', 'commandcode-go', 'claude-subscription']);
it('owns the list this catalog retires', () => {
// `RETIRED_PROVIDER_TYPES` is the one source of truth; the registry must
// agree so a name that is retired but not registered (or vice versa) fails
// here rather than at a send time.
assert.deepEqual(RETIRED_PROVIDER_TYPES, [
'opencode-free',
'commandcode-go',
'claude-subscription',
]);
assert.deepEqual(
retired,
RETIRED_PROVIDER_TYPES,
'isRetiredProvider must match RETIRED_PROVIDER_TYPES',
);
});

it('keeps a retired provider registered but unwired', () => {
Expand Down
76 changes: 76 additions & 0 deletions packages/core/src/__tests__/provider-retirement.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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 { spawnSync } from 'node:child_process';
import { describe, it } from 'node:test';
import { isRetiredProvider, RETIRED_PROVIDER_TYPES } from '../provider-retirement.js';

describe('provider retirement', () => {
it('owns the known retired providers', () => {
assert.deepEqual(RETIRED_PROVIDER_TYPES, [
'opencode-free',
'commandcode-go',
'claude-subscription',
]);
});

it('flags every retired provider type', () => {
for (const type of RETIRED_PROVIDER_TYPES) {
assert.equal(isRetiredProvider(type), true, type);
}
});

it('does not flag an active provider', () => {
assert.equal(isRetiredProvider('openai'), false);
});

it('does not flag an unknown provider (a stored connection can outlive the catalog)', () => {
assert.equal(isRetiredProvider('some-future-provider'), false);
});

it('loads without runtime imports', () => {
// Use a fresh process so an already-cached registry cannot hide a dependency.
// Run against emitted JavaScript: type-only imports must have been erased.
const moduleUrl = new URL('../provider-retirement.js', import.meta.url).href;
const result = spawnSync(
process.execPath,
[
'--input-type=module',
'--eval',
`
import { registerHooks } from 'node:module';
const moduleUrl = process.argv[1];
registerHooks({
resolve(specifier, context, nextResolve) {
if (context.parentURL === moduleUrl) {
throw new Error('Unexpected retirement dependency: ' + specifier);
}
return nextResolve(specifier, context);
},
});
await import(moduleUrl);
`,
moduleUrl,
],
{ encoding: 'utf8', timeout: 10_000 },
);
assert.equal(result.status, 0, result.error?.message ?? result.stderr);
});
});
2 changes: 1 addition & 1 deletion packages/core/src/connection-readiness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import {
type LlmConnection,
} from './llm-connections.js';
import { isModelExplicitlyUnsupportedForChat } from './model-catalog.js';
import { isRetiredProvider } from './provider-registry.js';
import { isRetiredProvider } from './provider-retirement.js';

/**
* Canonical reasons why an LlmConnection is not ready to send.
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/model-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
providerFallbackModelIds,
providerSupportsModelDiscovery,
} from './llm-connections.js';
import { isRetiredProvider } from './provider-retirement.js';
import { lookupModelMetadata, resolveModelVisionSupport } from './model-metadata.js';
import {
modelOverride,
Expand Down Expand Up @@ -234,7 +235,7 @@ export function buildConnectionModelCatalogEntries(
// renders, but they stop being selectable. Without this the pickers would
// keep offering models that can no longer send — `runtimeAdapter:
// 'unavailable'` blocks the send, not the choice.
providerRetired: defaults.retired === true,
providerRetired: isRetiredProvider(connection.providerType),
...(connection.modelOverrides ? { modelOverrides: connection.modelOverrides } : {}),
// Keep manually configured models visible while disabled, even when no
// provider inventory lists them. Selection still controls their use.
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/provider-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
providerSupportsModelDiscovery,
type ProviderType,
} from './llm-connections.js';
import { isRetiredProvider } from './provider-retirement.js';

/**
* The credential operations this contract admits. One entry per operation the
Expand Down Expand Up @@ -65,7 +66,7 @@ export function deriveProviderAuthContract(input: {
// nowhere: no Runtime adapter to send on, no sign-in to complete, no endpoint
// to test. Deleting the connection is what clears the credential this machine
// still holds.
if (!defaults || defaults.retired === true) {
if (!defaults || isRetiredProvider(input.providerType)) {
return { requiresSecret, actionAvailability: actions({}) };
}

Expand Down
20 changes: 4 additions & 16 deletions packages/core/src/provider-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,9 @@ export interface ProviderDefaults {
runtimeAdapter: ProviderRuntimeAdapter;
/** Additional request protocols; omitted models still use runtimeAdapter. */
protocolAdapters?: Partial<Record<ModelApiProtocol, ProviderRuntimeAdapter>>;
/**
* Maka used to offer this provider and no longer does. The entry stays
* registered so stored connections still decode; it just cannot be used.
*/
retired?: true;
// Whether Maka still offers a provider is owned by `provider-retirement.ts`,
// not by a flag here: the Desktop first screen needs that answer without
// loading the generated models.dev tables this module is built from.
modelDiscovery: ProviderModelDiscovery;
category: ProviderCategory;
catalogGroup?: ProviderCatalogGroup;
Expand Down Expand Up @@ -1206,7 +1204,6 @@ const providerRegistry = {
fallbackModels: [],
status: 'phase3-experimental',
runtimeAdapter: { kind: 'unavailable' },
retired: true,
modelDiscovery: {
kind: 'fallback',
reason: 'OpenCode restricts its free tier to the OpenCode client.',
Expand Down Expand Up @@ -1499,7 +1496,6 @@ const providerRegistry = {
fallbackModels: [],
status: 'phase3-experimental',
runtimeAdapter: { kind: 'unavailable' },
retired: true,
modelDiscovery: {
kind: 'fallback',
reason: 'The GO plan was reached through the official CLI\u2019s private transport.',
Expand Down Expand Up @@ -1636,7 +1632,6 @@ const providerRegistry = {
],
status: 'phase3-experimental',
runtimeAdapter: { kind: 'unavailable' },
retired: true,
modelDiscovery: {
kind: 'fallback',
reason:
Expand Down Expand Up @@ -1711,14 +1706,7 @@ export function providerMenuLabel(providerType: string): string | undefined {
return defaults && (defaults.menuLabel ?? defaults.label);
}

/**
* A provider Maka used to offer and no longer does. Read this rather than
* inferring retirement from an unavailable adapter: a provider that was never
* wired looks identical from there and is not the same thing.
*/
export function isRetiredProvider(providerType: string): boolean {
return providerDefaultsOf(providerType)?.retired === true;
}
export { isRetiredProvider, RETIRED_PROVIDER_TYPES } from './provider-retirement.js';

/**
* Whether a request on this provider may carry an output-token limit. The
Expand Down
65 changes: 65 additions & 0 deletions packages/core/src/provider-retirement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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.
*/

/**
* Provider retirement — the one owner of "did Maka stop offering this
* provider?".
*
* Kept out of `provider-registry.ts` on purpose. Retirement is a handful of
* provider-type strings, but the registry is built from the generated
* models.dev tables, so anything that imported the predicate from there pulled
* the whole metadata snapshot in with it. The Desktop first screen needs this
* boolean and nothing else from the catalog; see
* `docs/model-metadata-firstscreen-optimization.md`.
*
* The type-only import below is erased at build time, so this module has no
* runtime dependency on the registry.
*/

import type { ProviderType } from './provider-registry.js';

/**
* Provider types Maka used to offer and no longer does. The registry keeps its
* entry so stored connections still decode; retirement only says it cannot be
* used to send.
*
* `packages/core/src/__tests__/provider-catalog-contract.test.ts` pins this
* list against the registry, so a name that is not a registered provider — or a
* retired provider that is still wired to a Runtime adapter — fails there.
*/
export const RETIRED_PROVIDER_TYPES: readonly ProviderType[] = [
'opencode-free',
'commandcode-go',
'claude-subscription',
];

const RETIRED_PROVIDER_TYPE_SET: ReadonlySet<string> = new Set<string>(RETIRED_PROVIDER_TYPES);

/**
* A provider Maka used to offer and no longer does. Read this rather than
* inferring retirement from an unavailable adapter: a provider that was never
* wired looks identical from there and is not the same thing.
*
* Takes a plain `string` because stored connections outlive the catalog: a
* connection written by a newer build can name a provider type this one does
* not know, and an unknown type is not retired.
*/
export function isRetiredProvider(providerType: string): boolean {
return RETIRED_PROVIDER_TYPE_SET.has(providerType);
}
Loading