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);