Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b26b865
refactor(core): build every model catalog entry through one projection
Astro-Han Aug 31, 2026
1b114eb
refactor(core): resolve a model's thinking and vision inside its cata…
Astro-Han Aug 31, 2026
ba74830
feat(runtime-host): make the Host the authority for the model catalog
Astro-Han Aug 31, 2026
95da124
refactor(core): give the provider registry one exported name
Astro-Han Aug 31, 2026
313f5de
refactor(core): drop the two catalog entry fields nothing reads
Astro-Han Aug 31, 2026
97813c6
refactor(core): drop the chat-default validator no caller had
Astro-Han Aug 31, 2026
8484ae7
refactor(core): drop the model-choice provenance nothing produces or …
Astro-Han Sep 1, 2026
79e4253
refactor(core): route the last registry lookups through providerDefau…
Astro-Han Sep 1, 2026
2a231b3
refactor(core): drop the availability field that only restated the re…
Astro-Han Sep 1, 2026
f701554
Merge origin/main into refactor/model-catalog-host-authority
Astro-Han Sep 1, 2026
455a121
test(runtime-host): declare the catalog entry count in two hosted-tar…
Astro-Han Sep 1, 2026
ab572cb
fix(core): admit the fallback rows a resolved catalog actually carries
Astro-Han Sep 1, 2026
69094af
fix(core): let the auth contract's unknown-provider branch actually run
Astro-Han Sep 1, 2026
39a7a51
fix(cli): read the TUI's opening context window from the resolved cat…
Astro-Han Sep 1, 2026
696149b
fix(core): drop unservable Codex ids from the stored selection too
Astro-Han Sep 1, 2026
53fc939
fix(desktop): show the Host's entries while the connection editor is …
Astro-Han Sep 1, 2026
3d6307e
Merge remote-tracking branch 'origin/main' into refactor/model-catalo…
Astro-Han Sep 1, 2026
1edbf85
Merge remote-tracking branch 'origin/main' into refactor/model-catalo…
Astro-Han Sep 1, 2026
3073ce0
refactor(core): ship only the catalog-entry fields something reads
Astro-Han Sep 1, 2026
d540823
Merge remote-tracking branch 'origin/main' into refactor/model-catalo…
Astro-Han Sep 1, 2026
75ed23e
refactor(core): give a provider's shipped baseline one authority
Astro-Han Sep 1, 2026
5576ed4
refactor(core): make provider recognition go through its one owner
Astro-Han Sep 1, 2026
478adba
Merge remote-tracking branch 'origin/main' into refactor/model-catalo…
Astro-Han Sep 1, 2026
dc680ac
chore: regenerate the astryx surface inventory after the merge
Astro-Han Sep 1, 2026
16b1719
refactor(core): let the Host alone decide what a picker may offer
Astro-Han Sep 1, 2026
638820d
refactor(core): keep only the admission answer in the auth contract
Astro-Han Sep 1, 2026
52d32ff
refactor(core): drop the registry fields and helpers nothing reads
Astro-Han Sep 1, 2026
8eefe46
refactor(cli): stop threading TUI fields no screen reads
Astro-Han Sep 1, 2026
052430d
refactor(desktop): read the renderer's two model constants from core
Astro-Han Sep 1, 2026
902c967
refactor(core): home the offerable-models answer on the connection mo…
Astro-Han Sep 1, 2026
ecf7e9a
fix(desktop): name models from the Host's entries everywhere a client…
Astro-Han Sep 1, 2026
7e78145
refactor(storage): stop persisting the registry's shipped inventory
Astro-Han Sep 1, 2026
ff29a65
refactor(core): drop the catalog pricing seam nothing produces
Astro-Han Sep 1, 2026
e5a0d62
refactor(core): derive a provider's wire from its adapter instead of …
Astro-Han Sep 1, 2026
2b0f47b
refactor(core): ask the one question the inventory taxonomy answered
Astro-Han Sep 1, 2026
9976b24
refactor(core): make an action's availability a boolean
Astro-Han Sep 1, 2026
b1b64c8
refactor(cli): make a model choice carry its account identity
Astro-Han Sep 1, 2026
012a21c
refactor(runtime-host): keep discovery bookkeeping off the catalog wire
Astro-Han Sep 1, 2026
64c306c
refactor(runtime-host): keep override provenance off the catalog wire
Astro-Han Sep 1, 2026
fcb8e58
refactor(core): keep only the auth actions something admits
Astro-Han Sep 1, 2026
c90dd2c
docs(runtime-host): state the catalog authority on the seam that carr…
Astro-Han Sep 1, 2026
d7081ca
docs(runtime-host): say the catalog rule in plain words
Astro-Han Sep 1, 2026
42d3adc
docs(runtime-host): drop the author line from the catalog comment
Astro-Han Sep 1, 2026
3d524e9
refactor(desktop): stop sending an enabled-model list the Host discards
Astro-Han Sep 1, 2026
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
6 changes: 3 additions & 3 deletions apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,7 @@
"@maka/core/ui-locale": 1
},
"importSpecifiers": 11,
"nonTriviaTokens": 1267
"nonTriviaTokens": 1264
},
"src/renderer/app-shell-session-start-actions.ts": {
"importDeclarations": 7,
Expand Down Expand Up @@ -4360,8 +4360,8 @@
"./settings-status-badge.js": 1,
"./subagent-preset-presentation.js": 1,
"@astryxdesign/core": 1,
"@maka/core/llm-connections": 2,
"@maka/core/model-thinking": 2,
"@maka/core/llm-connections": 1,
"@maka/core/model-thinking": 1,
"@maka/core/settings": 1,
"@maka/core/subagent-settings": 1,
"@maka/ui": 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import { deferred } from '@maka/core/test-only/async-primitives';
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import type { LlmConnection } from '@maka/core/llm-connections';
import type { ProjectedLlmConnection } from '@maka/core/llm-connections';
import type { StoredMessage } from '@maka/core/session';
import type { DesktopSessionSummary } from '../../preload/bridge-contract.js';
import { createAppShellSessionSettingsActions } from '../../renderer/app-shell-session-settings-actions.js';
Expand Down Expand Up @@ -63,7 +63,7 @@ function pendingClaimOver(state: Record<string, boolean>): SessionPendingClaim {

function createHarness(options: {
confirm?: () => Promise<boolean>;
connections?: LlmConnection[];
connections?: ProjectedLlmConnection[];
messages?: StoredMessage[];
permissionModeResult?: 'ask' | 'bypass';
} = {}) {
Expand Down Expand Up @@ -111,7 +111,7 @@ function createHarness(options: {
const actions = createAppShellSessionSettingsActions({
uiLocale: 'zh',
activeIdRef,
connections: options.connections ?? ([{ slug: 'e2e', name: 'E2E' }] as LlmConnection[]),
connections: options.connections ?? ([{ slug: 'e2e', name: 'E2E', catalogEntries: [] }] as unknown as ProjectedLlmConnection[]),
messages: options.messages ?? [],
permissionModePending: pendingClaimOver(permissionModePending),
sessionModelPending: pendingClaimOver(sessionModelPending),
Expand Down Expand Up @@ -278,9 +278,9 @@ describe('AppShell session settings actions', () => {
it('includes connection names when a switch rebinds the connection', async () => {
const harness = createHarness({
connections: [
{ slug: 'e2e', name: 'Primary' },
{ slug: 'relay', name: 'Relay' },
] as LlmConnection[],
{ slug: 'e2e', name: 'Primary', catalogEntries: [] },
{ slug: 'relay', name: 'Relay', catalogEntries: [] },
] as unknown as ProjectedLlmConnection[],
});

const modelChange = harness.actions.setSessionModel({
Expand Down
94 changes: 91 additions & 3 deletions apps/desktop/src/main/__tests__/model-catalog-choices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,24 @@

import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import type { IdentifiedLlmConnection } from '@maka/core/llm-connections';
import type {
IdentifiedLlmConnection,
ProjectedLlmConnection,
} from '@maka/core/llm-connections';
import {
resolveConnectionModelCatalog,
resolveDraftConnectionModelCatalog,
type ModelCatalogEntry,
} from '@maka/core/model-catalog';
import { buildChatModelChoices } from '@maka/core/chat-model-choice';
import { pickNewChatModel } from '../../renderer/shell-chat-model-selection.js';
import { buildCatalogDailyReviewModelOptions } from '../../renderer/model-catalog-choices.js';

function connection(
overrides: Partial<IdentifiedLlmConnection> &
Pick<IdentifiedLlmConnection, 'slug' | 'providerType'>,
): IdentifiedLlmConnection {
return {
): ProjectedLlmConnection {
const stored: IdentifiedLlmConnection = {
connectionId: `connection-${overrides.slug}`,
name: overrides.slug,
defaultModel: '',
Expand All @@ -37,6 +46,9 @@ function connection(
updatedAt: 1,
...overrides,
};
// The Host resolves the catalog and projects it; tests build connections the
// same way so they exercise what a client actually receives.
return { ...stored, catalogEntries: resolveConnectionModelCatalog(stored) };
}

describe('model catalog picker helpers', () => {
Expand Down Expand Up @@ -110,4 +122,80 @@ describe('model catalog picker helpers', () => {
assert.ok(choices.every((choice) => !(choice.connectionName ?? '').includes('@')));
});

it('renders the Host entry, not a local rebuild, while the editor is unedited', () => {
// A Host that knows this model and a Desktop that does not: the entry says
// the model cannot serve as a chat default and carries a name this build
// has never heard. An unedited editor must show what the Host decided —
// rebuilding locally is exactly the version disagreement the projection
// ends, and here it would also offer a model the Host ruled out.
const stored = {
connectionId: 'connection-relay',
slug: 'relay',
name: 'Relay',
providerType: 'openai-compatible' as const,
defaultModel: 'host-only-model',
enabled: true,
enabledModelIds: ['host-only-model'],
models: [{ id: 'host-only-model' }],
modelSource: 'fetched' as const,
createdAt: 1,
updatedAt: 1,
};
const hostEntry: ModelCatalogEntry = {
...resolveConnectionModelCatalog(stored)[0],
displayName: 'Host-only image model',
canUseAsChatDefault: false,
};
const connection: ProjectedLlmConnection = { ...stored, catalogEntries: [hostEntry] };
const draft = {
models: stored.models,
modelSource: stored.modelSource,
enabledModelIds: stored.enabledModelIds,
};

const unedited = resolveDraftConnectionModelCatalog(connection, draft);
assert.deepEqual(unedited, [hostEntry]);

// And the exception still applies: a draft the Host has not seen is the
// one thing the client resolves for itself.
const edited = resolveDraftConnectionModelCatalog(connection, {
...draft,
models: [...stored.models, { id: 'just-fetched' }],
});
assert.deepEqual(
edited.map((entry) => entry.id).sort(),
['host-only-model', 'just-fetched'],
);
assert.notEqual(edited[0]?.displayName, 'Host-only image model');
});

it('does not offer Daily Review a Codex model the subscription cannot serve', () => {
// A connection saved while `gpt-5-codex` was still picker-visible keeps it
// in `enabledModelIds`. The inventory filter alone left it there, and the
// catalog listed it back as a model no inventory describes — selectable,
// and failing at the provider once a scheduled run sent to it.
const options = buildCatalogDailyReviewModelOptions(
[
connection({
slug: 'codex',
providerType: 'openai-codex',
defaultModel: 'gpt-5.5',
enabledModelIds: ['gpt-5.5', 'gpt-5-codex'],
models: [{ id: 'gpt-5.5' }],
modelSource: 'fetched',
}),
],
'',
);
const keys = options.map(([key]) => key);
assert.ok(
keys.includes('codex::gpt-5.5'),
`expected the servable model to be offered, got ${JSON.stringify(keys)}`,
);
assert.equal(
keys.includes('codex::gpt-5-codex'),
false,
`unsupported Codex model was offered: ${JSON.stringify(keys)}`,
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
type AddProviderField,
} from '../../renderer/settings/provider-add-submission.js';
import {
PROVIDER_DEFAULTS,
PROVIDER_REGISTRY,
providerSupportsModelDiscovery,
type CreateConnectionInput,
type IdentifiedLlmConnection,
Expand Down Expand Up @@ -89,8 +89,8 @@ test('no provider type demands a model id at creation', () => {
// Stated across the catalog rather than for the two relays alone: the rule
// that came back would be a per-provider `if`, and asserting only where it
// used to live would let it reappear next door.
for (const providerType of Object.keys(PROVIDER_DEFAULTS) as ProviderType[]) {
const defaults = PROVIDER_DEFAULTS[providerType];
for (const providerType of Object.keys(PROVIDER_REGISTRY) as ProviderType[]) {
const defaults = PROVIDER_REGISTRY[providerType];
if (defaults.status === 'phase3-experimental') continue;
const issue = validateAddProviderDraft(
draft({
Expand Down Expand Up @@ -160,7 +160,7 @@ test('a successful catalog fetch reports no error', async () => {
});

test('a provider without discovery is not asked, and reports no error', async () => {
const withoutDiscovery = (Object.keys(PROVIDER_DEFAULTS) as ProviderType[]).find(
const withoutDiscovery = (Object.keys(PROVIDER_REGISTRY) as ProviderType[]).find(
(providerType) => !providerSupportsModelDiscovery(providerType),
);
assert.ok(withoutDiscovery, 'expected at least one provider with no discovery endpoint');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import assert from 'node:assert/strict';
import test from 'node:test';
import { PROVIDER_DEFAULTS } from '@maka/core/llm-connections';
import { PROVIDER_REGISTRY } from '@maka/core/llm-connections';
import {
endpointCarriesCredentials,
providerEndpointPresentation,
Expand All @@ -34,12 +34,12 @@ test('fixed Alibaba access paths expose their distinct effective endpoints read-
const tokenPlanChina = providerEndpointPresentation({ providerType: 'alibaba-token-plan-cn' });

assert.deepEqual(api, {
value: PROVIDER_DEFAULTS.alibaba.baseUrl,
value: PROVIDER_REGISTRY.alibaba.baseUrl,
editable: false,
emptyState: 'missing',
});
assert.deepEqual(tokenPlanChina, {
value: PROVIDER_DEFAULTS['alibaba-token-plan-cn'].baseUrl,
value: PROVIDER_REGISTRY['alibaba-token-plan-cn'].baseUrl,
editable: false,
emptyState: 'missing',
});
Expand Down Expand Up @@ -118,7 +118,7 @@ test('custom relays and local runtimes retain endpoint editing', () => {
assert.deepEqual(
providerEndpointPresentation({ providerType: 'ollama' }),
{
value: PROVIDER_DEFAULTS.ollama.baseUrl,
value: PROVIDER_REGISTRY.ollama.baseUrl,
editable: true,
emptyState: 'missing',
},
Expand All @@ -140,7 +140,7 @@ test('derived and OAuth endpoints remain visible but read-only', () => {
assert.deepEqual(
providerEndpointPresentation({ providerType: 'openai-codex' }),
{
value: PROVIDER_DEFAULTS['openai-codex'].baseUrl,
value: PROVIDER_REGISTRY['openai-codex'].baseUrl,
editable: false,
emptyState: 'managed',
},
Expand All @@ -161,7 +161,7 @@ test('providers with model-level endpoint overrides say so when showing the defa
assert.deepEqual(
providerEndpointPresentation({ providerType: 'zenmux' }),
{
value: PROVIDER_DEFAULTS.zenmux.baseUrl,
value: PROVIDER_REGISTRY.zenmux.baseUrl,
editable: false,
emptyState: 'missing',
modelOverrides: true,
Expand All @@ -170,7 +170,7 @@ test('providers with model-level endpoint overrides say so when showing the defa
assert.deepEqual(
providerEndpointPresentation({ providerType: 'cohere' }),
{
value: PROVIDER_DEFAULTS.cohere.baseUrl,
value: PROVIDER_REGISTRY.cohere.baseUrl,
editable: false,
emptyState: 'missing',
modelOverrides: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@

import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import type { ConnectionCatalogSnapshot, ConnectionTarget } from '@maka/core/runtime-policy';
import type { ConnectionTarget } from '@maka/core/runtime-policy';
import type {
RuntimeHostConnectionCatalogEntry as ConnectionCatalogEntry,
RuntimeHostConnectionCatalogSnapshot as ConnectionCatalogSnapshot,
} from '@maka/runtime-host/client';
import {
synchronizeRuntimeHostAccountConnection,
type RuntimeHostAccountConnectionClient,
Expand All @@ -42,9 +46,9 @@ function catalogWithoutDefault(): ConnectionCatalogSnapshot {
providerType: 'openai-codex',
enabled: true,
enabledModelIds: ['gpt-5-codex', 'gpt-5-codex-mini'],
catalogEntries: [],
models: [{ id: 'gpt-5-codex' }, { id: 'gpt-5-codex-mini' }],
modelSource: 'fallback',
modelsFetchedAt: 0,
},
],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import type { AppSettings } from '@maka/core/settings';
import type {
ConnectionCatalogSnapshot,
CredentialLocator,
} from '@maka/core/runtime-policy';
import { gatherRuntimeHostConfig } from '../runtime-host-config-ipc-main.js';
import type {
RuntimeHostConnectionCatalogEntry as ConnectionCatalogEntry,
RuntimeHostConnectionCatalogSnapshot as ConnectionCatalogSnapshot,
} from '@maka/runtime-host/client';import { gatherRuntimeHostConfig } from '../runtime-host-config-ipc-main.js';

const CATALOG: ConnectionCatalogSnapshot = {
revision: 1,
Expand All @@ -41,6 +43,7 @@ const CATALOG: ConnectionCatalogSnapshot = {
providerType: 'deepseek',
enabled: true,
enabledModelIds: ['deepseek-v4-pro'],
catalogEntries: [],
models: [{ id: 'deepseek-v4-pro' }],
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,39 @@

import assert from 'node:assert/strict';
import test from 'node:test';
import { OPENCODE_FREE_DEFAULT_ENABLED_MODELS } from '@maka/core/llm-connections';
import type { ConnectionCatalogSnapshot } from '@maka/core/runtime-policy';
import { defaultEnabledModelIdsWhenOmitted } from '@maka/core/llm-connections';
import type {
RuntimeHostConnectionCatalogEntry as ConnectionCatalogEntry,
RuntimeHostConnectionCatalogSnapshot as ConnectionCatalogSnapshot,
} from '@maka/runtime-host/client';
import {
projectHostConnections,
projectHostConnectionTest,
registerRuntimeHostConnectionsIpc,
} 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(
() =>
normalizeCreateConnectionInputForIpc({
name: 'Injected',
slug: 'injected',
providerType,
enabled: true,
}),
/Invalid Connection input/,
providerType,
);
}
});

test('registers pure Connection reads for replacement-Host retry', () => {
const reads = new Set<string>();
Expand Down Expand Up @@ -75,6 +101,7 @@ test('retries connection delete after a stale revision instead of failing perman
providerType: 'openai-compatible',
baseUrl: 'https://openrouter.ai/api/v1',
enabled: true,
catalogEntries: [],
enabledModelIds: ['model-1'],
models: [{ id: 'model-1' }],
},
Expand Down Expand Up @@ -285,6 +312,7 @@ test('preserves the provider default inventory beside the recommended model', as
providerType: 'opencode-free',
enabled: true,
enabledModelIds: createdModels,
catalogEntries: [],
models: [],
},
],
Expand All @@ -311,7 +339,7 @@ test('preserves the provider default inventory beside the recommended model', as
});

// Snapshot-derived set; assert the contract, not today's ids.
assert.deepEqual(createdModels, [...OPENCODE_FREE_DEFAULT_ENABLED_MODELS]);
assert.deepEqual(createdModels, [...OPENCODE_FREE_ENABLED_MODEL_IDS]);
});

test('projects the Host default target without inventing a second Connection authority', () => {
Expand All @@ -328,6 +356,8 @@ test('projects the Host default target without inventing a second Connection aut
defaultModel: 'model-1',
enabledModelIds: ['model-1', 'model-2'],
models: [{ id: 'model-1' }, { id: 'model-2' }],
// Carried through from the Host projection, not rebuilt here.
catalogEntries: [],
createdAt: 0,
updatedAt: 4,
},
Expand Down Expand Up @@ -395,6 +425,7 @@ function catalog(): ConnectionCatalogSnapshot {
baseUrl: 'https://openrouter.ai/api/v1',
enabled: true,
enabledModelIds: ['model-1', 'model-2'],
catalogEntries: [],
models: [{ id: 'model-1' }, { id: 'model-2' }],
},
],
Expand Down
Loading