HiAPI
Blog

Tutorial

How to Use gpt-6.1-terra via the HiAPI API: curl, Python, and a Working Request

Oct 31, 2026 12 min read
文章封面图:淡紫色与墨黑色两大平面色块以一条细紫色硬边相接,构图平静而对称。

What a gpt-6.1-terra request actually needs

Every call to gpt-6.1-terra on HiAPI comes down to four things: a URL, an auth header, the model identifier, and the payload. Get those right and the rest of this article is just plumbing.

Endpoint

All model calls go through a single base URL on HiAPI. You do not need to juggle separate hosts per model — the model name in the body is what routes the request. Point your client at the HiAPI base URL and keep it in a variable so you only change it in one place.

Auth header

Authentication is a bearer token passed in the Authorization header. The token is an API key issued in your HiAPI account. Never hardcode it in source files; read it from an environment variable such as HIAPI_API_KEY so the same code works locally, in CI, and in production.

Model identifier

The body must name the model explicitly: "model": "gpt-6.1-terra". This string is case-sensitive and must match exactly. A wrong or truncated model name is one of the most common causes of a rejected request, so treat it as a constant in your code rather than a free-form string.

Payload fields

At minimum, a request carries the model and the input content — the prompt or messages you want the model to act on. Around that core you can add whatever optional parameters the model supports (output format hints, length controls, and so on), but start with the smallest payload that produces a valid response. A minimal body is far easier to debug than a fully decorated one, and it isolates whether a failure is about your payload or your plumbing.

One structural point worth internalizing early: some responses are returned inline, and some are returned as a job you must poll. We cover how to tell them apart later, but design your request layer now so the response handling can switch between the two without a rewrite.

A working curl request, line by line

Here is a complete call you can paste into a terminal once your key is exported. It uses the HiAPI base URL and a minimal JSON body.

curl -sS -X POST "$HIAPI_BASE_URL/v1/chat/completions" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-6.1-terra",
    "messages": [
      {"role": "user", "content": "Summarize the water cycle in three sentences."}
    ]
  }'

What each flag does

-X POST sets the HTTP method. Model calls are POSTs because they carry a body.

-sS keeps curl quiet about progress but still shows real errors. Without -S, a failed request can look like silence.

-H "Authorization: Bearer $HIAPI_API_KEY" attaches your credential. The Bearer prefix and the single space after it are part of the scheme; omitting either produces an auth failure that looks unrelated to headers.

-H "Content-Type: application/json" tells the server the body is JSON. Some gateways tolerate a missing content type, but do not rely on it.

-d '...' is the body. Single quotes keep the shell from interpolating the JSON. If you want to work with a file instead, replace -d '...' with -d @payload.json and keep the JSON in a file you can lint and diff.

Two habits that save time

First, when something fails, add -i to print the response status line and headers. A 200 with an unexpected body reads very differently from a 401 or 422, and you want to see that distinction immediately. Second, pipe the output through a JSON formatter (curl ... | python -m json.tool) so nested response fields are readable. Both flags are harmless, and you will use them constantly while adapting the call.

Porting the call to Python

The Python version should preserve the exact semantics of the curl call: same URL, same headers, same body, same parse. Using the standard library keeps dependencies at zero and makes the translation obvious.

import json
import os
import urllib.request

HIAPI_BASE_URL = os.environ["HIAPI_BASE_URL"].rstrip("/")
HIAPI_API_KEY = os.environ["HIAPI_API_KEY"]

def call_terra(prompt: str, model: str = "gpt-6.1-terra") -> dict:
    body = {
        "model": model,
        "messages": [{"role": "user", "content": prompt}],
    }
    req = urllib.request.Request(
        f"{HIAPI_BASE_URL}/v1/chat/completions",
        data=json.dumps(body).encode("utf-8"),
        headers={
            "Authorization": f"Bearer {HIAPI_API_KEY}",
            "Content-Type": "application/json",
        },
        method="POST",
    )
    with urllib.request.urlopen(req, timeout=120) as resp:
        return json.loads(resp.read().decode("utf-8"))

result = call_terra("Summarize the water cycle in three sentences.")
print(json.dumps(result, indent=2))

Notes on the translation

Headers are built once inside the request object, mirroring the two -H flags. The body is a Python dict serialized with json.dumps, which is the programmatic equivalent of the single-quoted curl body — and it removes any quoting hazards on Windows shells.

The function takes the model as a defaulted parameter rather than duplicating the literal everywhere. That keeps a single source of truth for the model name while letting you override it deliberately.

Error handling is deliberately absent here so the happy path stays readable. In real code, wrap the urlopen call and catch the HTTP error exception, reading .read() from the error object to see the server's message. Timeouts are set explicitly; block forever and you will eventually hang a worker.

If you prefer httpx or requests, the mapping is one-to-one: requests.post(url, headers=..., json=body, timeout=120). The json= argument serializes the dict for you. Nothing about the semantics changes, which is the point — the curl call is the contract, and Python is just a second rendering of it.

Deciding between a single call and an async job

Not every request is best served by waiting. The decision hinges on how long the work takes and whether your caller can afford to block.

When a single call is right

Short, interactive requests — a summary, a classification, a small rewrite — should be a plain synchronous call. The response comes back inline, you parse it, you return it. This is the simplest code path and the easiest to reason about in tests. If a user is waiting on a UI, synchronous is the expected shape.

When to treat it as a job

