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.
Files changed (41) hide show
  1. {unitysvc_core-0.2.7/src/unitysvc_core.egg-info → unitysvc_core-0.2.9}/PKG-INFO +1 -1
  2. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/pyproject.toml +1 -1
  3. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/__init__.py +0 -2
  4. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/base.py +5 -5
  5. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/offering_v1.py +10 -5
  6. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/service.py +24 -51
  7. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/validators.py +2 -5
  8. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/schema/listing_v1.json +1 -498
  9. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/schema/offering_v1.json +3 -596
  10. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9/src/unitysvc_core.egg-info}/PKG-INFO +1 -1
  11. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core.egg-info/SOURCES.txt +1 -0
  12. unitysvc_core-0.2.9/tests/test_access_interface_routing.py +34 -0
  13. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/tests/test_validator.py +3 -33
  14. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/LICENSE +0 -0
  15. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/README.md +0 -0
  16. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/setup.cfg +0 -0
  17. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/__init__.py +0 -0
  18. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/documents.py +0 -0
  19. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/listing_data.py +0 -0
  20. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/listing_v1.py +0 -0
  21. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/offering_data.py +0 -0
  22. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/pricing.py +0 -0
  23. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/promotion_data.py +0 -0
  24. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/provider_data.py +0 -0
  25. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/provider_v1.py +0 -0
  26. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/models/service_group_data.py +0 -0
  27. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/py.typed +0 -0
  28. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/schema/base.json +0 -0
  29. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/schema/promotion_v1.json +0 -0
  30. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/schema/provider_v1.json +0 -0
  31. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/schema/service_group_v1.json +0 -0
  32. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/utils.py +0 -0
  33. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core/validator.py +0 -0
  34. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
  35. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core.egg-info/requires.txt +0 -0
  36. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/src/unitysvc_core.egg-info/top_level.txt +0 -0
  37. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/tests/test_pricing.py +0 -0
  38. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/tests/test_service_group.py +0 -0
  39. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/tests/test_service_status.py +0 -0
  40. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/tests/test_utils.py +0 -0
  41. {unitysvc_core-0.2.7 → unitysvc_core-0.2.9}/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.7
3
+ Version: 0.2.9
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.7"
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 the client's LLM API format and the
247
- # format the upstream provider accepts (e.g. Anthropic Messages -> OpenAI
248
- # Chat). Config carries ``upstream_format``; the gateway performs the
249
- # conversion in-process (#923/#929).
250
- llm_translate = "llm_translate"
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
- # Override with typed models for file validation
48
- upstream_access_config: dict[str, UpstreamAccessConfigData] = Field( # type: ignore[assignment]
49
- description="Upstream access channels, keyed by channel name",
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, UpstreamAccessConfigData]) -> dict[str, UpstreamAccessConfigData]:
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 (customer side).
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: {scope, limit, limit_per_customer, limit_per_user}
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 (scope / limit*).
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