fix(opencode): use catalog models in selector
This commit is contained in:
parent
147af0e0e5
commit
9289afd01e
4 changed files with 180 additions and 5 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue