fix(opencode): hydrate summary model catalog

This commit is contained in:
777genius 2026-05-20 15:15:42 +03:00
parent c3b6d2dea8
commit d7f82e54d1
4 changed files with 191 additions and 19 deletions

View file

@ -120,21 +120,13 @@ export function isOpenCodeCatalogHydrating(
return false;
}
if (
provider.modelCatalogRefreshState === 'ready' ||
provider.modelCatalogRefreshState === 'error'
) {
if (provider.modelCatalogRefreshState === 'error') {
return false;
}
const hasOnlySummaryFallback =
provider.models.length === 0 ||
provider.models.every((model) => model.trim() === 'opencode/big-pickle');
return (
hasOnlySummaryFallback &&
(provider.modelCatalogRefreshState === 'loading' ||
provider.runtimeCapabilities?.modelCatalog?.dynamic === true)
provider.modelCatalogRefreshState === 'loading' ||
provider.runtimeCapabilities?.modelCatalog?.dynamic === true
);
}
@ -142,7 +134,7 @@ export function isConnectionManagedRuntimeProvider(provider: CliProviderStatus):
return provider.providerId === 'codex';
}
function getCodexCurrentRuntimeLabel(provider: CliProviderStatus): string {
function getCodexCurrentRuntimeLabel(): string {
return CODEX_NATIVE_LABEL;
}
@ -213,7 +205,7 @@ export function getProviderCurrentRuntimeSummary(provider: CliProviderStatus): s
}
const prefix = provider.authenticated ? 'Current runtime' : 'Selected runtime';
return `${prefix}: ${getCodexCurrentRuntimeLabel(provider)}`;
return `${prefix}: ${getCodexCurrentRuntimeLabel()}`;
}
export function formatProviderStatusText(provider: CliProviderStatus): string {

View file

@ -93,6 +93,18 @@ function isModelOnlyFallbackProviderStatus(provider: CliProviderStatus | undefin
);
}
function isOpenCodeSummaryOnlyCatalogStatus(provider: CliProviderStatus | undefined): boolean {
if (provider?.providerId !== 'opencode') {
return false;
}
if (provider.modelCatalog?.providerId === 'opencode' && provider.modelCatalog.models.length > 0) {
return false;
}
return provider.runtimeCapabilities?.modelCatalog?.dynamic === true;
}
function isHydratedMultimodelProviderStatus(provider: CliProviderStatus | undefined): boolean {
if (!provider) {
return false;
@ -102,6 +114,10 @@ function isHydratedMultimodelProviderStatus(provider: CliProviderStatus | undefi
return false;
}
if (isOpenCodeSummaryOnlyCatalogStatus(provider)) {
return false;
}
return !(
provider.supported === false &&
provider.authenticated === false &&
@ -131,7 +147,11 @@ function getProviderStatus(
}
function hasOpenCodeModels(provider: CliProviderStatus | undefined): boolean {
return provider?.providerId === 'opencode' && provider.models.length > 0;
return (
provider?.providerId === 'opencode' &&
provider.models.length > 0 &&
!isOpenCodeSummaryOnlyCatalogStatus(provider)
);
}
function hasCodexRuntimeReady(provider: CliProviderStatus | undefined): boolean {

View file

@ -1,16 +1,15 @@
import { describe, expect, it } from 'vitest';
import {
formatProviderStatusText,
getProviderConnectionModeSummary,
getProviderCredentialSummary,
getProviderCurrentRuntimeSummary,
isProviderInventoryOnlyFallback,
isOpenCodeCatalogHydrating,
isConnectionManagedRuntimeProvider,
isOpenCodeCatalogHydrating,
isProviderInventoryOnlyFallback,
shouldShowProviderConnectAction,
} from '@renderer/components/runtime/providerConnectionUi';
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
import { describe, expect, it } from 'vitest';
import type { CliProviderStatus } from '@shared/types';
@ -259,12 +258,18 @@ describe('providerConnectionUi', () => {
...provider,
modelCatalogRefreshState: 'ready',
})
).toBe(false);
).toBe(true);
expect(
isOpenCodeCatalogHydrating({
...provider,
models: ['opencode/big-pickle', 'openrouter/qwen/qwen3-coder-plus'],
})
).toBe(true);
expect(
isOpenCodeCatalogHydrating({
...provider,
modelCatalogRefreshState: 'error',
})
).toBe(false);
});

