unitysvc-core 0.2.24__tar.gz → 0.2.26__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.24/src/unitysvc_core.egg-info → unitysvc_core-0.2.26}/PKG-INFO +1 -1
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/pyproject.toml +1 -1
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/models/__init__.py +4 -4
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/models/base.py +34 -16
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/models/provider_data.py +8 -5
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/models/service.py +5 -5
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/models/validators.py +29 -12
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/schema/provider_v1.json +21 -27
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26/src/unitysvc_core.egg-info}/PKG-INFO +1 -1
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/tests/test_provider_account_rate_limits.py +59 -1
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/tests/test_validators.py +21 -4
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/LICENSE +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/README.md +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/setup.cfg +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/__init__.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/models/documents.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/models/listing_data.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/models/listing_v1.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/models/offering_data.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/models/offering_v1.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/models/pricing.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/models/promotion_data.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/models/provider_v1.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/models/service_group_data.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/py.typed +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/schema/base.json +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/schema/listing_v1.json +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/schema/offering_v1.json +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/schema/promotion_v1.json +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/schema/service_group_v1.json +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/utils.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/validator.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core.egg-info/SOURCES.txt +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core.egg-info/requires.txt +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core.egg-info/top_level.txt +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/tests/test_access_interface_routing.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/tests/test_hidden_path_skip.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/tests/test_pricing.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/tests/test_service_group.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/tests/test_service_status.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/tests/test_utils.py +0 -0
- {unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/tests/test_validator.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.26"
|
|
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" }]
|
|
@@ -17,13 +17,13 @@ from .base import (
|
|
|
17
17
|
PricingTypeEnum,
|
|
18
18
|
ProviderStatusEnum,
|
|
19
19
|
QuotaResetCycleEnum,
|
|
20
|
-
|
|
20
|
+
RateLimitUnit,
|
|
21
21
|
RequestTransformEnum,
|
|
22
22
|
SellerTypeEnum,
|
|
23
23
|
ServiceGroupStatusEnum,
|
|
24
24
|
ServiceTypeEnum,
|
|
25
25
|
ServiceVisibilityEnum,
|
|
26
|
-
|
|
26
|
+
TimeWindow,
|
|
27
27
|
UpstreamStatusEnum, # Backwards compatibility alias for OfferingStatusEnum
|
|
28
28
|
)
|
|
29
29
|
from .documents import DocumentData
|
|
@@ -130,14 +130,14 @@ __all__ = [
|
|
|
130
130
|
"PricingTypeEnum",
|
|
131
131
|
"ProviderStatusEnum",
|
|
132
132
|
"QuotaResetCycleEnum",
|
|
133
|
-
"
|
|
133
|
+
"RateLimitUnit",
|
|
134
134
|
"RequestTransformEnum",
|
|
135
135
|
"ROUTABLE_GROUP_TYPES",
|
|
136
136
|
"SellerTypeEnum",
|
|
137
137
|
"ServiceGroupStatusEnum",
|
|
138
138
|
"ServiceTypeEnum",
|
|
139
139
|
"ServiceVisibilityEnum",
|
|
140
|
-
"
|
|
140
|
+
"TimeWindow",
|
|
141
141
|
"UpstreamStatusEnum", # Backwards compatibility alias for OfferingStatusEnum
|
|
142
142
|
# Pricing — primitives
|
|
143
143
|
"PriceStr",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Enum types and basic definitions shared across the data models.
|
|
2
2
|
|
|
3
|
-
This module contains only enums
|
|
3
|
+
This module contains only enums, closed-value type aliases, and simple
|
|
4
|
+
constants. Data classes with
|
|
4
5
|
behavior (pricing, documents, service constraints, etc.) and validation
|
|
5
6
|
functions live in their own modules:
|
|
6
7
|
|
|
@@ -13,6 +14,7 @@ functions live in their own modules:
|
|
|
13
14
|
from __future__ import annotations
|
|
14
15
|
|
|
15
16
|
from enum import StrEnum
|
|
17
|
+
from typing import Literal
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class AccessMethodEnum(StrEnum):
|
|
@@ -234,13 +236,37 @@ class QuotaResetCycleEnum(StrEnum):
|
|
|
234
236
|
yearly = "yearly"
|
|
235
237
|
|
|
236
238
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
239
|
+
# Rate-limit unit and time window: Literals, not StrEnums.
|
|
240
|
+
#
|
|
241
|
+
# Both are used by ``ProviderAccountRateLimit``, which is nested inside
|
|
242
|
+
# ``ProviderData`` — a model the unitysvc backend serves in BOTH directions (a
|
|
243
|
+
# request-body field on ``POST /seller/services``, a response field on
|
|
244
|
+
# ``GET /seller/services/{id}``).
|
|
245
|
+
#
|
|
246
|
+
# An enum one level down makes pydantic treat the *outer* model's validation and
|
|
247
|
+
# serialization schemas as distinct, so FastAPI emits an ``X-Input``/``X-Output``
|
|
248
|
+
# pair — both halves carrying the same ``title``. Python SDK codegen names a
|
|
249
|
+
# top-level component from its title, so the halves collide on one class name:
|
|
250
|
+
# the generator drops the second and silently removes every schema that
|
|
251
|
+
# ``$ref``s it. That is how ``ServiceDetailResponse`` vanished from
|
|
252
|
+
# unitysvc-sellers 0.3.7+, leaving ``client.services.get()`` returning raw dicts
|
|
253
|
+
# (unitysvc/unitysvc-sellers#205).
|
|
254
|
+
#
|
|
255
|
+
# An enum *directly* on a served model is fine, and the rendered JSON schema is
|
|
256
|
+
# identical either way — the split is an artifact of how the two modes are
|
|
257
|
+
# deduplicated, not a real difference. A Literal sidesteps it and keeps the
|
|
258
|
+
# values just as closed: same ``enum`` in the JSON schema, same rejection of an
|
|
259
|
+
# unlisted value, and a string-literal union in every generated client.
|
|
260
|
+
RateLimitUnit = Literal[
|
|
261
|
+
"requests",
|
|
262
|
+
"tokens",
|
|
263
|
+
"input_tokens",
|
|
264
|
+
"output_tokens",
|
|
265
|
+
"bytes",
|
|
266
|
+
"concurrent",
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
TimeWindow = Literal["second", "minute", "hour", "day", "month"]
|
|
244
270
|
|
|
245
271
|
|
|
246
272
|
class RequestTransformEnum(StrEnum):
|
|
@@ -317,14 +343,6 @@ class ServiceVisibilityEnum(StrEnum):
|
|
|
317
343
|
private = "private"
|
|
318
344
|
|
|
319
345
|
|
|
320
|
-
class TimeWindowEnum(StrEnum):
|
|
321
|
-
second = "second"
|
|
322
|
-
minute = "minute"
|
|
323
|
-
hour = "hour"
|
|
324
|
-
day = "day"
|
|
325
|
-
month = "month"
|
|
326
|
-
|
|
327
|
-
|
|
328
346
|
class OfferingStatusEnum(StrEnum):
|
|
329
347
|
"""
|
|
330
348
|
Status values that sellers can set for service offerings.
|
|
@@ -13,7 +13,7 @@ from typing import Any
|
|
|
13
13
|
|
|
14
14
|
from pydantic import BaseModel, ConfigDict, EmailStr, Field, HttpUrl, model_validator
|
|
15
15
|
|
|
16
|
-
from .base import ProviderStatusEnum,
|
|
16
|
+
from .base import ProviderStatusEnum, RateLimitUnit, TimeWindow
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class ProviderAccountRateLimit(BaseModel):
|
|
@@ -48,9 +48,12 @@ class ProviderAccountRateLimit(BaseModel):
|
|
|
48
48
|
|
|
49
49
|
limit: int = Field(gt=0, description="Maximum allowed — in flight for `concurrent`, per window otherwise")
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
# RateLimitUnit / TimeWindow are Literals rather than enums, because an enum
|
|
52
|
+
# nested this deep splits every model that carries one. See base.py for the
|
|
53
|
+
# mechanism — it is not a style preference.
|
|
54
|
+
unit: RateLimitUnit = Field(description="What is being limited (requests, tokens, concurrent, …)")
|
|
52
55
|
|
|
53
|
-
window:
|
|
56
|
+
window: TimeWindow | None = Field(
|
|
54
57
|
default=None,
|
|
55
58
|
description="Time window. Omitted for `concurrent`, which is a gauge rather than a counter.",
|
|
56
59
|
)
|
|
@@ -72,11 +75,11 @@ class ProviderAccountRateLimit(BaseModel):
|
|
|
72
75
|
it. Requiring the field to match the unit stops a limit being authored
|
|
73
76
|
that the enforcement layer cannot honour as written.
|
|
74
77
|
"""
|
|
75
|
-
if self.unit
|
|
78
|
+
if self.unit == "concurrent":
|
|
76
79
|
if self.window is not None:
|
|
77
80
|
raise ValueError("`concurrent` is an in-flight gauge and takes no window; omit it")
|
|
78
81
|
elif self.window is None:
|
|
79
|
-
raise ValueError(f"`{self.unit
|
|
82
|
+
raise ValueError(f"`{self.unit}` is counted over a window; set `window`")
|
|
80
83
|
return self
|
|
81
84
|
|
|
82
85
|
|
|
@@ -14,8 +14,8 @@ from .base import (
|
|
|
14
14
|
ContentFilterEnum,
|
|
15
15
|
OveragePolicyEnum,
|
|
16
16
|
QuotaResetCycleEnum,
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
RateLimitUnit,
|
|
18
|
+
TimeWindow,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
|
|
@@ -26,8 +26,8 @@ class RateLimit(BaseModel):
|
|
|
26
26
|
|
|
27
27
|
# Core rate limit definition
|
|
28
28
|
limit: int = Field(description="Maximum allowed in the time window")
|
|
29
|
-
unit:
|
|
30
|
-
window:
|
|
29
|
+
unit: RateLimitUnit = Field(description="What is being limited")
|
|
30
|
+
window: TimeWindow = Field(description="Time window for the limit")
|
|
31
31
|
|
|
32
32
|
# Optional additional info
|
|
33
33
|
description: str | None = Field(default=None, max_length=255, description="Human-readable description")
|
|
@@ -52,7 +52,7 @@ class ServiceConstraints(BaseModel):
|
|
|
52
52
|
# Usage Quotas & Billing
|
|
53
53
|
monthly_quota: int | None = Field(default=None, description="Monthly usage quota (requests, tokens, etc.)")
|
|
54
54
|
daily_quota: int | None = Field(default=None, description="Daily usage quota (requests, tokens, etc.)")
|
|
55
|
-
quota_unit:
|
|
55
|
+
quota_unit: RateLimitUnit | None = Field(default=None, description="Unit for quota limits")
|
|
56
56
|
quota_reset_cycle: QuotaResetCycleEnum | None = Field(default=None, description="How often quotas reset")
|
|
57
57
|
overage_policy: OveragePolicyEnum | None = Field(default=None, description="What happens when quota is exceeded")
|
|
58
58
|
|
|
@@ -815,20 +815,39 @@ def validate_mcp_offering(data: dict[str, Any] | None) -> list[str]:
|
|
|
815
815
|
"""
|
|
816
816
|
if not data or not isinstance(data, dict):
|
|
817
817
|
return []
|
|
818
|
-
if data.get("service_type") != "mcp":
|
|
819
|
-
return []
|
|
820
818
|
|
|
819
|
+
service_type = data.get("service_type")
|
|
820
|
+
capabilities = data.get("capabilities")
|
|
821
821
|
channels = data.get("upstream_access_config")
|
|
822
|
+
mcp_channels = (
|
|
823
|
+
sorted(
|
|
824
|
+
name
|
|
825
|
+
for name, config in (channels or {}).items()
|
|
826
|
+
if isinstance(config, dict) and config.get("access_method") == "mcp"
|
|
827
|
+
)
|
|
828
|
+
if isinstance(channels, dict)
|
|
829
|
+
else []
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
if service_type != "mcp":
|
|
833
|
+
declarations: list[str] = []
|
|
834
|
+
if isinstance(capabilities, list) and "mcp-delegation" in capabilities:
|
|
835
|
+
declarations.append("capability 'mcp-delegation'")
|
|
836
|
+
if mcp_channels:
|
|
837
|
+
declarations.append("MCP upstream channel(s) " + ", ".join(repr(name) for name in mcp_channels))
|
|
838
|
+
if declarations:
|
|
839
|
+
return [
|
|
840
|
+
f"service_type {service_type!r} may not declare "
|
|
841
|
+
+ "; ".join(declarations)
|
|
842
|
+
+ "; use service_type 'mcp' instead"
|
|
843
|
+
]
|
|
844
|
+
return []
|
|
845
|
+
|
|
822
846
|
if not channels or not isinstance(channels, dict):
|
|
823
847
|
return ["upstream_access_config: an MCP service must declare at least one channel"]
|
|
824
848
|
|
|
825
|
-
if not
|
|
826
|
-
|
|
827
|
-
):
|
|
828
|
-
return [
|
|
829
|
-
"upstream_access_config: an MCP service must have at least one channel "
|
|
830
|
-
"with access_method 'mcp'"
|
|
831
|
-
]
|
|
849
|
+
if not mcp_channels:
|
|
850
|
+
return ["upstream_access_config: an MCP service must have at least one channel with access_method 'mcp'"]
|
|
832
851
|
|
|
833
852
|
return []
|
|
834
853
|
|
|
@@ -901,9 +920,7 @@ def validate_listing_mcp_base_urls(user_access_interfaces: dict[str, Any] | None
|
|
|
901
920
|
f"a non-empty 'namespace' in routing_key"
|
|
902
921
|
)
|
|
903
922
|
else:
|
|
904
|
-
errors.extend(
|
|
905
|
-
validate_mcp_namespace(namespace, f"{field}.routing_key.namespace")
|
|
906
|
-
)
|
|
923
|
+
errors.extend(validate_mcp_namespace(namespace, f"{field}.routing_key.namespace"))
|
|
907
924
|
|
|
908
925
|
return errors
|
|
909
926
|
|
|
@@ -187,20 +187,37 @@
|
|
|
187
187
|
"type": "string"
|
|
188
188
|
},
|
|
189
189
|
"unit": {
|
|
190
|
-
"
|
|
191
|
-
"
|
|
190
|
+
"description": "What is being limited (requests, tokens, concurrent, …)",
|
|
191
|
+
"enum": [
|
|
192
|
+
"requests",
|
|
193
|
+
"tokens",
|
|
194
|
+
"input_tokens",
|
|
195
|
+
"output_tokens",
|
|
196
|
+
"bytes",
|
|
197
|
+
"concurrent"
|
|
198
|
+
],
|
|
199
|
+
"title": "Unit",
|
|
200
|
+
"type": "string"
|
|
192
201
|
},
|
|
193
202
|
"window": {
|
|
194
203
|
"anyOf": [
|
|
195
204
|
{
|
|
196
|
-
"
|
|
205
|
+
"enum": [
|
|
206
|
+
"second",
|
|
207
|
+
"minute",
|
|
208
|
+
"hour",
|
|
209
|
+
"day",
|
|
210
|
+
"month"
|
|
211
|
+
],
|
|
212
|
+
"type": "string"
|
|
197
213
|
},
|
|
198
214
|
{
|
|
199
215
|
"type": "null"
|
|
200
216
|
}
|
|
201
217
|
],
|
|
202
218
|
"default": null,
|
|
203
|
-
"description": "Time window. Omitted for `concurrent`, which is a gauge rather than a counter."
|
|
219
|
+
"description": "Time window. Omitted for `concurrent`, which is a gauge rather than a counter.",
|
|
220
|
+
"title": "Window"
|
|
204
221
|
}
|
|
205
222
|
},
|
|
206
223
|
"required": [
|
|
@@ -221,18 +238,6 @@
|
|
|
221
238
|
"title": "ProviderStatusEnum",
|
|
222
239
|
"type": "string"
|
|
223
240
|
},
|
|
224
|
-
"RateLimitUnitEnum": {
|
|
225
|
-
"enum": [
|
|
226
|
-
"requests",
|
|
227
|
-
"tokens",
|
|
228
|
-
"input_tokens",
|
|
229
|
-
"output_tokens",
|
|
230
|
-
"bytes",
|
|
231
|
-
"concurrent"
|
|
232
|
-
],
|
|
233
|
-
"title": "RateLimitUnitEnum",
|
|
234
|
-
"type": "string"
|
|
235
|
-
},
|
|
236
241
|
"ServicesPopulator": {
|
|
237
242
|
"additionalProperties": false,
|
|
238
243
|
"description": "Configuration for automatically populating service data.",
|
|
@@ -289,17 +294,6 @@
|
|
|
289
294
|
},
|
|
290
295
|
"title": "ServicesPopulator",
|
|
291
296
|
"type": "object"
|
|
292
|
-
},
|
|
293
|
-
"TimeWindowEnum": {
|
|
294
|
-
"enum": [
|
|
295
|
-
"second",
|
|
296
|
-
"minute",
|
|
297
|
-
"hour",
|
|
298
|
-
"day",
|
|
299
|
-
"month"
|
|
300
|
-
],
|
|
301
|
-
"title": "TimeWindowEnum",
|
|
302
|
-
"type": "string"
|
|
303
297
|
}
|
|
304
298
|
},
|
|
305
299
|
"additionalProperties": false,
|
|
@@ -7,10 +7,13 @@ can state an individual customer's share, which depends on who is active at
|
|
|
7
7
|
request time.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
from typing import get_args
|
|
11
|
+
|
|
10
12
|
import pytest
|
|
11
13
|
from pydantic import ValidationError
|
|
12
14
|
|
|
13
15
|
from unitysvc_core.models import ProviderAccountRateLimit, ProviderV1
|
|
16
|
+
from unitysvc_core.models.base import RateLimitUnit, TimeWindow
|
|
14
17
|
|
|
15
18
|
BASE = {
|
|
16
19
|
"name": "fireworks",
|
|
@@ -84,8 +87,63 @@ def test_several_dimensions_coexist() -> None:
|
|
|
84
87
|
{"name": "fireworks_input_tokens", "limit": 60000, "unit": "input_tokens", "window": "minute"},
|
|
85
88
|
],
|
|
86
89
|
)
|
|
87
|
-
assert [rl.unit
|
|
90
|
+
assert [rl.unit for rl in provider.rate_limits] == [
|
|
88
91
|
"concurrent",
|
|
89
92
|
"requests",
|
|
90
93
|
"input_tokens",
|
|
91
94
|
]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_the_value_sets_are_the_documented_ones() -> None:
|
|
98
|
+
"""``RateLimitUnit``/``TimeWindow`` are the only definition of these sets.
|
|
99
|
+
|
|
100
|
+
They replaced ``RateLimitUnitEnum``/``TimeWindowEnum`` (see base.py for why
|
|
101
|
+
a Literal and not an enum). Pinning the members here makes a silent
|
|
102
|
+
widening or narrowing fail rather than quietly changing what a provider may
|
|
103
|
+
author and what every generated client will accept.
|
|
104
|
+
"""
|
|
105
|
+
assert set(get_args(RateLimitUnit)) == {
|
|
106
|
+
"requests",
|
|
107
|
+
"tokens",
|
|
108
|
+
"input_tokens",
|
|
109
|
+
"output_tokens",
|
|
110
|
+
"bytes",
|
|
111
|
+
"concurrent",
|
|
112
|
+
}
|
|
113
|
+
assert set(get_args(TimeWindow)) == {"second", "minute", "hour", "day", "month"}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_the_nested_model_pulls_in_no_named_subschema() -> None:
|
|
117
|
+
"""The condition that broke Python SDK codegen — kept from coming back.
|
|
118
|
+
|
|
119
|
+
An enum (or any named sub-schema) one level down makes pydantic treat the
|
|
120
|
+
*outer* model's validation and serialization schemas as distinct. FastAPI
|
|
121
|
+
then splits every model that carries this one into an ``X-Input``/
|
|
122
|
+
``X-Output`` pair whose halves share a title, and Python SDK codegen drops
|
|
123
|
+
one of them plus everything referencing it — silently. ``ProviderData`` is
|
|
124
|
+
such a carrier: it is a request body on ``POST /seller/services`` and a
|
|
125
|
+
response field on ``GET /seller/services/{id}``.
|
|
126
|
+
"""
|
|
127
|
+
assert ProviderAccountRateLimit.model_json_schema().get("$defs", {}) == {}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_the_value_set_is_still_closed() -> None:
|
|
131
|
+
with pytest.raises(ValidationError):
|
|
132
|
+
ProviderAccountRateLimit(name="fireworks_typo", limit=10, unit="concurent")
|
|
133
|
+
with pytest.raises(ValidationError):
|
|
134
|
+
ProviderAccountRateLimit(name="fireworks_rpm", limit=10, unit="requests", window="fortnight")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_values_read_back_as_plain_strings() -> None:
|
|
138
|
+
"""The one behaviour change from dropping the enums.
|
|
139
|
+
|
|
140
|
+
``.unit``/``.window`` are ``str``, not enum members, so ``.unit.value`` no
|
|
141
|
+
longer works — ``.unit`` does. Consumers already saw strings over the wire
|
|
142
|
+
and in ``model_dump(mode="json")``; only in-process attribute access
|
|
143
|
+
differs.
|
|
144
|
+
"""
|
|
145
|
+
limit = ProviderAccountRateLimit(name="fireworks_rpm", limit=600, unit="requests", window="minute")
|
|
146
|
+
|
|
147
|
+
assert limit.unit == "requests"
|
|
148
|
+
assert type(limit.unit) is str
|
|
149
|
+
assert limit.model_dump(mode="json")["window"] == "minute"
|
|
@@ -583,9 +583,28 @@ class TestValidateMcpOffering:
|
|
|
583
583
|
def test_valid_mcp_offering_passes(self) -> None:
|
|
584
584
|
assert validate_mcp_offering(self._offering()) == []
|
|
585
585
|
|
|
586
|
-
def
|
|
586
|
+
def test_non_mcp_offering_without_mcp_declarations_is_ignored(self) -> None:
|
|
587
587
|
assert validate_mcp_offering({"service_type": "llm"}) == []
|
|
588
588
|
|
|
589
|
+
def test_non_mcp_offering_cannot_declare_mcp_delegation(self) -> None:
|
|
590
|
+
errors = validate_mcp_offering({"service_type": "proxy", "capabilities": ["mcp-delegation"]})
|
|
591
|
+
assert errors
|
|
592
|
+
assert "mcp-delegation" in errors[0]
|
|
593
|
+
assert "service_type 'mcp'" in errors[0]
|
|
594
|
+
|
|
595
|
+
def test_non_mcp_offering_cannot_declare_an_mcp_channel(self) -> None:
|
|
596
|
+
errors = validate_mcp_offering(
|
|
597
|
+
{
|
|
598
|
+
"service_type": "proxy",
|
|
599
|
+
"upstream_access_config": {
|
|
600
|
+
"upstream": {"access_method": "mcp", "base_url": "https://mcp.example.test"}
|
|
601
|
+
},
|
|
602
|
+
}
|
|
603
|
+
)
|
|
604
|
+
assert errors
|
|
605
|
+
assert "MCP upstream channel" in errors[0]
|
|
606
|
+
assert "service_type 'mcp'" in errors[0]
|
|
607
|
+
|
|
589
608
|
def test_requires_a_channel(self) -> None:
|
|
590
609
|
errors = validate_mcp_offering(self._offering(upstream_access_config={}))
|
|
591
610
|
assert errors
|
|
@@ -593,9 +612,7 @@ class TestValidateMcpOffering:
|
|
|
593
612
|
|
|
594
613
|
def test_requires_an_mcp_access_method_channel(self) -> None:
|
|
595
614
|
errors = validate_mcp_offering(
|
|
596
|
-
self._offering(
|
|
597
|
-
upstream_access_config={"x": {"access_method": "http", "base_url": "https://x"}}
|
|
598
|
-
)
|
|
615
|
+
self._offering(upstream_access_config={"x": {"access_method": "http", "base_url": "https://x"}})
|
|
599
616
|
)
|
|
600
617
|
assert errors
|
|
601
618
|
assert "access_method 'mcp'" 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
|
{unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/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
|
{unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/src/unitysvc_core/schema/service_group_v1.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unitysvc_core-0.2.24 → unitysvc_core-0.2.26}/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
|