DanyAPI

How it works

Reverse-engineered provider protocols and account limits.

DeepSeek

Protocol reverse-engineered from the chat.deepseek.com main bundle and its sha3 wasm module:

  • Auth: POST /api/v0/users/logindata.biz_data.user.token, then Authorization: Bearer <token>.
  • Headers: x-client-bundle-id, x-client-platform, x-client-version, x-client-locale, x-client-timezone-offset.
  • Session: POST /api/v0/chat_session/create (empty body) → chat_session.id.
  • Generation: POST /api/v0/chat/completion: {chat_session_id, parent_message_id, model_type, prompt, ref_file_ids, thinking_enabled, search_enabled, action, preempt}.
  • Response - text/event-stream: ready events, deltas (SET/APPEND/BATCH, response/... paths), finish, close.
  • PoW header X-DS-PoW-Response - base64 of {algorithm, challenge, salt, answer, signature, target_path}. The challenge is single-use: answer = minimal counter c where DeepSeekHashV1(f"{salt}_{expire_at}_" + str(c)) matches challenge (32 bytes). The server iterates c in [0, difficulty).

Qwen

Protocol reverse-engineered from the chat.qwen.ai frontend bundle:

  • Auth: POST /api/v2/auths/signin with {email, password} where the password is SHA-256 hex of the plain text → data.token (JWT). Requests send it as Authorization: Bearer <token> and the token cookie.
  • Headers: source: web, version, X-Request-Id, Timezone, browser sec-ch-ua/User-Agent/Origin/Referer.
  • Session: POST /api/v2/chats/new ({chatId, models, chat_type: "t2t", chat_mode: "normal", timestamp}) → data.id (the chat id).
  • Generation: POST /api/v2/chat/completions?chat_id=<id> with {stream, version: "2.1", incremental_output, chat_id, model, parent_id, messages: [{fid, parentId, role, content, chat_type: "t2t", feature_config: {thinking_enabled, output_schema: "phase", ...}}]}. The chat history lives server-side; parent_id points at the last assistant response id, so the next turn continues the same conversation.
  • Response - text/event-stream of OpenAI-style JSON chunks: {"choices": [{"delta": {"role", "content", "phase", "status"}}], "response_id", "usage"}. The response.created chunk opens the stream with the assistant response_id; content is streamed in the answer phase, thinking in think/DeepThinking/thinking_summary phases, and the stream ends with a chunk whose delta.status is finished.

Account limits

  • One chat.deepseek.com account can generate one message at a time (otherwise the server replies parallel_chat_limit). DanyAPI keeps an account pool and distributes concurrent requests across accounts; if all are busy, requests wait in a queue. More tokens = more parallel generations. The same applies to chat.qwen.ai accounts (Qwen uses its own pool, so DeepSeek and Qwen parallel generations are independent).
  • Sessions are tied to the account they were created on: repeat requests with the same session_id (or the same cached message context) route to the same account, so the conversation history stays intact server-side.
  • The in-memory session/context cache is LRU-bounded per account and per provider. When an entry is evicted, the corresponding chat is no longer reused and a new one is created on the next request; the explicit session_id remains the reliable way to pin a conversation. Unused entries also expire after DANYAPI_SESSION_TTL_SECONDS, and session-id → account affinity is cleaned up together with the cache, so memory stays bounded on long-running instances.
  • DeepSeek may throttle accounts (especially the expert model deepseek-v4-pro - "limited resource"). Responses with finish_reason expert_busy_use_default / parallel_chat_limit / server_busy / busy are retried automatically (up to 5 retries with exponential backoff). If all attempts are exhausted:

    • non-stream requests get HTTP 429 with the DeepSeek error text;
    • stream requests get an SSE error event with finish_reason.
  • Qwen may reply Too_Many_Requests / RateLimited / quotaLimited; those are retried automatically the same way (up to 5 retries), then reported as HTTP 429 or an SSE error event.
  • The DeepSeek PoW challenge is single-use - a new one is solved per request (the next one is prefetched in advance so you don't wait).
  • When all accounts are busy, requests wait for a free account. Set DANYAPI_ACQUIRE_TIMEOUT (seconds) to cap that wait and get an HTTP 429 ("all accounts are busy") instead of waiting forever.
  • Long server-side conversations eventually exceed the model's context window. When that happens DanyAPI detects the context-limit error, discards the overflowing chat (so it is never reused) and reports the failure: HTTP 400 for non-stream requests, or an SSE error event with finish_reason: "length" for stream requests. The next request automatically starts with a fresh conversation. To avoid hitting the limit often, keep Qwen built-in tools disabled (see Account setup) and rotate long conversations client-side.