Astrodyne

Code examples

Every example on one page — non-streaming, streaming, and model listing, in curl, Python, and JavaScript. All examples use the ASTRODYNE_API_KEY environment variable and the YOUR_MODEL_ID model.

Chat Completions

curl
curl https://api.astrodyne.ai/v1/chat/completions \
  -H "Authorization: Bearer $ASTRODYNE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "model": "YOUR_MODEL_ID",
       "messages": [{"role": "user", "content": "Hello"}]
     }'
Python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.astrodyne.ai/v1",
    api_key=os.environ["ASTRODYNE_API_KEY"],
)

resp = client.chat.completions.create(
    model="YOUR_MODEL_ID",
    messages=[{"role": "user", "content": "Hello"}],
)
print(resp.choices[0].message.content)
JavaScript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.astrodyne.ai/v1",
  apiKey: process.env.ASTRODYNE_API_KEY,
});

const resp = await client.chat.completions.create({
  model: "YOUR_MODEL_ID",
  messages: [{ role: "user", content: "Hello" }],
});
console.log(resp.choices[0].message.content);

Streaming

curl
curl https://api.astrodyne.ai/v1/chat/completions \
  -H "Authorization: Bearer $ASTRODYNE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "model": "YOUR_MODEL_ID",
       "stream": true,
       "messages": [{"role": "user", "content": "Hello"}]
     }'
Python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.astrodyne.ai/v1",
    api_key=os.environ["ASTRODYNE_API_KEY"],
)

stream = client.chat.completions.create(
    model="YOUR_MODEL_ID",
    messages=[{"role": "user", "content": "Hello"}],
    stream=True,
)
for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="", flush=True)
JavaScript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.astrodyne.ai/v1",
  apiKey: process.env.ASTRODYNE_API_KEY,
});

const stream = await client.chat.completions.create({
  model: "YOUR_MODEL_ID",
  messages: [{ role: "user", content: "Hello" }],
  stream: true,
});
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}

Listing models

curl
curl https://api.astrodyne.ai/v1/models \
  -H "Authorization: Bearer $ASTRODYNE_API_KEY"
Python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.astrodyne.ai/v1",
    api_key=os.environ["ASTRODYNE_API_KEY"],
)

for model in client.models.list():
    print(model.id)
JavaScript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.astrodyne.ai/v1",
  apiKey: process.env.ASTRODYNE_API_KEY,
});

for await (const model of client.models.list()) {
  console.log(model.id);
}

API keys

These use your account session, not an API key — a key can never manage keys. See the API keys API.

curl

Python

JavaScript

Rotating a key

curl
# Rotate: same key id, new secret. The old secret stops working now.
curl -X POST https://api.astrodyne.ai/v1/api-keys/akey-YOUR-KEY-ID/rotate \
  -H "Authorization: Bearer $ASTRODYNE_SESSION_TOKEN"

# Revoke. Idempotent — a second call is still a success.
curl -X DELETE https://api.astrodyne.ai/v1/api-keys/akey-YOUR-KEY-ID \
  -H "Authorization: Bearer $ASTRODYNE_SESSION_TOKEN"
Python
import os
import httpx

session = {"Authorization": f"Bearer {os.environ['ASTRODYNE_SESSION_TOKEN']}"}
key_id = "akey-YOUR-KEY-ID"

with httpx.Client(base_url="https://api.astrodyne.ai") as http:
    rotated = http.post(f"/v1/api-keys/{key_id}/rotate", headers=session)
    rotated.raise_for_status()
    body = rotated.json()

    # Same id, new secret: usage history stays attached to the key.
    assert body["id"] == key_id
    new_secret = body["secret"]

    # Deploy new_secret everywhere BEFORE this point — the previous secret
    # stopped working the moment the call above returned.

    # When you are finished with a key entirely:
    revoked = http.delete(f"/v1/api-keys/{key_id}", headers=session).json()
    print(revoked["status"], "changed:", revoked["changed"])
JavaScript
const session = { Authorization: `Bearer ${process.env.ASTRODYNE_SESSION_TOKEN}` };
const keyId = "akey-YOUR-KEY-ID";

const rotated = await (await fetch(
  `https://api.astrodyne.ai/v1/api-keys/${keyId}/rotate`,
  { method: "POST", headers: session },
)).json();

// Same id, new secret: usage history stays attached to the key.
console.log(rotated.id === keyId, rotated.secret_once);

