agi.diy

docs

Open-weight VLA policies for the SO-101 arm, served over Zenoh. You send observations, you get back a chunk of actions. 1 token per action. Everything on this page was run against the live service on 2026-10-24.

Start here · Your robot · Your own loop · Billing · Errors · Links

Start here

60 seconds. Python 3.10–3.12.

  1. Login with GitHub. New accounts get 200,000 tokens ($20).
  2. Dashboard → Create key. The key is shown once; copy it.
  3. Install and log in:
GIT_LFS_SKIP_SMUDGE=1 pip install agi-diy "lerobot[remote] @ git+https://github.com/huggingface/lerobot@d3793b7e"
agi login            # paste the key · verifies it · writes ~/.agi/credentials (chmod 600)

agi-diy has no dependencies of its own. The second package is LeRobot's remote-inference client (zenoh + msgpack). It lives on lerobot main today — PyPI lerobot 0.6.1 has no remote extra — so the git line is the one that works. AGI_API_KEY=agi_sk_live_… in the environment replaces agi login.

  1. Send one call:
agi models
# act/so101      ready    slots 2/2  horizon 100  fps 30  so101-degrees-gripper-percent-v1
# smolvla/so101  ready    slots 3/3  horizon 50   fps 30  so101-degrees-gripper-percent-v1

agi run act/so101 --probe
# connected tcp/bore.pub:35706 in 320 ms · features observation.state(6,), observation.images.top(480, 640, 3), observation.images.wrist(480, 640, 3) · action 6×100 @ 30 Hz
# chunk 1/3: shape (100, 6) dtype float32 · 296 ms
# chunk 2/3: shape (100, 6) dtype float32 · 63 ms
# chunk 3/3: shape (100, 6) dtype float32 · 96 ms
# billed 300 tokens (1 per action) · `agi usage`

The probe sends zeros for every feature the model expects and prints the chunk it gets back. It costs horizon tokens per chunk. The first chunk of a session is the slow one (model wake + relay); after that, measured from a Mac on another network through the public relay: act/so101 63–316 ms, smolvla/so101 436–835 ms.

Your robot

Two models, one arm. Both speak so101-degrees-gripper-percent-v1: six joints in degrees, gripper in percent.

modelactions per callfpscameras (480×640 RGB)
act/so10110030top, wrist
smolvla/so1015030front, wrist

Weights: act/so101 = vineetr196/act_so101_pickplace_v1; smolvla/so101 = edge-inference/smolvla-so101-pick-orange (sim-trained). GET /v1/models is the live version of this table (action_horizon, cameras, slots_free).

Nothing but LeRobot

If your arm already runs with LeRobot, agi rollout leases a slot, fills in the remote-inference flags and runs lerobot-rollout for you. Camera names must match the model's.

agi rollout act/so101 -- \
  --robot.type=so101_follower --robot.port=/dev/tty.usbmodem... \
  --robot.cameras='{"top": {"type": "opencv", "index_or_path": 0, "width": 640, "height": 480, "fps": 30},
                    "wrist": {"type": "opencv", "index_or_path": 1, "width": 640, "height": 480, "fps": 30}}' \
  --fps=30 --task="pick up the boat"

lerobot-rollout itself needs lerobot[dataset,feetech] on top of [remote]. The lease ends when the process exits.

strands-robots

import agi
from strands_robots import Robot

session = agi.Policy("act/so101")
arm = Robot("so101", port="/dev/tty.usbmodem...")
arm.run(session.policy(), task="pick up the boat", seconds=30)
session.close()

session.policy() is a strands-robots policy bound to the lease: LeRobot client underneath, JPEG on the wire, client mode. session.url is zenoh://<host>:<port>/d_…, which strands_robots.Policy(url) also accepts.

Your own loop

Python

import agi, numpy as np

