positronic-platform-client 0.37.1__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.
- positronic_platform_client-0.37.1/.gitignore +167 -0
- positronic_platform_client-0.37.1/PKG-INFO +500 -0
- positronic_platform_client-0.37.1/README.md +488 -0
- positronic_platform_client-0.37.1/platform_client/billing.py +159 -0
- positronic_platform_client-0.37.1/platform_client/boards.py +24 -0
- positronic_platform_client-0.37.1/platform_client/catalog.py +62 -0
- positronic_platform_client-0.37.1/platform_client/client.py +336 -0
- positronic_platform_client-0.37.1/platform_client/config.py +147 -0
- positronic_platform_client-0.37.1/platform_client/device_flow.py +506 -0
- positronic_platform_client-0.37.1/platform_client/enums.py +240 -0
- positronic_platform_client-0.37.1/platform_client/errors.py +136 -0
- positronic_platform_client-0.37.1/platform_client/eval_plan.py +713 -0
- positronic_platform_client-0.37.1/platform_client/identity_providers.py +104 -0
- positronic_platform_client-0.37.1/platform_client/ids.py +95 -0
- positronic_platform_client-0.37.1/platform_client/model_config.py +13 -0
- positronic_platform_client-0.37.1/platform_client/policy_container.py +97 -0
- positronic_platform_client-0.37.1/platform_client/policy_images.py +50 -0
- positronic_platform_client-0.37.1/platform_client/py.typed +0 -0
- positronic_platform_client-0.37.1/platform_client/requests.py +106 -0
- positronic_platform_client-0.37.1/platform_client/responses.py +565 -0
- positronic_platform_client-0.37.1/platform_client/routes.py +30 -0
- positronic_platform_client-0.37.1/platform_client/tasks.py +33 -0
- positronic_platform_client-0.37.1/platform_client/tests/test_client.py +992 -0
- positronic_platform_client-0.37.1/platform_client/tests/test_config.py +148 -0
- positronic_platform_client-0.37.1/platform_client/tests/test_device_flow.py +1167 -0
- positronic_platform_client-0.37.1/platform_client/tests/test_enums.py +169 -0
- positronic_platform_client-0.37.1/platform_client/tests/test_eval_plan.py +961 -0
- positronic_platform_client-0.37.1/platform_client/tests/test_ids.py +129 -0
- positronic_platform_client-0.37.1/platform_client/tests/test_models.py +1206 -0
- positronic_platform_client-0.37.1/platform_client/tests/test_policy_container.py +117 -0
- positronic_platform_client-0.37.1/platform_client/tests/test_policy_images.py +77 -0
- positronic_platform_client-0.37.1/platform_client/tests/test_tasks.py +31 -0
- positronic_platform_client-0.37.1/pyproject.toml +30 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# poetry
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
102
|
+
#poetry.lock
|
|
103
|
+
|
|
104
|
+
# pdm
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
106
|
+
#pdm.lock
|
|
107
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
108
|
+
# in version control.
|
|
109
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
110
|
+
.pdm.toml
|
|
111
|
+
|
|
112
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
113
|
+
__pypackages__/
|
|
114
|
+
|
|
115
|
+
# Celery stuff
|
|
116
|
+
celerybeat-schedule
|
|
117
|
+
celerybeat.pid
|
|
118
|
+
|
|
119
|
+
# SageMath parsed files
|
|
120
|
+
*.sage.py
|
|
121
|
+
|
|
122
|
+
# Environments
|
|
123
|
+
.env
|
|
124
|
+
.venv
|
|
125
|
+
.venv-typecheck
|
|
126
|
+
env/
|
|
127
|
+
venv/
|
|
128
|
+
ENV/
|
|
129
|
+
env.bak/
|
|
130
|
+
venv.bak/
|
|
131
|
+
|
|
132
|
+
# Spyder project settings
|
|
133
|
+
.spyderproject
|
|
134
|
+
.spyproject
|
|
135
|
+
|
|
136
|
+
# Rope project settings
|
|
137
|
+
.ropeproject
|
|
138
|
+
|
|
139
|
+
# mkdocs documentation
|
|
140
|
+
/site
|
|
141
|
+
|
|
142
|
+
# mypy
|
|
143
|
+
.mypy_cache/
|
|
144
|
+
.dmypy.json
|
|
145
|
+
dmypy.json
|
|
146
|
+
|
|
147
|
+
# Pyre type checker
|
|
148
|
+
.pyre/
|
|
149
|
+
|
|
150
|
+
# pytype static type analyzer
|
|
151
|
+
.pytype/
|
|
152
|
+
|
|
153
|
+
# Cython debug symbols
|
|
154
|
+
cython_debug/
|
|
155
|
+
|
|
156
|
+
# PyCharm
|
|
157
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
158
|
+
# be added to the global gitignore or merged into this project gitignore. For a PyCharm
|
|
159
|
+
# project, it is generally recommended to exclude comment-based line additions.
|
|
160
|
+
.idea/
|
|
161
|
+
|
|
162
|
+
# VSCode
|
|
163
|
+
.vscode/
|
|
164
|
+
*.code-workspace
|
|
165
|
+
|
|
166
|
+
# Environment variables
|
|
167
|
+
.env*
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: positronic-platform-client
|
|
3
|
+
Version: 0.37.1
|
|
4
|
+
Summary: Typed wire contract and HTTP client for the Positronic evaluation platform API
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: httpx>=0.27
|
|
8
|
+
Requires-Dist: positronic-evals==0.1.0
|
|
9
|
+
Requires-Dist: pydantic>=2.11
|
|
10
|
+
Requires-Dist: typing-extensions>=4.12.2
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# positronic-platform-client
|
|
14
|
+
|
|
15
|
+
The wire contract for the Positronic evaluation platform, and a thin HTTP client over it: request
|
|
16
|
+
and response models, id and enum types, the error envelope, and one `PlatformClient` method per API
|
|
17
|
+
endpoint.
|
|
18
|
+
|
|
19
|
+
> **Alpha, under rapid development.** Names, fields, endpoints and behaviour change without notice,
|
|
20
|
+
> and nothing here is covered by a backwards-compatibility guarantee. Pin the exact version you
|
|
21
|
+
> tested against, and expect to edit your code when you move off it.
|
|
22
|
+
|
|
23
|
+
The library depends on `pydantic`, `httpx`, `typing-extensions` and `positronic-evals`, which holds
|
|
24
|
+
the eval names and definitions (`evals/`). So a service that only speaks to the platform installs it on
|
|
25
|
+
its own, at the exact version it was written against:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv add "positronic-platform-client==0.37.1"
|
|
29
|
+
uv add "positronic-platform-client @ git+https://github.com/Positronic-Robotics/positronic@<tag or commit>#subdirectory=client"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The package is not on PyPI yet. Until it is, use the second line, pinned to a tag or a commit. The
|
|
33
|
+
client pins `positronic-evals` exactly, and the index supplies it. So a git install of an unreleased
|
|
34
|
+
client needs that `positronic-evals` version released first. The
|
|
35
|
+
platform serves the same contract as an OpenAPI schema at `<platform>/openapi.json`, and `/docs`
|
|
36
|
+
browses it.
|
|
37
|
+
|
|
38
|
+
`platform_client` never imports `positronic`. One command ships here, `platform-register`, which
|
|
39
|
+
mints a key through a sign-in. The commands that drive an eval, `positronic eval run`, `eval status`,
|
|
40
|
+
`eval list`, `eval catalog` and `positronic account`, ship with `positronic`, which depends on this
|
|
41
|
+
package.
|
|
42
|
+
|
|
43
|
+
## Registering
|
|
44
|
+
|
|
45
|
+
`platform-register` mints the API key that every other call needs. It runs a device flow: it
|
|
46
|
+
prints a short code and a URL, waits while you sign in and approve the code in a browser, and
|
|
47
|
+
registers the account that signed in. The package install above puts the command on your path.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
platform-register --alias=<display name>
|
|
51
|
+
export POSITRONIC_PLATFORM_API_KEY=<the key the command printed>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The platform names its identity provider at `GET /v1/auth.config`, and the command runs the device
|
|
55
|
+
flow of that provider:
|
|
56
|
+
|
|
57
|
+
- WorkOS, when the platform names a WorkOS client. You sign in with the methods that the platform's
|
|
58
|
+
WorkOS environment offers, such as Google, GitHub or an email code.
|
|
59
|
+
`--workos-client-id` and `POSITRONIC_PLATFORM_WORKOS_CLIENT_ID` name another client instead.
|
|
60
|
+
- GitHub, when the platform takes GitHub tokens and names no WorkOS client. The command uses the
|
|
61
|
+
platform's own OAuth app, whose client id it carries as its default. `--client-id` and
|
|
62
|
+
`POSITRONIC_PLATFORM_GITHUB_CLIENT_ID` name another app instead. The GitHub token carries the scopes
|
|
63
|
+
`read:user` and `user:email`.
|
|
64
|
+
|
|
65
|
+
The command keeps the access token in memory, sends it once to `users.register`, and keeps no other
|
|
66
|
+
token. WorkOS also sends a refresh token, which the command drops. The platform reads the account
|
|
67
|
+
once, mints a key, and stores no token of the provider.
|
|
68
|
+
|
|
69
|
+
A second run returns the same account and no key: the platform cannot read back a key it issued.
|
|
70
|
+
Run `platform-register --rotate` to mint a new key on a machine that lost it.
|
|
71
|
+
|
|
72
|
+
`--platform-url` and `POSITRONIC_PLATFORM_URL` name a platform other than the default. The command
|
|
73
|
+
refuses a plain `http` platform that is not loopback. Staging has no TLS and is reached over the
|
|
74
|
+
tailnet: pass `--plaintext-http` to reach it. A client that `PlatformClient` builds itself uses a proxy
|
|
75
|
+
from the environment only for an `https` platform, so such a proxy never carries a key or a token over
|
|
76
|
+
plain `http`. A client you pass in keeps its own proxy setting.
|
|
77
|
+
|
|
78
|
+
`users.register` checks the token with the provider before it answers, and a caller that gives up
|
|
79
|
+
first loses the key the platform minted. Pass `timeout=client.register_timeout()` to
|
|
80
|
+
`PlatformClient.register`: `register_timeout()` reads `auth.config`, where the platform states its
|
|
81
|
+
deadline for the WorkOS check, and covers the longest check the platform runs. Without `timeout`,
|
|
82
|
+
`register` sends one request and waits 90 s.
|
|
83
|
+
|
|
84
|
+
## Credit accounts
|
|
85
|
+
|
|
86
|
+
`PlatformClient.billing_account(org)` reads the member's billing role, credit balance, tariff, and configured purchase packages.
|
|
87
|
+
One credit is `60_000_000_000` integer units.
|
|
88
|
+
The account response carries the operator-configured tariff for recorded episodes and duration.
|
|
89
|
+
The gateway freezes the accepted request's quote and reserves its full maximum before execution.
|
|
90
|
+
The gateway configures `duration_rounding_sec`, with a default of 30 seconds, and rounds each episode duration up to that interval.
|
|
91
|
+
`Tariff.for_rates` requires an explicit interval. Use zero for exact nanosecond pricing.
|
|
92
|
+
The quote reserves the charge for the rounded cap. Zero duration adds no duration charge.
|
|
93
|
+
Tariffs without that field use exact nanosecond pricing and the v1 version hash.
|
|
94
|
+
|
|
95
|
+
The credit balance belongs to the organization. `QuotaLimit` values from `users.me` describe independent limits and use each limit's own `scale`.
|
|
96
|
+
The period credits meter uses six units per credit. Its remaining quota does not describe or fund the credit balance.
|
|
97
|
+
The `packaged` mode uses the organization's configured invoice terms. The `pay_as_you_go` mode spends the frozen request quote against its credit balance.
|
|
98
|
+
|
|
99
|
+
`PlatformClient.create_purchase` takes a `BillingPurchaseCreateRequest` with an organization, package id, and transaction key.
|
|
100
|
+
Reuse the same key to read the same owned purchase after a lost response. A new purchase requires the billing spender role.
|
|
101
|
+
The configured package fixes its credit units, currency, and amount in currency minor units.
|
|
102
|
+
`currency` is the lowercase ISO 4217 code used to verify the Stripe price and payment.
|
|
103
|
+
For example, `amount_minor=17` means 17 yen for `jpy`, or 0.17 dollars for `usd`.
|
|
104
|
+
The currency does not change the package's credit units.
|
|
105
|
+
The response carries a Checkout URL only when the initiating member can still pay the purchase.
|
|
106
|
+
Credits appear after the gateway verifies the payment. A browser redirect does not grant credits.
|
|
107
|
+
|
|
108
|
+
`PlatformClient.get_purchase(id)` and `list_purchases(org)` read purchases without creating another Checkout session.
|
|
109
|
+
`list_purchases` returns one page with up to 50 purchases by default. The gateway clamps larger limits to 200.
|
|
110
|
+
Pass `response.next` as `after` to read the next page. Continue until `response.next` is `None`.
|
|
111
|
+
The gateway requires a list limit. Older clients that omit it receive HTTP 422 instead of an incomplete history.
|
|
112
|
+
An organization member can read purchase history. A reviewed or credited purchase carries no payable URL.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
positronic account credits account --org=acme
|
|
116
|
+
positronic account credits buy --org=acme --package-id=<configured-package> --transaction-key=<stable-key>
|
|
117
|
+
positronic account credits get-purchase --id=<purchase-id>
|
|
118
|
+
positronic account credits list-purchases --org=acme
|
|
119
|
+
positronic account credits list-purchases --org=acme --limit=50 --after='"last-purchase-id"'
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`users.me` lists every organization the caller belongs to in `organizations`.
|
|
123
|
+
Older platforms that omit `organizations` produce an empty list.
|
|
124
|
+
`account`, `buy` and `list-purchases` use the sole membership when the caller gives no `--org`.
|
|
125
|
+
With zero or multiple memberships, they require `--org`, including when the server omits `organizations`.
|
|
126
|
+
An explicit `--org` selects that organization without fetching memberships.
|
|
127
|
+
Each command prints the organization it uses on stderr, for example `org: acme (sole organization)`.
|
|
128
|
+
|
|
129
|
+
These commands print typed JSON with exact integer units and configured currency amounts.
|
|
130
|
+
Quote a numeric-looking text argument with inner quotes, for example `--transaction-key='"20261005"'`.
|
|
131
|
+
|
|
132
|
+
## Eval plans
|
|
133
|
+
|
|
134
|
+
An eval is a list of tasks. The platform offers named evals, and a customer composes one: an
|
|
135
|
+
`EvalPlan` names the catalogue tasks to run, the policies (endpoints) to run them on, and the
|
|
136
|
+
episodes each endpoint takes on each task. A plan either states its own `tasks` or names an `eval`
|
|
137
|
+
the catalogue expands into them; both arrive at the same set. The plan states the count once. A task may override it for that task, and an endpoint may
|
|
138
|
+
override it for that endpoint, so a 10 + 10 + 2 round is one plan. The scene fields sit on the
|
|
139
|
+
plan and on a task: `tote_placement`, `camera_vantage`, `external_cameras`, `clutter` and
|
|
140
|
+
`start_pose`. An endpoint states only its count. `episodes_total` is a checksum a caller may state.
|
|
141
|
+
`max_cap_per_episode_sec` is the upper bound on every task's cap.
|
|
142
|
+
|
|
143
|
+
`request_type` is required and states the rules a plan runs under. `private_eval` runs for the org
|
|
144
|
+
it names, and the caller must be a member of that org. The org's approvals decide the evals, the
|
|
145
|
+
tasks and the endpoint kinds it may use. `nebius_competition` names one eval and one image
|
|
146
|
+
endpoint, and counts against the daily quota. `--org` states a `private_eval` on the command line.
|
|
147
|
+
|
|
148
|
+
`rig_shape` names the embodiment a plan that states its tasks runs on: `franka`, a single-arm Franka,
|
|
149
|
+
or `yam`, a bimanual YAM. A plan that names none runs on a `franka`. The platform picks the rig of
|
|
150
|
+
that shape. A plan that names an eval runs on the embodiment the eval pins, and it is refused when it
|
|
151
|
+
names a `rig_shape` other than `franka`. The gateway refuses `sim`. The platform runs every task of a
|
|
152
|
+
plan on the rig its `rig_shape` names, and it does not check that a task suits that rig: a plan of
|
|
153
|
+
two-arm tasks states `rig_shape: yam`.
|
|
154
|
+
|
|
155
|
+
```yaml
|
|
156
|
+
request_type: {type: private_eval, org: acme} # or {type: nebius_competition}
|
|
157
|
+
rig_shape: franka # franka | yam
|
|
158
|
+
tasks:
|
|
159
|
+
- eight-spoons-into-grey-tote # a bare id takes the plan's endpoints and counts
|
|
160
|
+
- task_id: marker-in-mug # a mapping overrides for that task alone
|
|
161
|
+
episodes_per_endpoint: 2
|
|
162
|
+
cap_per_episode_sec: 120
|
|
163
|
+
endpoints: [candidate] # a list replaces the plan's list for this task
|
|
164
|
+
endpoints:
|
|
165
|
+
- name: baseline # remote: the caller's own server
|
|
166
|
+
wire: websocket_tls # names the wire, then that wire's address
|
|
167
|
+
address: {host: baseline.example, port: 443, path: /api/v1/session, query: mode=native}
|
|
168
|
+
- name: candidate # served: the platform brings it up and records the address
|
|
169
|
+
kind: served
|
|
170
|
+
spec: dreamzero
|
|
171
|
+
wire: grpc
|
|
172
|
+
episodes_per_endpoint: 10
|
|
173
|
+
episodes_total: 22
|
|
174
|
+
cap_per_episode_sec: 180
|
|
175
|
+
max_cap_per_episode_sec: 300
|
|
176
|
+
policy_preset: production
|
|
177
|
+
tote_placement: random # left | right | random | none
|
|
178
|
+
external_cameras: {side: random} # per mount, by the task's name for it
|
|
179
|
+
start_pose: droid_reset # nominal | droid_reset
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`start_pose` names where the arm starts each episode: `nominal`, the rig's own pose, or
|
|
183
|
+
`droid_reset`, the reset pose of the DROID data collection. A plan that names none takes the pose
|
|
184
|
+
of each task's catalogue entry. A `yam` rig opens each episode at its own pose, whatever the plan
|
|
185
|
+
names.
|
|
186
|
+
|
|
187
|
+
An endpoint states where its policy comes from (`kind`) and the wire a session runs over (`wire`).
|
|
188
|
+
Every kind names its wire. There is no default.
|
|
189
|
+
|
|
190
|
+
`wire` is a name from `positronic_wire.registry`. Each wire dials its own address fields:
|
|
191
|
+
|
|
192
|
+
| `wire` | Address fields |
|
|
193
|
+
|---|---|
|
|
194
|
+
| `websocket` | `host`, `port`, `path`, `query` |
|
|
195
|
+
| `websocket_tls` | `host`, `port`, `path`, `query` |
|
|
196
|
+
| `websocket_unix` | `uds`, `path`, `query` |
|
|
197
|
+
| `grpc` | `host`, `port`, `path`, `query` |
|
|
198
|
+
| `grpc_tls` | `host`, `port`, `path`, `query` |
|
|
199
|
+
| `roboarena` | `host`, `port` |
|
|
200
|
+
|
|
201
|
+
`path` is the session route: `/api/v1/session`. `query` defaults to empty.
|
|
202
|
+
|
|
203
|
+
Each kind carries its own locator:
|
|
204
|
+
|
|
205
|
+
| `kind` | Carries |
|
|
206
|
+
|---|---|
|
|
207
|
+
| `remote` | `address`, the fields its `wire` dials |
|
|
208
|
+
| `served` | `spec`; the platform brings it up and records the address |
|
|
209
|
+
| `image` | `image`, the container; takes only the `websocket` wire, which the platform opens every image session over |
|
|
210
|
+
|
|
211
|
+
- A record carries no URL. A `url` field is refused. A scheme in `host` is refused.
|
|
212
|
+
- The kinds refuse each other's fields. An entry carries one answer to each question.
|
|
213
|
+
|
|
214
|
+
A `remote` endpoint may state `max_concurrent_sessions`, an integer of 1 or more. The platform then
|
|
215
|
+
holds at most that many sessions open to the policy at once. A server that serves one session at a
|
|
216
|
+
time states 1. With no value, the platform picks the count. Only an entry that names its `address`
|
|
217
|
+
states it: a label on a task takes the value of the plan endpoint it names.
|
|
218
|
+
|
|
219
|
+
Each address field holds one grammar. The wire writes each value into what it dials with no change,
|
|
220
|
+
so the grammar admits only a value the wire can write as it is:
|
|
221
|
+
|
|
222
|
+
| Field | Holds | Refused |
|
|
223
|
+
|---|---|---|
|
|
224
|
+
| `host` | A hostname: labels of letters, digits, `-` and `_`, joined by `.`, with an optional last `.`. Or an IPv4 address, or an IPv6 address with no brackets | A scheme, a port, a path, userinfo, brackets, an IPv6 zone index, any other character. The wire adds the brackets that an IPv6 address needs |
|
|
225
|
+
| `port` | An integer from 1 to 65535 | Any other value |
|
|
226
|
+
| `path` | The session route, `/api/v1/session` | Any other route, a checkpoint appended to it included |
|
|
227
|
+
| `query` | The session params with no leading `?`, in visible ASCII characters. Empty by default | A leading `?`, a `#`, a space, any other character |
|
|
228
|
+
| `uds` | An absolute socket path | A relative path, a NUL byte |
|
|
229
|
+
|
|
230
|
+
Percent-encode a character that `query` refuses: a space is `%20`, and `#` is `%23`.
|
|
231
|
+
|
|
232
|
+
`positronic eval run --from-file` files that plan with `submissions.create`. The file is YAML or
|
|
233
|
+
JSON, and an `--eval` value is a name. Two or more endpoints make one blind sample: the operator is
|
|
234
|
+
told no policy, and each episode records which one served it. `eval status` and `eval list` read it back by
|
|
235
|
+
the submission id every run carries. The platform records the plan, the rollouts coordinator runs
|
|
236
|
+
it on a rig of the shape it names, and a `blocked` run waits on what its `reason` names. A plan that states its own
|
|
237
|
+
tasks needs a customer grant; a key without one is refused `forbidden`, and so is `catalog.tasks`.
|
|
238
|
+
Write to hi@phail.ai for a grant. A rig plan queues for an operator, so it answers `pending` with a
|
|
239
|
+
`queue_position`, and it does not count against the `submissions.day` quota: that quota counts the
|
|
240
|
+
image runs the platform executes itself. `users.me` names the grant's client in `client`.
|
|
241
|
+
|
|
242
|
+
Each run of a rig plan carries `episodes`: what the run took on, and what it recorded. `done` moves
|
|
243
|
+
as the rig records each episode. A finished rig plan also carries `replay`, a page that plays back its
|
|
244
|
+
episodes, and `outcome`, the kept, judged and successful episodes per endpoint. `replay` is absent
|
|
245
|
+
until the platform has built the page. The rig writes the episodes to the org's own bucket, and
|
|
246
|
+
`artifacts.result` names the prefix they landed under. Read it with the org's own credential for that
|
|
247
|
+
bucket: `submissions.artifacts` refuses a rig plan.
|
|
248
|
+
|
|
249
|
+
The answer to `submissions.create` carries `resolved`, the plan as the rig runs it:
|
|
250
|
+
`episodes_total`, and for each task the count per endpoint, the cap, the preset, each side, the
|
|
251
|
+
vantage, the clutter objects and the episode order. A level states a value, else the task's
|
|
252
|
+
catalogue entry gives it, else the platform draws it. The platform makes each draw once per plan, so every run of
|
|
253
|
+
the plan lays out that scene and that table, and runs the episodes in that order.
|
|
254
|
+
`submissions.get` carries the same `resolved`. A rig plan whose task resolves no
|
|
255
|
+
`cap_per_episode_sec` or no `policy_preset` at any level is refused `bad_request`, and the refusal
|
|
256
|
+
names each task and what it lacks. Name a preset the rig carries: `production` serves each
|
|
257
|
+
episode from one of the plan's endpoints.
|
|
258
|
+
|
|
259
|
+
`submissions.resolve` takes the same plan and answers with `resolved` alone. For a pay_as_you_go
|
|
260
|
+
organization, that `ResolvedPlan` carries a `credit_quote`. It files nothing, spends no quota and
|
|
261
|
+
returns no submission id. A plan with a `transaction_key` draws from that key,
|
|
262
|
+
so a dry run shows the draws a submission under the same key then makes. Without a key, the draws
|
|
263
|
+
are an example. From Python, `PlatformClient.resolve_plan` makes the call.
|
|
264
|
+
|
|
265
|
+
`EvalPlan` refuses unknown fields. `EvalPlan.model_validate(plan)` raises on one before anything
|
|
266
|
+
reaches the platform.
|
|
267
|
+
|
|
268
|
+
A plan states its own tasks and endpoints. A policy image run names a catalog eval:
|
|
269
|
+
[Submit a policy image](../docs/submit-a-policy-image.md) says what the platform requires of the
|
|
270
|
+
image, and how to build, test and submit it.
|
|
271
|
+
|
|
272
|
+
A policy image is one endpoint of a plan: `--policy-image` states an `image` endpoint on the
|
|
273
|
+
`websocket` wire, and `--eval` names the eval whose tasks it runs. `plan_of_image` builds that shape.
|
|
274
|
+
|
|
275
|
+
A plan that names an eval may run a random sample of the eval's trials. `sample` states the size as
|
|
276
|
+
`count`, a number of trials, or as `percent`, a share of the eval's trials rounded up, and a `seed`
|
|
277
|
+
(0 when none is given). The same seed draws the same trials, and the run takes them in the eval's
|
|
278
|
+
own order. A count larger than the eval's trials is refused `bad_request`. A sampled run ranks on no
|
|
279
|
+
board. A plan that states its own tasks takes no sample. On the command line, `--sample-count` or
|
|
280
|
+
`--sample-percent` states the size and `--sample-seed` the seed.
|
|
281
|
+
|
|
282
|
+
```yaml
|
|
283
|
+
eval: molmo.franka_pick_mini_full
|
|
284
|
+
sample: {percent: 10, seed: 7} # or {count: 50, seed: 7}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
An `image` endpoint whose registry serves no anonymous caller states `image_credential`. A plan
|
|
288
|
+
file names the registry user and the FILE the password is in. `positronic eval run --from-file`
|
|
289
|
+
reads it as `EvalPlan[RegistryCredentialFile]`, then `plan_with_passwords_read` gives the `EvalPlan`
|
|
290
|
+
a request carries, whose `RegistryCredential` holds the password. From Python, `credential_from_file`
|
|
291
|
+
builds that credential.
|
|
292
|
+
|
|
293
|
+
```yaml
|
|
294
|
+
endpoints:
|
|
295
|
+
- name: policy
|
|
296
|
+
kind: image
|
|
297
|
+
wire: websocket
|
|
298
|
+
image: registry.example.com/you/policy@sha256:...
|
|
299
|
+
image_credential:
|
|
300
|
+
username: a-reader
|
|
301
|
+
password_file: ~/.config/positronic/registry-password
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`positronic eval catalog` prints what the key may name: `catalog.evals` lists the evals a plan
|
|
305
|
+
names, and `catalog.tasks` the tasks a plan may compose, each with its `embodiment`. Every registered
|
|
306
|
+
user sees the evals a submission can name. A customer grant adds the rig's evals and tasks, filtered
|
|
307
|
+
to the entries offered to the grant's client.
|
|
308
|
+
|
|
309
|
+
From Python, `PlatformClient` takes and answers the models in `platform_client.eval_plan` and
|
|
310
|
+
`platform_client.catalog`. The rollouts coordinator's request record is a subclass of `EvalPlan`, so the ask has one
|
|
311
|
+
definition.
|
|
312
|
+
|
|
313
|
+
`ResolvedPlan.rig_shape` names the plan's rig shape. Missing or invalid shapes fail response validation.
|
|
314
|
+
|
|
315
|
+
## Public evals, labels and scores
|
|
316
|
+
|
|
317
|
+
`positronic-evals` holds the definitions of the public evals, the labelers and the scoring rules.
|
|
318
|
+
Its README describes them. An eval name, `EvalRef`, is `positronic_evals.evals.EvalRef`.
|
|
319
|
+
|
|
320
|
+
## From the command line
|
|
321
|
+
|
|
322
|
+
`positronic` carries the other commands, and a checkout needs no installation step. `eval run`
|
|
323
|
+
runs an eval here when given a policy, on the platform when given a policy image, and on the lab rig
|
|
324
|
+
when given a plan file. To get a key, run `platform-register`, which prints the `export` line, or
|
|
325
|
+
`account register`, which saves the key in the record the other commands read. Both run the device
|
|
326
|
+
flow of the provider that the platform names. In a checkout, run them through `uv run`. With a token
|
|
327
|
+
in `POSITRONIC_PLATFORM_CREDENTIAL`, `account register` registers with that token and runs no sign-in.
|
|
328
|
+
Its sign-in refuses a plain `http` platform that is not loopback unless you pass `--plaintext_http`. A
|
|
329
|
+
token from `POSITRONIC_PLATFORM_CREDENTIAL` goes to the platform as given, like the key of every other
|
|
330
|
+
command.
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
platform-register --alias=<display name> # in a checkout: uv run platform-register
|
|
334
|
+
export POSITRONIC_PLATFORM_API_KEY=<the key it printed>
|
|
335
|
+
|
|
336
|
+
uv run positronic eval run --eval=<name> --policy-image=org/policy@sha256:…
|
|
337
|
+
uv run positronic eval run --from-file=positronic/cli/examples/rig_plan.yaml --org=<org>
|
|
338
|
+
uv run positronic eval run --from-file=positronic/cli/examples/yam_plan.yaml --org=<org>
|
|
339
|
+
uv run positronic eval status --id=<hex id>
|
|
340
|
+
uv run positronic eval list
|
|
341
|
+
uv run positronic eval cancel --id=<hex id>
|
|
342
|
+
uv run positronic eval catalog
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
`positronic/cli/examples/` runs the whole flow end to end.
|
|
346
|
+
|
|
347
|
+
A command that fails exits with a status that tells whose fault the failure is:
|
|
348
|
+
|
|
349
|
+
| Status | Fault | Cases |
|
|
350
|
+
|---|---|---|
|
|
351
|
+
| 1 | the caller | the platform refused the request with a 4xx, no API key or a bad key, a quota used up, a run rejected at the door, a sign-in the provider refused (an unknown client id, a refused or expired code) |
|
|
352
|
+
| 2 | usage | the arguments are wrong before any call: an id that is not hexadecimal, a plan file that does not parse |
|
|
353
|
+
| 3 | the platform | a 5xx, an `internal_error` or `upstream_unavailable` code, no answer, a timeout, an answer the client cannot read, a replayed run that ended for a platform reason, an identity provider that is unreachable or unavailable, a platform that takes no sign-in this client can run |
|
|
354
|
+
|
|
355
|
+
The read commands take `--json`: `eval status`, `eval list`, `eval catalog`, and `account credits account`,
|
|
356
|
+
`get-purchase` and `list-purchases`. They print the data as JSON on stdout. `eval list --json` prints every
|
|
357
|
+
page as one page, with `next` null. `eval catalog --json` prints the evals and the tasks as one object, with
|
|
358
|
+
`tasks` null where the key may compose no plan. A refusal by the platform prints its error envelope as JSON on
|
|
359
|
+
stdout. Any other failure prints its message on stderr. The exit status is the same with and without `--json`.
|
|
360
|
+
|
|
361
|
+
An argument that a command does not declare, or a required argument that is missing, stops with a traceback
|
|
362
|
+
and status 1. `--help` lists the arguments of a command.
|
|
363
|
+
|
|
364
|
+
## Configuration
|
|
365
|
+
|
|
366
|
+
Calls go to `https://platform.positronic.ro` with nothing set. The environment carries the rest:
|
|
367
|
+
|
|
368
|
+
| Variable | Holds |
|
|
369
|
+
|---|---|
|
|
370
|
+
| `POSITRONIC_PLATFORM_URL` | a platform other than the default one, overridden per call by `--platform-url` |
|
|
371
|
+
| `POSITRONIC_PLATFORM_API_KEY` | the key `register` mints — read from the environment or the saved record, never an argument, so it reaches no process listing |
|
|
372
|
+
| `POSITRONIC_PLATFORM_CONFIG_DIR` | where `positronic account register` saves that record, else `~/.config/positronic-platform` |
|
|
373
|
+
| `POSITRONIC_PLATFORM_CREDENTIAL` | a token of the platform's identity provider, for `account register` in a script — read the same way, for the same reason. Without it, `account register` signs in itself |
|
|
374
|
+
| `POSITRONIC_PLATFORM_WORKOS_CLIENT_ID` | a WorkOS client other than the one the platform names, for both register commands |
|
|
375
|
+
| `POSITRONIC_PLATFORM_GITHUB_CLIENT_ID` | a GitHub OAuth app other than the platform's own, for both register commands |
|
|
376
|
+
|
|
377
|
+
The record's key and its platform are read one at a time. A command that names another platform, and
|
|
378
|
+
no key, sends the record's key to the platform it names. The client speaks one wire contract and runs
|
|
379
|
+
against any platform that serves it, but it is built for ours. You take the risk of another one.
|
|
380
|
+
|
|
381
|
+
`rankings.list` names the boards, and `rankings.get?board=<slug>` reads one. From the command line,
|
|
382
|
+
`positronic/cli/examples/nebius_competition/standings.py` prints the boards, or the rows of one:
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
uv run positronic/cli/examples/nebius_competition/standings.py
|
|
386
|
+
uv run positronic/cli/examples/nebius_competition/standings.py --board=<slug>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
From Python, `PlatformClient.list_boards` and `.rankings` read them, the latter taking a `BoardRef`
|
|
390
|
+
(`platform_client.boards`). Both take the key when one is set and work without: a public board is
|
|
391
|
+
readable by anyone, a tenant's board only by its members.
|
|
392
|
+
|
|
393
|
+
A board row reads `<display name>#<tag>`. The name is an alias and is not unique — a board may hide
|
|
394
|
+
it altogether — so the tag is what tells two rows apart, and it is how you find your own: it is the
|
|
395
|
+
same on every board you appear on.
|
|
396
|
+
|
|
397
|
+
A row with `baseline` set is a reference model, and `baseline.publisher` names the organization that
|
|
398
|
+
released it. A baseline row has no `rank`. The other rows rank 1..N among themselves, and the board
|
|
399
|
+
lists every row in score order.
|
|
400
|
+
|
|
401
|
+
Each row carries `replay`, a page that plays back the camera video of the row's submission, with the
|
|
402
|
+
task and outcome of each episode. The page opens without a key, and `replay` is absent until the
|
|
403
|
+
platform has built it.
|
|
404
|
+
|
|
405
|
+
`replay.data_url` names the JSON object the page renders from. `about` identifies the submission,
|
|
406
|
+
and each episode carries its values and one absolute URL per camera video. A page on another origin
|
|
407
|
+
can read the object, the videos and both `rankings` routes with no key:
|
|
408
|
+
|
|
409
|
+
```json
|
|
410
|
+
{
|
|
411
|
+
"title": "<board title> — 0ddba7",
|
|
412
|
+
"url": "https://app.positronic.ro/v/<token>/",
|
|
413
|
+
"about": [
|
|
414
|
+
{"key": "board", "label": "Board", "value": "<slug>"},
|
|
415
|
+
{"key": "eval", "label": "Eval", "value": "<eval>"},
|
|
416
|
+
{"key": "tag", "label": "Tag", "value": "0ddba7"},
|
|
417
|
+
{"key": "submission_id", "label": "Submission", "value": "<hex id>"},
|
|
418
|
+
{"key": "primary_score", "label": "success_rate", "value": 0.35}
|
|
419
|
+
],
|
|
420
|
+
"columns": [{"key": "task", "label": "Task"}, {"key": "outcome", "label": "Outcome"}],
|
|
421
|
+
"episodes": [
|
|
422
|
+
{
|
|
423
|
+
"index": 0,
|
|
424
|
+
"values": {"task": "<task>", "outcome": "Success"},
|
|
425
|
+
"duration_s": 24.1,
|
|
426
|
+
"videos": [
|
|
427
|
+
{
|
|
428
|
+
"camera": "image.exterior",
|
|
429
|
+
"url": "https://app.positronic.ro/v/<token>/episodes/0/image.exterior.mp4",
|
|
430
|
+
"src": "episodes/0/image.exterior.mp4",
|
|
431
|
+
"width": 640,
|
|
432
|
+
"height": 360,
|
|
433
|
+
"duration_s": 24.1
|
|
434
|
+
}
|
|
435
|
+
]
|
|
436
|
+
}
|
|
437
|
+
]
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
`columns` labels the keys of each episode's `values`. `src` is the video's path relative to the
|
|
442
|
+
object. Every video of an episode starts at the episode's first camera frame, so one clock plays them
|
|
443
|
+
together.
|
|
444
|
+
|
|
445
|
+
## From Python
|
|
446
|
+
|
|
447
|
+
```python
|
|
448
|
+
from platform_client.client import PlatformClient
|
|
449
|
+
from platform_client.eval_plan import plan_of_image
|
|
450
|
+
from platform_client.policy_images import PolicyImage
|
|
451
|
+
|
|
452
|
+
with PlatformClient(api_key=key) as client:
|
|
453
|
+
boards = client.list_boards().boards # public; each names the eval it ranks
|
|
454
|
+
for board in boards:
|
|
455
|
+
print(board.board, board.eval, board.primary_metric)
|
|
456
|
+
created = client.create_submission(plan_of_image(PolicyImage('org/policy:v1'), boards[0].eval))
|
|
457
|
+
view = client.get_submission(created.submission_id)
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
The platform resolves the image reference to a digest at submission and records it as
|
|
461
|
+
`policy_image_digest`, so a tag is safe for the run: the run uses the bytes the tag named at that
|
|
462
|
+
moment. Pin by digest to know which image was scored.
|
|
463
|
+
|
|
464
|
+
An **eval** is the whole of what a submission chooses: it names a task suite and the embodiment that
|
|
465
|
+
runs it — one simulator, or one real robot — so there is no second axis to get wrong. The platform
|
|
466
|
+
owns the set of names, and `PUBLIC_EVALS` names only the public ones. Do not copy a name from a
|
|
467
|
+
document; read it from the platform:
|
|
468
|
+
|
|
469
|
+
- `rankings.list` needs no key. Each board it returns names the eval it ranks.
|
|
470
|
+
- `submissions.create` with a name the platform does not offer raises a `PlatformError` whose
|
|
471
|
+
`evals` carries every eval on offer, with a board or without one.
|
|
472
|
+
|
|
473
|
+
Ids are 64-bit ints in Python and bare lowercase hex on the wire; closed sets are `IntEnum`s carried
|
|
474
|
+
as slugs. A non-2xx response raises `PlatformError`, which carries the parsed error envelope: `code`
|
|
475
|
+
for a program, `message` for a human, `reason_code` where a terminal caller fault has one, `quota`
|
|
476
|
+
where a 429 names the rule that refused the request, and `evals` where the eval asked for is not one
|
|
477
|
+
of them. A response with no envelope, such as a proxy's HTML page, still raises `PlatformError`: the
|
|
478
|
+
client builds an `internal_error` envelope around the raw body and sets `synthesized_envelope`.
|
|
479
|
+
`ReasonCode.is_platform_fault` says whether a reason is the platform's fault, which refunds the quota,
|
|
480
|
+
or the caller's, which charges it.
|
|
481
|
+
|
|
482
|
+
`submissions.get` answers one model per status. A finished run reads its score off `scores` and its
|
|
483
|
+
outputs off `artifacts`. A failed run reads `reason_code` and `reason`, and `artifacts` links the
|
|
484
|
+
records the run wrote: `result` is its outcome, and `diagnostics` says why its policy failed.
|
|
485
|
+
`policy_log` is the policy container's own output, and a finished or failed run carries it. Each
|
|
486
|
+
link is a signed URL, because the submitter holds no credential for the bucket. `artifacts` is
|
|
487
|
+
absent on a run that wrote nothing, `diagnostics` on a run whose record was not written, and
|
|
488
|
+
`policy_log` on a run whose container printed nothing.
|
|
489
|
+
|
|
490
|
+
`submissions.artifacts` lists what a finished run wrote, one page at a time. Each entry names its
|
|
491
|
+
`key` under the submission's own prefix, its `size`, and a signed `url` that expires. Pass `prefix`
|
|
492
|
+
to keep the page to one part of the tree, so one attempt's episodes are `attempts/<n>/episodes/`
|
|
493
|
+
and `result.json`'s `attempt_location` names the attempt that scored. Pass `after` with the `next`
|
|
494
|
+
of the page before it to read the rest. The route answers a finished run alone: a run still going
|
|
495
|
+
has a part-written prefix, and a run that failed reads its records off `submissions.get`. It refuses
|
|
496
|
+
a rig plan, whose episodes are in the org's own bucket (§Eval plans).
|
|
497
|
+
|
|
498
|
+
`users.me` reports the plan's rules as a list of `QuotaLimit`, each with its own key, window and
|
|
499
|
+
subject; `MeResponse.quota_for(QUOTA_SUBMISSIONS_DAY)` reads one by key, from the keys the package
|
|
500
|
+
publishes beside `QuotaLimit`.
|