// Deploy rotated.secret everywhere BEFORE this point — the previous secret
// stopped working the moment the call above returned.

const revoked = await (await fetch(
  `https://api.astrodyne.ai/v1/api-keys/${keyId}`,
  { method: "DELETE", headers: session },
)).json();
console.log(revoked.status, revoked.changed);

Usage

curl
# Request history. Ranges are [start, end) — end is EXCLUSIVE.
curl "https://api.astrodyne.ai/v1/usage?limit=25" \
  -H "Authorization: Bearer $ASTRODYNE_SESSION_TOKEN"

# Totals, and spend per model.
curl "https://api.astrodyne.ai/v1/usage/summary" \
  -H "Authorization: Bearer $ASTRODYNE_SESSION_TOKEN"
curl "https://api.astrodyne.ai/v1/usage/by-model" \
  -H "Authorization: Bearer $ASTRODYNE_SESSION_TOKEN"
Python
import os
import httpx

session = {"Authorization": f"Bearer {os.environ['ASTRODYNE_SESSION_TOKEN']}"}

with httpx.Client(base_url="https://api.astrodyne.ai") as http:
    summary = http.get("/v1/usage/summary", headers=session).json()
    print(summary["request_count"], "requests,", summary["spend_usd"], "USD")

    # Page through request history. The cursor is opaque; do not construct one.
    cursor = None
    while True:
        params = {"limit": 100}
        if cursor:
            params["cursor"] = cursor
        page = http.get("/v1/usage", headers=session, params=params).json()
        for record in page["data"]:
            # billable_* is what you were charged on.
            # raw_* is what the model reported, and is None when it
            # reported nothing — do not treat that as zero.
            print(record["request_id"], record["model"],
                  record["billable_total_tokens"], record["charged_units"])
        if not page["has_more"]:
            break
        cursor = page["next_cursor"]
JavaScript
const session = { Authorization: `Bearer ${process.env.ASTRODYNE_SESSION_TOKEN}` };

const summary = await (await fetch(
  "https://api.astrodyne.ai/v1/usage/summary", { headers: session },
)).json();
console.log(summary.request_count, "requests,", summary.spend_usd, "USD");

// Page through history. The cursor is opaque; never construct one.
let cursor = null;
do {
  const url = new URL("https://api.astrodyne.ai/v1/usage");
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);

  const page = await (await fetch(url, { headers: session })).json();
  for (const record of page.data) {
    // billable_* is what you were charged on. raw_* is what the model
    // reported and is null when it reported nothing — not zero.
    console.log(record.request_id, record.model, record.billable_total_tokens);
  }
  cursor = page.has_more ? page.next_cursor : null;
} while (cursor);

Billing

curl
# Balance, derived from the append-only transaction record.
curl https://api.astrodyne.ai/v1/billing/balance \
  -H "Authorization: Bearer $ASTRODYNE_SESSION_TOKEN"

# Transaction history.
curl "https://api.astrodyne.ai/v1/billing/transactions?limit=25" \
  -H "Authorization: Bearer $ASTRODYNE_SESSION_TOKEN"
Python
import os
import httpx

session = {"Authorization": f"Bearer {os.environ['ASTRODYNE_SESSION_TOKEN']}"}

with httpx.Client(base_url="https://api.astrodyne.ai") as http:
    balance = http.get("/v1/billing/balance", headers=session).json()

    # Integer units are the source of truth: 1 USD = 100_000_000 units.
    # The decimal string is provided so you never have to divide.
    print(balance["balance_units"], "units =", balance["balance_usd"], "USD")

    history = http.get("/v1/billing/transactions", headers=session,
                       params={"limit": 25}).json()
    for entry in history["data"]:
        # type is a closed set: funding, usage, refund, reversal, adjustment.
        print(entry["created_at"], entry["type"], entry["amount_usd"])
JavaScript
const session = { Authorization: `Bearer ${process.env.ASTRODYNE_SESSION_TOKEN}` };

const balance = await (await fetch(
  "https://api.astrodyne.ai/v1/billing/balance", { headers: session },
)).json();

// Integer units are the source of truth: 1 USD = 100_000_000 units.
console.log(balance.balance_units, "units =", balance.balance_usd, "USD");

const history = await (await fetch(
  "https://api.astrodyne.ai/v1/billing/transactions?limit=25", { headers: session },
)).json();
for (const entry of history.data) {
  // type is a closed set: funding, usage, refund, reversal, adjustment.
  console.log(entry.created_at, entry.type, entry.amount_usd);
}

