unitysvc-core 0.2.27__tar.gz → 0.2.29__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.29}/PKG-INFO +1 -1
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/pyproject.toml +1 -1
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/models/__init__.py +4 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/models/pricing.py +299 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29/src/unitysvc_core.egg-info}/PKG-INFO +1 -1
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core.egg-info/SOURCES.txt +1 -0
- unitysvc_core-0.2.29/tests/test_effective_price.py +311 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/LICENSE +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/README.md +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/setup.cfg +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/__init__.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/models/base.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/models/documents.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/models/listing_data.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/models/listing_v1.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/models/offering_data.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/models/offering_v1.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/models/promotion_data.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/models/provider_data.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/models/provider_v1.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/models/service.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/models/service_group_data.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/models/validators.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/py.typed +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/schema/base.json +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/schema/listing_v1.json +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/schema/offering_v1.json +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/schema/promotion_v1.json +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/schema/provider_v1.json +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/schema/service_group_v1.json +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/utils.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core/validator.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core.egg-info/requires.txt +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/src/unitysvc_core.egg-info/top_level.txt +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/tests/test_access_interface_routing.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/tests/test_hidden_path_skip.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/tests/test_offering_data.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/tests/test_pricing.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/tests/test_provider_account_rate_limits.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/tests/test_service_group.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/tests/test_service_status.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/tests/test_utils.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/tests/test_validator.py +0 -0
- {unitysvc_core-0.2.27 → unitysvc_core-0.2.29}/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.29"
|
|
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,192 @@ 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
|
+
|
|
345
|
+
def _zero_rate(figure: EffectivePrice, description: str | None) -> EffectivePrice | None:
|
|
346
|
+
"""A zero-rate price's effective price: free, or what the customer pays upstream.
|
|
347
|
+
|
|
348
|
+
A ``0`` with a description is a bring-your-own-key price: the customer pays
|
|
349
|
+
the upstream provider, at the rate the description states. A description
|
|
350
|
+
that states no parseable rate, or $0/$0 (what generators write when they
|
|
351
|
+
lack the provider's price), is unknown. A ``0`` with no description is
|
|
352
|
+
genuinely free.
|
|
353
|
+
"""
|
|
354
|
+
if figure.amount != 0 or not description:
|
|
355
|
+
return figure
|
|
356
|
+
upstream = upstream_price_from_description(description)
|
|
357
|
+
if upstream is None or upstream == 0:
|
|
358
|
+
return None
|
|
359
|
+
return EffectivePrice(amount=upstream, unit=UNIT_TOKENS, source="upstream")
|
|
360
|
+
|
|
361
|
+
|
|
174
362
|
# ============================================================================
|
|
175
363
|
# Pricing Models - Discriminated Union for type-safe pricing validation
|
|
176
364
|
# ============================================================================
|
|
@@ -207,6 +395,15 @@ class BasePriceData(BaseModel):
|
|
|
207
395
|
description="URL to upstream provider's pricing page",
|
|
208
396
|
)
|
|
209
397
|
|
|
398
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
399
|
+
"""What using this price realistically costs per unit, or None if unknown.
|
|
400
|
+
|
|
401
|
+
A comparison figure for sorting, filtering and routing; never billed.
|
|
402
|
+
Every pricing type defines it, so a composite price computes its figure
|
|
403
|
+
from its children. Unknown is never 0. Never raises.
|
|
404
|
+
"""
|
|
405
|
+
return None
|
|
406
|
+
|
|
210
407
|
|
|
211
408
|
class TokenPriceData(BasePriceData):
|
|
212
409
|
"""
|
|
@@ -294,6 +491,21 @@ class TokenPriceData(BasePriceData):
|
|
|
294
491
|
"""Token divisor based on the pricing type."""
|
|
295
492
|
return {"one_million_tokens": 1_000_000, "one_thousand_tokens": 1_000, "one_token": 1}[self.type]
|
|
296
493
|
|
|
494
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
495
|
+
"""``(input + 4*output) / 5`` when priced separately, else ``price``; per 1M tokens.
|
|
496
|
+
|
|
497
|
+
A zero rate with a description is a bring-your-own-key price: see
|
|
498
|
+
``_zero_rate``.
|
|
499
|
+
"""
|
|
500
|
+
if self.input is not None and self.output is not None:
|
|
501
|
+
per_unit = _blend(Decimal(self.input), Decimal(self.output))
|
|
502
|
+
elif self.price is not None:
|
|
503
|
+
per_unit = Decimal(self.price)
|
|
504
|
+
else:
|
|
505
|
+
return None
|
|
506
|
+
figure = _per_normalized_unit(self.type, per_unit)
|
|
507
|
+
return None if figure is None else _zero_rate(figure, self.description)
|
|
508
|
+
|
|
297
509
|
def calculate_cost(
|
|
298
510
|
self,
|
|
299
511
|
usage: UsageData,
|
|
@@ -361,6 +573,10 @@ class TimePriceData(BasePriceData):
|
|
|
361
573
|
description="Price per one unit of the specified type",
|
|
362
574
|
)
|
|
363
575
|
|
|
576
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
577
|
+
"""``price`` in the dimension's normalized unit."""
|
|
578
|
+
return _per_normalized_unit(self.type, Decimal(self.price))
|
|
579
|
+
|
|
364
580
|
def calculate_cost(
|
|
365
581
|
self,
|
|
366
582
|
usage: UsageData,
|
|
@@ -402,6 +618,10 @@ class ImagePriceData(BasePriceData):
|
|
|
402
618
|
description="Price per image",
|
|
403
619
|
)
|
|
404
620
|
|
|
621
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
622
|
+
"""``price`` per image."""
|
|
623
|
+
return _rate(Decimal(self.price), UNIT_IMAGE)
|
|
624
|
+
|
|
405
625
|
def calculate_cost(
|
|
406
626
|
self,
|
|
407
627
|
usage: UsageData,
|
|
@@ -442,6 +662,10 @@ class StepPriceData(BasePriceData):
|
|
|
442
662
|
description="Price per step/iteration",
|
|
443
663
|
)
|
|
444
664
|
|
|
665
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
666
|
+
"""``price`` per step."""
|
|
667
|
+
return _rate(Decimal(self.price), UNIT_STEP)
|
|
668
|
+
|
|
445
669
|
def calculate_cost(
|
|
446
670
|
self,
|
|
447
671
|
usage: UsageData,
|
|
@@ -495,6 +719,10 @@ class DataPriceData(BasePriceData):
|
|
|
495
719
|
description="Price per one unit of the specified type",
|
|
496
720
|
)
|
|
497
721
|
|
|
722
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
723
|
+
"""``price`` in the dimension's normalized unit."""
|
|
724
|
+
return _per_normalized_unit(self.type, Decimal(self.price))
|
|
725
|
+
|
|
498
726
|
def calculate_cost(
|
|
499
727
|
self,
|
|
500
728
|
usage: UsageData,
|
|
@@ -556,6 +784,10 @@ class CountPriceData(BasePriceData):
|
|
|
556
784
|
description="Price per one unit of the specified type",
|
|
557
785
|
)
|
|
558
786
|
|
|
787
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
788
|
+
"""``price`` in the dimension's normalized unit."""
|
|
789
|
+
return _per_normalized_unit(self.type, Decimal(self.price))
|
|
790
|
+
|
|
559
791
|
def calculate_cost(
|
|
560
792
|
self,
|
|
561
793
|
usage: UsageData,
|
|
@@ -602,6 +834,10 @@ class CharacterPriceData(BasePriceData):
|
|
|
602
834
|
description="Price per one unit of the specified type",
|
|
603
835
|
)
|
|
604
836
|
|
|
837
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
838
|
+
"""``price`` in the dimension's normalized unit."""
|
|
839
|
+
return _per_normalized_unit(self.type, Decimal(self.price))
|
|
840
|
+
|
|
605
841
|
def calculate_cost(
|
|
606
842
|
self,
|
|
607
843
|
usage: UsageData,
|
|
@@ -670,6 +906,10 @@ class RevenueSharePriceData(BasePriceData):
|
|
|
670
906
|
description="Percentage of customer charge that goes to the seller (0-100)",
|
|
671
907
|
)
|
|
672
908
|
|
|
909
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
910
|
+
"""A share of the customer charge has no per-unit rate: unknown."""
|
|
911
|
+
return None
|
|
912
|
+
|
|
673
913
|
def calculate_cost(
|
|
674
914
|
self,
|
|
675
915
|
usage: UsageData,
|
|
@@ -710,6 +950,14 @@ class ConstantPriceData(BasePriceData):
|
|
|
710
950
|
description="Fixed price (positive for charge, negative for discount)",
|
|
711
951
|
)
|
|
712
952
|
|
|
953
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
954
|
+
"""``price`` per request.
|
|
955
|
+
|
|
956
|
+
A ``0`` with a description is a bring-your-own-key price: see
|
|
957
|
+
``_zero_rate``.
|
|
958
|
+
"""
|
|
959
|
+
return _zero_rate(_rate(Decimal(self.price), UNIT_REQUEST), self.description)
|
|
960
|
+
|
|
713
961
|
def calculate_cost(
|
|
714
962
|
self,
|
|
715
963
|
usage: UsageData,
|
|
@@ -783,6 +1031,15 @@ class AddPriceData(BasePriceData):
|
|
|
783
1031
|
self.price = _extract_nominal_price(self.prices[0])
|
|
784
1032
|
return self
|
|
785
1033
|
|
|
1034
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1035
|
+
"""The sum of the children; unknown if any child is unknown or the units differ."""
|
|
1036
|
+
figures = [_child_effective_price(p) for p in self.prices]
|
|
1037
|
+
known = [f for f in figures if f is not None]
|
|
1038
|
+
if len(known) != len(figures) or len({f.unit for f in known}) != 1:
|
|
1039
|
+
return None
|
|
1040
|
+
source: Literal["rate", "upstream"] = "upstream" if any(f.source == "upstream" for f in known) else "rate"
|
|
1041
|
+
return EffectivePrice(amount=sum((f.amount for f in known), Decimal(0)), unit=known[0].unit, source=source)
|
|
1042
|
+
|
|
786
1043
|
def calculate_cost(
|
|
787
1044
|
self,
|
|
788
1045
|
usage: UsageData,
|
|
@@ -843,6 +1100,13 @@ class MultiplyPriceData(BasePriceData):
|
|
|
843
1100
|
self.price = base_price
|
|
844
1101
|
return self
|
|
845
1102
|
|
|
1103
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1104
|
+
"""The base's figure times ``factor``."""
|
|
1105
|
+
base = _child_effective_price(self.base)
|
|
1106
|
+
if base is None:
|
|
1107
|
+
return None
|
|
1108
|
+
return base.model_copy(update={"amount": base.amount * Decimal(self.factor)})
|
|
1109
|
+
|
|
846
1110
|
def calculate_cost(
|
|
847
1111
|
self,
|
|
848
1112
|
usage: UsageData,
|
|
@@ -897,6 +1161,10 @@ class MaxPriceData(BasePriceData):
|
|
|
897
1161
|
self.price = _extract_nominal_price(self.prices[0])
|
|
898
1162
|
return self
|
|
899
1163
|
|
|
1164
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1165
|
+
"""The largest child, as billed."""
|
|
1166
|
+
return _largest(_child_effective_price(p) for p in self.prices)
|
|
1167
|
+
|
|
900
1168
|
def calculate_cost(
|
|
901
1169
|
self,
|
|
902
1170
|
usage: UsageData,
|
|
@@ -955,6 +1223,10 @@ class MinPriceData(BasePriceData):
|
|
|
955
1223
|
self.price = _extract_nominal_price(self.prices[0])
|
|
956
1224
|
return self
|
|
957
1225
|
|
|
1226
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1227
|
+
"""The smallest child, as billed."""
|
|
1228
|
+
return _smallest(_child_effective_price(p) for p in self.prices)
|
|
1229
|
+
|
|
958
1230
|
def calculate_cost(
|
|
959
1231
|
self,
|
|
960
1232
|
usage: UsageData,
|
|
@@ -1012,6 +1284,14 @@ class FirstPriceData(BasePriceData):
|
|
|
1012
1284
|
self.price = _extract_nominal_price(self.prices[0])
|
|
1013
1285
|
return self
|
|
1014
1286
|
|
|
1287
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1288
|
+
"""The first child with a known figure, mirroring the billing order."""
|
|
1289
|
+
for price_data in self.prices:
|
|
1290
|
+
figure = _child_effective_price(price_data)
|
|
1291
|
+
if figure is not None:
|
|
1292
|
+
return figure
|
|
1293
|
+
return None
|
|
1294
|
+
|
|
1015
1295
|
def calculate_cost(
|
|
1016
1296
|
self,
|
|
1017
1297
|
usage: UsageData,
|
|
@@ -1246,6 +1526,10 @@ class ExprPriceData(BasePriceData):
|
|
|
1246
1526
|
description="Arithmetic expression using usage metrics (e.g., 'input_tokens / 1000000 * 2.5')",
|
|
1247
1527
|
)
|
|
1248
1528
|
|
|
1529
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1530
|
+
"""An arbitrary expression has no per-unit rate: unknown."""
|
|
1531
|
+
return None
|
|
1532
|
+
|
|
1249
1533
|
def calculate_cost(
|
|
1250
1534
|
self,
|
|
1251
1535
|
usage: UsageData,
|
|
@@ -1320,6 +1604,10 @@ class TieredPriceData(BasePriceData):
|
|
|
1320
1604
|
self.price = _extract_nominal_price(first_tier.price)
|
|
1321
1605
|
return self
|
|
1322
1606
|
|
|
1607
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1608
|
+
"""The largest tier's figure: the worst case a customer can pay."""
|
|
1609
|
+
return _largest(_child_effective_price(tier.price) for tier in self.tiers)
|
|
1610
|
+
|
|
1323
1611
|
def calculate_cost(
|
|
1324
1612
|
self,
|
|
1325
1613
|
usage: UsageData,
|
|
@@ -1403,6 +1691,13 @@ class GraduatedPriceData(BasePriceData):
|
|
|
1403
1691
|
self.price = self.tiers[0].unit_price
|
|
1404
1692
|
return self
|
|
1405
1693
|
|
|
1694
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1695
|
+
"""The largest ``unit_price`` (the worst case), per ``based_on``'s normalized unit.
|
|
1696
|
+
|
|
1697
|
+
Unknown when ``based_on`` has no unit (``customer_charge``, an expression).
|
|
1698
|
+
"""
|
|
1699
|
+
return _per_normalized_unit(self.based_on, max(Decimal(tier.unit_price) for tier in self.tiers))
|
|
1700
|
+
|
|
1406
1701
|
def calculate_cost(
|
|
1407
1702
|
self,
|
|
1408
1703
|
usage: UsageData,
|
|
@@ -1526,6 +1821,10 @@ class ChannelPriceData(BasePriceData):
|
|
|
1526
1821
|
self.description = f"{lo} - {hi}"
|
|
1527
1822
|
return self
|
|
1528
1823
|
|
|
1824
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1825
|
+
"""The largest figure among all channels: the worst case a customer can pay."""
|
|
1826
|
+
return _largest(_child_effective_price(p) for p in self.channels.values())
|
|
1827
|
+
|
|
1529
1828
|
def calculate_cost(
|
|
1530
1829
|
self,
|
|
1531
1830
|
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,311 @@
|
|
|
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 TestByokZeroRateTokens:
|
|
116
|
+
"""Generators also write BYOK as a zero token rate, e.g. Mistral models."""
|
|
117
|
+
|
|
118
|
+
MISTRAL = (
|
|
119
|
+
"Free ~ BYOK | usage is billed by Mistral AI directly at $1.5 / $7.5 / $0.15 per 1M input/output/cached tokens"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def test_zero_split_rate_reads_description(self) -> None:
|
|
123
|
+
byok = {"type": "one_million_tokens", "input": "0", "output": "0", "cached_input": "0"}
|
|
124
|
+
figure = ep({**byok, "description": self.MISTRAL})
|
|
125
|
+
assert figure == rate("6.3", "one_million_tokens", "upstream")
|
|
126
|
+
|
|
127
|
+
def test_zero_unified_rate_reads_description(self) -> None:
|
|
128
|
+
figure = ep({"type": "one_thousand_tokens", "price": "0", "description": HF_UPSTREAM})
|
|
129
|
+
assert figure == rate("3.8", "one_million_tokens", "upstream")
|
|
130
|
+
|
|
131
|
+
def test_zero_rate_without_description_is_free(self) -> None:
|
|
132
|
+
assert ep({"type": "one_million_tokens", "input": "0", "output": "0"}) == rate("0", "one_million_tokens")
|
|
133
|
+
|
|
134
|
+
def test_zero_rate_with_unparseable_description_is_unknown(self) -> None:
|
|
135
|
+
assert ep({"type": "one_million_tokens", "price": "0", "description": "Bring your own key"}) is None
|
|
136
|
+
|
|
137
|
+
def test_nonzero_rate_ignores_description(self) -> None:
|
|
138
|
+
priced = {**TOKENS, "description": self.MISTRAL}
|
|
139
|
+
assert ep(priced) == rate("1.8", "one_million_tokens")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class TestUpstreamPriceFromDescription:
|
|
143
|
+
@pytest.mark.parametrize(
|
|
144
|
+
("text", "expected"),
|
|
145
|
+
[
|
|
146
|
+
("billed by Hugging Face directly at $1.4 / $4.4 per 1M input/output tokens", "3.8"),
|
|
147
|
+
("$1.518/$4.554 / 1M input/output tokens", "3.9468"),
|
|
148
|
+
("$4 / $20 / $0.2 per 1M input/output/cached tokens", "16.8"),
|
|
149
|
+
("$3.45/$0.345/$17.25 / 1M in/cached/out tokens", "14.49"),
|
|
150
|
+
("$0.50 / 1M tokens", "0.50"),
|
|
151
|
+
("$0.001 / $0.002 per 1K input/output tokens", "1.8"),
|
|
152
|
+
],
|
|
153
|
+
)
|
|
154
|
+
def test_known_shapes(self, text: str, expected: str) -> None:
|
|
155
|
+
assert upstream_price_from_description(text) == Decimal(expected)
|
|
156
|
+
|
|
157
|
+
@pytest.mark.parametrize(
|
|
158
|
+
"text",
|
|
159
|
+
[
|
|
160
|
+
None,
|
|
161
|
+
"",
|
|
162
|
+
"Bring your own OpenAI key",
|
|
163
|
+
"$1 / $2 per 1M input/output/cached tokens", # three labels, two amounts
|
|
164
|
+
"$1 / $2 per 1M input/cached tokens", # no output
|
|
165
|
+
"$0.01 per message", # not tokens
|
|
166
|
+
"$1 / $2 per 1M tokens", # two amounts, no labels
|
|
167
|
+
],
|
|
168
|
+
)
|
|
169
|
+
def test_unknown_shapes_fail_closed(self, text: str | None) -> None:
|
|
170
|
+
assert upstream_price_from_description(text) is None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class TestComposites:
|
|
174
|
+
def test_add_sums_one_unit(self) -> None:
|
|
175
|
+
figure = ep({"type": "add", "prices": [PER_REQUEST, {"type": "constant", "price": "0.005"}]})
|
|
176
|
+
assert figure == rate("0.015", "request")
|
|
177
|
+
|
|
178
|
+
def test_add_with_mixed_units_is_unknown(self) -> None:
|
|
179
|
+
assert ep({"type": "add", "prices": [TOKENS, PER_REQUEST]}) is None
|
|
180
|
+
|
|
181
|
+
def test_add_with_unknown_child_is_unknown(self) -> None:
|
|
182
|
+
assert ep({"type": "add", "prices": [PER_REQUEST, {"type": "revenue_share", "percentage": "10"}]}) is None
|
|
183
|
+
|
|
184
|
+
def test_multiply_scales_base(self) -> None:
|
|
185
|
+
assert ep({"type": "multiply", "factor": "0.5", "base": TOKENS}) == rate("0.9", "one_million_tokens")
|
|
186
|
+
|
|
187
|
+
def test_multiply_keeps_upstream_source(self) -> None:
|
|
188
|
+
assert ep({"type": "multiply", "factor": "2", "base": BYOK}) == rate("7.6", "one_million_tokens", "upstream")
|
|
189
|
+
|
|
190
|
+
def test_max_and_min(self) -> None:
|
|
191
|
+
prices = [TOKENS, CHEAP_TOKENS]
|
|
192
|
+
assert ep({"type": "max", "prices": prices}) == rate("1.8", "one_million_tokens")
|
|
193
|
+
assert ep({"type": "min", "prices": prices}) == rate("0.5", "one_million_tokens")
|
|
194
|
+
|
|
195
|
+
def test_mixed_units_compare_within_dominant_unit(self) -> None:
|
|
196
|
+
# Tokens outrank requests, which outrank every other unit.
|
|
197
|
+
prices = [{"type": "one_second", "price": "100"}, PER_REQUEST, CHEAP_TOKENS]
|
|
198
|
+
assert ep({"type": "max", "prices": prices}) == rate("0.5", "one_million_tokens")
|
|
199
|
+
prices = [{"type": "one_second", "price": "100"}, PER_REQUEST]
|
|
200
|
+
assert ep({"type": "max", "prices": prices}) == rate("0.01", "request")
|
|
201
|
+
|
|
202
|
+
def test_first_takes_first_known(self) -> None:
|
|
203
|
+
prices = [{"type": "revenue_share", "percentage": "10"}, PER_REQUEST, TOKENS]
|
|
204
|
+
assert ep({"type": "first", "prices": prices}) == rate("0.01", "request")
|
|
205
|
+
|
|
206
|
+
def test_invalid_child_is_skipped_not_raised(self) -> None:
|
|
207
|
+
assert ep({"type": "max", "prices": [{"type": "nonsense"}, PER_REQUEST]}) == rate("0.01", "request")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class TestWorstCase:
|
|
211
|
+
def test_tiered_takes_largest_tier_not_first(self) -> None:
|
|
212
|
+
tiered = {
|
|
213
|
+
"type": "tiered",
|
|
214
|
+
"based_on": "request_count",
|
|
215
|
+
"tiers": [
|
|
216
|
+
{"up_to": 1000, "price": CHEAP_TOKENS},
|
|
217
|
+
{"up_to": None, "price": TOKENS},
|
|
218
|
+
],
|
|
219
|
+
}
|
|
220
|
+
assert ep(tiered) == rate("1.8", "one_million_tokens")
|
|
221
|
+
|
|
222
|
+
@pytest.mark.parametrize(
|
|
223
|
+
("based_on", "unit_prices", "expected"),
|
|
224
|
+
[
|
|
225
|
+
("request_count", ["0.005", "0.01", "0.008"], rate("0.01", "request")),
|
|
226
|
+
("input_tokens", ["0.000001", "0.000002"], rate("2", "one_million_tokens")),
|
|
227
|
+
("one_thousand_tokens", ["0.003"], rate("3", "one_million_tokens")),
|
|
228
|
+
("one_minute", ["0.01"], rate("0.6", "one_hour")),
|
|
229
|
+
("bytes_out", ["0.000000001"], rate("1.073741824", "one_gigabyte")),
|
|
230
|
+
("images_generated", ["0.04"], rate("0.04", "image")),
|
|
231
|
+
("count", ["0.002"], rate("0.002", "request")),
|
|
232
|
+
],
|
|
233
|
+
)
|
|
234
|
+
def test_graduated_takes_largest_unit_price(
|
|
235
|
+
self, based_on: str, unit_prices: list[str], expected: EffectivePrice
|
|
236
|
+
) -> None:
|
|
237
|
+
tiers: list[dict[str, Any]] = [{"up_to": (i + 1) * 1000, "unit_price": p} for i, p in enumerate(unit_prices)]
|
|
238
|
+
tiers[-1]["up_to"] = None
|
|
239
|
+
assert ep({"type": "graduated", "based_on": based_on, "tiers": tiers}) == expected
|
|
240
|
+
|
|
241
|
+
@pytest.mark.parametrize("based_on", ["customer_charge", "input_tokens + output_tokens"])
|
|
242
|
+
def test_graduated_without_unit_is_unknown(self, based_on: str) -> None:
|
|
243
|
+
graduated = {"type": "graduated", "based_on": based_on, "tiers": [{"up_to": None, "unit_price": "0.1"}]}
|
|
244
|
+
assert ep(graduated) is None
|
|
245
|
+
|
|
246
|
+
def test_channel_takes_largest_not_default(self) -> None:
|
|
247
|
+
channel = {
|
|
248
|
+
"type": "channel",
|
|
249
|
+
"default": "byok",
|
|
250
|
+
"channels": {"byok": {"type": "constant", "price": "0"}, "plus": PER_REQUEST},
|
|
251
|
+
}
|
|
252
|
+
assert ep(channel) == rate("0.01", "request")
|
|
253
|
+
|
|
254
|
+
def test_channel_byok_upstream_beats_cheaper_managed(self) -> None:
|
|
255
|
+
channel = {"type": "channel", "default": "managed", "channels": {"managed": TOKENS, "byok": BYOK}}
|
|
256
|
+
assert ep(channel) == rate("3.8", "one_million_tokens", "upstream")
|
|
257
|
+
|
|
258
|
+
def test_channel_ignores_unknown_channels(self) -> None:
|
|
259
|
+
channel = {
|
|
260
|
+
"type": "channel",
|
|
261
|
+
"default": "managed",
|
|
262
|
+
"channels": {"managed": TOKENS, "byok": NO_AMOUNT},
|
|
263
|
+
}
|
|
264
|
+
assert ep(channel) == rate("1.8", "one_million_tokens")
|
|
265
|
+
|
|
266
|
+
def test_all_unknown_is_unknown(self) -> None:
|
|
267
|
+
channel = {
|
|
268
|
+
"type": "channel",
|
|
269
|
+
"default": "byok",
|
|
270
|
+
"channels": {"byok": NO_AMOUNT},
|
|
271
|
+
}
|
|
272
|
+
assert ep(channel) is None
|
|
273
|
+
|
|
274
|
+
def test_nested_composites_resolve_recursively(self) -> None:
|
|
275
|
+
nested = {
|
|
276
|
+
"type": "channel",
|
|
277
|
+
"default": "managed",
|
|
278
|
+
"channels": {
|
|
279
|
+
"managed": {
|
|
280
|
+
"type": "tiered",
|
|
281
|
+
"based_on": "request_count",
|
|
282
|
+
"tiers": [
|
|
283
|
+
{"up_to": 100, "price": {"type": "multiply", "factor": "2", "base": TOKENS}},
|
|
284
|
+
{"up_to": None, "price": CHEAP_TOKENS},
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
"byok": {"type": "constant", "price": "0"},
|
|
288
|
+
},
|
|
289
|
+
}
|
|
290
|
+
assert ep(nested) == rate("3.6", "one_million_tokens")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_does_not_read_the_stored_nominal() -> None:
|
|
294
|
+
# The auto-filled composite ``price`` (first tier) is not the effective price.
|
|
295
|
+
tiered = validate_pricing(
|
|
296
|
+
{
|
|
297
|
+
"type": "tiered",
|
|
298
|
+
"based_on": "request_count",
|
|
299
|
+
"tiers": [{"up_to": 10, "price": CHEAP_TOKENS}, {"up_to": None, "price": TOKENS}],
|
|
300
|
+
}
|
|
301
|
+
)
|
|
302
|
+
assert tiered.price == "0.5"
|
|
303
|
+
assert tiered.effective_price() == rate("1.8", "one_million_tokens")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def test_effective_price_serializes_compactly() -> None:
|
|
307
|
+
assert rate("3.8", "one_million_tokens", "upstream").model_dump(mode="json") == {
|
|
308
|
+
"amount": "3.8",
|
|
309
|
+
"unit": "one_million_tokens",
|
|
310
|
+
"source": "upstream",
|
|
311
|
+
}
|
|
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.29}/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.29}/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.29}/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
|