Notice: GPT Image 2.6 Flare and Sunburst are newly launched with a limited-time 20% off, try it now
HiAPI
Blog

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

Nov 12, 2026 18 min read
封面图:深墨绿底色上,两组扁平缎带呈经纬交错编织成规整的开放式网格,缎带为克制的黄铜色与暗绿色。
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/v1 as 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.
  • curl for the first test, and Python 3.9+ with the requests library 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 a role (system, user, or assistant) and content. Required and non-empty. The ordering is your conversation history; the model sees exactly this array and nothing else.
  • stream — boolean, default false. When true, 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

  • -sS keeps curl quiet about progress but still surfaces errors. Without the second S, 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_URL and HIAPI_API_KEY in your shell profile or a loaded .env file.
  • 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.json instead — this avoids a class of quoting bugs that look like malformed-JSON errors.
  • The system message is optional but worth including. It is the cheapest place to pin down tone and output format.
  • max_tokens is 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:

  1. choices is a non-empty array.
  2. choices[0].message.content holds a real answer.
  3. choices[0].finish_reason is 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 of data= — the requests library serializes the dict and sets the content type for you. Hand-serializing invites escaping mistakes.
  • timeout=60 always — 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.
  • MODEL from 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:

  1. Accumulate, do not replace. Each delta is a fragment. Append delta.content to a buffer; the full answer is the concatenation of every fragment in order.
  2. Skip empty deltas. The first and last events frequently carry no content. Guard against appending None.
  3. Treat [DONE] as the terminator, not as data. Stop reading there; anything after it is not part of the response.
  4. Read finish_reason from 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=True on 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 content key. 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

  1. Send the request.
  2. If choices[0].finish_reason indicates a tool call, read choices[0].message.tool_calls. Each entry has an id, a function name, and arguments — a JSON-encoded string, not an object. Parse it.
  3. 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.
  4. Append the assistant's message — including its tool_calls — to your messages array.
  5. Append a new message with role tool, the matching tool_call_id, and the result as content.
  6. 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 Bearer exactly.
  • 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.tool pass; when built by code, ensure the dict is not accidentally nested one level too deep.
  • Check: messages is non-empty and every entry has both role and content.
  • Check: role values are from the supported set. A typo like role: "human" is silently wrong rather than loudly wrong in some clients.
  • Check: tools entries have type: "function" and a nested function object. 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_tokens if 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 Authorization header 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_tokens is 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_reason and 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 →

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