Handling every failure

A complete client covering 401, 403, 404, 409, 429 and 5xx, with the retry rules from Errors applied.

Python
import os
import time
import httpx

headers = {"Authorization": f"Bearer {os.environ['ASTRODYNE_API_KEY']}"}
body = {"model": "YOUR_MODEL_ID",
        "messages": [{"role": "user", "content": "Hello"}]}


def complete(http, attempt=0):
    response = http.post("/v1/chat/completions", headers=headers, json=body)

    # Log this on every outcome, success or failure. It is what support needs.
    request_id = response.headers.get("X-Astrodyne-Request-Id")

    if response.status_code == 200:
        return response.json()

    # Every failure has this exact shape. Branch on code, never on message.
    error = response.json()["error"]
    code = error["code"]

    if response.status_code == 401:
        raise SystemExit(f"Bad credential ({code}). Check ASTRODYNE_API_KEY.")

    if response.status_code == 403:
        # Authenticated, but this account may not use the API. Retrying
        # cannot help and nothing was charged.
        raise SystemExit(f"Access denied ({code}).")

    if response.status_code == 404:
        # The model is unknown OR not permitted for this key — deliberately
        # indistinguishable. Call /v1/models to see what you may use.
        raise SystemExit(f"Not found ({code}): {error['param']}")

    if response.status_code == 409:
        # An Idempotency-Key collision. idempotency_in_progress is worth
        # waiting on; idempotency_conflict means the body changed.
        if code == "idempotency_in_progress" and attempt < 5:
            time.sleep(2 ** attempt)
            return complete(http, attempt + 1)
        raise SystemExit(f"Conflict ({code}).")

    if response.status_code == 429:
        # Honour Retry-After when it is there; back off when it is not.
        wait = float(response.headers.get("Retry-After", 2 ** attempt))
        if attempt < 5:
            time.sleep(wait)
            return complete(http, attempt + 1)
        raise SystemExit("Rate limited; giving up.")

    if response.status_code >= 500:
        # billing_error is the one 5xx you must NOT retry: the request
        # completed and the charge is already flagged for review.
        if code == "billing_error":
            raise SystemExit(f"Billing error; quote request id {request_id}.")
        if attempt < 5:
            time.sleep(2 ** attempt)
            return complete(http, attempt + 1)

    raise SystemExit(f"Unhandled {response.status_code} {code} ({request_id})")


with httpx.Client(base_url="https://api.astrodyne.ai", timeout=60) as http:
    print(complete(http)["choices"][0]["message"]["content"])
JavaScript
const headers = {
  Authorization: `Bearer ${process.env.ASTRODYNE_API_KEY}`,
  "Content-Type": "application/json",
};
const body = JSON.stringify({
  model: "YOUR_MODEL_ID",
  messages: [{ role: "user", content: "Hello" }],
});

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function complete(attempt = 0) {
  const response = await fetch("https://api.astrodyne.ai/v1/chat/completions", {
    method: "POST", headers, body,
  });

  // Log this on every outcome. It is what support needs.
  const requestId = response.headers.get("X-Astrodyne-Request-Id");
  if (response.ok) return response.json();

  // Every failure has this exact shape. Branch on code, never on message.
  const { error } = await response.json();

  if (response.status === 401) throw new Error(`Bad credential (${error.code})`);
  if (response.status === 403) throw new Error(`Access denied (${error.code})`);
  if (response.status === 404) throw new Error(`Not found (${error.code})`);

  if (response.status === 409) {
    if (error.code === "idempotency_in_progress" && attempt < 5) {
      await sleep(2 ** attempt * 1000);
      return complete(attempt + 1);
    }
    throw new Error(`Conflict (${error.code})`);
  }

  if (response.status === 429 && attempt < 5) {
    const wait = Number(response.headers.get("Retry-After") ?? 2 ** attempt);
    await sleep(wait * 1000);
    return complete(attempt + 1);
  }

  if (response.status >= 500) {
    // billing_error is the one 5xx you must NOT retry.
    if (error.code === "billing_error") {
      throw new Error(`Billing error; quote request id ${requestId}`);
    }
    if (attempt < 5) {
      await sleep(2 ** attempt * 1000);
      return complete(attempt + 1);
    }
  }

  throw new Error(`Unhandled ${response.status} ${error.code} (${requestId})`);
}

const result = await complete();
console.log(result.choices[0].message.content);