Skip to content

Commit aeaf8f1

Browse files
feat: improve model selector filtering and pricing calculation
- Add vendor, context length, and name exclusion filters - Fix blended price to use weighted 75/25 input/output ratio - Sort non-recommended models by release date (newest first) - Replace settings dropdown with full ModelSelector component - Remove "show all" toggle in favor of smarter defaults
1 parent a2ffbd3 commit aeaf8f1

6 files changed

Lines changed: 98 additions & 90 deletions

File tree

src/shared/constants.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import type { MatchingPreferences } from './types';
66

77
// src/shared/constants.ts
88
export const RECOMMENDED_MODELS = [
9-
'anthropic/claude-sonnet-4.5', // Higher quality option
10-
'anthropic/claude-haiku-4.5', // Default - fast, cheap, good for this use case
9+
'anthropic/claude-sonnet-4.5', // Default - Higher quality option
10+
'anthropic/claude-haiku-4.5', // Faster, cheaper, low quality option
1111
] as const;
1212

1313
// Default model - fast, cheap, good enough for matching/reply generation
@@ -123,10 +123,29 @@ export const MODEL_CACHE_TTL = 60 * 60 * 1000;
123123
export const MAX_STYLE_EXAMPLES_IN_PROMPT = 15;
124124

125125
// Default max price filter for models (per 1M tokens blended)
126-
export const DEFAULT_MAX_MODEL_PRICE = 10;
127-
128-
// Default max age filter for models (in days)
129-
export const DEFAULT_MAX_MODEL_AGE_DAYS = 365;
126+
export const DEFAULT_MAX_MODEL_PRICE = 6;
127+
128+
// Default max age filter for models (in days) - 7 months
129+
export const DEFAULT_MAX_MODEL_AGE_DAYS = 210;
130+
131+
// Default minimum context window size
132+
export const DEFAULT_MIN_CONTEXT_LENGTH = 200000;
133+
134+
// Allowed model vendors (prefix matching against model ID)
135+
export const DEFAULT_ALLOWED_VENDORS = ['anthropic', 'google', 'openai', 'x-ai'] as const;
136+
137+
// Model name substrings to exclude (case-insensitive)
138+
export const DEFAULT_MODEL_NAME_EXCLUSIONS = [
139+
'image',
140+
'flash',
141+
'nano',
142+
'mini',
143+
'-mini',
144+
'fast',
145+
'lite',
146+
'codex',
147+
'thinking',
148+
] as const;
130149

131150
// Number of reply suggestions to generate per post
132151
export const REPLY_SUGGESTIONS_COUNT = 3;

src/shared/types.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,10 +294,14 @@ export interface ModelFilterOptions {
294294
maxPrice?: number;
295295
/** Maximum model age in days */
296296
maxAgeDays?: number;
297+
/** Minimum context window size */
298+
minContextLength?: number;
299+
/** Allowed vendor prefixes (e.g., 'anthropic', 'openai') */
300+
allowedVendors?: string[];
301+
/** Substrings to exclude from model names (case-insensitive) */
302+
nameExclusions?: string[];
297303
/** Text search query */
298304
searchQuery?: string;
299-
/** Whether to show all models or just recommended */
300-
showAll?: boolean;
301305
}
302306

