DanyAPI

Usage

Chat, sessions, attachments, tool calling, JSON mode and errors.

Basic usage

OpenAI SDK usage (drop-in replacement for the official API):

python
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)
bash
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:

bash
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.

json
{
                    "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:

FieldDefaultNotes
modeldeepseek-v4-flashdeepseek-* 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
streamfalseSSE stream (data: chunks + data: [DONE])
thinkingprovider-specificDeepSeek: off by default, on for deepseek-v4-pro; Qwen: on by default
searchfalseWeb search; DeepSeek honors it only for deepseek-v4-flash
session_idnullContinue an exact server-side conversation
usernullScopes the stateless context fingerprint (multi-tenant isolation)
filesnullDeepSeek attachments: {name, content (base64), content_type}
tools, tool_choice, parallel_tool_callsnullEmulated tool calling (see below)
response_formatnullEmulated JSON mode (see below)
stream_optionsnull{"include_usage": true} adds usage to the final SSE chunk
temperature, top_pnullAccepted 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%\danyapi on Windows, /tmp/danyapi on 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:

bash
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"}]
                    }'
json
{
                    "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:

bash
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:

  1. When tools are 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.
  2. 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 streamed delta.tool_calls chunks, both with finish_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.

  3. 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_id from 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 tools are 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:

bash
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

  • system messages are collected and injected as the model's system prompt in front of the first user turn (upstream web APIs have no dedicated system field).
  • GET /health returns {"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), so completion_tokens is the total generated in this conversation so far and prompt_tokens is always 0 (total_tokens equals completion_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.

StatusWhen
400Bad request: invalid base64/files, per-model attachment rules violated, context length exceeded
401DeepSeek auth error (invalid/expired token); the account is then marked broken and excluded from the pool
404Unknown model name
429All accounts busy (DANYAPI_ACQUIRE_TIMEOUT expired) or upstream throttling after retries are exhausted
502Upstream request failed (network, file upload, Qwen WAF challenge)
503Provider 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.