Agent integration guide
Complete integration guides, also available as one text document or individual Markdown files.
- Agent quickstart
- Authentication and Connect boundaries
- CLI products and public sources
- Model discovery and capability limits
- Streaming text and detecting failure
- Client-side tool-call cycle
- Structured output: validate, do not assume
- Errors, retry policy and request IDs
- Integration sandbox and enable prerequisite
Agent quickstart
Choose a real model and make a first request with a regular API key. Read Markdown
For applications, automation and your own agents, use a regular Aether API key. Create it in an authorized project in https://dashboard.aetherapi.dev. A Connect subscription credential is restricted to supported interactive coding connectors; it is not a general API key.
- API origin: https://api.aetherapi.dev. OpenAI-compatible SDK base URL: https://api.aetherapi.dev/v1. Append /chat/completions to that SDK base, not /v1/chat/completions again.
- HTTP authentication: Authorization: Bearer <your API key>. Keep keys in environment variables or a secret store; never embed them in frontend bundles or logs.
- Discover exact model IDs, supported_endpoints and per-endpoint capabilities with GET https://api.aetherapi.dev/v1/models. Do not infer an ID from a marketing name or use the first model without checking its endpoint.
- Machine-readable contract: GET https://api.aetherapi.dev/openapi.json (OpenAPI 3.0.3, no authentication). Its schemas describe possible request and response shapes, not the capabilities of a particular model.
- Live generation can consume credits. Use the separately enabled /v1/sandbox routes for simulated integration checks. Never silently fall back from sandbox to live requests.
Select a model
The public model catalogue can be read without credentials. This command retrieves the catalogue. Select a chat model, then set AETHER_MODEL to that exact ID. The model-discovery document includes a filtered listing. Node.js 20 or later is required for the JavaScript examples; no SDK dependency is needed.
curl --fail-with-body https://api.aetherapi.dev/v1/models
# Set AETHER_API_KEY privately in your environment.
# Set AETHER_MODEL to an exact catalogue ID supporting /v1/chat/completions.Make a request
Save as quickstart.mjs and run node quickstart.mjs. This checks the current public catalogue before making one live request. It does not prove access for your key or advanced model features.
const origin = "https://api.aetherapi.dev";
const apiKey = process.env.AETHER_API_KEY;
const model = process.env.AETHER_MODEL;
if (!apiKey || !model) throw new Error("Set AETHER_API_KEY and AETHER_MODEL");
const catalogueResponse = await fetch(origin + "/v1/models");
if (!catalogueResponse.ok) throw new Error("Model discovery failed: " + catalogueResponse.status);
const catalogue = await catalogueResponse.json();
const entry = catalogue.data.find(item => item.id === model);
if (!entry?.supported_endpoints?.includes("/v1/chat/completions")) {
throw new Error("Choose an exact model ID supporting /v1/chat/completions");
}
const response = await fetch(origin + "/v1/chat/completions", {
method: "POST",
headers: { Authorization: "Bearer " + apiKey, "Content-Type": "application/json" },
body: JSON.stringify({ model, messages: [{ role: "user", content: "Say hello in one sentence." }] }),
});
const result = await response.json();
if (!response.ok || result.error) {
throw new Error(JSON.stringify({ status: response.status, error: result.error,
request_id: response.headers.get("x-request-id") }));
}
console.log(result.choices[0].message.content);The next documents cover authentication, model capabilities, streaming, client-side tool execution, structured output, retries with optional idempotency and the sandbox. Read those limits before adding autonomous actions.
Authentication and Connect boundaries
Use existing project permissions and distinguish API keys from Connect credentials. Read Markdown
Regular API keys
Send Authorization: Bearer <key> to authenticated /v1 endpoints. x-api-key is also accepted, including for /v1/messages. If both headers are sent they must contain the same credential; different credentials produce conflicting_authorization (401). Never use a dashboard session token as an API key.
Use a key from a project you are authorized to access. The key determines project attribution and billing; selecting a model or changing request headers does not grant project membership. Account status, key enabled/revoked state, model restrictions, project membership and spending limits are enforced by the server. The public model catalogue is not proof that a key can use a model.
Connect credentials
Connect credentials use the aeth_dev_ prefix and require an active subscription and a supported interactive coding connector. Credential client IDs include aether-cli, claude-code, codex, opencode and pi; the aether-cli identity belongs to the Connect launcher, not the separate Go aethercli application. Do not repurpose stored Connect credentials for generic scripts, background jobs or arbitrary OpenAI-compatible clients. Use a regular API key for those workloads.
- Connect generation is restricted to text coding via /v1/chat/completions, /v1/responses and /v1/messages. /v1/messages/count_tokens and Connect usage/budget endpoints are also permitted. The authenticated Connect model catalogue is separate from the public API catalogue.
- Connect excludes image, audio, video and embedding endpoints, multimodal input/output, hosted tools and server-side conversation context. Client-side coding tools are supported through the connector transports.
- Connect credentials cannot authenticate /v1/sandbox. Sandbox examples require a regular API key.
- Connect applies daily quota, request-rate and output-token limits. devpass_* error codes are still used by the backend for Connect errors.
Protocol-specific requests
For /v1/chat/completions send messages; for /v1/responses send input; for /v1/messages send messages and max_tokens, and set anthropic-version: 2023-06-01. Choose a model whose supported_endpoints includes the requested endpoint. Do not assume every public model is translated between all three protocols.
CLI products and public sources
Distinguish the Connect launcher from the independent Go text-model CLI. Read Markdown
Connect launcher: aether
The API repository's devpass-cli is the Node.js source for the Connect login and coding-tool launcher. Its npm package is @aetherapi/cli; native packaged releases are also offered. Commands include aether login, aether connect codex, aether codex, usage, budget and update. It supplies Connect credentials to supported interactive connectors. It is not the Go text-model client.
Connect launcher source and installation instructions
Go text-model client: aethercli
The independent github.com/Aculnaj/aethercli application uses a regular Aether API key. Its main-branch README documents model discovery (aethercli models --json), prompts such as aethercli ask "Hello" --model <exact-model-id>, streaming, chat/TUI sessions, explicit requests (aethercli api --protocol chat --request request.json) and an explicit sandbox command (aethercli sandbox --scenario success --json) that always uses /v1/sandbox and still needs the server-side sandbox flag. Its --json flag describes CLI output; it does not establish model-side JSON Schema enforcement.
Directly readable Go AetherCLI README
Directly readable Go AetherCLI command reference
Released binaries can lag the main branch. Run aethercli --help --json for the machine-readable help of the installed version; the CLI's own reference is authoritative for its flags, exit codes and limits. This page asserts no CLI behaviour beyond those sources. Do not interchange aether and aethercli commands or use a Connect credential where a regular API key is required.
Model discovery and capability limits
Select actual catalogue IDs and read endpoint-specific supported, unsupported or unknown capabilities. Read Markdown
GET https://api.aetherapi.dev/v1/models returns an OpenAI-style object with data entries. Use id verbatim as the request model. supported_endpoints lists public routes for that ID; endpoint is its primary route. An ID may include slashes: preserve it as a JSON string.
Each entry also has capabilities_version: 1 and a capabilities object keyed by public request endpoint. There is no model-wide feature flag: the same model can behave differently on /v1/chat/completions, /v1/responses and /v1/messages.
# Requires jq. Discovery does not generate text or consume credits.
curl --fail-with-body --silent --show-error https://api.aetherapi.dev/v1/models \
| jq --arg endpoint /v1/chat/completions '.data[]
| select(.supported_endpoints | index($endpoint))
| {id, endpoint: $endpoint, capabilities: .capabilities[$endpoint]}'Read capabilities as supported, unsupported or unknown
- Feature values are the strings supported, unsupported or unknown: tool_calling (client-executed function tools, not hosted tools), parallel_tool_calls, structured_outputs.json_schema (schema-constrained output; no JSON-object mode is implied), modalities.input and modalities.output for text, image, audio, video, file and embedding, and reasoning.supported.
- supported: every provider the route may fall back to confirms the feature and Aether preserves it. unsupported: at least one possible fallback lacks it or Aether removes it. unknown: support is not established. A missing capabilities object, endpoint entry or field is unknown. Never treat unknown as supported.
- Do not reuse one endpoint's values for another endpoint. reasoning.options lists guaranteed common request values (null means unknown). max_output_tokens is a metadata limit or null when unknown; output_token_limit is Aether's own request ceiling or null. context is display metadata, not a guaranteed token limit.
- Many routes report unknown until operators add verified per-provider declarations. Prices and models change; refresh discovery when a model is missing or a route changes.
Check access with your key
Send your regular API key to GET /v1/models and each endpoint capability also contains access: {allowed, reason}. reason is null when allowed, otherwise model_not_allowed, model_tier_restricted or route_unavailable. access covers model policy and route eligibility only, not balance, budget, quota or capacity. An invalid key returns an authentication error instead of the public catalogue.
# Chat models with supported client tool calls that this key may use.
curl --fail-with-body --silent --show-error https://api.aetherapi.dev/v1/models \
-H "Authorization: Bearer $AETHER_API_KEY" \
| jq --arg endpoint /v1/chat/completions '[.data[]
| select(.capabilities[$endpoint].tool_calling == "supported")
| select(.capabilities[$endpoint].access.allowed == true)
| .id]'What discovery does not prove
- A listed or access-allowed model is not a promise that a request succeeds. Budget, quota, wallet limits and provider availability are checked per request.
- For regular API keys the general JSON proxy forwards request fields such as tools, tool_choice and response_format to the selected provider. Forwarding is not support: rely on a feature only when its capability is supported for that exact endpoint, and still handle failures.
- A failed feature probe must not trigger silent selection of an unrelated model.
wrong_endpoint (400) can include error.endpoints; choose one of those routes only if your client supports its protocol. model_not_found (404) calls for refreshing the catalogue and correcting the ID. The OpenAPI document describes request and response shapes; the capabilities object is the per-model source.
Streaming text and detecting failure
Consume Chat Completions SSE and recognize partial or failed output. Read Markdown
For a chat model that supports streaming, set stream: true on POST /v1/chat/completions. The response uses text/event-stream. This example requires curl and jq, plus AETHER_API_KEY and an exact AETHER_MODEL chosen from the catalogue.
jq -n --arg model "$AETHER_MODEL" \
'{model: $model, messages: [{role: "user", content: "Say hello briefly."}], stream: true}' \
| curl --no-buffer --fail-with-body --silent --show-error \
https://api.aetherapi.dev/v1/chat/completions \
-H "Authorization: Bearer $AETHER_API_KEY" \
-H 'Content-Type: application/json' --data-binary @-Client parsing rules
- Use an SSE parser that buffers across network chunks, decodes UTF-8 incrementally and dispatches events at blank lines. A TCP/read chunk is not an entire event. Ignore comment/keepalive lines.
- For Chat Completions, parse data JSON, read choices[].delta.content and track each choice separately. Some events contain role or usage only, or choices: []; do not assume content is present.
- If tools are streamed, accumulate delta.tool_calls by index, joining fragmented function.name and function.arguments. Parse arguments and execute only after the complete tool call has arrived and the turn has finished successfully.
- Check every JSON event for error before accepting text. HTTP 200 only means the stream started. A live chat failure can contain data: {"error": {...}} followed by data: [DONE]; [DONE] alone does not prove success.
- A disconnect before the protocol finishes is incomplete output. Keep it marked partial, do not execute tools from it, and do not splice a retried answer onto it. finish_reason: length also means the answer was truncated.
/v1/responses and /v1/messages use their own named SSE events. Responses errors can appear as event: error or response.failed; Anthropic uses event: error. Use a parser for the chosen protocol rather than treating these as chat delta objects. Do not assume that switching endpoints makes streaming available for every model.
For deterministic parser checks, use the enabled sandbox's success and stream_error scenarios. stream_error emits partial text or tool-call arguments, then sandbox_simulated_stream_error without a successful completion (no [DONE] on chat). See the sandbox document before testing; no production enablement is assumed.
Client-side tool-call cycle
Complete a bounded Chat Completions tool loop and return results using tool_call_id. Read Markdown
This example requires a regular API key and a chat model whose capabilities["/v1/chat/completions"].tool_calling is supported; with unknown, verify the route yourself first. Aether forwards the tool definitions; your application validates arguments and executes the functions. It does not execute arbitrary local functions for you. parallel_tool_calls tells you whether several calls per turn are reliable; the loop below handles several either way. The enabled sandbox can rehearse one deterministic call-and-result cycle without a provider (see the sandbox document); that does not prove a live model's tool support.
- Send messages and tools. Inspect choices[0].message.tool_calls after a successful response.
- Append the entire assistant message including its tool_calls to the conversation. Validate every call against your allowlist and argument schema before running it.
- Append one role: tool message per call, with tool_call_id equal to the returned call.id and content as a string (JSON.stringify for objects). Return all results for a turn before requesting the next assistant turn.
- Send the updated messages and tool definitions again. Repeat until the assistant returns a final response; limit the number of rounds and handle truncated or failed turns explicitly.
Save as tools.mjs and run node tools.mjs with Node.js 20 or later. The only allowed function returns the current UTC time. For tools with external effects, enforce permissions and any required user approval in your application.
const apiKey = process.env.AETHER_API_KEY;
const model = process.env.AETHER_MODEL;
if (!apiKey || !model) throw new Error("Set AETHER_API_KEY and AETHER_MODEL");
const tools = [{ type: "function", function: {
name: "get_utc_time", description: "Read the current UTC time.",
parameters: { type: "object", properties: {}, additionalProperties: false },
} }];
const messages = [{ role: "user", content: "Call get_utc_time and tell me the UTC time." }];
for (let round = 0; round < 4; round++) {
const response = await fetch("https://api.aetherapi.dev/v1/chat/completions", {
method: "POST",
headers: { Authorization: "Bearer " + apiKey, "Content-Type": "application/json" },
body: JSON.stringify({ model, messages, tools }),
});
const result = await response.json();
if (!response.ok || result.error) throw new Error("Request failed: " + (result.error?.code || response.status));
const choice = result.choices?.[0];
const message = choice?.message;
if (!message) throw new Error("Missing assistant message");
if (!["stop", "tool_calls"].includes(choice.finish_reason)) throw new Error("Incomplete assistant turn");
const calls = message.tool_calls || [];
if (!calls.length) {
if (choice.finish_reason !== "stop") throw new Error("Missing tool calls");
console.log(message.content);
break;
}
if (choice.finish_reason !== "tool_calls") throw new Error("Unexpected tool-call finish reason");
// Validate the whole batch before executing any function.
for (const call of calls) {
if (!call.id || call.type !== "function" || call.function?.name !== "get_utc_time") {
throw new Error("Unknown tool call");
}
const args = JSON.parse(call.function.arguments);
if (!args || Array.isArray(args) || typeof args !== "object" || Object.keys(args).length) {
throw new Error("get_utc_time expects an empty object");
}
}
messages.push(message);
for (const call of calls) {
messages.push({ role: "tool", tool_call_id: call.id,
content: JSON.stringify({ utc: new Date().toISOString() }) });
}
if (round === 3) throw new Error("Tool round limit reached");
}Structured output: validate, do not assume
Use JSON Schema output only where the catalogue reports support, and validate every answer locally. Read Markdown
For regular API keys, the proxy forwards response_format (Chat Completions), text.format (Responses) or output_config.format (Messages) to the provider. Aether does not validate or enforce the schema itself. Do not infer support from OpenAI-compatible request syntax.
Check capabilities[endpoint].structured_outputs.json_schema in model discovery. supported means every possible fallback confirms schema-constrained output and Aether preserves it; it does not promise every JSON Schema keyword, and no json_object mode is implied. With unsupported or unknown, do not send the option. Request JSON in the prompt and validate the result locally instead. Prompting for JSON is not constrained decoding and can fail; reject invalid or truncated output and bound any repair attempts. Validate even when support is reported.
Validate a text answer before using it
This Node.js example uses the same regular API-key environment as the quickstart. It deliberately relies on no unverified response_format option. Save as structured-output.mjs. A validation error is an expected failure mode and must stop downstream actions.
const apiKey = process.env.AETHER_API_KEY;
const model = process.env.AETHER_MODEL;
if (!apiKey || !model) throw new Error("Set AETHER_API_KEY and AETHER_MODEL");
const response = await fetch("https://api.aetherapi.dev/v1/chat/completions", {
method: "POST",
headers: { Authorization: "Bearer " + apiKey, "Content-Type": "application/json" },
body: JSON.stringify({ model, messages: [{ role: "user",
content: 'Return only a JSON object with exactly one field: "summary" (a string). Summarize: The sky is blue.' }] }),
});
const result = await response.json();
if (!response.ok || result.error) throw new Error("Request failed");
if (result.choices?.[0]?.finish_reason !== "stop") throw new Error("Incomplete output");
const value = JSON.parse(result.choices[0].message.content);
if (!value || Array.isArray(value) || typeof value !== "object"
|| Object.keys(value).length !== 1 || typeof value.summary !== "string") {
throw new Error("Invalid summary schema");
}
console.log(value.summary);The sandbox can rehearse json_schema output for Chat Completions, Responses and Messages with a bounded schema subset (see the sandbox document). It returns a value generated from the schema, not model output, so it cannot establish live support or semantic accuracy.
Errors, retry policy and request IDs
Classify error codes, read retry-safety fields and use optional idempotency only where it is enabled. Read Markdown
For OpenAI-style endpoints inspect error.code, error.type, error.param and error.hint; keep error.request_id or the X-Request-ID response header for diagnostics. /v1/messages uses the Anthropic envelope with type: error and a top-level request_id; the same diagnostic fields are inside its error object. Branch on codes, status and the fields below, not on message text. Do not log credentials or full sensitive prompts.
{
"error": {
"message": "Insufficient credits for this request.",
"type": "insufficient_quota",
"code": "insufficient_credits",
"param": null,
"hint": "Add credits to the wallet used by this key. For shared projects, contact the project owner.",
"request_id": "req_example",
"retryable": false,
"retry_after_ms": null,
"safe_to_retry": true,
"execution_status": "not_started",
"usage_may_have_occurred": false
}
}Retry-safety fields
- retryable: the cause is classified as temporary. It says nothing about whether repeating is safe or free.
- retry_after_ms: the wait in milliseconds, mirrored from Retry-After (delta seconds or HTTP date), or null when unknown. It does not promise free capacity after the wait.
- safe_to_retry: repeating this request cannot cause a second generation by it, because the error happened before any provider attempt or the request was a read (GET, HEAD, OPTIONS). After any provider attempt of a write request it is false, even for 429 or a connection error.
- execution_status: not_started (no provider attempt) or unknown. usage_may_have_occurred: true means usage is possible, not that a charge is confirmed.
- Inside an SSE stream the HTTP status is already sent; read these fields from the error event.
Decide what to retry
- 400/413/422: fix the request, size or endpoint. wrong_endpoint can supply error.endpoints. 404 model_not_found: refresh model discovery; do not keep retrying the same ID.
- 401: fix missing_authorization, invalid_api_key or conflicting_authorization. 403: resolve key/account/project/model access or billing state. Do not work around restrictions by changing identity.
- Budget and quota codes daily_limit_reached, monthly_limit_reached, project_member_daily_limit_reached, project_member_monthly_limit_reached, insufficient_credits and devpass_daily_limit_reached require budget, quota reset or user action. Many use 403; do not classify by status alone or assume every limit is a transient 429.
- For transient rate_limit_exceeded, provider_rate_limit_exceeded and devpass_rpm_limit, wait at least retry_after_ms or Retry-After when supplied. Use bounded exponential backoff with jitter when no delay is supplied.
- 502 provider_unavailable, 503 temporary service failures and 504 upstream_timeout may warrant a bounded retry, but only repeat a POST automatically when safe_to_retry is true or under an accepted Idempotency-Key. sandbox_disabled and configuration/access errors need corrective action, not repeated generation calls.
- Retry policy belongs to the client; Aether performs no client retries beyond its internal provider fallback. SDKs may already retry automatically; avoid multiplying application and SDK retry loops.
Repetition and partial output
Aether may try more than one configured provider before returning a result. A timeout, disconnect or failed stream does not prove that no work ran or that no usage accrued; if no response arrives at all, the outcome is unknown. X-Request-ID is for correlation, not deduplication: sending the same ID again does not prevent a second execution. Set a total time/attempt budget and decide explicitly about possible repeated work and cost before retrying a POST.
Keep a separate ID per client attempt and record the returned request ID. For stream errors inspect events even after HTTP 200; discard incomplete tool calls and treat previously received text as partial. Never automatically rerun external tool side effects just because generation was retried.
Optional idempotency for generation
The Idempotency-Key header is honoured only when the operator configures AETHER_IDEMPOTENCY_DIR on the API server; production enablement is not asserted here. Without it, a supplied key is rejected with 400 idempotency_not_supported before any provider call, never silently ignored. Supported: regular API keys and non-streaming JSON POSTs without a query string to /v1/chat/completions, /v1/responses, /v1/messages, /v1/images/generations and /v1/videos. Streaming, background mode, Connect credentials, multipart/audio and other generation endpoints return idempotency_not_supported. The sandbox offers no idempotency guarantee.
- Send the header once: 1–128 characters, first alphanumeric, then ASCII letters, digits and _.:-. Otherwise idempotency_invalid_key. A key is bound to your user, project and API key. Path, JSON body and the anthropic-version, anthropic-beta, x-aether-usage and accept headers form its fingerprint; a different request under the same key returns 409 idempotency_conflict.
- First accepted call: X-Aether-Idempotency: created. A finished JSON result is replayed with the same status and body, without a new provider call or charge, with X-Aether-Idempotency: replayed and X-Aether-Original-Request-ID. Stored errors are replayed too; their Retry-After never starts a new attempt under that key.
- 409 idempotency_in_progress with Retry-After: 2: still running, or unknown after a crash. Poll only with the same key and an identical request; a new key can start a second billed operation. Pending entries are never released by time, so this can persist (fail closed).
- 409 idempotency_result_unavailable: the result cannot be replayed, for example over 1 MiB, not JSON or a dropped connection; it is not executed again. 503 idempotency_store_unavailable with Retry-After: 1: no new dispatch, but an earlier operation under this key cannot be ruled out. Do not fall back to a request without the key.
- When another worker holds the store lock, the server waits a bounded time measured from arrival: up to 2 seconds to accept the request, otherwise 503 idempotency_store_unavailable; up to 10 seconds to record the result, otherwise the entry stays unresolved (idempotency_in_progress, fail closed). Locks are never taken over automatically.
- Finished results are kept for 24 hours after completion; afterwards the same key starts a new operation. Every call rechecks account, key, project, model and budget permissions, so a replay can still be rejected.
The guarantee is at most one local provider dispatch per accepted key, identity and retention window, including internal fallbacks. It is not exactly-once execution at the provider, not a free retry and not a transaction between provider, billing and the store. A video replay restores only the job-creation response.
Look up your own request
GET /v1/requests/<request_id> with the same regular API key returns stored HTTP metadata for up to 100 completed requests of that key with this public request ID: timestamp, endpoint, method, model, status, result (success, error or cancelled), error_code, streaming, duration_ms and known token and credit values, plus truncated and coverage. Unknown, foreign and expired IDs all return found: false with entries: []. Even project owners see only rows of the key used. Retention is at most 30 days.
This is delayed metadata, not a live status, response retrieval or video job status. A stream that failed after HTTP 200 can show result: error. found: false does not prove that a request never ran, so it cannot replace safe_to_retry or an Idempotency-Key.
Integration sandbox and enable prerequisite
Rehearse text, tool-call, structured-output and error flows deterministically without calling a provider. Read Markdown
Enablement and authentication
The server operator must explicitly set AETHER_SANDBOX_ENABLED=true for the running backend. This is a server configuration prerequisite; a client cannot enable it with a request header, body or model name. Public production enablement is not asserted here. When disabled, /v1/sandbox returns 503 sandbox_disabled. Ask the operator for an enabled test environment; do not fall back to a live endpoint.
Use a regular API key. Account, key, project membership and model-block checks still apply. Connect credentials are not allowed on sandbox routes. Sandbox skips the credit-balance requirement and credit reservations; existing key and project-member spending limits and account suspension checks still apply. It never calls a provider, consumes credits or changes usage aggregates. Calls can appear as zero-cost sandbox-marked entries in credit history.
- Sandbox SDK base URL: https://api.aetherapi.dev/v1/sandbox (replace the origin for an operator-provided test environment).
- GET /v1/sandbox/models advertises aether-sandbox. POST /v1/sandbox/chat/completions, /v1/sandbox/responses and /v1/sandbox/messages accept text, client function tools and json_schema output in their own protocol shapes, each with optional stream: true.
- The sandbox path selects simulation. POST accepts any nonempty model name and echoes it, subject to authorization; use aether-sandbox as the canonical fixture. Passing a real model name here does not test that model.
- On live /v1 routes, model: aether-sandbox or a sandbox body property is rejected with sandbox_requires_explicit_path, even when the sandbox is disabled. Always select the explicit sandbox path.
Run one simulated request
The following requires curl, jq and AETHER_API_KEY. It uses AETHER_ORIGIN if an operator supplies a separate test server. It makes no changes to server configuration.
origin="$AETHER_ORIGIN"
if [ -z "$origin" ]; then origin="https://api.aetherapi.dev"; fi
jq -n '{model: "aether-sandbox", messages: [{role: "user", content: "Hello"}],
sandbox: {scenario: "success"}}' \
| curl --fail-with-body --silent --show-error \
"$origin/v1/sandbox/chat/completions" \
-H "Authorization: Bearer $AETHER_API_KEY" \
-H 'Content-Type: application/json' --data-binary @-A successful chat response contains [Aether Sandbox] Simulated response. No provider was called. and zero token usage. Response headers include x-aether-sandbox: true and x-aether-sandbox-scenario. Inspect those headers to confirm the intended mode.
Scenarios and limits
- success (default): fixed text, normal JSON or SSE completion.
- rate_limit: HTTP 429, sandbox_simulated_429, Retry-After: 1.
- timeout: HTTP 504, sandbox_simulated_timeout; this is a simulated error, not a real hanging provider connection.
- provider_unavailable: HTTP 503, sandbox_simulated_provider_unavailable, Retry-After: 1.
- stream_error: requires stream: true; emits partial text or tool-call arguments and sandbox_simulated_stream_error, then ends without a successful completion (no [DONE] on chat). Use it to test incomplete output handling. Every scenario also applies to tool and schema requests.
- sandbox.delay_ms accepts integers 0–5000. The timeout scenario defaults to 250 ms; other scenarios default to 0. Token-limit fields, if supplied, must be integers 1–4096; /messages requires max_tokens.
- Not supported: images, audio, video, thinking, hosted/server or custom tools, parallel tool calls and stored Responses conversations (previous_response_id, conversation, store). Unknown fields return sandbox_invalid_request. Request validation runs before the chosen scenario.
- 64 kb JSON request limit; 60 requests/minute/account, 300/minute/IP; 4 concurrent/account, 16/IP, 128 globally. Real sandbox_rate_limit or sandbox_capacity errors are separate from simulated failures.
Rehearse a tool-call cycle
- With tools, the sandbox returns exactly one function call; without a tool choice the first definition wins. Chat accepts tool_choice auto, none, required or {type: function, function: {name}}; Responses the same strings or {type: function, name}; Messages {type: auto}, {type: none}, {type: any} or {type: tool, name}. tools: [] produces text. parallel_tool_calls may only be false.
- The tool is never executed. Send the next request with the same tool definitions, the complete conversation and one synthetic result directly after its call: Chat a role: tool message with tool_call_id; Responses the returned output items followed by {type: function_call_output, call_id, output}; Messages a user tool_result block with tool_use_id. Missing, duplicate or orphaned results are rejected. The sandbox stores no conversation state.
- After the last result the answer is [Aether Sandbox] Simulated tool result accepted. No provider was called. with finish_reason stop (Messages: stop_reason end_turn), or the generated schema value when an output schema is set.
- Arguments are generated from the tool schema. sandbox.tool_arguments may supply an object for a new call; it is validated against the same schema. The same choice, arguments and history give the same call_sandbox_… ID. Call IDs are not authentication.
This Chat Completions body declares one tool and an output schema. POST it to /v1/sandbox/chat/completions: the answer has finish_reason: tool_calls and one weather call with arguments {"city":"Berlin"}. Append that assistant message and {role: "tool", tool_call_id: <call id>, content: "synthetic result"} to messages, then send the body again: the assistant content is {"simulated":true} with finish_reason: stop.
{
"model": "aether-sandbox",
"messages": [{ "role": "user", "content": "What is the weather in Berlin?" }],
"tools": [{ "type": "function", "function": {
"name": "weather",
"parameters": { "type": "object", "properties": { "city": { "type": "string", "enum": ["Berlin"] } },
"required": ["city"], "additionalProperties": false }
} }],
"response_format": { "type": "json_schema", "json_schema": {
"name": "result", "strict": true,
"schema": { "type": "object", "properties": { "simulated": { "type": "boolean" } },
"required": ["simulated"], "additionalProperties": false }
} }
}Structured output and the supported schema subset
- Output schema: Chat response_format {type: json_schema, json_schema: {name, strict, schema}}; Responses text.format {type: json_schema, name, strict, schema}; Messages output_config.format {type: json_schema, schema}. Names are 1–64 letters, digits, _ or -; strict is optional. Other formats such as json_object are rejected. The JSON value arrives as normal assistant text, immediately without tools or after the tool result.
- Schemas for output and tool arguments: object root; every node has exactly one type of object, array, string, integer, number, boolean or null; objects require additionalProperties: false and allow properties and required; arrays require items and allow minItems/maxItems from 0 to 16; primitives allow enum with 1–32 values; title and description are allowed. Every other keyword, including $ref, anyOf, oneOf, allOf, const, default, format, pattern and minimum, is rejected.
- Generated values are deterministic: every property, the first enum value, otherwise "sandbox", 0, true or null; arrays get max(minItems, min(1, maxItems)) items.
- Per schema at most 8192 bytes, 128 nodes, 8 levels and 32 properties per object; at most 16384 bytes per generated value or submitted arguments; at most 16 tool definitions and 128 conversation items per request; tool names up to 64 characters (Messages: 128).
- Chat and Responses add four 400 codes with param, hint and request ID: sandbox_invalid_tool, sandbox_invalid_tool_arguments, sandbox_invalid_tool_result and sandbox_unsupported_schema. Messages keeps the Anthropic invalid_request_error envelope.
- With stream: true the call is split into protocol events: Chat delta.tool_calls arguments, finish_reason: tool_calls and [DONE]; Responses response.function_call_arguments.delta through response.completed; Messages input_json_delta through message_stop. Parse arguments or JSON only after a successful completion.
A sandbox success verifies client transport, tool-cycle bookkeeping and parsing only. It does not prove a live model's availability, tool support, schema guarantees, context size, pricing or authorization for that model, and sandbox behaviour is not reflected in the production capability catalogue.