Skip to main content

infro

PyPI CI Python License: MIT Typed

The official Python client for the INFRO API — one endpoint for text, image, video and audio models.

pip install infro
from infro import Infro

infro = Infro()  # reads INFRO_API_KEY

image = infro.images.generate(
    model="bfl/flux-2-pro",
    prompt="a lighthouse in fog, 35mm",
)
print(image["data"][0]["url"])
print(f"cost: ${image['usage']['cost']}")

Why this and not the OpenAI SDK

For text, either works — INFRO serves an OpenAI-compatible /v1/chat/completions and any client that lets you override the base URL will talk to it. What the OpenAI SDK cannot express is the rest of the platform:

  • Images, video and audio, with usage.cost on every response.
  • Video as an async job, with a signed webhook when the render lands.
  • INFRO's request extensions — routing, fallbacks, logging, metadata — as ordinary keyword arguments rather than an untyped extra_body dictionary.
  • The error taxonomy as exceptions, so "should I retry this?" is except RateLimitError and not a substring match on a message.

Configuration

infro = Infro(
    api_key="sk_infro_...",          # or INFRO_API_KEY in the environment
    base_url="https://api.infro.io/v1",
    timeout=600.0,                    # seconds, per attempt
    max_retries=2,                    # attempts *after* the first
)

The key falls back to INFRO_API_KEY because a key in source is a key in version control.

Text

Shaped exactly like the OpenAI SDK, so moving a codebase across is a rename:

completion = infro.chat.completions.create(
    model="anthropic/claude-sonnet-5",
    messages=[{"role": "user", "content": "Hello"}],
)
print(completion["choices"][0]["message"]["content"])
print(completion["model"])   # the model that *served* — matters with fallbacks
print(completion["route"])   # "primary" or "standby_a" — a position, never a vendor

Streaming yields chunks and ends at [DONE]:

for chunk in infro.chat.completions.create(
    model="anthropic/claude-sonnet-5",
    messages=[{"role": "user", "content": "Write a haiku about fog."}],
    stream=True,
):
    delta = chunk["choices"][0]["delta"].get("content")
    if delta:
        print(delta, end="", flush=True)

A stream that ends without [DONE] raises rather than returning quietly: the gateway closes that way when a request fails after the first byte, so silence would hand you a truncated answer that looks complete.

Routing, fallbacks and cost control

completion = infro.chat.completions.create(
    model="openai/gpt-5.6-terra",
    messages=[{"role": "user", "content": "Hello"}],
    routing={"policy": "fastest", "regions": ["us", "eu"]},
    fallbacks=["deepseek/deepseek-v4-flash"],
    metadata={"user": "u_42", "feature": "summariser"},
    logging=False,   # this request's content is never stored
)

metadata is echoed on the request record and exported as OpenTelemetry span attributes, so per-feature cost attribution needs no extra instrumentation.

Video

Renders take minutes, so they are jobs. A webhook is the production path:

job = infro.videos.create(
    model="kuaishou/kling-o3",
    prompt="slow push-in on a lighthouse in fog",
    duration_seconds=6,
    webhook={"url": "https://api.example.com/hooks/infro"},
)
print(job["id"], job["status"])

In a script or a test, where there is nowhere for a webhook to land, poll:

finished = infro.jobs.wait_for(job["id"])
print(finished["output"]["url"])

wait_for raises on a failed render rather than returning it, because a caller who awaited "the finished video" will use whatever comes back as one.

Audio

audio = infro.audio.speech(
    model="elevenlabs/eleven-v3",
    input="The lighthouse keeper watched the fog roll in.",
    voice="rachel",
)
open("out.mp3", "wb").write(audio)

transcript = infro.audio.transcribe(
    model="openai/whisper-large-v3",
    file="https://example.com/recording.mp3",
)
print(transcript["text"])

Errors

from infro import BudgetExceededError, RateLimitError, UpstreamError

try:
    infro.chat.completions.create(model="...", messages=[...])
except BudgetExceededError as error:
    ...          # top up; retrying will not help
except RateLimitError as error:
    ...          # already retried for you; error.retry_after is the server's own hint
except UpstreamError as error:
    print(error.request_id)   # the first thing support will ask for

Every exception carries status, type, code, request_id and retryable. retryable is keyed on the HTTP status, not the type, because the status is what the docs promise and what a proxy preserves.

Retries

Only the four statuses the docs name as retryable — 408, 429, 502, 503 — and always with an Idempotency-Key, so a retry of a request the gateway already accepted returns the first response instead of doing the work twice. Retry-After wins over the computed backoff; jitter stops a fleet that failed together from retrying together.

Streams are never retried: past the first byte the caller has already seen part of an answer.

Async

Every method has an awaitable twin with the same signature:

from infro import AsyncInfro

async with AsyncInfro() as infro:
    completion = await infro.chat.completions.create(
        model="anthropic/claude-sonnet-5",
        messages=[{"role": "user", "content": "Hello"}],
    )

Anything this client does not model

The gateway grows faster than a client library, so there is an escape hatch that keeps the authentication and the retries:

data = infro.request("GET", "/usage?from=2026-08-01")

Documentation

https://infro.io/docs — the API reference, the error table, and the routing, fallback and privacy semantics this client is a thin shell over.

Metadata

Release files for infro 0.1.1

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

Source distribution (sdist)

Source distribution for infro 0.1.1
File Size Uploaded
infro-0.1.1.tar.gz 20.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for infro 0.1.1
File Interpreter ABI Platform
infro-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 38.3 kB

Release files / infro-0.1.1.tar.gz

Download URL infro-0.1.1.tar.gz
Size 20.7 kB
Tags Source
SHA-256 checksum
How to use checksums
a600cbfe3e5d257e4f02393c70ff5a2fa5a001a1c439a6fca67970046c0b59ee
BLAKE2b-256 checksum
How to use checksums
7d80faefd0ee209a5a539b7ea29181fe0160d0293a2494c96c04e76a3c6db743
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.3

Release files / infro-0.1.1-py3-none-any.whl

Download URL infro-0.1.1-py3-none-any.whl
Size 17.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3ea5526ffb16a39fd0a103efcd337120853dceec25a8dfdeb656c1443f90aaf2
BLAKE2b-256 checksum
How to use checksums
a87412851f1138d2e0e24cbb51f5bc0edc6d500894608fc4d24eb5e0662132f3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.3

Release history Release notifications | RSS feed

0.1.3

2 release files

0.1.2

2 release files

This release

0.1.1 This release

2 release files

0.1.0

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