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; return false;
} }
if ( if (provider.modelCatalogRefreshState === 'error') {
provider.modelCatalogRefreshState === 'ready' ||
provider.modelCatalogRefreshState === 'error'
) {
return false; return false;
} }
const hasOnlySummaryFallback =
provider.models.length === 0 ||
provider.models.every((model) => model.trim() === 'opencode/big-pickle');
return ( return (
hasOnlySummaryFallback && provider.modelCatalogRefreshState === 'loading' ||
(provider.modelCatalogRefreshState === 'loading' || provider.runtimeCapabilities?.modelCatalog?.dynamic === true
provider.runtimeCapabilities?.modelCatalog?.dynamic === true)
); );
} }
@ -142,7 +134,7 @@ export function isConnectionManagedRuntimeProvider(provider: CliProviderStatus):
return provider.providerId === 'codex'; return provider.providerId === 'codex';
} }
function getCodexCurrentRuntimeLabel(provider: CliProviderStatus): string { function getCodexCurrentRuntimeLabel(): string {
return CODEX_NATIVE_LABEL; return CODEX_NATIVE_LABEL;
} }
@ -213,7 +205,7 @@ export function getProviderCurrentRuntimeSummary(provider: CliProviderStatus): s
} }
const prefix = provider.authenticated ? 'Current runtime' : 'Selected runtime'; const prefix = provider.authenticated ? 'Current runtime' : 'Selected runtime';
return `${prefix}: ${getCodexCurrentRuntimeLabel(provider)}`; return `${prefix}: ${getCodexCurrentRuntimeLabel()}`;
} }
export function formatProviderStatusText(provider: CliProviderStatus): string { 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 { function isHydratedMultimodelProviderStatus(provider: CliProviderStatus | undefined): boolean {
if (!provider) { if (!provider) {
return false; return false;
@ -102,6 +114,10 @@ function isHydratedMultimodelProviderStatus(provider: CliProviderStatus | undefi
return false; return false;
} }
if (isOpenCodeSummaryOnlyCatalogStatus(provider)) {
return false;
}
return !( return !(
provider.supported === false && provider.supported === false &&
provider.authenticated === false && provider.authenticated === false &&
@ -131,7 +147,11 @@ function getProviderStatus(
} }
function hasOpenCodeModels(provider: CliProviderStatus | undefined): boolean { 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 { function hasCodexRuntimeReady(provider: CliProviderStatus | undefined): boolean {

View file

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

View file

@ -237,6 +237,28 @@ describe('cliInstallerSlice', () => {
expect(getModelOnlyFallbackProviderIds(status)).toEqual(['opencode']); 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', () => { it('keeps connection-enriched checking placeholders incomplete until provider hydration finishes', () => {
const status = createMultimodelStatus([ const status = createMultimodelStatus([
createMultimodelProvider({ createMultimodelProvider({
@ -1002,6 +1024,139 @@ describe('cliInstallerSlice', () => {
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' }, 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', () => { describe('installCli', () => {