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(
|
const catalogItem = providerStatus?.modelCatalog?.models.find(
|
||||||
(item) => item.launchModel === model || item.id === model
|
(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 {
|
function normalizeBadgeText(value: string): string {
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,7 @@ import {
|
||||||
getOpenCodeQualifiedModelSourceLabel,
|
getOpenCodeQualifiedModelSourceLabel,
|
||||||
parseOpenCodeQualifiedModelRef,
|
parseOpenCodeQualifiedModelRef,
|
||||||
} from '@shared/utils/opencodeModelRef';
|
} from '@shared/utils/opencodeModelRef';
|
||||||
import {
|
import { filterVisibleProviderRuntimeModels } from '@shared/utils/providerModelVisibility';
|
||||||
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 type { CliProviderId, CliProviderStatus, TeamProviderId } from '@shared/types';
|
import type { CliProviderId, CliProviderStatus, TeamProviderId } from '@shared/types';
|
||||||
|
|
||||||
|
|
@ -420,6 +415,10 @@ function isFreeOpenCodeModelForOrdering(
|
||||||
}
|
}
|
||||||
|
|
||||||
const runtimeModel = getRuntimeCatalogModel(providerId, model, providerStatus);
|
const runtimeModel = getRuntimeCatalogModel(providerId, model, providerStatus);
|
||||||
|
if (runtimeModel?.metadata?.free === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const badgeLabel = runtimeModel?.badgeLabel?.trim().toLowerCase();
|
const badgeLabel = runtimeModel?.badgeLabel?.trim().toLowerCase();
|
||||||
if (badgeLabel) {
|
if (badgeLabel) {
|
||||||
return badgeLabel === 'free';
|
return badgeLabel === 'free';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import React, { act } from 'react';
|
import React, { act } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
|
|
||||||
import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges';
|
import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
function render(element: React.ReactElement): HTMLDivElement {
|
function render(element: React.ReactElement): HTMLDivElement {
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
|
|
@ -171,6 +171,54 @@ describe('ProviderModelBadges', () => {
|
||||||
expect(host.textContent?.match(/Free/g)).toHaveLength(1);
|
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', () => {
|
it('does not duplicate a catalog badge that matches the displayed model label', () => {
|
||||||
const host = render(
|
const host = render(
|
||||||
<ProviderModelBadges
|
<ProviderModelBadges
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getVisibleTeamProviderModels,
|
getVisibleTeamProviderModels,
|
||||||
isAnthropicOneMillionContextTeamModel,
|
isAnthropicOneMillionContextTeamModel,
|
||||||
isAnthropicSonnetOneMillionContextTeamModel,
|
isAnthropicSonnetOneMillionContextTeamModel,
|
||||||
isAnthropicSonnetTeamModel,
|
isAnthropicSonnetTeamModel,
|
||||||
} from '@renderer/utils/teamModelCatalog';
|
} from '@renderer/utils/teamModelCatalog';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('teamModelCatalog', () => {
|
describe('teamModelCatalog', () => {
|
||||||
it('filters UI-disabled Codex models from provider badge lists', () => {
|
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', () => {
|
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