303307
/**

src/sidepanel/components/ModelSelector.vue

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,7 @@ const emit = defineEmits<{
1212
(e: 'update:modelValue', value: string): void;
1313
}>();
1414
15-
const {
16-
displayModels,
17-
filteredModels,
18-
isLoading,
19-
error,
20-
filterOptions,
21-
fetchModels,
22-
refreshModels,
23-
setSearchQuery,
24-
toggleShowAll,
25-
formatPrice,
26-
} = useModels();
15+
const { filteredModels, isLoading, error, fetchModels, refreshModels, setSearchQuery, formatPrice } = useModels();
2716
2817
const { config } = useConfig();
2918
@@ -40,10 +29,7 @@ const selectedModelId = computed(() => props.modelValue ?? config.value.selected
4029
4130
// Get selected model details
4231
const selectedModel = computed(() => {
43-
return (
44-
displayModels.value.find((m) => m.id === selectedModelId.value) ||
45-
filteredModels.value.find((m) => m.id === selectedModelId.value)
46-
);
32+
return filteredModels.value.find((m) => m.id === selectedModelId.value);
4733
});
4834
4935
// Cost tier color
@@ -165,17 +151,6 @@ onMounted(() => {
165151
</svg>
166152
</button>
167153
</div>
168-
169-
<!-- Show all toggle -->
170-
<label class="mt-2 flex items-center gap-2 text-xs">
171-
<input
172-
type="checkbox"
173-
:checked="filterOptions.showAll"
174-
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
175-
@change="toggleShowAll"
176-
/>
177-
<span class="text-gray-600">Show all models</span>
178-
</label>
179154
</div>
180155

181156
<!-- Error state -->
@@ -185,7 +160,7 @@ onMounted(() => {
185160
</div>
186161

187162
<!-- Loading state -->
188-
<div v-else-if="isLoading && displayModels.length === 0" class="p-4 text-center">
163+
<div v-else-if="isLoading && filteredModels.length === 0" class="p-4 text-center">
189164
<svg class="mx-auto h-5 w-5 animate-spin text-blue-600" fill="none" viewBox="0 0 24 24">
190165
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
191166
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
@@ -194,12 +169,12 @@ onMounted(() => {
194169
</div>
195170

196171
<!-- Empty state -->
197-
<div v-else-if="displayModels.length === 0" class="p-4 text-center text-sm text-gray-500">No models found</div>
172+
<div v-else-if="filteredModels.length === 0" class="p-4 text-center text-sm text-gray-500">No models found</div>
198173

199174
<!-- Model list -->
200175
<div v-else class="divide-y divide-gray-100">
201176
<button
202-
v-for="model in displayModels"
177+
v-for="model in filteredModels"
203178
:key="model.id"
204179
type="button"
205180
class="flex w-full items-start gap-3 px-3 py-2.5 text-left hover:bg-gray-50"

src/sidepanel/composables/useModels.ts

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import {
1010
RECOMMENDED_MODELS,
1111
DEFAULT_MAX_MODEL_PRICE,
1212
DEFAULT_MAX_MODEL_AGE_DAYS,
13+
DEFAULT_MIN_CONTEXT_LENGTH,
14+
DEFAULT_ALLOWED_VENDORS,
15+
DEFAULT_MODEL_NAME_EXCLUSIONS,
1316
COST_TIER_THRESHOLDS,
1417
} from '@shared/constants';
1518

@@ -24,17 +27,23 @@ const fromCache = ref(false);
2427
const filterOptions = ref<ModelFilterOptions>({
2528
maxPrice: DEFAULT_MAX_MODEL_PRICE,
2629
maxAgeDays: DEFAULT_MAX_MODEL_AGE_DAYS,
30+
minContextLength: DEFAULT_MIN_CONTEXT_LENGTH,
31+
allowedVendors: [...DEFAULT_ALLOWED_VENDORS],
32+
nameExclusions: [...DEFAULT_MODEL_NAME_EXCLUSIONS],
2733
searchQuery: '',
28-
showAll: false,
2934
});
3035

3136
/**
32-
* Calculate blended price (average of prompt and completion)
37+
* Calculate blended price per 1M tokens (weighted: 75% input, 25% output)
38+
* OpenRouter returns prices per token, so we multiply by 1M
3339
*/
3440
function calculateBlendedPrice(pricing: { prompt: string; completion: string }): number {
35-
const promptPrice = parseFloat(pricing.prompt) || 0;
36-
const completionPrice = parseFloat(pricing.completion) || 0;
37-
return (promptPrice + completionPrice) / 2;
41+
const inputPricePerToken = parseFloat(pricing.prompt) || 0;
42+
const outputPricePerToken = parseFloat(pricing.completion) || 0;
43+
// Convert to per 1M tokens and apply 3:1 weighting (input:output)
44+
const inputPer1M = inputPricePerToken * 1_000_000;
45+
const outputPer1M = outputPricePerToken * 1_000_000;
46+
return (3 * inputPer1M + outputPer1M) / 4;
3847
}
3948

