diff --git a/.env.example b/.env.example index a30f8af..adcbbb9 100644 --- a/.env.example +++ b/.env.example @@ -1,29 +1,41 @@ -# DeepSeek provider: one or more bearer tokens, comma-separated +# TOKENS ========================== +# Open Developer -> Network settings; make sure logged in; find a '/chat/completion' URL call and get the Authorization: Bearer XXXXXXXXXX) +# DeepSeek/Qwen Bearer tokens (multiple = comma-separated) DEEPSEEK_TOKENS= - -# Qwen provider: one or more bearer tokens, comma-separated QWEN_TOKENS= -# address the API server binds to + +# CONNECTION ====================== +# address the API server binds (alt HOST, PORT, PROXY, API_KEY) DANYAPI_HOST=0.0.0.0 -# port the API server listens on DANYAPI_PORT=8000 +# (optional) outgoing proxy for upstream communication +# (e.g. socks5://127.0.0.1:1080 or http://127.0.0.1:8080, docker: socks5://host.docker.internal:1080) +DANYAPI_PROXY= +# (optional) if enforcing USER Bearer token on user-api calls +DANYAPI_API_KEY= + + +# SETTINGS ======================== # upstream request timeout in seconds DANYAPI_TIMEOUT=60 # human-like delay before each upstream request (seconds) DANYAPI_HUMAN_DELAY_MIN=0.5 DANYAPI_HUMAN_DELAY_MAX=3.0 # seconds to wait for a free account before returning 429 (empty = wait forever) -DANYAPI_ACQUIRE_TIMEOUT= -# max server-side chats cached per provider (LRU) for stateless session reuse +DANYAPI_ACQUIRE_TIMEOUT=600 + +# SESSION: session_id -> maps to arbitrary user value (ex. session_id: "george") +# max server-side session-maps chats cached per provider (LRU) for stateless session reuse DANYAPI_SESSION_CACHE_SIZE=128 -# seconds an unused session/context stays reusable (0 = never expire) -DANYAPI_SESSION_TTL_SECONDS=3600 +# seconds an unused session/context stays reusable (0 = never expire, currently 7 days) +DANYAPI_SESSION_TTL_SECONDS=604800 # directory for on-disk session cache (default = system temp dir, e.g. %TEMP%\danyapi) DANYAPI_CACHE_DIR= # set to 1/true/yes to disable on-disk cache entirely DANYAPI_CACHE_DISABLED= -# log level: DEBUG, INFO, WARNING, ERROR + +# LOGGING: level -> DEBUG, INFO, WARNING, ERROR DANYAPI_LOG_LEVEL=INFO # file path for persistent logs (empty = console only) DANYAPI_LOG_FILE= @@ -31,9 +43,10 @@ DANYAPI_LOG_FILE= DANYAPI_LOG_MAX_BYTES=10485760 # number of rotated log files to keep DANYAPI_LOG_BACKUP_COUNT=3 + # set to 0/false/no/off to disable usage tracking (token counter stats) DANYAPI_USAGE_ENABLED=1 # max recent usage records kept in memory for /v1/usage DANYAPI_USAGE_MAX_RECORDS=1000 # auto-update to the latest GitHub release on each start (0 disables) -DANYAPI_AUTO_UPDATE=1 \ No newline at end of file +DANYAPI_AUTO_UPDATE=1 diff --git a/Dockerfile b/Dockerfile index 8b10ffb..8b4e500 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,12 +8,13 @@ RUN pip install --no-cache-dir -r requirements.txt COPY danyapi ./danyapi RUN apt-get update \ - && apt-get install -y --no-install-recommends gcc libc6-dev nodejs \ + && apt-get install -y --no-install-recommends gcc libc6-dev nodejs curl \ && gcc -O3 -pthread -funroll-loops -flto -fomit-frame-pointer -o danyapi/deepseek/pow_solver danyapi/deepseek/pow_solver.c \ && apt-get purge -y gcc libc6-dev \ && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/* +ENV PYTHONUNBUFFERED=1 ENV DANYAPI_HOST=0.0.0.0 ENV DANYAPI_PORT=8000 diff --git a/app.py b/app.py index cc34840..0a87456 100644 --- a/app.py +++ b/app.py @@ -42,9 +42,10 @@ def main() -> None: import uvicorn from danyapi.config import settings - from danyapi.logging import uvicorn_log_config + from danyapi.logging import log_startup_info, uvicorn_log_config print(f"DanyAPI starting on {settings.host}:{settings.port}") + log_startup_info() uvicorn.run( "danyapi.api.openai:app", host=settings.host, diff --git a/danyapi/__main__.py b/danyapi/__main__.py index ef4ce44..168bdda 100644 --- a/danyapi/__main__.py +++ b/danyapi/__main__.py @@ -13,7 +13,9 @@ def main() -> None: ) sys.exit(1) from danyapi.config import settings - from danyapi.logging import uvicorn_log_config + from danyapi.logging import log_startup_info, uvicorn_log_config + + log_startup_info() uvicorn.run( "danyapi.api.openai:app", diff --git a/danyapi/api/openai.py b/danyapi/api/openai.py index 5e1199f..bb45fed 100644 --- a/danyapi/api/openai.py +++ b/danyapi/api/openai.py @@ -7,6 +7,7 @@ import logging import random import re +import secrets import time import uuid from contextlib import asynccontextmanager @@ -17,7 +18,7 @@ import httpx from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import HTMLResponse, StreamingResponse +from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, Field, field_validator @@ -125,7 +126,7 @@ class FileSpec(BaseModel): class ChatCompletionRequest(BaseModel): - model: str = Field(default="deepseek-v4-flash") + model: str | None = None messages: list[ChatMessage] = Field(default_factory=list) stream: bool = False temperature: float | None = None @@ -271,6 +272,8 @@ async def lifespan(app: FastAPI): app.state.qwen_models = [] if not accounts and not qwen_accounts: raise RuntimeError("no valid credentials: set DEEPSEEK_TOKENS or QWEN_TOKENS") + app.state.default_model = _determine_default_model(app) + log.info("default model: %s", app.state.default_model) yield finally: for ds_acct in accounts: @@ -318,6 +321,22 @@ async def _fetch_qwen_models(client: QwenClient) -> list[dict]: return models +def _determine_default_model(app_obj: FastAPI | None = None) -> str: + target = app_obj or app + pool = getattr(target.state, "pool", None) + qwen_pool = getattr(target.state, "qwen_pool", None) + qwen_models: list[dict] = getattr(target.state, "qwen_models", []) + + if pool and pool.accounts: + return next(iter(MODEL_TYPE_BY_NAME), "deepseek-v4-flash") + if qwen_pool and qwen_pool.accounts and qwen_models: + chat_models = [m["id"] for m in qwen_models if m.get("model_type") == "chat"] + if chat_models: + return chat_models[0] + return qwen_models[0]["id"] + return "deepseek-v4-flash" + + app = FastAPI(title="DanyAPI", lifespan=lifespan) app.add_middleware( @@ -490,6 +509,9 @@ async def add_tokens(tokens: dict) -> dict: _write_env_tokens(merged_ds, merged_qw) settings.deepseek_tokens = merged_ds settings.qwen_tokens = merged_qw + if added_ds or added_qw: + app.state.default_model = _determine_default_model(app) + log.info("default model updated: %s", app.state.default_model) parts = [] if added_ds: @@ -522,8 +544,11 @@ async def _extract_request_model(request: Request) -> str | None: return None if isinstance(payload, dict): model = payload.get("model") - if isinstance(model, str): - return model + if isinstance(model, str) and model.strip(): + return model.strip() + default_m = getattr(app.state, "default_model", None) + if default_m: + return f"{default_m} (default)" return None @@ -543,11 +568,13 @@ def _log_request_failure(request: Request, model: str | None, duration: float, s ) -def _log_request_success(request: Request, duration: float) -> None: +def _log_request_success(request: Request, duration: float, model: str | None = None) -> None: + model_part = f"model={model} " if model else "" log.info( - "%s %s success (%.0fms)", + "%s %s %ssuccess (%.0fms)", request.method, request.url.path, + model_part, duration, ) @@ -573,10 +600,43 @@ async def _log_request_failures(request: Request, call_next): status=response.status_code, ) elif request.url.path == "/v1/chat/completions": - _log_request_success(request, (time.monotonic() - started) * 1000) + _log_request_success(request, (time.monotonic() - started) * 1000, await _extract_request_model(request)) return response +PUBLIC_PATHS = {"/", "/favicon.ico", "/health"} + + +@app.middleware("http") +async def _authenticate_request(request: Request, call_next): + if settings.api_key and request.method != "OPTIONS": + path = request.url.path.rstrip("/") or "/" + if path not in PUBLIC_PATHS and not path.startswith("/docs"): + auth_header = request.headers.get("Authorization", "") + token = "" + if auth_header.startswith("Bearer "): + token = auth_header[7:].strip() + elif auth_header: + token = auth_header.strip() + + if not token: + token = request.headers.get("x-api-key", "").strip() + + if not token or not secrets.compare_digest(token, settings.api_key): + return JSONResponse( + status_code=401, + content={ + "error": { + "message": "Incorrect API key provided or missing authorization token.", + "type": "invalid_request_error", + "param": None, + "code": "invalid_api_key", + } + }, + ) + return await call_next(request) + + MAX_FILES_PER_REQUEST = 50 MAX_FILE_SIZE = 100 * 1024 * 1024 @@ -813,6 +873,8 @@ def _resolve_provider(model: str) -> str: @app.post("/v1/chat/completions") async def chat_completions(req: ChatCompletionRequest) -> Any: + if not req.model: + req.model = getattr(app.state, "default_model", "deepseek-v4-flash") provider = _resolve_provider(req.model) if provider == "qwen": return await _chat_completions_qwen(req) @@ -850,7 +912,7 @@ async def image_generations(req: ImageGenerationRequest) -> dict: data.append({"url": url}) continue try: - async with httpx.AsyncClient(follow_redirects=True, timeout=30) as hc: + async with httpx.AsyncClient(follow_redirects=True, timeout=30, proxy=settings.proxy) as hc: img_resp = await hc.get(url) if img_resp.status_code != 200: log.warning("image download failed (%s) for %s, returning url", img_resp.status_code, url) diff --git a/danyapi/config.py b/danyapi/config.py index 31fc57c..1a6f29b 100644 --- a/danyapi/config.py +++ b/danyapi/config.py @@ -45,8 +45,10 @@ def _env_str(key: str, default: str = "") -> str: class Settings: def __init__(self) -> None: - self.host = os.environ.get("DANYAPI_HOST", "0.0.0.0") - self.port = _env_int("DANYAPI_PORT", 8000) + self.host = _env_str("DANYAPI_HOST") or _env_str("HOST") or "0.0.0.0" + self.port = _env_int("DANYAPI_PORT", 0) or _env_int("PORT", 0) or 8000 + self.proxy = _env_str("DANYAPI_PROXY") or _env_str("PROXY") or None + self.api_key = _env_str("DANYAPI_API_KEY") or _env_str("API_KEY") or None tokens = [t.strip() for t in os.environ.get("DEEPSEEK_TOKENS", "").split(",") if t.strip()] self.deepseek_tokens = tokens qwen_tokens = [t.strip() for t in os.environ.get("QWEN_TOKENS", "").split(",") if t.strip()] @@ -55,8 +57,8 @@ def __init__(self) -> None: self.acquire_timeout = _env_float_opt("DANYAPI_ACQUIRE_TIMEOUT") self.session_cache_size = _env_int("DANYAPI_SESSION_CACHE_SIZE", 128) self.session_ttl = _env_float("DANYAPI_SESSION_TTL_SECONDS", 3600.0) - self.log_level = _env_str("DANYAPI_LOG_LEVEL", "INFO") or "INFO" - self.log_file = _env_str("DANYAPI_LOG_FILE") + self.log_level = _env_str("DANYAPI_LOG_LEVEL") or _env_str("LOG_LEVEL") or "INFO" + self.log_file = _env_str("DANYAPI_LOG_FILE") or _env_str("LOG_FILE") self.log_max_bytes = _env_int("DANYAPI_LOG_MAX_BYTES", 10 * 1024 * 1024) self.log_backup_count = _env_int("DANYAPI_LOG_BACKUP_COUNT", 3) self.cache_dir = _env_str("DANYAPI_CACHE_DIR") diff --git a/danyapi/deepseek/client.py b/danyapi/deepseek/client.py index 9be759b..9399f6a 100644 --- a/danyapi/deepseek/client.py +++ b/danyapi/deepseek/client.py @@ -7,6 +7,8 @@ import httpx +from ..config import settings + log = logging.getLogger("danyapi.deepseek") BASE_URL = "https://chat.deepseek.com" @@ -48,6 +50,7 @@ def __init__( token: str | None = None, device_id: str | None = None, timeout: float = 60.0, + proxy: str | None = None, ) -> None: self.token = token self.device_id = device_id or new_device_id() @@ -65,6 +68,7 @@ def __init__( headers=headers, timeout=httpx.Timeout(timeout), follow_redirects=True, + proxy=proxy or settings.proxy, ) async def aclose(self) -> None: @@ -101,6 +105,9 @@ async def check_auth(self) -> bool: params={"did": self.device_id, "scope": "main"}, ) return resp.json().get("code") == 0 + except (httpx.ConnectError, httpx.ProxyError, httpx.TimeoutException) as exc: + log.error("upstream connection failed (%s): check network/proxy settings", exc) + return False except (httpx.HTTPError, ValueError): return False diff --git a/danyapi/logging.py b/danyapi/logging.py index 0d26c85..b4172d4 100644 --- a/danyapi/logging.py +++ b/danyapi/logging.py @@ -1,5 +1,6 @@ from __future__ import annotations +import ipaddress import logging import re import sys @@ -156,7 +157,7 @@ def configure() -> None: root.setLevel(level) if not _has_handler(root, CONSOLE_HANDLER_NAME): - console = logging.StreamHandler() + console = logging.StreamHandler(sys.stdout) console.name = CONSOLE_HANDLER_NAME console.setLevel(level) console.setFormatter(_ColorFormatter()) @@ -195,3 +196,97 @@ def uvicorn_log_config() -> dict: "uvicorn.access": {"handlers": [], "level": level, "propagate": True}, }, } + + +IP_CHECK_ENDPOINTS = [ + "https://ifconfig.me/ip", + "https://api.ipify.org", + "https://icanhazip.com", +] + + +def _is_valid_ip(text: str) -> bool: + try: + ipaddress.ip_address(text.strip()) + return True + except ValueError: + return False + + +def get_outgoing_ip(proxy: str | None = None, timeout: float = 4.0) -> tuple[str | None, str | None]: + proxy_url = proxy if (isinstance(proxy, str) and proxy.strip()) else None + last_err: str | None = None + + # 1. Try httpx + try: + import httpx + + with httpx.Client(proxy=proxy_url, timeout=timeout) as client: + for url in IP_CHECK_ENDPOINTS: + try: + resp = client.get(url) + if resp.status_code == 200: + candidate = resp.text.strip() + if candidate and _is_valid_ip(candidate): + return candidate, None + except Exception as exc: + last_err = f"{type(exc).__name__}: {exc}" + continue + except Exception as exc: + last_err = f"{type(exc).__name__}: {exc}" + + # 2. Fallback to curl if available + try: + import shutil + import subprocess + + curl_path = shutil.which("curl") + if curl_path: + for url in IP_CHECK_ENDPOINTS: + cmd = [curl_path, "-s", "--max-time", str(int(timeout))] + if proxy_url: + if proxy_url.startswith("socks5://") or proxy_url.startswith("socks5h://"): + socks_addr = proxy_url.split("://", 1)[1] + cmd.extend(["--socks5-hostname", socks_addr]) + else: + cmd.extend(["-x", proxy_url]) + cmd.append(url) + try: + res = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 1) + candidate = res.stdout.strip() + if res.returncode == 0 and candidate and _is_valid_ip(candidate): + return candidate, None + except Exception: + continue + except Exception: + pass + + return None, last_err + + +def log_startup_info() -> None: + from danyapi.config import settings + + log = logging.getLogger("danyapi") + + raw_proxy = getattr(settings, "proxy", None) + proxy = raw_proxy if (isinstance(raw_proxy, str) and raw_proxy.strip()) else None + proxy_desc = f"via proxy {proxy}" if proxy else "direct, no proxy" + + try: + ip, err = get_outgoing_ip(proxy=proxy, timeout=4.0) + if ip: + log.info("outgoing IP: %s (%s)", ip, proxy_desc) + elif err: + log.warning("could not determine outgoing IP (%s): %s", proxy_desc, err) + else: + log.warning("could not determine outgoing IP (%s)", proxy_desc) + except Exception as exc: + log.warning("could not determine outgoing IP (%s): %s", proxy_desc, exc) + + raw_api_key = getattr(settings, "api_key", None) + api_key = raw_api_key if (isinstance(raw_api_key, str) and raw_api_key.strip()) else None + if api_key: + log.info("authentication: Bearer token required") + else: + log.info("authentication: open (no API_KEY set)") diff --git a/danyapi/qwen/client.py b/danyapi/qwen/client.py index 1595ce6..a8b30c0 100644 --- a/danyapi/qwen/client.py +++ b/danyapi/qwen/client.py @@ -8,6 +8,8 @@ import httpx +from ..config import settings + log = logging.getLogger("danyapi.qwen") BASE_URL = "https://chat.qwen.ai" @@ -62,6 +64,7 @@ def __init__( self, token: str | None = None, timeout: float = 60.0, + proxy: str | None = None, ) -> None: self.token = token headers = { @@ -75,6 +78,7 @@ def __init__( headers=headers, timeout=httpx.Timeout(timeout), follow_redirects=True, + proxy=proxy or settings.proxy, ) if token: self.http.cookies.set("token", token, domain="chat.qwen.ai", path="/") @@ -132,6 +136,9 @@ async def check_auth(self) -> bool: if isinstance(nested, dict) and nested.get("id"): return True return bool(payload.get("id")) + except (httpx.ConnectError, httpx.ProxyError, httpx.TimeoutException) as exc: + log.error("upstream connection failed (%s): check network/proxy settings", exc) + return False except (httpx.HTTPError, ValueError): return False diff --git a/danyapi/reg/captcha.py b/danyapi/reg/captcha.py index 12468e0..c5cbc4b 100644 --- a/danyapi/reg/captcha.py +++ b/danyapi/reg/captcha.py @@ -6,6 +6,8 @@ import httpx +from ..config import settings + HCAPTCHA_SITEKEY = "352e5376-f2cc-43fe-a744-e51640449610" HCAPTCHA_PAGE_URL = "https://chat.deepseek.com/sign_up" @@ -59,7 +61,7 @@ def __init__(self, api_key: str, timeout: float = 180.0, poll_interval: float = self.poll_interval = poll_interval async def solve(self) -> str: - async with httpx.AsyncClient(timeout=30.0) as http: + async with httpx.AsyncClient(timeout=30.0, proxy=settings.proxy) as http: try: resp = await http.get( "https://2captcha.com/in.php", @@ -105,7 +107,7 @@ def __init__(self, api_key: str, timeout: float = 180.0, poll_interval: float = self.poll_interval = poll_interval async def solve(self) -> str: - async with httpx.AsyncClient(timeout=30.0) as http: + async with httpx.AsyncClient(timeout=30.0, proxy=settings.proxy) as http: try: resp = await http.post( "https://api.capsolver.com/createTask", diff --git a/danyapi/reg/deepseek.py b/danyapi/reg/deepseek.py index 4f86ab6..1669d3c 100644 --- a/danyapi/reg/deepseek.py +++ b/danyapi/reg/deepseek.py @@ -7,6 +7,7 @@ import httpx +from ..config import settings from ..pow import solve_challenge log = logging.getLogger("danyapi.reg.deepseek") @@ -75,13 +76,20 @@ def guest_pow_header(salt: str, answer: int) -> dict[str, str]: class DeepSeekRegistrar: - def __init__(self, device_id: str | None = None, timeout: float = 60.0, waf_token: str | None = None) -> None: + def __init__( + self, + device_id: str | None = None, + timeout: float = 60.0, + waf_token: str | None = None, + proxy: str | None = None, + ) -> None: self.device_id = device_id or new_device_id() self.http = httpx.AsyncClient( base_url=BASE_URL, headers={"User-Agent": USER_AGENT, **CLIENT_HEADERS}, timeout=httpx.Timeout(timeout), follow_redirects=True, + proxy=proxy or settings.proxy, ) if waf_token: self.http.cookies.set("aws-waf-token", waf_token, domain="chat.deepseek.com", path="/") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bc5ffde --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + danyapi: + # you can build from source: docker compose up --build + build: . + # or comment above out, and load latest from repo. + # image: ghcr.io/fanatfanata/danyapi:latest + container_name: danyapi + restart: unless-stopped + ports: + - "8000:8000" + env_file: + - ./.env + volumes: + #- ./web:/app/web:ro + - /etc/localtime:/etc/localtime:ro diff --git a/docs/DOCS.md b/docs/DOCS.md new file mode 100644 index 0000000..bb11c4e --- /dev/null +++ b/docs/DOCS.md @@ -0,0 +1,128 @@ +# DANYAPI DOCS +-------------- +The DanyAPI provides a server-side dispatcher to communicate with Deepseek/Qwen. + +# CONFIGURATION +The config has 3 main areas: tokens, connections and settings (session, logging). + +### Tokens +Edit the *.env* file with the tokens. + +Start up Deepseek or Qwen and login. Then open the Developer-Debugger and look at the network requests. Look for a request with 'completion' in it. Click on that and look for the Authorization: Bearer XXXXXXXXXXX. + +That sequence of characters is what you need to put into the DEEPSEEK/QWEN tokens. It will send the requests like your user-id based on that token. Don't let other people use this API as it may get your user banned. Treat this system as a privilege to use. + +### Connections +By default it binds to all IPs on the system at port 8000. If you want to run it through a proxy (ex. to make it appear to have a residential IP), you would set the proxy in DANYAPI_PROXY. + +If you want to force incoming requests to you to have a Bearer-token, you can set that in DANYAPI_API_KEY. If it's not set, all requests are accepted (only run it locally then!) + +### Settings +The 'session' is like a chat conversation. So each call you make can be grouped together in the same session (which saves lots of context tokens, as it remembers prior conversation). You can set the session_id to whatever, ex a name, etc. It saves 128 by default for 7 days, but examine the .env.example and change. + +### Logging & more +By default it logs everything to the docker log, or stdout but you can choose what to log, or even if a file. + +You can log tokens used, and auto-update. + + +# RUNNING +You can run it on your system, or more easily you can run it from docker: + +``` +docker compose up +``` + +It will show log output like: + ✔ Image danyapi-danyapi Built 149.6s + ✔ Container danyapi Recreated 0.5s +Attaching to danyapi + +danyapi | (21:37:33) outgoing IP: 99.88.77.66 (via proxy socks5://host.docker.internal:1080) +danyapi | (21:37:33) authentication: open (no API_KEY set) +danyapi | (21:37:35) deepseek accounts ready: 1 +danyapi | (21:37:35) default model: deepseek-v4-flash +danyapi | (21:37:35) DanyAPI running on http://0.0.0.0:8000 (Press CTRL+C to quit) +danyapi | (21:42:35) deepseek create session success (1520ms) +danyapi | (21:42:49) deepseek completion success (14234ms) +danyapi | (21:42:49) POST /v1/chat/completions success (17800ms) +danyapi | (21:59:23) deepseek completion success (2910ms) +danyapi | (21:59:23) POST /v1/chat/completions success (3650ms) + + +# ENDPOINTS + +### LLM Endpoints (OpenAI-Compatible) +- POST **/v1/chat/completions** — Chat, reasoning (thinking), search, tools, sessions (session_id), and streaming. + * messages (array, required): List of message objects (role: system | user | assistant, content: string). + * optional: model (string), stream (boolean, default: false), thinking (boolean), search (boolean), session_id (string): continue conversation, tools (array). +- POST **/v1/images/generations** — Text-to-image generation powered by Qwen. + * prompt (string, required): Text description of the image to generate. + * optional: model (string, default: qwen-image-gen), n (integer, default: 1), size (string, default: 1024x1024), response_format (string, default: url). + +- _Note: If Bearer Auth given, that will be required in calls_. + +### Models & Token Management +- GET **/v1/models** — List of all available DeepSeek and Qwen models. +- GET **/v1/usage** — Real-time request and token consumption statistics. +- POST **/v1/tokens** — Hot-add new tokens without restarting the server. + +### Health & Diagnostics +- GET **/health** — Provider readiness, active accounts, and cache metrics. + +### Web UI & Documentation +- GET **/** — Interactive token management and usage dashboard. +- GET **/docs/** — Landing page, playground, and guides. +- GET **/openapi.json** & **GET /redoc** — OpenAPI specs and ReDoc interactive viewer. + + +# EXAMPLES +See the models available: +``` +curl -s http://localhost:8000/v1/models +``` + +By default the first model with a valid token is selected if none specified (so if deepseek token available, deepseek-v4-flash is selected). + +We will use curl to show the same prompts: +``` +curl -s http://localhost:8000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "messages": [ + {"role": "system", "content": "You are a helpful assistant that summarizes news articles."}, + {"role": "user", "content": "Please summarize the results of this poll: https://slashdot.org/poll/3284/how-much-of-your-coding-is-done-by-ai-coding-agents-these-days"} + ], + "session_id": "george"}' +``` + +Then follow-up chats woulds keep the same session_id, ex. +``` +curl -s http://localhost:8000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "messages": [ + {"role": "system", "content": "You are a helpful assistant that summarizes news articles."}, + {"role": "user", "content": "What was the top pick?"} + ], + "session_id": "george"}' +``` + +### Image Generation (Qwen) + +If you have a Qwen token in this, you can generate images: +``` +curl -s http://localhost:8000/v1/images/generations \ + -H "Content-Type: application/json" \ + -d '{ + "prompt": "Futuristic skyline at dusk, cyberpunk style, digital art", + "size": "1024x1024" + }' +``` + +### Usage Tracking +You can use the webpage (if it's enabled to see from a browser), or: + +``` +curl -s http://localhost:8000/v1/usage +``` diff --git a/requirements.txt b/requirements.txt index 526f6ac..a36bc05 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ fastapi uvicorn -httpx +httpx[socks] pydantic python-dotenv pillow