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;
|
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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue