sequence-base 0.3.0__py3-none-any.whl

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 (84) hide show
  1. sequence_base/__init__.py +134 -0
  2. sequence_base/auth.py +35 -0
  3. sequence_base/codec/__init__.py +25 -0
  4. sequence_base/codec/image.py +136 -0
  5. sequence_base/codec/resize.py +147 -0
  6. sequence_base/constants/__init__.py +0 -0
  7. sequence_base/constants/env.py +14 -0
  8. sequence_base/constants/headers.py +5 -0
  9. sequence_base/constants/version.py +9 -0
  10. sequence_base/contract/__init__.py +111 -0
  11. sequence_base/contract/accounts.py +93 -0
  12. sequence_base/contract/act.py +89 -0
  13. sequence_base/contract/benchmarks.py +130 -0
  14. sequence_base/contract/connect.py +53 -0
  15. sequence_base/contract/deployments.py +197 -0
  16. sequence_base/contract/errors.py +45 -0
  17. sequence_base/contract/eval.py +201 -0
  18. sequence_base/contract/flags.py +92 -0
  19. sequence_base/contract/policy.py +172 -0
  20. sequence_base/errors/__init__.py +0 -0
  21. sequence_base/errors/transport.py +36 -0
  22. sequence_base/obs/__init__.py +0 -0
  23. sequence_base/obs/log.py +57 -0
  24. sequence_base/obs/rid.py +31 -0
  25. sequence_base/schema_export.py +238 -0
  26. sequence_base/schemas/ActionChunk.json +102 -0
  27. sequence_base/schemas/ActionContract.json +43 -0
  28. sequence_base/schemas/ActionStep.json +23 -0
  29. sequence_base/schemas/AdapterTransform.json +42 -0
  30. sequence_base/schemas/Autoscaler.json +43 -0
  31. sequence_base/schemas/BenchmarkRegisterRequest.json +302 -0
  32. sequence_base/schemas/BenchmarkSpec.json +401 -0
  33. sequence_base/schemas/BootstrapResponse.json +76 -0
  34. sequence_base/schemas/ClampInfo.json +27 -0
  35. sequence_base/schemas/CodeBundle.json +27 -0
  36. sequence_base/schemas/Concurrency.json +21 -0
  37. sequence_base/schemas/Conditions.json +59 -0
  38. sequence_base/schemas/ConnectRequest.json +16 -0
  39. sequence_base/schemas/ConnectResponse.json +81 -0
  40. sequence_base/schemas/DeploymentAccepted.json +22 -0
  41. sequence_base/schemas/DeploymentPolicy.json +101 -0
  42. sequence_base/schemas/DeploymentRequest.json +467 -0
  43. sequence_base/schemas/DeploymentStatusResponse.json +45 -0
  44. sequence_base/schemas/ErrorBody.json +32 -0
  45. sequence_base/schemas/Estimate.json +25 -0
  46. sequence_base/schemas/EvalDryRunResponse.json +41 -0
  47. sequence_base/schemas/EvalReport.json +179 -0
  48. sequence_base/schemas/EvalRequest.json +111 -0
  49. sequence_base/schemas/EvalResponse.json +88 -0
  50. sequence_base/schemas/ImageFrame.json +36 -0
  51. sequence_base/schemas/ImageSpec.json +123 -0
  52. sequence_base/schemas/ImageStepApt.json +18 -0
  53. sequence_base/schemas/ImageStepEnv.json +18 -0
  54. sequence_base/schemas/ImageStepRun.json +18 -0
  55. sequence_base/schemas/ImageStepUvPip.json +18 -0
  56. sequence_base/schemas/Lease.json +28 -0
  57. sequence_base/schemas/LossyTransform.json +21 -0
  58. sequence_base/schemas/MetricAgg.json +87 -0
  59. sequence_base/schemas/Observation.json +133 -0
  60. sequence_base/schemas/PolicyRegistration.json +438 -0
  61. sequence_base/schemas/PolicySpec.json +346 -0
  62. sequence_base/schemas/Proprioception.json +67 -0
  63. sequence_base/schemas/RenderSpec.json +22 -0
  64. sequence_base/schemas/Resources.json +29 -0
  65. sequence_base/schemas/RolloutResult.json +59 -0
  66. sequence_base/schemas/SecretCreateRequest.json +25 -0
  67. sequence_base/schemas/SecretInfo.json +44 -0
  68. sequence_base/schemas/SecretRef.json +16 -0
  69. sequence_base/schemas/SeqFlags.json +33 -0
  70. sequence_base/schemas/Step.json +68 -0
  71. sequence_base/schemas/SuiteAgg.json +26 -0
  72. sequence_base/schemas/TaskAgg.json +26 -0
  73. sequence_base/schemas/Timeouts.json +20 -0
  74. sequence_base/schemas/UsageReport.json +38 -0
  75. sequence_base/schemas/Variant.json +24 -0
  76. sequence_base/schemas/VolumeAttach.json +26 -0
  77. sequence_base/schemas/VolumeMount.json +21 -0
  78. sequence_base/schemas/VolumeRef.json +28 -0
  79. sequence_base/schemas/WarmingError.json +50 -0
  80. sequence_base/telemetry/__init__.py +13 -0
  81. sequence_base/telemetry/events.py +101 -0
  82. sequence_base-0.3.0.dist-info/METADATA +12 -0
  83. sequence_base-0.3.0.dist-info/RECORD +84 -0
  84. sequence_base-0.3.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,93 @@
