Skip to content
Merged
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
11 changes: 11 additions & 0 deletions packages/core/src/__tests__/bootstrap-connections.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,22 @@ import {
OPENCODE_FREE_DEFAULT_ENABLED_MODELS,
OPENCODE_FREE_DEFAULT_MODEL,
OPENCODE_FREE_LEGACY_DEFAULT_MODEL,
defaultEnabledModelIdsWhenOmitted,
resolveBootstrapConnections,
resolveOpenCodeFreeBootstrapMigration,
} from '../bootstrap-connections.js';
import type { LlmConnection } from '../llm-connections.js';

describe('defaultEnabledModelIdsWhenOmitted', () => {
it('fills OpenCode Free with the default free inventory and leaves others unset', () => {
assert.deepEqual(defaultEnabledModelIdsWhenOmitted('opencode-free'), [
...OPENCODE_FREE_DEFAULT_ENABLED_MODELS,
]);
assert.equal(defaultEnabledModelIdsWhenOmitted('openai'), undefined);
assert.equal(defaultEnabledModelIdsWhenOmitted('openrouter'), undefined);
});
});

describe('resolveBootstrapConnections — zero-credential default seed', () => {
it('selects one default while keeping the credential-free fallback', () => {
const cases = [
Expand Down
21 changes: 20 additions & 1 deletion packages/core/src/bootstrap-connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import type { LlmConnection, ProviderType, UpdateConnectionInput } from './llm-connections.js';

export const OPENCODE_FREE_DEFAULT_MODEL = 'nemotron-3-ultra-free';
/** Models enabled on a fresh OpenCode Free bootstrap (default first). */
/** Models enabled on a fresh OpenCode Free connection (default first). */
export const OPENCODE_FREE_DEFAULT_ENABLED_MODELS = [
OPENCODE_FREE_DEFAULT_MODEL,
'mimo-v2.5-free',
Expand Down Expand Up @@ -68,6 +68,25 @@ const OPENAI_ENV_SEED: Omit<BootstrapConnectionSeed, 'isDefault'> = {
defaultModel: 'gpt-4o-mini',
};

/**
* Default `enabledModelIds` when a create call omits them.
*
* Most providers stay on the historical "only the default model" seed. OpenCode
* Free is the exception: #2431 made the free inventory the product default for
* both bootstrap and a user-driven "保存供应商" create, so omitting the field
* must not collapse back to a one-model connection.
*
* Returns `undefined` when create should keep the generic single-default rule.
* An explicit empty or partial list from the caller is still honored — this
* only fills a missing selection.
*/
export function defaultEnabledModelIdsWhenOmitted(
providerType: ProviderType,
): readonly string[] | undefined {
if (providerType === 'opencode-free') return OPENCODE_FREE_DEFAULT_ENABLED_MODELS;
return undefined;
}

/**
* Resolve the bootstrap connection seeds for a fresh install.
*
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,7 @@ export {
OPENCODE_FREE_DEFAULT_ENABLED_MODELS,
OPENCODE_FREE_DEFAULT_MODEL,
OPENCODE_FREE_LEGACY_DEFAULT_MODEL,
defaultEnabledModelIdsWhenOmitted,
resolveBootstrapConnections,
resolveOpenCodeFreeBootstrapMigration,
} from './bootstrap-connections.js';
Expand Down
34 changes: 34 additions & 0 deletions packages/storage/src/__tests__/connection-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,40 @@ describe('FileConnectionStore', () => {
});
});

// Onboarding "保存供应商" (and any other create path that only states a
// defaultModel) must land the same free inventory bootstrap seeds — not a
// one-model connection that then fails the first-run e2e contract.
test('creates OpenCode Free with the default free inventory when enabled models are omitted', async () => {
await withConnectionStore(async (store) => {
const created = await store.create({
slug: 'opencode-free',
name: 'OpenCode Free',
providerType: 'opencode-free',
defaultModel: 'nemotron-3-ultra-free',
});

assert.deepEqual(created.enabledModelIds, [
'nemotron-3-ultra-free',
'mimo-v2.5-free',
'deepseek-v4-flash-free',
]);
});
});

test('still honors an explicit subset when creating OpenCode Free', async () => {
await withConnectionStore(async (store) => {
const created = await store.create({
slug: 'opencode-free',
name: 'OpenCode Free',
providerType: 'opencode-free',
defaultModel: 'nemotron-3-ultra-free',
enabledModelIds: ['nemotron-3-ultra-free'],
});

assert.deepEqual(created.enabledModelIds, ['nemotron-3-ultra-free']);
});
});

test('migrates a legacy connection to only its default model enabled', async () => {
await withConnectionStore(async (store, dir) => {
await writeFile(
Expand Down
5 changes: 4 additions & 1 deletion packages/storage/src/connection-store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { defaultEnabledModelIdsWhenOmitted } from '@maka/core';
import {
PROVIDER_DEFAULTS,
connectionEnabledModelIds,
Expand Down Expand Up @@ -83,7 +84,9 @@ class FileConnectionStore implements ConnectionStore {
enabled: true,
enabledModelIds: connectionEnabledModelIds({
defaultModel,
enabledModelIds: input.enabledModelIds,
// Only fill a missing selection. Explicit [] / subset stays as stated.
enabledModelIds:
input.enabledModelIds ?? defaultEnabledModelIdsWhenOmitted(input.providerType),
}),
createdAt: now,
updatedAt: now,
Expand Down
Loading