unitysvc-core 0.2.28__tar.gz → 0.2.30__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.28/src/unitysvc_core.egg-info → unitysvc_core-0.2.30}/PKG-INFO +1 -1
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/pyproject.toml +1 -1
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/models/__init__.py +4 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/models/pricing.py +28 -17
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/models/validators.py +72 -1
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/validator.py +10 -1
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30/src/unitysvc_core.egg-info}/PKG-INFO +1 -1
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/tests/test_effective_price.py +27 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/tests/test_validator.py +39 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/tests/test_validators.py +52 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/LICENSE +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/README.md +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/setup.cfg +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/__init__.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/models/base.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/models/documents.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/models/listing_data.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/models/listing_v1.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/models/offering_data.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/models/offering_v1.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/models/promotion_data.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/models/provider_data.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/models/provider_v1.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/models/service.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/models/service_group_data.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/py.typed +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/schema/base.json +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/schema/listing_v1.json +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/schema/offering_v1.json +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/schema/promotion_v1.json +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/schema/provider_v1.json +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/schema/service_group_v1.json +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/utils.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core.egg-info/SOURCES.txt +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core.egg-info/requires.txt +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core.egg-info/top_level.txt +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/tests/test_access_interface_routing.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/tests/test_hidden_path_skip.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/tests/test_offering_data.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/tests/test_pricing.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/tests/test_provider_account_rate_limits.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/tests/test_service_group.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/tests/test_service_status.py +0 -0
- {unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/tests/test_utils.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.30"
|
|
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" }]
|
|
@@ -83,12 +83,14 @@ from .service_group_data import (
|
|
|
83
83
|
validate_service_group,
|
|
84
84
|
)
|
|
85
85
|
from .validators import (
|
|
86
|
+
PLATFORM_SERVICES_DIRNAME,
|
|
86
87
|
SUPPORTED_SERVICE_OPTIONS,
|
|
87
88
|
suggest_valid_name,
|
|
88
89
|
validate_listing_gateway_base_urls,
|
|
89
90
|
validate_listing_s3_base_urls,
|
|
90
91
|
validate_listing_smtp_base_urls,
|
|
91
92
|
validate_name,
|
|
93
|
+
validate_platform_listing_base_urls,
|
|
92
94
|
validate_s3_gateway_alias,
|
|
93
95
|
validate_service_identifier,
|
|
94
96
|
validate_service_options,
|
|
@@ -177,7 +179,9 @@ __all__ = [
|
|
|
177
179
|
"validate_service_identifier",
|
|
178
180
|
"validate_service_options",
|
|
179
181
|
"validate_s3_gateway_alias",
|
|
182
|
+
"PLATFORM_SERVICES_DIRNAME",
|
|
180
183
|
"validate_listing_gateway_base_urls",
|
|
184
|
+
"validate_platform_listing_base_urls",
|
|
181
185
|
"validate_listing_s3_base_urls",
|
|
182
186
|
"validate_listing_smtp_base_urls",
|
|
183
187
|
"suggest_valid_name",
|
|
@@ -342,6 +342,23 @@ def upstream_price_from_description(text: str | None) -> Decimal | None:
|
|
|
342
342
|
return _blend(by_label["in"], by_label["out"]) * scale
|
|
343
343
|
|
|
344
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
|
+
|
|
345
362
|
# ============================================================================
|
|
346
363
|
# Pricing Models - Discriminated Union for type-safe pricing validation
|
|
347
364
|
# ============================================================================
|
|
@@ -475,14 +492,19 @@ class TokenPriceData(BasePriceData):
|
|
|
475
492
|
return {"one_million_tokens": 1_000_000, "one_thousand_tokens": 1_000, "one_token": 1}[self.type]
|
|
476
493
|
|
|
477
494
|
def effective_price(self) -> EffectivePrice | None:
|
|
478
|
-
"""``(input + 4*output) / 5`` when priced separately, else ``price``; per 1M tokens.
|
|
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
|
+
"""
|
|
479
500
|
if self.input is not None and self.output is not None:
|
|
480
501
|
per_unit = _blend(Decimal(self.input), Decimal(self.output))
|
|
481
502
|
elif self.price is not None:
|
|
482
503
|
per_unit = Decimal(self.price)
|
|
483
504
|
else:
|
|
484
505
|
return None
|
|
485
|
-
|
|
506
|
+
figure = _per_normalized_unit(self.type, per_unit)
|
|
507
|
+
return None if figure is None else _zero_rate(figure, self.description)
|
|
486
508
|
|
|
487
509
|
def calculate_cost(
|
|
488
510
|
self,
|
|
@@ -931,19 +953,10 @@ class ConstantPriceData(BasePriceData):
|
|
|
931
953
|
def effective_price(self) -> EffectivePrice | None:
|
|
932
954
|
"""``price`` per request.
|
|
933
955
|
|
|
934
|
-
A ``0`` with a description is a bring-your-own-key price:
|
|
935
|
-
|
|
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.
|
|
956
|
+
A ``0`` with a description is a bring-your-own-key price: see
|
|
957
|
+
``_zero_rate``.
|
|
939
958
|
"""
|
|
940
|
-
|
|
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")
|
|
959
|
+
return _zero_rate(_rate(Decimal(self.price), UNIT_REQUEST), self.description)
|
|
947
960
|
|
|
948
961
|
def calculate_cost(
|
|
949
962
|
self,
|
|
@@ -1904,7 +1917,5 @@ def validate_pricing(
|
|
|
1904
1917
|
"""
|
|
1905
1918
|
from pydantic import TypeAdapter
|
|
1906
1919
|
|
|
1907
|
-
adapter: TypeAdapter[
|
|
1908
|
-
TypeAdapter(Pricing)
|
|
1909
|
-
)
|
|
1920
|
+
adapter: TypeAdapter[Pricing] = TypeAdapter(Pricing)
|
|
1910
1921
|
return adapter.validate_python(data)
|
|
@@ -558,9 +558,16 @@ _API_GATEWAY_PREFIX = "${API_GATEWAY_BASE_URL}"
|
|
|
558
558
|
_GATEWAY_SEGMENT_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]*$")
|
|
559
559
|
|
|
560
560
|
|
|
561
|
-
def validate_listing_gateway_base_urls(
|
|
561
|
+
def validate_listing_gateway_base_urls(
|
|
562
|
+
user_access_interfaces: dict[str, Any] | None,
|
|
563
|
+
*,
|
|
564
|
+
platform_service: bool = False,
|
|
565
|
+
) -> list[str]:
|
|
562
566
|
"""Validate ``${API_GATEWAY_BASE_URL}/...`` base_urls.
|
|
563
567
|
|
|
568
|
+
A **platform service**'s listing (``platform_service=True``) follows its own
|
|
569
|
+
rule instead: see :func:`validate_platform_listing_base_urls`.
|
|
570
|
+
|
|
564
571
|
The gateway base_url is intentionally left almost unconstrained: a provider
|
|
565
572
|
may route many services through a single base_url and differentiate them by
|
|
566
573
|
other parts of the request (e.g. ``routing_key`` / ``model``), so we cannot
|
|
@@ -582,6 +589,8 @@ def validate_listing_gateway_base_urls(user_access_interfaces: dict[str, Any] |
|
|
|
582
589
|
|
|
583
590
|
Returns a list of error messages (empty if all valid).
|
|
584
591
|
"""
|
|
592
|
+
if platform_service:
|
|
593
|
+
return validate_platform_listing_base_urls(user_access_interfaces)
|
|
585
594
|
if not user_access_interfaces or not isinstance(user_access_interfaces, dict):
|
|
586
595
|
return []
|
|
587
596
|
|
|
@@ -633,6 +642,68 @@ def validate_listing_gateway_base_urls(user_access_interfaces: dict[str, Any] |
|
|
|
633
642
|
return errors
|
|
634
643
|
|
|
635
644
|
|
|
645
|
+
# A platform service is published from ``platform-services/<provider>/<name>/``
|
|
646
|
+
# (unitysvc#2569); its listing is the only one allowed a ``/p`` address.
|
|
647
|
+
PLATFORM_SERVICES_DIRNAME = "platform-services"
|
|
648
|
+
|
|
649
|
+
# Same grammar the platform enforces at publish time.
|
|
650
|
+
_PLATFORM_BASE_URL_RE = re.compile(r"^\$\{API_GATEWAY_BASE_URL\}/p/(?P<route>[a-z0-9][a-z0-9_-]*)$")
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def validate_platform_listing_base_urls(user_access_interfaces: dict[str, Any] | None) -> list[str]:
|
|
654
|
+
"""Validate a platform service listing's ``/p`` address.
|
|
655
|
+
|
|
656
|
+
A platform service is called at ``${API_GATEWAY_BASE_URL}/p/<route>``, which
|
|
657
|
+
ordinary listings may not use (single-letter prefixes are reserved). Its
|
|
658
|
+
listing must declare that address and nothing else:
|
|
659
|
+
|
|
660
|
+
- at least one interface;
|
|
661
|
+
- every ``base_url`` exactly ``${API_GATEWAY_BASE_URL}/p/<route>`` — lowercase
|
|
662
|
+
letters, digits, ``-`` and ``_``, starting with a letter or digit, no
|
|
663
|
+
further path;
|
|
664
|
+
- one ``<route>`` shared by all interfaces;
|
|
665
|
+
- the primary interface's ``routing_key``, if set, an object.
|
|
666
|
+
|
|
667
|
+
Mirrors the platform's publish-time check, so authors see these errors
|
|
668
|
+
before upload. Returns a list of error messages (empty if valid).
|
|
669
|
+
"""
|
|
670
|
+
if (
|
|
671
|
+
not isinstance(user_access_interfaces, dict)
|
|
672
|
+
or not user_access_interfaces
|
|
673
|
+
or not all(isinstance(v, dict) for v in user_access_interfaces.values())
|
|
674
|
+
):
|
|
675
|
+
return [
|
|
676
|
+
"user_access_interfaces: a platform service must declare its "
|
|
677
|
+
"'${API_GATEWAY_BASE_URL}/p/<route>' address (an object of interface objects keyed by name)"
|
|
678
|
+
]
|
|
679
|
+
|
|
680
|
+
errors: list[str] = []
|
|
681
|
+
routes: set[str] = set()
|
|
682
|
+
for iface_name, iface in user_access_interfaces.items():
|
|
683
|
+
base_url = iface.get("base_url")
|
|
684
|
+
match = _PLATFORM_BASE_URL_RE.match(base_url) if isinstance(base_url, str) else None
|
|
685
|
+
if match is None:
|
|
686
|
+
errors.append(
|
|
687
|
+
f"user_access_interfaces.{iface_name}.base_url: a platform service's base_url must be "
|
|
688
|
+
f"exactly '${{API_GATEWAY_BASE_URL}}/p/<route>', got {base_url!r}"
|
|
689
|
+
)
|
|
690
|
+
else:
|
|
691
|
+
routes.add(match.group("route"))
|
|
692
|
+
if len(routes) > 1:
|
|
693
|
+
errors.append(
|
|
694
|
+
f"user_access_interfaces: all interfaces must share one /p route, got {', '.join(sorted(routes))}"
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
primary = sorted(
|
|
698
|
+
user_access_interfaces.items(),
|
|
699
|
+
key=lambda item: (not bool(item[1].get("is_primary")), int(item[1].get("sort_order") or 0), item[0]),
|
|
700
|
+
)[0]
|
|
701
|
+
routing_key = primary[1].get("routing_key")
|
|
702
|
+
if routing_key is not None and not isinstance(routing_key, dict):
|
|
703
|
+
errors.append(f"user_access_interfaces.{primary[0]}.routing_key: must be an object")
|
|
704
|
+
return errors
|
|
705
|
+
|
|
706
|
+
|
|
636
707
|
def _earliest_dynamic_marker(s: str) -> int | None:
|
|
637
708
|
"""Return the index of the earliest dynamic-substitution marker in ``s``.
|
|
638
709
|
|
|
@@ -940,6 +940,7 @@ class DataValidator:
|
|
|
940
940
|
# Validate S3, SMTP, MCP, and API-gateway interfaces (listing_v1 only)
|
|
941
941
|
if schema_name == "listing_v1":
|
|
942
942
|
from .models.validators import (
|
|
943
|
+
PLATFORM_SERVICES_DIRNAME,
|
|
943
944
|
validate_access_interface_names,
|
|
944
945
|
validate_listing_gateway_base_urls,
|
|
945
946
|
validate_listing_jinja_var_references,
|
|
@@ -950,7 +951,15 @@ class DataValidator:
|
|
|
950
951
|
|
|
951
952
|
uai = data.get("user_access_interfaces")
|
|
952
953
|
errors.extend(validate_access_interface_names(uai))
|
|
953
|
-
|
|
954
|
+
# A listing under platform-services/ is a platform service: its
|
|
955
|
+
# address is /p, which every other listing is refused.
|
|
956
|
+
try:
|
|
957
|
+
parts = file_path.resolve().relative_to(self.data_dir.resolve()).parts
|
|
958
|
+
except ValueError:
|
|
959
|
+
parts = file_path.parts
|
|
960
|
+
errors.extend(
|
|
961
|
+
validate_listing_gateway_base_urls(uai, platform_service=PLATFORM_SERVICES_DIRNAME in parts[:-1])
|
|
962
|
+
)
|
|
954
963
|
errors.extend(validate_listing_s3_base_urls(uai))
|
|
955
964
|
errors.extend(validate_listing_smtp_base_urls(uai))
|
|
956
965
|
errors.extend(validate_listing_mcp_base_urls(uai))
|
|
@@ -112,6 +112,33 @@ class TestByokConstant:
|
|
|
112
112
|
assert ep(priced) == rate("0.02", "request")
|
|
113
113
|
|
|
114
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
|
+
|
|
115
142
|
class TestUpstreamPriceFromDescription:
|
|
116
143
|
@pytest.mark.parametrize(
|
|
117
144
|
("text", "expected"),
|
|
@@ -1637,3 +1637,42 @@ class TestSvcpassDisposition:
|
|
|
1637
1637
|
data = {"service_options": {"ops_testing_parameters": {"api_key": "__sigv4__"}}}
|
|
1638
1638
|
errors = validator.validate_api_key_secrets(data)
|
|
1639
1639
|
assert errors and "secrets reference format" in errors[0]
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
class TestPlatformServiceListingFile:
|
|
1643
|
+
"""``DataValidator`` applies the ``/p`` rule to listings under
|
|
1644
|
+
``platform-services/`` and the reserved-prefix rule everywhere else (#2569)."""
|
|
1645
|
+
|
|
1646
|
+
@staticmethod
|
|
1647
|
+
def _write(path: Path, base_url: str) -> Path:
|
|
1648
|
+
import json
|
|
1649
|
+
|
|
1650
|
+
path.parent.mkdir(parents=True)
|
|
1651
|
+
path.write_text(
|
|
1652
|
+
json.dumps(
|
|
1653
|
+
{
|
|
1654
|
+
"name": "labs/demo",
|
|
1655
|
+
"user_access_interfaces": {"default": {"access_method": "http", "base_url": base_url}},
|
|
1656
|
+
}
|
|
1657
|
+
)
|
|
1658
|
+
)
|
|
1659
|
+
return path
|
|
1660
|
+
|
|
1661
|
+
@staticmethod
|
|
1662
|
+
def _gateway_errors(validator: DataValidator, path: Path) -> list[str]:
|
|
1663
|
+
_, errors = validator.validate_data_file(path, schema_name="listing_v1", check_name_consistency=False)
|
|
1664
|
+
return [e for e in errors if "base_url" in e]
|
|
1665
|
+
|
|
1666
|
+
def test_platform_service_listing_may_use_p(self, schema_dir, tmp_path):
|
|
1667
|
+
path = self._write(tmp_path / "platform-services/labs/demo/listing.json", "${API_GATEWAY_BASE_URL}/p/llm")
|
|
1668
|
+
assert self._gateway_errors(DataValidator(tmp_path, schema_dir), path) == []
|
|
1669
|
+
|
|
1670
|
+
def test_platform_service_listing_must_use_p(self, schema_dir, tmp_path):
|
|
1671
|
+
path = self._write(tmp_path / "platform-services/labs/demo/listing.json", "${API_GATEWAY_BASE_URL}/llm")
|
|
1672
|
+
errors = self._gateway_errors(DataValidator(tmp_path, schema_dir), path)
|
|
1673
|
+
assert len(errors) == 1 and "/p/<route>" in errors[0]
|
|
1674
|
+
|
|
1675
|
+
def test_ordinary_listing_may_not_use_p(self, schema_dir, tmp_path):
|
|
1676
|
+
path = self._write(tmp_path / "specs/demo/listing.json", "${API_GATEWAY_BASE_URL}/p/llm")
|
|
1677
|
+
errors = self._gateway_errors(DataValidator(tmp_path, schema_dir), path)
|
|
1678
|
+
assert len(errors) == 1 and "reserved single-letter prefix" in errors[0]
|
|
@@ -13,6 +13,7 @@ from unitysvc_core.models.validators import (
|
|
|
13
13
|
validate_listing_mcp_base_urls,
|
|
14
14
|
validate_mcp_namespace,
|
|
15
15
|
validate_mcp_offering,
|
|
16
|
+
validate_platform_listing_base_urls,
|
|
16
17
|
validate_service_identifier,
|
|
17
18
|
)
|
|
18
19
|
|
|
@@ -623,3 +624,54 @@ class TestValidateMcpOffering:
|
|
|
623
624
|
offering = self._offering()
|
|
624
625
|
offering["user_access_interfaces"] = _mcp_uai()
|
|
625
626
|
assert validate_mcp_offering(offering) == []
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
class TestPlatformListingBaseUrls:
|
|
630
|
+
"""A platform service's listing declares exactly its ``/p`` address (#2569)."""
|
|
631
|
+
|
|
632
|
+
@staticmethod
|
|
633
|
+
def _p(*base_urls: str, routing_key=None) -> dict:
|
|
634
|
+
uai = {f"i{n}": {"access_method": "http", "base_url": url} for n, url in enumerate(base_urls)}
|
|
635
|
+
if routing_key is not None:
|
|
636
|
+
uai["i0"]["routing_key"] = routing_key
|
|
637
|
+
return uai
|
|
638
|
+
|
|
639
|
+
def test_accepts_one_route(self) -> None:
|
|
640
|
+
uai = self._p("${API_GATEWAY_BASE_URL}/p/llm", "${API_GATEWAY_BASE_URL}/p/llm", routing_key={"model": "x"})
|
|
641
|
+
assert validate_platform_listing_base_urls(uai) == []
|
|
642
|
+
assert validate_listing_gateway_base_urls(uai, platform_service=True) == []
|
|
643
|
+
|
|
644
|
+
def test_ordinary_listing_still_refuses_p(self) -> None:
|
|
645
|
+
errors = validate_listing_gateway_base_urls(self._p("${API_GATEWAY_BASE_URL}/p/llm"))
|
|
646
|
+
assert any("reserved single-letter prefix" in e for e in errors)
|
|
647
|
+
|
|
648
|
+
@pytest.mark.parametrize(
|
|
649
|
+
"base_url",
|
|
650
|
+
[
|
|
651
|
+
"${API_GATEWAY_BASE_URL}/llm",
|
|
652
|
+
"${API_GATEWAY_BASE_URL}/p/llm/v1",
|
|
653
|
+
"${API_GATEWAY_BASE_URL}/p/LLM",
|
|
654
|
+
"${API_GATEWAY_BASE_URL}/p/{{ service_name }}",
|
|
655
|
+
"${API_GATEWAY_BASE_URL}/p/",
|
|
656
|
+
"https://example.com/p/llm",
|
|
657
|
+
],
|
|
658
|
+
)
|
|
659
|
+
def test_rejects_anything_but_exactly_p_route(self, base_url: str) -> None:
|
|
660
|
+
errors = validate_platform_listing_base_urls(self._p(base_url))
|
|
661
|
+
assert len(errors) == 1
|
|
662
|
+
assert "user_access_interfaces.i0.base_url" in errors[0]
|
|
663
|
+
|
|
664
|
+
def test_rejects_mixed_routes(self) -> None:
|
|
665
|
+
errors = validate_platform_listing_base_urls(
|
|
666
|
+
self._p("${API_GATEWAY_BASE_URL}/p/llm", "${API_GATEWAY_BASE_URL}/p/embed")
|
|
667
|
+
)
|
|
668
|
+
assert errors == ["user_access_interfaces: all interfaces must share one /p route, got embed, llm"]
|
|
669
|
+
|
|
670
|
+
def test_rejects_non_object_routing_key(self) -> None:
|
|
671
|
+
errors = validate_platform_listing_base_urls(self._p("${API_GATEWAY_BASE_URL}/p/llm", routing_key="x"))
|
|
672
|
+
assert errors == ["user_access_interfaces.i0.routing_key: must be an object"]
|
|
673
|
+
|
|
674
|
+
@pytest.mark.parametrize("value", [None, {}, "not a dict", {"a": "not an object"}])
|
|
675
|
+
def test_requires_the_address(self, value) -> None:
|
|
676
|
+
errors = validate_platform_listing_base_urls(value)
|
|
677
|
+
assert len(errors) == 1 and "/p/<route>" in errors[0]
|
|
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.28 → unitysvc_core-0.2.30}/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
|
{unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/src/unitysvc_core/schema/service_group_v1.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unitysvc_core-0.2.28 → unitysvc_core-0.2.30}/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
|