Skip to content
Merged

Dev #36

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
589d7e6
feat(docs): add standalone token extractor (get_tokens.py + run_get_t…
SHARKgamestudio Sep 14, 2026
bd427d7
fix(docs): replace em-dashes in get_tokens.py to satisfy repo guards
SHARKgamestudio Sep 14, 2026
defaa2b
fix(docs): drop unused datetime import in get_tokens.py (ruff F401)
SHARKgamestudio Sep 14, 2026
69fe01f
style(docs): wrap lines over 160 chars in get_tokens.py (ruff E501)
SHARKgamestudio Sep 14, 2026
1f109cc
refactor(docs): thread the server port explicitly instead of global r…
SHARKgamestudio Sep 14, 2026
8f18a64
style(docs): apply ruff format to get_tokens.py
SHARKgamestudio Sep 14, 2026
7f42a74
fix(app): pass explicit check=False to subprocess.run in pow solver b…
SHARKgamestudio Sep 14, 2026
1108015
fix(docs): resolve pylint E1101 on sys.stdout.encoding in get_tokens.py
SHARKgamestudio Sep 14, 2026
165932a
refactor(docs): rename token extractor to token_utility and add Unix …
SHARKgamestudio Sep 14, 2026
4e644b4
Merge pull request #35 from SHARKgamestudio/dev
FANATFANATA Sep 14, 2026
7311dcb
fix: flush incremental.finish() in streaming paths (DeepSeek/Qwen) + …
FANATFANATA Sep 15, 2026
ccefc09
docs: remove BYOK section and update public instance auth note
FANATFANATA Sep 15, 2026
eb21c8b
docs: clarify public instance API key format, remove BYOK from env ex…
FANATFANATA Sep 15, 2026
963d2cc
docs: simplify public instance API key note
FANATFANATA Sep 15, 2026
058005a
docs: add example request and update public instance description
FANATFANATA Sep 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,3 @@ DANYAPI_LOG_BACKUP_COUNT=3
DANYAPI_USAGE_ENABLED=1
DANYAPI_USAGE_MAX_RECORDS=1000
DANYAPI_AUTO_UPDATE=1
BYOK=0
BYOK_MODE=0
44 changes: 19 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
# DanyAPI

OpenAI-compatible HTTP API built on Python + FastAPI. Instead of the paid APIs it talks to the internal APIs of the free web clients [chat.deepseek.com](https://chat.deepseek.com) and [chat.qwen.ai](https://chat.qwen.ai) using server-side accounts created from your own free provider tokens (`DEEPSEEK_TOKENS` / `QWEN_TOKENS`). API consumers need no keys - all upstream requests are made by the configured server tokens.
OpenAI compatible HTTP API built on Python + FastAPI. Instead of the paid APIs it talks to the internal APIs of the free web clients.

[![CI](https://img.shields.io/github/actions/workflow/status/FANATFANATA/DanyAPI/ci.yml?branch=prod)](https://github.com/FANATFANATA/DanyAPI/actions)
[![GitHub Release](https://img.shields.io/github/v/release/FANATFANATA/DanyAPI?sort=semver)](https://github.com/FANATFANATA/DanyAPI/releases)
[![Python](https://img.shields.io/badge/Python-blue)](https://www.python.org/)
[![Docker](https://img.shields.io/badge/GHCR-ghcr.io%2Ffanatfanata%2Fdanyapi-blue)](https://github.com/FANATFANATA/DanyAPI/pkgs/container/danyapi)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/FANATFANATA/DanyAPI/blob/prod/LICENSE)

## Public hosted instance (full free)
## Public hosted instance

Don't want to self-host? A public, fully free instance is already running in production - no signup, no keys, no limits on your side:
A public instance is already running in production (BYOK_MODE=1):

- API base URL: `https://danyapi.cloudpub.ru/v1/`
- Landing page: `https://danyapi.cloudpub.ru/`

Point any OpenAI-compatible client at `https://danyapi.cloudpub.ru/v1/` with a dummy `api_key` and it just works. The instance is backed by the same free provider tokens described below; treat it as best-effort.
Point any OpenAI compatible client at API base URL with a valid tokens, unauthenticated requests are rejected with 401. The API key should be the raw token (e.g. "token1,token2", same in .env).

### Example request

```bash
curl -X POST https://danyapi.cloudpub.ru/v1/chat/completions \
-H "Authorization: Bearer token1,token2,token3" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4.1-flash-thinking",
"messages": [
{"role": "user", "content": "Hi from example request!"}
]
}'
```

## Install & Upgrade

Requires Python 3.10+ (CI tests 3.10-3.14).
Requires Python 3.10+.

Windows (PowerShell):

Expand All @@ -42,25 +55,6 @@ docker run -d -p 8000:8000 \
ghcr.io/fanatfanata/danyapi:latest
```

## BYOK mode (bring your own key)

Set `BYOK=1` (or `BYOK_MODE=1`) in `.env` to switch from server-side `.env` tokens to per-request provider tokens. In this mode the client passes its own provider token(s) directly as the API key, and DanyAPI uses them for upstream requests instead of `DEEPSEEK_TOKENS`/`QWEN_TOKENS`:

```python
from openai import OpenAI

client = OpenAI(
base_url="https://your-instance/v1/",
api_key="your-deepseek-or-qwen-token", # sent upstream to the provider
)
```

- The key is read from `Authorization: Bearer <key>`, `x-api-key` header, or the `api_key` body field (priority in that order).
- Several keys can be supplied comma-separated (`api_key="tok1,tok2"`) - each valid key adds a parallel account, exactly like multiple `.env` tokens.
- Models still select the provider: `deepseek-*` / listed DeepSeek models go to DeepSeek, `qwen*` / listed Qwen models go to Qwen with their corresponding key type.
- Requests without a key (or with an invalid key for the selected provider) are rejected with `401`.
- Qwen model list is fetched lazily from the first valid Qwen key used.

## Contacts

[Creator](https://t.me/DanyaVoredom) · [Telegram channel](https://t.me/DanyAPIFree) · [Website](https://fanatfanata.github.io/DanyAPI/)
6 changes: 3 additions & 3 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def build_solver() -> None:
str(src_path),
f"/Fe:{bin_path}",
]
res = subprocess.run(cmd, capture_output=True, text=True)
res = subprocess.run(cmd, capture_output=True, text=True, check=False)
success = res.returncode == 0
else:
cmd_fast = [
Expand All @@ -82,7 +82,7 @@ def build_solver() -> None:
"-o",
str(bin_path),
]
res = subprocess.run(cmd_fast, capture_output=True, text=True)
res = subprocess.run(cmd_fast, capture_output=True, text=True, check=False)
if res.returncode == 0:
success = True
else:
Expand All @@ -94,7 +94,7 @@ def build_solver() -> None:
"-o",
str(bin_path),
]
res2 = subprocess.run(cmd_compat, capture_output=True, text=True)
res2 = subprocess.run(cmd_compat, capture_output=True, text=True, check=False)
success = res2.returncode == 0

if success:
Expand Down
51 changes: 51 additions & 0 deletions danyapi/api/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -2321,6 +2321,57 @@ async def _stream_openai(
],
}
)
for event in incremental.finish():
if event.event == "ready" and isinstance(event.data, dict):
response_message_id = event.data.get("response_message_id")
if response_message_id:
stop_message_id = response_message_id
rec.handle(event)
c_diff, r_diff = rec.take_diffs()
if not (c_diff or r_diff):
continue
got_content = True
if not role_sent:
role_sent = True
yield _sse(
{
"id": chunk_id,
"object": "chat.completion.chunk",
"created": created,
"model": model,
"choices": [
{
"index": 0,
"delta": {"role": "assistant"},
"finish_reason": None,
}
],
}
)
delta2: dict = {}
if c_diff:
if tool_mode:
content_buf += c_diff
else:
delta2["content"] = c_diff
if r_diff:
delta2["reasoning_content"] = r_diff
if delta2:
yield _sse(
{
"id": chunk_id,
"object": "chat.completion.chunk",
"created": created,
"model": model,
"choices": [
{
"index": 0,
"delta": delta2,
"finish_reason": None,
}
],
}
)
except BaseException:
stopped = True
if rec.id:
Expand Down
54 changes: 54 additions & 0 deletions danyapi/qwen/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,60 @@ async def stream_openai(
for line in pending:
yield line
pending.clear()
for event in incremental.finish():
rec.handle(event)
c_diff, r_diff = rec.take_diffs()
if c_diff or r_diff:
got_content = True
if not role_sent:
role_sent = True
pending.append(
_sse(
{
"id": chunk_id,
"object": "chat.completion.chunk",
"created": created,
"model": model,
"choices": [
{
"index": 0,
"delta": {"role": "assistant"},
"finish_reason": None,
}
],
}
)
)
delta2: dict = {}
if c_diff:
if tool_mode:
content_buf += c_diff
else:
delta2["content"] = c_diff
if r_diff:
delta2["reasoning_content"] = r_diff
if delta2:
pending.append(
_sse(
{
"id": chunk_id,
"object": "chat.completion.chunk",
"created": created,
"model": model,
"choices": [
{
"index": 0,
"delta": delta2,
"finish_reason": None,
}
],
}
)
)
if got_content:
for line in pending:
yield line
pending.clear()
except BaseException:
stopped = True
if rec.response_id:
Expand Down
37 changes: 37 additions & 0 deletions docs/token_utility.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
@echo off
setlocal
title DanyAPI Token Utility
cd /d "%~dp0"

rem Locate a Python interpreter (any version 3.6+, 32 or 64-bit - no deps needed)
set "PY="

where python >nul 2>nul && set "PY=python"
if not defined PY (
where py >nul 2>nul && set "PY=py -3"
)
if not defined PY (
if exist "%LocalAppData%\Programs\Python\Python310-32\python.exe" (
set "PY=%LocalAppData%\Programs\Python\Python310-32\python.exe"
)
)

if not defined PY (
echo [ERROR] Python not found. Install Python 3.6+ from https://python.org
echo and make sure "Add Python to PATH" is checked.
pause
exit /b 1
)

echo Using interpreter: %PY%
echo.

%PY% token_utility.py %*
if errorlevel 1 (
echo.
echo [ERROR] Script exited with an error.
pause
exit /b 1
)

endlocal
Loading
Loading