Skip to main content

sidekick-sdk

CI PyPI Python License

Send a camera frame and an instruction. Get back robot actions.

One API for every robotics foundation model. You ask for a job ("fold the towel"), not a checkpoint, and Sidekick picks a model that can do it, calls it, and falls to another if the first one is down.

pip install sidekick-sdk

One dependency (httpx), so it installs on a robot control computer, or inside a ROS container, without dragging a web framework along.

You will need a key: get one at sidekickrobotics.ai/api.


1. Your first call

Copy this whole block and run it. It works before you have a camera or a robot attached: the image below is a 1x1 placeholder so the call goes through.

from sidekick_sdk import Sidekick

sk = Sidekick(api_key="sk-sidekick-...")

image = ("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mNgYGAA"
         "AAAEAAHI6uv5AAAAAElFTkSuQmCC")

act = sk.act(
    model="sidekick/auto:manipulation",   # a job, not a checkpoint
    instruction="fold the towel",         # what you want done
    action_space="joint_pos_14",          # what shape of action you can execute
    horizon=4,                            # how many future steps to plan
    observations=[{
        "frames": [{"camera": "primary", "b64": image}],
        "proprio": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    }],
)

print(act.route["model"])      # which model actually answered
print(act.usage["cost_usd"])   # what this call cost you
print(act.steps)               # the actions

You should see something like:

physical-intelligence/pi-0
0.03262
[[0.0614, -0.0042, ...], [0.0672, -0.0008, ...], ...]

What each argument means

Argument What to put there
model "sidekick/auto:manipulation" asks for any live manipulation model. You can also name one directly, e.g. "physical-intelligence/pi-0".
instruction Plain English. The policy was trained on language, so say what you would say to a person.
action_space The shape of action your robot can execute. joint_pos_14 is 14 joint angles, 7 per arm. Get the full list with sk.taxonomy().
horizon How many future timesteps to plan. 4 is fine to start; 16 is normal for real control.
observations What the robot can see and feel right now. See below.

The observation

This is the part worth reading twice, because getting it wrong produces a confident answer rather than an error.

{
    "frames": [{"camera": "primary", "b64": image}],   # what it sees
    "proprio": [0] * 14,                               # where it is now
}
  • frames is a list, because most policies want several camera views at once. Each needs a camera name and one of b64 (base64 image data) or url.
  • proprio is the robot's current joint positions. It must have the same number of values that your action space declares. joint_pos_14 means fourteen, which is why there are fourteen zeros above.

2. What you get back

act.steps      # list of timesteps, each a list of joint targets
act.space      # the action space, e.g. "joint_pos_14"
act.route      # which model and provider answered, and whether it fell back
act.usage      # cost_usd, gpu_seconds, action_steps

act.steps is a chunk, not a single command. With horizon=4 you get four timesteps to be issued one after another, act.action["dt_ms"] apart (66 ms for pi0). They are close together because a robot moves smoothly: over 66 ms, no joint moves more than a fraction of a degree.

act.route is the audit trail. It names the model that answered and lists every attempt, including failures, so a fallback is visible rather than silent:

act.route["model"]          # "physical-intelligence/pi-0"
act.route["fallback_used"]  # True if the first choice did not answer
act.route["simulated"]      # True means placeholder output, no GPU ran

Always check route["simulated"] before moving a robot. A simulated response is structurally identical to a real one, and setting provider={"allow_simulated": False} on the call refuses it outright.


3. Say what you care about

One argument changes how the router picks:

sk.act(..., preference="reliable")
Value Picks by
"balanced" a blend of the three below (the default)
"fastest" lowest measured latency
"cheapest" lowest price
"reliable" highest success rate

For hard limits rather than preferences, use provider:

sk.act(..., provider={"max_latency_ms": 900, "allow_simulated": False})

4. The four things you can ask

These are separate calls on purpose. A grounding model returns pixel coordinates and a policy returns joint targets, so the API will not let one quietly stand in for the other.

Call The question You get back
sk.act(...) what should I do next? an action chunk
sk.predict(...) what happens next? predicted future frames
sk.ground(...) where is the thing? points and boxes, 0 to 1
sk.evaluate(...) is this policy any good? a benchmark job
g = sk.ground(observations=[obs], instruction="point at every graspable object")
print(g.points)     # [{"label": "mug", "x": 0.42, "y": 0.61}, ...]