1
+ """
2
+ 契约 6 — accounts / secret / billing wire types (full picture in docs/platform/accounts.md).
3
+
4
+ Secret-leak rule, mechanically checkable (goal 15). Two kinds of secret:
5
+ (a) platform-minted — `direct_secret`, TLS private key. May appear in the DOWN direction only:
6
+ ConnectResponse.direct_secret, BootstrapResponse.direct_secret + tls_key.
7
+ (b) user Secret-Manager values. May appear in the WRITE direction only:
8
+ SecretCreateRequest.env, BootstrapResponse.env (injected into the worker subprocess).
9
+ These four fields are the entire allow-list for carrying a plaintext secret value. Everything returned
10
+ to a client, reported asynchronously, or written to a report/log (SecretRef, SecretInfo, UsageReport,
11
+ eval report, /act response) exposes only names / env_keys — never a value-bearing field.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from datetime import datetime
16
+
17
+ from pydantic import BaseModel, ConfigDict, Field
18
+
19
+ from sequence_base.contract.connect import Lease
20
+
21
+ __all__ = [
22
+ "SecretRef",
23
+ "SecretCreateRequest",
24
+ "SecretInfo",
25
+ "BootstrapResponse",
26
+ "UsageReport",
27
+ ]
28
+
29
+
30
+ class SecretRef(BaseModel):
31
+ """A by-name reference to a secret (PolicySpec.secrets / BenchmarkSpec.secrets). Name only — the
32
+ value lives in Secret Manager and is injected at worker bootstrap. Never carries a value."""
33
+
34
+ model_config = ConfigDict(extra="forbid")
35
+
36
+ name: str
37
+
38
+
39
+ class SecretCreateRequest(BaseModel):
40
+ """POST /v1/secrets — one name = one group of env values (e.g. openrouter = {OPENROUTER_API_KEY}).
41
+ `env` carries plaintext values; this is the WRITE-direction allow-list field (goal 15)."""
42
+
43
+ # F6: write-direction plaintext carrier — hide_input_in_errors so a malformed body's ValidationError
44
+ # str() never echoes the raw secret value (symmetric with SecretInfo). RUNTIME config, no schema change.
45
+ model_config = ConfigDict(extra="forbid", hide_input_in_errors=True)
46
+
47
+ name: str
48
+ env: dict[str, str] = Field(..., description="ENV_KEY → value; write-only, never echoed back")
49
+
50
+
51
+ class SecretInfo(BaseModel):
52
+ """GET /v1/secrets element — metadata only, NO values. Exposes `env_keys`, not `env`."""
53
+
54
+ # hide_input_in_errors: if a malformed wire response carries a value-bearing extra field, extra="forbid"
55
+ # raises ValidationError; without this, its str() would echo the raw input (the secret value). Defense in
56
+ # depth so no caller that forgets to guard model_validate can leak it. RUNTIME config, not a wire shape —
57
+ # does NOT change the serialized schema, so CONTRACT_VERSION is unaffected.
58
+ model_config = ConfigDict(extra="forbid", hide_input_in_errors=True)
59
+
60
+ name: str
61
+ env_keys: list[str] = Field(..., description="The env var names in this secret — values withheld")
62
+ created_at: datetime
63
+ last_used_at: datetime | None = None
64
+
65
+
66
+ class BootstrapResponse(BaseModel):
67
+ """POST /v1/internal/bootstrap (worker instance-identity JWT) — everything a worker needs to serve
68
+ direct TLS `/act`. `direct_secret` + `tls_key` are DOWN-direction platform secrets; `env` is the
69
+ WRITE-direction injection of the policy's declared user secrets into its subprocess (goal 15)."""
70
+
71
+ # F6: carries down-direction platform secrets + write-direction user env — hide_input_in_errors so a
72
+ # malformed body's ValidationError never echoes plaintext (symmetric with SecretInfo). RUNTIME config.
73
+ model_config = ConfigDict(extra="forbid", hide_input_in_errors=True)
74
+
75
+ direct_secret: str = Field(..., description="Platform-minted bearer the worker's guard checks")
76
+ tls_key: str = Field(..., description="Self-signed TLS private key (down-direction platform secret)")
77
+ tls_cert: str = Field(..., description="Self-signed TLS certificate (public)")
78
+ env: dict[str, str] = Field(..., description="Policy's user secrets, expanded ENV_KEY → value")
79
+ weights_uri: str
80
+ lease: Lease
81
+
82
+
83
+ class UsageReport(BaseModel):
84
+ """POST /v1/internal/usage — the worker's async metering report. Carries counts only, no secrets.
85
+ This usage shape is defined here (sequence-base) once, so gateway and worker cannot drift."""
86
+
87
+ model_config = ConfigDict(extra="forbid")
88
+
89
+ deployment_id: str
90
+ window_start: datetime
91
+ window_end: datetime
92
+ requests: int
93
+ controlled_seconds: float
@@ -0,0 +1,89 @@
1
+ """
2
+ Shared /act contract — the obs/action shapes used by the robot SDK, the gateway, and the
3
+ container harness. One definition, three consumers, so the wire format cannot drift.
4
+
5
+ Extracted verbatim (fields) from general-sequences/backend/sequences/api/schema.py. Only the
6
+ core DATA types live here; request/response/perf envelopes (ActRequest, ActResponse, ActPerf …)
7
+ stay in the gateway, which is their only consumer.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from enum import Enum
12
+
13
+ from pydantic import BaseModel, ConfigDict, Field
14
+
15
+
16
+ class ImageFrame(BaseModel):
17
+ """One camera frame at one instant. Repeat a `view` (ordered by timestamp_ms) for a series."""
18
+
19
+ model_config = ConfigDict(extra="forbid")
20
+
21
+ view: str = Field(..., description="Logical view name, e.g. top / wrist_left / exterior_1. "
22
+ "May appear more than once to express a time series")
23
+ # URLs are NOT fetched — the string is passed to the worker untouched (an SSRF guard would be
24
+ # needed before ever dereferencing one). {schema.py:76}
25
+ data: str = Field(..., description="Base64-encoded image (PNG or JPEG). URLs are not fetched.")
26
+ timestamp_ms: int | None = Field(default=None,
27
+ description="Orders multiple frames of a view; array order used when omitted")
28
+
29
+
30
+ class Proprioception(BaseModel):
31
+ """Proprioceptive state. Only joint_positions is required; the rest is zero-padded per model."""
32
+
33
+ model_config = ConfigDict(extra="forbid")
34
+
35
+ joint_positions: list[float] = Field(..., description="Joint angles, in radians")
36
+ joint_velocities: list[float] | None = None
37
+ # [x,y,z, qx,qy,qz,qw]; the quaternion avoids gimbal lock from Euler angles.
38
+ end_effector_pose: list[float] | None = Field(default=None, min_length=7, max_length=7)
39
+ # A list, not a scalar: a dual-arm setup has two grippers.
40
+ gripper: list[float] | None = None
41
+
42
+
43
+ class Observation(BaseModel):
44
+ """The robot's world at one instant: camera frames + proprioception + a language instruction."""
45
+
46
+ model_config = ConfigDict(extra="forbid")
47
+
48
+ # max_length bounds the frame COUNT (not bytes; body size is capped by the gateway middleware).
49
+ # 256 clears the widest catalogue window while refusing a memory-exhaustion list. {schema.py:97}
50
+ images: list[ImageFrame] = Field(..., min_length=1, max_length=256)
51
+ proprioception: Proprioception
52
+ instruction: str
53
+
54
+
55
+ class ActionSpace(str, Enum):
56
+ """Must be explicit: the same floats mean opposite things across spaces — executing an absolute
57
+ joint position as a delta flings the arm into its limit stops. {schema.py:124}"""
58
+
59
+ JOINT_ABSOLUTE = "joint_absolute"
60
+ JOINT_DELTA = "joint_delta"
61
+ EE_ABSOLUTE = "ee_absolute"
62
+ EE_DELTA = "ee_delta"
63
+
64
+
65
+ class ActionStep(BaseModel):
66
+ model_config = ConfigDict(extra="forbid")
67
+
68
+ index: int
69
+ values: list[float]
70
+
71
+
72
+ class ActionChunk(BaseModel):
73
+ """A chunk of actions. `covers_seconds` is what the robot schedules the next request against —
74
+ it must ask again before this much motion finishes playing out."""
75
+
76
+ model_config = ConfigDict(extra="forbid")
77
+
78
+ steps: list[ActionStep]
79
+ # Echoed back, not left to the caller: the same floats mean different things per body, and the
80
+ # caller may not be the code that chose the model.
81
+ robot: str
82
+ action_space: ActionSpace
83
+ action_dim: int
84
+ control_frequency_hz: float
85
+ covers_seconds: float
86
+ # How many steps to execute before asking again; None → execute the whole chunk.
87
+ open_loop_horizon: int | None = None
88
+ # How long the executed steps (open_loop_horizon of them) keep the arm moving.
89
+ sustains_seconds: float | None = None
@@ -0,0 +1,130 @@
1
+ """
2
+ 契约 5 (registration half) — benchmark is a second authorizable artifact, its registration symmetric to
3
+ a policy's. `BenchmarkSpec` carries the same infra declaration a PolicySpec does, plus a structured
4
+ `Conditions` (the eval matrix), versioned `assets` volumes, an optional `render` (mp4) declaration, and
5
+ SeqFlags→method bindings covering SETUP / RESET / CHECK / SCORE. The eval *run* wire types (RolloutResult,
6
+ EvalReport, …) live in contract/eval.py.
7
+
8
+ eval-raw revision: the platform provides the reset→step→check loop as a tool; the user hooks the goal in.
9
+ `Step` is what `@seq.check` returns each step (platform never invents a success metric — the user names
10
+ them); `RenderSpec` (a.k.a. `seq.Record`) declares the mp4 the platform records. Note `Step` here is the
11
+ benchmark per-step result — a different type from ImageSpec's build `ImageStep*` variants.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
16
+
17
+ from sequence_base.contract.accounts import SecretRef
18
+ from sequence_base.contract.flags import BENCHMARK_BINDABLE, SeqFlags
19
+ from sequence_base.contract.policy import ImageSpec, VolumeRef
20
+
21
+ __all__ = [
22
+ "Variant",
23
+ "Conditions",
24
+ "RenderSpec",
25
+ "Step",
26
+ "BenchmarkSpec",
27
+ "BenchmarkRegisterRequest",
28
+ ]
29
+
30
+
31
+ class Variant(BaseModel):
32
+ """One structured variant axis of the eval matrix (e.g. axis="lighting", values=["day","night"])."""
33
+
34
+ model_config = ConfigDict(extra="forbid")
35
+
36
+ axis: str
37
+ values: list[str]
38
+
39
+
40
+ class Conditions(BaseModel):
41
+ """The eval matrix — structured, not a bare dict. `tasks` and `seeds` required; `variants` optional."""
42
+
43
+ model_config = ConfigDict(extra="forbid")
44
+
45
+ tasks: list[str]
46
+ seeds: list[int]
47
+ variants: list[Variant] = Field(default_factory=list)
48
+
49
+
50
+ class RenderSpec(BaseModel):
51
+ """a.k.a. `seq.Record` — declares the mp4 the platform records (money-shot is a platform tool, not user
52
+ glue). `camera` picks which camera stream to pull frames from; `fps` is the output frame rate. Used two
53
+ ways: as `BenchmarkSpec.render` (record the whole eval) and as `Step.record` (a per-frame hint)."""
54
+
55
+ model_config = ConfigDict(extra="forbid")
56
+
57
+ camera: str = Field(..., description="Which camera to pull frames from, e.g. 'wrist' / 'agentview'")
58
+ fps: int = 30
59
+
60
+
61
+ class Step(BaseModel):
62
+ """What `@seq.check` returns each step. The platform settles each env per-step from this and NEVER
63
+ invents a success metric — `done` is the user's call, and `metrics` keys are user-named (the platform
64
+ neither enumerates nor validates the names; each value must be a float). A per-frame `record` hint may
65
+ ask the platform to record this frame into the mp4."""
66
+
67
+ model_config = ConfigDict(extra="forbid")
68
+
69
+ done: bool
70
+ terminated: bool = False
71
+ truncated: bool = False
72
+ metrics: dict[str, float] = Field(
73
+ default_factory=dict, description="User-named metrics; platform does not know the field names"
74
+ )
75
+ record: RenderSpec | None = None
76
+
77
+
78
+ class BenchmarkSpec(BaseModel):
79
+ """What `@seq.benchmark(...)` accrues — symmetric to PolicySpec. `secrets` are by-name only (accounts
80
+ ⑦: a benchmark also references secrets by name). `assets` are versioned scene/mesh/URDF/MJCF/USD volumes
81
+ the worker mounts at /assets (reusing S2's Volume mechanism). `render` optionally declares the mp4.
82
+ `max_steps` is the platform SAFETY cap (cost backstop, records `truncated` — NOT semantic termination;
83
+ `done` is decided by @seq.check). `bindings` covers SETUP / RESET / CHECK / SCORE (ROLLOUT is deprecated
84
+ and must not appear)."""
85
+
86
+ model_config = ConfigDict(extra="forbid")
87
+
88
+ name: str
89
+ image: ImageSpec
90
+ conditions: Conditions
91
+ max_steps: int = Field(..., description="Platform safety cap; truncates and records truncated")
92
+ handler_ref: str
93
+ bindings: dict[SeqFlags, str] = Field(..., description="SETUP / RESET / CHECK / SCORE → method name")
94
+ gpu: str | list[str] | None = None
95
+ assets: list[VolumeRef] = Field(default_factory=list)
96
+ render: RenderSpec | None = None
97
+ cpu: int | None = None
98
+ memory_gb: int | None = None
99
+ secrets: list[SecretRef] = Field(default_factory=list)
100
+
101
+ @model_validator(mode="after")
102
+ def _bindings_only_benchmark_hooks(self) -> "BenchmarkSpec":
103
+ # eval-raw: only SETUP / RESET / CHECK / SCORE are bindable. ROLLOUT is deprecated (never bound) and
104
+ # a policy-lifecycle flag has no place here — either is a malformed registration, caught at build.
105
+ bad = set(self.bindings) - set(BENCHMARK_BINDABLE)
106
+ if bad:
107
+ names = ", ".join(sorted(f.name or str(f.value) for f in bad))
108
+ raise ValueError(
109
+ f"benchmark bindings may only cover SETUP / RESET / CHECK / SCORE; got disallowed {names}"
110
+ " (ROLLOUT is deprecated and must not be bound)"
111
+ )
112
+ return self
113
+
114
+
115
+ class BenchmarkRegisterRequest(BaseModel):
116
+ """POST /v1/benchmarks — register body, same types as the spec (ImageSpec / Conditions / VolumeRef /
117
+ RenderSpec). Carries assets / render / cpu / memory_gb / max_steps so the gateway can build the image,
118
+ provision the `assets` Volume, and register in one call."""
119
+
120
+ model_config = ConfigDict(extra="forbid")
121
+
122
+ name: str
123
+ image: ImageSpec
124
+ conditions: Conditions
125
+ max_steps: int
126
+ handler_ref: str
127
+ assets: list[VolumeRef] = Field(default_factory=list)
128
+ render: RenderSpec | None = None
129
+ cpu: int | None = None
130
+ memory_gb: int | None = None
@@ -0,0 +1,53 @@
1
+ """
2
+ 契约 3 — connect / lease (client → gateway; control plane, first call + renewal).
3
+
4
+ Control plane / data plane split: `connect(model)` goes through the gateway once (admission → acquire →
5
+ ensure worker up) and hands back the *direct* coordinates the client then uses to hit the worker's
6
+ `/act` without the gateway in the hot path (契约 4). The cold-start fallback is a defined wire body,
7
+ not an HTTP header alone — a 503 returns `WarmingError` (contract/errors.py), so `503 + Retry-After`
8
+ and the warming ETA are typed and checkable.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from datetime import datetime
13
+
14
+ from pydantic import BaseModel, ConfigDict, Field
15
+
16
+ # The 503 cold/warming body is defined once in contract.errors and reused here — no second shape.
17
+ from sequence_base.contract.errors import WarmingError
18
+
19
+ __all__ = ["Lease", "ConnectRequest", "ConnectResponse", "WarmingError"]
20
+
21
+
22
+ class Lease(BaseModel):
23
+ """A time-boxed grant of the direct coordinates. Renewed before `expires_at`; `epoch` is the
24
+ worker/secret generation — a client holding a stale epoch against a new worker gets 401 and must
25
+ re-connect."""
26
+
27
+ model_config = ConfigDict(extra="forbid")
28
+
29
+ ttl_s: int
30
+ expires_at: datetime
31
+ epoch: int = Field(..., description="Worker/secret generation counter; rolls on worker change")
32
+
33
+
34
+ class ConnectRequest(BaseModel):
35
+ model_config = ConfigDict(extra="forbid")
36
+
37
+ model: str = Field(..., description="Deployment name to connect to")
38
+
39
+
40
+ class ConnectResponse(BaseModel):
41
+ """200 — the full set of coordinates for a direct worker connection.
42
+
43
+ `direct_secret` is a plaintext platform-minted bearer for the worker's `/act`; it is on the
44
+ allow-list of fields permitted to carry a secret value in the down direction (契约 6 / goal 15)."""
45
+
46
+ model_config = ConfigDict(extra="forbid")
47
+
48
+ worker_url: str = Field(..., description="Worker static-IP direct URL, e.g. https://<ip>:8080")
49
+ cert_fingerprint: str = Field(..., description="Self-signed cert fingerprint the SDK pins")
50
+ direct_secret: str = Field(..., description="Bearer for the worker /act; valid within the lease")
51
+ lease: Lease
52
+ warming: bool = Field(default=False, description="True → worker still warming; see warming_eta_s")
53
+ warming_eta_s: int | None = Field(default=None, description="Seconds until warm, when warming")
@@ -0,0 +1,197 @@
1
+ """
2
+ 契约 2 — `POST /v1/deployments` (SDK `seq deploy` → gateway) and its responses.
3
+
4
+ The request is the normalized wire form of a PolicySpec: one big body (à la Modal's single `Function`
5
+ message — new config adds a field, never a new endpoint) whose sub-shapes reuse contract 1's types
6
+ (ImageSpec, VolumeMount, Concurrency, ActionContract). Field names align with the PolicySpec side —
7
+ `gpu` is normalized to list[str] here, the memory field is `memory_gb` (goal 7).
8
+
9
+ Responses have no open-ended fields: 202 → status fixed to `building`; GET → status is a restricted
10
+ enum building | ready | failed, with a failure channel (`error`) when failed (goal 8).
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import base64
15
+ import binascii
16
+ import hashlib
17
+ from enum import Enum
18
+ from typing import Literal
19
+
20
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
21
+
22
+ from sequence_base.contract.accounts import SecretRef
23
+ from sequence_base.contract.policy import (
24
+ ActionContract,
25
+ Concurrency,
26
+ ImageSpec,
27
+ VolumeMount,
28
+ )
29
+
30
+ __all__ = [
31
+ "MAX_CODE_BUNDLE_BYTES",
32
+ "CodeBundle",
33
+ "Resources",
34
+ "Autoscaler",
35
+ "Timeouts",
36
+ "VolumeAttach",
37
+ "DeploymentPolicy",
38
+ "DeploymentRequest",
39
+ "DeploymentAccepted",
40
+ "DeploymentState",
41
+ "DeploymentStatusResponse",
42
+ ]
43
+
44
+
45
+ class Resources(BaseModel):
46
+ """= Modal Resources + GPUConfig. `gpu` is list[str] (the PolicySpec `str | list[str]` normalized)."""
47
+
48
+ model_config = ConfigDict(extra="forbid")
49
+
50
+ gpu: list[str]
51
+ cpu: int
52
+ memory_gb: int
53
+
54
+
55
+ class Autoscaler(BaseModel):
56
+ """= Modal AutoscalerSettings. Defaults mirror PolicySpec so an omitted block scale-to-zeroes."""
57
+
58
+ model_config = ConfigDict(extra="forbid")
59
+
60
+ min_containers: int = 0
61
+ max_containers: int | None = None
62
+ buffer_containers: int | None = None
63
+ scaledown_window: int = 600
64
+
65
+
66
+ class Timeouts(BaseModel):
67
+ model_config = ConfigDict(extra="forbid")
68
+
69
+ request: int
70
+ startup: int
71
+
72
+
73
+ class VolumeAttach(BaseModel):
74
+ """A non-weights volume mount in the deploy body (goal 7). Distinct from `weights` (a `VolumeMount`,
75
+ a provisioned pull at the reserved `/data`): an explicit `volumes=` attachment names a persistent
76
+ volume by `vol://<name>` ref, pins its container `mount`, and carries the `create_if_missing`
77
+ provisioning intent so the gateway either binds an existing volume or creates it — the semantic the
78
+ wire `VolumeMount` deliberately does not carry. Not reused/extended from `VolumeMount` because
79
+ `create_if_missing` is meaningless for the weights pull."""
80
+
81
+ model_config = ConfigDict(extra="forbid")
82
+
83
+ uri: str
84
+ mount: str
85
+ create_if_missing: bool
86
+
87
+
88
+ class DeploymentPolicy(BaseModel):
89
+ """The policy block of the deployments body. `handler_ref` and `action` are required; `action` is the
90
+ fixed obs→ActionChunk contract, not a bare dict.
91
+
92
+ `secrets` carries the policy's declared secret BUNDLES BY NAME ONLY (list[SecretRef]) — the gateway
93
+ validates each name exists and belongs to the deployer at submit time (accounts ⑦ / S3 goal 14), and the
94
+ worker bootstrap fetches the matching env group by name. A secret VALUE never travels here: SecretRef is
95
+ name-only, so this field cannot carry one (S3 goal 15). Defaults to `[]` so a policy with no secrets is
96
+ unaffected."""
97
+
98
+ model_config = ConfigDict(extra="forbid")
99
+
100
+ handler_ref: str
101
+ action: ActionContract
102
+ streaming: bool = False
103
+ frames_per_chunk: int | None = None
104
+ secrets: list[SecretRef] = Field(default_factory=list)
105
+
106
+
107
+ MAX_CODE_BUNDLE_BYTES = 8 * 1024 * 1024
108
+ """Upper bound on the compressed code bundle. Policy source is kilobytes; anything near this is weights or
109
+ data that belongs in `weights` / a volume, and the bound keeps the deploy body well under the gateway's
110
+ request limit (base64 adds a third)."""
111
+
112
+
113
+ class CodeBundle(BaseModel):
114
+ """The policy's own source, shipped with the deploy so the worker can import `policy.handler_ref`
115
+ (what Modal's automount does for the entrypoint module). A gzip'd tar of the handler's top-level module
116
+ or package, base64 on the wire, with the sha256 of the decoded bytes so a truncated or altered upload
117
+ is refused at the edge instead of failing as an ImportError on a GPU."""
118
+
119
+ model_config = ConfigDict(extra="forbid")
120
+
121
+ format: Literal["tar.gz"] = "tar.gz"
122
+ data: str
123
+ sha256: str
124
+
125
+ @model_validator(mode="after")
126
+ def _intact(self) -> "CodeBundle":
127
+ raw = self.raw()
128
+ if len(raw) > MAX_CODE_BUNDLE_BYTES:
129
+ raise ValueError(f"code bundle is {len(raw)} bytes; the limit is {MAX_CODE_BUNDLE_BYTES} "
130
+ "(ship weights/data via `weights` or a volume, not with the code)")
131
+ if hashlib.sha256(raw).hexdigest() != self.sha256:
132
+ raise ValueError("code bundle sha256 does not match its data")
133
+ return self
134
+
135
+ def raw(self) -> bytes:
136
+ try:
137
+ return base64.b64decode(self.data, validate=True)
138
+ except binascii.Error as exc:
139
+ raise ValueError(f"code bundle data is not valid base64: {exc}") from None
140
+
141
+ @classmethod
142
+ def from_bytes(cls, raw: bytes) -> "CodeBundle":
143
+ return cls(data=base64.b64encode(raw).decode("ascii"), sha256=hashlib.sha256(raw).hexdigest())
144
+
145
+
146
+ class DeploymentRequest(BaseModel):
147
+ """The full `POST /v1/deployments` body. Required: name, image, weights, resources, policy (goal 18).
148
+ autoscaler / concurrency / timeouts are typed optional knobs with sane defaults."""
149
+
150
+ model_config = ConfigDict(extra="forbid")
151
+
152
+ name: str
153
+ image: ImageSpec
154
+ weights: VolumeMount
155
+ resources: Resources
156
+ policy: DeploymentPolicy
157
+ # The handler's source. None only when `handler_ref` is importable from the image itself (a package the
158
+ # ImageSpec installs); otherwise the worker has nothing to import and the build fails.
159
+ code: CodeBundle | None = None
160
+ # Non-weights volumes the policy declares (goal 7). List, not dict, so a mount is never duplicated as
161
+ # both key and value; the gateway only stores the dump, no consumer needs dict lookup.
162
+ volumes: list[VolumeAttach] = Field(default_factory=list)
163
+ autoscaler: Autoscaler = Field(default_factory=Autoscaler)
164
+ # An omitted @seq.concurrent must NOT send null: the gateway would then have no concurrency to scale by.
165
+ # Materialize the spec default — contracts.md §2's example {"max_inputs": 1, "target_inputs": 1}, i.e.
166
+ # one input per container (Modal's no-`@modal.concurrent` semantics) — same discipline as `autoscaler`
167
+ # (goal 13).
168
+ concurrency: Concurrency = Field(default_factory=lambda: Concurrency(max_inputs=1, target_inputs=1))
169
+ timeouts: Timeouts | None = None
170
+
171
+
172
+ class DeploymentAccepted(BaseModel):
173
+ """202 — the build is async; the id comes back first, status pinned to `building`."""
174
+
175
+ model_config = ConfigDict(extra="forbid")
176
+
177
+ deployment_id: str
178
+ status: Literal["building"] = "building"
179
+
180
+
181
+ class DeploymentState(str, Enum):
182
+ """GET status — a restricted enum, not a free string (same discipline as EvalStatus)."""
183
+
184
+ BUILDING = "building"
185
+ READY = "ready"
186
+ FAILED = "failed"
187
+
188
+
189
+ class DeploymentStatusResponse(BaseModel):
190
+ """GET /v1/deployments/{id}. `error` carries the build-failure reason when status is FAILED —
191
+ the failure channel the spec and the old goal both lacked (goal 8)."""
192
+
193
+ model_config = ConfigDict(extra="forbid")
194
+
195
+ status: DeploymentState
196
+ model: str
197
+ error: str | None = Field(default=None, description="Build-failure reason; set only when FAILED")
@@ -0,0 +1,45 @@
1
+ """
2
+ Shared error-body wire types (契约 4 / 契约 6 client semantics). These are the *bodies* returned to a
3
+ client on the sad path — distinct from `sequence_base.errors.transport`, which is the internal
4
+ exception taxonomy the gateway / harness raise. Defined here once so "is an error response a wire
5
+ type?" has one answer: yes, and this is its shape.
6
+
7
+ 402 OutOfCredit → ErrorBody(code=OUT_OF_CREDIT) (connect / lease renew / eval balance)
8
+ 401 Unauthorized → ErrorBody(code=UNAUTHORIZED) (secret expired / epoch rolled)
9
+ 503 Warming → WarmingError(code=WARMING, ...) (worker cold / warming; carries retry hints)
10
+
11
+ `WarmingError` is the single definition of the 503 body: contract 3's connect cold-path (503 +
12
+ Retry-After / warming eta) reuses it rather than declaring a second {retry_after_s, eta_s} shape.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from enum import Enum
17
+
18
+ from pydantic import BaseModel, ConfigDict, Field
19
+
20
+
21
+ class ErrorCode(str, Enum):
22
+ """Restricted set — a client switches on this, so it is an enum, not a free string."""
23
+
24
+ OUT_OF_CREDIT = "out_of_credit"
25
+ UNAUTHORIZED = "unauthorized"
26
+ WARMING = "warming"
27
+
28
+
29
+ class ErrorBody(BaseModel):
30
+ """The base error body (402 / 401): a machine-readable `code` plus a human `message`."""
31
+
32
+ model_config = ConfigDict(extra="forbid")
33
+
34
+ code: ErrorCode
35
+ message: str
36
+
37
+
38
+ class WarmingError(ErrorBody):
39
+ """503 body — worker cold / warming. Adds the two retry hints so `503 + Retry-After` and the
40
+ warming ETA are expressible in the type, not smuggled in an HTTP header alone. `code` defaults to
41
+ WARMING; a caller waits `retry_after_s` and, when known, learns `eta_s` until hot."""
42
+
43
+ code: ErrorCode = ErrorCode.WARMING
44
+ retry_after_s: int
45
+ eta_s: int | None = Field(default=None, description="Seconds until warm, when estimable")