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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (providerId !== 'codex' || providerStatus?.modelCatalog?.providerId !== 'codex') {
|
if (
|
||||||
|
(providerId !== 'codex' && providerId !== 'opencode') ||
|
||||||
|
providerStatus?.modelCatalog?.providerId !== providerId
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const models = providerStatus.modelCatalog.models
|
const models = providerStatus.modelCatalog.models
|
||||||
.filter((model) => !model.hidden)
|
.filter((model) => !model.hidden)
|
||||||
.map((model) => model.launchModel.trim())
|
.map((model) => model.launchModel.trim() || model.id.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
return models.length > 0 ? models : null;
|
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(
|
function getSupplementalVisibleModels(
|
||||||
providerId: SupportedProviderId,
|
providerId: SupportedProviderId,
|
||||||
models: readonly string[]
|
models: readonly string[]
|
||||||
|
|
@ -510,11 +525,16 @@ export function getVisibleTeamProviderModels(
|
||||||
models: readonly string[],
|
models: readonly string[],
|
||||||
providerStatus?: RuntimeAwareProviderStatus | null
|
providerStatus?: RuntimeAwareProviderStatus | null
|
||||||
): string[] {
|
): string[] {
|
||||||
|
const sourceModels =
|
||||||
|
providerId === 'opencode' && models.length === 0
|
||||||
|
? (getRuntimeCatalogLaunchModels(providerId, providerStatus) ?? models)
|
||||||
|
: models;
|
||||||
|
|
||||||
return sortTeamProviderModels(
|
return sortTeamProviderModels(
|
||||||
providerId,
|
providerId,
|
||||||
filterVisibleProviderRuntimeModels(
|
filterVisibleProviderRuntimeModels(
|
||||||
providerId,
|
providerId,
|
||||||
getSupplementalVisibleModels(providerId, models)
|
getSupplementalVisibleModels(providerId, sourceModels)
|
||||||
),
|
),
|
||||||
providerStatus
|
providerStatus
|
||||||
).filter((model) => !isRuntimeHiddenTeamModel(providerId, model, providerStatus));
|
).filter((model) => !isRuntimeHiddenTeamModel(providerId, model, providerStatus));
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getAvailableTeamProviderModelOptions,
|
getAvailableTeamProviderModelOptions,
|
||||||
getAvailableTeamProviderModels,
|
getAvailableTeamProviderModels,
|
||||||
|
|
@ -10,6 +8,7 @@ import {
|
||||||
normalizeTeamModelForUi,
|
normalizeTeamModelForUi,
|
||||||
type TeamModelRuntimeProviderStatus,
|
type TeamModelRuntimeProviderStatus,
|
||||||
} from '@renderer/utils/teamModelAvailability';
|
} from '@renderer/utils/teamModelAvailability';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
function createCodexProviderStatus(
|
function createCodexProviderStatus(
|
||||||
models: string[],
|
models: string[],
|
||||||
|
|
@ -246,6 +245,91 @@ describe('teamModelAvailability', () => {
|
||||||
).toBe('openrouter/moonshotai/kimi-k2');
|
).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', () => {
|
it('reports OpenCode openai routes unavailable when OpenAI auth is invalid', () => {
|
||||||
const providerStatus = createOpenCodeProviderStatus(['openai/gpt-5.4', 'opencode/big-pickle'], {
|
const providerStatus = createOpenCodeProviderStatus(['openai/gpt-5.4', 'opencode/big-pickle'], {
|
||||||
statusMessage: 'OpenAI token invalid',
|
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', () => {
|
it('detects Sonnet aliases with or without 1M suffix', () => {
|
||||||
expect(isAnthropicSonnetTeamModel('sonnet')).toBe(true);
|
expect(isAnthropicSonnetTeamModel('sonnet')).toBe(true);
|
||||||
expect(isAnthropicSonnetTeamModel('sonnet[1m]')).toBe(true);
|
expect(isAnthropicSonnetTeamModel('sonnet[1m]')).toBe(true);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue