Coverage for tests/test_custom_prices.py: 98.85%
85 statements
« prev ^ index » next coverage.py v7.9.2, created at 2026-09-27 04:08 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2026-09-27 04:08 +0000
1from __future__ import annotations
3from copy import deepcopy
4from dataclasses import dataclass
5from decimal import Decimal
7import pytest
8from inline_snapshot import snapshot
10from voice_prices import Usage, calc_price, data, types
11from voice_prices.data_snapshot import DataSnapshot, set_custom_snapshot
12from voice_prices.update_prices import UpdatePrices
15@dataclass
16class CustomModelPrice(types.ModelPrice):
17 sausage_price: Decimal | None = None
19 def calc_price(self, usage: types.AbstractUsage) -> types.CalcPrice:
20 price = super().calc_price(usage)
21 if isinstance(usage, CustomUsage) and self.sausage_price is not None: 21 ↛ 23line 21 didn't jump to line 23 because the condition on line 21 was always true
22 price['total_price'] += self.sausage_price * usage.sausages
23 return price
26class AltUpdatePrices(UpdatePrices):
27 def fetch(self) -> DataSnapshot | None:
28 custom_providers = [
29 types.Provider(
30 id='testing',
31 name='Testing',
32 api_pattern=r'.*testing\.example\.com',
33 models=[
34 types.ModelInfo(
35 id='foobar',
36 match=types.ClauseEquals('foobar'),
37 name='Foobar',
38 prices=types.ModelPrice(input_mtok=Decimal('1'), output_mtok=Decimal('2')),
39 ),
40 types.ModelInfo(
41 id='sausage',
42 match=types.ClauseStartsWith('sausage'),
43 name='Sausage',
44 prices=CustomModelPrice(
45 input_mtok=Decimal('1'),
46 output_mtok=Decimal('2'),
47 sausage_price=Decimal('3'),
48 ),
49 ),
50 ],
51 )
52 ]
53 return DataSnapshot(providers=custom_providers, from_auto_update=False)
56class ExtraUpdatePrices(UpdatePrices):
57 def fetch(self) -> DataSnapshot | None:
58 providers = deepcopy(data.providers)
59 openai = next(provider for provider in providers if provider.id == 'openai')
60 openai.models.insert(
61 0,
62 types.ModelInfo(
63 id='gpt-4o',
64 match=types.ClauseStartsWith('gpt-4o'),
65 name='gpt-4o Custom',
66 prices=CustomModelPrice(
67 input_mtok=Decimal('1'),
68 output_mtok=Decimal('2'),
69 sausage_price=Decimal('3'),
70 ),
71 ),
72 )
73 return DataSnapshot(providers=providers, from_auto_update=False)
76@dataclass
77class CustomUsage:
78 sausages: int
80 requests: int | None = None
82 input_tokens: int | None = None
84 cache_write_tokens: int | None = None
85 cache_read_tokens: int | None = None
87 output_tokens: int | None = None
89 input_audio_tokens: int | None = None
90 cache_audio_read_tokens: int | None = None
91 output_audio_tokens: int | None = None
93 # TTS + STT fields added in v0.x / v0.0.7 of the voice-prices fork; included
94 # here so CustomUsage continues to satisfy the AbstractUsage protocol.
95 # Duration fields migrated from int to Decimal in v0.0.7.
96 characters: int | None = None
97 audio_output_seconds: Decimal | None = None
98 audio_input_seconds: Decimal | None = None
99 voice_class: str | None = None
102def test_alt_source():
103 with AltUpdatePrices() as update_prices:
104 update_prices.wait()
105 price = calc_price(
106 Usage(input_tokens=1_000_000, output_tokens=1_000_000),
107 model_ref='foobar',
108 provider_id='testing',
109 )
110 assert price.input_price == snapshot(Decimal('1'))
111 assert price.output_price == snapshot(Decimal('2'))
112 assert price.total_price == snapshot(Decimal('3'))
113 assert price.model.name == snapshot('Foobar')
114 assert price.provider.id == snapshot('testing')
115 assert price.auto_update_timestamp is None
117 with pytest.raises(LookupError, match="Unable to find provider provider_id='testing'"):
118 calc_price(
119 Usage(input_tokens=1_000_000, output_tokens=1_000_000),
120 model_ref='foobar',
121 provider_id='testing',
122 )
125def test_alt_source_sausage():
126 set_custom_snapshot(AltUpdatePrices().fetch())
127 try:
128 price = calc_price(
129 CustomUsage(sausages=3, input_tokens=1_000_000, output_tokens=1_000_000),
130 model_ref='sausage',
131 provider_id='testing',
132 )
133 assert price.input_price == snapshot(Decimal('1'))
134 assert price.output_price == snapshot(Decimal('2'))
135 assert price.total_price == snapshot(Decimal('12'))
136 assert price.model.name == snapshot('Sausage')
137 assert price.provider.id == snapshot('testing')
138 assert price.auto_update_timestamp is None
139 finally:
140 set_custom_snapshot(None)
143def test_extra_source_normal():
144 with ExtraUpdatePrices() as update_prices:
145 update_prices.wait()
146 price = calc_price(
147 CustomUsage(sausages=3, input_tokens=1_000_000, output_tokens=1_000_000),
148 model_ref='gpt-4',
149 provider_id='openai',
150 )
151 assert price.input_price == snapshot(Decimal('30'))
152 assert price.output_price == snapshot(Decimal('60'))
153 assert price.total_price == snapshot(Decimal('90'))
154 assert price.model.name == snapshot('gpt 4')
155 assert price.provider.id == snapshot('openai')
156 assert price.auto_update_timestamp is None
159def test_extra_source_sausage():
160 with ExtraUpdatePrices() as update_prices:
161 update_prices.wait()
162 price = calc_price(
163 CustomUsage(sausages=3, input_tokens=1_000_000, output_tokens=1_000_000),
164 model_ref='gpt-4o',
165 provider_id='openai',
166 )
167 assert price.input_price == snapshot(Decimal('1'))
168 assert price.output_price == snapshot(Decimal('2'))
169 assert price.total_price == snapshot(Decimal('12'))
170 assert price.model.name == snapshot('gpt-4o Custom')
171 assert price.provider.id == snapshot('openai')
172 assert price.auto_update_timestamp is None