unitysvc-core 0.2.7__tar.gz → 0.2.9__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.7/src/unitysvc_core.egg-info → unitysvc_core-0.2.9}/PKG-INFO +1 -1
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/pyproject.toml +1 -1
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/__init__.py +0 -2
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/base.py +5 -5
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/offering_v1.py +10 -5
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/service.py +24 -51
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/validators.py +2 -5
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/schema/listing_v1.json +1 -498
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/schema/offering_v1.json +3 -596
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9/src/unitysvc_core.egg-info}/PKG-INFO +1 -1
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core.egg-info/SOURCES.txt +1 -0
- unitysvc_core-0.2.9/tests/test_access_interface_routing.py +34 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/tests/test_validator.py +3 -33
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/LICENSE +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/README.md +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/setup.cfg +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/__init__.py +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/documents.py +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/listing_data.py +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/listing_v1.py +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/offering_data.py +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/pricing.py +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/promotion_data.py +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/provider_data.py +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/provider_v1.py +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/service_group_data.py +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/py.typed +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/schema/base.json +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/schema/promotion_v1.json +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/schema/provider_v1.json +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/schema/service_group_v1.json +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/utils.py +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/validator.py +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core.egg-info/requires.txt +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core.egg-info/top_level.txt +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/tests/test_pricing.py +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/tests/test_service_group.py +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/tests/test_service_status.py +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/tests/test_utils.py +0 -0
- {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/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.9"
|
|
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" }]
|
|
@@ -72,7 +72,6 @@ from .service import (
|
|
|
72
72
|
RateLimit,
|
|
73
73
|
ServiceConstraints,
|
|
74
74
|
ServiceStatus,
|
|
75
|
-
UpstreamAccessConfigData,
|
|
76
75
|
)
|
|
77
76
|
from .service_group_data import (
|
|
78
77
|
SERVICE_GROUP_SCHEMA_VERSION,
|
|
@@ -108,7 +107,6 @@ __all__ = [
|
|
|
108
107
|
# Shared / access models
|
|
109
108
|
"DocumentData",
|
|
110
109
|
"AccessInterfaceData",
|
|
111
|
-
"UpstreamAccessConfigData",
|
|
112
110
|
"RateLimit",
|
|
113
111
|
"ServiceConstraints",
|
|
114
112
|
"ServiceStatus",
|
|
@@ -243,11 +243,11 @@ class RequestTransformEnum(StrEnum):
|
|
|
243
243
|
body_transformer = "body_transformer"
|
|
244
244
|
# Simple body replacement from rendered enrollment data
|
|
245
245
|
set_body = "set_body"
|
|
246
|
-
# Translate the request/response between
|
|
247
|
-
#
|
|
248
|
-
#
|
|
249
|
-
#
|
|
250
|
-
|
|
246
|
+
# Translate the request/response between LLM API formats (Anthropic Messages
|
|
247
|
+
# <-> OpenAI Chat). The value names the converter (e.g. ``anthropic_to_openai``);
|
|
248
|
+
# the gateway auto-detects the incoming format and translates in-process only
|
|
249
|
+
# when needed (#923/#929/#1400). Replaces the earlier ``llm_translate`` key.
|
|
250
|
+
llm_translator = "llm_translator"
|
|
251
251
|
|
|
252
252
|
|
|
253
253
|
class ServiceTypeEnum(StrEnum):
|
|
@@ -6,7 +6,6 @@ from pydantic import ConfigDict, Field, HttpUrl, field_validator
|
|
|
6
6
|
from .documents import DocumentData
|
|
7
7
|
from .offering_data import ServiceOfferingData
|
|
8
8
|
from .pricing import Pricing
|
|
9
|
-
from .service import UpstreamAccessConfigData
|
|
10
9
|
from .validators import validate_channel_name, validate_service_identifier
|
|
11
10
|
|
|
12
11
|
|
|
@@ -44,9 +43,15 @@ class OfferingV1(ServiceOfferingData):
|
|
|
44
43
|
# Convenience field for logo (converted to documents during import)
|
|
45
44
|
logo: str | HttpUrl | None = None
|
|
46
45
|
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
# Required in file validation (base allows None for API flexibility). Each
|
|
47
|
+
# value is an OPAQUE per-channel JSON object: the gateway reads
|
|
48
|
+
# upstream_access_config as raw JSONB, and channels are genuinely
|
|
49
|
+
# heterogeneous (http has base_url; smtp has host/port; s3 has bucket/region;
|
|
50
|
+
# a raw channel wraps arbitrary fields). A typed model here enforced nothing
|
|
51
|
+
# (extra="allow") while misfitting non-HTTP channels — so it is intentionally
|
|
52
|
+
# left open and validated by the gateway/backend at use. See unitysvc/unitysvc#1717.
|
|
53
|
+
upstream_access_config: dict[str, dict[str, Any]] = Field( # type: ignore[assignment]
|
|
54
|
+
description="Upstream access channels, keyed by channel name (opaque per-channel objects)",
|
|
50
55
|
)
|
|
51
56
|
|
|
52
57
|
documents: dict[str, DocumentData] | None = Field( # type: ignore[assignment]
|
|
@@ -72,7 +77,7 @@ class OfferingV1(ServiceOfferingData):
|
|
|
72
77
|
|
|
73
78
|
@field_validator("upstream_access_config")
|
|
74
79
|
@classmethod
|
|
75
|
-
def validate_channel_names(cls, v: dict[str,
|
|
80
|
+
def validate_channel_names(cls, v: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
|
76
81
|
"""Validate every channel name (key of ``upstream_access_config``).
|
|
77
82
|
|
|
78
83
|
Channel names are selected via the ``<name>@<channel>`` identifier
|
|
@@ -38,6 +38,15 @@ class RateLimit(BaseModel):
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
class ServiceConstraints(BaseModel):
|
|
41
|
+
"""DEPRECATED — planned SLA/quota surface that was never enforced.
|
|
42
|
+
|
|
43
|
+
No routing resolver or gateway path ever read these fields; they were
|
|
44
|
+
authored nowhere in seller data. The ``constraints`` field has been removed
|
|
45
|
+
from ``AccessInterfaceData``. The class is retained (still exported) only so
|
|
46
|
+
existing imports do not break; it is unused and slated for removal. Do not
|
|
47
|
+
add new references. See unitysvc/unitysvc#1717.
|
|
48
|
+
"""
|
|
49
|
+
|
|
41
50
|
model_config = ConfigDict(extra="forbid")
|
|
42
51
|
|
|
43
52
|
# Usage Quotas & Billing
|
|
@@ -81,7 +90,21 @@ class ServiceConstraints(BaseModel):
|
|
|
81
90
|
|
|
82
91
|
|
|
83
92
|
class AccessInterfaceData(BaseModel):
|
|
84
|
-
"""User-facing access interface data
|
|
93
|
+
"""User-facing access interface data — a **pure routing-resolution object**.
|
|
94
|
+
|
|
95
|
+
It answers "which candidate does this request address?" — nothing more. It
|
|
96
|
+
carries only the customer-facing address (``base_url``), the request
|
|
97
|
+
``routing_key``, and selection/visibility metadata (``is_active`` /
|
|
98
|
+
``is_primary`` / ``sort_order``). All upstream-access and enforcement
|
|
99
|
+
concerns (endpoint credentials, transformers, response-eval rules, rate
|
|
100
|
+
limits) live on the per-channel ``upstream_access_config`` entry — an
|
|
101
|
+
opaque JSON object the gateway reads as raw JSONB — which is where the
|
|
102
|
+
gateway resolves them.
|
|
103
|
+
|
|
104
|
+
Historically this also carried ``rate_limits`` / ``constraints`` /
|
|
105
|
+
``response_rules``; those were authored nowhere in customer-facing
|
|
106
|
+
interfaces and have moved to the channel (rate limits) or been dropped
|
|
107
|
+
(``constraints`` — never enforced). See unitysvc/unitysvc#1717.
|
|
85
108
|
|
|
86
109
|
Note: The interface name is NOT stored here - it's the key in the interfaces dict.
|
|
87
110
|
When stored in the database, the backend extracts the key as the name field.
|
|
@@ -100,61 +123,11 @@ class AccessInterfaceData(BaseModel):
|
|
|
100
123
|
description="Request routing key for matching (e.g., {'model': 'gpt-4'})",
|
|
101
124
|
)
|
|
102
125
|
|
|
103
|
-
rate_limits: list[RateLimit] | None = Field(
|
|
104
|
-
default=None,
|
|
105
|
-
description="Rate limit",
|
|
106
|
-
)
|
|
107
|
-
constraints: ServiceConstraints | None = Field(default=None, description="Service constraints and conditions")
|
|
108
|
-
response_rules: dict[str, dict[str, Any] | str] | None = Field(
|
|
109
|
-
default=None,
|
|
110
|
-
description="Response evaluation rules keyed by rule name. "
|
|
111
|
-
"Values are either a rule dict or a Jinja2 template string. Validated by the backend.",
|
|
112
|
-
)
|
|
113
126
|
is_active: bool = Field(default=True, description="Whether interface is active")
|
|
114
127
|
is_primary: bool = Field(default=False, description="Whether this is the primary interface")
|
|
115
128
|
sort_order: int = Field(default=0, description="Display order")
|
|
116
129
|
|
|
117
130
|
|
|
118
|
-
class UpstreamAccessConfigData(AccessInterfaceData):
|
|
119
|
-
"""One upstream access channel — a named entry in ``upstream_access_config``.
|
|
120
|
-
|
|
121
|
-
Each channel is a complete way for the gateway to reach the upstream: a wire
|
|
122
|
-
protocol (``access_method``), endpoint (``base_url``), credential (``api_key``),
|
|
123
|
-
routing key, and rate-limit/quality restrictions. A service may declare several
|
|
124
|
-
channels (keyed by free-form channel name, e.g. ``"managed"``, ``"byok"``,
|
|
125
|
-
``"managed-eu"``); the gateway selects one per request.
|
|
126
|
-
|
|
127
|
-
The channel's *type* is derived from its credential/endpoint provenance:
|
|
128
|
-
``managed`` (seller's key, ``${ secrets.* }``), ``byok`` (customer's key,
|
|
129
|
-
``${ customer_secrets.* }``), or ``byoe`` (customer's key + customer-templated
|
|
130
|
-
``base_url``).
|
|
131
|
-
|
|
132
|
-
Extends AccessInterfaceData with extra="allow" to support protocol-specific
|
|
133
|
-
configuration fields (e.g., S3 bucket/region, SMTP host/port) that the
|
|
134
|
-
gateway needs to reach the upstream service.
|
|
135
|
-
"""
|
|
136
|
-
|
|
137
|
-
model_config = ConfigDict(extra="allow")
|
|
138
|
-
|
|
139
|
-
# base_url is optional for upstream configs (e.g., S3 uses bucket + region instead)
|
|
140
|
-
base_url: str | None = Field(default=None, max_length=500, description="Base URL for api access") # type: ignore[assignment]
|
|
141
|
-
|
|
142
|
-
api_key: str | None = Field(
|
|
143
|
-
default=None,
|
|
144
|
-
max_length=2000,
|
|
145
|
-
description=(
|
|
146
|
-
"Upstream API key, or a svcpass disposition (#1198). The value "
|
|
147
|
-
"controls what the gateway does with the inbound svcpass token "
|
|
148
|
-
"before forwarding upstream: unset/'' or '__strip__' strip the "
|
|
149
|
-
"svcpass-bearing header (other auth headers pass through); "
|
|
150
|
-
"'__forward__' keeps svcpass on its original header (only to a "
|
|
151
|
-
"trusted platform host); any other value (a '${ secrets.X }' / "
|
|
152
|
-
"'${ customer_secrets.X }' reference) overrides svcpass with that "
|
|
153
|
-
"credential on the source header."
|
|
154
|
-
),
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
|
|
158
131
|
class ServiceStatus(BaseModel):
|
|
159
132
|
"""Backend-assigned service **status / identity sidecar** (the ``service.json``
|
|
160
133
|
file) — *not* the authored service data.
|
|
@@ -253,7 +253,7 @@ def validate_channel_name(name: str, entity_type: str = "channel") -> str:
|
|
|
253
253
|
|
|
254
254
|
|
|
255
255
|
SUPPORTED_SERVICE_OPTIONS: dict[str, type | tuple[type, ...]] = {
|
|
256
|
-
"enrollment": dict, # Per-enrollment config: {
|
|
256
|
+
"enrollment": dict, # Per-enrollment config: {limit, limit_per_customer, limit_per_user}
|
|
257
257
|
"routing_vars": dict, # Seller-managed operational variables for template resolution at request time
|
|
258
258
|
"ops_testing_parameters": dict,
|
|
259
259
|
"prompt_recurrence": bool, # Prompt recurrence options during enrollment
|
|
@@ -264,7 +264,6 @@ SUPPORTED_SERVICE_OPTIONS: dict[str, type | tuple[type, ...]] = {
|
|
|
264
264
|
|
|
265
265
|
# Inner keys of the ``enrollment`` service option and their expected types.
|
|
266
266
|
SUPPORTED_ENROLLMENT_OPTIONS: dict[str, type | tuple[type, ...]] = {
|
|
267
|
-
"scope": str, # "customer" (default) | "global"
|
|
268
267
|
"limit": int, # global active-enrollment cap per service
|
|
269
268
|
"limit_per_customer": int,
|
|
270
269
|
"limit_per_user": int,
|
|
@@ -294,8 +293,6 @@ def _validate_enrollment_option(value: dict[str, Any]) -> list[str]:
|
|
|
294
293
|
type_name = expected_type.__name__ if isinstance(expected_type, type) else str(expected_type)
|
|
295
294
|
errors.append(f"service_options.enrollment.{key} must be {type_name}, got {type(val).__name__}")
|
|
296
295
|
continue
|
|
297
|
-
if key == "scope" and val not in ("customer", "global"):
|
|
298
|
-
errors.append(f"service_options.enrollment.scope must be 'customer' or 'global', got {val!r}")
|
|
299
296
|
if key.startswith("limit") and isinstance(val, int) and val <= 0:
|
|
300
297
|
errors.append(f"service_options.enrollment.{key} must be a positive integer, got {val}")
|
|
301
298
|
return errors
|
|
@@ -332,7 +329,7 @@ def validate_service_options(service_options: dict[str, Any] | None) -> list[str
|
|
|
332
329
|
errors.append(f"service_options.{key} must be {type_name}, got {type(value).__name__}")
|
|
333
330
|
continue
|
|
334
331
|
|
|
335
|
-
# Validate the nested enrollment config (
|
|
332
|
+
# Validate the nested enrollment config (limit*).
|
|
336
333
|
if key == "enrollment" and isinstance(value, dict):
|
|
337
334
|
errors.extend(_validate_enrollment_option(value))
|
|
338
335
|
|