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
- Sign up and create an API key in the console (it starts with sk-).
- Point your client at https://api.aifans.fan/v1 and authenticate with your key.
- 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
| Endpoint | Purpose |
|---|---|
| POST /v1/chat/completions | OpenAI-compatible chat: streaming, thinking, tool calls, JSON output, image input |
| POST /v1/messages | Anthropic-compatible Messages API (used by Claude Code and other tools) |
| GET /v1/models | Models this key may use |
| GET /v1/account | Account 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
| model | APIs | Thinking | Pricing |
|---|---|---|---|
| deepseek-v4.1-flash | OpenAI / Anthropic | on by default | peak / off-peak |
| deepseek-v4-pro | OpenAI / Anthropic | on by default | peak / off-peak |
| hy3 | OpenAI / Anthropic | on by default | flat |
| hy4-preview | OpenAI / Anthropic | on by default | flat |
| hy-mt2-pro | OpenAI | — | flat |
| hy-mt2-plus | OpenAI | — | flat |
| hy-mt2-lite | OpenAI | — | flat |
| glm-5.3 | OpenAI / Anthropic | on by default | flat |
| glm-5.3-flash | OpenAI / Anthropic | on by default | flat |
| minimax-m2.7 | OpenAI / Anthropic | on by default | flat |
| kimi-k3 | OpenAI / Anthropic | on by default | flat |
Prices are on Models and prices.
Chat completions POST /v1/chat/completions
| Parameter | Type | Description |
|---|---|---|
| model | string, required | Model name, see the table above |
| messages | array, required | The conversation. role is system / user / assistant / tool; content is a string, or an array of text and image parts (see Image input) |
| max_tokens | integer | Maximum tokens to generate, thinking included. Use 1000 or more for reasoning models, or the budget may go to thinking alone |
| stream | boolean | Stream the answer as server-sent events, see Streaming |
| stream_options | object | {"include_usage": true}: the last chunk carries token usage |
| temperature / top_p | number | Sampling. Some models ignore them in thinking mode |
| stop | string / array | Stop generating at these strings |
| tools / tool_choice | array / string | Function calling, see Tool calls |
| response_format | object | {"type": "json_object"} returns JSON, see JSON output |
| thinking | object | {"type": "disabled"} switches thinking off (DeepSeek V4 family), see Thinking |
| reasoning_effort | string | low / 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"}]}'
| Parameter | Type | Description |
|---|---|---|
| model | string, required | Model name |
| max_tokens | integer, required | Maximum tokens to generate (thinking included) |
| messages | array, required | Alternating user / assistant messages |
| system | string / array | System prompt |
| stream | boolean | Streaming; events are message_start, content_block_start / delta / stop, message_delta, message_stop and ping |
| tools / tool_choice | array / object | Tool use, Anthropic format |
| temperature / top_p / stop_sequences | Sampling 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"
}
}
| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_json / invalid_request | The body is not valid JSON, or not a JSON object |
| 400 | unsupported_protocol | This model does not support this API (e.g. an OpenAI-only model called on /v1/messages) |
| 400 | content_blocked | The request was refused by our content checks; not billed |
| 401 | invalid_api_key | The API key is missing or wrong |
| 402 | insufficient_balance | Your balance is used up: please top up |
| 403 | key_disabled | This key or account is disabled |
| 403 | ip_not_allowed | The source IP is not on this key's allow-list |
| 403 | key_quota_exceeded | This key reached the daily / monthly / total spending limit you set |
| 404 | model_not_found | Unknown model, or this key may not use it |
| 429 | rate_limited | Over this key's requests-per-minute limit, or its tokens-per-minute limit on one model; Retry-After says how many seconds to wait |
| 429 | upstream_unavailable | The model is busy (its per-minute capacity is used up); Retry-After says how many seconds to wait. Not billed |
| 502 | upstream_unavailable | The 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
raiseBilling
- 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
Use DeepSeek, GLM and Kimi in Claude Code
Use DeepSeek in Cline, Roo Code and Kilo Code
Set up Cursor and other OpenAI-compatible tools
Use DeepSeek in Continue (VS Code and JetBrains)
Use DeepSeek in OpenCode and Aider
What Claude Code costs per month on DeepSeek, GLM or Kimi
Use DeepSeek, GLM and Kimi in Zed
Models
Kimi K3 API: price, context window and how to call it
GLM-5.3 and GLM-5.3 Flash API pricing
MiniMax M2.7 API: price, context and code
Translation API for 33 languages, per token
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.