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

1from __future__ import annotations 

2 

3from copy import deepcopy 

4from dataclasses import dataclass 

5from decimal import Decimal 

6 

7import pytest 

8from inline_snapshot import snapshot 

9 

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 

13 

14 

15@dataclass 

16class CustomModelPrice(types.ModelPrice): 

17 sausage_price: Decimal | None = None 

18 

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 

24 

25 

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) 

54 

55 

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) 

74 

75 

76@dataclass 

77class CustomUsage: 

78 sausages: int 

79 

80 requests: int | None = None 

81 

82 input_tokens: int | None = None 

83 

84 cache_write_tokens: int | None = None 

85 cache_read_tokens: int | None = None 

86 

87 output_tokens: int | None = None 

88 

89 input_audio_tokens: int | None = None 

90 cache_audio_read_tokens: int | None = None 

91 output_audio_tokens: int | None = None 

92 

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 

100 

101 

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 

116 

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 ) 

123 

124 

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) 

141 

142 

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 

157 

158 

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