state = np.zeros(6, dtype=np.float32)                     # shoulder_pan … gripper
top = wrist = np.zeros((480, 640, 3), dtype=np.uint8)     # the model's two cameras
with agi.Policy("act/so101") as session:
    actions = session.predict(
        {"observation.state": state, "observation.images.top": top, "observation.images.wrist": wrist},
        task="pick up the boat",
    )                                                     # np.ndarray [100, 6] — session.action_horizon rows

session.features() lists the names, shapes and dtypes the model expects. Images cross the wire as JPEG (session.encoding, from the lease): raw RGB through the public relay stalls. Timeouts default to 30 s (handshake_timeout_s, action_timeout_s, startup_timeout_s); override with session.connect(action_timeout_s=…) or session.predict(obs, timeout_s=…). connect() tries every endpoint the runner advertised, in order.

For a real control loop take LeRobot's own client: session.connect() returns a connected lerobot.remote_inference.RemoteClient, session.config() the matching RemoteInferenceConfig. LeRobot's remote-inference docs apply unchanged.

Any client, over HTTP

A session is a lease on one running model. Create it, point any LeRobot remote-inference client at it, delete it.

curl -s -X POST https://api.agi.diy/v1/sessions \
  -H "authorization: Bearer $AGI_API_KEY" -H "content-type: application/json" \
  -d '{"model":"act/so101"}'
# {"id":"ses_…","model":"act/so101","state":"leased","deployment":"d_…",
#  "zenoh":{"endpoint":"tcp/<relay>:<port>","endpoints":["tcp/…"],"mode":"client","encoding":"jpeg"},
#  "semantics":"so101-degrees-gripper-percent-v1","features":{"observation.state":{"shape":[6],"dtype":"float32"},…},
#  "action_horizon":100,"fps":30,"connect_by":"…","expires_at":"…","tokens_used":0}

Use zenoh.endpoint + deployment with encoding=jpeg and zenoh_mode=client. Connect within 60 s (connect_by); the lease lasts ttl_s (default 600, max 3600). DELETE /v1/sessions/<id> when done. Send a real User-Agent: Cloudflare answers 403 to a bare Python-urllib.

JavaScript

npm i agi-diy
import { createClient } from "agi-diy";
const agi = createClient({ apiKey: process.env.AGI_API_KEY });
const session = await agi.session("act/so101");   // session.endpoint, session.deployment, session.lease.action_horizon
await session.end();

The JavaScript package manages keys, sessions and usage. Inference itself runs through a LeRobot client.

Billing

agi usage
# credits 198450 tokens · used 1550 tokens in 17 calls
#   act/so101            14 calls      1400 tokens
#   smolvla/so101         3 calls       150 tokens

Errors

Always {"error": "snake_case", "message": "One sentence."}, plus retry_after_s when waiting helps. The SDK raises agi.AuthError, agi.InsufficientCredits, agi.UnknownModel, agi.NoSlots (.retry_after_s) and agi.MissingDependency for the same cases.

statuserrorwhat to do
400bad_requestThe body is not what the endpoint takes; compare it with the example above.
401unauthorizedSend Authorization: Bearer agi_sk_live_…, or log in again; keys you revoked stay revoked.
402insufficient_creditsYour balance is 0 tokens; the dashboard shows what is left.
403— (Cloudflare page)Set a User-Agent header; a bare Python-urllib is refused before it reaches us.
404not_foundThe id or route does not exist, or the session belongs to another account.
404unknown_modelNo runner serves that model right now; pick one from GET /v1/models.
409no_slotsEvery slot of that model is leased; retry after retry_after_s seconds.
409session_endedThat session is over (end_reason says why); create a new one.
429rate_limitedSlow down and retry after retry_after_s seconds.
502upstreamThe page server did not answer; the API itself is fine, retry.
500internalOur fault; retry once, then check status.

On the wire: Remote inference request deadline exceeded from LeRobot means the observation never made it in time — use encoding=jpeg (the SDK does) and a timeout ≥ 30 s for the first call.