Skip to content

Commit f969f86

Browse files
authored
feat(models): add isFree field to model metadata (#1094)
## Summary Add an `isFree` boolean field to the OpenRouter model metadata, computed using the existing `isFreeModel()` function. This makes each model's free status directly available in the API response rather than requiring clients to re-derive it. The field is placed next to `preferredIndex` in both the Zod schema and the `enhancedModelList` builder. ## Verification - [x] `pnpm typecheck` — passes with no errors - [x] Manual review of `isFree` values in approval files against `isFreeModel()` logic ## Visual Changes N/A ## Reviewer Notes - The `isFree` field is set via `isFreeModel(model.id)` in `enhancedModelList()`, so every model returned by the enhanced endpoint will have this field. - The Zod schema marks `isFree` as optional since raw OpenRouter models won't have it — only enhanced models will. - Both approval JSON files updated with expected `isFree` values matching the current `isFreeModel` logic.
2 parents 33e05b6 + 2d18e7c commit f969f86

6 files changed

Lines changed: 54 additions & 25 deletions

File tree

src/lib/kilo-auto-model.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -167,16 +167,17 @@ const BALANCED_MODE_TO_MODEL = new Map<string, ResolvedAutoModel>([
167167
['code', BALANCED_CODE_MODEL],
168168
]);
169169

170-
const legacyMapping: Record<string, string | undefined> = {
171-
'kilo/auto': KILO_AUTO_FRONTIER_MODEL.id,
172-
'kilo/auto-free': KILO_AUTO_FREE_MODEL.id,
173-
'kilo/auto-small': KILO_AUTO_SMALL_MODEL.id,
170+
export const KILO_AUTO_FREE_MODEL_DEPRECATED = 'kilo/auto-free';
171+
172+
const legacyMapping: Record<string, AutoModel | undefined> = {
173+
'kilo/auto': KILO_AUTO_FRONTIER_MODEL,
174+
[KILO_AUTO_FREE_MODEL_DEPRECATED]: KILO_AUTO_FREE_MODEL,
175+
'kilo/auto-small': KILO_AUTO_SMALL_MODEL,
174176
};
175177

176178
export function deprecatedAutoModelsToPreventNewExtensionModelPickerFromGettingStuck(): AutoModel[] {
177179
return Object.entries(legacyMapping)
178-
.map(([legacyId, currentId]) => {
179-
const model = AUTO_MODELS.find(m => m.id === currentId);
180+
.map(([legacyId, model]) => {
180181
if (!model) return null;
181182
return {
182183
...model,
@@ -189,7 +190,7 @@ export function deprecatedAutoModelsToPreventNewExtensionModelPickerFromGettingS
189190
}
190191

191192
export function resolveAutoModel(model: string, modeHeader: string | null): ResolvedAutoModel {
192-
const mappedModel = legacyMapping[model] ?? model;
193+
const mappedModel = legacyMapping[model]?.id ?? model;
193194
if (mappedModel === KILO_AUTO_FREE_MODEL.id) {
194195
return { model: minimax_m25_free_model.public_id };
195196
}

src/lib/models.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import {
66
KILO_AUTO_BALANCED_MODEL,
77
KILO_AUTO_FREE_MODEL,
8+
KILO_AUTO_FREE_MODEL_DEPRECATED,
89
KILO_AUTO_FRONTIER_MODEL,
910
} from '@/lib/kilo-auto-model';
1011
import {
@@ -43,6 +44,7 @@ export function isFreeModel(model: string): boolean {
4344
return (
4445
kiloFreeModels.some(m => m.public_id === model && m.is_enabled) ||
4546
model === KILO_AUTO_FREE_MODEL.id ||
47+
model === KILO_AUTO_FREE_MODEL_DEPRECATED ||
4648
(model ?? '').endsWith(':free') ||
4749
model === 'openrouter/free' ||
4850
isOpenRouterStealthModel(model ?? '')

src/lib/organizations/organization-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ export const OpenRouterProvidersResponseSchema = z.object({
183183
const OpenRouterModelSchema = z.object({
184184
// kilocode additions:
185185
preferredIndex: z.number().optional(),
186+
isFree: z.boolean().optional(),
186187
settings: ModelSettingsSchema.optional(),
187188
versioned_settings: VersionedSettingsSchema.optional(),
188189
opencode: OpenCodeSettingsSchema.optional(),

src/lib/providers/openrouter/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { kiloFreeModels, preferredModels } from '@/lib/models';
1+
import { isFreeModel, kiloFreeModels, preferredModels } from '@/lib/models';
22
import { PROVIDERS } from '@/lib/providers';
33
import type { OpenRouterModel } from '@/lib/organizations/organization-types';
44
import {
@@ -74,6 +74,7 @@ function enhancedModelList(models: OpenRouterModel[]) {
7474
...model,
7575
name: skipSuffix ? model.name : isNew ? model.name + ' (new)' : model.name,
7676
preferredIndex: preferredIndex >= 0 ? preferredIndex : undefined,
77+
isFree: isFreeModel(model.id),
7778
settings: model.settings ?? getModelSettings(model.id),
7879
versioned_settings: model.versioned_settings ?? getVersionedModelSettings(model.id),
7980
opencode: model.opencode ?? getOpenCodeSettings(model.id),

src/tests/openrouter-models-sorting.approved.json

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"family": "claude",
4141
"prompt": "anthropic"
4242
},
43-
"preferredIndex": 0
43+
"preferredIndex": 0,
44+
"isFree": false
4445
},
4546
{
4647
"id": "kilo-auto/balanced",
@@ -85,7 +86,8 @@
8586
"apply_diff"
8687
]
8788
},
88-
"preferredIndex": 1
89+
"preferredIndex": 1,
90+
"isFree": false
8991
},
9092
{
9193
"id": "kilo-auto/free",
@@ -131,7 +133,8 @@
131133
"edit_file"
132134
]
133135
},
134-
"preferredIndex": 2
136+
"preferredIndex": 2,
137+
"isFree": true
135138
},
136139
{
137140
"id": "minimax/minimax-m2.5:free",
@@ -176,6 +179,7 @@
176179
],
177180
"default_parameters": {},
178181
"preferredIndex": 4,
182+
"isFree": true,
179183
"settings": {
180184
"included_tools": [
181185
"search_and_replace"
@@ -230,6 +234,7 @@
230234
],
231235
"default_parameters": {},
232236
"preferredIndex": 6,
237+
"isFree": true,
233238
"versioned_settings": {
234239
"4.146.0": {
235240
"included_tools": [
@@ -270,6 +275,7 @@
270275
"internal_reasoning": "0"
271276
},
272277
"context_length": 200000,
278+
"isFree": false,
273279
"opencode": {
274280
"variants": {
275281
"none": {
@@ -336,6 +342,7 @@
336342
"internal_reasoning": "0"
337343
},
338344
"context_length": 200000,
345+
"isFree": false,
339346
"opencode": {
340347
"variants": {
341348
"none": {
@@ -402,6 +409,7 @@
402409
"internal_reasoning": "0"
403410
},
404411
"context_length": 1000000,
412+
"isFree": false,
405413
"versioned_settings": {
406414
"4.146.0": {
407415
"included_tools": [
@@ -442,6 +450,7 @@
442450
"internal_reasoning": "0"
443451
},
444452
"context_length": 128000,
453+
"isFree": false,
445454
"settings": {
446455
"included_tools": [
447456
"apply_patch"
@@ -515,7 +524,8 @@
515524
"web_search": "0",
516525
"internal_reasoning": "0"
517526
},
518-
"context_length": 32000
527+
"context_length": 32000,
528+
"isFree": false
519529
},
520530
{
521531
"id": "qwen/qwen3-coder",
@@ -544,7 +554,8 @@
544554
"web_search": "0",
545555
"internal_reasoning": "0"
546556
},
547-
"context_length": 32000
557+
"context_length": 32000,
558+
"isFree": false
548559
},
549560
{
550561
"id": "corethink:free",
@@ -584,7 +595,8 @@
584595
"temperature",
585596
"tools"
586597
],
587-
"default_parameters": {}
598+
"default_parameters": {},
599+
"isFree": true
588600
},
589601
{
590602
"id": "giga-potato",
@@ -627,6 +639,7 @@
627639
"tools"
628640
],
629641
"default_parameters": {},
642+
"isFree": true,
630643
"versioned_settings": {
631644
"4.146.0": {
632645
"included_tools": [
@@ -677,7 +690,8 @@
677690
"temperature",
678691
"tools"
679692
],
680-
"default_parameters": {}
693+
"default_parameters": {},
694+
"isFree": true
681695
},
682696
{
683697
"id": "x-ai/grok-code-fast-1:optimized:free",
@@ -720,7 +734,8 @@
720734
"reasoning",
721735
"include_reasoning"
722736
],
723-
"default_parameters": {}
737+
"default_parameters": {},
738+
"isFree": true
724739
},
725740
{
726741
"id": "kilo-auto/small",
@@ -761,7 +776,8 @@
761776
"opencode": {
762777
"family": "gpt",
763778
"prompt": "codex"
764-
}
779+
},
780+
"isFree": false
765781
},
766782
{
767783
"id": "kilo/auto",
@@ -802,7 +818,8 @@
802818
"opencode": {
803819
"family": "claude",
804820
"prompt": "anthropic"
805-
}
821+
},
822+
"isFree": false
806823
},
807824
{
808825
"id": "kilo/auto-free",
@@ -847,7 +864,8 @@
847864
"apply_diff",
848865
"edit_file"
849866
]
850-
}
867+
},
868+
"isFree": true
851869
},
852870
{
853871
"id": "kilo/auto-small",
@@ -888,7 +906,8 @@
888906
"opencode": {
889907
"family": "gpt",
890908
"prompt": "codex"
891-
}
909+
},
910+
"isFree": false
892911
}
893912
]
894913
}

src/tests/openrouter-models.test.approved.json

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"internal_reasoning": "0"
2525
},
2626
"context_length": 1000000,
27-
"preferredIndex": 0
27+
"preferredIndex": 0,
28+
"isFree": false
2829
},
2930
{
3031
"id": "anthropic/claude-sonnet-4",
@@ -50,7 +51,8 @@
5051
"internal_reasoning": "0"
5152
},
5253
"context_length": 200000,
53-
"preferredIndex": 2
54+
"preferredIndex": 2,
55+
"isFree": false
5456
},
5557
{
5658
"id": "anthropic/claude-3.7-sonnet",
@@ -76,7 +78,8 @@
7678
"internal_reasoning": "0"
7779
},
7880
"context_length": 200000,
79-
"preferredIndex": 3
81+
"preferredIndex": 3,
82+
"isFree": false
8083
},
8184
{
8285
"id": "openai/gpt-4.1",
@@ -102,7 +105,8 @@
102105
"internal_reasoning": "0"
103106
},
104107
"context_length": 128000,
105-
"preferredIndex": 5
108+
"preferredIndex": 5,
109+
"isFree": false
106110
},
107111
{
108112
"id": "some-other-model",
@@ -127,7 +131,8 @@
127131
"web_search": "0",
128132
"internal_reasoning": "0"
129133
},
130-
"context_length": 32000
134+
"context_length": 32000,
135+
"isFree": false
131136
}
132137
]
133138
}

0 commit comments

Comments
 (0)