fix(opencode): preserve hydrated model lists
This commit is contained in:
parent
16c1e4b47d
commit
e3ff8e1df5
4 changed files with 258 additions and 2 deletions
|
|
@ -224,10 +224,16 @@ function mergeProviderStatusCatalogCache(
|
|||
): CliProviderStatus {
|
||||
const modelCatalog = incomingProvider.modelCatalog ?? currentProvider.modelCatalog ?? null;
|
||||
const incomingRefreshState = incomingProvider.modelCatalogRefreshState ?? null;
|
||||
const shouldPreserveCurrentModels =
|
||||
incomingProvider.models.length === 0 ||
|
||||
(incomingProvider.providerId === 'opencode' &&
|
||||
incomingProvider.modelCatalog == null &&
|
||||
incomingProvider.runtimeCapabilities?.modelCatalog?.dynamic === true &&
|
||||
currentProvider.models.length > incomingProvider.models.length);
|
||||
|
||||
return {
|
||||
...incomingProvider,
|
||||
models: incomingProvider.models.length > 0 ? incomingProvider.models : currentProvider.models,
|
||||
models: shouldPreserveCurrentModels ? currentProvider.models : incomingProvider.models,
|
||||
modelCatalog,
|
||||
modelCatalogRefreshState:
|
||||
modelCatalog && incomingRefreshState !== 'error'
|
||||
|
|
|
|||
|
|
@ -204,9 +204,15 @@ function mergeProviderCatalogCache(
|
|||
): CliProviderStatus {
|
||||
const modelCatalog = incomingProvider.modelCatalog ?? currentProvider.modelCatalog ?? null;
|
||||
const incomingRefreshState = incomingProvider.modelCatalogRefreshState ?? null;
|
||||
const shouldPreserveCurrentModels =
|
||||
incomingProvider.models.length === 0 ||
|
||||
(incomingProvider.providerId === 'opencode' &&
|
||||
incomingProvider.modelCatalog == null &&
|
||||
incomingProvider.runtimeCapabilities?.modelCatalog?.dynamic === true &&
|
||||
currentProvider.models.length > incomingProvider.models.length);
|
||||
return {
|
||||
...incomingProvider,
|
||||
models: incomingProvider.models.length > 0 ? incomingProvider.models : currentProvider.models,
|
||||
models: shouldPreserveCurrentModels ? currentProvider.models : incomingProvider.models,
|
||||
modelCatalog,
|
||||
modelCatalogRefreshState:
|
||||
modelCatalog && incomingRefreshState !== 'error'
|
||||
|
|
|
|||
|
|
@ -712,6 +712,145 @@ describe('CliInstallerService', () => {
|
|||
expect(verifiedProvider?.modelVerificationState).toBe('idle');
|
||||
expect(verifiedProvider?.modelAvailability).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not shrink cached OpenCode models when a provider refresh returns summary-only models', async () => {
|
||||
allowConsoleLogs();
|
||||
vi.mocked(getConfiguredCliFlavor).mockReturnValue('agent_teams_orchestrator');
|
||||
vi.mocked(getCliFlavorUiOptions).mockReturnValue({
|
||||
displayName: 'agent_teams_orchestrator',
|
||||
supportsSelfUpdate: false,
|
||||
showVersionDetails: false,
|
||||
showBinaryPath: false,
|
||||
});
|
||||
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/usr/local/bin/claude');
|
||||
vi.mocked(execCli).mockImplementation(async (_binaryPath, args) => {
|
||||
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
|
||||
if (normalizedArgs === '--version') {
|
||||
return { stdout: '2.3.4', stderr: '' };
|
||||
}
|
||||
throw new Error(`Unexpected execCli call: ${normalizedArgs}`);
|
||||
});
|
||||
|
||||
vi.spyOn(ClaudeMultimodelBridgeService.prototype, 'getProviderStatuses').mockResolvedValue([
|
||||
{
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
supported: true,
|
||||
authenticated: true,
|
||||
authMethod: 'opencode_managed',
|
||||
verificationState: 'verified',
|
||||
modelVerificationState: 'idle',
|
||||
statusMessage: null,
|
||||
detailMessage: null,
|
||||
models: [
|
||||
'opencode/big-pickle',
|
||||
'openai/gpt-5.4',
|
||||
'openrouter/openai/gpt-oss-20b:free',
|
||||
],
|
||||
modelCatalog: {
|
||||
schemaVersion: 1,
|
||||
providerId: 'opencode',
|
||||
source: 'app-server',
|
||||
status: 'ready',
|
||||
fetchedAt: '2026-05-20T00:00:00.000Z',
|
||||
staleAt: '2026-05-20T00:10:00.000Z',
|
||||
defaultModelId: 'opencode/big-pickle',
|
||||
defaultLaunchModel: 'opencode/big-pickle',
|
||||
models: [
|
||||
{
|
||||
id: 'opencode/big-pickle',
|
||||
launchModel: 'opencode/big-pickle',
|
||||
displayName: 'opencode/big-pickle',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text'],
|
||||
supportsPersonality: true,
|
||||
isDefault: true,
|
||||
upgrade: false,
|
||||
source: 'app-server',
|
||||
badgeLabel: 'Free',
|
||||
},
|
||||
{
|
||||
id: 'openai/gpt-5.4',
|
||||
launchModel: 'openai/gpt-5.4',
|
||||
displayName: 'openai/gpt-5.4',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text'],
|
||||
supportsPersonality: true,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'app-server',
|
||||
},
|
||||
],
|
||||
diagnostics: {
|
||||
configReadState: 'ready',
|
||||
appServerState: 'healthy',
|
||||
},
|
||||
},
|
||||
modelCatalogRefreshState: 'ready',
|
||||
modelAvailability: [],
|
||||
runtimeCapabilities: { modelCatalog: { dynamic: true, source: 'app-server' } },
|
||||
canLoginFromUi: false,
|
||||
capabilities: { teamLaunch: true, oneShot: false, extensions: undefined as never },
|
||||
selectedBackendId: null,
|
||||
resolvedBackendId: null,
|
||||
availableBackends: [],
|
||||
externalRuntimeDiagnostics: [],
|
||||
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
||||
connection: null,
|
||||
},
|
||||
] as never);
|
||||
|
||||
vi.spyOn(ClaudeMultimodelBridgeService.prototype, 'getProviderStatus').mockResolvedValue({
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
supported: true,
|
||||
authenticated: true,
|
||||
authMethod: 'opencode_managed',
|
||||
verificationState: 'verified',
|
||||
modelVerificationState: 'idle',
|
||||
statusMessage: null,
|
||||
detailMessage: null,
|
||||
models: ['opencode/big-pickle'],
|
||||
modelCatalog: null,
|
||||
modelCatalogRefreshState: 'loading',
|
||||
modelAvailability: [],
|
||||
runtimeCapabilities: { modelCatalog: { dynamic: true, source: 'app-server' } },
|
||||
canLoginFromUi: false,
|
||||
capabilities: { teamLaunch: true, oneShot: false, extensions: undefined as never },
|
||||
selectedBackendId: null,
|
||||
resolvedBackendId: null,
|
||||
availableBackends: [],
|
||||
externalRuntimeDiagnostics: [],
|
||||
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
||||
connection: null,
|
||||
} as never);
|
||||
|
||||
await service.getStatus();
|
||||
await service.getProviderStatus('opencode');
|
||||
|
||||
const latestSnapshot = (
|
||||
service as unknown as {
|
||||
latestStatusSnapshot?: Awaited<ReturnType<CliInstallerService['getStatus']>>;
|
||||
}
|
||||
).latestStatusSnapshot;
|
||||
const opencode = latestSnapshot?.providers.find(
|
||||
(provider) => provider.providerId === 'opencode'
|
||||
);
|
||||
expect(opencode?.models).toEqual([
|
||||
'opencode/big-pickle',
|
||||
'openai/gpt-5.4',
|
||||
'openrouter/openai/gpt-oss-20b:free',
|
||||
]);
|
||||
expect(opencode?.modelCatalog?.models.map((model) => model.id)).toEqual([
|
||||
'opencode/big-pickle',
|
||||
'openai/gpt-5.4',
|
||||
]);
|
||||
expect(opencode?.modelCatalogRefreshState).toBe('ready');
|
||||
});
|
||||
});
|
||||
|
||||
describe('install mutex', () => {
|
||||
|
|
|
|||
|
|
@ -1486,6 +1486,111 @@ describe('cliInstallerSlice', () => {
|
|||
expect(provider?.modelCatalog?.defaultModelId).toBe('gpt-5.4');
|
||||
});
|
||||
|
||||
it('keeps cached OpenCode model list when summary refresh only reports big-pickle', async () => {
|
||||
const currentProvider = createMultimodelProvider({
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
authenticated: true,
|
||||
authMethod: 'opencode_managed',
|
||||
statusMessage: null,
|
||||
models: [
|
||||
'opencode/big-pickle',
|
||||
'openai/gpt-5.4',
|
||||
'openrouter/openai/gpt-oss-20b:free',
|
||||
],
|
||||
modelCatalogRefreshState: 'ready',
|
||||
modelCatalog: {
|
||||
schemaVersion: 1,
|
||||
providerId: 'opencode',
|
||||
source: 'app-server',
|
||||
status: 'ready',
|
||||
fetchedAt: '2026-05-20T00:00:00.000Z',
|
||||
staleAt: '2026-05-20T00:10:00.000Z',
|
||||
defaultModelId: 'opencode/big-pickle',
|
||||
defaultLaunchModel: 'opencode/big-pickle',
|
||||
models: [
|
||||
{
|
||||
id: 'opencode/big-pickle',
|
||||
launchModel: 'opencode/big-pickle',
|
||||
displayName: 'opencode/big-pickle',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text'],
|
||||
supportsPersonality: true,
|
||||
isDefault: true,
|
||||
upgrade: false,
|
||||
source: 'app-server',
|
||||
badgeLabel: 'Free',
|
||||
},
|
||||
{
|
||||
id: 'openai/gpt-5.4',
|
||||
launchModel: 'openai/gpt-5.4',
|
||||
displayName: 'openai/gpt-5.4',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text'],
|
||||
supportsPersonality: true,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'app-server',
|
||||
},
|
||||
],
|
||||
diagnostics: {
|
||||
configReadState: 'ready',
|
||||
appServerState: 'healthy',
|
||||
},
|
||||
},
|
||||
runtimeCapabilities: {
|
||||
modelCatalog: {
|
||||
dynamic: true,
|
||||
source: 'app-server',
|
||||
},
|
||||
},
|
||||
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
||||
});
|
||||
|
||||
useStore.setState({
|
||||
cliStatus: createMultimodelStatus([currentProvider]),
|
||||
});
|
||||
vi.mocked(api.cliInstaller.getProviderStatus).mockResolvedValue(
|
||||
createMultimodelProvider({
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
authenticated: true,
|
||||
authMethod: 'opencode_managed',
|
||||
statusMessage: null,
|
||||
models: ['opencode/big-pickle'],
|
||||
modelCatalog: null,
|
||||
modelCatalogRefreshState: 'loading',
|
||||
runtimeCapabilities: {
|
||||
modelCatalog: {
|
||||
dynamic: true,
|
||||
source: 'app-server',
|
||||
},
|
||||
},
|
||||
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
||||
})
|
||||
);
|
||||
|
||||
await useStore.getState().fetchCliProviderStatus('opencode');
|
||||
|
||||
const provider = useStore
|
||||
.getState()
|
||||
.cliStatus?.providers.find((candidate) => candidate.providerId === 'opencode');
|
||||
expect(provider?.models).toEqual([
|
||||
'opencode/big-pickle',
|
||||
'openai/gpt-5.4',
|
||||
'openrouter/openai/gpt-oss-20b:free',
|
||||
]);
|
||||
expect(provider?.modelCatalog?.models.map((model) => model.id)).toEqual([
|
||||
'opencode/big-pickle',
|
||||
'openai/gpt-5.4',
|
||||
]);
|
||||
expect(provider?.modelCatalogRefreshState).toBe('ready');
|
||||
});
|
||||
|
||||
it('keeps OpenCode refresh status-only even when model verification is requested', async () => {
|
||||
const nextProvider = createMultimodelProvider({
|
||||
providerId: 'opencode',
|
||||
|
|
|
|||
Loading…
Reference in a new issue