unitysvc-core 0.2.30__tar.gz → 0.2.31__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.
Files changed (45) hide show
  1. {unitysvc_core-0.2.30/src/unitysvc_core.egg-info → unitysvc_core-0.2.31}/PKG-INFO +1 -1
  2. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/pyproject.toml +1 -1
  3. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/models/pricing.py +19 -5
  4. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/validator.py +40 -0
  5. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31/src/unitysvc_core.egg-info}/PKG-INFO +1 -1
  6. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/tests/test_pricing.py +50 -0
  7. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/tests/test_validator.py +29 -0
  8. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/LICENSE +0 -0
  9. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/README.md +0 -0
  10. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/setup.cfg +0 -0
  11. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/__init__.py +0 -0
  12. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/models/__init__.py +0 -0
  13. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/models/base.py +0 -0
  14. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/models/documents.py +0 -0
  15. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/models/listing_data.py +0 -0
  16. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/models/listing_v1.py +0 -0
  17. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/models/offering_data.py +0 -0
  18. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/models/offering_v1.py +0 -0
  19. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/models/promotion_data.py +0 -0
  20. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/models/provider_data.py +0 -0
  21. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/models/provider_v1.py +0 -0
  22. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/models/service.py +0 -0
  23. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/models/service_group_data.py +0 -0
  24. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/models/validators.py +0 -0
  25. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/py.typed +0 -0
  26. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/schema/base.json +0 -0
  27. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/schema/listing_v1.json +0 -0
  28. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/schema/offering_v1.json +0 -0
  29. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/schema/promotion_v1.json +0 -0
  30. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/schema/provider_v1.json +0 -0
  31. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/schema/service_group_v1.json +0 -0
  32. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core/utils.py +0 -0
  33. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core.egg-info/SOURCES.txt +0 -0
  34. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
  35. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core.egg-info/requires.txt +0 -0
  36. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/src/unitysvc_core.egg-info/top_level.txt +0 -0
  37. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/tests/test_access_interface_routing.py +0 -0
  38. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/tests/test_effective_price.py +0 -0
  39. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/tests/test_hidden_path_skip.py +0 -0
  40. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/tests/test_offering_data.py +0 -0
  41. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/tests/test_provider_account_rate_limits.py +0 -0
  42. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/tests/test_service_group.py +0 -0
  43. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/tests/test_service_status.py +0 -0
  44. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/tests/test_utils.py +0 -0
  45. {unitysvc_core-0.2.30 → unitysvc_core-0.2.31}/tests/test_validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitysvc-core
3
- Version: 0.2.30
3
+ Version: 0.2.31
4
4
  Summary: Shared data models and validation helpers for UnitySVC packages
5
5
  Author-email: Bo Peng <bo.peng@unitysvc.com>
6
6
  Maintainer-email: Bo Peng <bo.peng@unitysvc.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "unitysvc-core"
7
- version = "0.2.30"
7
+ version = "0.2.31"
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" }]
@@ -12,13 +12,14 @@ Contains:
12
12
  from __future__ import annotations
13
13
 
14
14
  import ast
15
+ import functools
15
16
  import operator
16
17
  import re
17
18
  from collections.abc import Callable, Iterable
18
19
  from decimal import Decimal, InvalidOperation
19
20
  from typing import Annotated, Any, Literal
20
21
 
21
- from pydantic import BaseModel, ConfigDict, Field, model_validator
22
+ from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, model_validator
22
23
  from pydantic.functional_validators import BeforeValidator
23
24
 
24
25
 
@@ -1876,6 +1877,22 @@ Pricing = Annotated[
1876
1877
  ]
1877
1878
 
1878
1879
 
