Skip to main content

runbios-sdk

Official Python SDK for the Run BiOS fine-tuning platform API.

Installation

The distribution is runbios-sdk; the import name is bios (runbios is provided as an alias). (bios on PyPI is an unrelated project — installing it will not give you this SDK.)

pip install runbios-sdk

Quick Start

from bios import RunBiOS

client = RunBiOS(api_key="bios-...")

# Search the hosted catalog. Rows come straight from the model registry, so
# they are snake_case, and the HANDLE you pass everywhere else is repo_id
# (`id` is the registry UUID).
result = client.models.search(query="llama", type="llm", limit=5)
for model in result["models"]:
    print(f"{model['repo_id']} -- {model['params_total_b']}B params")

# Create a training job
job = client.training.create(
    idempotency_key="training-create-20260711-0001",
    model="meta-llama/Llama-3.1-8B-Instruct",
    dataset_ids=["ds_abc123", "ds_def456"],
    method="sft",
    adapter="lora",
    epochs=3,
    learning_rate=2e-4,
    lora_rank=16,
)
print(f"Job {job['id']} created -- status: {job['status']}")

Authentication

The SDK supports two authentication methods:

API Key (recommended). Keys start with bios- (legacy usf- keys stay valid):

client = RunBiOS(api_key="bios-...")

Environment variables. When api_key, base_url, or inference_key is omitted, the SDK reads RUNBIOS_API_KEY, RUNBIOS_BASE_URL, and RUNBIOS_INFERENCE_KEY from the environment (the legacy BIOS_API_KEY, BIOS_BASE_URL, and BIOS_INFERENCE_KEY names are still accepted as fallbacks):

export RUNBIOS_API_KEY=bios-...
export RUNBIOS_BASE_URL=https://api.runbios.ai   # optional; this is the default
export RUNBIOS_INFERENCE_KEY=sk-bios-...        # optional; defaults to RUNBIOS_API_KEY
client = RunBiOS()  # uses RUNBIOS_API_KEY / RUNBIOS_BASE_URL / RUNBIOS_INFERENCE_KEY

JWT Access Token:

client = RunBiOS(
    access_token="eyJhbG...",
    org_id="org_abc123",
)

Configuration

Parameter Type Default Description
api_key str RUNBIOS_API_KEY env var (legacy BIOS_API_KEY accepted) API key for authentication (prefix: bios-; legacy usf- keys stay valid)
access_token str None JWT access token (alternative to API key)
org_id str None Organization ID (required for JWT auth)
workspace_id str None Workspace ID (optional, overrides key default)
base_url str RUNBIOS_BASE_URL env var (legacy BIOS_BASE_URL accepted), then https://api.runbios.ai Canonical production hostname (release-gated; this documentation does not assert current availability). During prelaunch/dev, pass https://api-dev.runbios.ai explicitly.
timeout float 30.0 Request timeout in seconds
inference_key str RUNBIOS_INFERENCE_KEY env var (legacy BIOS_INFERENCE_KEY accepted), then api_key Key used by client.inference for /v1 calls. A per-deployment sk-bios-... key, or the platform api_key itself when it carries the serverless scope — you never pass the same key twice
inference_base_url str base_url Explicit dev or production inference hostname
inference_timeout float 900.0 End-to-end inference/stream timeout in seconds

Resources

Inference

Inference keys are separate from control-plane API keys, but you never pass one twice: inference_key falls back to RUNBIOS_INFERENCE_KEY and then to the control-plane api_key, so a platform key carrying the serverless scope calls /v1 directly. Pass an explicit inference_key for a per-deployment sk-bios-... key. Streaming yields each OpenAI SSE chunk as a dictionary and closes the upstream response when the iterator is closed. Calls are never retried automatically; if your application chooses to retry, reuse the same idempotency_key. The header is propagated, but the SDK does not claim server-side replay/deduplication unless the endpoint returns an explicit replay acknowledgement.

client = RunBiOS(
    api_key="bios-control-plane-key",
    inference_key="sk-bios-deployment-key",  # omit to reuse api_key
    # Use https://api-dev.runbios.ai explicitly during dev.
)

tools = [{
    "type": "function",
    "function": {
        "name": "lookup",
        "parameters": {"type": "object", "properties": {"id": {"type": "integer"}}},
    },
}]

stream = client.inference.stream_chat_completions(
    messages=[{"role": "user", "content": "Look up record 42"}],
    tools=tools,
    idempotency_key="chat-42-attempt-1",
)
try:
    for chunk in stream:
        print(chunk)
finally:
    stream.close()  # cancels/disconnects an unfinished generation

Serverless catalog models

