API Gateway
https://gateway.agentsky.dev serves the Anthropic Messages API, the OpenAI Chat Completions, Responses and Embeddings APIs, and the decisions API behind one AgentSky key, plus APIs for search, maps, email, speech, transcription, SEO data and image generation (the run API). Every completed model call is billed; the charge appears inline on most responses and is always retrievable via GET /v1/generation.
Base URLs
| Client | Base URL |
|---|---|
Anthropic SDK, Claude Code (ANTHROPIC_BASE_URL) | https://gateway.agentsky.dev |
OpenAI SDK, Codex (OPENAI_BASE_URL) | https://gateway.agentsky.dev/v1 |
| Decisions API | https://gateway.agentsky.dev |
Authenticate with an AgentSky API key (ast_…, created under Developer → API keys) as x-api-key or Authorization: Bearer. Model calls and runs need the write scope; catalog and account reads need read.
Routes
| Method | Path | Notes |
|---|---|---|
| POST | /v1/messages | Anthropic Messages API, streamed or not |
| POST | /v1/messages/count_tokens | unmetered; requires a model route that supports token counting |
| POST | /v1/chat/completions, /v1/responses, /v1/embeddings | OpenAI APIs |
| POST | /api/alpha/decisions | decision models: {model, state, questions} → an answer per question |
| GET | /v1/models, /v1/models/{vendor}/{model} | public catalog with per-token prices, context, and modalities |
| GET | /v1/endpoints, /v1/endpoints/{provider}/{endpoint} | public run catalog, and one API with its input fields and limits |
| GET | /v1/providers | API provider list |
| POST | /v1/run | {provider, endpoint, input} → run object |
| GET | /v1/runs/by-idempotency | read a saved run using its original Idempotency-Key and API token |
| GET | /v1/runs/{runId} | retrieve a saved run owned by your account |
| GET | /v1/credits | balance and total used |
| GET | /v1/generation?id= | one call's record: tokens and cost |
Model ids
Use the catalog id (anthropic/claude-sonnet-5, qwen/qwen-max), an accepted alias (glm/glm-5.2, kimi/k3), or the bare id (claude-sonnet-5). A vendor prefix that matches nothing is a 404.
Anthropic compatibility
Messages supports JSON and streaming responses. Token counting is available for active Anthropic resources whose IDs end in -anthropic-api; it requires read scope and does not charge the wallet. Other resource routes may have different endpoint support.
Send anthropic-version on GET /v1/models to receive the Anthropic SDK list format (data, has_more, first_id, last_id), with limit, before_id, and after_id pagination. Retrieve a model with GET /v1/models/{id} and the same anthropic-version header; URL-encode the full model ID when it contains a slash. Without this header, the endpoint returns the AgentSky catalog with prices. Unknown model capability metadata is returned as null.
On Anthropic responses, request-id preserves the upstream request identifier when one is supplied and valid. Gateway-generated errors and Models responses use a gateway identifier instead. x-agentsky-request-id remains the separate AgentSky support reference; include both identifiers when reporting a problem. A request ID or thinking signature alone does not establish a direct connection to Anthropic.
For Messages requests to anthropic/claude-fable-5-1-anthropic-api, use thinking: {"type": "adaptive"} and output_config.effort to control reasoning. Explicit thinking.type values of disabled or enabled are not supported on this resource.
Message Batches (/v1/messages/batches) is not currently supported. Batch pricing and any batch discount are not offered by this gateway at this time.
Decision models
A decision model answers typed questions about supplied state instead of generating text, so it is served on /api/alpha/decisions rather than the chat paths — the catalog marks it "type": "decision" with "wires": ["decisions"]. Send {model, state, questions}, where each named question is {type: "noul" | "choice" | "score", instructions, criteria?}; the reply carries an answer with a probability per question. A chat model on this path is a 404, as is a decision model on the chat paths. The wire does not stream: stream: true is a 400. Errors use {"error":{"code","message"}}.
Failover
A model may be served from more than one place. The gateway tries them in its own order and fails over to the next when one is unreachable, times out before the first byte, or answers 408, 429, 401, 403 or 5xx; any other reply is returned as-is, and nothing is retried once response bytes have been sent. Which upstream serves a call is platform configuration, not a request option: OpenRouter's provider body field and Vercel's providerOptions.gateway are accepted and dropped before forwarding.
Cost in the response
Model calls: usage.cost (USD) on the final usage object — the usage-only last chunk of an OpenAI Chat Completions stream, response.completed on the Responses API, message_delta on the Anthropic wire, or the JSON body. Responses API streams ending in response.incomplete or response.failed are billed when they carry usage, but those terminal events are relayed without a usage.cost field; use GET /v1/generation?id=<response id> to retrieve the charge. Runs: price.amount.value is the customer charge in price.amount.currency (USD). The public billing cost fields have null values; they do not expose supplier costs. A RUNNING run may report zero before settlement; this is not a final price or a free generation. Prices are per token or per unit as listed in the catalog; GET /v1/generation?id=<upstream id or call id> returns the record once it has landed.
Runs
POST /v1/run
{ "provider": "exa", "endpoint": "search", "input": { "query": "agent gateways", "numResults": 5 } }
200 { "runId": "run_…", "status": "COMPLETED", "provider": "exa", "endpoint": "search",
"output": { … vendor response … },
"price": { "type": "PER_CALL", "amount": { "value": 0.014, "currency": "USD" } },
"providerResponse": { "httpStatus": 200 }, … }A request can return a completed run, or a saved run with status: RUNNING. Image and video generation can outlast the HTTP connection. A network timeout, HTML error page, or other non-JSON response does not tell you whether generation succeeded. Read GET /v1/runs/{runId} until the status is COMPLETED, FAILED, or TIMED_OUT; use a key with both read and write scopes.
Send an Idempotency-Key on the first POST. Save it, the exact request body, and any returned runId before retrying. If you lost the run ID, send GET /v1/runs/by-idempotency with the same API token and original Idempotency-Key; it requires read scope and never starts another supplier request. A 404 means no saved run is visible yet, so keep the original key and retry the read; a missing or malformed key is 400. The response is private and not cached. You can also repeat the POST with the same API key, idempotency key, and body to retrieve the existing run; that path may create a run if the first POST never arrived. Reusing the key with a different body returns 409 idempotency_conflict. Use a new key only for an intentionally new generation. Do not rotate keys or automatically submit a fresh paid request after a timeout.
Node.js example: submit, recover, and poll
Save this as run.mjs and run it with Node.js 22 or later. Set AGENTSKY_API_KEY and AGENTSKY_IDEMPOTENCY_KEY in your environment. Choose a unique idempotency key for this generation and keep it unchanged when restarting this script. Set RUN_ID to resume a known run directly. This example stops waiting after 30 minutes without cancelling the server-side job.
const apiKey = process.env.AGENTSKY_API_KEY;
const idempotencyKey = process.env.AGENTSKY_IDEMPOTENCY_KEY;
if (!apiKey || !idempotencyKey) {
throw new Error("Set AGENTSKY_API_KEY and a saved AGENTSKY_IDEMPOTENCY_KEY");
}
const baseUrl = "https://gateway.agentsky.dev";
const body = JSON.stringify({
provider: "openai",
endpoint: "gpt-image-2",
input: { prompt: "A red bicycle on a white background", quality: "low" },
});
let runId = process.env.RUN_ID;
const deadline = Date.now() + 30 * 60 * 1000;
let finished = false;
while (Date.now() < deadline) {
let response;
let run;
try {
response = await fetch(runId ? baseUrl + "/v1/runs/" + encodeURIComponent(runId) : baseUrl + "/v1/run", {
method: runId ? "GET" : "POST",
headers: {
Authorization: "Bearer " + apiKey,
"Content-Type": "application/json",
...(runId ? {} : { "Idempotency-Key": idempotencyKey }),
},
...(runId ? {} : { body }),
signal: AbortSignal.timeout(Math.min(180_000, deadline - Date.now())),
});
run = await response.json();
} catch {
if (response && response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429) {
throw new Error("HTTP " + response.status + "; keep the saved request and key for investigation");
}
await new Promise(resolve => setTimeout(resolve, 5000));
continue;
}
if (run.runId) {
runId = run.runId;
console.log("Save runId:", runId);
if (run.status === "COMPLETED") {
console.log("Output:", run.output);
console.log("Customer charge:", run.price.amount);
finished = true;
break;
}
if (run.status === "FAILED" || run.status === "TIMED_OUT") {
throw new Error(JSON.stringify({ runId, status: run.status, error: run.error, price: run.price }));
}
} else if (!response.ok && response.status !== 408 && response.status !== 429 && response.status < 500) {
throw new Error(JSON.stringify(run.error ?? run));
} else if (response.ok) {
throw new Error("Unexpected response; retain the same request and idempotency key");
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
if (!finished) {
throw new Error("Still unresolved. Resume with the same saved key/body or RUN_ID=" + (runId ?? "unknown"));
}A terminal failure includes error: { code, message }. An HTTP failure or connection timeout does not establish the run outcome or charge: a pending generation can still complete and settle. Retrieve the saved run and check its terminal status and price.amount; keep the run ID for reconciliation. Do not treat a pending zero amount as final or start a new generation merely because the connection failed.
Reading image and video outputs
The output object follows the endpoint's response shape; there is no universal binary wrapper. Check the endpoint reference and select the appropriate field after COMPLETED:
| Endpoint family | Media location in the run |
|---|---|
| GPT Image | run.output.data[].b64_json (base64 image) |
| Nano / Gemini image | run.output.candidates[].content.parts[].inlineData.data, with inlineData.mimeType; other parts can contain text |
| Wan image | run.output.output.choices[].message.content[].image (URL) |
| Video | H3, Seedance, Wan and HappyHorse: run.output.video_url (URL). |
| Binary audio endpoints | run.output.data (base64), with mediaType and bytes |
For a GPT image, save Buffer.from(run.output.data[0].b64_json, "base64") using Node's writeFile; use the returned format or MIME type for the file extension. For URL outputs, download the returned URL promptly and save the response bytes. Gateway media URLs contain a signed token valid for one hour: keep them private. Reading the run again issues a fresh link while the saved run and underlying media remain available; this does not guarantee permanent storage of the provider's media.
Errors
Errors take the shape of the wire: Anthropic { "type": "error", "error": { "type", "message" } }, OpenAI { "error": { "message", "type", "param", "code" } }, and { "error": { "code", "message" } } on run and account routes. Codes you will see: model_not_found, endpoint_not_found, pricing_unavailable (503, the SKU has no rate yet), upstream_unavailable, insufficient_credits / insufficient_quota (spend gate), rate_limited.
Limits
Request bodies up to 10 MB on the model wires and 1 MB on most APIs (35 MB for audio uploads). Media outputs up to 20 MB inline. Per-key rate limits apply.
AgentSky