Treat the call as async when the work is long-running or produces a substantial artifact, and when a dropped connection would mean redoing expensive work. In those cases you submit the request, receive an identifier for the job, and poll until it reaches a terminal state. The submission itself returns quickly, so your worker or HTTP handler is not pinned for the duration.

A practical rule: if you find yourself reaching for an unusually long client timeout to make a call "work," that is a signal to switch to a job. Timeouts are not a scheduling mechanism.

Keep the switch cheap

Do not fork your codebase into two unrelated clients. Wrap both paths behind one function that returns either an immediate result or a job handle, and let the caller ask for the mode it wants. That way the request construction — model, auth, payload — stays shared, and only the awaiting strategy differs. It also means debugging is consistent: you can reproduce an async failure synchronously when the payload is small.

Be honest about your own workload rather than guessing at platform behavior. If your request routinely finishes in a couple of seconds, keep it synchronous and skip the complexity. The async path earns its keep on the requests that are genuinely slow, not on everything.

Polling for completion without hammering the API

A polling loop has three jobs: wait long enough to be polite, stop eventually, and surface the terminal state clearly. Here is a compact version.

import time

def poll_until_done(fetch_status, job_id, interval=3.0, timeout=600.0):
    deadline = time.monotonic() + timeout
    while True:
        status = fetch_status(job_id)
        state = status.get("status")
        if state in ("succeeded", "failed", "cancelled"):
            return status
        if time.monotonic() > deadline:
            raise TimeoutError(f"job {job_id} not finished after {timeout}s")
        time.sleep(interval)

Why this shape

fetch_status is injected, so the same loop works whether you poll over HTTP or against an internal queue. That also makes it trivial to test with a fake that returns running twice and then succeeded.

The state check is an explicit allowlist of terminal states rather than a check for a single "done" string. Terminal states differ — success, failure, cancellation — and you want to stop on any of them rather than spin until the timeout because you only looked for one.

time.monotonic() is used for the deadline so wall-clock adjustments cannot extend or shorten the wait unexpectedly.

Choosing an interval

Start with a few seconds and resist the urge to poll in a tight loop. A sub-second interval burns request budget and adds load without making the job finish sooner. If you expect long jobs, consider a mild backoff — for example, increase the interval after each check up to a ceiling. Keep the ceiling modest; the goal is fewer requests, not a multi-minute blind spot.

Timeouts are not optional

Always set one, and make it generous relative to the work but finite. When it fires, do not silently return an empty result. Raise, log the job identifier, and let the caller decide whether to retry — a job that timed out in polling may still be running, so a blind resubmit can duplicate work.

Downloading and storing outputs

If a response includes artifact references — URLs, file identifiers, or storage keys — treat them as transient. They may expire, they may be scoped to your account, and they may not survive a retry. The durable copy is the one you save yourself.

The fetch-and-persist pattern

When a job completes, read the artifact reference from the terminal status, fetch it once, and write it to a destination you control: a local path for scripts, or your object storage bucket for anything resembling production. Record the job identifier alongside the stored file so you can trace an artifact back to the request that produced it.

def save_output(fetch_bytes, artifact_url, dest_path):
    data = fetch_bytes(artifact_url)
    with open(dest_path, "wb") as f:
        f.write(data)
    return dest_path

Practices that prevent quiet data loss

Write to a temporary path and rename on success, so a failed download never leaves a half-written file that looks valid. Verify the byte count or checksum if the response exposes one. For anything large, stream to disk instead of loading the entire payload into memory.

Keep the raw response of the final status call as well. JSON is cheap to store, and when someone asks why a file looks wrong, the status document with its model name, timestamps, and job id is the fastest way to reconstruct what happened.

Finally, separate retrieval from processing. Download, store, then transform. If you parse while downloading, a transient network error looks like a formatting bug, and you will spend an afternoon on the wrong problem.

Debugging the usual failures

Most failures fall into four buckets. Each has a quick isolation test.

Auth errors

Symptom: a 401 or 403 before anything about your payload matters. Check that the key is actually loaded — print whether the environment variable is set, not its value. Verify the Bearer prefix and the space after it. Confirm the header name is spelled correctly and that no proxy is stripping it. If the same key works in curl but not in your app, your app is constructing headers differently; compare the literal header dict against the curl flags.

Malformed payload

Symptom: a 400 or 422 with a message about the body. Validate your JSON before sending — json.dumps will serialize almost anything, including a dict you did not mean to build. Check for trailing commas pasted from a config file, content passed as a list where a string is expected, and missing required fields. Reduce to the minimal body from the first section and add fields back until it breaks.

Wrong model name

Symptom: a rejection that looks like a bad request or a model-not-found message. The model string must be exactly gpt-6.1-terra. Common causes: a stale default in a helper function, an environment variable holding a different value, or a typo from retyping rather than copy-pasting. Keep it as a module constant and grep for duplicates.

Poll timeouts

Symptom: your loop exits with a timeout while the job is still alive. First, confirm the job identifier you are polling is the one you submitted — printing both at submission is the easiest check. Then look at your terminal-state list: a state you did not anticipate will cause the loop to run forever. Finally, check your interval and deadline against the actual job duration; a deadline tuned for a short request will always fire on a long one. Log the last status payload on timeout so you know what state the job was in when you gave up.

Work through these in order — auth, payload, model, polling — because each layer masks the ones beneath it. A broken header makes every payload look wrong.

Related articles

View all articles →

Start building with persistent AI artifacts

Get your API key and call image, video and audio models through one task-based API.

Read the API docs