View file

@ -237,6 +237,28 @@ describe('cliInstallerSlice', () => {
expect(getModelOnlyFallbackProviderIds(status)).toEqual(['opencode']);
});
it('classifies OpenCode summary-only model lists as incomplete until catalog hydration', () => {
const status = createMultimodelStatus([
createMultimodelProvider({
providerId: 'opencode',
displayName: 'OpenCode',
authenticated: true,
authMethod: 'opencode_managed',
models: ['opencode/big-pickle'],
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
runtimeCapabilities: {
modelCatalog: {
dynamic: true,
source: 'app-server',
},
},
}),
]);
expect(getIncompleteMultimodelProviderIds(status)).toEqual(['opencode']);
expect(getModelOnlyFallbackProviderIds(status)).toEqual([]);
});
it('keeps connection-enriched checking placeholders incomplete until provider hydration finishes', () => {
const status = createMultimodelStatus([
createMultimodelProvider({
@ -1002,6 +1024,139 @@ describe('cliInstallerSlice', () => {
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
});
});
it('refreshes OpenCode when bootstrap metadata has summary-only big-pickle models', async () => {
const mockStatus = createMultimodelStatus([
createMultimodelProvider({
providerId: 'anthropic',
displayName: 'Anthropic',
authenticated: true,
authMethod: 'oauth_token',
models: ['claude-sonnet-4-5'],
backend: { kind: 'anthropic', label: 'Anthropic' },
}),
createMultimodelProvider({
providerId: 'codex',
displayName: 'Codex',
authenticated: true,
authMethod: 'chatgpt',
models: ['gpt-5.4'],
backend: { kind: 'codex-native', label: 'Codex' },
}),
createMultimodelProvider({
providerId: 'opencode',
displayName: 'OpenCode',
authenticated: true,
authMethod: 'opencode_managed',
models: ['opencode/big-pickle'],
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
runtimeCapabilities: {
modelCatalog: {
dynamic: true,
source: 'app-server',
},
},
}),
]);
vi.mocked(api.cliInstaller.getStatus).mockResolvedValue(mockStatus);
vi.mocked(api.cliInstaller.getProviderStatus).mockImplementation((providerId) => {
if (providerId === 'opencode') {
return Promise.resolve(
createMultimodelProvider({
providerId: 'opencode',
displayName: 'OpenCode',
authenticated: true,
authMethod: 'opencode_managed',
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',
},
{
id: 'openrouter/openai/gpt-oss-20b:free',
launchModel: 'openrouter/openai/gpt-oss-20b:free',
displayName: 'openrouter/openai/gpt-oss-20b:free',
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: true,
isDefault: false,
upgrade: false,
source: 'app-server',
badgeLabel: 'Free',
},
],
diagnostics: {
configReadState: 'ready',
appServerState: 'healthy',
},
},
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
runtimeCapabilities: {
modelCatalog: {
dynamic: true,
source: 'app-server',
},
},
})
);
}
return Promise.reject(new Error(`Unexpected provider status request for ${providerId}`));
});
await useStore.getState().bootstrapCliStatus({ multimodelEnabled: true });
expect(api.cliInstaller.getProviderStatus).toHaveBeenCalledTimes(1);
expect(api.cliInstaller.getProviderStatus).toHaveBeenCalledWith('opencode');
const opencode = useStore
.getState()
.cliStatus?.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).toHaveLength(3);
});
});
describe('installCli', () => {