4049
/**
@@ -62,12 +71,26 @@ export function useModels() {
6271
// Computed values
6372
const recommendedModels = computed(() => {
6473
const recommendedSet = new Set<string>(RECOMMENDED_MODELS);
65-
return models.value.filter((m) => recommendedSet.has(m.id));
74+
const filtered = models.value.filter((m) => recommendedSet.has(m.id));
75+
// Sort by RECOMMENDED_MODELS order
76+
const orderMap = new Map<string, number>(RECOMMENDED_MODELS.map((id, idx) => [id, idx]));
77+
return filtered.sort((a, b) => (orderMap.get(a.id) ?? Infinity) - (orderMap.get(b.id) ?? Infinity));
6678
});
6779

6880
const filteredModels = computed(() => {
6981
let result = [...models.value];
7082

83+
// Apply vendor filter
84+
if (filterOptions.value.allowedVendors?.length) {
85+
const vendors = filterOptions.value.allowedVendors;
86+
result = result.filter((m) => vendors.some((v) => m.id.startsWith(`${v}/`)));
87+
}
88+
89+
// Apply context length filter
90+
if (filterOptions.value.minContextLength !== undefined) {
91+
result = result.filter((m) => m.context_length >= (filterOptions.value.minContextLength ?? 0));
92+
}
93+
7194
// Apply price filter
7295
if (filterOptions.value.maxPrice !== undefined) {
7396
result = result.filter((m) => {
@@ -85,6 +108,16 @@ export function useModels() {
85108
});
86109
}
87110

111+
// Apply name exclusions filter
112+
if (filterOptions.value.nameExclusions?.length) {
113+
const exclusions = filterOptions.value.nameExclusions.map((e) => e.toLowerCase());
114+
result = result.filter((m) => {
115+
const nameLower = m.name.toLowerCase();
116+
const idLower = m.id.toLowerCase();
117+
return !exclusions.some((ex) => nameLower.includes(ex) || idLower.includes(ex));
118+
});
119+
}
120+
88121
// Apply search filter
89122
if (filterOptions.value.searchQuery) {
90123
const query = filterOptions.value.searchQuery.toLowerCase();
@@ -96,28 +129,27 @@ export function useModels() {
96129
);
97130
}
98131

99-
// Sort: recommended first, then by price
132+
// Sort: recommended first (in RECOMMENDED_MODELS order), then by release date (newest first)
133+
const recommendedOrder = new Map<string, number>(RECOMMENDED_MODELS.map((id, idx) => [id, idx]));
100134
result.sort((a, b) => {
101-
// Recommended models first
135+
// Recommended models first, preserving RECOMMENDED_MODELS order
102136
if (a.isRecommended && !b.isRecommended) return -1;
103137
if (!a.isRecommended && b.isRecommended) return 1;
138+
if (a.isRecommended && b.isRecommended) {
139+
const orderA = recommendedOrder.get(a.id) ?? Infinity;
140+
const orderB = recommendedOrder.get(b.id) ?? Infinity;
141+
return orderA - orderB;
142+
}
104143

105-
// Then by price (cheaper first)
106-
const priceA = calculateBlendedPrice(a.pricing);
107-
const priceB = calculateBlendedPrice(b.pricing);
108-
return priceA - priceB;
144+
// Non-recommended: sort by release date (newest first)
145+
const createdA = a.created ?? 0;
146+
const createdB = b.created ?? 0;
147+
return createdB - createdA;
109148
});
110149

111150
return result;
112151
});
113152

114-
const displayModels = computed(() => {
115-
if (filterOptions.value.showAll) {
116-
return filteredModels.value;
117-
}
118-
return recommendedModels.value;
119-
});
120-
121153
const hasRecommendedModels = computed(() => recommendedModels.value.length > 0);
122154

123155
/**
@@ -174,16 +206,6 @@ export function useModels() {
174206
filterOptions.value = { ...filterOptions.value, searchQuery: query };
175207
}
176208

177-
/**
178-
* Toggle show all models
179-
*/
180-
function toggleShowAll(): void {
181-
filterOptions.value = {
182-
...filterOptions.value,
183-
showAll: !filterOptions.value.showAll,
184-
};
185-
}
186-
187209
/**
188210
* Get model by ID
189211
*/
@@ -214,14 +236,11 @@ export function useModels() {
214236
}
215237

