diff --git a/apps/mobile/src/app/(app)/agent-chat/model-picker.tsx b/apps/mobile/src/app/(app)/agent-chat/model-picker.tsx
index 201687d180..375cb69f1c 100644
--- a/apps/mobile/src/app/(app)/agent-chat/model-picker.tsx
+++ b/apps/mobile/src/app/(app)/agent-chat/model-picker.tsx
@@ -5,6 +5,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Pressable, ScrollView, TextInput, View } from 'react-native';
import { Text } from '@/components/ui/text';
+import {
+ FREE_MODEL_DATA_LABEL,
+ getFreeModelDataAccessibilityLabel,
+ isFreeModelOption,
+} from '@/lib/free-model-data-disclosure';
import { useThemeColors } from '@/lib/hooks/use-theme-colors';
import { type ModelOption, thinkingEffortLabel } from '@/lib/hooks/use-available-models';
import { clearModelPickerBridge, getModelPickerBridge } from '@/lib/picker-bridge';
@@ -217,6 +222,7 @@ export default function ModelPickerScreen() {
const modelOption = item.model;
const selected = modelOption.id === selectedModel;
+ const collectsData = isFreeModelOption(modelOption);
const hasVariants = modelOption.variants.length > 1;
return (
@@ -227,11 +233,28 @@ export default function ModelPickerScreen() {
handleSelectModel(modelOption.id);
}}
accessibilityRole="button"
- accessibilityLabel={`${modelOption.name}${selected ? ', selected' : ''}`}
+ accessibilityLabel={`${collectsData ? getFreeModelDataAccessibilityLabel(modelOption.name) : modelOption.name}${selected ? ', selected' : ''}`}
>
{modelOption.name}
{modelOption.id}
+ {collectsData ? (
+
+
+ {FREE_MODEL_DATA_LABEL}
+
+
+ ) : null}
{selected && }
diff --git a/apps/mobile/src/components/agents/model-selector.tsx b/apps/mobile/src/components/agents/model-selector.tsx
index f465821478..b5cf64c006 100644
--- a/apps/mobile/src/components/agents/model-selector.tsx
+++ b/apps/mobile/src/components/agents/model-selector.tsx
@@ -1,8 +1,12 @@
import { Pressable, View } from 'react-native';
import { type Href, useRouter } from 'expo-router';
-import { Brain, ChevronDown } from 'lucide-react-native';
+import { Brain, ChevronDown, Info } from 'lucide-react-native';
import { Text } from '@/components/ui/text';
+import {
+ getFreeModelDataAccessibilityLabel,
+ isFreeModelOption,
+} from '@/lib/free-model-data-disclosure';
import { type ModelOption, thinkingEffortLabel } from '@/lib/hooks/use-available-models';
import { useThemeColors } from '@/lib/hooks/use-theme-colors';
import { setModelPickerBridge } from '@/lib/picker-bridge';
@@ -39,9 +43,13 @@ export function ModelSelector({
const selectedModel = options.find(m => m.id === value);
const label = selectedModel?.name ?? (value || 'Model');
+ const collectsData = isFreeModelOption(selectedModel);
const hasVariants = selectedModel ? selectedModel.variants.length > 1 : false;
const variantLabel = variant ? thinkingEffortLabel(variant) : '';
const compactVariantLabel = variant ? compactThinkingEffortLabel(variant) : '';
+ const dataLabel = collectsData ? getFreeModelDataAccessibilityLabel(label) : label;
+ const accessibilityLabel =
+ hasVariants && variantLabel ? `${dataLabel}, ${variantLabel} thinking effort` : dataLabel;
function handlePress() {
if (effectivelyDisabled) {
@@ -61,9 +69,7 @@ export function ModelSelector({
onPress={handlePress}
disabled={effectivelyDisabled}
accessibilityRole="button"
- accessibilityLabel={
- hasVariants && variantLabel ? `${label}, ${variantLabel} thinking effort` : label
- }
+ accessibilityLabel={accessibilityLabel}
className={cn(
'max-w-[240px] shrink flex-row items-center gap-1.5 rounded-full bg-secondary px-3 py-1.5 active:opacity-70',
effectivelyDisabled && 'opacity-50'
@@ -76,6 +82,7 @@ export function ModelSelector({
>
{label}
+ {collectsData ? : null}
{hasVariants && compactVariantLabel ? (
diff --git a/apps/mobile/src/lib/free-model-data-disclosure.test.ts b/apps/mobile/src/lib/free-model-data-disclosure.test.ts
new file mode 100644
index 0000000000..2d592cffa8
--- /dev/null
+++ b/apps/mobile/src/lib/free-model-data-disclosure.test.ts
@@ -0,0 +1,25 @@
+import { describe, expect, it } from 'vitest';
+import {
+ FREE_MODEL_DATA_LABEL,
+ getFreeModelDataAccessibilityLabel,
+ isFreeModelOption,
+} from './free-model-data-disclosure';
+
+describe('free model data disclosure', () => {
+ it('uses the disclosure label expected in model pickers', () => {
+ expect(FREE_MODEL_DATA_LABEL).toBe('Free - data collected');
+ });
+
+ it('detects explicit and known free model options', () => {
+ expect(isFreeModelOption({ id: 'anthropic/claude', isFree: true })).toBe(true);
+ expect(isFreeModelOption({ id: 'openrouter/free' })).toBe(true);
+ expect(isFreeModelOption({ id: 'openrouter/model-alpha' })).toBe(true);
+ expect(isFreeModelOption({ id: 'anthropic/claude' })).toBe(false);
+ });
+
+ it('adds a data collection phrase to accessibility labels', () => {
+ expect(getFreeModelDataAccessibilityLabel('Kilo Auto')).toBe(
+ 'Kilo Auto, free model, usage data collected'
+ );
+ });
+});
diff --git a/apps/mobile/src/lib/free-model-data-disclosure.ts b/apps/mobile/src/lib/free-model-data-disclosure.ts
new file mode 100644
index 0000000000..bda117da52
--- /dev/null
+++ b/apps/mobile/src/lib/free-model-data-disclosure.ts
@@ -0,0 +1,20 @@
+export const FREE_MODEL_DATA_LABEL = 'Free - data collected';
+export const FREE_MODEL_DATA_SHORT_LABEL = 'Data collected';
+
+export function isFreeModelOption(model: { id: string; isFree?: boolean } | undefined) {
+ if (!model) {
+ return false;
+ }
+ return (
+ model.isFree === true ||
+ model.id === 'kilo-auto/free' ||
+ model.id.endsWith(':free') ||
+ model.id === 'openrouter/free' ||
+ (model.id.startsWith('openrouter/') &&
+ (model.id.endsWith('-alpha') || model.id.endsWith('-beta')))
+ );
+}
+
+export function getFreeModelDataAccessibilityLabel(label: string) {
+ return `${label}, free model, usage data collected`;
+}
diff --git a/apps/mobile/src/lib/hooks/use-available-models.ts b/apps/mobile/src/lib/hooks/use-available-models.ts
index 9676c75fdd..f10190e619 100644
--- a/apps/mobile/src/lib/hooks/use-available-models.ts
+++ b/apps/mobile/src/lib/hooks/use-available-models.ts
@@ -12,12 +12,14 @@ export type ModelOption = {
name: string;
variants: string[];
isPreferred: boolean;
+ isFree?: boolean;
};
type ModelResponse = {
data: {
id: string;
name: string;
+ isFree?: boolean;
preferredIndex?: number;
opencode?: {
variants?: Record;
@@ -98,6 +100,7 @@ export function useAvailableModels(organizationId: string | undefined) {
const items = data.data.map(model => ({
id: model.id,
name: formatShortModelName(model.name),
+ isFree: model.isFree,
variants: Object.keys(model.opencode?.variants ?? {}),
preferredIndex: model.preferredIndex,
}));
@@ -123,6 +126,7 @@ export function useAvailableModels(organizationId: string | undefined) {
name: item.name,
variants: item.variants,
isPreferred: item.preferredIndex !== undefined,
+ isFree: item.isFree,
}));
}, [data]);
diff --git a/apps/web/src/app/(app)/claw/components/SettingsTab.tsx b/apps/web/src/app/(app)/claw/components/SettingsTab.tsx
index c67210ebf5..17ec0c3468 100644
--- a/apps/web/src/app/(app)/claw/components/SettingsTab.tsx
+++ b/apps/web/src/app/(app)/claw/components/SettingsTab.tsx
@@ -1956,7 +1956,11 @@ export function SettingsTab({
const modelOptions = useMemo(
() =>
getSettingsModelOptions({
- models: (modelsData?.data || []).map(model => ({ id: model.id, name: model.name })),
+ models: (modelsData?.data || []).map(model => ({
+ id: model.id,
+ name: model.name,
+ isFree: model.isFree,
+ })),
trackedOpenClawVersion: trackedVersion,
runningOpenClawVersion: runningVersion,
isRunning,
diff --git a/apps/web/src/app/(app)/cloud/webhooks/[triggerId]/EditWebhookTriggerContent.tsx b/apps/web/src/app/(app)/cloud/webhooks/[triggerId]/EditWebhookTriggerContent.tsx
index ae6bcb1473..629002f2bf 100644
--- a/apps/web/src/app/(app)/cloud/webhooks/[triggerId]/EditWebhookTriggerContent.tsx
+++ b/apps/web/src/app/(app)/cloud/webhooks/[triggerId]/EditWebhookTriggerContent.tsx
@@ -86,7 +86,12 @@ export function EditWebhookTriggerContent({
// Transform models to ModelOption format
const modelOptions = useMemo(
- () => (modelsData?.data || []).map(model => ({ id: model.id, name: model.name })),
+ () =>
+ (modelsData?.data || []).map(model => ({
+ id: model.id,
+ name: model.name,
+ isFree: model.isFree,
+ })),
[modelsData?.data]
);
diff --git a/apps/web/src/app/(app)/cloud/webhooks/new/CreateWebhookTriggerContent.tsx b/apps/web/src/app/(app)/cloud/webhooks/new/CreateWebhookTriggerContent.tsx
index 99c861a774..bbc2ea5a4a 100644
--- a/apps/web/src/app/(app)/cloud/webhooks/new/CreateWebhookTriggerContent.tsx
+++ b/apps/web/src/app/(app)/cloud/webhooks/new/CreateWebhookTriggerContent.tsx
@@ -72,7 +72,12 @@ export function CreateWebhookTriggerContent({ organizationId }: CreateWebhookTri
// Transform models to ModelOption format
const modelOptions = useMemo(
- () => (modelsData?.data || []).map(model => ({ id: model.id, name: model.name })),
+ () =>
+ (modelsData?.data || []).map(model => ({
+ id: model.id,
+ name: model.name,
+ isFree: model.isFree,
+ })),
[modelsData?.data]
);
diff --git a/apps/web/src/app/(app)/gastown/[townId]/rigs/[rigId]/settings/RigSettingsPageClient.tsx b/apps/web/src/app/(app)/gastown/[townId]/rigs/[rigId]/settings/RigSettingsPageClient.tsx
index 61621384ad..b5d92c100f 100644
--- a/apps/web/src/app/(app)/gastown/[townId]/rigs/[rigId]/settings/RigSettingsPageClient.tsx
+++ b/apps/web/src/app/(app)/gastown/[townId]/rigs/[rigId]/settings/RigSettingsPageClient.tsx
@@ -96,7 +96,12 @@ export function RigSettingsPageClient({ townId, rigId, organizationId }: Props)
} = useModelSelectorList(organizationId);
const modelOptions = useMemo(
- () => modelsData?.data.map(model => ({ id: model.id, name: model.name })) ?? [],
+ () =>
+ modelsData?.data.map(model => ({
+ id: model.id,
+ name: model.name,
+ isFree: model.isFree,
+ })) ?? [],
[modelsData]
);
diff --git a/apps/web/src/app/(app)/gastown/[townId]/settings/TownSettingsPageClient.tsx b/apps/web/src/app/(app)/gastown/[townId]/settings/TownSettingsPageClient.tsx
index 3cbd3198f3..f6b7ccf5b3 100644
--- a/apps/web/src/app/(app)/gastown/[townId]/settings/TownSettingsPageClient.tsx
+++ b/apps/web/src/app/(app)/gastown/[townId]/settings/TownSettingsPageClient.tsx
@@ -141,7 +141,12 @@ export function TownSettingsPageClient({ townId, readOnly = false, organizationI
} = useModelSelectorList(organizationId);
const modelOptions = useMemo(
- () => modelsData?.data.map(model => ({ id: model.id, name: model.name })) ?? [],
+ () =>
+ modelsData?.data.map(model => ({
+ id: model.id,
+ name: model.name,
+ isFree: model.isFree,
+ })) ?? [],
[modelsData]
);
diff --git a/apps/web/src/app/(app)/gastown/onboarding/OnboardingStepModel.tsx b/apps/web/src/app/(app)/gastown/onboarding/OnboardingStepModel.tsx
index d77d457ab6..cec7dbe157 100644
--- a/apps/web/src/app/(app)/gastown/onboarding/OnboardingStepModel.tsx
+++ b/apps/web/src/app/(app)/gastown/onboarding/OnboardingStepModel.tsx
@@ -327,7 +327,12 @@ export function OnboardingStepModel() {
} = useModelSelectorList(undefined);
const modelOptions = useMemo(
- () => modelsData?.data.map(model => ({ id: model.id, name: model.name })) ?? [],
+ () =>
+ modelsData?.data.map(model => ({
+ id: model.id,
+ name: model.name,
+ isFree: model.isFree,
+ })) ?? [],
[modelsData]
);
diff --git a/apps/web/src/components/app-builder/AppBuilderChat.tsx b/apps/web/src/components/app-builder/AppBuilderChat.tsx
index cd6597ceef..7429c83761 100644
--- a/apps/web/src/components/app-builder/AppBuilderChat.tsx
+++ b/apps/web/src/components/app-builder/AppBuilderChat.tsx
@@ -532,7 +532,7 @@ export function AppBuilderChat({ organizationId }: AppBuilderChatProps) {
const inputModalities = m.architecture?.input_modalities || [];
const supportsVision =
inputModalities.includes('image') || inputModalities.includes('image_url');
- return { id: m.id, name: m.name, supportsVision };
+ return { id: m.id, name: m.name, supportsVision, isFree: m.isFree };
}),
[availableModels]
);
diff --git a/apps/web/src/components/app-builder/AppBuilderLanding.tsx b/apps/web/src/components/app-builder/AppBuilderLanding.tsx
index 6e9f4be6a7..f3fb0e54be 100644
--- a/apps/web/src/components/app-builder/AppBuilderLanding.tsx
+++ b/apps/web/src/components/app-builder/AppBuilderLanding.tsx
@@ -632,7 +632,7 @@ export function AppBuilderLanding({ organizationId, onProjectCreated }: AppBuild
const inputModalities = m.architecture?.input_modalities || [];
const supportsVision =
inputModalities.includes('image') || inputModalities.includes('image_url');
- return { id: m.id, name: m.name, supportsVision };
+ return { id: m.id, name: m.name, supportsVision, isFree: m.isFree };
}),
[availableModels]
);
diff --git a/apps/web/src/components/cloud-agent-next/NewSessionPanel.tsx b/apps/web/src/components/cloud-agent-next/NewSessionPanel.tsx
index ad5244747d..86346b7fce 100644
--- a/apps/web/src/components/cloud-agent-next/NewSessionPanel.tsx
+++ b/apps/web/src/components/cloud-agent-next/NewSessionPanel.tsx
@@ -151,6 +151,7 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New
allModels.map(model => ({
id: model.id,
name: model.name,
+ isFree: model.isFree,
variants: model.opencode?.variants ? Object.keys(model.opencode.variants) : undefined,
}))
),
diff --git a/apps/web/src/components/cloud-agent-next/hooks/useOrganizationModels.ts b/apps/web/src/components/cloud-agent-next/hooks/useOrganizationModels.ts
index 6340d0d55a..f1d355d1cc 100644
--- a/apps/web/src/components/cloud-agent-next/hooks/useOrganizationModels.ts
+++ b/apps/web/src/components/cloud-agent-next/hooks/useOrganizationModels.ts
@@ -39,6 +39,7 @@ export function useOrganizationModels(organizationId?: string): UseOrganizationM
openRouterModels?.data.map(model => ({
id: model.id,
name: model.name,
+ isFree: model.isFree,
variants: model.opencode?.variants ? Object.keys(model.opencode.variants) : undefined,
})) ?? []
);
diff --git a/apps/web/src/components/cloud-agent/CloudSessionsPage.tsx b/apps/web/src/components/cloud-agent/CloudSessionsPage.tsx
index d68fb1f961..5396393257 100644
--- a/apps/web/src/components/cloud-agent/CloudSessionsPage.tsx
+++ b/apps/web/src/components/cloud-agent/CloudSessionsPage.tsx
@@ -80,6 +80,7 @@ export function CloudSessionsPage({ organizationId }: CloudSessionsPageProps) {
allModels.map(model => ({
id: model.id,
name: model.name,
+ isFree: model.isFree,
variants: model.opencode?.variants ? Object.keys(model.opencode.variants) : undefined,
})),
[allModels]
diff --git a/apps/web/src/components/cloud-agent/hooks/useOrganizationModels.ts b/apps/web/src/components/cloud-agent/hooks/useOrganizationModels.ts
index 7a236d3d15..745716b7a8 100644
--- a/apps/web/src/components/cloud-agent/hooks/useOrganizationModels.ts
+++ b/apps/web/src/components/cloud-agent/hooks/useOrganizationModels.ts
@@ -34,7 +34,13 @@ export function useOrganizationModels(organizationId?: string): UseOrganizationM
// Format models for the combobox
const modelOptions = useMemo(() => {
- return openRouterModels?.data.map(model => ({ id: model.id, name: model.name })) ?? [];
+ return (
+ openRouterModels?.data.map(model => ({
+ id: model.id,
+ name: model.name,
+ isFree: model.isFree,
+ })) ?? []
+ );
}, [openRouterModels]);
return {
diff --git a/apps/web/src/components/cloud-agent/profile-editor/KiloCommandsTab.tsx b/apps/web/src/components/cloud-agent/profile-editor/KiloCommandsTab.tsx
index 0b78f0c474..0b96c44ed9 100644
--- a/apps/web/src/components/cloud-agent/profile-editor/KiloCommandsTab.tsx
+++ b/apps/web/src/components/cloud-agent/profile-editor/KiloCommandsTab.tsx
@@ -188,6 +188,7 @@ function KiloCommandForm(props: KiloCommandFormProps) {
(modelsData?.data ?? []).map(m => ({
id: m.id,
name: m.name,
+ isFree: m.isFree,
variants: m.opencode?.variants ? Object.keys(m.opencode.variants) : undefined,
})),
[modelsData]
diff --git a/apps/web/src/components/cloud-agent/profile-editor/ProfileAgentsTab.tsx b/apps/web/src/components/cloud-agent/profile-editor/ProfileAgentsTab.tsx
index 04a795302b..8ba53ad82e 100644
--- a/apps/web/src/components/cloud-agent/profile-editor/ProfileAgentsTab.tsx
+++ b/apps/web/src/components/cloud-agent/profile-editor/ProfileAgentsTab.tsx
@@ -326,6 +326,7 @@ function AgentForm({
(modelsData?.data ?? []).map(m => ({
id: m.id,
name: m.name,
+ isFree: m.isFree,
variants: m.opencode?.variants ? Object.keys(m.opencode.variants) : undefined,
})),
[modelsData]
diff --git a/apps/web/src/components/integrations/DiscordIntegrationDetails.tsx b/apps/web/src/components/integrations/DiscordIntegrationDetails.tsx
index 4ecfd496dc..316bd93046 100644
--- a/apps/web/src/components/integrations/DiscordIntegrationDetails.tsx
+++ b/apps/web/src/components/integrations/DiscordIntegrationDetails.tsx
@@ -40,7 +40,13 @@ export function DiscordIntegrationDetails({
useModelSelectorList(organizationId);
const modelOptions = useMemo(() => {
- return openRouterModels?.data.map(model => ({ id: model.id, name: model.name })) ?? [];
+ return (
+ openRouterModels?.data.map(model => ({
+ id: model.id,
+ name: model.name,
+ isFree: model.isFree,
+ })) ?? []
+ );
}, [openRouterModels]);
// Track selected model
diff --git a/apps/web/src/components/integrations/LinearIntegrationDetails.tsx b/apps/web/src/components/integrations/LinearIntegrationDetails.tsx
index 2a1c4568c3..12b30c3536 100644
--- a/apps/web/src/components/integrations/LinearIntegrationDetails.tsx
+++ b/apps/web/src/components/integrations/LinearIntegrationDetails.tsx
@@ -49,7 +49,13 @@ export function LinearIntegrationDetails({
useModelSelectorList(organizationId);
const modelOptions = useMemo(() => {
- return openRouterModels?.data.map(model => ({ id: model.id, name: model.name })) ?? [];
+ return (
+ openRouterModels?.data.map(model => ({
+ id: model.id,
+ name: model.name,
+ isFree: model.isFree,
+ })) ?? []
+ );
}, [openRouterModels]);
const [selectedModel, setSelectedModel] = useState('');
diff --git a/apps/web/src/components/integrations/SlackIntegrationDetails.tsx b/apps/web/src/components/integrations/SlackIntegrationDetails.tsx
index b598658a67..7482dac313 100644
--- a/apps/web/src/components/integrations/SlackIntegrationDetails.tsx
+++ b/apps/web/src/components/integrations/SlackIntegrationDetails.tsx
@@ -68,7 +68,13 @@ export function SlackIntegrationDetails({
useModelSelectorList(organizationId);
const modelOptions = useMemo(() => {
- return openRouterModels?.data.map(model => ({ id: model.id, name: model.name })) ?? [];
+ return (
+ openRouterModels?.data.map(model => ({
+ id: model.id,
+ name: model.name,
+ isFree: model.isFree,
+ })) ?? []
+ );
}, [openRouterModels]);
// Track selected model
diff --git a/apps/web/src/components/shared/ModelCombobox.tsx b/apps/web/src/components/shared/ModelCombobox.tsx
index 2c8203149f..18a2bb72df 100644
--- a/apps/web/src/components/shared/ModelCombobox.tsx
+++ b/apps/web/src/components/shared/ModelCombobox.tsx
@@ -13,16 +13,22 @@ import {
CommandItem,
CommandList,
} from '@/components/ui/command';
-import { ChevronsUpDown, Check, Image } from 'lucide-react';
+import { ChevronsUpDown, Check, Image, Info } from 'lucide-react';
import { cn } from '@/lib/utils';
import { preferredModels } from '@/lib/ai-gateway/models';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { formatShortModelDisplayName } from '@/lib/format-model-name';
+import {
+ FREE_MODEL_DATA_LABEL,
+ getFreeModelDataTooltip,
+ isFreeModelOption,
+} from '@/components/shared/free-model-data-disclosure';
export type ModelOption = {
id: string; // e.g., "anthropic/claude-sonnet-4.5"
name: string; // e.g., "Claude Sonnet 4.5"
supportsVision?: boolean;
+ isFree?: boolean;
/** Ordered list of variant key names (e.g., ["none","low","medium","high","max"]) */
variants?: string[];
};
@@ -111,6 +117,7 @@ export function ModelCombobox({
const selectedModel = models.find(model => model.id === value);
const isCompact = variant === 'compact';
const showLabel = !isCompact && label;
+ const selectedIsFree = isFreeModelOption(selectedModel);
if (isLoading) {
if (isCompact) {
@@ -209,9 +216,10 @@ export function ModelCombobox({
className={cn('h-9 justify-between gap-1.5', className)}
ref={triggerRef}
>
-
+
{selectedModel ? formatShortModelDisplayName(selectedModel.name) : placeholder}
+ {selectedIsFree && }
@@ -244,6 +252,7 @@ export function ModelCombobox({
Supports vision
)}
+ {isFreeModelOption(model) && }
{model.id}
@@ -281,6 +290,7 @@ export function ModelCombobox({
Supports vision
)}
+ {isFreeModelOption(model) && }
{model.id}
@@ -324,7 +334,10 @@ export function ModelCombobox({
className={cn('w-full justify-between', className)}
ref={triggerRef}
>
- {selectedModel ? selectedModel.name : placeholder}
+
+ {selectedModel ? selectedModel.name : placeholder}
+
+ {selectedIsFree && }
@@ -361,6 +374,7 @@ export function ModelCombobox({
Supports vision
)}
+ {isFreeModelOption(model) && }
{model.id}
@@ -398,6 +412,7 @@ export function ModelCombobox({
Supports vision
)}
+ {isFreeModelOption(model) && }
{model.id}
@@ -419,3 +434,28 @@ export function ModelCombobox({
);
}
+
+function FreeModelDataIcon() {
+ return (
+
+
+
+
+ {getFreeModelDataTooltip()}
+
+ );
+}
+
+function FreeModelDataBadge() {
+ return (
+
+
+
+
+ {FREE_MODEL_DATA_LABEL}
+
+
+ {getFreeModelDataTooltip()}
+
+ );
+}
diff --git a/apps/web/src/components/shared/free-model-data-disclosure.test.ts b/apps/web/src/components/shared/free-model-data-disclosure.test.ts
new file mode 100644
index 0000000000..48b856d3ae
--- /dev/null
+++ b/apps/web/src/components/shared/free-model-data-disclosure.test.ts
@@ -0,0 +1,23 @@
+import { describe, expect, it } from '@jest/globals';
+import {
+ FREE_MODEL_DATA_LABEL,
+ getFreeModelDataTooltip,
+ isFreeModelOption,
+} from './free-model-data-disclosure';
+
+describe('free model data disclosure', () => {
+ it('uses the disclosure label expected in model pickers', () => {
+ expect(FREE_MODEL_DATA_LABEL).toBe('Free - data collected');
+ });
+
+ it('detects explicit and known free model options', () => {
+ expect(isFreeModelOption({ id: 'anthropic/claude', isFree: true })).toBe(true);
+ expect(isFreeModelOption({ id: 'openrouter/free' })).toBe(true);
+ expect(isFreeModelOption({ id: 'openrouter/model-alpha' })).toBe(true);
+ expect(isFreeModelOption({ id: 'anthropic/claude' })).toBe(false);
+ });
+
+ it('describes why the free model indicator is shown', () => {
+ expect(getFreeModelDataTooltip()).toContain('model improvement');
+ });
+});
diff --git a/apps/web/src/components/shared/free-model-data-disclosure.ts b/apps/web/src/components/shared/free-model-data-disclosure.ts
new file mode 100644
index 0000000000..d7bb5d9b24
--- /dev/null
+++ b/apps/web/src/components/shared/free-model-data-disclosure.ts
@@ -0,0 +1,20 @@
+export const FREE_MODEL_DATA_LABEL = 'Free - data collected';
+export const FREE_MODEL_DATA_SHORT_LABEL = 'Data collected';
+
+export function getFreeModelDataTooltip() {
+ return 'Usage with free Kilo models is collected for model improvement.';
+}
+
+export function isFreeModelOption(model: { id: string; isFree?: boolean } | undefined) {
+ if (!model) {
+ return false;
+ }
+ return (
+ model.isFree === true ||
+ model.id === 'kilo-auto/free' ||
+ model.id.endsWith(':free') ||
+ model.id === 'openrouter/free' ||
+ (model.id.startsWith('openrouter/') &&
+ (model.id.endsWith('-alpha') || model.id.endsWith('-beta')))
+ );
+}