Tutorial
How to Call kimi-k3.1-pro via the HiAPI API: curl, Python, and a Working Request
A hands-on integration guide covering endpoints, request parameters, streaming, tool calls, and error triage
On this page
Why call kimi-k3.1-pro through an aggregation API
Every model integration carries the same hidden cost: the first day is fun, the second month is maintenance. When you wire kimi-k3.1-pro directly into your application, you inherit that provider's authentication scheme, error semantics, quota model, and release cadence. That is fine for a single-model prototype. It becomes friction the moment you want a second model, a second environment, or a fallback path when one endpoint misbehaves.
An aggregation API collapses those differences into one contract. HiAPI exposes kimi-k3.1-pro behind an OpenAI-compatible request shape, which means the client code you write here is not a one-off. Swap the model identifier and the same request structure carries over to other models on the platform — same headers, same message array, same streaming envelope.
There are three practical reasons this matters for kimi-k3.1-pro specifically:
- One credential surface. You manage a single API key and a single base URL instead of a growing set of provider accounts.
- Predictable payloads. The request and response schema is stable across models, so your parsing, retry, and logging layers are written once.
- Migration is cheap. If a later workload favors a different model, the change is a string in the payload, not a rewrite of your transport code.
The rest of this guide is the integration itself: the request anatomy, a copy-ready curl call, the equivalent Python client, then the two features that usually cause trouble in production — streaming and tool calls — followed by an error triage checklist.
What you need before starting
- A HiAPI API key, stored wherever your project keeps secrets.
- Your account's API base URL from the HiAPI console. All examples below use
https://api.hiapi.ai/v1as the placeholder base; confirm the exact host in your console before shipping. - A model identifier string for kimi-k3.1-pro — copy it verbatim from the model list in the console, since identifiers are case-sensitive.
curlfor the first test, and Python 3.9+ with therequestslibrary for the client example.
The request shape: endpoint, headers, and model parameters
Every chat call follows the same anatomy. Understanding each part once saves you from debugging field by field later.
Endpoint
Chat completions live at a single path relative to your base URL:
POST {BASE_URL}/chat/completions
That is the only endpoint you need for the examples in this guide. Streaming and tool calls are not separate endpoints — they are options on the same request body.
Headers
Two headers do the real work:
Authorization: Bearer $HIAPI_API_KEY
Content-Type: application/json
The key goes in the Authorization header with the Bearer prefix. Do not put it in the URL, in a query parameter, or in the request body — all three leak it into logs and error reports. Set Content-Type explicitly; some HTTP clients will not infer it for you.
Body fields that matter
A minimal body has four meaningful fields:
model— the kimi-k3.1-pro identifier. Required. This is the one field most likely to be wrong on a first attempt, because it must match exactly.messages— an ordered array of objects, each with arole(system,user, orassistant) andcontent. Required and non-empty. The ordering is your conversation history; the model sees exactly this array and nothing else.stream— boolean, defaultfalse. Whentrue, the response arrives as server-sent events rather than a single JSON body. Covered in its own section below.temperature— optional sampling control. Lower values produce more deterministic output; higher values produce more variety. Pick a value once per use case and leave it alone rather than tuning it per request.
Optional fields you may add as your integration matures:
max_tokens— an upper bound on generated length. Useful as a cost and latency guardrail. Note that this bounds output, not input.tools— an array of tool definitions, described later.tool_choice— controls whether and how the model may invoke the tools you supplied.stop— one or more sequences that terminate generation.
The response you should expect
A non-streaming response returns a JSON object whose choices array contains at least one entry. Inside that entry you will find a message object with a role and content, plus a finish_reason telling you why generation stopped. Always branch on finish_reason rather than assuming the content is complete — a truncated response still has content, it just is not the whole answer.
Write your parser against that structure, not against a stringified response. It will survive every model change you make on the platform.
A working curl request
Start here. If this returns a valid response, your key, base URL, and model identifier are all correct, and any later problem is in the code rather than the credentials.
curl -sS "$HIAPI_BASE_URL/chat/completions" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kimi-k3.1-pro",
"messages": [
{"role": "system", "content": "You are a concise technical assistant."},
{"role": "user", "content": "Explain what an idempotency key is in two sentences."}
],
"temperature": 0.3,
"max_tokens": 256
}'
Notes on each field
-sSkeeps curl quiet about progress but still surfaces errors. Without the secondS, a failed request can exit silently, which wastes a debugging cycle.- Environment variables keep the key off your shell history and out of your terminal scrollback. Export
HIAPI_BASE_URLandHIAPI_API_KEYin your shell profile or a loaded.envfile. - Quoting the body in single quotes is correct for POSIX shells as long as the payload contains no single quote. The moment you need one, move the body into a file and pass
-d @body.jsoninstead — this avoids a class of quoting bugs that look like malformed-JSON errors. - The
systemmessage is optional but worth including. It is the cheapest place to pin down tone and output format. max_tokensis deliberately modest in this example. It caps both latency and spend on a smoke test.
Reading the result
A successful call returns HTTP 200 with a JSON body. Confirm three things before moving on:
choicesis a non-empty array.choices[0].message.contentholds a real answer.choices[0].finish_reasonis a normal completion value, not a length cutoff.
If the body is an error object instead, skip ahead to the troubleshooting section — the error message will usually name the exact field at fault.
A tip for repeatability
Save that curl call as a shell script in your repo. A one-command smoke test against the live API is the fastest way to distinguish "my integration broke" from "my account or key changed." Run it first whenever something looks wrong.
The same request in Python
The Python version should look boring — that is the goal. All the interesting decisions (model identifier, base URL, timeout) belong in configuration, not scattered through call sites.
import os
import requests
BASE_URL = os.environ["HIAPI_BASE_URL"].rstrip("/")
API_KEY = os.environ["HIAPI_API_KEY"]
MODEL = os.environ.get("HIAPI_MODEL", "kimi-k3.1-pro")
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
def chat(messages, temperature=0.3, max_tokens=512, timeout=60):
payload = {
"model": MODEL,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
}
resp = requests.post(
f"{BASE_URL}/chat/completions",
headers=HEADERS,
json=payload,
timeout=timeout,
)
resp.raise_for_status()
data = resp.json()
choice = data["choices"][0]
return choice["message"]["content"], choice.get("finish_reason")
if __name__ == "__main__":
text, reason = chat([
{"role": "system", "content": "You are a concise technical assistant."},
{"role": "user", "content": "Explain what an idempotency key is in two sentences."},
])
print(reason)
print(text)
Why it is structured this way
json=instead ofdata=— therequestslibrary serializes the dict and sets the content type for you. Hand-serializing invites escaping mistakes.timeout=60always — an unset timeout is the single most common cause of a worker process hanging until its supervisor kills it. Every network call in a service should have one.raise_for_status()before parsing — this turns HTTP-level failures into exceptions at the call site rather than letting a malformed body flow downstream.MODELfrom the environment — the model identifier is the only thing that changes when you switch models. Keeping it in config means your switch is a deployment variable, not a code change.- Returning
finish_reason— callers almost always need to know whether the answer was truncated.
Keeping it reusable across models
The signature chat(messages, ...) deliberately exposes nothing model-specific. If you later route a cheaper workload to a different model, you either pass an explicit model override or construct a second client object with a different MODEL. The transport, headers, retry policy, and error handling remain untouched.
A note on retries
Do not retry blindly inside chat(). Retry at the layer that knows whether the operation is safe to repeat — a failed completion is usually safe, but a request that already mutated state downstream is not. Retry only on transient transport failures and on rate-limit responses, and only with capped exponential backoff.
Streaming responses without breaking your client
Streaming is the difference between a chat UI that feels instant and one that sits blank for ten seconds. It also introduces the most integration bugs, because the response is no longer one JSON document.
How to turn it on
Add "stream": true to the request body. Nothing else about the request changes.
with requests.post(
f"{BASE_URL}/chat/completions",
headers=HEADERS,
json={"model": MODEL, "messages": messages, "stream": True},
stream=True,
timeout=120,
) as resp:
resp.raise_for_status()
for line in resp.iter_lines(decode_unicode=True):
if not line or not line.startswith("data:"):
continue
chunk = line[len("data:"):].strip()
if chunk == "[DONE]":
break
# parse chunk as JSON and read choices[0].delta.content
What actually arrives
The body is a sequence of server-sent events. Each event is a data: line carrying a small JSON object. Inside choices[0] there is a delta object rather than a message object — deltas usually contain a fragment of content, and may contain nothing at all of interest. The stream ends with a sentinel line data: [DONE].
Four rules cover almost every mistake:
- Accumulate, do not replace. Each delta is a fragment. Append
delta.contentto a buffer; the full answer is the concatenation of every fragment in order. - Skip empty deltas. The first and last events frequently carry no content. Guard against appending
None. - Treat
[DONE]as the terminator, not as data. Stop reading there; anything after it is not part of the response. - Read
finish_reasonfrom the final event that carries one. In streaming mode, completion status arrives in a late delta rather than a top-level field.
Pitfalls worth naming
- Forgetting
stream=Trueon the client call. You will receive the entire stream as one buffered body, which defeats the purpose and can look like a very slow non-streaming request. - Timeouts sized for non-streaming. A long generation streams for longer than a short request takes. Give the streaming path a more generous read timeout, and consider an idle-timer that aborts if no bytes arrive for a chosen interval.
- Parsing line-by-line without checking the prefix. Not every line is data. Blank lines separate events; ignore them.
- Assuming a delta has a
contentkey. Tool-call streaming puts other keys in the delta. Always read defensively. - Forgetting the client-side consumer. A server that streams into a buffer and only emits at the end is technically streaming and practically not. Make sure something downstream renders as fragments arrive.
When not to stream
Batch jobs, background summarization, and anything whose output is consumed programmatically rather than shown to a human are usually simpler without streaming. Non-streaming responses are a single parse, easier to log, and easier to validate against a schema. Stream where latency is user-visible; do not stream by default.
Adding tool calls to the flow
Tool calls let the model ask your code to perform an action instead of hallucinating an answer. The protocol is a two-turn conversation: you declare tools, the model may respond with a request to invoke one, you execute it, and you send the result back.
Declaring tools
Add a tools array to the request. Each entry wraps a function description:
{
"model": "kimi-k3.1-pro",
"messages": [{"role": "user", "content": "What's the weather in Lisbon?"}],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Return current weather for a named city.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"}
},
"required": ["city"]
}
}
}
],
"tool_choice": "auto"
}
The description on the function and on each parameter is not decoration — it is the model's only guidance for when and how to call the tool. Write it as you would for a new teammate.
The response loop
- Send the request.
- If
choices[0].finish_reasonindicates a tool call, readchoices[0].message.tool_calls. Each entry has anid, a functionname, andarguments— a JSON-encoded string, not an object. Parse it. - Execute the function in your code. Validate the arguments before doing anything with them; the model can produce a plausible-looking payload with a missing field or wrong type.
- Append the assistant's message — including its
tool_calls— to yourmessagesarray. - Append a new message with role
tool, the matchingtool_call_id, and the result ascontent. - Send the request again. The model now composes a final answer using the tool output.
Step 4 is the one people skip, and it is the one that breaks. If you send back only the tool result without the assistant's tool-call turn, the conversation has a dangling reference and the model has no record of what it asked for.
Handling multiple calls
The response may contain more than one tool call in the same turn. Execute each and return one tool message per call, each carrying its own tool_call_id. Do not merge them.
Controlling invocation
tool_choice is your policy lever: auto lets the model decide, requiring a specific tool forces one, and disabling tools tells it to answer directly. Keep a budget — tool loops can run long if a tool keeps failing and the model keeps retrying. Cap iterations in your own loop and return a clear message when the cap is hit, rather than letting the request grind.
Treat tool output as untrusted
The model reads tool results as text. Keep them small, structured, and free of instructions, and validate arguments on your side. A tool that returns unbounded text is a cost problem as well as a correctness risk.
Troubleshooting common errors
Most integration failures fall into five buckets. Work down this list before changing your prompt.
1. Authentication and authorization
- Symptom: an HTTP status in the 401/403 range.
- Check: the key is present, pasted without leading or trailing whitespace, and prefixed with
Bearerexactly. - Check: the base URL matches the one shown in the HiAPI console — a stale or pasted-wrong host produces confusing auth errors.
- Fix: re-export the environment variable, then rerun the curl smoke test before touching your code.
2. Malformed payloads
- Symptom: a request-validation error naming a field, or an unparseable body.
- Check: the body is valid JSON. When it is built by hand, verify with a quick
json.toolpass; when built by code, ensure the dict is not accidentally nested one level too deep. - Check:
messagesis non-empty and every entry has bothroleandcontent. - Check:
rolevalues are from the supported set. A typo likerole: "human"is silently wrong rather than loudly wrong in some clients. - Check:
toolsentries havetype: "function"and a nestedfunctionobject. Flattened definitions are the most common tool-call error.
3. Model identifier
- Symptom: a not-found style error, or a message that the model is unavailable to your account.
- Check: the identifier is copied verbatim from the console. Case and separators matter.
- Check: the model is enabled for your key. A valid identifier for a model your account cannot call will still fail.
4. Context and length limits
- Symptom: an error about the request exceeding the maximum context, or a response that ends with a length-related
finish_reason. - Check: the sum of your system prompt, message history, and any tool definitions. Tool schemas count toward the budget and are easy to forget.
- Fix: trim history, summarize earlier turns, or split the task. For output truncation, raise
max_tokensif the workload genuinely needs more and leave it bounded otherwise.
5. Rate limits and timeouts
- Symptom: a rate-limit status under load, or a client-side read timeout.
- Check: whether retries are being issued without any backoff. A tight retry loop turns a brief throttle into a sustained one.
- Fix: honor the rate-limit signal the response directs you to, use capped exponential backoff with jitter, and keep concurrency per key under control. For timeouts, check the timeout value on the client first — a client-side limit that is shorter than the model's generation time produces exactly this symptom.
A general debugging discipline
Log four things on every failure: the HTTP status, the response body, the model identifier, and whether stream was set. Those four explain the large majority of incidents. And keep the curl smoke test current — when it passes and your application fails, you know the problem is in your code, not your credentials.
Reading the error message
Aggregation layers are usually explicit about which field caused a rejection. Resist the urge to rewrite the prompt. Read the message, fix the named field, and retry once. If the same error persists, verify the request against your smoke test before going deeper.
Before you ship: a production checklist
The gap between "it worked in my terminal" and "it runs in production" is mostly a list of things nobody writes down. Here is the one worth copying.
Configuration and secrets
- The API key lives in your secret manager, never in source control, and never in a URL or log line. Confirm that your HTTP client cannot print the
Authorizationheader at debug level. - The base URL and model identifier are configuration, not constants scattered through the codebase. Confirm the base URL against the console.
- Rotate keys on a schedule and after any team member departs. Your client code should pick up a rotated key without a code change.
Request hygiene
- Every outbound call has an explicit timeout, and the streaming path has a more generous one plus an idle-abort rule.
max_tokensis set on every call path. An unbounded generation is an unbounded bill.- Sampling settings are fixed per use case and documented, so results are reproducible when you need to debug.
Response handling
- Your parser branches on
finish_reasonand treats a truncation as a distinct outcome rather than a complete answer. - Streaming accumulation is tested against a short response as well as a long one, including a response that ends early.
- Tool-call arguments are validated before use, and the assistant's tool-call turn is always echoed back into the message array.
- Tool loops have an iteration cap with a clear failure message.
Failure behavior
- Retries are bounded, use capped exponential backoff with jitter, and apply only to safe-to-repeat operations.
- Rate-limit responses are honored rather than hammered.
- Failures are logged with status, body, model identifier, and stream flag — the four fields that actually resolve incidents.
- The curl smoke test runs in CI or on a schedule, so you learn about credential or model changes from a dashboard rather than a customer.
Cost and capacity
- You are aware of how your usage is metered — the platform bills by tokens processed, so both long prompts and long generations matter. Check the pricing panel in the console for current rates.
- You know which workload is the largest consumer and whether it needs the full model or a lighter one.
- You have a fallback path: if a call fails repeatedly, your app degrades visibly rather than silently hanging.
The last question
Ask whether the integration is observable by someone who did not write it. If a developer on your team can read the smoke test, the error log fields, and the config, and understand how to diagnose a failure, you are done. If not, the checklist is not finished.
With a working curl call, a reusable Python client, and this list, kimi-k3.1-pro is integrated rather than merely connected.
Related articles
View all articles →-
Tutorial
ElevenLabs Music API on HiAPI: Generate Music with curl, Python, and Prompt Recipes
A hands-on tutorial for calling elevenlabs/music-v2.5 through HiAPI, plus copy-paste prompt recipes for style, mood, and duration
12 min read -
Tutorial
How to Use gpt-image-2.5-sunburst/image-to-image via the HiAPI API: curl and Python
A working request, reference-image upload flow, and error triage for developers
10 min read -
Tutorial
How to Use gemini-omni-1.2-flash via the HiAPI API: curl and Python
13 min read