216238
/**
217-
* Format price for display
239+
* Format price for display (blended price per 1M tokens)
218240
*/
219241
function formatPrice(pricing: { prompt: string; completion: string }): string {
220242
const blendedPrice = calculateBlendedPrice(pricing);
221-
if (blendedPrice < 0.01) {
222-
return `$${(blendedPrice * 1000).toFixed(3)}/1K`;
223-
}
224-
return `$${blendedPrice.toFixed(4)}/1M`;
243+
return `$${blendedPrice.toFixed(2)}/1M`;
225244
}
226245

227246
return {
@@ -236,15 +255,13 @@ export function useModels() {
236255
// Computed
237256
recommendedModels,
238257
filteredModels,
239-
displayModels,
240258
hasRecommendedModels,
241259

242260
// Actions
243261
fetchModels,
244262
refreshModels,
245263
updateFilters,
246264
setSearchQuery,
247-
toggleShowAll,
248265
getModel,
249266
isValidModel,
250267
getModelDisplayInfo,

src/sidepanel/views/SettingsView.vue

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { useSettingsView } from '../composables/useSettingsView';
33
import ApiKeyInput from '../components/ApiKeyInput.vue';
44
import ExampleCommentsList from '../components/ExampleCommentsList.vue';
5-
import { RECOMMENDED_MODELS } from '@shared/constants';
5+
import ModelSelector from '../components/ModelSelector.vue';
66
77
const {
88
// Form state
@@ -228,14 +228,7 @@ const {
228228
<h2 class="mb-3 text-sm font-medium text-gray-900">AI Model</h2>
229229
<div>
230230
<label class="mb-1 block text-xs text-gray-500"> Selected Model </label>
231-
<select
232-
v-model="selectedModel"
233-
class="w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-blue-500 focus:ring-blue-500"
234-
>
235-
<option v-for="model in RECOMMENDED_MODELS" :key="model" :value="model">
236-
{{ model }}
237-
</option>
238-
</select>
231+
<ModelSelector v-model="selectedModel" />
239232
<p class="mt-1 text-xs text-gray-500">
240233
This model will be used for AI-powered matching and reply generation.
241234
</p>

tests/background/openrouter.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@ describe('OpenRouter API Client', () => {
154154

155155
describe('getValidRecommendedModels', () => {
156156
const mockModels: OpenRouterModel[] = [
157+
{
158+
id: 'anthropic/claude-sonnet-4.5',
159+
name: 'Claude Sonnet 4.5',
160+
context_length: 100000,
161+
pricing: { prompt: '0.003', completion: '0.015' },
162+
isRecommended: true,
163+
},
157164
{
158165
id: 'anthropic/claude-haiku-4.5',
159166
name: 'Claude Haiku 4.5',
@@ -168,13 +175,6 @@ describe('OpenRouter API Client', () => {
168175
pricing: { prompt: '0.03', completion: '0.06' },
169176
isRecommended: false,
170177
},
171-
{
172-
id: 'anthropic/claude-sonnet-4.5',
173-
name: 'Claude Sonnet 4.5',
174-
context_length: 100000,
175-
pricing: { prompt: '0.003', completion: '0.015' },
176-
isRecommended: true,
177-
},
178178
];
179179

180180
it('should return only recommended models', () => {

0 commit comments

Comments
 (0)