Call any catalog model by id on the unified /v1 endpoint with a workspace platform key that carries the serverless scope — no per-deployment key. The gateway routes by model; dedicated deployments and serverless models share the same endpoint. reasoning_effort is forwarded, and streaming surfaces content and reasoning_content deltas incrementally.

# A platform key with the serverless scope is enough — `api_key` doubles as the
# inference key, so RunBiOS(api_key=K).inference.chat_completions(...) just works.
client = RunBiOS(api_key="bios-platform-key-with-serverless-scope")

stream = client.inference.stream_chat_completions(
    model="meta-llama/Llama-3.1-8B-Instruct",  # serverless catalog id
    messages=[{"role": "user", "content": "Explain tensor parallelism briefly."}],
    reasoning_effort="low",
)
try:
    for chunk in stream:
        delta = (chunk.get("choices") or [{}])[0].get("delta", {})
        if delta.get("reasoning_content"):
            print(delta["reasoning_content"], end="", flush=True)
        if delta.get("content"):
            print(delta["content"], end="", flush=True)
finally:
    stream.close()

# Non-streaming; the final response carries "usage" when the model reports it.
completion = client.inference.chat_completions(
    model="meta-llama/Llama-3.1-8B-Instruct",
    messages=[{"role": "user", "content": "One sentence on GPUs."}],
)
choice = completion["choices"][0]
answer = choice["message"].get("content")
if answer:
    print(answer)
else:
    # A 200 is NOT proof the model answered. Reasoning models can spend the
    # whole budget inside reasoning_content and return content: null with
    # finish_reason "length" — an empty answer that looks like success. Always
    # read choices[0].message.content, and raise max_tokens (or lower
    # reasoning_effort) when finish_reason is "length".
    print("no answer:", choice.get("finish_reason"),
          "reasoning tokens only:", bool(choice["message"].get("reasoning_content")))

Streaming billing is charged server-side on completed usage; the SDK only needs to request usage where the endpoint exposes it (no client change).

Models