Going further

Everything above works with a placeholder image and no robot. The rest of this is what you need once real hardware is involved.

Sending real camera frames

A frame can be a URL, a file path, or base64. sk.observation() accepts any of the three and works out which it is:

obs = sk.observation(image_path="frame.jpg", proprio=robot.joints())
obs = sk.observation(image_url="https://cdn.example/frame.jpg", proprio=...)

For a live camera, encode to base64 yourself. Use JPEG, not PNG: the same 640x480 scene is about 24,000 base64 characters as JPEG against 135,000 as PNG, and you pay that on every frame of every call, so it shows up as latency long before it shows up as bandwidth.

import base64, cv2

cams = {"cam_high": cv2.VideoCapture(0)}       # one entry per view

def grab(name: str) -> str:
    ok, frame = cams[name].read()
    if not ok:
        raise RuntimeError(f"{name}: camera read failed")
    _, buf = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 90])
    return base64.b64encode(buf).decode()

obs = sk.observation(cameras={"cam_high": grab("cam_high")},
                     proprio=robot.joints())

Multi-camera rigs pass every view in one dict. Most manipulation policies want several; pi0.5 on ALOHA expects four:

cams = {"cam_high":        cv2.VideoCapture(0),
        "cam_low":         cv2.VideoCapture(1),
        "cam_left_wrist":  cv2.VideoCapture(2),
        "cam_right_wrist": cv2.VideoCapture(3)}

obs = sk.observation(cameras={name: grab(name) for name in cams},
                     proprio=robot.joints())

Two mistakes here fail silently rather than raising:

  • Camera names must match what the checkpoint was trained with. Send the wrist view as the head view and you get a well-formed action chunk computed from the wrong scene. The live pi0 and pi0.5 deployments expect cam_high, cam_low, cam_left_wrist and cam_right_wrist; a single frame named primary also works, and is what the first call above sends. For any other checkpoint, take the names from its model card.
  • proprio must be the width the action space declares, in the order the policy was trained on.

Connecting your robot

robot in these examples is your object. This SDK talks to the API; it ships no driver and knows nothing about your kinematics, limits or e-stop. It needs two things from you:

class Robot:
    """Wrap whatever you already use: ROS 2, Interbotix, LeRobot, serial."""

    def joints(self) -> list[float]:
        """Current joint positions, matching the action space width and order."""
        ...

    def set_joint_positions(self, targets: list[float]) -> None:
        """Command one timestep. Must return promptly, never block."""
        ...

Play the chunk out one step at a time rather than firing it in one go:

import time

dt = act.action["dt_ms"] / 1000.0        # 0.066 for pi0
for step in act.steps:
    robot.set_joint_positions(step)
    time.sleep(dt)

Check three things before any of it reaches an actuator. Each is a response that is perfectly well-formed and still wrong for your robot:

if act.route.get("simulated"):
    raise RuntimeError("placeholder output, no GPU ran")
if act.space != "joint_pos_14":
    raise RuntimeError(f"got action space {act.space}, this arm is joint_pos_14")
if any(len(s) != 14 for s in act.steps):
    raise RuntimeError("wrong number of joints for this robot")

Control loops that keep up

The sleep loop above is fine for one move and wrong for sustained control: it stops commanding while the next chunk is fetched. You cannot call a cloud API once per actuation. A control loop runs at 30 to 200 Hz and a policy answers in a few hundred milliseconds, so asking per tick leaves the robot standing still.

ActionStream solves this. It plays the current chunk from a local buffer while fetching the next one on a background thread:

stream = sk.stream_actions(
    model="sidekick/auto:manipulation",
    instruction="fold the towel",
    action_space="joint_pos_14", dof=14,
    preference="fastest",
    observe=lambda: sk.observation(cameras={n: grab(n) for n in cams},
                                   proprio=robot.joints()),
)

stream.warm_up()          # first call, while the arm is still braked
with stream:              # starts the background thread
    while running:
        step = stream.next_action()
        if step is None:
            robot.hold()  # policy fell behind: decelerate, never repeat
        else:
            robot.set_joint_positions(step)
        time.sleep(stream.dt_s)

