fix(opencode): honor free model metadata
This commit is contained in:
parent
e3ff8e1df5
commit
147af0e0e5
4 changed files with 143 additions and 10 deletions
|
|
@ -53,7 +53,11 @@ function getCatalogBadgeLabel(
|
|||
const catalogItem = providerStatus?.modelCatalog?.models.find(
|
||||
(item) => item.launchModel === model || item.id === model
|
||||
);
|
||||
return catalogItem?.badgeLabel?.trim() || null;
|
||||
const badgeLabel = catalogItem?.badgeLabel?.trim();
|
||||
if (badgeLabel) {
|
||||
return badgeLabel;
|
||||
}
|
||||
return catalogItem?.metadata?.free === true ? 'Free' : null;
|
||||
}
|
||||
|
||||
function normalizeBadgeText(value: string): string {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,7 @@ import {
|
|||
getOpenCodeQualifiedModelSourceLabel,
|
||||
parseOpenCodeQualifiedModelRef,
|
||||
} from '@shared/utils/opencodeModelRef';
|
||||
import {
|
||||
filterVisibleProviderRuntimeModels,
|
||||
GPT_5_1_CODEX_MINI_UI_DISABLED_MODEL,
|
||||
GPT_5_2_CODEX_UI_DISABLED_MODEL,
|
||||
GPT_5_3_CODEX_SPARK_UI_DISABLED_MODEL,
|
||||
} from '@shared/utils/providerModelVisibility';
|
||||
import { filterVisibleProviderRuntimeModels } from '@shared/utils/providerModelVisibility';
|
||||
|
||||
import type { CliProviderId, CliProviderStatus, TeamProviderId } from '@shared/types';
|
||||
|
||||
|
|
@ -420,6 +415,10 @@ function isFreeOpenCodeModelForOrdering(
|
|||
}
|
||||
|
||||
const runtimeModel = getRuntimeCatalogModel(providerId, model, providerStatus);
|
||||
if (runtimeModel?.metadata?.free === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const badgeLabel = runtimeModel?.badgeLabel?.trim().toLowerCase();
|
||||
if (badgeLabel) {
|
||||
return badgeLabel === 'free';
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React, { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
function render(element: React.ReactElement): HTMLDivElement {
|
||||
const host = document.createElement('div');
|
||||
|
|
@ -171,6 +171,54 @@ describe('ProviderModelBadges', () => {
|
|||
expect(host.textContent?.match(/Free/g)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders OpenCode free badges from metadata when badgeLabel is absent', () => {
|
||||
const host = render(
|
||||
<ProviderModelBadges
|
||||
providerId="opencode"
|
||||
models={['openrouter/openai/gpt-oss-20b']}
|
||||
providerStatus={{
|
||||
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: 'openrouter/openai/gpt-oss-20b',
|
||||
launchModel: 'openrouter/openai/gpt-oss-20b',
|
||||
displayName: 'openrouter/openai/gpt-oss-20b',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text'],
|
||||
supportsPersonality: true,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'app-server',
|
||||
badgeLabel: null,
|
||||
metadata: { free: true },
|
||||
},
|
||||
],
|
||||
diagnostics: {
|
||||
configReadState: 'ready',
|
||||
appServerState: 'healthy',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(host.textContent).toContain('gpt-oss');
|
||||
expect(host.textContent).toContain('Free');
|
||||
});
|
||||
|
||||
it('does not duplicate a catalog badge that matches the displayed model label', () => {
|
||||
const host = render(
|
||||
<ProviderModelBadges
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
getVisibleTeamProviderModels,
|
||||
isAnthropicOneMillionContextTeamModel,
|
||||
isAnthropicSonnetOneMillionContextTeamModel,
|
||||
isAnthropicSonnetTeamModel,
|
||||
} from '@renderer/utils/teamModelCatalog';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('teamModelCatalog', () => {
|
||||
it('filters UI-disabled Codex models from provider badge lists', () => {
|
||||
|
|
@ -139,6 +138,89 @@ describe('teamModelCatalog', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('orders OpenCode free models by metadata when badge labels are absent', () => {
|
||||
expect(
|
||||
getVisibleTeamProviderModels(
|
||||
'opencode',
|
||||
[
|
||||
'openai/gpt-5.4',
|
||||
'opencode/big-pickle',
|
||||
'openrouter/openai/gpt-oss-20b',
|
||||
],
|
||||
{
|
||||
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,
|
||||
metadata: { free: false },
|
||||
},
|
||||
{
|
||||
id: 'openrouter/openai/gpt-oss-20b',
|
||||
launchModel: 'openrouter/openai/gpt-oss-20b',
|
||||
displayName: 'openrouter/openai/gpt-oss-20b',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text'],
|
||||
supportsPersonality: true,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'app-server',
|
||||
badgeLabel: null,
|
||||
metadata: { free: true },
|
||||
},
|
||||
{
|
||||
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: null,
|
||||
metadata: { free: true },
|
||||
},
|
||||
],
|
||||
diagnostics: {
|
||||
configReadState: 'ready',
|
||||
appServerState: 'healthy',
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
).toEqual([
|
||||
'opencode/big-pickle',
|
||||
'openrouter/openai/gpt-oss-20b',
|
||||
'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