1880
+ @functools.cache
1881
+ def _pricing_adapter() -> TypeAdapter[Pricing]:
1882
+ """The ``Pricing`` union's validator, built once and reused.
1883
+
1884
+ Building a ``TypeAdapter`` compiles the validation schema for every member
1885
+ of the union, which costs ~0.4-0.5 ms; validating with it afterwards costs
1886
+ microseconds. ``validate_pricing`` used to build one per call, and
1887
+ composite prices (``multiply``, ``add``, ...) validate their nested parts
1888
+ through it too, so pricing one usage event built it several times: ~2 ms
1889
+ of CPU per event in the usage consumer (unitysvc/unitysvc#2591). Built
1890
+ lazily rather than at import so ``Pricing``'s forward references are
1891
+ resolved first. ``TypeAdapter`` is safe to share across threads.
1892
+ """
1893
+ return TypeAdapter(Pricing)
1894
+
1895
+
1879
1896
  def validate_pricing(
1880
1897
  data: dict[str, Any],
1881
1898
  ) -> (
@@ -1915,7 +1932,4 @@ def validate_pricing(
1915
1932
  >>> validated = validate_pricing(data)
1916
1933
  >>> print(validated.input) # "0.5"
1917
1934
  """
1918
- from pydantic import TypeAdapter
1919
-
1920
- adapter: TypeAdapter[Pricing] = TypeAdapter(Pricing)
1921
- return adapter.validate_python(data)
1935
+ return _pricing_adapter().validate_python(data)
@@ -12,6 +12,12 @@ from jsonschema.validators import Draft7Validator
12
12
 
13
13
  from .utils import SCHEMA_TO_STEM, is_hidden_path, load_data_file, schema_for_path
14
14
 
15
+ # ``details.model``: a provider-neutral model name (``glm-5.2``, not
16
+ # ``zai-org/GLM-5.2`` or Fireworks' ``glm-5p2``). Lowercase, no ``org/``
17
+ # prefix. The backend re-checks the same pattern before it shows a name on a
18
+ # platform tier (``app/crud/platform_served.py``).
19
+ CANONICAL_MODEL_RE = re.compile(r"^[a-z0-9][a-z0-9._:-]*$")
20
+
15
21
  # svcpass api_key dispositions (#1198, #1786). On an upstream_access_config
16
22
  # interface these literal values are not credentials: "" / "__strip__" strip
17
23
  # the svcpass-bearing header, "__forward__" forwards svcpass verbatim, and
@@ -665,6 +671,37 @@ class DataValidator:
665
671
 
666
672
  return errors
667
673
 
674
+ def validate_offering_model_name(self, data: dict[str, Any], schema_name: str) -> list[str]:
675
+ """Validate the optional ``details.model`` of an offering.
676
+
677
+ ``details.model`` names the model an offering serves in a
678
+ provider-neutral way, independent of the provider's own spelling
679
+ in ``details.upstream_model``. A platform tier lists its members'
680
+ names as the models it is currently served by (unitysvc#2567).
681
+
682
+ Only the shape is checked (lowercase, no ``org/`` prefix); there
683
+ is no registry of allowed names. Absent is fine.
684
+
685
+ Args:
686
+ data: The offering data being validated.
687
+ schema_name: The schema name (only ``offering_v1`` is checked).
688
+
689
+ Returns:
690
+ List of validation error messages.
691
+ """
692
+ if schema_name != "offering_v1":
693
+ return []
694
+ details = data.get("details")
695
+ if not isinstance(details, dict) or "model" not in details:
696
+ return []
697
+ value = details["model"]
698
+ if isinstance(value, str) and CANONICAL_MODEL_RE.match(value):
699
+ return []
700
+ return [
701
+ f"details.model must be a provider-neutral model name: lowercase letters, "
702
+ f"digits and '.', '_', ':', '-', with no 'org/' prefix (e.g. 'glm-5.2'), got {value!r}."
703
+ ]
704
+
668
705
  def validate_connectivity_test_exists(self, data: dict[str, Any], schema_name: str) -> list[str]:
669
706
  """Validate that at least one connectivity_test document exists.
670
707
 
@@ -921,6 +958,9 @@ class DataValidator:
921
958
  llm_metadata_errors = self.validate_llm_offering_metadata(data, schema_name)
922
959
  errors.extend(llm_metadata_errors)
923
960
 
961
+ # Validate the optional provider-neutral details.model (offering_v1 only)
962
+ errors.extend(self.validate_offering_model_name(data, schema_name))
963
+
924
964
  # Validate MCP offerings declare an mcp upstream channel (offering_v1
925
965
  # only); the customer-facing gateway interface is checked below with
926
966
  # the other listing_v1 interface rules.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitysvc-core
3
- Version: 0.2.30
3
+ Version: 0.2.31
4
4
  Summary: Shared data models and validation helpers for UnitySVC packages
5
5
  Author-email: Bo Peng <bo.peng@unitysvc.com>
6
6
  Maintainer-email: Bo Peng <bo.peng@unitysvc.com>
@@ -2156,3 +2156,53 @@ class TestChannelPriceData:
2156
2156
  assert pricing.calculate_cost(usage, request_count=10, channel="managed") == Decimal("5")
2157
2157
  assert pricing.calculate_cost(usage, request_count=10_000, channel="managed") == Decimal("50")
2158
2158
  assert pricing.calculate_cost(usage, request_count=10_000, channel="byok") == Decimal("0")
2159
+
2160
+
2161
+ class TestValidatePricingAdapterIsBuiltOnce:
2162
+ """``validate_pricing`` reuses one ``TypeAdapter`` (unitysvc/unitysvc#2591).
2163
+
2164
+ Building the adapter compiles the whole ``Pricing`` union's schema
2165
+ (~0.4-0.5 ms); the usage consumer validates several prices per event, so
2166
+ rebuilding it per call made billing CPU-bound.
2167
+ """
2168
+
2169
+ def test_repeated_calls_build_the_adapter_once(self, monkeypatch: pytest.MonkeyPatch) -> None:
2170
+ import unitysvc_core.models.pricing as pricing
2171
+
2172
+ built: list[object] = []
2173
+ real = pricing.TypeAdapter
2174
+
2175
+ def counting(*args: object, **kwargs: object) -> object:
2176
+ built.append(args)
2177
+ return real(*args, **kwargs)
2178
+
2179
+ pricing._pricing_adapter.cache_clear()
2180
+ monkeypatch.setattr(pricing, "TypeAdapter", counting)
2181
+ try:
2182
+ token = {"type": "one_million_tokens", "input": "0.5", "output": "1.5"}
2183
+ composite = {
2184
+ "type": "multiply",
2185
+ "factor": "1.05",
2186
+ "base": {"type": "one_million_tokens", "input": "0.5", "output": "1.5"},
2187
+ }
2188
+ for _ in range(50):
2189
+ assert validate_pricing(token).input == "0.5"
2190
+ validate_pricing(composite)
2191
+ finally:
2192
+ pricing._pricing_adapter.cache_clear()
2193
+ assert len(built) == 1
2194
+
2195
+ def test_results_match_a_fresh_adapter(self) -> None:
2196
+ from pydantic import TypeAdapter
2197
+
2198
+ from unitysvc_core.models.pricing import Pricing
2199
+
2200
+ for data in (
2201
+ {"type": "one_million_tokens", "input": "0.5", "output": "1.5"},
2202
+ {"type": "multiply", "factor": "2", "base": {"type": "constant", "price": "1"}},
2203
+ ):
2204
+ assert validate_pricing(data) == TypeAdapter(Pricing).validate_python(data)
2205
+
2206
+ def test_invalid_data_still_raises(self) -> None:
2207
+ with pytest.raises(ValueError):
2208
+ validate_pricing({"type": "no_such_price_type"})
@@ -1164,6 +1164,35 @@ class TestAccessInterfaceNameValidation:
1164
1164
  assert "'ok_two'" not in joined
1165
1165
 
1166
1166
 
1167
+ class TestOfferingModelNameValidation:
1168
+ """Tests for ``validate_offering_model_name``: ``details.model`` is an
1169
+ optional, provider-neutral model name, checked for shape only."""
1170
+
1171
+ @pytest.mark.parametrize("name", ["glm-5.2", "kimi-k2.6", "deepseek-v4-pro", "qwen3:8b", "gpt_oss_120b"])
1172
+ def test_canonical_names_pass(self, schema_dir, example_data_dir, name):
1173
+ validator = DataValidator(example_data_dir, schema_dir)
1174
+ data = {"service_type": "llm", "details": {"model": name}}
1175
+ assert validator.validate_offering_model_name(data, "offering_v1") == []
1176
+
1177
+ def test_absent_passes(self, schema_dir, example_data_dir):
1178
+ validator = DataValidator(example_data_dir, schema_dir)
1179
+ assert validator.validate_offering_model_name({"details": {}}, "offering_v1") == []
1180
+ assert validator.validate_offering_model_name({}, "offering_v1") == []
1181
+
1182
+ @pytest.mark.parametrize("name", ["zai-org/glm-5.2", "GLM-5.2", "", "-glm", "glm 5.2", None, 5])
1183
+ def test_provider_spellings_rejected(self, schema_dir, example_data_dir, name):
1184
+ validator = DataValidator(example_data_dir, schema_dir)
1185
+ data = {"service_type": "llm", "details": {"model": name}}
1186
+ errors = validator.validate_offering_model_name(data, "offering_v1")
1187
+ assert len(errors) == 1
1188
+ assert "details.model" in errors[0]
1189
+
1190
+ def test_other_schemas_ignored(self, schema_dir, example_data_dir):
1191
+ validator = DataValidator(example_data_dir, schema_dir)
1192
+ data = {"details": {"model": "Not/Canonical"}}
1193
+ assert validator.validate_offering_model_name(data, "listing_v1") == []
1194
+
1195
+
1167
1196
  class TestLLMOfferingMetadataValidation:
1168
1197
  """Tests for ``validate_llm_offering_metadata``.
1169
1198
 
File without changes
File without changes
File without changes