Search the model catalog, fetch training configs, and check adapter compatibility. The catalog lists only models hosted on Run BiOS (the platform's own verified registry, mirrored in Run BiOS storage) — every result can be trained and deployed; it is never a live Hugging Face search.

Search results are the registry's own rows: snake_case fields, repo_id as the model handle (id is the registry UUID), plus maxContext — the native context window that caps a deployment's context_length — and weightBytes, the on-disk weight size. query becomes the registry's q filter, which is the only search parameter it reads.

# Search models
result = client.models.search(query="llama", type="llm", limit=10)
for m in result["models"]:
    print(m["repo_id"], m["params_total_b"], m["surface_type"],
          m.get("maxContext"), m.get("weightBytes"))

# One model, by its author/name handle
detail = client.models.get("meta-llama/Llama-3.1-8B-Instruct")
print(detail["model"]["architecture"], detail["model"]["maxContext"])

# The context ceiling on its own — None when the registry does not record it
print(client.models.native_max_context("meta-llama/Llama-3.1-8B-Instruct"))

# Get model config
config = client.models.get_config("meta-llama/Llama-3.1-8B-Instruct")
print(f"{config['totalParams']}B params, MoE: {config['isMoE']}")

# Check adapter compatibility
compat = client.models.get_adapter_compatibility(
    model_type="llama",
    training_method="rlhf",
    rlhf_algorithm="dpo",
)
usable = [a for a in compat["adapters"] if a["compatible"]]
print(f"{len(usable)} compatible adapters")

Datasets

Upload, import, preview, and manage training datasets.

# List datasets
datasets = client.datasets.list()

# Upload a dataset
uploaded = client.datasets.upload(
    file_path="./training_data.jsonl",
    name="My SFT Dataset",
)
print(f"Uploaded: {uploaded['id']}")

# Import from HuggingFace
imported = client.datasets.import_from_huggingface(
    repo_id="HuggingFaceH4/ultrachat_200k",
    integration_id="int_abc123",
    name="Ultrachat SFT",
    subset="default",
    split="train_sft",
)

# Preview dataset rows
preview = client.datasets.preview("ds_abc123", page=1, page_size=5)
print(preview["samples"][0].keys() if preview["samples"] else [])

# Validate before uploading
result = client.datasets.validate("./data.jsonl")
if result["format_valid"]:
    print(f"Valid {result['detected_format']} with {result['num_samples']} rows")
else:
    print("Errors:", result["validation_errors"])

# Search HuggingFace Hub
hub_results = client.datasets.search_hub(query="code instruct")

# Preview a Hub dataset
hub_preview = client.datasets.preview_hub(
    dataset_id="HuggingFaceH4/ultrachat_200k",
    subset="default",
    split="train_sft",
    limit=5,
)

# Get format specs
specs = client.datasets.get_format_specs()

# Get storage usage
usage = client.datasets.get_storage_usage()

# Delete a dataset
client.datasets.delete("ds_abc123")

Connected Hub search and exact source selection

Use client.integrations.list() to discover connected accounts, then select the returned ID explicitly. client.integrations.browse(integration_id, query=..., page=..., limit=...) searches with that account's access. Stored credentials are never returned by these discovery methods.

Pass the same integration_id to datasets.preview_hub and datasets.import_from_huggingface. Preview returns available_configs and available_splits; choose the actual subset/split rather than assuming train. A missing or unauthorized integration is an error, not anonymous fallback. Public imports without an integration use datasets.register_huggingface and require workspace_id on the client or method call.

Both import methods accept revision, max_samples, sample_strategy and import_mode. The service records immutable source metadata for resume. A Hub preview is not a guarantee of readiness or a preview of an arbitrary older revision: inspect the registered dataset when importing an explicit revision. Poll datasets.get_status(id) until ready before selecting it for training; a client polling timeout does not cancel the background import.

Training

Create, monitor, stop, and resume fine-tuning jobs.

dataset_ids preserves source order. Pass the same mixing dictionary to preflight and create: {"mode": "interleave", "weights": [3, 1], "seed": 42}. Weights correspond to the ordered IDs, not alphabetic order. Modes are sequential, shuffle, interleave, and phased; phased plans contain ordered phases with name, portion, optional weights, and shuffle. The default composition seed is 42. Source pins, order, plan and checksum are retained for metadata-only reconstruction on resume.

request = {
    "idempotency_key": "training-create-20260711-0001",
    "model": "meta-llama/Llama-3.1-8B-Instruct",
    "dataset_ids": ["ds_abc123", "ds_def456"],
    "method": "sft",
    "adapter": "lora",
    "epochs": 3,
    "learning_rate": 2e-4,
    "lora_rank": 16,
    "lora_alpha": 32,
    "gpu_type": "A100_80GB",
    "gpu_count": 1,
    "gpu_priorities": [
        {"gpu_type": "A100_80GB", "gpu_count": 1},
        {"gpu_type": "H100_80GB", "gpu_count": 1},
        {"gpu_type": "L40S_48GB", "gpu_count": 1},
    ],
    "queue_if_unavailable": True,
    "queue_deadline": "2026-07-18T00:00:00Z",
    "max_price_hour_cents": 500,
}

# Side-effect-free validation, canonical sizing, live stock and alternatives
check = client.training.preflight(request)
print(check["request_hash"], check.get("recommended"), check["queue_eligible"])

# Create the paid job only after reviewing preflight
job = client.training.create(**request)

# List jobs
jobs = client.training.list(status="running")
page = client.training.list_page(limit=50, offset=0)

# Get job details
job = client.training.get("job_abc123")
print(f"Status: {job['status']}, Progress: {job.get('progress', 0)}%")

# Get metrics
metrics = client.training.get_metrics("job_abc123")
for point in metrics["metrics"]:
    print(point["step"], point.get("loss"))

# Get checkpoints
checkpoints = client.training.get_checkpoints("job_abc123")
for cp in checkpoints:
    print(f"{cp['name']}: {cp['size_bytes']} bytes")

# Get logs
logs = client.training.get_logs("job_abc123")
for entry in logs["logs"]:
    print(entry["level"], entry["message"])

# Stop a job
client.training.stop("job_abc123", keep_data=True)

# Resume a stopped job
client.training.resume("job_abc123", idempotency_key="training-resume-20260711-0001")

# Delete a checkpoint
client.training.delete_checkpoint("job_abc123", "cp_xyz789")

Wallet

View wallet balance and transaction history.

balance_cents is the deposited balance; available_balance_cents is what can actually be spent right now (balance minus active_holds_cents and accruing_cents). Spend decisions read the second one. Auto top-up is flat (auto_topup_enabled / auto_topup_threshold / auto_topup_amount), and the transaction list is a wrapped page.

# Get balance
balance = client.wallet.get_balance()
print(f"Balance:   ${balance['balance_cents'] / 100:.2f}")
print(f"Spendable: ${balance['available_balance_cents'] / 100:.2f}")
print(f"On hold:   ${balance['active_holds_cents'] / 100:.2f}")
print(f"Accruing:  ${balance['accruing_cents'] / 100:.2f}")

# List transactions -- the rows are WRAPPED, so iterate ["transactions"]
page = client.wallet.get_transactions(limit=20)
print(f"{page['total']} transactions")
for t in page["transactions"]:
    print(f"{t['type']}/{t['category']}: ${t['amount_cents'] / 100:.2f} -- {t.get('description')}")

# Get pricing
pricing = client.wallet.get_pricing()

Inference deployment management

The same client.inference resource that makes OpenAI-compatible chat calls also manages model-serving deployments (create, monitor, stop, and delete).

allow_capacity_queue=True is explicit consent to wait for stock, so it requires 3 to 5 ranked gpu_priorities — the SDK rejects the combination locally before any request. Leave the queue off to book exactly one placement.

serving_mode, model_task, and supports_images are all server-derived and immutable: the platform reads them off the resolved model and rejects a conflicting assertion, so omit them and read the result back from the deployment. Which model_task values are accepted depends on the model, so the SDK does not judge one locally — it forwards whatever you pass and the server rules on it. preflight() echoes the derived task at canonical_request["model_task"] if you want to see it before creating.

context_length is optional and follows one policy: omit it and the server pre-fills min(native_max, 262144); the window is adjustable only when the model's native max exceeds the 32,768 floor, and it can never exceed the model's own native max. It is also a sizing input — a bigger window means a bigger KV cache, which can raise the minimum GPU count. Read the ceiling with client.models.native_max_context(model_id) first, and read it back from native_max_context on the deployment detail. Pass it as an int: the local capacity pre-check only runs on a window it can prove fits, so anything else (a numeric string included) is forwarded for the server to answer.

request = {
    "name": "llama-api",
    "source_type": "hf_model",
    "hf_model_id": "meta-llama/Llama-3.1-8B-Instruct",
    "gpu_type": "H100_80GB",
    "gpu_count": 1,
    # Queueing needs 3-5 ranked placements; the first is the one booked now.
    "allow_capacity_queue": True,
    "gpu_priorities": [
        {"gpu_type": "H100_80GB", "gpu_count": 1},
        {"gpu_type": "A100_80GB", "gpu_count": 1},
        {"gpu_type": "L40S_48GB", "gpu_count": 2},
    ],
    "max_price_hour_cents": 500,
}

# Never ask for more context than the model has.
native_max = client.models.native_max_context(request["hf_model_id"])
if native_max:
    request["context_length"] = min(65536, native_max)

# No wallet mutation and no GPU allocation.
check = client.inference.preflight(request)
print(check["selected_gpu"], check["alternatives"], check["billing"])

deployment = client.inference.create(request, idempotency_key="deploy-create-20260711-0001")
print(deployment["inference_key"])  # returned once; store securely

status = client.inference.status(deployment["id"])
print(status["status"], status.get("status_reason"), status.get("queue_expires_at"), status.get("wallet_authorization_status"))
# The detail resolves the per-deployment serving settings a list row omits.
print(status["context_length"], status["native_max_context"], status["model_task"])
# status stays "failed" for existing filters when status_reason is "queue_expired".
notification_history = client.inference.notifications(deployment["id"])
print([(row["event_type"], row["state"], row["attempt_count"]) for row in notification_history])

# Bounded newest-first listing; keep filters unchanged while using next_cursor.
# LIST rows are a lean grid projection: the model handle is `model_ref` (there
# is no `model` key) and the serving settings above are absent -- call
# client.inference.get(id) for those.
page = client.inference.list_page(limit=100, status="running", search="llama")
if page["has_more"]:
    older = client.inference.list_page(
        limit=100, status="running", search="llama", cursor=page["next_cursor"]
    )
    print(older["deployments"])
# Lazy traversal avoids one unbounded response.
for item in client.inference.iter_all(status="running"):
    print(item["name"], item["model_ref"], item["requests_total"])

client.inference.stop(deployment["id"])
client.inference.delete(deployment["id"])

GPU

View GPU pricing and get hardware recommendations.

# Get all GPU pricing
pricing = client.gpu.get_pricing()
for gpu in pricing["gpus"]:
    print(f"{gpu['display_name']}: {gpu['price_display']} -- {gpu['vram_gb']}GB VRAM")

# Authoritative model-aware choices, live stock, total pricing, and alternatives
options = client.gpu.get_options(
    "meta-llama/Llama-3.1-8B-Instruct",
    train_type="qlora",
    method="sft",
)

# Get recommended GPU for a model
rec = client.gpu.get_recommended("meta-llama/Llama-3.1-8B-Instruct")
if rec:
    print(f"Recommended: {rec['display_name']} x{rec['recommended_count']}")
    print(f"Total VRAM: {rec['total_vram_gb']}GB")
    print(f"Cost: ${rec['estimated_cost_per_hour_cents'] / 100:.2f}/hr")
    print(f"Reason: {rec['reason']}")

Introspect

Discover the permissions and scope of your API key.

info = client.introspect()
print(f"Org: {info['org']['name']}")
print(f"Scopes: {', '.join(info['scopes'])}")
print(f"Allowed tools: {len(info['allowed_mcp_tools'])}")

Error Handling

All API errors raise ApiError with status, code, request_id, and message attributes.

from bios import RunBiOS, ApiError

client = RunBiOS(api_key="bios-...")

try:
    job = client.training.get("bad_id")
except ApiError as e:
    if e.status == 404:
        print("Job not found")
    elif e.status == 401:
        print("Invalid API key")
    elif e.status == 403:
        print("Insufficient permissions")
    else:
        print(f"API error {e.status}: {e.message}")
        if e.request_id:
            print(f"Request ID: {e.request_id}")

GPU rejections

Every rejected GPU selection raises CapacityUnavailableError (a subclass of ApiError), for training and for inference, so one except reaches all of them and every one carries the same recovery data: alternatives (the canonical available_gpus list), minimum_requirement, selected, reason, and checked_at.

The rejections split into two families. Read permanent (or queue_offered), never the status or the code, to tell them apart:

reason code HTTP queue_offered permanent
insufficient_stock CAPACITY_UNAVAILABLE 409 True False
model_too_large GPU_TYPE_TOO_SMALL 400 False True
below_model_minimum GPU_COUNT_BELOW_MINIMUM 400 False True
invalid_gpu_count GPU_COUNT_INVALID 400 False True
gpu_unsupported GPU_TYPE_UNSUPPORTED 400 False True

CAPACITY_UNAVAILABLE means the GPU is not in stock right now, so waiting or joining the capacity queue can still get the request booked. The other four are fixed facts about the request: waiting does not help and the capacity queue cannot help either, however long the request waits. Only a different GPU type or a different GPU count can make it run.

from bios import RunBiOS, CapacityUnavailableError

try:
    deployment = client.inference.create(request, idempotency_key="deploy-create-20260725-0001")
except CapacityUnavailableError as e:
    print(e.message)
    print(e.reason, e.code, e.minimum_requirement)
    if e.permanent:
        # Never offer to wait here. Pick from the alternatives instead.
        for gpu in e.alternatives:
            print(gpu["gpu_type"], gpu["min_gpus"], gpu["valid_counts"])
    elif e.queue_offered:
        print("Out of stock right now. Retry later, or submit with the capacity queue.")
    if e.gpu_priorities_entry:
        print(f"gpu_priorities entry {e.gpu_priorities_entry} is the one to change")

inference.create runs the same check before it submits anything, so a selection that can never run this model raises the identical error (same code, same 400, queue_offered False) without a round trip. The service stays the authority and re-checks on create.

Python Version Support

  • Python 3.10+

License

MIT

Release files for runbios-sdk 0.2.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for runbios-sdk 0.2.3
File Size Uploaded
runbios_sdk-0.2.3.tar.gz 130.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for runbios-sdk 0.2.3
File Interpreter ABI Platform
runbios_sdk-0.2.3-py3-none-any.whl Python 3 none any Details

Total release size: 228.6 kB

Release files / runbios_sdk-0.2.3.tar.gz

Download URL runbios_sdk-0.2.3.tar.gz
Size 130.6 kB
Tags Source
SHA-256 checksum
How to use checksums
371ce5dac3d0ac40b5c743e7f79a2e3b360f03d5909654cd7e3e2c56ff6813d6
BLAKE2b-256 checksum
How to use checksums
2d1a704a615e0d1eb70472e04af7198f4f134b16a7bdffebfa078d0148cd0a83
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release files / runbios_sdk-0.2.3-py3-none-any.whl

Download URL runbios_sdk-0.2.3-py3-none-any.whl
Size 98.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5b8e21de1646637f7ce8971ca93f5f16f724946cb21d0c9a4a42b437dd2909e1
BLAKE2b-256 checksum
How to use checksums
c5e5bda3361fd6820f3478ffcef94b7ff0e255ba7775d7c46865336411d1a374
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release history Release notifications | RSS feed

0.2.19

2 release files

0.2.18

2 release files

0.2.17

2 release files

0.2.16

2 release files

0.2.15

2 release files

0.2.14

2 release files

0.2.13

2 release files

0.2.12

2 release files

0.2.11

2 release files

0.2.10

2 release files

0.2.9

2 release files

0.2.8

2 release files

0.2.7

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

This release

0.2.3 This release

2 release files

0.2.2

2 release 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