Research preview. Apply for access. We review each application and email you when it's approved, with your API key ready on the API keys page.

Cold-boot waiting

These snippets keep one request open until the model answers, even when its startup takes many minutes.

A request waits for a startup when the model has scaled to zero. It can also wait on a kept-warm model during a restart or a deployment. Models lists which models scale to zero.

During a startup, the model waits for GPU capacity and then loads, and neither step has a fixed length. While the request waits, the API sends keepalive bytes so that neither the public endpoint nor your client closes the connection as idle.

curl

curl --fail-with-body --no-buffer --max-time 900 \
  "$ACS_API_BASE/completions" \
  -H "Authorization: Bearer $ACS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "llama-405b", "prompt": "Hello", "max_tokens": 4}'

--max-time 900 limits the whole transfer to 15 minutes, which covers the API's 14-minute limit on a non-streaming request. On a stream, the 14-minute limit applies only to the wait for the model's first byte. The public endpoint ends every HTTP request, a stream included, after 15 minutes.

If your key already runs 16 requests, a new request waits for a free slot before its 14 minutes start. --max-time counts that wait too, so it can expire first.

--fail-with-body catches only a failure that arrives before the HTTP 200 response, so also check the body for an error key.

Python

import os
from openai import APIError, OpenAI

client = OpenAI(
    api_key=os.environ["ACS_API_KEY"],
    base_url=os.environ.get("ACS_API_BASE", "https://infra.acsresearch.org/v1"),
    timeout=900,     # maximum idle read gap, not an elapsed-time deadline
    max_retries=0,   # keep one request, do not duplicate a long startup
)

try:
    with client.completions.create(
        model="llama-405b", prompt="Hello", max_tokens=4, stream=True,
    ) as stream:
        for chunk in stream:
            for choice in chunk.choices:
                print(choice.text, end="", flush=True)
except APIError as exc:
    # A structured SSE error also raises after HTTP 200 / heartbeat comments.
    # Any output already printed belongs to an incomplete request.
    print(f"Request failed: {exc}")
finally:
    client.close()

Because this snippet streams, each kind of failure reaches your code in a different way:

  • An error that the API reports inside the stream raises APIError.
  • With openai-python 3.14.0 or later, a dropped connection raises APIConnectionError, and a read gap longer than 900 seconds raises APITimeoutError. Both are subclasses of APIError, so this snippet catches them.
  • Earlier versions raise an exception from the SDK's HTTP library in those two cases, and this snippet does not catch it.
  • If the stream closes early without an error, the loop ends with no exception.

Treat the text as complete only after a chunk reports a finish_reason.

Errors inside a 200 response

Without streaming

If a non-streaming request runs longer than about 150 seconds, the API sends HTTP 200 and then whitespace until the result arrives. A warm model gets the same early HTTP 200 as a cold one. JSON allows whitespace before a value, so openai-python and other JSON parsers skip it.

The HTTP status cannot change once the API has sent it, so any later failure, including the 14-minute deadline, arrives in the body as an {"error": {...}} object. For an activation capture or a prompt_logprobs: -1 request, a failure after the first byte cuts the body short instead.

When the body contains the error object, openai-python does not raise an exception. It returns a Completion with choices set to None and the error in an extra error field. Check for the error before you read choices:

from openai import OpenAI


def completion_or_raise(resp):
    """Surface an error that arrived inside an HTTP 200 body."""
    err = getattr(resp, "error", None)  # extra field, None on a normal completion
    if err:
        raise RuntimeError(f"{err.get('code')}: {err.get('message')}")
    return resp


client = OpenAI(api_key="...", base_url="https://infra.acsresearch.org/v1",
                timeout=900, max_retries=0)
resp = completion_or_raise(
    client.completions.create(model="llama-405b", prompt="Hello", max_tokens=4)
)
print(resp.choices[0].text)

With client.completions.with_raw_response.create(...), .http_response.json().get("error") returns the same error object.

A failure before the HTTP 200 response gets its real status code and the headers in this table:

Failure Status Retry-After X-Acs-Upstream-Error-Kind
Upstream 502, 503, or 504 after retries, vllm_oom, vllm_engine_dead, or upstream_unreachable 502 Yes Yes
Any other upstream 5xx after retries 502 No Yes
wrapper_saturated or circuit_open 503 Yes Yes
modal_cold_boot 503 No No
upstream_deadline 504 No Yes
upstream_auth 502 No Yes

An upstream error that comes from a bad request returns 400 without retries.

To watch a non-streaming startup, poll the model's status from a second terminal and read warm and boot.stage_label.

curl -s "$ACS_API_BASE/models/llama-405b/status" \
  -H "Authorization: Bearer $ACS_API_KEY"

With streaming

With stream: true, the API returns HTTP 200 before it contacts the model, so every failure after that arrives as a data: {"error": ...} frame.

While the stream waits, the API sends a : keepalive comment every 5 seconds. If the API judged the model cold, a startup progress comment replaces each keepalive until the first token arrives. The openai-python SDK ignores both kinds of comment.

The 14-minute deadline

The deadline ends the request with one of two codes. A non-streaming request nearly always gets the code inside the HTTP 200 response body, because the HTTP 200 arrives after about 150 seconds.

The code depends on whether the API judged the model cold when the request started. A model counts as cold when no completion on it finished within its idle time. The rule also covers kept-warm models, so an idle kept-warm model counts as cold while its container runs. A kept-warm model can still count as warm during a restart.

Code When retryable What to do
modal_cold_boot The API judged the model cold. false The model can still be starting. Poll the model's status, and send the request again when warm is true.
upstream_deadline The API judged the model warm. false Lower max_tokens. The same request would time out again. Completion jobs run inside the same window, so they do not help.

Do not resend after a dropped connection

A broken connection does not prove that inference stopped, so resending can run the same work twice. Do not repeat a synchronous completion automatically after a timeout, a truncated response, or a stream that ended without its final frame.

For a plain text completion, submit a completion job once with a stable Idempotency-Key, and poll the job after you reconnect. Completion jobs reject streaming, logprobs, prompt_logprobs, and activation requests.

Next steps