Home / API reference

API reference

Updated 2026-10-16 · OpenAI- and Anthropic-compatible: your SDKs, retry logic and logging middleware keep working.

Connection details

Base URL (OpenAI compatible)
https://api.aifans.fan/v1
Base URL (Anthropic compatible)
https://api.aifans.fan
API key
Create one in the console; it starts with sk- (no account yet? sign up)
Recommended models
deepseek-v4.1-flash, deepseek-v4-pro (all models and prices)

Quick start

  1. Sign up and create an API key in the console (it starts with sk-).
  2. Point your client at https://api.aifans.fan/v1 and authenticate with your key.
  3. Pick a model (for example deepseek-v4.1-flash) and send a request.

curl

curl https://api.aifans.fan/v1/chat/completions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "deepseek-v4.1-flash", "messages": [{"role": "user", "content": "Hello"}], "max_tokens": 1000}'

Python

# pip install openai
from openai import OpenAI

client = OpenAI(api_key="YOUR_API_KEY", base_url="https://api.aifans.fan/v1")
resp = client.chat.completions.create(
    model="deepseek-v4.1-flash",
    messages=[{"role": "user", "content": "Hello"}],
    max_tokens=1000,
)
print(resp.choices[0].message.content)

Node.js

// npm install openai
import OpenAI from "openai";

const client = new OpenAI({ apiKey: process.env.API_KEY, baseURL: "https://api.aifans.fan/v1" });
const r = await client.chat.completions.create({
  model: "deepseek-v4.1-flash",
  messages: [{ role: "user", content: "Hello" }],
  max_tokens: 1000,
});
console.log(r.choices[0].message.content);

Already on the OpenAI SDK? Change the base URL, the API key and the model; see Switch from the OpenAI SDK.

Endpoints and authentication

EndpointPurpose
POST /v1/chat/completionsOpenAI-compatible chat: streaming, thinking, tool calls, JSON output, image input
POST /v1/messagesAnthropic-compatible Messages API (used by Claude Code and other tools)
GET /v1/modelsModels this key may use
GET /v1/accountAccount balance
  • Base URL: OpenAI protocol https://api.aifans.fan/v1; for the Anthropic protocol use https://api.aifans.fan (the SDK adds /v1/messages).
  • Authentication: Authorization: Bearer YOUR_API_KEY; the Anthropic endpoint also accepts x-api-key: YOUR_API_KEY.
  • Request ID: every response carries an x-request-id header. Quote it when you contact support; the console's usage list shows it too.
  • Parameters pass through: apart from mapping model to its model version, the request body goes to the model unchanged; whether a parameter takes effect depends on the model.

Models and capabilities

modelAPIsThinkingPricing
deepseek-v4.1-flashOpenAI / Anthropicon by defaultpeak / off-peak
deepseek-v4-proOpenAI / Anthropicon by defaultpeak / off-peak
hy3OpenAI / Anthropicon by defaultflat
hy4-previewOpenAI / Anthropicon by defaultflat
hy-mt2-proOpenAI—flat
hy-mt2-plusOpenAI—flat
hy-mt2-liteOpenAI—flat
glm-5.3OpenAI / Anthropicon by defaultflat
glm-5.3-flashOpenAI / Anthropicon by defaultflat
minimax-m2.7OpenAI / Anthropicon by defaultflat
kimi-k3OpenAI / Anthropicon by defaultflat

Prices are on Models and prices.

Chat completions POST /v1/chat/completions

ParameterTypeDescription
modelstring, requiredModel name, see the table above
messagesarray, requiredThe conversation. role is system / user / assistant / tool; content is a string, or an array of text and image parts (see Image input)
max_tokensintegerMaximum tokens to generate, thinking included. Use 1000 or more for reasoning models, or the budget may go to thinking alone
streambooleanStream the answer as server-sent events, see Streaming
stream_optionsobject{"include_usage": true}: the last chunk carries token usage
temperature / top_pnumberSampling. Some models ignore them in thinking mode
stopstring / arrayStop generating at these strings
tools / tool_choicearray / stringFunction calling, see Tool calls
response_formatobject{"type": "json_object"} returns JSON, see JSON output
thinkingobject{"type": "disabled"} switches thinking off (DeepSeek V4 family), see Thinking
reasoning_effortstringlow / high / max: how hard the model thinks (DeepSeek V4 family)

Response