Three things to know:

  • next_action() never blocks and never raises. All network work is on the background thread, so your servo loop stays real-time.
  • None is a real answer, not an error. The buffer is dry or the chunk has gone stale. Hold or decelerate. Repeating the last command forever is a robot acting on a world that has moved on.
  • It runs the three safety checks for you and raises UnsafeResponse on a simulated route, a wrong action space, or a step that is not dof wide.

stream.calls, stream.failures and stream.spend_usd track what it has done. A full runnable loop is in examples/control_loop.py.

Finding models, and checking cost first

sk.routes()                        # aliases that are live right now
sk.models(domain="manipulation")   # the catalog
sk.model("physical-intelligence/pi-0")
sk.taxonomy()                      # every valid domain, family, action space
sk.action_space_dims("joint_pos_14")   # -> 14
sk.usage()                         # your spend so far
sk.key()                           # what this key is allowed to do

Call sk.taxonomy() rather than hard-coding action-space or domain strings.

preview_route() is a free dry run: it returns the ranked plan and the price without running any model.

plan = sk.preview_route(model="sidekick/auto:manipulation", contract="act")
print(plan[0]["estimated_list_usd"])   # the published rate
print(plan[0]["estimated_cost_usd"])   # what it would cost right now

Those two differ when the model is cold: waking a GPU costs real money and so does the minute it stays warm, and that is charged to the call that caused it. Run it twice and the second number drops.

Configuration

Sidekick(api_key=..., base_url=..., timeout=120.0, max_retries=2)

base_url defaults to $SIDEKICK_ORIGIN, then to the public gateway, so pointing at a private deployment needs no code change. The client is also a context manager, which closes the connection pool on exit:

with Sidekick(api_key="sk-sidekick-...") as sk:
    ...

When something goes wrong

SidekickError means the call did not succeed. It carries .status, .code and .body.

from sidekick_sdk import Sidekick, SidekickError, UnsafeResponse

try:
    act = sk.act(...)
except SidekickError as e:
    print(e.status, e.code)     # 409 no_route_available

Common ones:

Status Meaning What to do
401 bad or missing key check SIDEKICK_API_KEY
402 out of credits top up at the console
404 unknown model check the id against sk.models()
409 no live model fits loosen provider, or check sk.routes()

UnsafeResponse is separate on purpose. It means the call succeeded and the answer is wrong for this robot, which is the more dangerous case because nothing about the response looks broken.


Contributing

Issues and pull requests are welcome. See CONTRIBUTING.md.

License

Apache-2.0. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sidekick_sdk-0.1.2.tar.gz (26.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sidekick_sdk-0.1.2-py3-none-any.whl (19.5 kB view details)

Uploaded Python 3

File details

Details for the file sidekick_sdk-0.1.2.tar.gz.

File metadata

  • Download URL: sidekick_sdk-0.1.2.tar.gz
  • Upload date:
  • Size: 26.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for sidekick_sdk-0.1.2.tar.gz
Algorithm Hash digest
SHA256 421c3dbeb55cee0f0257b2a53d7b6649ad959865058b6f2fde4511048ea793e3
MD5 ce34940913e53f93699639bb34dc0bd8
BLAKE2b-256 a6b5e57d939c258e8f337f41d12f7356b78f1bc2794573321d0a384a93976291

See more details on using hashes here.

Provenance

The following attestation bundles were made for sidekick_sdk-0.1.2.tar.gz:

Publisher: publish.yml on sidekickrobo/sidekick-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sidekick_sdk-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: sidekick_sdk-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 19.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for sidekick_sdk-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 e61e519fe0b8704b88eeaaedfce0c6684e9c4ef86f6d11543b2a95a98dca34ee
MD5 ceeb209931d0eaea68dda44e097a8b95
BLAKE2b-256 87379f2962663e38e8d70c43503f598d4e472973237cfa51af2adddad0f1cff3

See more details on using hashes here.

Provenance

The following attestation bundles were made for sidekick_sdk-0.1.2-py3-none-any.whl:

Publisher: publish.yml on sidekickrobo/sidekick-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page