nolgia 0.1.0

Official Rust SDK for the Nolgia API: image, video, audio and agent generation, generated from the OpenAPI spec
Documentation

nolgia (Rust SDK)

Official Rust client for the Nolgia API, published on crates.io as nolgia with API docs on docs.rs/nolgia. Every endpoint is generated from the public OpenAPI spec with progenitor; the client, retries, typed errors, wait, stream, upload, download, pagination and agent helpers are hand-written. Async on tokio, with an optional blocking client.

Install

cargo add nolgia

That is the whole install: the crate re-exports its runtime (nolgia::tokio) and serde_json (nolgia::json!). Add --features blocking for the synchronous client. Minimum Rust: 1.88.

Authenticate

Requests carry an API key, a Personal Access Token (nol_...), as a bearer token. Create one in the app at nolgia.com/settings/api-tokens (or POST /pat with a signed-in session). The plaintext token is shown once.

use nolgia::tokio;

#[tokio::main]
async fn main() -> nolgia::Result<()> {
    // Reads NOLGIA_API_KEY (or NOLGIA_TOKEN), and NOLGIA_BASE_URL when set.
    let nolgia = nolgia::Client::from_env()?;
    // Or pass the key yourself:
    let nolgia = nolgia::Client::new("nol_...")?;
    Ok(())
}

The client talks to production, https://api.nolgia.ai/v1, unless you set a base URL (see Staging below). nolgia::Client::builder() sets the timeouts, the retry policy, a default Idempotency-Key and a User-Agent suffix.

First image

POST /generate/image is asynchronous: it answers 202 with a job, and GET /jobs/{id}/wait long-polls until the asset is ready (images usually land in seconds). subscribe does both and lists the outputs.

use nolgia::tokio;

#[tokio::main]
async fn main() -> nolgia::Result<()> {
    let nolgia = nolgia::Client::from_env()?;
    let done = nolgia.subscribe(nolgia::image("gpt-image-2", "a red fox in the snow")).await?;
    nolgia.download(done.url().expect("an image"), "fox.png").await?;
    Ok(())
}

model is required on every generation; GET /models (nolgia.list_models()) is the live catalog of ids, capabilities and prices. nolgia::image, nolgia::video and nolgia::audio build the request; add any other field with .set("aspect_ratio", "16:9"). The typed builders in nolgia::types (GenerateVideoRequest::builder(), ...) work too.

First video

Video is the same job model with a longer wait. submit returns as soon as the job is queued; wait long-polls GET /jobs/{id}/wait, calling again when a window closes with a 408 (the job is still running).

use nolgia::tokio;

#[tokio::main]
async fn main() -> nolgia::Result<()> {
    let nolgia = nolgia::Client::from_env()?;
    let request = nolgia::video("seedance-2.5", "a red fox trots through fresh snow at dawn, slow dolly in")
        .set("duration_seconds", 5);

    // Free: what it will cost, from the submit path's own pricer.
    println!("{} credits", nolgia.quote(request.clone()).await?.credits);

    let handle = nolgia.submit(request).await?;
    match handle.result().await {
        Ok(done) => {
            nolgia.download(done.url().expect("a video"), "fox.mp4").await?;
        }
        Err(nolgia::Error::Job(failed)) => {
            // `credits_refunded`: Some(true) refunded, Some(false) charged,
            // None unknown. Never read None as "no charge".
            eprintln!("{} ({}), refunded: {:?}", failed.message, failed.code, failed.credits_refunded);
        }
        Err(err) => return Err(err),
    }
    Ok(())
}

A wait gives up after 30 minutes by default (WaitOptions) with Error::Timeout; that only stops waiting, it does not cancel the job, and the credits are still spent. handle.cancel() cancels it on the server (POST /jobs/{id}/cancel): the returned job's cancellation says what the provider did and whether the credits were refunded, and a job that already finished is refused with 409 and code job_not_cancellable.

handle.events() streams live status (GET /jobs/{id}/sse) instead of polling; nolgia.session_events(session_id, last_event_id) streams an agent session's transcript.

List assets

Every operation is a method with a builder for its parameters. .typed() turns the result into nolgia::Result, and paginate_assets follows next_cursor for you.

use nolgia::{StreamExt, Typed, tokio};

#[tokio::main]
async fn main() -> nolgia::Result<()> {
    let nolgia = nolgia::Client::from_env()?;

    let page = nolgia.list_assets().limit(10u64).send().typed().await?;
    for asset in &page.items {
        println!("{} {} {}", asset.id, asset.modality, asset.signed_url);
    }

    let mut every = std::pin::pin!(nolgia.paginate_assets(|list| list.limit(100u64)));
    while let Some(asset) = every.next().await {
        println!("{}", asset?.id);
    }
    Ok(())
}