{
  "id": "806fcf67-…",
  "object": "chat.completion",
  "created": 1792132071,
  "model": "deepseek-v4.1-flash",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "I'm an AI assistant: I can answer questions, write and code."
      }
    }
  ],
  "usage": {
    "prompt_tokens": 8,
    "completion_tokens": 31,
    "total_tokens": 39,
    "prompt_tokens_details": {
      "cached_tokens": 0
    },
    "completion_tokens_details": {
      "reasoning_tokens": 0
    }
  }
}
  • choices[0].message.content is the answer; reasoning models also return reasoning_content (the thinking).
  • finish_reason: stop finished, length cut off at max_tokens, tool_calls the model wants to call a tool.
  • usage.prompt_tokens is all input (cache hits included), of which prompt_tokens_details.cached_tokens is billed at the cached-input price; completion_tokens is all output (thinking included), of which completion_tokens_details.reasoning_tokens is thinking. Billing uses exactly these numbers, see Billing.

Streaming

With "stream": true the response is server-sent events: one data: {JSON} line per chunk, the answer in choices[0].delta.content (thinking in delta.reasoning_content), ending with data: [DONE].

data: {"id":"8008…","object":"chat.completion.chunk","model":"deepseek-v4.1-flash","choices":[{"index":0,"delta":{"role":"assistant","content":"One"}}]}

data: {"id":"8008…","object":"chat.completion.chunk","model":"deepseek-v4.1-flash","choices":[{"index":0,"delta":{"content":", two, three"}}]}

data: {"id":"8008…","object":"chat.completion.chunk","model":"deepseek-v4.1-flash","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":7,"completion_tokens":32,"total_tokens":39}}

data: [DONE]

Python:

stream = client.chat.completions.create(
    model="deepseek-v4.1-flash", max_tokens=1000, stream=True,
    stream_options={"include_usage": True},
    messages=[{"role": "user", "content": "Write a short poem about spring"}])
for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)
    if chunk.usage:  # the last chunk
        print("\n", chunk.usage)
  • Stream long answers and thinking models: a non-streaming request returns nothing until the model has finished, and times out after 300 seconds without data.
  • A stream cut off by a provider error is not billed; if you disconnect yourself, the part already generated is billed.

Thinking

deepseek-v4.1-flash, deepseek-v4-pro, hy3, hy4-preview, glm-5.3, glm-5.3-flash, minimax-m2.7, kimi-k3 are reasoning models: they think first, then answer.

  • The thinking is in message.reasoning_content (streaming: delta.reasoning_content), the answer in content.
  • Thinking is billed as output tokens and counts toward max_tokens; see usage.completion_tokens_details.reasoning_tokens. Asking “which is larger, 9.11 or 9.8?” took about 400 thinking tokens and 80 answer tokens.
  • For the DeepSeek V4 family, "thinking": {"type": "disabled"} switches thinking off and "reasoning_effort": "low" reduces it. Translation, classification and extraction get faster and cheaper.
r = client.chat.completions.create(
    model="deepseek-v4.1-flash", max_tokens=1000,
    messages=[{"role": "user", "content": "Translate into French: the weather is nice today"}],
    extra_body={"thinking": {"type": "disabled"}},   # Python SDK: non-standard fields go in extra_body
)

Tool calls (function calling)

Describe functions in tools with JSON Schema. When the model needs one it returns tool_calls (with finish_reason tool_calls); run the function and send the result back as a role: "tool" message, and the model answers.

import json

