fix(opencode): hydrate summary model catalog
This commit is contained in:
parent
c3b6d2dea8
commit
d7f82e54d1
4 changed files with 191 additions and 19 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue