feat(runtime): filter free provider models
This commit is contained in:
parent
1b836530e7
commit
0e8ccf2905
4 changed files with 208 additions and 20 deletions
|
|
@ -1336,8 +1336,8 @@ function ModelBadges({
|
||||||
}): JSX.Element | null {
|
}): JSX.Element | null {
|
||||||
const modelRecommendation = getOpenCodeTeamModelRecommendation(model.modelId);
|
const modelRecommendation = getOpenCodeTeamModelRecommendation(model.modelId);
|
||||||
const localRoute = model.routeKind === 'configured_local';
|
const localRoute = model.routeKind === 'configured_local';
|
||||||
const builtinFreeRoute = model.routeKind === 'builtin_free';
|
|
||||||
const connectedRoute = model.routeKind === 'connected_provider';
|
const connectedRoute = model.routeKind === 'connected_provider';
|
||||||
|
const freeModel = isFreeRuntimeProviderModel(model);
|
||||||
const verified =
|
const verified =
|
||||||
model.proofState === 'verified' ||
|
model.proofState === 'verified' ||
|
||||||
model.availability === 'available' ||
|
model.availability === 'available' ||
|
||||||
|
|
@ -1351,8 +1351,7 @@ function ModelBadges({
|
||||||
const unknown = model.accessKind === 'unknown_model' || model.accessKind === 'no_model';
|
const unknown = model.accessKind === 'unknown_model' || model.accessKind === 'no_model';
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!model.free &&
|
!freeModel &&
|
||||||
!builtinFreeRoute &&
|
|
||||||
!model.default &&
|
!model.default &&
|
||||||
!usedForNewTeams &&
|
!usedForNewTeams &&
|
||||||
!modelRecommendation &&
|
!modelRecommendation &&
|
||||||
|
|
@ -1403,7 +1402,7 @@ function ModelBadges({
|
||||||
Used in team picker
|
Used in team picker
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
{model.free ? (
|
{freeModel ? (
|
||||||
<Badge className="bg-emerald-400/15 px-1.5 py-0 text-[10px] text-emerald-200">free</Badge>
|
<Badge className="bg-emerald-400/15 px-1.5 py-0 text-[10px] text-emerald-200">free</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
{localRoute ? (
|
{localRoute ? (
|
||||||
|
|
@ -1412,9 +1411,6 @@ function ModelBadges({
|
||||||
<Badge className="bg-sky-400/15 px-1.5 py-0 text-[10px] text-sky-200">configured</Badge>
|
<Badge className="bg-sky-400/15 px-1.5 py-0 text-[10px] text-sky-200">configured</Badge>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{builtinFreeRoute && !model.free ? (
|
|
||||||
<Badge className="bg-emerald-400/15 px-1.5 py-0 text-[10px] text-emerald-200">free</Badge>
|
|
||||||
) : null}
|
|
||||||
{connectedRoute ? (
|
{connectedRoute ? (
|
||||||
<Badge className="bg-emerald-400/15 px-1.5 py-0 text-[10px] text-emerald-100">
|
<Badge className="bg-emerald-400/15 px-1.5 py-0 text-[10px] text-emerald-100">
|
||||||
connected
|
connected
|
||||||
|
|
@ -1441,6 +1437,19 @@ function ModelBadges({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isFreeRuntimeProviderModel(model: RuntimeProviderModelDto): boolean {
|
||||||
|
const normalizedModelId = model.modelId.trim().toLowerCase();
|
||||||
|
return (
|
||||||
|
model.free ||
|
||||||
|
model.routeKind === 'builtin_free' ||
|
||||||
|
model.accessKind === 'builtin_free' ||
|
||||||
|
normalizedModelId === 'opencode/big-pickle' ||
|
||||||
|
normalizedModelId.includes(':free') ||
|
||||||
|
normalizedModelId.endsWith('-free') ||
|
||||||
|
normalizedModelId.endsWith('/free')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isUnknownOpenCodeModelRoute(model: RuntimeProviderModelDto): boolean {
|
function isUnknownOpenCodeModelRoute(model: RuntimeProviderModelDto): boolean {
|
||||||
return model.accessKind === 'unknown_model' || model.accessKind === 'no_model';
|
return model.accessKind === 'unknown_model' || model.accessKind === 'no_model';
|
||||||
}
|
}
|
||||||
|
|
@ -1485,7 +1494,7 @@ function getOpenCodeModelSearchText(model: RuntimeProviderModelDto): string {
|
||||||
model.proofState,
|
model.proofState,
|
||||||
model.availability,
|
model.availability,
|
||||||
model.accessReason ?? '',
|
model.accessReason ?? '',
|
||||||
model.free ? 'free' : '',
|
isFreeRuntimeProviderModel(model) ? 'free' : '',
|
||||||
model.default ? 'default' : '',
|
model.default ? 'default' : '',
|
||||||
model.requiresExecutionProof ? 'needs test needs probe' : '',
|
model.requiresExecutionProof ? 'needs test needs probe' : '',
|
||||||
recommendation?.label ?? '',
|
recommendation?.label ?? '',
|
||||||
|
|
@ -1928,10 +1937,15 @@ function ProviderModelList({
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const pickerOpen = state.modelPickerProviderId === provider.providerId;
|
const pickerOpen = state.modelPickerProviderId === provider.providerId;
|
||||||
const [recommendedOnly, setRecommendedOnly] = useState(false);
|
const [recommendedOnly, setRecommendedOnly] = useState(false);
|
||||||
|
const [freeOnly, setFreeOnly] = useState(false);
|
||||||
const hasRecommendedModels = useMemo(
|
const hasRecommendedModels = useMemo(
|
||||||
() => state.models.some((model) => isOpenCodeTeamModelRecommended(model.modelId)),
|
() => state.models.some((model) => isOpenCodeTeamModelRecommended(model.modelId)),
|
||||||
[state.models]
|
[state.models]
|
||||||
);
|
);
|
||||||
|
const hasFreeModels = useMemo(
|
||||||
|
() => state.models.some((model) => isFreeRuntimeProviderModel(model)),
|
||||||
|
[state.models]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasRecommendedModels) {
|
if (!hasRecommendedModels) {
|
||||||
|
|
@ -1939,11 +1953,18 @@ function ProviderModelList({
|
||||||
}
|
}
|
||||||
}, [hasRecommendedModels]);
|
}, [hasRecommendedModels]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasFreeModels) {
|
||||||
|
setFreeOnly(false);
|
||||||
|
}
|
||||||
|
}, [hasFreeModels]);
|
||||||
|
|
||||||
const visibleModels = useMemo(
|
const visibleModels = useMemo(
|
||||||
() =>
|
() =>
|
||||||
state.models
|
state.models
|
||||||
.map((model, index) => ({ model, index }))
|
.map((model, index) => ({ model, index }))
|
||||||
.filter(({ model }) => !recommendedOnly || isOpenCodeTeamModelRecommended(model.modelId))
|
.filter(({ model }) => !recommendedOnly || isOpenCodeTeamModelRecommended(model.modelId))
|
||||||
|
.filter(({ model }) => !freeOnly || isFreeRuntimeProviderModel(model))
|
||||||
.sort((left, right) => {
|
.sort((left, right) => {
|
||||||
const recommendationOrder = compareOpenCodeTeamModelRecommendations(
|
const recommendationOrder = compareOpenCodeTeamModelRecommendations(
|
||||||
left.model.modelId,
|
left.model.modelId,
|
||||||
|
|
@ -1952,8 +1973,15 @@ function ProviderModelList({
|
||||||
return recommendationOrder || left.index - right.index;
|
return recommendationOrder || left.index - right.index;
|
||||||
})
|
})
|
||||||
.map(({ model }) => model),
|
.map(({ model }) => model),
|
||||||
[recommendedOnly, state.models]
|
[freeOnly, recommendedOnly, state.models]
|
||||||
);
|
);
|
||||||
|
const emptyModelListMessage = recommendedOnly
|
||||||
|
? freeOnly
|
||||||
|
? 'No recommended free models found.'
|
||||||
|
: 'No recommended models found.'
|
||||||
|
: freeOnly
|
||||||
|
? 'No free models found.'
|
||||||
|
: 'No models found.';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 space-y-3 border-t border-white/10 pt-3">
|
<div className="mt-4 space-y-3 border-t border-white/10 pt-3">
|
||||||
|
|
@ -1993,6 +2021,27 @@ function ProviderModelList({
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{hasFreeModels ? (
|
||||||
|
<div
|
||||||
|
className="flex h-10 items-center gap-2 rounded-md border border-white/10 px-3"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
onKeyDown={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={`runtime-provider-${provider.providerId}-free-only`}
|
||||||
|
checked={freeOnly}
|
||||||
|
disabled={disabled || state.modelsLoading}
|
||||||
|
onCheckedChange={(checked) => setFreeOnly(checked === true)}
|
||||||
|
className="size-3.5"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor={`runtime-provider-${provider.providerId}-free-only`}
|
||||||
|
className="cursor-pointer text-xs font-normal text-[var(--color-text-secondary)]"
|
||||||
|
>
|
||||||
|
Free only
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{state.modelsError ? (
|
{state.modelsError ? (
|
||||||
|
|
@ -2010,9 +2059,7 @@ function ProviderModelList({
|
||||||
>
|
>
|
||||||
{!pickerOpen || state.modelsLoading ? <RuntimeProviderModelLoadingSkeleton /> : null}
|
{!pickerOpen || state.modelsLoading ? <RuntimeProviderModelLoadingSkeleton /> : null}
|
||||||
{pickerOpen && !state.modelsLoading && visibleModels.length === 0 && !state.modelsError ? (
|
{pickerOpen && !state.modelsLoading && visibleModels.length === 0 && !state.modelsError ? (
|
||||||
<div className="text-sm text-[var(--color-text-muted)]">
|
<div className="text-sm text-[var(--color-text-muted)]">{emptyModelListMessage}</div>
|
||||||
{recommendedOnly ? 'No recommended models found.' : 'No models found.'}
|
|
||||||
</div>
|
|
||||||
) : null}
|
) : null}
|
||||||
{pickerOpen
|
{pickerOpen
|
||||||
? visibleModels.map((model) => (
|
? visibleModels.map((model) => (
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,7 @@ interface OpenCodeModelOptionMetadata {
|
||||||
pricingInfo: OpenCodeModelPricingInfo | null;
|
pricingInfo: OpenCodeModelPricingInfo | null;
|
||||||
searchText: string;
|
searchText: string;
|
||||||
isRecommended: boolean;
|
isRecommended: boolean;
|
||||||
|
isFree: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OpenCodeVirtualHeadingRow {
|
interface OpenCodeVirtualHeadingRow {
|
||||||
|
|
@ -220,6 +221,24 @@ function buildOpenCodeModelSearchText({
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isFreeOpenCodeModelOption({
|
||||||
|
option,
|
||||||
|
routeMetadata,
|
||||||
|
pricingInfo,
|
||||||
|
}: {
|
||||||
|
option: TeamRuntimeModelOption;
|
||||||
|
routeMetadata: NonNullable<ProviderModelCatalogItem['metadata']>['opencode'] | null;
|
||||||
|
pricingInfo: OpenCodeModelPricingInfo | null;
|
||||||
|
}): boolean {
|
||||||
|
const badgeLabel = option.badgeLabel?.trim().toLowerCase();
|
||||||
|
return (
|
||||||
|
pricingInfo?.free === true ||
|
||||||
|
routeMetadata?.routeKind === 'builtin_free' ||
|
||||||
|
badgeLabel === 'free' ||
|
||||||
|
isFreeOpenCodeModelRoute(option.value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getOpenCodeModelGridColumnCount(width: number): number {
|
function getOpenCodeModelGridColumnCount(width: number): number {
|
||||||
const safeWidth = Number.isFinite(width) ? Math.max(0, width) : 0;
|
const safeWidth = Number.isFinite(width) ? Math.max(0, width) : 0;
|
||||||
if (safeWidth <= 0) {
|
if (safeWidth <= 0) {
|
||||||
|
|
@ -721,6 +740,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const multimodelEnabled = useStore((s) => s.appConfig?.general?.multimodelEnabled ?? true);
|
const multimodelEnabled = useStore((s) => s.appConfig?.general?.multimodelEnabled ?? true);
|
||||||
const [recommendedOnly, setRecommendedOnly] = useState(false);
|
const [recommendedOnly, setRecommendedOnly] = useState(false);
|
||||||
|
const [freeOnly, setFreeOnly] = useState(false);
|
||||||
const [modelQuery, setModelQuery] = useState('');
|
const [modelQuery, setModelQuery] = useState('');
|
||||||
const [openCodeSourceFilterOpen, setOpenCodeSourceFilterOpen] = useState(false);
|
const [openCodeSourceFilterOpen, setOpenCodeSourceFilterOpen] = useState(false);
|
||||||
const [openCodeSourceQuery, setOpenCodeSourceQuery] = useState('');
|
const [openCodeSourceQuery, setOpenCodeSourceQuery] = useState('');
|
||||||
|
|
@ -958,6 +978,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
pricingInfo,
|
pricingInfo,
|
||||||
}),
|
}),
|
||||||
isRecommended: isRecommendedTeamModelRecommendation(recommendation),
|
isRecommended: isRecommendedTeamModelRecommendation(recommendation),
|
||||||
|
isFree: isFreeOpenCodeModelOption({ option, routeMetadata, pricingInfo }),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [effectiveProviderId, modelOptions, openCodeCatalogModelById]);
|
}, [effectiveProviderId, modelOptions, openCodeCatalogModelById]);
|
||||||
|
|
@ -969,6 +990,10 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
() => openCodeModelMetadata.some((metadata) => metadata.isRecommended),
|
() => openCodeModelMetadata.some((metadata) => metadata.isRecommended),
|
||||||
[openCodeModelMetadata]
|
[openCodeModelMetadata]
|
||||||
);
|
);
|
||||||
|
const hasFreeOpenCodeModels = useMemo(
|
||||||
|
() => openCodeModelMetadata.some((metadata) => metadata.isFree),
|
||||||
|
[openCodeModelMetadata]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (previousSelectedProviderIdRef.current === selectedProviderId) {
|
if (previousSelectedProviderIdRef.current === selectedProviderId) {
|
||||||
|
|
@ -984,6 +1009,12 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
}
|
}
|
||||||
}, [effectiveProviderId, hasRecommendedOpenCodeModels, recommendedOnly]);
|
}, [effectiveProviderId, hasRecommendedOpenCodeModels, recommendedOnly]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (freeOnly && (effectiveProviderId !== 'opencode' || !hasFreeOpenCodeModels)) {
|
||||||
|
setFreeOnly(false);
|
||||||
|
}
|
||||||
|
}, [effectiveProviderId, freeOnly, hasFreeOpenCodeModels]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (previousEffectiveProviderIdRef.current === effectiveProviderId) {
|
if (previousEffectiveProviderIdRef.current === effectiveProviderId) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1023,6 +1054,9 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
if (recommendedOnly && !metadata.isRecommended) {
|
if (recommendedOnly && !metadata.isRecommended) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (freeOnly && !metadata.isFree) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const sourceInfo = metadata.sourceInfo;
|
const sourceInfo = metadata.sourceInfo;
|
||||||
if (!sourceInfo) {
|
if (!sourceInfo) {
|
||||||
|
|
@ -1040,7 +1074,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
return Array.from(sourceOptions.values()).sort((left, right) =>
|
return Array.from(sourceOptions.values()).sort((left, right) =>
|
||||||
left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })
|
left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })
|
||||||
);
|
);
|
||||||
}, [effectiveProviderId, openCodeModelMetadata, recommendedOnly]);
|
}, [effectiveProviderId, freeOnly, openCodeModelMetadata, recommendedOnly]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedOpenCodeSourceIds.size === 0) {
|
if (selectedOpenCodeSourceIds.size === 0) {
|
||||||
|
|
@ -1105,6 +1139,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
const concreteOptions = openCodeModelMetadata
|
const concreteOptions = openCodeModelMetadata
|
||||||
.filter((metadata) => metadata.option.value.trim().length > 0)
|
.filter((metadata) => metadata.option.value.trim().length > 0)
|
||||||
.filter((metadata) => !recommendedOnly || metadata.isRecommended)
|
.filter((metadata) => !recommendedOnly || metadata.isRecommended)
|
||||||
|
.filter((metadata) => !freeOnly || metadata.isFree)
|
||||||
.filter((metadata) => {
|
.filter((metadata) => {
|
||||||
if (selectedOpenCodeSourceIds.size === 0) {
|
if (selectedOpenCodeSourceIds.size === 0) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -1123,7 +1158,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
return recommendationOrder || left.index - right.index;
|
return recommendationOrder || left.index - right.index;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (recommendedOnly) {
|
if (recommendedOnly || freeOnly) {
|
||||||
return concreteOptions;
|
return concreteOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1135,6 +1170,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
];
|
];
|
||||||
}, [
|
}, [
|
||||||
effectiveProviderId,
|
effectiveProviderId,
|
||||||
|
freeOnly,
|
||||||
modelQuery,
|
modelQuery,
|
||||||
openCodeModelMetadata,
|
openCodeModelMetadata,
|
||||||
recommendedOnly,
|
recommendedOnly,
|
||||||
|
|
@ -1220,6 +1256,15 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
effectiveProviderId === 'opencode' &&
|
effectiveProviderId === 'opencode' &&
|
||||||
!shouldShowOpenCodeCatalogLoading &&
|
!shouldShowOpenCodeCatalogLoading &&
|
||||||
visibleConcreteModelOptionCount > OPENCODE_MODEL_VIRTUALIZATION_THRESHOLD;
|
visibleConcreteModelOptionCount > OPENCODE_MODEL_VIRTUALIZATION_THRESHOLD;
|
||||||
|
const emptyModelListMessage = trimmedModelQuery
|
||||||
|
? 'No models match this search.'
|
||||||
|
: effectiveProviderId === 'opencode' && recommendedOnly && freeOnly
|
||||||
|
? 'No recommended free OpenCode models are available in the current runtime list.'
|
||||||
|
: effectiveProviderId === 'opencode' && freeOnly
|
||||||
|
? 'No free OpenCode models are available in the current runtime list.'
|
||||||
|
: effectiveProviderId === 'opencode' && recommendedOnly
|
||||||
|
? 'No recommended OpenCode models are available in the current runtime list.'
|
||||||
|
: 'No models are available in the current runtime list.';
|
||||||
const activeProviderDisabledReason = activeProviderSelectable
|
const activeProviderDisabledReason = activeProviderSelectable
|
||||||
? null
|
? null
|
||||||
: getProviderDisabledReason(effectiveProviderId);
|
: getProviderDisabledReason(effectiveProviderId);
|
||||||
|
|
@ -1689,7 +1734,8 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
) : null}
|
) : null}
|
||||||
{!shouldShowOpenCodeCatalogLoading &&
|
{!shouldShowOpenCodeCatalogLoading &&
|
||||||
((effectiveProviderId === 'opencode' && openCodeSourceOptions.length > 1) ||
|
((effectiveProviderId === 'opencode' && openCodeSourceOptions.length > 1) ||
|
||||||
hasRecommendedOpenCodeModels) ? (
|
hasRecommendedOpenCodeModels ||
|
||||||
|
hasFreeOpenCodeModels) ? (
|
||||||
<div className="mb-2 flex flex-wrap items-center gap-2">
|
<div className="mb-2 flex flex-wrap items-center gap-2">
|
||||||
{effectiveProviderId === 'opencode' && openCodeSourceOptions.length > 1 ? (
|
{effectiveProviderId === 'opencode' && openCodeSourceOptions.length > 1 ? (
|
||||||
<Popover
|
<Popover
|
||||||
|
|
@ -1785,6 +1831,22 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{hasFreeOpenCodeModels ? (
|
||||||
|
<div className="flex w-fit items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
id="opencode-team-model-free-only"
|
||||||
|
checked={freeOnly}
|
||||||
|
onCheckedChange={(checked) => setFreeOnly(checked === true)}
|
||||||
|
className="size-3.5"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor="opencode-team-model-free-only"
|
||||||
|
className="cursor-pointer text-[11px] font-normal text-[var(--color-text-secondary)]"
|
||||||
|
>
|
||||||
|
Free only
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{effectiveProviderId === 'opencode' ? (
|
{effectiveProviderId === 'opencode' ? (
|
||||||
|
|
@ -1869,11 +1931,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
)}
|
)}
|
||||||
{visibleModelOptions.length === 0 && !shouldShowOpenCodeCatalogLoading ? (
|
{visibleModelOptions.length === 0 && !shouldShowOpenCodeCatalogLoading ? (
|
||||||
<div className="rounded-md border border-white/10 px-3 py-2 text-xs text-[var(--color-text-muted)]">
|
<div className="rounded-md border border-white/10 px-3 py-2 text-xs text-[var(--color-text-muted)]">
|
||||||
{trimmedModelQuery
|
{emptyModelListMessage}
|
||||||
? 'No models match this search.'
|
|
||||||
: effectiveProviderId === 'opencode' && recommendedOnly
|
|
||||||
? 'No recommended OpenCode models are available in the current runtime list.'
|
|
||||||
: 'No models are available in the current runtime list.'}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -610,6 +610,21 @@ describe('TeamModelSelector disabled Codex models', () => {
|
||||||
expect(notRecommendedIndex).toBeGreaterThan(unavailableIndex);
|
expect(notRecommendedIndex).toBeGreaterThan(unavailableIndex);
|
||||||
|
|
||||||
expect(host.textContent).toContain('Recommended only');
|
expect(host.textContent).toContain('Recommended only');
|
||||||
|
expect(host.textContent).toContain('Free only');
|
||||||
|
|
||||||
|
const freeOnlyToggle = host.querySelector<HTMLElement>('#opencode-team-model-free-only');
|
||||||
|
expect(freeOnlyToggle).not.toBeNull();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
freeOnlyToggle?.click();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.textContent).toContain('openai/gpt-oss-120b:free');
|
||||||
|
expect(host.textContent).toContain('big-pickle');
|
||||||
|
expect(host.textContent).toContain('openai/gpt-oss-20b:free');
|
||||||
|
expect(host.textContent).not.toContain('qwen/qwen3-coder-plus');
|
||||||
|
expect(host.textContent).not.toContain('anthropic/claude-sonnet-4.6');
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
root.unmount();
|
root.unmount();
|
||||||
|
|
|
||||||
|
|
@ -1908,6 +1908,74 @@ describe('RuntimeProviderManagementPanelView', () => {
|
||||||
expect(actions.useModelForNewTeams).not.toHaveBeenCalled();
|
expect(actions.useModelForNewTeams).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('filters provider model picker rows to free models', async () => {
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
const actions = createActions();
|
||||||
|
const connectedProvider = {
|
||||||
|
...createState().view!.providers[0],
|
||||||
|
state: 'connected' as const,
|
||||||
|
ownership: ['managed'] as const,
|
||||||
|
modelCount: 2,
|
||||||
|
actions: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(RuntimeProviderManagementPanelView, {
|
||||||
|
state: createState({
|
||||||
|
view: {
|
||||||
|
...createState().view!,
|
||||||
|
providers: [connectedProvider],
|
||||||
|
},
|
||||||
|
providers: [connectedProvider],
|
||||||
|
selectedProviderId: 'openrouter',
|
||||||
|
modelPickerProviderId: 'openrouter',
|
||||||
|
modelPickerMode: 'use',
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
providerId: 'openrouter',
|
||||||
|
modelId: 'openrouter/anthropic/claude-haiku-4.5',
|
||||||
|
displayName: 'anthropic/claude-haiku-4.5',
|
||||||
|
sourceLabel: 'OpenRouter',
|
||||||
|
free: true,
|
||||||
|
default: false,
|
||||||
|
availability: 'untested',
|
||||||
|
routeKind: 'connected_provider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
providerId: 'openrouter',
|
||||||
|
modelId: 'openrouter/anthropic/claude-sonnet-4.6',
|
||||||
|
displayName: 'anthropic/claude-sonnet-4.6',
|
||||||
|
sourceLabel: 'OpenRouter',
|
||||||
|
free: false,
|
||||||
|
default: false,
|
||||||
|
availability: 'untested',
|
||||||
|
routeKind: 'connected_provider',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
actions,
|
||||||
|
disabled: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.textContent).toContain('Free only');
|
||||||
|
expect(host.textContent).toContain('anthropic/claude-haiku-4.5');
|
||||||
|
expect(host.textContent).toContain('anthropic/claude-sonnet-4.6');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
host.querySelector<HTMLElement>('#runtime-provider-openrouter-free-only')?.click();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.textContent).toContain('anthropic/claude-haiku-4.5');
|
||||||
|
expect(host.textContent).not.toContain('anthropic/claude-sonnet-4.6');
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps the model search input enabled while model results are loading', async () => {
|
it('keeps the model search input enabled while model results are loading', async () => {
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
document.body.appendChild(host);
|
document.body.appendChild(host);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue