Usage
Chat, sessions, attachments, tool calling, JSON mode and errors.
Basic usage
OpenAI SDK usage (drop-in replacement for the official API):
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="dummy")
r = client.chat.completions.create(
model="deepseek-v4-flash",
messages=[{"role": "user", "content": "Hello!"}],
)
print(r.choices[0].message.content)
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "deepseek-v4-flash", "messages": [{"role": "user", "content": "Hello!"}]}'
Or with a Qwen model:
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "qwen3.8-max", "messages": [{"role": "user", "content": "Hello!"}]}'
Sessions
Multi-turn: the response includes session_id; pass it in the next request to continue the same conversation.
{
"model": "deepseek-v4-flash",
"messages": [{"role": "user", "content": "2+2?"}],
"session_id": "<id from the previous response>",
"thinking": true,
"search": false,
"stream": true
}
Stateless clients (no session_id) don't lose context either: the server
derives a fingerprint from the system/user messages and reuses the
matching server-side chat. If the message context is identical - the same
chat is used; if the context is a continuation of a previously seen one -
that chat is continued, so multi-turn conversations work even when the
client never echoes session_id. Pass session_id to force an exact
conversation (or to branch off into an independent chat). The in-memory
cache is LRU-bounded per provider (DANYAPI_SESSION_CACHE_SIZE, default
128).
The context fingerprint is scoped by the user field when the client sends
it: two different user values never share a server-side chat even for
identical messages, so stateless multi-tenant clients stay isolated. Cache
entries expire after DANYAPI_SESSION_TTL_SECONDS (default 3600, 0
disables expiry) so stale chats are dropped instead of being reused.
Qwen chats are also model-aware: a session_id created for one Qwen model
is automatically migrated to a new chat if a request switches models.
No context is ever duplicated or lost:
- A reused chat receives only the delta: the new user message, or the tool round tail (tool results) in the case of a tool call. The full conversation history lives server-side in the chat.
- The tool schema / system prompt are injected once, into the first message of a chat, and are not repeated in every follow-up message.
- If a chat cannot be matched (cache miss or eviction), the whole message history is replayed into a fresh chat, so the model always sees the full conversation.
Request fields
POST /v1/chat/completions accepts:
| Field | Default | Notes |
|---|---|---|
model | deepseek-v4-flash | deepseek-* routes to DeepSeek, qwen* to Qwen; anything else is HTTP 404 |
messages | [] | OpenAI format; content may be a string or a list of text/image_url parts |
stream | false | SSE stream (data: chunks + data: [DONE]) |
thinking | provider-specific | DeepSeek: off by default, on for deepseek-v4-pro; Qwen: on by default |
search | false | Web search; DeepSeek honors it only for deepseek-v4-flash |
session_id | null | Continue an exact server-side conversation |
user | null | Scopes the stateless context fingerprint (multi-tenant isolation) |
files | null | DeepSeek attachments: {name, content (base64), content_type} |
tools, tool_choice, parallel_tool_calls | null | Emulated tool calling (see below) |
response_format | null | Emulated JSON mode (see below) |
stream_options | null | {"include_usage": true} adds usage to the final SSE chunk |
temperature, top_p | null | Accepted for compatibility but ignored: the upstream web APIs have no sampling parameters |
The response additionally carries reasoning_content (the thinking trace)
in message (non-stream) or delta (stream) when thinking is enabled,
and session_id to continue the conversation.
Session persistence
The session registry (chat ids, context fingerprints, session → account affinity, accumulated usage counters) is written to disk as JSON files, so conversations survive server restarts and keep pointing at the same server-side chats and accounts:
- Location: DANYAPI_CACHE_DIR, default is the system temp dir (
%TEMP%\danyapion Windows,/tmp/danyapion Linux/macOS). - Files:
<provider>-sessions-default.json,<provider>-contexts-default.json,<provider>-affinities-default.json. - Writes are atomic (temp file + rename), so a crash mid-write cannot corrupt the cache.
- Set DANYAPI_CACHE_DISABLED=1 to keep everything in memory only.
- In Docker, mount DANYAPI_CACHE_DIR as a volume if you want sessions to survive container recreation.
Note that the upstream chats themselves live on the provider side; the local cache only maps your session_id / message context to them.
File attachments (DeepSeek)
Send files as base64 in the files field, or as image_url (data URI) parts inside a message:
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash",
"messages": [{"role": "user", "content": "What magic number is in the file?"}],
"files": [{"name": "secret.txt", "content": "<base64>", "content_type": "text/plain"}]
}'
{
"model": "deepseek-v4-vision",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "Describe the image."},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,<base64>"}}
]
}]
}
Per-model limits: deepseek-v4-vision accepts images only; deepseek-v4-flash accepts images (OCR) and text files; deepseek-v4-pro accepts no files. Max 50 files, 100 MB each per request.
Tool calling (emulated)
Neither chat.deepseek.com nor chat.qwen.ai exposes a native function-calling
API, so DanyAPI emulates it at the proxy layer with prompt injection. The
OpenAI-compatible tools, tool_choice and parallel_tool_calls request
fields are accepted:
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash",
"messages": [{"role": "user", "content": "What is the weather in Moscow?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the weather in a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}],
"tool_choice": "auto"
}'
How it works:
-
When
toolsare present, the function schema and a strict JSON instruction (with a concrete example, no template placeholders) are injected into the prompt sent to the upstream model. -
The model replies with a tool call - DanyAPI understands several formats and normalizes them all to a proper OpenAI response:
- JSON
{"tool_calls": [{"name": "...", "arguments": {...}}]}; - legacy
{"function_call": {...}}; - a bare dict
{"name": "...", "arguments": {...}}(Qwen/DeepSeek style) or a bare array[...]of them; - XML/Anthropic style
<tool_calls><invoke name="...">...</invoke></tool_calls>(arguments as child tags,<parameter name="...">, or inline JSON).
The result is
message.tool_calls(non-stream) or streameddelta.tool_callschunks, both withfinish_reason: "tool_calls". Any number of calls in one reply are supported (parallel_tool_calls), so clients that ship many tools (e.g. opencode) work out of the box. - JSON
-
You run the tool, then send back the result:
{"role": "tool", "tool_call_id": "<id>", "content": "22C, sunny"}. DanyAPI renders the tool results into the prompt and continues the conversation until the model answers (or calls more tools).
Notes:
-
tool_choice:"auto"(default),"none"(tools are accepted but no schema is injected),"required", or{"type": "function", "function": {"name": "<tool>"}}. -
Pass the
session_idfrom the first response back in the tool-result request to keep the conversation server-side. Stateless clients work too: the context cache reuses the chat from the previous round, so the tool results are sent as the continuation of the same server-side conversation instead of replaying the whole history into a brand-new chat. If a session cannot be matched (e.g. the cache was evicted), the whole message history (including tool results) is replayed into the prompt instead, so plain OpenAI-protocol clients keep working. -
While
toolsare present, streamed replies are buffered until the model finishes so the reply can be classified as a tool call or plain text. Reasoning (reasoning_content) is streamed live in both cases.
JSON mode (emulated)
response_format is emulated the same way as tool calling - the JSON
constraint (and an optional schema) is injected into the prompt:
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash",
"messages": [{"role": "user", "content": "Extract the city and temperature."}],
"response_format": {
"type": "json_schema",
"json_schema": {
"schema": {
"type": "object",
"properties": {"city": {"type": "string"}, "temperature": {"type": "number"}},
"required": ["city", "temperature"]
}
}
}
}'
response_format accepts "json_object", {"type": "json_object"} and
{"type": "json_schema", "json_schema": {...}}. As with tool calling this is
prompt-level emulation: replies are JSON in practice but not guaranteed
schema-valid - validate on the client side.
System prompt and health
-
systemmessages are collected and injected as the model's system prompt in front of the first user turn (upstream web APIs have no dedicatedsystemfield). -
GET /healthreturns{"status": "ok", "deepseek": true, "qwen": true}plus per-provider cache stats (deepseek_stats/qwen_stats: account health, session-affinity count, context-cache size and hit/miss counters) - useful for readiness probes and load balancers. - When the client disconnects mid-generation, DanyAPI tells the upstream provider to stop the stream, so the server-side chat does not keep a partial response.
Token usage
Every response carries an OpenAI-style usage object, and both providers
report it accumulated per conversation (like the official API, where the
counter grows with every turn of the same chat):
-
Qwen - the upstream reports per-turn prompt/completion counts; DanyAPI
sums them per chat.
prompt_tokens= total input tokens processed by this conversation so far,completion_tokens= total output generated,total_tokens= their sum. -
DeepSeek - the web API only exposes a single cumulative counter
(
accumulated_token_usage), socompletion_tokensis the total generated in this conversation so far andprompt_tokensis always0(total_tokensequalscompletion_tokens).
The counters live on the session, so a new conversation starts from zero and
continuing a session_id keeps counting. They are also persisted in the
on-disk session cache (survive restarts).
Streaming usage: pass "stream_options": {"include_usage": true} and the
final SSE chunk carries the same accumulated usage (like the official API).
Error handling
Non-stream requests get a plain HTTP error; stream requests get an SSE error event (and data: [DONE]) once the stream is open.
| Status | When |
|---|---|
400 | Bad request: invalid base64/files, per-model attachment rules violated, context length exceeded |
401 | DeepSeek auth error (invalid/expired token); the account is then marked broken and excluded from the pool |
404 | Unknown model name |
429 | All accounts busy (DANYAPI_ACQUIRE_TIMEOUT expired) or upstream throttling after retries are exhausted |
502 | Upstream request failed (network, file upload, Qwen WAF challenge) |
503 | Provider not configured (no tokens/email for that provider) or all its accounts are broken |
Retries: expert_busy_use_default / parallel_chat_limit / server_busy /
busy (DeepSeek) and Too_Many_Requests / RateLimited / quotaLimited
(Qwen) are retried automatically up to 5 times with exponential backoff
(1s, capped at 8s) before surfacing as errors. Responses can also end with
finish_reason: "content_filter" when the upstream moderates the output.
See Account limits.