fix(opencode): use catalog models in selector

This commit is contained in:
777genius 2026-05-20 17:20:55 +03:00
parent 147af0e0e5
commit 9289afd01e
4 changed files with 180 additions and 5 deletions

View file

@ -235,13 +235,16 @@ function getRuntimeCatalogModels(
return null;
}
if (providerId !== 'codex' || providerStatus?.modelCatalog?.providerId !== 'codex') {
if (
(providerId !== 'codex' && providerId !== 'opencode') ||
providerStatus?.modelCatalog?.providerId !== providerId
) {
return null;
}
const models = providerStatus.modelCatalog.models
.filter((model) => !model.hidden)
.map((model) => model.launchModel.trim())
.map((model) => model.launchModel.trim() || model.id.trim())
.filter(Boolean);
return models.length > 0 ? models : null;
}

View file

@ -494,6 +494,21 @@ function isRuntimeHiddenTeamModel(
);
}
function getRuntimeCatalogLaunchModels(
providerId: SupportedProviderId,
providerStatus?: RuntimeAwareProviderStatus | null
): string[] | null {
if (providerStatus?.modelCatalog?.providerId !== providerId) {
return null;
}
const models = providerStatus.modelCatalog.models
.filter((model) => !model.hidden)
.map((model) => model.launchModel.trim() || model.id.trim())
.filter(Boolean);
return models.length > 0 ? models : null;
}
function getSupplementalVisibleModels(
providerId: SupportedProviderId,
models: readonly string[]
@ -510,11 +525,16 @@ export function getVisibleTeamProviderModels(
models: readonly string[],
providerStatus?: RuntimeAwareProviderStatus | null
): string[] {
const sourceModels =
providerId === 'opencode' && models.length === 0
? (getRuntimeCatalogLaunchModels(providerId, providerStatus) ?? models)
: models;
return sortTeamProviderModels(
providerId,
filterVisibleProviderRuntimeModels(
providerId,
getSupplementalVisibleModels(providerId, models)
getSupplementalVisibleModels(providerId, sourceModels)
),
providerStatus
).filter((model) => !isRuntimeHiddenTeamModel(providerId, model, providerStatus));

View file

@ -1,5 +1,3 @@
import { describe, expect, it } from 'vitest';
import {
getAvailableTeamProviderModelOptions,
getAvailableTeamProviderModels,
@ -10,6 +8,7 @@ import {
normalizeTeamModelForUi,
type TeamModelRuntimeProviderStatus,
} from '@renderer/utils/teamModelAvailability';
import { describe, expect, it } from 'vitest';
function createCodexProviderStatus(
models: string[],
@ -246,6 +245,91 @@ describe('teamModelAvailability', () => {
).toBe('openrouter/moonshotai/kimi-k2');
});
it('uses the OpenCode model catalog when runtime models are missing', () => {
const providerStatus = createOpenCodeProviderStatus([], {
modelCatalog: {
schemaVersion: 1,
providerId: 'opencode',
source: 'app-server',
status: 'ready',
fetchedAt: '2026-05-12T00:00:00.000Z',
staleAt: '2026-05-12T00:10:00.000Z',
defaultModelId: 'opencode/big-pickle',
defaultLaunchModel: 'opencode/big-pickle',
models: [
{
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',
badgeLabel: null,
},
{
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: 'openrouter/hidden-model',
launchModel: 'openrouter/hidden-model',
displayName: 'openrouter/hidden-model',
hidden: true,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: true,
isDefault: false,
upgrade: false,
source: 'app-server',
badgeLabel: null,
},
],
diagnostics: {
configReadState: 'ready',
appServerState: 'healthy',
},
},
});
expect(getAvailableTeamProviderModels('opencode', providerStatus)).toEqual([
'opencode/big-pickle',
'openai/gpt-5.4',
]);
expect(getAvailableTeamProviderModelOptions('opencode', providerStatus)).toEqual([
{ value: '', label: 'Default', badgeLabel: 'Default' },
{
value: 'opencode/big-pickle',
label: 'big-pickle',
badgeLabel: 'OpenCode',
availabilityStatus: 'available',
availabilityReason: null,
},
{
value: 'openai/gpt-5.4',
label: 'GPT-5.4',
badgeLabel: 'OpenAI',
availabilityStatus: 'available',
availabilityReason: null,
},
]);
});
it('reports OpenCode openai routes unavailable when OpenAI auth is invalid', () => {
const providerStatus = createOpenCodeProviderStatus(['openai/gpt-5.4', 'opencode/big-pickle'], {
statusMessage: 'OpenAI token invalid',

View file

@ -221,6 +221,74 @@ describe('teamModelCatalog', () => {
]);
});
it('uses the OpenCode model catalog when the runtime model list is empty', () => {
expect(
getVisibleTeamProviderModels('opencode', [], {
providerId: 'opencode',
authMethod: 'opencode_managed',
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
modelCatalog: {
schemaVersion: 1,
providerId: 'opencode',
source: 'app-server',
status: 'ready',
fetchedAt: '2026-05-12T00:00:00.000Z',
staleAt: '2026-05-12T00:10:00.000Z',
defaultModelId: 'opencode/big-pickle',
defaultLaunchModel: 'opencode/big-pickle',
models: [
{
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',
badgeLabel: null,
},
{
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: 'openrouter/hidden-model',
launchModel: 'openrouter/hidden-model',
displayName: 'openrouter/hidden-model',
hidden: true,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: true,
isDefault: false,
upgrade: false,
source: 'app-server',
badgeLabel: null,
},
],
diagnostics: {
configReadState: 'ready',
appServerState: 'healthy',
},
},
})
).toEqual(['opencode/big-pickle', 'openai/gpt-5.4']);
});
it('detects Sonnet aliases with or without 1M suffix', () => {
expect(isAnthropicSonnetTeamModel('sonnet')).toBe(true);
expect(isAnthropicSonnetTeamModel('sonnet[1m]')).toBe(true);