unitysvc-core 0.2.26__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.26/src/unitysvc_core.egg-info → unitysvc_core-0.2.28}/PKG-INFO +1 -1
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/pyproject.toml +1 -1
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/models/__init__.py +4 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/models/offering_data.py +1 -2
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/models/offering_v1.py +0 -8
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/models/pricing.py +402 -3
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/schema/listing_v1.json +70 -1
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/schema/offering_v1.json +70 -1
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28/src/unitysvc_core.egg-info}/PKG-INFO +1 -1
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core.egg-info/SOURCES.txt +2 -0
- unitysvc_core-0.2.28/tests/test_effective_price.py +284 -0
- unitysvc_core-0.2.28/tests/test_offering_data.py +14 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/tests/test_pricing.py +151 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/LICENSE +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/README.md +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/setup.cfg +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/__init__.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/models/base.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/models/documents.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/models/listing_data.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/models/listing_v1.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/models/promotion_data.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/models/provider_data.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/models/provider_v1.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/models/service.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/models/service_group_data.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/models/validators.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/py.typed +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/schema/base.json +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/schema/promotion_v1.json +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/schema/provider_v1.json +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/schema/service_group_v1.json +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/utils.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core/validator.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core.egg-info/requires.txt +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/src/unitysvc_core.egg-info/top_level.txt +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/tests/test_access_interface_routing.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/tests/test_hidden_path_skip.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/tests/test_provider_account_rate_limits.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/tests/test_service_group.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/tests/test_service_status.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/tests/test_utils.py +0 -0
- {unitysvc_core-0.2.26 → unitysvc_core-0.2.28}/tests/test_validator.py +0 -0
- {unitysvc_core-0.2.26 → 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",
|
|
@@ -29,14 +29,6 @@ class OfferingV1(ServiceOfferingData):
|
|
|
29
29
|
# File-specific fields for validation
|
|
30
30
|
time_created: datetime
|
|
31
31
|
|
|
32
|
-
# Override to make required in file validation (base has Optional for API
|
|
33
|
-
# flexibility). ``summary`` needs no override — it is required on the base
|
|
34
|
-
# itself: the teaser replaces the retired two-mode description convention
|
|
35
|
-
# (unitysvc/unitysvc#1838), and the description is free-form long-form text.
|
|
36
|
-
description: str = Field( # type: ignore[assignment]
|
|
37
|
-
description="Service description (long-form; the summary carries the teaser)",
|
|
38
|
-
)
|
|
39
|
-
|
|
40
32
|
# Static information (optional — not all service types have meaningful details)
|
|
41
33
|
details: dict[str, Any] = Field( # type: ignore[assignment]
|
|
42
34
|
default_factory=dict,
|
|
@@ -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
|
|
|
@@ -110,6 +112,7 @@ class UsageData(BaseModel):
|
|
|
110
112
|
- **tokens**: one_token, one_thousand_tokens, one_million_tokens
|
|
111
113
|
- **data**: one_byte, one_kilobyte, one_megabyte, one_gigabyte
|
|
112
114
|
- **count**: count, one_thousand, one_million
|
|
115
|
+
- **characters**: characters, one_character, one_thousand_characters, one_million_characters
|
|
113
116
|
|
|
114
117
|
Only one field per group should be set per usage instance.
|
|
115
118
|
|
|
@@ -145,6 +148,24 @@ class UsageData(BaseModel):
|
|
|
145
148
|
one_megabyte: float | None = None
|
|
146
149
|
one_gigabyte: float | None = None
|
|
147
150
|
|
|
151
|
+
# Names the GATEWAY actually ships (usage_event.lua), kept as aliases so they
|
|
152
|
+
# survive ``extra="ignore"``. ``images_generated`` and ``duration_seconds``
|
|
153
|
+
# join their equivalence groups below; the byte counters deliberately do
|
|
154
|
+
# NOT — see DataPriceData.based_on.
|
|
155
|
+
images_generated: int | None = None
|
|
156
|
+
duration_seconds: float | None = None
|
|
157
|
+
bytes_in: int | None = None
|
|
158
|
+
bytes_out: int | None = None
|
|
159
|
+
|
|
160
|
+
# Character-based usage (text-to-speech and similar).
|
|
161
|
+
# ``characters`` is what providers actually report — DashScope's TTS answers
|
|
162
|
+
# ``usage: {"characters": N}`` — and is the group's base unit, the way
|
|
163
|
+
# ``seconds`` is for time.
|
|
164
|
+
characters: int | None = None
|
|
165
|
+
one_character: int | None = None
|
|
166
|
+
one_thousand_characters: float | None = None
|
|
167
|
+
one_million_characters: float | None = None
|
|
168
|
+
|
|
148
169
|
# Count-based usage (images, steps, requests)
|
|
149
170
|
# count + scaled variants form an equivalence group.
|
|
150
171
|
count: int | None = None
|
|
@@ -152,6 +173,175 @@ class UsageData(BaseModel):
|
|
|
152
173
|
one_million: float | None = None
|
|
153
174
|
|
|
154
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
|
+
|
|
155
345
|
# ============================================================================
|
|
156
346
|
# Pricing Models - Discriminated Union for type-safe pricing validation
|
|
157
347
|
# ============================================================================
|
|
@@ -188,6 +378,15 @@ class BasePriceData(BaseModel):
|
|
|
188
378
|
description="URL to upstream provider's pricing page",
|
|
189
379
|
)
|
|
190
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
|
+
|
|
191
390
|
|
|
192
391
|
class TokenPriceData(BasePriceData):
|
|
193
392
|
"""
|
|
@@ -275,6 +474,16 @@ class TokenPriceData(BasePriceData):
|
|
|
275
474
|
"""Token divisor based on the pricing type."""
|
|
276
475
|
return {"one_million_tokens": 1_000_000, "one_thousand_tokens": 1_000, "one_token": 1}[self.type]
|
|
277
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
|
+
|
|
278
487
|
def calculate_cost(
|
|
279
488
|
self,
|
|
280
489
|
usage: UsageData,
|
|
@@ -342,6 +551,10 @@ class TimePriceData(BasePriceData):
|
|
|
342
551
|
description="Price per one unit of the specified type",
|
|
343
552
|
)
|
|
344
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
|
+
|
|
345
558
|
def calculate_cost(
|
|
346
559
|
self,
|
|
347
560
|
usage: UsageData,
|
|
@@ -383,6 +596,10 @@ class ImagePriceData(BasePriceData):
|
|
|
383
596
|
description="Price per image",
|
|
384
597
|
)
|
|
385
598
|
|
|
599
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
600
|
+
"""``price`` per image."""
|
|
601
|
+
return _rate(Decimal(self.price), UNIT_IMAGE)
|
|
602
|
+
|
|
386
603
|
def calculate_cost(
|
|
387
604
|
self,
|
|
388
605
|
usage: UsageData,
|
|
@@ -400,10 +617,13 @@ class ImagePriceData(BasePriceData):
|
|
|
400
617
|
Returns:
|
|
401
618
|
Calculated cost based on image count
|
|
402
619
|
"""
|
|
403
|
-
|
|
620
|
+
# Via the equivalence group, so the gateway's ``images_generated`` counts
|
|
621
|
+
# as well as an explicit ``count``.
|
|
622
|
+
units = _resolve_equivalent_metric("count", usage)
|
|
623
|
+
if units is None:
|
|
404
624
|
raise ValueError("Image pricing requires 'count' in usage data")
|
|
405
625
|
|
|
406
|
-
return Decimal(self.price) *
|
|
626
|
+
return Decimal(self.price) * units
|
|
407
627
|
|
|
408
628
|
|
|
409
629
|
class StepPriceData(BasePriceData):
|
|
@@ -420,6 +640,10 @@ class StepPriceData(BasePriceData):
|
|
|
420
640
|
description="Price per step/iteration",
|
|
421
641
|
)
|
|
422
642
|
|
|
643
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
644
|
+
"""``price`` per step."""
|
|
645
|
+
return _rate(Decimal(self.price), UNIT_STEP)
|
|
646
|
+
|
|
423
647
|
def calculate_cost(
|
|
424
648
|
self,
|
|
425
649
|
usage: UsageData,
|
|
@@ -452,14 +676,31 @@ class DataPriceData(BasePriceData):
|
|
|
452
676
|
|
|
453
677
|
Usage can be provided in **any** data unit — cross-unit conversion is
|
|
454
678
|
handled automatically via equivalence groups.
|
|
679
|
+
|
|
680
|
+
The gateway ships ``bytes_in`` and ``bytes_out`` on every request. Those are
|
|
681
|
+
two directions of one dimension, so they are deliberately NOT both in the
|
|
682
|
+
data equivalence group: a group lookup returns whichever field the dict
|
|
683
|
+
happens to yield first, which would make the billed direction depend on
|
|
684
|
+
iteration order. ``based_on`` names the direction instead, so it is a
|
|
685
|
+
property of the price rather than an accident of the payload.
|
|
455
686
|
"""
|
|
456
687
|
|
|
457
688
|
type: Literal["one_byte", "one_kilobyte", "one_megabyte", "one_gigabyte"] = "one_megabyte"
|
|
458
689
|
|
|
690
|
+
based_on: Literal["bytes_out", "bytes_in", "bytes_total"] = Field(
|
|
691
|
+
default="bytes_out",
|
|
692
|
+
description="Which byte counter to price. Defaults to egress, the "
|
|
693
|
+
"conventional billable direction; 'bytes_total' sums both.",
|
|
694
|
+
)
|
|
695
|
+
|
|
459
696
|
price: PriceStr = Field(
|
|
460
697
|
description="Price per one unit of the specified type",
|
|
461
698
|
)
|
|
462
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
|
+
|
|
463
704
|
def calculate_cost(
|
|
464
705
|
self,
|
|
465
706
|
usage: UsageData,
|
|
@@ -477,7 +718,24 @@ class DataPriceData(BasePriceData):
|
|
|
477
718
|
Returns:
|
|
478
719
|
Calculated cost based on data usage
|
|
479
720
|
"""
|
|
480
|
-
|
|
721
|
+
# The named byte counter wins when present — it is what the gateway
|
|
722
|
+
# actually sends. Fall back to the canonical one_byte..one_gigabyte
|
|
723
|
+
# units so a caller supplying those directly is unaffected.
|
|
724
|
+
raw_bytes: Decimal | None = None
|
|
725
|
+
if self.based_on == "bytes_total":
|
|
726
|
+
if usage.bytes_in is not None or usage.bytes_out is not None:
|
|
727
|
+
raw_bytes = Decimal(usage.bytes_in or 0) + Decimal(usage.bytes_out or 0)
|
|
728
|
+
else:
|
|
729
|
+
value = getattr(usage, self.based_on, None)
|
|
730
|
+
if value is not None:
|
|
731
|
+
raw_bytes = Decimal(str(value))
|
|
732
|
+
|
|
733
|
+
units: Decimal | None
|
|
734
|
+
if raw_bytes is not None:
|
|
735
|
+
units = raw_bytes / EQUIVALENCE_GROUPS["data"][self.type]
|
|
736
|
+
else:
|
|
737
|
+
units = _resolve_equivalent_metric(self.type, usage)
|
|
738
|
+
|
|
481
739
|
if units is None:
|
|
482
740
|
raise ValueError(f"Data pricing ({self.type}) requires a data field in usage data")
|
|
483
741
|
|
|
@@ -504,6 +762,10 @@ class CountPriceData(BasePriceData):
|
|
|
504
762
|
description="Price per one unit of the specified type",
|
|
505
763
|
)
|
|
506
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
|
+
|
|
507
769
|
def calculate_cost(
|
|
508
770
|
self,
|
|
509
771
|
usage: UsageData,
|
|
@@ -528,6 +790,61 @@ class CountPriceData(BasePriceData):
|
|
|
528
790
|
return Decimal(self.price) * units
|
|
529
791
|
|
|
530
792
|
|
|
793
|
+
class CharacterPriceData(BasePriceData):
|
|
794
|
+
"""
|
|
795
|
+
Price data for character-scaled pricing (text-to-speech and similar).
|
|
796
|
+
|
|
797
|
+
Supported types: ``one_character``, ``one_thousand_characters``,
|
|
798
|
+
``one_million_characters``. The ``characters`` equivalence-group field is
|
|
799
|
+
the base unit, and it is the name providers actually report — DashScope's
|
|
800
|
+
TTS answers ``usage: {"characters": N}``.
|
|
801
|
+
|
|
802
|
+
Usage may be given in any unit of the group; conversion is automatic.
|
|
803
|
+
|
|
804
|
+
Separate from ``CountPriceData`` on purpose: characters and counts are
|
|
805
|
+
different dimensions, and collapsing them would let "per 1M characters" and
|
|
806
|
+
"per 1M images" resolve against each other's usage.
|
|
807
|
+
"""
|
|
808
|
+
|
|
809
|
+
type: Literal["one_character", "one_thousand_characters", "one_million_characters"] = "one_million_characters"
|
|
810
|
+
|
|
811
|
+
price: PriceStr = Field(
|
|
812
|
+
description="Price per one unit of the specified type",
|
|
813
|
+
)
|
|
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
|
+
|
|
819
|
+
def calculate_cost(
|
|
820
|
+
self,
|
|
821
|
+
usage: UsageData,
|
|
822
|
+
customer_charge: Decimal | None = None,
|
|
823
|
+
request_count: int | None = None,
|
|
824
|
+
channel: str | None = None,
|
|
825
|
+
) -> Decimal:
|
|
826
|
+
"""Calculate cost for character-scaled pricing.
|
|
827
|
+
|
|
828
|
+
Args:
|
|
829
|
+
usage: Usage data with any character field
|
|
830
|
+
customer_charge: Not used (ignored)
|
|
831
|
+
request_count: Not used (ignored)
|
|
832
|
+
|
|
833
|
+
Returns:
|
|
834
|
+
Calculated cost based on character usage
|
|
835
|
+
|
|
836
|
+
Raises:
|
|
837
|
+
ValueError: if no character field is populated. Raising rather than
|
|
838
|
+
treating absence as zero is deliberate — a silent zero here is a
|
|
839
|
+
charge of nothing for work actually done.
|
|
840
|
+
"""
|
|
841
|
+
units = _resolve_equivalent_metric(self.type, usage)
|
|
842
|
+
if units is None:
|
|
843
|
+
raise ValueError(f"Character pricing ({self.type}) requires a character field in usage data")
|
|
844
|
+
|
|
845
|
+
return Decimal(self.price) * units
|
|
846
|
+
|
|
847
|
+
|
|
531
848
|
def _validate_percentage_string(v: Any) -> str:
|
|
532
849
|
"""Validate that percentage values are strings representing valid decimals in range 0-100."""
|
|
533
850
|
# First use the standard price validation
|
|
@@ -567,6 +884,10 @@ class RevenueSharePriceData(BasePriceData):
|
|
|
567
884
|
description="Percentage of customer charge that goes to the seller (0-100)",
|
|
568
885
|
)
|
|
569
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
|
+
|
|
570
891
|
def calculate_cost(
|
|
571
892
|
self,
|
|
572
893
|
usage: UsageData,
|
|
@@ -607,6 +928,23 @@ class ConstantPriceData(BasePriceData):
|
|
|
607
928
|
description="Fixed price (positive for charge, negative for discount)",
|
|
608
929
|
)
|
|
609
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
|
+
|
|
610
948
|
def calculate_cost(
|
|
611
949
|
self,
|
|
612
950
|
usage: UsageData,
|
|
@@ -680,6 +1018,15 @@ class AddPriceData(BasePriceData):
|
|
|
680
1018
|
self.price = _extract_nominal_price(self.prices[0])
|
|
681
1019
|
return self
|
|
682
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
|
+
|
|
683
1030
|
def calculate_cost(
|
|
684
1031
|
self,
|
|
685
1032
|
usage: UsageData,
|
|
@@ -740,6 +1087,13 @@ class MultiplyPriceData(BasePriceData):
|
|
|
740
1087
|
self.price = base_price
|
|
741
1088
|
return self
|
|
742
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
|
+
|
|
743
1097
|
def calculate_cost(
|
|
744
1098
|
self,
|
|
745
1099
|
usage: UsageData,
|
|
@@ -794,6 +1148,10 @@ class MaxPriceData(BasePriceData):
|
|
|
794
1148
|
self.price = _extract_nominal_price(self.prices[0])
|
|
795
1149
|
return self
|
|
796
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
|
+
|
|
797
1155
|
def calculate_cost(
|
|
798
1156
|
self,
|
|
799
1157
|
usage: UsageData,
|
|
@@ -852,6 +1210,10 @@ class MinPriceData(BasePriceData):
|
|
|
852
1210
|
self.price = _extract_nominal_price(self.prices[0])
|
|
853
1211
|
return self
|
|
854
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
|
+
|
|
855
1217
|
def calculate_cost(
|
|
856
1218
|
self,
|
|
857
1219
|
usage: UsageData,
|
|
@@ -909,6 +1271,14 @@ class FirstPriceData(BasePriceData):
|
|
|
909
1271
|
self.price = _extract_nominal_price(self.prices[0])
|
|
910
1272
|
return self
|
|
911
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
|
+
|
|
912
1282
|
def calculate_cost(
|
|
913
1283
|
self,
|
|
914
1284
|
usage: UsageData,
|
|
@@ -940,6 +1310,7 @@ class FirstPriceData(BasePriceData):
|
|
|
940
1310
|
EQUIVALENCE_GROUPS: dict[str, dict[str, Decimal]] = {
|
|
941
1311
|
"time": {
|
|
942
1312
|
"seconds": Decimal(1),
|
|
1313
|
+
"duration_seconds": Decimal(1), # what usage_event.lua ships
|
|
943
1314
|
"one_second": Decimal(1),
|
|
944
1315
|
"one_minute": Decimal(60),
|
|
945
1316
|
"one_hour": Decimal(3600),
|
|
@@ -959,9 +1330,16 @@ EQUIVALENCE_GROUPS: dict[str, dict[str, Decimal]] = {
|
|
|
959
1330
|
},
|
|
960
1331
|
"count": {
|
|
961
1332
|
"count": Decimal(1),
|
|
1333
|
+
"images_generated": Decimal(1), # what usage_event.lua ships
|
|
962
1334
|
"one_thousand": Decimal(1000),
|
|
963
1335
|
"one_million": Decimal(1_000_000),
|
|
964
1336
|
},
|
|
1337
|
+
"characters": {
|
|
1338
|
+
"characters": Decimal(1),
|
|
1339
|
+
"one_character": Decimal(1),
|
|
1340
|
+
"one_thousand_characters": Decimal(1000),
|
|
1341
|
+
"one_million_characters": Decimal(1_000_000),
|
|
1342
|
+
},
|
|
965
1343
|
}
|
|
966
1344
|
|
|
967
1345
|
# Reverse lookup: unit_name → (group_name, base_factor)
|
|
@@ -1135,6 +1513,10 @@ class ExprPriceData(BasePriceData):
|
|
|
1135
1513
|
description="Arithmetic expression using usage metrics (e.g., 'input_tokens / 1000000 * 2.5')",
|
|
1136
1514
|
)
|
|
1137
1515
|
|
|
1516
|
+
def effective_price(self) -> EffectivePrice | None:
|
|
1517
|
+
"""An arbitrary expression has no per-unit rate: unknown."""
|
|
1518
|
+
return None
|
|
1519
|
+
|
|
1138
1520
|
def calculate_cost(
|
|
1139
1521
|
self,
|
|
1140
1522
|
usage: UsageData,
|
|
@@ -1209,6 +1591,10 @@ class TieredPriceData(BasePriceData):
|
|
|
1209
1591
|
self.price = _extract_nominal_price(first_tier.price)
|
|
1210
1592
|
return self
|
|
1211
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
|
+
|
|
1212
1598
|
def calculate_cost(
|
|
1213
1599
|
self,
|
|
1214
1600
|
usage: UsageData,
|
|
@@ -1292,6 +1678,13 @@ class GraduatedPriceData(BasePriceData):
|
|
|
1292
1678
|
self.price = self.tiers[0].unit_price
|
|
1293
1679
|
return self
|
|
1294
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
|
+
|
|
1295
1688
|
def calculate_cost(
|
|
1296
1689
|
self,
|
|
1297
1690
|
usage: UsageData,
|
|
@@ -1415,6 +1808,10 @@ class ChannelPriceData(BasePriceData):
|
|
|
1415
1808
|
self.description = f"{lo} - {hi}"
|
|
1416
1809
|
return self
|
|
1417
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
|
+
|
|
1418
1815
|
def calculate_cost(
|
|
1419
1816
|
self,
|
|
1420
1817
|
usage: UsageData,
|
|
@@ -1448,6 +1845,7 @@ Pricing = Annotated[
|
|
|
1448
1845
|
| TimePriceData
|
|
1449
1846
|
| DataPriceData
|
|
1450
1847
|
| CountPriceData
|
|
1848
|
+
| CharacterPriceData
|
|
1451
1849
|
| ImagePriceData
|
|
1452
1850
|
| StepPriceData
|
|
1453
1851
|
| RevenueSharePriceData
|
|
@@ -1472,6 +1870,7 @@ def validate_pricing(
|
|
|
1472
1870
|
| TimePriceData
|
|
1473
1871
|
| DataPriceData
|
|
1474
1872
|
| CountPriceData
|
|
1873
|
+
| CharacterPriceData
|
|
1475
1874
|
| ImagePriceData
|
|
1476
1875
|
| StepPriceData
|
|
1477
1876
|
| RevenueSharePriceData
|