Skip to content

Commit bfc7968

Browse files
feat(models): add pricing property to KiloExclusiveModel
- Add Pricing and Usage types to KiloExclusiveModel - Remove 'free' flag from KiloExclusiveModelFlag - pricing: null indicates a free model (replaces 'free' flag) - Update qwen3.6 model with tiered pricing (low/high tier based on 256k input threshold) - Update all existing models to use pricing: null - Update isKiloExclusiveFreeModel to check pricing === null - Update convertFromKiloExclusiveModel to expose pricing in metadata - Add tests for qwen pricing calculation
1 parent 2ccef24 commit bfc7968

File tree

14 files changed

+225
-36
lines changed

14 files changed

+225
-36
lines changed

apps/web/src/lib/models.test.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ describe('isFreeModel', () => {
2121
expect(isFreeModel('openrouter/sonoma-sky-beta')).toBe(true);
2222
});
2323

24-
test('should return true for enabled Kilo exclusive models with free flag', () => {
25-
// Test with known Kilo exclusive models that are enabled and have free flag
24+
test('should return true for enabled Kilo exclusive models with no pricing', () => {
25+
// Test with known Kilo exclusive models that are enabled and have no pricing (free)
2626
const enabledFreeModels = kiloExclusiveModels.filter(
27-
m => m.status === 'public' && m.flags.includes('free')
27+
m => m.status === 'public' && !m.pricing
2828
);
2929

3030
// Should have at least some enabled free models
@@ -36,20 +36,23 @@ describe('isFreeModel', () => {
3636
}
3737
});
3838

39-
test('should return true for all Kilo exclusive models', () => {
40-
// All kilo exclusive models currently have 'free' in their flags
41-
// This test ensures isFreeModel returns true for all of them
42-
for (const model of kiloExclusiveModels) {
43-
if (model.status !== 'disabled') {
44-
expect(isFreeModel(model.public_id)).toBe(true);
45-
}
39+
test('should return false for enabled Kilo exclusive models with pricing', () => {
40+
// Models with pricing should NOT be free
41+
const pricedModels = kiloExclusiveModels.filter(m => m.status !== 'disabled' && !!m.pricing);
42+
43+
for (const model of pricedModels) {
44+
expect(isFreeModel(model.public_id)).toBe(false);
4645
}
4746
});
4847

49-
test('all Kilo exclusive models should have free flag', () => {
50-
// Verify that all kilo exclusive models have the 'free' flag
48+
test('all Kilo exclusive models should have either no pricing or valid pricing', () => {
49+
// Verify that all kilo exclusive models have valid pricing structure
5150
for (const model of kiloExclusiveModels) {
52-
expect(model.flags.includes('free')).toBe(true);
51+
if (model.pricing) {
52+
expect(typeof model.pricing.prompt_per_million).toBe('number');
53+
expect(typeof model.pricing.completion_per_million).toBe('number');
54+
expect(typeof model.pricing.calculate_mUsd).toBe('function');
55+
}
5356
}
5457
});
5558

apps/web/src/lib/models.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { MINIMAX_CURRENT_MODEL_ID, minimax_m25_free_model } from '@/lib/provider
2121
import { KIMI_CURRENT_MODEL_ID } from '@/lib/providers/moonshotai';
2222
import { morph_warp_grep_free_model } from '@/lib/providers/morph';
2323
import { gpt_oss_20b_free_model } from '@/lib/providers/openai';
24-
import { qwen36_plus_free_model } from '@/lib/providers/qwen';
24+
import { qwen36_plus_model } from '@/lib/providers/qwen';
2525
import { grok_code_fast_1_optimized_free_model } from '@/lib/providers/xai';
2626
import { mimo_v2_omni_free_model, mimo_v2_pro_free_model } from '@/lib/providers/xiaomi';
2727

@@ -69,7 +69,7 @@ export function isFreeModel(model: string): boolean {
6969

7070
export function isKiloExclusiveFreeModel(model: string): boolean {
7171
return kiloExclusiveModels.some(
72-
m => m.public_id === model && m.status !== 'disabled' && m.flags.includes('free')
72+
m => m.public_id === model && m.status !== 'disabled' && !m.pricing
7373
);
7474
}
7575

@@ -85,7 +85,7 @@ export const kiloExclusiveModels = [
8585
morph_warp_grep_free_model,
8686
grok_code_fast_1_optimized_free_model,
8787
seed_20_pro_free_model,
88-
qwen36_plus_free_model,
88+
qwen36_plus_model,
8989
trinity_large_thinking_free_model,
9090
] as KiloExclusiveModel[];
9191

@@ -99,6 +99,6 @@ function isOpenRouterStealthModel(model: string): boolean {
9999

100100
export function isDeadFreeModel(model: string): boolean {
101101
return !!kiloExclusiveModels.find(
102-
m => m.public_id === model && m.status === 'disabled' && m.flags.includes('free')
102+
m => m.public_id === model && m.status === 'disabled' && !m.pricing
103103
);
104104
}

apps/web/src/lib/providers/arcee.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ export const trinity_large_thinking_free_model: KiloExclusiveModel = {
88
context_length: 262_144,
99
max_completion_tokens: 262_144,
1010
status: 'public',
11-
flags: ['free', 'reasoning'],
11+
flags: ['reasoning'],
1212
gateway: 'openrouter',
1313
internal_id: 'arcee-ai/trinity-large-thinking',
1414
inference_provider: 'arcee-ai',
15+
pricing: null,
1516
};

apps/web/src/lib/providers/bytedance.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ export const seed_20_pro_free_model: KiloExclusiveModel = {
88
context_length: 256_000,
99
max_completion_tokens: 128_000,
1010
status: 'public',
11-
flags: ['free', 'reasoning', 'prompt_cache', 'vision'],
11+
flags: ['reasoning', 'prompt_cache', 'vision'],
1212
gateway: 'bytedance',
1313
internal_id: 'seed-2-0-pro-260328',
1414
inference_provider: 'seed',
15+
pricing: null,
1516
};

apps/web/src/lib/providers/corethink.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ export const corethink_free_model: KiloExclusiveModel = {
88
context_length: 78_000,
99
max_completion_tokens: 8192,
1010
status: 'public',
11-
flags: ['free'],
11+
flags: [],
1212
gateway: 'corethink',
1313
internal_id: 'corethink',
1414
inference_provider: 'corethink',
15+
pricing: null,
1516
};

apps/web/src/lib/providers/kilo-exclusive-model.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
11
import type { OpenRouterInferenceProviderId } from '@/lib/providers/openrouter/inference-provider-id';
22
import type { ProviderId } from '@/lib/providers/types';
33

4-
export type KiloExclusiveModelFlag = 'free' | 'reasoning' | 'prompt_cache' | 'vision';
4+
export type KiloExclusiveModelFlag = 'reasoning' | 'prompt_cache' | 'vision';
5+
6+
export type Usage = {
7+
inputTokens: number;
8+
outputTokens: number;
9+
cacheWriteTokens: number;
10+
cacheHitTokens: number;
11+
};
12+
13+
export type Pricing = {
14+
prompt_per_million: number;
15+
completion_per_million: number;
16+
input_cache_read_per_million: number | null;
17+
input_cache_write_per_million: number | null;
18+
calculate_mUsd(usage: Usage, pricing: Pricing): number;
19+
};
520

621
export type KiloExclusiveModel = {
722
public_id: string;
@@ -14,9 +29,17 @@ export type KiloExclusiveModel = {
1429
gateway: ProviderId;
1530
internal_id: string;
1631
inference_provider: OpenRouterInferenceProviderId | null;
32+
pricing: Pricing | null;
1733
};
1834

35+
function formatPrice(price: number | null): string | undefined {
36+
if (price === null) return undefined;
37+
return price.toFixed(7);
38+
}
39+
1940
export function convertFromKiloExclusiveModel(model: KiloExclusiveModel) {
41+
const pricing = model.pricing;
42+
const isFree = !pricing;
2043
return {
2144
id: model.public_id,
2245
canonical_slug: model.public_id,
@@ -33,13 +56,21 @@ export function convertFromKiloExclusiveModel(model: KiloExclusiveModel) {
3356
instruct_type: null,
3457
},
3558
pricing: {
36-
prompt: '0.0000000',
37-
completion: '0.0000000',
59+
prompt: isFree
60+
? '0.0000000'
61+
: (formatPrice(pricing.prompt_per_million / 1000000) ?? '0.0000000'),
62+
completion: isFree
63+
? '0.0000000'
64+
: (formatPrice(pricing.completion_per_million / 1000000) ?? '0.0000000'),
3865
request: '0',
3966
image: '0',
4067
web_search: '0',
4168
internal_reasoning: '0',
42-
input_cache_read: model.flags.includes('prompt_cache') ? '0.00000000' : undefined,
69+
input_cache_read: model.flags.includes('prompt_cache')
70+
? isFree
71+
? '0.00000000'
72+
: (formatPrice(pricing.input_cache_read_per_million ?? 0 / 1000000) ?? '0.00000000')
73+
: undefined,
4374
},
4475
top_provider: {
4576
context_length: model.context_length,

apps/web/src/lib/providers/minimax.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ export const minimax_m25_free_model: KiloExclusiveModel = {
88
context_length: 204800,
99
max_completion_tokens: 131072,
1010
status: 'hidden', // usable through kilo-auto/free
11-
flags: ['free', 'reasoning', 'prompt_cache'],
11+
flags: ['reasoning', 'prompt_cache'],
1212
gateway: 'openrouter',
1313
internal_id: 'minimax/minimax-m2.5',
1414
inference_provider: null,
15+
pricing: null,
1516
};
1617

1718
export function isMinimaxModel(model: string) {

apps/web/src/lib/providers/morph.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ export const morph_warp_grep_free_model: KiloExclusiveModel = {
88
context_length: 256000,
99
max_completion_tokens: 32000,
1010
status: 'hidden',
11-
flags: ['free'],
11+
flags: [],
1212
gateway: 'morph',
1313
internal_id: 'morph-warp-grep-v2',
1414
inference_provider: 'morph',
15+
pricing: null,
1516
};

apps/web/src/lib/providers/openai.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ export const gpt_oss_20b_free_model: KiloExclusiveModel = {
2020
context_length: 131072,
2121
max_completion_tokens: 32768,
2222
status: 'hidden', // usable through kilo-auto
23-
flags: ['free', 'reasoning'],
23+
flags: ['reasoning'],
2424
gateway: 'openrouter',
2525
internal_id: 'openai/gpt-oss-20b',
2626
inference_provider: null,
27+
pricing: null,
2728
};

apps/web/src/lib/providers/openrouter/sync-providers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ async function syncProviders() {
265265
provider_model_id: model.id,
266266
quantization: null,
267267
variant: 'default',
268-
is_free: true,
268+
is_free: !kfm.pricing,
269269
can_abort: true,
270270
max_prompt_tokens: model.top_provider.context_length,
271271
max_completion_tokens: model.top_provider.max_completion_tokens,

0 commit comments

Comments
 (0)