unitysvc-core 0.2.27__tar.gz → 0.2.28__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {unitysvc_core-0.2.27/src/unitysvc_core.egg-info → unitysvc_core-0.2.28}/PKG-INFO +1 -1
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/pyproject.toml +1 -1
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/models/__init__.py +4 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/models/pricing.py +286 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28/src/unitysvc_core.egg-info}/PKG-INFO +1 -1
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core.egg-info/SOURCES.txt +1 -0
- unitysvc_core-0.2.28/tests/test_effective_price.py +284 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/LICENSE +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/README.md +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/setup.cfg +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/__init__.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/models/base.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/models/documents.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/models/listing_data.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/models/listing_v1.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/models/offering_data.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/models/offering_v1.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/models/promotion_data.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/models/provider_data.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/models/provider_v1.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/models/service.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/models/service_group_data.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/models/validators.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/py.typed +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/schema/base.json +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/schema/listing_v1.json +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/schema/offering_v1.json +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/schema/promotion_v1.json +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/schema/provider_v1.json +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/schema/service_group_v1.json +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/utils.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/validator.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core.egg-info/requires.txt +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core.egg-info/top_level.txt +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/tests/test_access_interface_routing.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/tests/test_hidden_path_skip.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/tests/test_offering_data.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/tests/test_pricing.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/tests/test_provider_account_rate_limits.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/tests/test_service_group.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/tests/test_service_status.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/tests/test_utils.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/tests/test_validator.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/tests/test_validators.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "unitysvc-core"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.28"
|
|
8
8
|
description = "Shared data models and validation helpers for UnitySVC packages"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Bo Peng", email = "bo.peng@unitysvc.com" }]
|
|
@@ -38,6 +38,7 @@ from .pricing import (
|
|
|
38
38
|
ConstantPriceData,
|
|
39
39
|
CountPriceData,
|
|
40
40
|
DataPriceData,
|
|
41
|
+
EffectivePrice,
|
|
41
42
|
ExprPriceData,
|
|
42
43
|
FirstPriceData,
|
|
43
44
|
GraduatedPriceData,
|
|
@@ -56,6 +57,7 @@ from .pricing import (
|
|
|
56
57
|
TimePriceData,
|
|
57
58
|
TokenPriceData,
|
|
58
59
|
UsageData,
|
|
60
|
+
upstream_price_from_description,
|
|
59
61
|
validate_pricing,
|
|
60
62
|
)
|
|
61
63
|
from .promotion_data import (
|
|
@@ -146,6 +148,8 @@ __all__ = [
|
|
|
146
148
|
"Pricing",
|
|
147
149
|
"validate_pricing",
|
|
148
150
|
"BasePriceData",
|
|
151
|
+
"EffectivePrice",
|
|
152
|
+
"upstream_price_from_description",
|
|
149
153
|
# Pricing — simple types
|
|
150
154
|
"TokenPriceData",
|
|
151
155
|
"TimePriceData",
|
|
@@ -13,6 +13,8 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import ast
|
|
15
15
|
import operator
|
|
16
|
+
import re
|
|
17
|
+
from collections.abc import Callable, Iterable
|
|
16
18
|
from decimal import Decimal, InvalidOperation
|
|
17
19
|
from typing import Annotated, Any, Literal
|
|
18
20
|
|
|
@@ -171,6 +173,175 @@ class UsageData(BaseModel):
|
|
|
171
173
|
one_million: float | None = None
|
|
172
174
|
|
|
173
175
|
|
|
176
|
+
# ============================================================================
|
|
177
|
+
# Effective price — a per-unit comparison figure, never billed
|
|
178
|
+
# ============================================================================
|
|
179
|
+
|
|
180
|
+
#: Normalized units an effective price is expressed in. Each dimension has one,
|
|
181
|
+
#: so figures of the same dimension always compare directly.
|
|
182
|
+
UNIT_TOKENS = "one_million_tokens"
|
|
183
|
+
UNIT_REQUEST = "request"
|
|
184
|
+
UNIT_TIME = "one_hour"
|
|
185
|
+
UNIT_DATA = "one_gigabyte"
|
|
186
|
+
UNIT_CHARACTERS = "one_million_characters"
|
|
187
|
+
UNIT_IMAGE = "image"
|
|
188
|
+
UNIT_STEP = "step"
|
|
189
|
+
|
|
190
|
+
#: Each measured equivalence group's normalized unit (counts compare per request).
|
|
191
|
+
_GROUP_UNIT = {
|
|
192
|
+
"tokens": UNIT_TOKENS,
|
|
193
|
+
"time": UNIT_TIME,
|
|
194
|
+
"data": UNIT_DATA,
|
|
195
|
+
"characters": UNIT_CHARACTERS,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#: When figures in different units must be reduced to one (``max``, ``min``,
|
|
199
|
+
#: ``tiered``, ``channel``), the first unit present in this order wins; any
|
|
200
|
+
#: other unit follows alphabetically.
|
|
201
|
+
_UNIT_PRIORITY = (UNIT_TOKENS, UNIT_REQUEST)
|
|
202
|
+
|
|
203
|
+
#: Output tokens weigh 4x input: typical chat traffic, and the platform's
|
|
204
|
+
#: long-standing blend convention.
|
|
205
|
+
_OUTPUT_WEIGHT = Decimal(4)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class EffectivePrice(BaseModel):
|
|
209
|
+
"""What using a service realistically costs per unit, for comparison.
|
|
210
|
+
|
|
211
|
+
A best-effort estimate for sorting, filtering and routing — never billed.
|
|
212
|
+
``price`` (when set) is what is charged; this is computed from the pricing
|
|
213
|
+
object by ``effective_price()`` and never written back into it.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
217
|
+
|
|
218
|
+
amount: Decimal = Field(description="Cost per one ``unit``")
|
|
219
|
+
unit: str = Field(
|
|
220
|
+
description="Normalized unit: 'one_million_tokens', 'request', 'one_hour', 'one_gigabyte', "
|
|
221
|
+
"'one_million_characters', 'image' or 'step'",
|
|
222
|
+
)
|
|
223
|
+
source: Literal["rate", "upstream"] = Field(
|
|
224
|
+
default="rate",
|
|
225
|
+
description="'rate': from the price itself. 'upstream': a $0 price whose description states what the "
|
|
226
|
+
"customer pays the upstream provider (bring-your-own-key).",
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _blend(tokens_in: Decimal, tokens_out: Decimal) -> Decimal:
|
|
231
|
+
return (tokens_in + _OUTPUT_WEIGHT * tokens_out) / (1 + _OUTPUT_WEIGHT)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _unit_rank(unit: str) -> tuple[int, str]:
|
|
235
|
+
return (_UNIT_PRIORITY.index(unit), "") if unit in _UNIT_PRIORITY else (len(_UNIT_PRIORITY), unit)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _child_effective_price(price_data: Any) -> EffectivePrice | None:
|
|
239
|
+
"""The effective price of a nested pricing dict; None if it is unknown or invalid."""
|
|
240
|
+
if not isinstance(price_data, dict):
|
|
241
|
+
return None
|
|
242
|
+
try:
|
|
243
|
+
return validate_pricing(price_data).effective_price()
|
|
244
|
+
except Exception:
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _pick(
|
|
249
|
+
figures: Iterable[EffectivePrice | None],
|
|
250
|
+
choose: Callable[[Iterable[EffectivePrice]], EffectivePrice],
|
|
251
|
+
) -> EffectivePrice | None:
|
|
252
|
+
"""Reduce known figures to one, comparing only within the dominant unit."""
|
|
253
|
+
known = [f for f in figures if f is not None]
|
|
254
|
+
if not known:
|
|
255
|
+
return None
|
|
256
|
+
unit = min((f.unit for f in known), key=_unit_rank)
|
|
257
|
+
return choose(f for f in known if f.unit == unit)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _largest(figures: Iterable[EffectivePrice | None]) -> EffectivePrice | None:
|
|
261
|
+
return _pick(figures, lambda fs: max(fs, key=lambda f: f.amount))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _smallest(figures: Iterable[EffectivePrice | None]) -> EffectivePrice | None:
|
|
265
|
+
return _pick(figures, lambda fs: min(fs, key=lambda f: f.amount))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _rate(amount: Decimal, unit: str) -> EffectivePrice:
|
|
269
|
+
return EffectivePrice(amount=amount, unit=unit)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _per_normalized_unit(metric: str, unit_price: Decimal) -> EffectivePrice | None:
|
|
273
|
+
"""Express a price per one ``metric`` in the metric dimension's normalized unit.
|
|
274
|
+
|
|
275
|
+
``metric`` is a pricing type or a ``based_on`` name: an equivalence-group
|
|
276
|
+
unit, a token counter, ``request_count``, or a gateway byte counter. Any
|
|
277
|
+
other metric (``customer_charge``, an expression) has no unit: None.
|
|
278
|
+
"""
|
|
279
|
+
if metric in ("input_tokens", "output_tokens", "cached_input_tokens", "total_tokens"):
|
|
280
|
+
metric = "one_token"
|
|
281
|
+
elif metric in ("bytes_in", "bytes_out", "bytes_total"):
|
|
282
|
+
metric = "one_byte"
|
|
283
|
+
if metric == "request_count":
|
|
284
|
+
return _rate(unit_price, UNIT_REQUEST)
|
|
285
|
+
if metric == "images_generated":
|
|
286
|
+
return _rate(unit_price, UNIT_IMAGE)
|
|
287
|
+
if metric not in _UNIT_TO_GROUP:
|
|
288
|
+
return None
|
|
289
|
+
group, factor = _UNIT_TO_GROUP[metric]
|
|
290
|
+
if group == "count":
|
|
291
|
+
# One count is one call, event or message: compared per request.
|
|
292
|
+
return _rate(unit_price / factor, UNIT_REQUEST)
|
|
293
|
+
target = _GROUP_UNIT[group]
|
|
294
|
+
return _rate(unit_price * EQUIVALENCE_GROUPS[group][target] / factor, target)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
_DOLLARS = r"\$\s*\d+(?:\.\d+)?"
|
|
298
|
+
# "$1.4 / $4.4 per 1M input/output tokens", "$1.518/$4.554 / 1M input/output
|
|
299
|
+
# tokens", "$4 / $20 / $0.2 per 1M input/output/cached tokens",
|
|
300
|
+
# "$3.45/$0.345/$17.25 / 1M in/cached/out tokens", "$0.50 / 1M tokens".
|
|
301
|
+
_UPSTREAM_TOKEN_PRICE = re.compile(
|
|
302
|
+
rf"(?P<amounts>{_DOLLARS}(?:\s*/\s*{_DOLLARS})*)\s*(?:/|per)\s*1\s*(?P<scale>[MK])\s+"
|
|
303
|
+
r"(?:(?P<labels>[a-z_]+(?:\s*/\s*[a-z_]+)*)\s+)?tokens\b",
|
|
304
|
+
re.IGNORECASE,
|
|
305
|
+
)
|
|
306
|
+
_UPSTREAM_LABELS = {
|
|
307
|
+
"input": "in",
|
|
308
|
+
"in": "in",
|
|
309
|
+
"output": "out",
|
|
310
|
+
"out": "out",
|
|
311
|
+
"cached": "cached",
|
|
312
|
+
"cache": "cached",
|
|
313
|
+
"cached_input": "cached",
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def upstream_price_from_description(text: str | None) -> Decimal | None:
|
|
318
|
+
"""The upstream price per 1M tokens a description states, blended; or None.
|
|
319
|
+
|
|
320
|
+
Bring-your-own-key prices are ``0`` (we bill nothing) and their generators
|
|
321
|
+
write what the provider charges into the description, e.g. "billed by
|
|
322
|
+
Hugging Face directly at $1.4 / $4.4 per 1M input/output tokens".
|
|
323
|
+
|
|
324
|
+
Fails closed: text not in a known shape (labels that don't match the
|
|
325
|
+
amounts, no input/output pair, no token unit) is None, never 0.
|
|
326
|
+
"""
|
|
327
|
+
if not text:
|
|
328
|
+
return None
|
|
329
|
+
match = _UPSTREAM_TOKEN_PRICE.search(text)
|
|
330
|
+
if match is None:
|
|
331
|
+
return None
|
|
332
|
+
amounts = [Decimal(a) for a in re.findall(r"\d+(?:\.\d+)?", match["amounts"])]
|
|
333
|
+
scale = Decimal(1000) if match["scale"].upper() == "K" else Decimal(1)
|
|
334
|
+
if not match["labels"]:
|
|
335
|
+
return amounts[0] * scale if len(amounts) == 1 else None
|
|
336
|
+
labels = [_UPSTREAM_LABELS.get(label.strip().lower()) for label in match["labels"].split("/")]
|
|
337
|
+
if len(labels) != len(amounts) or None in labels:
|
|
338
|
+
return None
|
|
339
|
+
by_label = dict(zip(labels, amounts, strict=True))
|
|
340
|
+
if "in" not in by_label or "out" not in by_label:
|
|
341
|
+
return None
|
|
342
|
+
return _blend(by_label["in"], by_label["out"]) * scale
|
|
343
|
+
|
|
344
|
+
|
|
174
345
|
# ============================================================================
|
|
175
346
|
# Pricing Models - Discriminated Union for type-safe pricing validation
|
|
176
347
|
# ============================================================================
|
|
@@ -207,6 +378,15 @@ class BasePriceData(BaseModel):
|
|
|
207
378
|
description="URL to upstream provider's pricing page",
|
|
208
379
|
)
|
|
209
380
|
|
|
381
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
382
|
+
"""What using this price realistically costs per unit, or None if unknown.
|
|
383
|
+
|
|
384
|
+
A comparison figure for sorting, filtering and routing; never billed.
|
|
385
|
+
Every pricing type defines it, so a composite price computes its figure
|
|
386
|
+
from its children. Unknown is never 0. Never raises.
|
|
387
|
+
"""
|
|
388
|
+
return None
|
|
389
|
+
|
|
210
390
|
|
|
211
391
|
class TokenPriceData(BasePriceData):
|
|
212
392
|
"""
|
|
@@ -294,6 +474,16 @@ class TokenPriceData(BasePriceData):
|
|
|
294
474
|
"""Token divisor based on the pricing type."""
|
|
295
475
|
return {"one_million_tokens": 1_000_000, "one_thousand_tokens": 1_000, "one_token": 1}[self.type]
|
|
296
476
|
|
|
477
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
478
|
+
"""``(input + 4*output) / 5`` when priced separately, else ``price``; per 1M tokens."""
|
|
479
|
+
if self.input is not None and self.output is not None:
|
|
480
|
+
per_unit = _blend(Decimal(self.input), Decimal(self.output))
|
|
481
|
+
elif self.price is not None:
|
|
482
|
+
per_unit = Decimal(self.price)
|
|
483
|
+
else:
|
|
484
|
+
return None
|
|
485
|
+
return _per_normalized_unit(self.type, per_unit)
|
|
486
|
+
|
|
297
487
|
def calculate_cost(
|
|
298
488
|
self,
|
|
299
489
|
usage: UsageData,
|
|
@@ -361,6 +551,10 @@ class TimePriceData(BasePriceData):
|
|
|
361
551
|
description="Price per one unit of the specified type",
|
|
362
552
|
)
|
|
363
553
|
|
|
554
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
555
|
+
"""``price`` in the dimension's normalized unit."""
|
|
556
|
+
return _per_normalized_unit(self.type, Decimal(self.price))
|
|
557
|
+
|
|
364
558
|
def calculate_cost(
|
|
365
559
|
self,
|
|
366
560
|
usage: UsageData,
|
|
@@ -402,6 +596,10 @@ class ImagePriceData(BasePriceData):
|
|
|
402
596
|
description="Price per image",
|
|
403
597
|
)
|
|
404
598
|
|
|
599
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
600
|
+
"""``price`` per image."""
|
|
601
|
+
return _rate(Decimal(self.price), UNIT_IMAGE)
|
|
602
|
+
|
|
405
603
|
def calculate_cost(
|
|
406
604
|
self,
|
|
407
605
|
usage: UsageData,
|
|
@@ -442,6 +640,10 @@ class StepPriceData(BasePriceData):
|
|
|
442
640
|
description="Price per step/iteration",
|
|
443
641
|
)
|
|
444
642
|
|
|
643
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
644
|
+
"""``price`` per step."""
|
|
645
|
+
return _rate(Decimal(self.price), UNIT_STEP)
|
|
646
|
+
|
|
445
647
|
def calculate_cost(
|
|
446
648
|
self,
|
|
447
649
|
usage: UsageData,
|
|
@@ -495,6 +697,10 @@ class DataPriceData(BasePriceData):
|
|
|
495
697
|
description="Price per one unit of the specified type",
|
|
496
698
|
)
|
|
497
699
|
|
|
700
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
701
|
+
"""``price`` in the dimension's normalized unit."""
|
|
702
|
+
return _per_normalized_unit(self.type, Decimal(self.price))
|
|
703
|
+
|
|
498
704
|
def calculate_cost(
|
|
499
705
|
self,
|
|
500
706
|
usage: UsageData,
|
|
@@ -556,6 +762,10 @@ class CountPriceData(BasePriceData):
|
|
|
556
762
|
description="Price per one unit of the specified type",
|
|
557
763
|
)
|
|
558
764
|
|
|
765
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
766
|
+
"""``price`` in the dimension's normalized unit."""
|
|
767
|
+
return _per_normalized_unit(self.type, Decimal(self.price))
|
|
768
|
+
|
|
559
769
|
def calculate_cost(
|
|
560
770
|
self,
|
|
561
771
|
usage: UsageData,
|
|
@@ -602,6 +812,10 @@ class CharacterPriceData(BasePriceData):
|
|
|
602
812
|
description="Price per one unit of the specified type",
|
|
603
813
|
)
|
|
604
814
|
|
|
815
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
816
|
+
"""``price`` in the dimension's normalized unit."""
|
|
817
|
+
return _per_normalized_unit(self.type, Decimal(self.price))
|
|
818
|
+
|
|
605
819
|
def calculate_cost(
|
|
606
820
|
self,
|
|
607
821
|
usage: UsageData,
|
|
@@ -670,6 +884,10 @@ class RevenueSharePriceData(BasePriceData):
|
|
|
670
884
|
description="Percentage of customer charge that goes to the seller (0-100)",
|
|
671
885
|
)
|
|
672
886
|
|
|
887
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
888
|
+
"""A share of the customer charge has no per-unit rate: unknown."""
|
|
889
|
+
return None
|
|
890
|
+
|
|
673
891
|
def calculate_cost(
|
|
674
892
|
self,
|
|
675
893
|
usage: UsageData,
|
|
@@ -710,6 +928,23 @@ class ConstantPriceData(BasePriceData):
|
|
|
710
928
|
description="Fixed price (positive for charge, negative for discount)",
|
|
711
929
|
)
|
|
712
930
|
|
|
931
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
932
|
+
"""``price`` per request.
|
|
933
|
+
|
|
934
|
+
A ``0`` with a description is a bring-your-own-key price: the customer
|
|
935
|
+
pays the upstream provider, at the rate the description states. A
|
|
936
|
+
description that states no parseable rate, or $0/$0 (what generators
|
|
937
|
+
write when they lack the provider's price), is unknown. A ``0`` with no
|
|
938
|
+
description is genuinely free.
|
|
939
|
+
"""
|
|
940
|
+
amount = Decimal(self.price)
|
|
941
|
+
if amount != 0 or not self.description:
|
|
942
|
+
return _rate(amount, UNIT_REQUEST)
|
|
943
|
+
upstream = upstream_price_from_description(self.description)
|
|
944
|
+
if upstream is None or upstream == 0:
|
|
945
|
+
return None
|
|
946
|
+
return EffectivePrice(amount=upstream, unit=UNIT_TOKENS, source="upstream")
|
|
947
|
+
|
|
713
948
|
def calculate_cost(
|
|
714
949
|
self,
|
|
715
950
|
usage: UsageData,
|
|
@@ -783,6 +1018,15 @@ class AddPriceData(BasePriceData):
|
|
|
783
1018
|
self.price = _extract_nominal_price(self.prices[0])
|
|
784
1019
|
return self
|
|
785
1020
|
|
|
1021
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1022
|
+
"""The sum of the children; unknown if any child is unknown or the units differ."""
|
|
1023
|
+
figures = [_child_effective_price(p) for p in self.prices]
|
|
1024
|
+
known = [f for f in figures if f is not None]
|
|
1025
|
+
if len(known) != len(figures) or len({f.unit for f in known}) != 1:
|
|
1026
|
+
return None
|
|
1027
|
+
source: Literal["rate", "upstream"] = "upstream" if any(f.source == "upstream" for f in known) else "rate"
|
|
1028
|
+
return EffectivePrice(amount=sum((f.amount for f in known), Decimal(0)), unit=known[0].unit, source=source)
|
|
1029
|
+
|
|
786
1030
|
def calculate_cost(
|
|
787
1031
|
self,
|
|
788
1032
|
usage: UsageData,
|
|
@@ -843,6 +1087,13 @@ class MultiplyPriceData(BasePriceData):
|
|
|
843
1087
|
self.price = base_price
|
|
844
1088
|
return self
|
|
845
1089
|
|
|
1090
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1091
|
+
"""The base's figure times ``factor``."""
|
|
1092
|
+
base = _child_effective_price(self.base)
|
|
1093
|
+
if base is None:
|
|
1094
|
+
return None
|
|
1095
|
+
return base.model_copy(update={"amount": base.amount * Decimal(self.factor)})
|
|
1096
|
+
|
|
846
1097
|
def calculate_cost(
|
|
847
1098
|
self,
|
|
848
1099
|
usage: UsageData,
|
|
@@ -897,6 +1148,10 @@ class MaxPriceData(BasePriceData):
|
|
|
897
1148
|
self.price = _extract_nominal_price(self.prices[0])
|
|
898
1149
|
return self
|
|
899
1150
|
|
|
1151
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1152
|
+
"""The largest child, as billed."""
|
|
1153
|
+
return _largest(_child_effective_price(p) for p in self.prices)
|
|
1154
|
+
|
|
900
1155
|
def calculate_cost(
|
|
901
1156
|
self,
|
|
902
1157
|
usage: UsageData,
|
|
@@ -955,6 +1210,10 @@ class MinPriceData(BasePriceData):
|
|
|
955
1210
|
self.price = _extract_nominal_price(self.prices[0])
|
|
956
1211
|
return self
|
|
957
1212
|
|
|
1213
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1214
|
+
"""The smallest child, as billed."""
|
|
1215
|
+
return _smallest(_child_effective_price(p) for p in self.prices)
|
|
1216
|
+
|
|
958
1217
|
def calculate_cost(
|
|
959
1218
|
self,
|
|
960
1219
|
usage: UsageData,
|
|
@@ -1012,6 +1271,14 @@ class FirstPriceData(BasePriceData):
|
|
|
1012
1271
|
self.price = _extract_nominal_price(self.prices[0])
|
|
1013
1272
|
return self
|
|
1014
1273
|
|
|
1274
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1275
|
+
"""The first child with a known figure, mirroring the billing order."""
|
|
1276
|
+
for price_data in self.prices:
|
|
1277
|
+
figure = _child_effective_price(price_data)
|
|
1278
|
+
if figure is not None:
|
|
1279
|
+
return figure
|
|
1280
|
+
return None
|
|
1281
|
+
|
|
1015
1282
|
def calculate_cost(
|
|
1016
1283
|
self,
|
|
1017
1284
|
usage: UsageData,
|
|
@@ -1246,6 +1513,10 @@ class ExprPriceData(BasePriceData):
|
|
|
1246
1513
|
description="Arithmetic expression using usage metrics (e.g., 'input_tokens / 1000000 * 2.5')",
|
|
1247
1514
|
)
|
|
1248
1515
|
|
|
1516
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1517
|
+
"""An arbitrary expression has no per-unit rate: unknown."""
|
|
1518
|
+
return None
|
|
1519
|
+
|
|
1249
1520
|
def calculate_cost(
|
|
1250
1521
|
self,
|
|
1251
1522
|
usage: UsageData,
|
|
@@ -1320,6 +1591,10 @@ class TieredPriceData(BasePriceData):
|
|
|
1320
1591
|
self.price = _extract_nominal_price(first_tier.price)
|
|
1321
1592
|
return self
|
|
1322
1593
|
|
|
1594
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1595
|
+
"""The largest tier's figure: the worst case a customer can pay."""
|
|
1596
|
+
return _largest(_child_effective_price(tier.price) for tier in self.tiers)
|
|
1597
|
+
|
|
1323
1598
|
def calculate_cost(
|
|
1324
1599
|
self,
|
|
1325
1600
|
usage: UsageData,
|
|
@@ -1403,6 +1678,13 @@ class GraduatedPriceData(BasePriceData):
|
|
|
1403
1678
|
self.price = self.tiers[0].unit_price
|
|
1404
1679
|
return self
|
|
1405
1680
|
|
|
1681
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1682
|
+
"""The largest ``unit_price`` (the worst case), per ``based_on``'s normalized unit.
|
|
1683
|
+
|
|
1684
|
+
Unknown when ``based_on`` has no unit (``customer_charge``, an expression).
|
|
1685
|
+
"""
|
|
1686
|
+
return _per_normalized_unit(self.based_on, max(Decimal(tier.unit_price) for tier in self.tiers))
|
|
1687
|
+
|
|
1406
1688
|
def calculate_cost(
|
|
1407
1689
|
self,
|
|
1408
1690
|
usage: UsageData,
|
|
@@ -1526,6 +1808,10 @@ class ChannelPriceData(BasePriceData):
|
|
|
1526
1808
|
self.description = f"{lo} - {hi}"
|
|
1527
1809
|
return self
|
|
1528
1810
|
|
|
1811
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1812
|
+
"""The largest figure among all channels: the worst case a customer can pay."""
|
|
1813
|
+
return _largest(_child_effective_price(p) for p in self.channels.values())
|
|
1814
|
+
|
|
1529
1815
|
def calculate_cost(
|
|
1530
1816
|
self,
|
|
1531
1817
|
usage: UsageData,
|
|
@@ -31,6 +31,7 @@ src/unitysvc_core/schema/promotion_v1.json
|
|
|
31
31
|
src/unitysvc_core/schema/provider_v1.json
|
|
32
32
|
src/unitysvc_core/schema/service_group_v1.json
|
|
33
33
|
tests/test_access_interface_routing.py
|
|
34
|
+
tests/test_effective_price.py
|
|
34
35
|
tests/test_hidden_path_skip.py
|
|
35
36
|
tests/test_offering_data.py
|
|
36
37
|
tests/test_pricing.py
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Tests for ``effective_price()``: the per-unit comparison figure every pricing type defines."""
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from unitysvc_core.models.pricing import (
|
|
10
|
+
BasePriceData,
|
|
11
|
+
EffectivePrice,
|
|
12
|
+
Pricing,
|
|
13
|
+
upstream_price_from_description,
|
|
14
|
+
validate_pricing,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def ep(data: dict[str, Any]) -> EffectivePrice | None:
|
|
19
|
+
return validate_pricing(data).effective_price()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def rate(amount: str, unit: str, source: str = "rate") -> EffectivePrice:
|
|
23
|
+
return EffectivePrice(amount=Decimal(amount), unit=unit, source=source) # type: ignore[arg-type]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
TOKENS = {"type": "one_million_tokens", "input": "1", "output": "2"} # blend 1.8
|
|
27
|
+
CHEAP_TOKENS = {"type": "one_million_tokens", "price": "0.5"}
|
|
28
|
+
PER_REQUEST = {"type": "constant", "price": "0.01"}
|
|
29
|
+
HF_UPSTREAM = "billed by Hugging Face directly at $1.4 / $4.4 per 1M input/output tokens"
|
|
30
|
+
BYOK = {"type": "constant", "price": "0", "description": HF_UPSTREAM}
|
|
31
|
+
NO_AMOUNT = {"type": "constant", "price": "0", "description": "Bring your own key"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_every_pricing_type_defines_its_own_effective_price() -> None:
|
|
35
|
+
members = typing.get_args(typing.get_args(Pricing)[0])
|
|
36
|
+
assert len(members) == 18
|
|
37
|
+
for cls in members:
|
|
38
|
+
assert "effective_price" in vars(cls), f"{cls.__name__} does not define effective_price()"
|
|
39
|
+
assert BasePriceData().effective_price() is None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TestSimpleTypes:
|
|
43
|
+
def test_token_split_is_blended(self) -> None:
|
|
44
|
+
assert ep({"type": "one_million_tokens", "input": "1.4", "output": "4.4"}) == rate("3.8", "one_million_tokens")
|
|
45
|
+
|
|
46
|
+
def test_token_split_ignores_cached_input(self) -> None:
|
|
47
|
+
figure = ep({"type": "one_million_tokens", "input": "1", "cached_input": "0.1", "output": "2"})
|
|
48
|
+
assert figure == rate("1.8", "one_million_tokens")
|
|
49
|
+
|
|
50
|
+
def test_token_unified(self) -> None:
|
|
51
|
+
assert ep(CHEAP_TOKENS) == rate("0.5", "one_million_tokens")
|
|
52
|
+
|
|
53
|
+
@pytest.mark.parametrize(
|
|
54
|
+
("kind", "price", "per_million"),
|
|
55
|
+
[("one_thousand_tokens", "0.002", "2"), ("one_token", "0.000003", "3")],
|
|
56
|
+
)
|
|
57
|
+
def test_token_scales_normalize_to_per_million(self, kind: str, price: str, per_million: str) -> None:
|
|
58
|
+
assert ep({"type": kind, "price": price}) == rate(per_million, "one_million_tokens")
|
|
59
|
+
|
|
60
|
+
def test_count_is_per_request(self) -> None:
|
|
61
|
+
assert ep({"type": "one_thousand", "price": "10"}) == rate("0.01", "request")
|
|
62
|
+
assert ep({"type": "one_million", "price": "2"}) == rate("0.000002", "request")
|
|
63
|
+
|
|
64
|
+
@pytest.mark.parametrize(
|
|
65
|
+
("kind", "price", "per_hour"),
|
|
66
|
+
[
|
|
67
|
+
("one_second", "0.001", "3.6"),
|
|
68
|
+
("one_minute", "0.006", "0.36"),
|
|
69
|
+
("one_hour", "2", "2"),
|
|
70
|
+
("one_day", "24", "1"),
|
|
71
|
+
],
|
|
72
|
+
)
|
|
73
|
+
def test_time_normalizes_to_per_hour(self, kind: str, price: str, per_hour: str) -> None:
|
|
74
|
+
assert ep({"type": kind, "price": price}) == rate(per_hour, "one_hour")
|
|
75
|
+
|
|
76
|
+
def test_data_normalizes_to_per_gigabyte(self) -> None:
|
|
77
|
+
assert ep({"type": "one_megabyte", "price": "0.001"}) == rate("1.024", "one_gigabyte")
|
|
78
|
+
assert ep({"type": "one_gigabyte", "price": "0.09"}) == rate("0.09", "one_gigabyte")
|
|
79
|
+
|
|
80
|
+
def test_characters_normalize_to_per_million(self) -> None:
|
|
81
|
+
assert ep({"type": "one_thousand_characters", "price": "0.015"}) == rate("15", "one_million_characters")
|
|
82
|
+
|
|
83
|
+
def test_image_and_step(self) -> None:
|
|
84
|
+
assert ep({"type": "image", "price": "0.04"}) == rate("0.04", "image")
|
|
85
|
+
assert ep({"type": "step", "price": "0.001"}) == rate("0.001", "step")
|
|
86
|
+
|
|
87
|
+
def test_constant_is_per_request(self) -> None:
|
|
88
|
+
assert ep(PER_REQUEST) == rate("0.01", "request")
|
|
89
|
+
|
|
90
|
+
def test_revenue_share_and_expr_are_unknown(self) -> None:
|
|
91
|
+
assert ep({"type": "revenue_share", "percentage": "70"}) is None
|
|
92
|
+
assert ep({"type": "expr", "expr": "input_tokens / 1000000 * 2.5"}) is None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TestByokConstant:
|
|
96
|
+
def test_zero_with_upstream_description(self) -> None:
|
|
97
|
+
assert ep(BYOK) == rate("3.8", "one_million_tokens", "upstream")
|
|
98
|
+
|
|
99
|
+
def test_zero_without_description_is_free(self) -> None:
|
|
100
|
+
assert ep({"type": "constant", "price": "0"}) == rate("0", "request")
|
|
101
|
+
|
|
102
|
+
def test_zero_with_unparseable_description_is_unknown(self) -> None:
|
|
103
|
+
assert ep(NO_AMOUNT) is None
|
|
104
|
+
|
|
105
|
+
def test_zero_zero_placeholder_is_unknown(self) -> None:
|
|
106
|
+
text = "billed directly at $0 / $0 per 1M input/output tokens"
|
|
107
|
+
placeholder = {"type": "constant", "price": "0", "description": text}
|
|
108
|
+
assert ep(placeholder) is None
|
|
109
|
+
|
|
110
|
+
def test_nonzero_ignores_description(self) -> None:
|
|
111
|
+
priced = {"type": "constant", "price": "0.02", "description": "$1 / $2 per 1M input/output tokens"}
|
|
112
|
+
assert ep(priced) == rate("0.02", "request")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TestUpstreamPriceFromDescription:
|
|
116
|
+
@pytest.mark.parametrize(
|
|
117
|
+
("text", "expected"),
|
|
118
|
+
[
|
|
119
|
+
("billed by Hugging Face directly at $1.4 / $4.4 per 1M input/output tokens", "3.8"),
|
|
120
|
+
("$1.518/$4.554 / 1M input/output tokens", "3.9468"),
|
|
121
|
+
("$4 / $20 / $0.2 per 1M input/output/cached tokens", "16.8"),
|
|
122
|
+
("$3.45/$0.345/$17.25 / 1M in/cached/out tokens", "14.49"),
|
|
123
|
+
("$0.50 / 1M tokens", "0.50"),
|
|
124
|
+
("$0.001 / $0.002 per 1K input/output tokens", "1.8"),
|
|
125
|
+
],
|
|
126
|
+
)
|
|
127
|
+
def test_known_shapes(self, text: str, expected: str) -> None:
|
|
128
|
+
assert upstream_price_from_description(text) == Decimal(expected)
|
|
129
|
+
|
|
130
|
+
@pytest.mark.parametrize(
|
|
131
|
+
"text",
|
|
132
|
+
[
|
|
133
|
+
None,
|
|
134
|
+
"",
|
|
135
|
+
"Bring your own OpenAI key",
|
|
136
|
+
"$1 / $2 per 1M input/output/cached tokens", # three labels, two amounts
|
|
137
|
+
"$1 / $2 per 1M input/cached tokens", # no output
|
|
138
|
+
"$0.01 per message", # not tokens
|
|
139
|
+
"$1 / $2 per 1M tokens", # two amounts, no labels
|
|
140
|
+
],
|
|
141
|
+
)
|
|
142
|
+
def test_unknown_shapes_fail_closed(self, text: str | None) -> None:
|
|
143
|
+
assert upstream_price_from_description(text) is None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class TestComposites:
|
|
147
|
+
def test_add_sums_one_unit(self) -> None:
|
|
148
|
+
figure = ep({"type": "add", "prices": [PER_REQUEST, {"type": "constant", "price": "0.005"}]})
|
|
149
|
+
assert figure == rate("0.015", "request")
|
|
150
|
+
|
|
151
|
+
def test_add_with_mixed_units_is_unknown(self) -> None:
|
|
152
|
+
assert ep({"type": "add", "prices": [TOKENS, PER_REQUEST]}) is None
|
|
153
|
+
|
|
154
|
+
def test_add_with_unknown_child_is_unknown(self) -> None:
|
|
155
|
+
assert ep({"type": "add", "prices": [PER_REQUEST, {"type": "revenue_share", "percentage": "10"}]}) is None
|
|
156
|
+
|
|
157
|
+
def test_multiply_scales_base(self) -> None:
|
|
158
|
+
assert ep({"type": "multiply", "factor": "0.5", "base": TOKENS}) == rate("0.9", "one_million_tokens")
|
|
159
|
+
|
|
160
|
+
def test_multiply_keeps_upstream_source(self) -> None:
|
|
161
|
+
assert ep({"type": "multiply", "factor": "2", "base": BYOK}) == rate("7.6", "one_million_tokens", "upstream")
|
|
162
|
+
|
|
163
|
+
def test_max_and_min(self) -> None:
|
|
164
|
+
prices = [TOKENS, CHEAP_TOKENS]
|
|
165
|
+
assert ep({"type": "max", "prices": prices}) == rate("1.8", "one_million_tokens")
|
|
166
|
+
assert ep({"type": "min", "prices": prices}) == rate("0.5", "one_million_tokens")
|
|
167
|
+
|
|
168
|
+
def test_mixed_units_compare_within_dominant_unit(self) -> None:
|
|
169
|
+
# Tokens outrank requests, which outrank every other unit.
|
|
170
|
+
prices = [{"type": "one_second", "price": "100"}, PER_REQUEST, CHEAP_TOKENS]
|
|
171
|
+
assert ep({"type": "max", "prices": prices}) == rate("0.5", "one_million_tokens")
|
|
172
|
+
prices = [{"type": "one_second", "price": "100"}, PER_REQUEST]
|
|
173
|
+
assert ep({"type": "max", "prices": prices}) == rate("0.01", "request")
|
|
174
|
+
|
|
175
|
+
def test_first_takes_first_known(self) -> None:
|
|
176
|
+
prices = [{"type": "revenue_share", "percentage": "10"}, PER_REQUEST, TOKENS]
|
|
177
|
+
assert ep({"type": "first", "prices": prices}) == rate("0.01", "request")
|
|
178
|
+
|
|
179
|
+
def test_invalid_child_is_skipped_not_raised(self) -> None:
|
|
180
|
+
assert ep({"type": "max", "prices": [{"type": "nonsense"}, PER_REQUEST]}) == rate("0.01", "request")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class TestWorstCase:
|
|
184
|
+
def test_tiered_takes_largest_tier_not_first(self) -> None:
|
|
185
|
+
tiered = {
|
|
186
|
+
"type": "tiered",
|
|
187
|
+
"based_on": "request_count",
|
|
188
|
+
"tiers": [
|
|
189
|
+
{"up_to": 1000, "price": CHEAP_TOKENS},
|
|
190
|
+
{"up_to": None, "price": TOKENS},
|
|
191
|
+
],
|
|
192
|
+
}
|
|
193
|
+
assert ep(tiered) == rate("1.8", "one_million_tokens")
|
|
194
|
+
|
|
195
|
+
@pytest.mark.parametrize(
|
|
196
|
+
("based_on", "unit_prices", "expected"),
|
|
197
|
+
[
|
|
198
|
+
("request_count", ["0.005", "0.01", "0.008"], rate("0.01", "request")),
|
|
199
|
+
("input_tokens", ["0.000001", "0.000002"], rate("2", "one_million_tokens")),
|
|
200
|
+
("one_thousand_tokens", ["0.003"], rate("3", "one_million_tokens")),
|
|
201
|
+
("one_minute", ["0.01"], rate("0.6", "one_hour")),
|
|
202
|
+
("bytes_out", ["0.000000001"], rate("1.073741824", "one_gigabyte")),
|
|
203
|
+
("images_generated", ["0.04"], rate("0.04", "image")),
|
|
204
|
+
("count", ["0.002"], rate("0.002", "request")),
|
|
205
|
+
],
|
|
206
|
+
)
|
|
207
|
+
def test_graduated_takes_largest_unit_price(
|
|
208
|
+
self, based_on: str, unit_prices: list[str], expected: EffectivePrice
|
|
209
|
+
) -> None:
|
|
210
|
+
tiers: list[dict[str, Any]] = [{"up_to": (i + 1) * 1000, "unit_price": p} for i, p in enumerate(unit_prices)]
|
|
211
|
+
tiers[-1]["up_to"] = None
|
|
212
|
+
assert ep({"type": "graduated", "based_on": based_on, "tiers": tiers}) == expected
|
|
213
|
+
|
|
214
|
+
@pytest.mark.parametrize("based_on", ["customer_charge", "input_tokens + output_tokens"])
|
|
215
|
+
def test_graduated_without_unit_is_unknown(self, based_on: str) -> None:
|
|
216
|
+
graduated = {"type": "graduated", "based_on": based_on, "tiers": [{"up_to": None, "unit_price": "0.1"}]}
|
|
217
|
+
assert ep(graduated) is None
|
|
218
|
+
|
|
219
|
+
def test_channel_takes_largest_not_default(self) -> None:
|
|
220
|
+
channel = {
|
|
221
|
+
"type": "channel",
|
|
222
|
+
"default": "byok",
|
|
223
|
+
"channels": {"byok": {"type": "constant", "price": "0"}, "plus": PER_REQUEST},
|
|
224
|
+
}
|
|
225
|
+
assert ep(channel) == rate("0.01", "request")
|
|
226
|
+
|
|
227
|
+
def test_channel_byok_upstream_beats_cheaper_managed(self) -> None:
|
|
228
|
+
channel = {"type": "channel", "default": "managed", "channels": {"managed": TOKENS, "byok": BYOK}}
|
|
229
|
+
assert ep(channel) == rate("3.8", "one_million_tokens", "upstream")
|
|
230
|
+
|
|
231
|
+
def test_channel_ignores_unknown_channels(self) -> None:
|
|
232
|
+
channel = {
|
|
233
|
+
"type": "channel",
|
|
234
|
+
"default": "managed",
|
|
235
|
+
"channels": {"managed": TOKENS, "byok": NO_AMOUNT},
|
|
236
|
+
}
|
|
237
|
+
assert ep(channel) == rate("1.8", "one_million_tokens")
|
|
238
|
+
|
|
239
|
+
def test_all_unknown_is_unknown(self) -> None:
|
|
240
|
+
channel = {
|
|
241
|
+
"type": "channel",
|
|
242
|
+
"default": "byok",
|
|
243
|
+
"channels": {"byok": NO_AMOUNT},
|
|
244
|
+
}
|
|
245
|
+
assert ep(channel) is None
|
|
246
|
+
|
|
247
|
+
def test_nested_composites_resolve_recursively(self) -> None:
|
|
248
|
+
nested = {
|
|
249
|
+
"type": "channel",
|
|
250
|
+
"default": "managed",
|
|
251
|
+
"channels": {
|
|
252
|
+
"managed": {
|
|
253
|
+
"type": "tiered",
|
|
254
|
+
"based_on": "request_count",
|
|
255
|
+
"tiers": [
|
|
256
|
+
{"up_to": 100, "price": {"type": "multiply", "factor": "2", "base": TOKENS}},
|
|
257
|
+
{"up_to": None, "price": CHEAP_TOKENS},
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
"byok": {"type": "constant", "price": "0"},
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
assert ep(nested) == rate("3.6", "one_million_tokens")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_does_not_read_the_stored_nominal() -> None:
|
|
267
|
+
# The auto-filled composite ``price`` (first tier) is not the effective price.
|
|
268
|
+
tiered = validate_pricing(
|
|
269
|
+
{
|
|
270
|
+
"type": "tiered",
|
|
271
|
+
"based_on": "request_count",
|
|
272
|
+
"tiers": [{"up_to": 10, "price": CHEAP_TOKENS}, {"up_to": None, "price": TOKENS}],
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
assert tiered.price == "0.5"
|
|
276
|
+
assert tiered.effective_price() == rate("1.8", "one_million_tokens")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_effective_price_serializes_compactly() -> None:
|
|
280
|
+
assert rate("3.8", "one_million_tokens", "upstream").model_dump(mode="json") == {
|
|
281
|
+
"amount": "3.8",
|
|
282
|
+
"unit": "one_million_tokens",
|
|
283
|
+
"source": "upstream",
|
|
284
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/models/service_group_data.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core/schema/service_group_v1.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unitysvc_core-0.2.27 → unitysvc_core-0.2.28}/src/unitysvc_core.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|