From 9289afd01ee7d6a4dea071d3ff0044b15c25f9a8 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 20 May 2026 17:20:55 +0300 Subject: [PATCH] fix(opencode): use catalog models in selector --- src/renderer/utils/teamModelAvailability.ts | 7 +- src/renderer/utils/teamModelCatalog.ts | 22 ++++- .../utils/teamModelAvailability.test.ts | 88 ++++++++++++++++++- test/renderer/utils/teamModelCatalog.test.ts | 68 ++++++++++++++ 4 files changed, 180 insertions(+), 5 deletions(-) diff --git a/src/renderer/utils/teamModelAvailability.ts b/src/renderer/utils/teamModelAvailability.ts index 9353f4c5..9f7f6220 100644 --- a/src/renderer/utils/teamModelAvailability.ts +++ b/src/renderer/utils/teamModelAvailability.ts @@ -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; } diff --git a/src/renderer/utils/teamModelCatalog.ts b/src/renderer/utils/teamModelCatalog.ts index daa18a03..1f34c84a 100644 --- a/src/renderer/utils/teamModelCatalog.ts +++ b/src/renderer/utils/teamModelCatalog.ts @@ -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)); diff --git a/test/renderer/utils/teamModelAvailability.test.ts b/test/renderer/utils/teamModelAvailability.test.ts index 7cc8a6c4..48548670 100644 --- a/test/renderer/utils/teamModelAvailability.test.ts +++ b/test/renderer/utils/teamModelAvailability.test.ts @@ -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', diff --git a/test/renderer/utils/teamModelCatalog.test.ts b/test/renderer/utils/teamModelCatalog.test.ts index 950c035f..ed92275d 100644 --- a/test/renderer/utils/teamModelCatalog.test.ts +++ b/test/renderer/utils/teamModelCatalog.test.ts @@ -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);