tools = [{"type": "function", "function": {
    "name": "get_weather", "description": "Today's weather in a city",
    "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}}]
messages = [{"role": "user", "content": "What's the weather in Singapore today?"}]

r = client.chat.completions.create(model="deepseek-v4.1-flash", messages=messages, tools=tools, max_tokens=1000)
msg = r.choices[0].message
if msg.tool_calls:                       # finish_reason == "tool_calls"
    messages.append(msg)
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)   # {"city": "Singapore"}
        result = {"city": args["city"], "weather": "sunny", "temp_c": 31}   # call your own function here
        messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
    r = client.chat.completions.create(model="deepseek-v4.1-flash", messages=messages, tools=tools, max_tokens=1000)
print(r.choices[0].message.content)

arguments is a JSON string produced by the model: validate it before use.

JSON output

Set "response_format": {"type": "json_object"} and the model returns one JSON object. Also say in the prompt that you want JSON and which fields.

r = client.chat.completions.create(
    model="deepseek-v4.1-flash", max_tokens=500,
    response_format={"type": "json_object"},
    messages=[{"role": "system", "content": "Reply with JSON only"},
              {"role": "user", "content": "Give {\"city\": city, \"country\": country} for Tokyo"}])
print(r.choices[0].message.content)   # {"city": "Tokyo", "country": "Japan"}

Image input

deepseek-v4.1-flash understands images: make content an array mixing text and image_url parts. An image can be a public URL or a base64 data URL, and is billed as input tokens.

import base64

with open("photo.png", "rb") as f:
    data_url = "data:image/png;base64," + base64.b64encode(f.read()).decode()

r = client.chat.completions.create(
    model="deepseek-v4.1-flash", max_tokens=500,
    messages=[{"role": "user", "content": [
        {"type": "text", "text": "What is in this picture?"},
        {"type": "image_url", "image_url": {"url": data_url}},   # or a public https URL
    ]}])

Context caching

Caching is automatic, with no parameter: when the start of a request (system prompt, tool definitions, earlier turns) matches a recent request, that part is billed at the lower cached-input price. The count is in usage.prompt_tokens_details.cached_tokens (Anthropic API: cache_read_input_tokens).

  • Put what does not change (system prompt, tool definitions, long documents) first and append new content at the end.
  • Multi-turn chats and coding agents hit the cache naturally: Claude Code resends the whole context on every step, and usually more than 95% of its input is billed at the cached price.
  • An unused cache expires after a while; the next request is then billed at the normal input price.

Anthropic API POST /v1/messages

Compatible with Anthropic's Messages API: the official Anthropic SDKs, Claude Code and similar tools work directly. Models on this endpoint: deepseek-v4.1-flash, deepseek-v4-pro, hy3, hy4-preview, glm-5.3, glm-5.3-flash, minimax-m2.7, kimi-k3.

curl https://api.aifans.fan/v1/messages \
  -H "x-api-key: YOUR_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{"model": "deepseek-v4.1-flash", "max_tokens": 1000, "messages": [{"role": "user", "content": "Hello"}]}'
ParameterTypeDescription
modelstring, requiredModel name
max_tokensinteger, requiredMaximum tokens to generate (thinking included)
messagesarray, requiredAlternating user / assistant messages
systemstring / arraySystem prompt
streambooleanStreaming; events are message_start, content_block_start / delta / stop, message_delta, message_stop and ping
tools / tool_choicearray / objectTool use, Anthropic format
temperature / top_p / stop_sequencesSampling and stop parameters

The anthropic-version and anthropic-beta headers are passed to the model unchanged.

Response

{
  "id": "3c8e42d7…",
  "type": "message",
  "role": "assistant",
  "model": "deepseek-v4.1-flash",
  "content": [
    {
      "type": "thinking",
      "thinking": "The user wants a one-sentence introduction…"
    },
    {
      "type": "text",
      "text": "I'm an AI assistant that answers questions and helps with all kinds of tasks."
    }
  ],
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 38,
    "output_tokens": 63
  }
}

Reasoning models put a thinking block before the text block: filter by type to get the answer.

Python (Anthropic SDK)

# pip install anthropic
import anthropic

client = anthropic.Anthropic(base_url="https://api.aifans.fan", api_key="YOUR_API_KEY")
msg = client.messages.create(
    model="deepseek-v4-pro", max_tokens=1000,
    system="You are a concise assistant",
    messages=[{"role": "user", "content": "Introduce yourself in one sentence"}])
print(msg.content[-1].text)

Claude Code setup: Use DeepSeek, GLM and Kimi in Claude Code.

Model list and balance

GET /v1/models

{
  "object": "list",
  "data": [
    {
      "id": "deepseek-v4.1-flash",
      "object": "model",
      "owned_by": "airouter"
    },
    {
      "id": "deepseek-v4-pro",
      "object": "model",
      "owned_by": "airouter"
    }
  ]
}

Lists only the models this key may use (you can restrict a key's models in the console).

GET /v1/account

{
  "balance_usd": 12.345678
}

Your balance in USD, vouchers not included; vouchers are on the console's Account page.

Errors

OpenAI-style error:

{
  "error": {
    "message": "Invalid or missing API key",
    "type": "invalid_api_key",
    "code": "invalid_api_key"
  }
}

Anthropic-style error:

{
  "type": "error",
  "error": {
    "type": "invalid_api_key",
    "message": "Invalid or missing API key"
  }
}
HTTPcodeMeaning
400invalid_json / invalid_requestThe body is not valid JSON, or not a JSON object
400unsupported_protocolThis model does not support this API (e.g. an OpenAI-only model called on /v1/messages)
400content_blockedThe request was refused by our content checks; not billed
401invalid_api_keyThe API key is missing or wrong
402insufficient_balanceYour balance is used up: please top up
403key_disabledThis key or account is disabled
403ip_not_allowedThe source IP is not on this key's allow-list
403key_quota_exceededThis key reached the daily / monthly / total spending limit you set
404model_not_foundUnknown model, or this key may not use it
429rate_limitedOver this key's requests-per-minute limit, or its tokens-per-minute limit on one model; Retry-After says how many seconds to wait
429upstream_unavailableThe model is busy (its per-minute capacity is used up); Retry-After says how many seconds to wait. Not billed
502upstream_unavailableThe model service is temporarily unavailable; retry later. Not billed
  • 4xx errors raised by the model (invalid parameters and the like) come back as invalid_request with the reason, and are not billed; a body that is too large returns 413 request_too_large.
  • Only successful calls are billed.

Rate limits and retries

  • Each key may make 60 requests per minute by default; above it you get 429 rate_limited with a Retry-After header (seconds). Ask support if you need more.
  • A busy model also returns 429 with Retry-After. Wait that long and retry; these failures are not billed. The OpenAI and Anthropic SDKs and Claude Code honour Retry-After by themselves.
  • Long contexts sent at a high rate (coding agents) are the most likely to hit a busy model: keeping the context short (/compact in Claude Code) and limiting concurrency both reduce 429s noticeably.
  • Retry 429 and 502 with exponential backoff, using Retry-After when present.

Python retry example

import time
from openai import OpenAI, APIStatusError

client = OpenAI(api_key="YOUR_API_KEY", base_url="https://api.aifans.fan/v1", max_retries=0)

def ask(prompt, retries=4):
    for attempt in range(retries + 1):
        try:
            r = client.chat.completions.create(
                model="deepseek-v4.1-flash", max_tokens=1000,
                messages=[{"role": "user", "content": prompt}])
            return r.choices[0].message.content
        except APIStatusError as e:
            if e.status_code in (429, 502) and attempt < retries:
                wait = e.response.headers.get("retry-after")
                time.sleep(float(wait) if wait else 2 ** attempt)   # Retry-After first, else 1s, 2s, 4s ...
                continue
            raise

Billing

  • Prepaid: each call is charged to your balance when it completes, by actual token usage. When the balance runs out you get 402, never a debt.
  • Input (cache miss), cache hits and output are priced separately; see Models and prices. Peak is 01:00–04:00 and 06:00–10:00 UTC on weekdays; all other times are off-peak, and so are weekends; models marked peak / off-peak are priced by when a request arrives.
  • Token counts come straight from the model's usage and are billed to a millionth of a dollar, never rounded up.
  • Failed calls (throttled, provider errors, streams cut off by the provider) are free; if you disconnect yourself, the part already generated is billed.
  • The console shows totals by model and by key, and exports every call and top-up as CSV. Online top-ups carry a payment processing fee of 5.5% (minimum $0.8), added at checkout and shown before you pay; it is not part of your credit.

Key security and limits

Set per key in the console; use one key per project, environment or tool.

  • Spending limits: daily, monthly and total (USD). Days and months are UTC. A key over its limit returns 403 key_quota_exceeded; other keys keep working.
  • IP allow-list: the server IPs allowed to call (single addresses or CIDR ranges, IPv4 / IPv6). Others get 403 ip_not_allowed. Empty means no restriction.
  • Allowed models: limit a key to certain models.
  • Limits are checked when a request starts and charged when it ends, so concurrent requests can take spending slightly past a limit.
  • Never put a key in a web page, a mobile app or a public repository; if one may have leaked, disable it in the console, effective immediately.

Data and privacy

  • We do not store your prompts or the model's answers. We keep the time, model, token counts, cost, latency, status and which key was used, for billing and troubleshooting.
  • We do not sell data and do not train on it. To answer a request, it passes through our servers in Singapore to the provider that runs the model; see the Privacy Policy.

Use it in popular tools and frameworks

Popular AI tools connect in a few minutes, each with step-by-step instructions:

Coding tools

Models

Chat apps

Apps and workflows

Productivity

Frameworks and SDKs

Choosing a provider

Any other tool that accepts a custom OpenAI-compatible provider works with three settings: base URL, API key and model name.