Errors

Every call returns nolgia::Result<T>. A refusal is Error::Api: the HTTP status, the machine-readable code, the detail to show and the request_id for support. Branch on the code, never on the text.

use nolgia::tokio;

#[tokio::main]
async fn main() -> nolgia::Result<()> {
    let nolgia = nolgia::Client::from_env()?;
    match nolgia.submit(nolgia::image("gpt-image-2", "a red fox")).await {
        Ok(handle) => println!("queued {}", handle.id()),
        // 402 out_of_credits: top up. Upgrading does not fix it.
        Err(err) if err.is_out_of_credits() => eprintln!("not enough credits"),
        // 402 Upgrade Required: a plan limit. Credits do not fix it.
        Err(err) if err.is_plan_required() => eprintln!("this model needs a higher plan"),
        Err(nolgia::Error::Api(refusal)) if refusal.is_duplicate_submission() => {
            eprintln!("already running as job {:?}", refusal.job_id);
        }
        Err(err) => return Err(err),
    }
    Ok(())
}

Error::Job is a job that failed or was canceled (code, message, credits_refunded, the job). The full code list is in the errors guide; unknown codes arrive verbatim as ErrorCode::Other.

Retries and idempotency

Requests the server refused for now (429) or failed to answer (500, 502, 503, 504, a dropped connection) are retried twice with exponential backoff and jitter, honouring Retry-After, whenever repeating them cannot do the work twice: reads, PUT/DELETE, anything with an Idempotency-Key, and the generation submits. Those are safe because the API refuses an identical submit within five minutes with 409 naming the existing job, so a lost response never becomes a second charge; submit resolves its own retry onto that job. RetryPolicy (Client::builder().retry(...)) changes the counts and delays.

To deliberately generate the same request again, send a distinct key: nolgia.submit_with(request, SubmitOptions::default().idempotency_key("run-2")), or nolgia.with_idempotency_key("run-2")? for every POST of a client clone. Repeating a key makes your own retries collapse onto one job.

Upload and download

use nolgia::{UploadOptions, tokio};

#[tokio::main]
async fn main() -> nolgia::Result<()> {
    let nolgia = nolgia::Client::from_env()?;
    // Signed upload: streams from disk straight to storage, any size the API takes.
    let asset = nolgia.upload_file("still.png", UploadOptions::default().tag("reference")).await?;
    // Use it as a reference in a generation.
    let request = nolgia::image("gpt-image-2", "the same scene at night")
        .set("reference_asset_ids", [asset.id]);
    let done = nolgia.subscribe(request).await?;
    // Downloads stream through `<path>.part` and never send your key to storage hosts.
    nolgia.download(done.url().expect("an image"), "night.png").await?;
    Ok(())
}

Agent

run_agent sends your NOLGIA Agent a message, waits for the reply, and lists the assets it made (each turn is billed; see the pricing page).

use nolgia::{RunAgentOptions, tokio};

#[tokio::main]
async fn main() -> nolgia::Result<()> {
    let nolgia = nolgia::Client::from_env()?;
    let reply = nolgia.run_agent(RunAgentOptions::new("Write three taglines for a coffee brand")).await?;
    println!("{:?}: {}", reply.status, reply.text);
    Ok(())
}

Blocking

With --features blocking, nolgia::blocking::Client offers the same helpers without async (it runs its own runtime, so do not call it from inside one):

let nolgia = nolgia::blocking::Client::from_env()?;
let done = nolgia.subscribe(nolgia::image("gpt-image-2", "a red fox in the snow"))?;
nolgia.download(done.url().unwrap(), "fox.png")?;

Examples

examples/ has runnable programs: image, video (with live progress), voice, agent_chat and presets. Run one with NOLGIA_API_KEY=nol_... cargo run --example image -- "a red fox". They spend credits, except presets without --run and image --quote.

Staging

Point the client at staging: nolgia::Client::builder().api_key("nol_...").base_url("https://api.stg.nolgia.ai/v1").build()? (or NOLGIA_BASE_URL=https://api.stg.nolgia.ai/v1). Staging tokens are minted on stg.nolgia.ai and do not work against production.

Docs

Layout

  • src/api.rs: generated, do not edit; regenerate with make sdk-rust from the repo root
  • codegen/: the generator (progenitor plus the spec rewrites it needs), not published
  • src/{client,transport,error,jobs,files,stream,pagination,agent,blocking}.rs, examples/, tests/, README.md: hand-written

The README's code blocks compile as doctests on every cargo test, so a regenerated client that renames a symbol breaks the build here instead of the README. tests/stg_smoke.rs runs against staging when NOLGIA_STG_API_KEY is set, and never spends credits.

License

MIT