Download API.en.md from a3216/ds2api: direct link, hf CLI and curl.
- Browser
- Download file 74.4 kB
-
https://huggingface.co/spaces/a3216/ds2api/resolve/fcdd4aa00c4b7afbcde9155c5ec0a9c5bceeeeb4/API.en.md
- Command line
-
hf download hf://spaces/a3216/ds2api@fcdd4aa00c4b7afbcde9155c5ec0a9c5bceeeeb4/API.en.md
-
curl -L -o API.en.md https://huggingface.co/spaces/a3216/ds2api/resolve/fcdd4aa00c4b7afbcde9155c5ec0a9c5bceeeeb4/API.en.md
DS2API API Reference
This document describes the actual behavior of the current Go codebase.
Docs: Overview / Architecture / Deployment / Testing / Prompt compatibility flow
Table of Contents
- Basics
- Configuration Best Practice
- Authentication
- Route Index
- Health Endpoints
- OpenAI-Compatible API
- Claude-Compatible API
- Gemini-Compatible API
- Ollama API
- Admin API
- Error Payloads
- cURL Examples
Basics
| Item | Details |
|---|---|
| Base URL | http://localhost:5001 or your deployment domain |
| Default Content-Type | application/json |
| Health probes | GET /healthz, GET /readyz |
| CORS | Enabled (uniformly covers /v1/*, /anthropic/*, /v1beta/models/*, /api/*, and /admin/*; echoes the browser Origin when present, otherwise *; default allow-list includes Content-Type, Authorization, X-API-Key, X-Ds2-Target-Account, X-Ds2-Source, X-Vercel-Protection-Bypass, X-Goog-Api-Key, Anthropic-Version, Anthropic-Beta, and also accepts third-party preflight-requested headers such as x-stainless-*; /v1/chat/completions on Vercel Node Runtime matches the same behavior; internal-only X-Ds2-Internal-Token remains blocked) |
- All JSON request bodies must be valid UTF-8; malformed byte sequences are rejected on ingress with
400 invalid json.
3.0 Adapter-Layer Notes
- OpenAI / Claude / Gemini protocols are now mounted on one shared
chirouter tree assembled ininternal/server/router.go. - Adapter responsibilities are streamlined to: request normalization → DeepSeek invocation → protocol-shaped rendering, reducing legacy split-logic paths.
- The full path from a protocol request to the pure-text web-chat prompt (normalized standard request, thinking-format injection, history split, completion payload assembly, response archiving) is documented in docs/prompt-compatibility.md, which is the source-of-truth document for that flow; this document only describes the visible fields and response shapes of each endpoint.
- Tool-calling semantics are aligned between Go and Node runtime: models should output the halfwidth-pipe DSML shell
<|DSML|tool_calls>→<|DSML|invoke name="...">→<|DSML|parameter name="...">; DS2API also accepts DSML wrapper aliases such as<dsml|tool_calls>and<|tool_calls>, common DSML separator drift such as<|DSML tool_calls>, collapsed DSML local names such as<DSMLtool_calls>, control-separator drift such as<DSML␂tool_calls>/ raw STX\x02, CJK angle bracket, fullwidth-bang / ideographic-comma separator drift, PascalCase local-name drift, and trailing attribute separator drift such as<DSM|parameter name="command"|>...〈/DSM|parameter〉,<!DSML!invoke name=“Bash”>,<、DSML、tool_calls>,<DSmartToolCalls>, or<DSMLtool_calls※>, arbitrary protocol prefixes such as<proto💥tool_calls>, and legacy canonical XML<tool_calls>→<invoke name="...">→<parameter name="...">. The scanner normalizes fixed local names (tool_calls/invoke/parameter) with non-structural separators before or after them back to XML before parsing, and also tolerates CDATA opener drift such as<![CDATA[/<、[CDATA[; only wrapped tool blocks or the narrow missing-opening-wrapper repair path enter the tool path, while bare<invoke>does not count as supported syntax. JSON literal parameter bodies are preserved as structured values, explicit empty or whitespace-only parameters are preserved as empty strings, malformed complete wrappers are released as plain text, and loose CDATA is narrowly repaired at final parse/flush when it can preserve a complete outer tool call. Admin APIseparates static config from runtime policy:/admin/config*for configuration state,/admin/settings*for runtime behavior.- When upstream returns a thinking-only response with no visible text, the Go main path and the Vercel Node streaming path retry once in the same DeepSeek session: it appends the prompt suffix
"Please provide a non-empty final answer or tool call."and setsparent_message_id. If that same-account retry would still end as429 upstream_empty_output, managed-account mode switches to the next available account, creates a fresh session, and retries the original payload once before returning 429. - Citation/reference marker boundary: streaming output hides upstream
[citation:N]/[reference:N]placeholders by default; non-stream output converts DeepSeek search reference markers into Markdown links.
Configuration Best Practice
Use config.json as the single source of truth:
cp config.example.json config.json
# Edit config.json (keys/accounts)
Use it per deployment mode:
- Local run: read
config.jsondirectly - Docker / Vercel: generate Base64 from
config.json, then setDS2API_CONFIG_JSON, or paste raw JSON directly
DS2API_CONFIG_JSON="$(base64 < config.json | tr -d '\n')"
For Vercel one-click bootstrap, you can set only DS2API_ADMIN_KEY first, then import config at /admin and sync env vars from the "Vercel Sync" page.
Authentication
Business Endpoints (/v1/*, /anthropic/*, /v1beta/models/*)
Two header formats accepted:
| Method | Example |
|---|---|
| Bearer Token | Authorization: Bearer <token> |
| API Key Header | x-api-key: <token> (no Bearer prefix) |
| Gemini-compatible | x-goog-api-key: <token> or ?key=<token> / ?api_key=<token> |
Auth behavior:
- Token is in
config.keys→ Managed account mode: DS2API auto-selects an account via rotation - Token is not in
config.keys→ Direct token mode: treated as a DeepSeek token directly
Optional header: X-Ds2-Target-Account: <email_or_mobile> — Pin a specific managed account; if the target account does not exist or the managed-account queue is exhausted, the request returns 429, and current responses do not include Retry-After. If the account exists but login/refresh fails, the request returns the underlying 401 or upstream error. Without a pinned target, managed-account completion requests try one alternate-account fresh retry before returning an empty-output 429; pinned-target requests and requests with no other available account do not switch.
Gemini-compatible clients can also send x-goog-api-key, ?key=, or ?api_key= as the caller credential source.
Admin Endpoints (/admin/*)
| Endpoint | Auth |
|---|---|
POST /admin/login |
Public |
GET /admin/verify |
Authorization: Bearer <jwt> (JWT only) |
Other /admin/* |
Authorization: Bearer <jwt> or Authorization: Bearer <admin_key> |
Route Index
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /healthz |
None | Liveness probe |
| HEAD | /healthz |
None | Liveness probe (no body) |
| GET | /readyz |
None | Readiness probe |
| HEAD | /readyz |
None | Readiness probe (no body) |
| GET | /v1/models |
None | OpenAI model list |
| GET | /v1/models/{id} |
None | OpenAI single-model query (alias accepted) |
| POST | /v1/chat/completions |
Business | OpenAI chat completions |
| POST | /v1/responses |
Business | OpenAI Responses API (stream/non-stream) |
| GET | /v1/responses/{response_id} |
Business | Query stored response (in-memory TTL) |
| POST | /v1/embeddings |
Business | OpenAI Embeddings API |
| POST | /v1/files |
Business | OpenAI Files upload (multipart/form-data) |
| GET | /v1/files/{file_id} |
Business | Retrieve uploaded file status |
| GET | /anthropic/v1/models |
None | Claude model list |
| POST | /anthropic/v1/messages |
Business | Claude messages |
| POST | /anthropic/v1/messages/count_tokens |
Business | Claude token counting |
| POST | /v1/messages |
Business | Claude shortcut path |
| POST | /messages |
Business | Claude shortcut path |
| POST | /v1/messages/count_tokens |
Business | Claude token counting shortcut |
| POST | /messages/count_tokens |
Business | Claude token counting shortcut |
| POST | /v1beta/models/{model}:generateContent |
Business | Gemini non-stream |
| POST | /v1beta/models/{model}:streamGenerateContent |
Business | Gemini stream |
| POST | /v1/models/{model}:generateContent |
Business | Gemini non-stream compat path |
| POST | /v1/models/{model}:streamGenerateContent |
Business | Gemini stream compat path |
| GET | /api/version |
None | Ollama version endpoint |
| GET | /api/tags |
None | Ollama model list |
| POST | /api/show |
None | Ollama model capability query (returns id + capabilities) |
| POST | /admin/login |
None | Admin login |
| GET | /admin/verify |
JWT | Verify admin JWT |
| GET | /admin/vercel/config |
Admin | Read preconfigured Vercel creds |
| GET | /admin/config |
Admin | Read sanitized config |
| POST | /admin/config |
Admin | Update config |
| GET | /admin/settings |
Admin | Read runtime settings |
| PUT | /admin/settings |
Admin | Update runtime settings (hot reload) |
| POST | /admin/settings/password |
Admin | Update admin password and invalidate old JWTs |
| GET | /admin/skills |
Admin | Read the skill library config (global switch, truncation limit, items) |
| PUT | /admin/skills |
Admin | Replace the whole skill library config (400 on validation failure) |
| POST | /admin/config/import |
Admin | Import config (merge/replace) |
| GET | /admin/config/export |
Admin | Export full config (config/json/base64) |
| POST | /admin/keys |
Admin | Add API key (optional name/remark) |
| PUT | /admin/keys/{key} |
Admin | Update API key metadata |
| DELETE | /admin/keys/{key} |
Admin | Delete API key |
| GET | /admin/proxies |
Admin | List proxies |
| POST | /admin/proxies |
Admin | Add proxy |
| PUT | /admin/proxies/{proxyID} |
Admin | Update proxy (empty password keeps old secret) |
| DELETE | /admin/proxies/{proxyID} |
Admin | Delete proxy (auto-unbind referenced accounts) |
| POST | /admin/proxies/test |
Admin | Test proxy connectivity |
| GET | /admin/accounts |
Admin | Paginated account list |
| POST | /admin/accounts |
Admin | Add account |
| PUT | /admin/accounts/{identifier} |
Admin | Update account name/remark |
| DELETE | /admin/accounts/{identifier} |
Admin | Delete account |
| PUT | /admin/accounts/{identifier}/proxy |
Admin | Bind/unbind proxy for an account |
| GET | /admin/queue/status |
Admin | Account queue status |
| POST | /admin/accounts/test |
Admin | Test one account |
| POST | /admin/accounts/test-all |
Admin | Test all accounts |
| POST | /admin/accounts/sessions/delete-all |
Admin | Delete all sessions for one account |
| POST | /admin/import |
Admin | Batch import keys/accounts |
| POST | /admin/test |
Admin | Test API through service |
| POST | /admin/dev/raw-samples/capture |
Admin | Fire one request and persist it as a raw sample |
| GET | /admin/dev/raw-samples/query |
Admin | Search current in-memory capture chains by prompt keyword |
| POST | /admin/dev/raw-samples/save |
Admin | Persist a selected in-memory capture chain as a raw sample |
| POST | /admin/vercel/sync |
Admin | Sync config to Vercel |
| GET | /admin/vercel/status |
Admin | Vercel sync status |
| POST | /admin/vercel/status |
Admin | Vercel sync status / draft compare |
| GET | /admin/export |
Admin | Export config JSON/Base64 |
| GET | /admin/dev/captures |
Admin | Read local packet-capture entries |
| DELETE | /admin/dev/captures |
Admin | Clear local packet-capture entries |
| GET | /admin/chat-history |
Admin | Read server-side conversation history (filters by caller/status/surface/keyword plus paging) |
| DELETE | /admin/chat-history |
Admin | Clear server-side conversation history |
| GET | /admin/chat-history/{id} |
Admin | Read one server-side conversation entry |
| DELETE | /admin/chat-history/{id} |
Admin | Delete one server-side conversation entry |
| PUT | /admin/chat-history/settings |
Admin | Update conversation history retention limit (0..500) |
| GET | /admin/chat-history/export |
Admin | Export conversation history with the same filters (json / markdown) |
| GET | /admin/version |
Admin | Check current version and latest Release |
OpenAI /v1/* paths are canonical. For clients configured with the bare DS2API service URL, the same OpenAI handlers are also exposed through root shortcuts: /models, /models/{id}, /chat/completions, /responses, /responses/{response_id}, /embeddings, /files, and /files/{file_id}.
Server-side records are essentially an archive of DeepSeek upstream responses: OpenAI Chat, OpenAI Responses, Claude Messages, and Gemini GenerateContent write an entry as soon as the upstream response arrives, before protocol translation/trimming. The list is ordered by request creation time (newest first), and streaming requests refresh status and detail while generating. Requests issued by the WebUI "API test" panel also land there. Each entry carries caller_id (the calling API key) plus caller_label (key name/remark/masked value), and the filterable surface, status, and status_code fields; list, detail, and export all read the same data (see conversation history).
Health Endpoints
GET /healthz
{"status": "ok"}
GET /readyz
{"status": "ready"}
OpenAI-Compatible API
GET /v1/models
No auth required. Returns the currently supported DeepSeek native model list.
Response:
{
"object": "list",
"data": [
{"id": "deepseek-v4-flash", "object": "model", "created": 1677610602, "owned_by": "deepseek", "permission": []},
{"id": "deepseek-v4-flash-nothinking", "object": "model", "created": 1677610602, "owned_by": "deepseek", "permission": []},
{"id": "deepseek-v4-flash-search", "object": "model", "created": 1677610602, "owned_by": "deepseek", "permission": []},
{"id": "deepseek-v4-flash-search-nothinking", "object": "model", "created": 1677610602, "owned_by": "deepseek", "permission": []}
]
}
Note:
/v1/modelsreturns normalized DeepSeek native model IDs. Common aliases are accepted only as request input and are not expanded as separate items in this endpoint. A-nothinkingsuffix means thinking/reasoning is always off, regardless of what the request asks for.The model line has collapsed to the four flash variants above:
deepseek-v4-pro/deepseek-v4-pro-search/deepseek-v4-visionand their-nothinkingforms are retired and no longer appear in this endpoint, Ollama/api/tags, or/anthropic/v1/models. Old names still work as request input and are silently mapped to the matching flash model (see the next section).
Model Alias Resolution
For chat / responses / embeddings, DS2API follows a wide-input/strict-output policy:
- Match DeepSeek native model IDs first.
- Then match exact keys in
model_aliases. - If the request name ends with
-nothinking, resolve the base alias and append the corresponding no-thinking variant. - If still unmatched, return
invalid_request_error. Unknown model families are not guessed heuristically; add explicit compatibility names throughmodel_aliases.
Built-in aliases come from internal/config/models.go; config.model_aliases can override or add mappings at runtime. Excerpt (all pointing at the flash models):
- OpenAI / Codex:
gpt-4o,gpt-4.1,gpt-5,gpt-5.5,gpt-5-codex,gpt-5.3-codex,codex-mini-latest→deepseek-v4-flash - OpenAI reasoning:
o1,o3,o4-mini→deepseek-v4-flash;o3-deep-research,o4-mini-deep-research→deepseek-v4-flash-search - Claude:
claude-opus-4-6,claude-sonnet-4-6,claude-haiku-4-5,claude-3-5-sonnet-latest→deepseek-v4-flash - Gemini:
gemini-2.5-pro,gemini-2.5-flash,gemini-3.1-pro,gemini-3-pro,gemini-3-flash,gemini-3.1-flash-lite,gemini-pro-vision→deepseek-v4-flash - Other exact built-in aliases:
llama-3.1-70b-instruct,qwen-max→deepseek-v4-flash
Aliases with a -nothinking suffix also map to the corresponding forced no-thinking DeepSeek model.
Retired DeepSeek model names keep old clients working; they are normalized silently at parse time:
| Requested model | Resolved to |
|---|---|
deepseek-v4-pro, deepseek-v4-vision |
deepseek-v4-flash |
deepseek-v4-pro-search |
deepseek-v4-flash-search |
deepseek-v4-pro-nothinking, deepseek-v4-vision-nothinking |
deepseek-v4-flash-nothinking |
deepseek-v4-pro-search-nothinking |
deepseek-v4-flash-search-nothinking |
The upstream model_type is now only default (a legacy expert / vision value is treated as default), so there is no separate vision or expert lane.
Retired historical families such as claude-1.*, claude-2.*, claude-instant-*, and gpt-3.5* are explicitly rejected.
Thinking and search resolution
The same rules apply to /v1/chat/completions, /v1/responses, /anthropic/v1/messages, and Gemini generateContent; they live in internal/util/thinking.go and internal/util/search.go, not in the protocol adapters.
Thinking, lowest priority first:
| Order | Source | Notes |
|---|---|---|
| 1 | Model default | -nothinking variants default off; other flash models default on |
| 2 | Explicit client override | Any matching field wins |
| 3 | -nothinking model |
Forces thinking off and outranks the client; a client cannot turn it back on |
Client override fields (the same keys inside extra_body are read too):
| Field | Accepted forms |
|---|---|
thinking |
bool; "enabled" / "disabled" (also on / off / true / false / none); {"type": ...} |
reasoning |
{"effort": ...}, {"type": ...}, {"enabled": ...}; a plain string is parsed as an effort value |
reasoning_effort |
minimal / low / medium / high / xhigh / max = on; none / disabled / disable / off / false = off |
Clients such as Kelivo translate their "thinking intensity" UI into reasoning_effort: omitting it follows the default (thinking on), sending none turns thinking off. DS2API only models on/off; the intensity tier itself is not forwarded.
Search, resolved as "model default → explicit client signal wins":
| Signal | Notes |
|---|---|
enable_search |
bool, or strings such as "true" / "false" / "on" / "off" / "enabled" / "disabled" |
web_search_options |
Presence alone counts as on |
search_options |
Presence alone counts as on |
Native search tool in tools |
type starting with web_search, type=google_search, or a tool object carrying googleSearch / google_search / web_search |
-search variants enable native search by default; an explicit enable_search=false turns it off for that request. Ordinary function tools (for example Kelivo's own search_web) are not search signals, so upstream search stays off by default — pick a -search model when you need it. Once a client starts sending the native signals above, DS2API follows automatically with no configuration change.
POST /v1/chat/completions
Path note: besides the canonical
/v1/chat/completions, DS2API also accepts the root shortcut/chat/completions. On Vercel Runtime,vercel.jsonrewrites only the canonical/v1/chat/completionspath to the Node streaming bridge; the root shortcut stays on the Go primary path. Use/v1/chat/completionson Vercel when real-time streaming is required.
Headers:
Authorization: Bearer your-api-key
Content-Type: application/json
Request body:
| Field | Type | Required | Notes |
|---|---|---|---|
model |
string | ✅ | DeepSeek native models + common aliases (gpt-5.5, gpt-5.4-mini, gpt-5.3-codex, o3, claude-opus-4-6, gemini-2.5-pro, gemini-3.1-pro, gemini-3-flash, etc.); -nothinking suffixes force thinking / reasoning off |
messages |
array | ✅ | OpenAI-style messages |
stream |
boolean | ❌ | Default false |
tools |
array | ❌ | Function calling schema |
thinking |
bool / string / object | ❌ | Explicit thinking switch that overrides the model default; ignored by -nothinking models |
reasoning / reasoning_effort |
object / string | ❌ | Alternative spelling of thinking; reasoning_effort=none turns thinking off, minimal..max turns it on |
enable_search |
bool / string | ❌ | Explicit search switch that overrides the model default (e.g. turn search off on a -search model) |
temperature, etc. |
any | ❌ | Accepted but final behavior depends on upstream |
Full precedence for thinking and search: Thinking and search resolution.
Non-Stream Response
{
"id": "<chat_session_id>",
"object": "chat.completion",
"created": 1738400000,
"model": "deepseek-v4-flash",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "final response",
"reasoning_content": "reasoning trace (when thinking is enabled)"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 20,
"total_tokens": 30,
"completion_tokens_details": {
"reasoning_tokens": 5
}
}
}
Streaming (stream=true)
SSE format: each frame is data: <json>\n\n, terminated by data: [DONE].
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"},"index":0}]}
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"reasoning_content":"..."},"index":0}]}
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"content":"..."},"index":0}]}
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{},"index":0,"finish_reason":"stop"}],"usage":{...}}
data: [DONE]
Field notes:
- First delta includes
role: assistant - When thinking is enabled, the stream may emit
delta.reasoning_content - Text emits
delta.content - Last chunk includes
finish_reasonandusage - Token counting prefers pass-through from upstream DeepSeek SSE (
accumulated_token_usage/token_usage), and only falls back to local estimation when upstream usage is absent. Failed/interrupted endings (for exampleresponse.failed) may not includeusage
Tool Calls
If
tools.enabled=false(the default), thetoolsin the request is ignored and the response never containstool_calls. The behavior below only applies once tool calling is enabled; see docs/prompt-compatibility.md for the risk.
When tools is present, DS2API performs anti-leak handling:
Non-stream: If detected, returns message.tool_calls, finish_reason=tool_calls, message.content=null.
{
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_xxx",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"beijing\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
]
}
Stream: Once high-confidence toolcall features are matched, DS2API emits delta.tool_calls immediately (without waiting for full argument closure), then keeps sending argument deltas; confirmed tool-call fragments are not forwarded as delta.content.
Additional notes:
- The parser treats the recommended halfwidth-pipe DSML shell tool blocks (
<|DSML|tool_calls>/<|DSML|invoke name="...">/<|DSML|parameter name="...">), DSML wrapper aliases (<dsml|tool_calls>,<|tool_calls>), common DSML separator drift (<|DSML tool_calls>/<|DSML invoke>/<|DSML parameter>), collapsed DSML local names (<DSMLtool_calls>/<DSMLinvoke>/<DSMLparameter>), control-separator drift (<DSML␂tool_calls>/ raw STX\x02), CJK angle bracket, fullwidth-bang / ideographic-comma separator drift, PascalCase local-name drift, and trailing attribute separator drift (<DSM|parameter name="command"|>...〈/DSM|parameter〉/<!DSML!invoke name=“Bash”>/<、DSML、tool_calls>/<DSmartToolCalls>/<DSMLtool_calls※>), arbitrary protocol prefixes (<proto💥tool_calls>), and legacy canonical XML tool blocks (<tool_calls>/<invoke name="...">/<parameter name="...">) as executable tool calls. These shells normalize non-structural separators back to XML first, while internal parsing remains XML-based; CDATA opener drift such as<![CDATA[/<、[CDATA[is also normalized for parameter bodies. Legacy<tools>,<tool_call>,<tool_name>,<param>,<function_call>,tool_use, antml variants, and standalone JSONtool_callspayloads are treated as plain text; complete but malformed wrappers are also released as plain text. - The parser no longer drops tool calls solely because parameter values are empty; explicit empty strings or whitespace-only parameters become empty strings in structured
tool_calls. Prompting still tells the model not to emit blank parameters, and missing/empty argument rejection belongs in the tool executor or client schema validation. - If the final visible response text is empty but the reasoning stream contains an executable tool call, Chat / Responses emits a standard OpenAI
tool_calls/function_calloutput during finalization. If thinking/reasoning was not enabled by the client, that reasoning text is used only for detection and is not exposed as visible text orreasoning_content. tool_callsshown inside fenced markdown code blocks (for example,json ...) are treated as examples, not executable calls.
GET /v1/models/{id}
No auth required. Alias values are accepted as path params (for example gpt-4o), and the returned object is the mapped DeepSeek model.
POST /v1/responses
OpenAI Responses-style endpoint, accepting either input or messages.
| Field | Type | Required | Notes |
|---|---|---|---|
model |
string | ✅ | Supports native models + alias mapping |
input |
string/array/object | ❌ | One of input or messages is required |
messages |
array | ❌ | One of input or messages is required |
instructions |
string | ❌ | Prepended as a system message |
stream |
boolean | ❌ | Default false |
tools |
array | ❌ | Same tool detection/translation policy as chat |
tool_choice |
string/object | ❌ | Supports auto/none/required and forced function selection ({"type":"function","name":"..."}) |
Non-stream: Returns a standard response object with an ID like resp_xxx, and stores it in in-memory TTL cache.
If tool_choice=required and no valid tool call is produced, DS2API returns HTTP 422 (error.code=tool_choice_violation).
Stream (SSE): minimal event sequence:
event: response.created
data: {"type":"response.created","id":"resp_xxx","status":"in_progress",...}
event: response.output_item.added
data: {"type":"response.output_item.added","response_id":"resp_xxx","item":{"type":"message|function_call",...},...}
event: response.content_part.added
data: {"type":"response.content_part.added","response_id":"resp_xxx","part":{"type":"output_text",...},...}
event: response.output_text.delta
data: {"type":"response.output_text.delta","response_id":"resp_xxx","item_id":"msg_xxx","output_index":0,"content_index":0,"delta":"..."}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","response_id":"resp_xxx","call_id":"call_xxx","delta":"..."}
event: response.function_call_arguments.done
data: {"type":"response.function_call_arguments.done","response_id":"resp_xxx","call_id":"call_xxx","name":"tool","arguments":"{...}"}
event: response.content_part.done
data: {"type":"response.content_part.done","response_id":"resp_xxx",...}
event: response.output_item.done
data: {"type":"response.output_item.done","response_id":"resp_xxx","item":{"type":"message|function_call",...},...}
event: response.completed
data: {"type":"response.completed","response":{...}}
data: [DONE]
If tool_choice=required is violated in stream mode, DS2API emits response.failed then [DONE] (no response.completed).
Current behavior: the parser tries to extract structured tool calls and does not enforce a hard allow-list reject; your tool executor should still validate against a whitelist before executing.
GET /v1/responses/{response_id}
Business auth required. Fetches cached responses created by POST /v1/responses (caller-scoped; only the same key/token can read).
Backed by in-memory TTL store. Default TTL is
900s(configurable viaresponses.store_ttl_seconds).
POST /v1/embeddings
Business auth required. Returns OpenAI-compatible embeddings shape.
| Field | Type | Required | Notes |
|---|---|---|---|
model |
string | ✅ | Supports native models + alias mapping |
input |
string/array | ✅ | Supports string, string array, token array |
Requires
embeddings.provider. Current supported values:mock/deterministic/builtin(all three use the same local deterministic implementation). If missing/unsupported, returns standard error shape with HTTP 501.
POST /v1/files
Business auth required. OpenAI Files-compatible upload endpoint; currently only multipart/form-data is supported.
| Field | Type | Required | Notes |
|---|---|---|---|
file |
file | ✅ | Binary payload |
purpose |
string | ❌ | Forwarded purpose field |
Constraints and behavior:
Content-Typemust bemultipart/form-data(otherwise400).- Total request size limit is 100 MiB (over-limit returns
413). - Success returns an OpenAI
fileobject (id/object/bytes/filename/purpose/status, etc.) and includesaccount_idfor source-account tracing.
GET /v1/files/{file_id}
Business auth required. Retrieves the current DeepSeek upload status for a file and returns an OpenAI file object. Returns 404 when no matching file is found.
Claude-Compatible API
Besides /anthropic/v1/*, DS2API also supports shortcut paths: /v1/messages, /messages, /v1/messages/count_tokens, /messages/count_tokens.
Implementation-wise this path is unified on the OpenAI Chat Completions parse-and-translate pipeline to avoid maintaining divergent parsing chains.
GET /anthropic/v1/models
No auth required.
Response:
{
"object": "list",
"data": [
{"id": "claude-sonnet-4-6", "object": "model", "created": 1715635200, "owned_by": "anthropic"},
{"id": "claude-haiku-4-5", "object": "model", "created": 1715635200, "owned_by": "anthropic"},
{"id": "claude-opus-4-6", "object": "model", "created": 1715635200, "owned_by": "anthropic"}
],
"first_id": "claude-opus-4-6",
"last_id": "claude-3-haiku-20240307",
"has_more": false
}
Note: the example is partial; besides the current primary aliases, the real response also includes Claude 4.x snapshots plus historical 3.x IDs and common aliases.
POST /anthropic/v1/messages
Headers:
x-api-key: your-api-key
Content-Type: application/json
anthropic-version: 2023-06-01
anthropic-versionis optional; DS2API auto-fills2023-06-01when absent.
Request body:
| Field | Type | Required | Notes |
|---|---|---|---|
model |
string | ✅ | For example claude-sonnet-4-6 / claude-opus-4-6 / claude-haiku-4-5 (compatible with claude-3-5-haiku-latest), plus historical Claude model IDs |
messages |
array | ✅ | Claude-style messages |
max_tokens |
number | ❌ | Auto-filled to 8192 when omitted; not strictly enforced by upstream bridge |
stream |
boolean | ❌ | Default false |
system |
string | ❌ | Optional system prompt |
tools |
array | ❌ | Claude tool schema |
thinking |
object | ❌ | Anthropic thinking config; translated into downstream reasoning control, and ignored by -nothinking models |
temperature |
number | ❌ | Passed through to the downstream bridge; if temperature and top_p are both present, temperature wins |
top_p |
number | ❌ | Passed through when temperature is absent |
stop_sequences |
array | ❌ | Passed through as downstream stop sequences |
tool_choice |
string/object | ❌ | Supports auto / none / required / {"type":"function","name":"..."} and is translated to downstream tool choice |
Note:
thinking,temperature,top_p,stop_sequences, andtool_choiceare translated through the compatibility bridge. Final behavior still depends on the selected model and upstream support. When bothtemperatureandtop_pare present,temperaturetakes precedence. Thinking and search precedence is identical to the OpenAI endpoints; see Thinking and search resolution. Iftools.enabled=false(the default), thetoolsin the request is ignored and the response never containstool_calls(atool_useblock on the Claude side).
Non-Stream Response
{
"id": "msg_1738400000000000000",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-6",
"content": [
{"type": "text", "text": "response"}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 12,
"output_tokens": 34
}
}
If tool use is detected, stop_reason becomes tool_use and content contains tool_use blocks.
Streaming (stream=true)
SSE uses paired event: + data: lines. Event type is also in JSON type.
event: message_start
data: {"type":"message_start","message":{...}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hello"}}
event: ping
data: {"type":"ping"}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":12}}
event: message_stop
data: {"type":"message_stop"}
Notes:
- Models that support thinking emit
thinkingblocks /thinking_deltaby default; explicit thinking disablement or-nothinkingmodels suppress them signature_deltais not emitted (DeepSeek does not provide verifiable thinking signatures)- In
toolsmode, the stream avoids leaking raw tool JSON and does not forceinput_json_delta
POST /anthropic/v1/messages/count_tokens
Request:
{
"model": "claude-sonnet-4-6",
"messages": [
{"role": "user", "content": "Hello"}
]
}
Response:
{
"input_tokens": 5
}
Gemini-Compatible API
Supported paths:
/v1beta/models/{model}:generateContent/v1beta/models/{model}:streamGenerateContent/v1/models/{model}:generateContent(compat path)/v1/models/{model}:streamGenerateContent(compat path)
Authentication is the same as other business routes (Authorization: Bearer <token> or x-api-key).
Implementation-wise this path is unified on the OpenAI Chat Completions parse-and-translate pipeline to avoid maintaining divergent parsing chains.
POST /v1beta/models/{model}:generateContent
Request body accepts Gemini-style contents / tools. Model names can use aliases and are mapped to DeepSeek models; a -nothinking suffix in the path model name resolves to the matching forced no-thinking model. Thinking and search precedence is identical to the OpenAI endpoints; see Thinking and search resolution. If tools.enabled=false (the default), the tools in the request is ignored and the response never contains tool_calls (a functionCall part on the Gemini side).
Response uses Gemini-compatible fields, including:
candidates[].content.parts[].textcandidates[].content.parts[].thought=truefor thinking outputcandidates[].content.parts[].functionCall(when tool call is produced)usageMetadata(promptTokenCount/candidatesTokenCount/totalTokenCount)
POST /v1beta/models/{model}:streamGenerateContent
Returns SSE (text/event-stream), each chunk as data: <json>:
- regular text: incremental text chunks
- thinking: incremental chunks with
parts[].thought=true toolsmode: buffered and emitted asfunctionCallat finalize phase- final chunk: includes
finishReason: "STOP"andusageMetadata - Token counting prefers pass-through from upstream DeepSeek SSE (
accumulated_token_usage/token_usage), and only falls back to local estimation when upstream usage is absent
Ollama API
POST /api/showrequest body:{"model":"<model-id>"}.GET /api/tagslists only the four flash model IDs (deepseek-v4-flash,deepseek-v4-flash-search, plus their-nothinkingvariants), matching/v1/models. Retired pro / vision names no longer appear, but they are still accepted as amodelinput.- Response uses lowercase
id(notID) and includescapabilitiesfor Ollama-style clients and strict schemas.
Example response:
{
"id": "deepseek-v4-flash",
"capabilities": ["tools", "thinking"]
}
Admin API
POST /admin/login
Public endpoint.
Request:
{
"admin_key": "admin",
"expire_hours": 24
}
expire_hours is optional, default 24.
Response:
{
"success": true,
"token": "<jwt>",
"expires_in": 86400
}
GET /admin/verify
Requires JWT: Authorization: Bearer <jwt>
Response:
{
"valid": true,
"expires_at": 1738400000,
"remaining_seconds": 72000
}
GET /admin/vercel/config
Returns Vercel preconfiguration status. Environment variables are preferred, then the saved vercel config block is used as a fallback.
{
"has_token": true,
"token_preview": "vc****en",
"token_source": "config",
"project_id": "prj_xxx",
"team_id": null
}
GET /admin/config
Returns sanitized config, including both keys and api_keys.
{
"keys": ["k1", "k2"],
"api_keys": [
{"key": "k1", "name": "Primary", "remark": "Production"},
{"key": "k2", "name": "Backup", "remark": "Load test"}
],
"env_backed": false,
"env_source_present": true,
"env_writeback_enabled": true,
"config_path": "/data/config.json",
"vercel": {
"has_token": true,
"token_preview": "vc****en",
"project_id": "prj_xxx",
"team_id": ""
},
"accounts": [
{
"identifier": "user@example.com",
"email": "user@example.com",
"mobile": "",
"has_password": true,
"has_token": true,
"token_preview": "abcde..."
}
],
"model_aliases": {
"claude-sonnet-4-6": "deepseek-v4-flash",
"claude-opus-4-6": "deepseek-v4-flash"
}
}
POST /admin/config
Only updates keys, api_keys, accounts, and model_aliases.
If both api_keys and keys are sent, the structured api_keys entries win so name / remark metadata is preserved; keys remains a legacy fallback.
Request:
{
"keys": ["k1", "k2"],
"api_keys": [
{"key": "k1", "name": "Primary", "remark": "Production"},
{"key": "k2", "name": "Backup", "remark": "Load test"}
],
"accounts": [
{"email": "user@example.com", "password": "pwd", "token": ""}
],
"model_aliases": {
"claude-sonnet-4-6": "deepseek-v4-flash",
"claude-opus-4-6": "deepseek-v4-flash"
}
}
api_keys[].thinking_injection (per-key override for thinking-format injection)
Every api_keys entry may carry thinking_injection to give that single API key its own injection prompt; the legacy keys form does not support the field.
{
"api_keys": [
{
"key": "k1",
"name": "Primary",
"remark": "Production",
"thinking_injection": {
"custom": true,
"enabled": true,
"prompt": "List the known constraints before answering, then give the conclusion."
}
},
{"key": "k2", "name": "Backup"}
]
}
| Field | Notes |
|---|---|
custom |
Defaults to false. When false (or the whole block is omitted) the key fully follows the global thinking_injection |
enabled |
Only used when custom=true; omitting it means "keep following the global switch" |
prompt |
Only used when custom=true; empty or whitespace-only means "keep following the global prompt" |
For the global switch and default prompt see thinking_injection under GET /admin/settings; for the injection position and the built-in default prompt text see docs/prompt-compatibility.md.
api_keys[].prompt_profile (per-key prompt profile override)
Every api_keys entry may carry prompt_profile to pick a preset or body for the prompt-profile layer, next to thinking_injection and skills; the legacy keys form does not support the field. The field currently takes effect through the config file / import-export only — the admin panel exposes the global section.
{
"api_keys": [
{
"key": "k1",
"name": "Writing key",
"prompt_profile": {"custom": true, "enabled": true, "name": "writing"}
},
{"key": "k2", "name": "Default key"}
]
}
| Field | Notes |
|---|---|
custom |
Defaults to false; when false or omitted the key fully follows the global prompt_profile |
enabled |
Only used when custom=true; false means this key injects no profile |
name |
Only used when custom=true; default / writing, overrides the global preset when non-empty |
prompt |
Only used when custom=true; overrides the preset body when non-empty (max 20000 characters) |
When the global prompt_profile.enabled is false, none of the above applies to any key.
prompt_style (built-in prompt style, global)
A top-level string. It only switches the built-in texts; anything you configure yourself is unaffected.
{"prompt_style": "legacy"}
| Value | Behaviour |
|---|---|
v2 (default, empty string also means this) |
After a history split the continuation prompt carries an "execution requirements" block (honour earlier requirements item by item) and the built-in execution contract (thinking injection) is injected |
legacy |
Continuation goes back to the short pre-ce5a019 wording and the built-in contract is not injected; a thinking_injection.prompt you configured explicitly is still injected |
An unknown value never breaks config loading: it falls back to v2 with a warning. PUT /admin/settings rejects unknown values with 400.
model_profiles (custom model list, global)
Expose several public model names on top of the same flash base model, each carrying its own prompt. Whichever name the client sends decides the prompt; entries never affect each other.
{
"model_profiles": [
{
"id": "deepseek-writer",
"base": "deepseek-v4-flash",
"name": "Writing",
"remark": "Long-form and rewrites",
"prompt": "## Writing rules\n…"
}
]
}
| Field | Notes |
|---|---|
id |
Required public model name. Charset [A-Za-z0-9._-], 1-64 characters, must not collide with a built-in model name (e.g. deepseek-v4-flash), unique across entries (case-insensitive) |
base |
Required base model: deepseek-v4-flash / -search / -nothinking / -search-nothinking |
name / remark |
Optional, only for readability in the panel and in exports |
prompt |
Optional prompt that applies to this model only, max 20000 characters; empty adds no extra spec |
Resolution and injection:
- Model resolution prefers custom entries over built-in aliases: a matching
idcalls upstream with itsbase(/v1/models,/v1/models/{id},/v1/chat/completions, Claude and Gemini entries all share the same resolver). /v1/modelslists the custom models too, withowned_by=ds2api.- The prompt is injected at the same point as
prompt_profile(after history splitting, before the skill layer) and the two are mutually exclusive: a non-empty modelpromptwins, otherwise the globalprompt_profileapplies. - At most 50 entries. See
GET/PUT /admin/model_profiles.
api_keys[].skills (per-key skill injection)
Every api_keys entry may carry skills to choose which skills that single API key injects. It sits next to thinking_injection; the legacy keys form does not support the field.
{
"api_keys": [
{"key": "k1", "name": "Primary", "skills": {"custom": true, "enabled": true, "ids": ["sql-report"]}},
{"key": "k2", "name": "Backup"}
]
}
| Field | Notes |
|---|---|
custom |
Defaults to false; when false or omitted the key uses every entry in skills.items |
enabled |
Only used when custom=true; false means this key injects nothing |
ids |
Only used when custom=true; non-empty means only the matching skills, empty means this key injects nothing |
custom |
enabled |
ids |
Result |
|---|---|---|---|
omitted or false |
any | any | Uses all items |
true |
false |
any | Nothing injected |
true |
omitted or true |
non-empty | Only the matching items |
true |
omitted or true |
empty | Nothing injected |
When the global skills.enabled=false, the whole table is moot and no key injects anything. For skill bodies and the admin endpoint see GET / PUT /admin/skills.
GET /admin/settings
Reads runtime settings and status, including:
successadmin(has_password_hash,jwt_expire_hours,jwt_valid_after_unix,default_password_warning)runtime(account_max_inflight,account_max_queue,global_max_inflight,token_refresh_interval_hours)runtime.token_refresh_interval_hoursis a session health-check interval: on expiry the gateway performs a password-free check (GET /api/v0/users/current) and only falls back to a password login when the server explicitly rejects the sessionconfig_snapshot: raw config values (admin/runtime/responses/embeddings/auto_delete/current_input_file/thinking_injection/prompt_profile/model_aliases) before env overridesresponses/embeddingsauto_delete(mode:none/single/all; legacysessions=trueis still treated asall)current_input_file(enableddefaults totrue, plusmin_chars)thinking_injection(enableddefaults totrue, pluspromptanddefault_prompt); an emptypromptuses the built-in default, and a single key can override it throughapi_keys[].thinking_injectionprompt_profile(enableddefaults tofalse,enabled_defaultalwaysfalse, plusname,promptandpresets);presetsis the built-in[{name, body}]list, so the panel can show the body a blank input falls back toprompt_style(mode: effective stylev2/legacy;modes: allowed values;default:v2;value_raw: the value stored in config)model_profiles(count,max_count,base_models,max_id_chars,max_prompt_chars); the entries themselves are managed through/admin/model_profilesmodel_aliasestools(enabled: master switch for tool calling, defaults tofalse;default_enabled: alwaysfalse, used by the panel to show the default)env_backed,needs_vercel_synctoolcallpolicy is fixed tofeature_match + highand is no longer returned or editable via settings
PUT /admin/settings
Hot-updates runtime settings. Supported fields:
admin.jwt_expire_hoursruntime.account_max_inflight/runtime.account_max_queue/runtime.global_max_inflight/runtime.token_refresh_interval_hoursresponses.store_ttl_secondsembeddings.providerauto_delete.modecurrent_input_file.enabled/current_input_file.min_charsthinking_injection.enabled/thinking_injection.promptprompt_profile.enabled/prompt_profile.name/prompt_profile.promptprompt_styletools.enabledmodel_aliasestoolcallpolicy is fixed and is no longer writable through settings
prompt_profile is the new prompt-profile layer and is off by default; name accepts only default / writing (anything else returns 400) and prompt is capped at 20000 characters (over the cap returns 400). Sending only some fields leaves the rest untouched:
{"prompt_profile": {"enabled": true, "name": "writing"}}
For the injection position, layering order and preset bodies see docs/prompt-compatibility.md and docs/prompt-design.md.
prompt_style is a top-level string (not an object); it accepts v2 / legacy / an empty string (empty means back to v2):
{"prompt_style": "legacy"}
The custom model list does not go through settings — use /admin/model_profiles below, which replaces the whole list.
tools.enabled only changes the field you actually send; the existing semantics of every other setting stay the same:
{"tools": {"enabled": true}}
When it is off (the default), requests that carry tools are handled as plain chat. DSML tool calling can trip upstream risk control and get an account banned, so enable it deliberately; the risk notes live in docs/prompt-compatibility.md.
POST /admin/settings/password
Updates admin password and invalidates existing JWTs.
Request example:
{"new_password":"your-new-password"}
It also accepts {"password":"your-new-password"}.
GET / PUT /admin/skills
A plain-text skill library, experimental and off by default (skills.enabled=false). The server concatenates skill bodies into the prompt, so the model follows a fixed routine without ever emitting a tool call.
- Global config:
skills.enabled(defaults tofalse),skills.max_chars(defaults to8000),skills.items - Per-key override:
api_keys[].skills, semantics inapi_keys[].skillsabove - Injection shape, injection order, and "why plain text instead of tool calling": the skills section of docs/prompt-compatibility.md (Chinese)
GET response:
{
"enabled": false,
"max_chars": 8000,
"items": [
{
"id": "sql-report",
"name": "SQL report",
"description": "Write SQL against a fixed metric definition",
"body": "# Skill body markdown\n..."
}
]
}
PUT request: same shape as GET; it replaces the whole library, no incremental merge.
{
"enabled": true,
"max_chars": 8000,
"items": [
{
"id": "sql-report",
"name": "SQL report",
"description": "Write SQL against a fixed metric definition",
"body": "# Reporting rules\nAggregate by day first, then split by channel."
}
]
}
PUT success response:
{"success": true, "count": 1}
Validation rules (any violation returns 400 with {"detail": "..."}):
| Field | Rule |
|---|---|
items |
At most 50 entries |
id |
Required, unique, character set [a-z0-9_-] |
body |
Required; at most 100000 characters per entry and 500000 characters in total |
max_chars |
Defaults to 8000; truncates by character (rune) and appends (技能内容已截断) when truncated; 0 means unlimited |
Validation runs on write and is independent of enabled, so you can author and fix the content with the switch still off, then turn it on. Over-limit bodies, duplicate ids, and invalid ids are never silently fixed.
GET / PUT /admin/model_profiles
The custom model list (public name + base model + per-model prompt). Like the skill library it is a standalone endpoint with whole-list replacement semantics: the items you send become the final list, and an empty array clears it.
GET response:
{
"items": [
{"id": "deepseek-writer", "base": "deepseek-v4-flash", "name": "Writing", "remark": "long-form", "prompt": "## Writing rules\n…"}
],
"base_models": ["deepseek-v4-flash", "deepseek-v4-flash-search", "deepseek-v4-flash-nothinking", "deepseek-v4-flash-search-nothinking"],
"max_count": 50,
"max_id_chars": 64,
"max_prompt_chars": 20000
}
PUT request:
{"items": [{"id": "deepseek-writer", "base": "deepseek-v4-flash", "prompt": "…"}]}
Validation (any failure returns 400 with {"detail": "..."}):
| Field | Rule |
|---|---|
items |
At most 50 entries |
id |
Required, unique (case-insensitive), charset [A-Za-z0-9._-], 1-64 characters, must not shadow a built-in model name |
base |
Required, one of the four deepseek-v4-flash names |
prompt |
Optional, ≤ 20000 characters |
Saving takes effect immediately: /v1/models and model-name resolution pick up the new entries without a restart.
POST /admin/config/import
Imports full config with:
mode=merge(default)mode=replace
The request can send config directly, or wrapped as {"config": {...}, "mode":"merge"}.
Query params ?mode=merge / ?mode=replace are also supported.
replace mode replaces the full config shape while preserving Vercel sync metadata. merge mode merges keys, api_keys, accounts, and model_aliases, and overwrites non-empty fields under admin, runtime, responses, and embeddings. Manage auto_delete and current_input_file via /admin/settings or the config file; legacy compat and toolcall fields are ignored.
Note:
mergemode does not updateauto_deleteorcurrent_input_file.
GET /admin/config/export
Exports full config in three forms: config, json, and base64.
POST /admin/keys
{"key": "new-api-key", "name": "Primary", "remark": "Production"}
Response: {"success": true, "total_keys": 3}
PUT /admin/keys/{key}
Updates the name / remark of the specified API key. The path key is read-only and cannot be changed.
{"name": "Backup", "remark": "Load test"}
Response: {"success": true, "total_keys": 3}
DELETE /admin/keys/{key}
Response: {"success": true, "total_keys": 2}
GET /admin/proxies
Lists proxy configs (password is never returned; use has_password as a marker).
POST /admin/proxies
Adds a proxy. Request accepts id (optional; auto-generated when omitted), name, type (http / socks5), host, port, username, password.
PUT /admin/proxies/{proxyID}
Updates a proxy. If password is an empty string, the existing secret is preserved.
DELETE /admin/proxies/{proxyID}
Deletes a proxy and automatically clears proxy_id on all accounts that reference it.
POST /admin/proxies/test
Tests proxy connectivity: provide proxy_id to test a saved proxy; omit it to run a one-off test using proxy fields in the request body.
GET /admin/accounts
Query params:
| Param | Default | Range |
|---|---|---|
page |
1 |
≥ 1 |
page_size |
10 |
1–5000 |
q |
empty | Filter by identifier / email / mobile |
Response:
{
"items": [
{
"identifier": "user@example.com",
"email": "user@example.com",
"mobile": "",
"has_password": true,
"has_token": true,
"token_preview": "abc...",
"test_status": "ok"
}
],
"total": 25,
"page": 1,
"page_size": 10,
"total_pages": 3
}
Returned items also include test_status, usually ok or failed.
POST /admin/accounts
{"email": "user@example.com", "password": "pwd"}
Response: {"success": true, "total_accounts": 6}
PUT /admin/accounts/{identifier}
Updates the name / remark of the specified account. The path identifier can be email or mobile and cannot be changed.
{"name": "Primary account", "remark": "Shared with the team"}
Response: {"success": true, "total_accounts": 6}
DELETE /admin/accounts/{identifier}
identifier can be email, mobile, or the synthetic id for token-only accounts (token:<hash>).
Response: {"success": true, "total_accounts": 5}
PUT /admin/accounts/{identifier}/proxy
Updates proxy binding for a specific account.
- Request body:
{"proxy_id":"..."}. - Use empty
proxy_idto unbind proxy. identifiersupports email / mobile / token-only synthetic id.
GET /admin/queue/status
{
"available": 3,
"in_use": 1,
"total": 4,
"available_accounts": ["a@example.com"],
"in_use_accounts": ["b@example.com"],
"max_inflight_per_account": 2,
"global_max_inflight": 8,
"recommended_concurrency": 8,
"waiting": 0,
"max_queue_size": 8
}
| Field | Description |
|---|---|
available |
Accounts that still have spare inflight capacity |
in_use |
Number of occupied in-flight slots |
total |
Total accounts |
available_accounts |
List of account IDs with remaining inflight capacity |
in_use_accounts |
List of account IDs currently in use |
max_inflight_per_account |
Per-account inflight limit |
global_max_inflight |
Global inflight limit |
recommended_concurrency |
Suggested concurrency (total × max_inflight_per_account) |
waiting |
Number of queued requests currently waiting |
max_queue_size |
Waiting queue limit |
POST /admin/accounts/test
| Field | Required | Notes |
|---|---|---|
identifier |
✅ | email / mobile / token-only synthetic id |
model |
❌ | default deepseek-v4-flash |
message |
❌ | if empty, only session creation is tested |
Response:
{
"account": "user@example.com",
"success": true,
"response_time": 1240,
"message": "API test successful (session creation only)",
"model": "deepseek-v4-flash",
"session_count": 0,
"config_writable": true,
"config_warning": ""
}
If a message is provided, thinking may also be included when the upstream response carries reasoning text.
When the configured file path is not writable (for example, read-only /app/config.json inside some containers), login/session testing still proceeds; config_warning is returned to indicate token persistence failed and the token is memory-only until restart.
POST /admin/accounts/test-all
Optional request field: model.
{
"total": 5,
"success": 4,
"failed": 1,
"results": [...]
}
The internal concurrency limit is currently fixed at 5.
POST /admin/accounts/sessions/delete-all
Deletes all DeepSeek sessions for a specific account. Request example:
{"identifier":"user@example.com"}
Response:
{"success": true, "message": "删除成功"}
If the account is missing or deletion fails, success becomes false and message contains the error.
The current handler returns the Chinese literal 删除成功 on success.
POST /admin/import
Batch import keys and accounts.
Request:
{
"keys": ["k1", "k2"],
"accounts": [
{"email": "user@example.com", "password": "pwd", "token": ""}
]
}
Response:
{
"success": true,
"imported_keys": 2,
"imported_accounts": 1
}
POST /admin/test
Test API availability through the service itself.
| Field | Required | Default |
|---|---|---|
model |
❌ | deepseek-v4-flash |
message |
❌ | 你好 |
api_key |
❌ | First key in config |
Response:
{
"success": true,
"status_code": 200,
"response": {"id": "..."}
}
POST /admin/dev/raw-samples/capture
Internally issues one /v1/chat/completions request through the service, then persists the request metadata and raw upstream SSE into tests/raw_stream_samples/<sample-id>/.
Common request fields:
| Field | Required | Default | Notes |
|---|---|---|---|
message |
No | 你好 |
Convenience single-turn user message |
messages |
No | Auto-derived from message |
OpenAI-style message array |
model |
No | deepseek-v4-flash |
Target model |
stream |
No | true |
Recommended to keep streaming enabled so raw SSE is recorded |
api_key |
No | First configured key | Business API key to use |
sample_id |
No | Auto-generated | Sample directory name |
On success, the response headers include:
X-Ds2-Sample-IdX-Ds2-Sample-DirX-Ds2-Sample-MetaX-Ds2-Sample-Upstream
If the request itself succeeds but the process did not record a new upstream capture, the endpoint returns:
{"detail":"no upstream capture was recorded"}
GET /admin/dev/raw-samples/query
Searches the current process's in-memory capture entries and groups completion + continue rounds by chat_session_id.
Query parameters:
| Param | Default | Notes |
|---|---|---|
q |
empty | Fuzzy match against request/response text |
limit |
20 |
Max number of chains returned |
Response fields include:
items[].chain_keyitems[].capture_idsitems[].round_countitems[].initial_labelitems[].request_previewitems[].response_preview
POST /admin/dev/raw-samples/save
Persists one selected in-memory capture chain into tests/raw_stream_samples/<sample-id>/.
Any one of these selectors is accepted:
{"chain_key":"session:xxxx","sample_id":"tmp-from-memory"}
{"capture_id":"cap_xxx","sample_id":"tmp-from-memory"}
{"query":"Guangzhou weather","sample_id":"tmp-from-memory"}
The success payload includes sample_id, dir, meta_path, and upstream_path.
POST /admin/vercel/sync
| Field | Required | Notes |
|---|---|---|
vercel_token |
❌ | If empty or __USE_PRECONFIG__, read env, then saved config |
project_id |
❌ | Fallback: VERCEL_PROJECT_ID, then saved config |
team_id |
❌ | Fallback: VERCEL_TEAM_ID, then saved config |
auto_validate |
❌ | Default true |
save_credentials |
❌ | Default true; saves explicitly supplied Vercel credentials for the next sync |
config_override |
❌ | Provide a full config object to sync instead of the runtime config |
Success response:
{
"success": true,
"validated_accounts": 3,
"message": "Config synced, redeploying...",
"deployment_url": "https://..."
}
Or manual deploy required:
{
"success": true,
"validated_accounts": 3,
"message": "Config synced to Vercel, please trigger redeploy manually",
"manual_deploy_required": true
}
Failed account checks are returned in failed_accounts, and any saved Vercel credentials are returned in saved_credentials.
GET /admin/vercel/status
{
"synced": true,
"last_sync_time": 1738400000,
"has_synced_before": true,
"env_backed": false,
"config_hash": "....",
"last_synced_hash": "....",
"draft_hash": "....",
"draft_differs": false
}
POST /admin/vercel/status can also accept config_override to compare a draft config against the current synced config.
GET /admin/export
{
"json": "{...}",
"base64": "ey4uLn0="
}
This is the same payload as GET /admin/config/export, just with a shorter path.
GET /admin/version
Checks the current build version and the latest GitHub Release:
{
"success": true,
"current_version": "3.0.0",
"current_tag": "v3.0.0",
"source": "file:VERSION",
"checked_at": "2026-03-29T00:00:00Z",
"latest_tag": "v3.0.0",
"latest_version": "3.0.0",
"release_url": "https://github.com/CJackHwang/ds2api/releases/tag/v3.0.0",
"published_at": "2026-03-28T12:00:00Z",
"has_update": false
}
If GitHub API access fails, the response includes check_error while still returning HTTP 200.
GET /admin/dev/captures
Reads local packet-capture status and recent entries (Admin auth required):
enabledlimitmax_body_bytesitems
DELETE /admin/dev/captures
Clears packet-capture entries:
{"success":true,"detail":"capture logs cleared"}
GET /admin/chat-history
Reads server-side conversation history with filtering and paging.
Query parameters:
| Parameter | Default | Matching rule |
|---|---|---|
caller |
empty | Exact match on caller_id (use callers[].caller_id from the response, not the key name) |
status |
empty | Case-insensitive exact match: streaming / success / error / stopped |
surface |
empty | Case-insensitive exact match, e.g. openai.chat_completions, openai.responses, claude.messages, gemini.generate_content |
q |
empty | Case-insensitive substring match over user_input / preview / model / caller_label / error |
offset |
0 |
Paging offset; offset and limit both 0 means "no paging" |
limit |
0 |
Page size; 0 means "no limit" (the settings retention limit is not used as a page size) |
Invalid or negative offset / limit values are treated as 0.
Response:
{
"version": 2,
"limit": 20,
"revision": 128,
"total": 137,
"callers": [
{"caller_id": "caller:9f2c1a2b3c4d5e6f", "label": "Primary", "configured": true, "count": 96},
{"caller_id": "caller:41ab0c9d8e7f6a5b", "label": "Backup", "configured": true, "count": 0}
],
"items": [
{
"id": "chat_20260418_093012_ab12",
"status": "success",
"caller_id": "caller:9f2c1a2b3c4d5e6f",
"caller_label": "Primary",
"account_id": "user@example.com",
"surface": "openai.chat_completions",
"model": "deepseek-v4-flash",
"stream": true,
"user_input": "today's news",
"preview": "……",
"status_code": 200,
"elapsed_ms": 4210,
"finish_reason": "stop",
"created_at": 1777000000000,
"updated_at": 1777000004000,
"detail_revision": 3
}
],
"path": "/data/chat_history.json"
}
totalis the filtered count,itemsis the current page; without any filter or paging parameteritemsstill holds all summaries.caller_labelis a readable label for the calling API key (name / remark / masked value); keys without a remark fall back to the masked value so the panel never shows a bare hash.callersaggregates thecaller_ids found in records and adds keys that are configured but have no records yet (countis0,configured=true), sorted bycountdescending andlabelascending.- Timestamp fields (
created_at/updated_at/completed_at) are milliseconds. If-None-Match+ETagrevalidation (304) only applies when no filter or paging parameter is set, because a filtered response body changes with the query.
GET /admin/chat-history/{id}
Reads one full record as {"item": {...}}. Besides the summary fields it carries messages, history_text (the full conversation transcript uploaded by the history split), final_prompt, reasoning_content, content, error, usage, and file_infos (uploaded file IDs, audit result, source). ETag / 304 is supported here too.
DELETE /admin/chat-history
Clears every server-side record and returns {"success": true}.
DELETE /admin/chat-history/{id}
Deletes one record and returns {"success": true}; a missing record returns 404.
PUT /admin/chat-history/settings
Updates the retention limit. limit accepts any integer in 0..500; there are no fixed tiers anymore.
{"limit": 120}
| Value | Behavior |
|---|---|
0 |
Recording is off: no new records are written |
1..500 |
Keep the newest N records and evict the rest by creation time |
| Anything else | 400 with detail = unsupported chat history limit: <n> |
The response carries success, limit, revision, and the trimmed items.
GET /admin/chat-history/export
Exports records with the same filter and paging parameters as the list endpoint (caller / status / surface / q / offset / limit); the response is a file download.
| Parameter | Notes |
|---|---|
format=json (default) |
JSON attachment shaped as {exported_at, count, filters, items}; exported_at is RFC3339, filters echoes the applied filters, and items holds full records including large fields such as messages / final_prompt / history_text |
format=markdown |
Markdown attachment titled # ds2api 响应记录导出, one ## N. <record id> section per record, with time, surface, model, account, caller, status, elapsed time, finish reason, plus UserInput / FinalPrompt / ReasoningContent / Content / Error code blocks |
| Any other value | 400 with detail = unsupported export format: <format> |
The response also sets Content-Disposition: attachment (filename ds2api-logs-<YYYYMMDD-HHMMSS>.json|.md) and Cache-Control: no-store. If a record is deleted mid-export it is skipped, and count reflects what was actually exported.
Error Payloads
Compatible routes (/v1/*, /anthropic/*) use the same error envelope:
{
"error": {
"message": "...",
"type": "invalid_request_error",
"code": "invalid_request",
"param": null
}
}
Admin routes keep {"detail":"..."}.
Gemini routes use Google-style errors:
{
"error": {
"code": 400,
"message": "invalid json",
"status": "INVALID_ARGUMENT"
}
}
Clients should handle HTTP status code plus error / detail fields.
Common status codes:
| Code | Meaning |
|---|---|
401 |
Authentication failed (invalid key/token, or expired admin JWT) |
429 |
Too many requests (exceeded inflight + queue capacity, or upstream thinking-only output with no visible answer; managed-account mode first tries one alternate-account fresh retry; current responses do not include Retry-After) |
503 |
Model unavailable or upstream error |
cURL Examples
OpenAI Non-Stream
curl http://localhost:5001/v1/chat/completions \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash",
"messages": [{"role": "user", "content": "Hello"}],
"stream": false
}'
OpenAI Stream
curl http://localhost:5001/v1/chat/completions \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash",
"messages": [{"role": "user", "content": "Explain quantum entanglement"}],
"stream": true
}'
OpenAI Responses (Stream)
curl http://localhost:5001/v1/responses \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5-codex",
"input": "Write a hello world in golang",
"stream": true
}'
OpenAI Embeddings
curl http://localhost:5001/v1/embeddings \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"input": ["first text", "second text"]
}'
OpenAI with Search
curl http://localhost:5001/v1/chat/completions \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash-search",
"messages": [{"role": "user", "content": "Latest news today"}],
"stream": true
}'
OpenAI Tool Calling
curl http://localhost:5001/v1/chat/completions \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash",
"messages": [{"role": "user", "content": "What is the weather in Beijing?"}],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"}
},
"required": ["city"]
}
}
}
]
}'
Gemini Non-Stream
curl "http://localhost:5001/v1beta/models/gemini-2.5-pro:generateContent" \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"contents": [
{
"role": "user",
"parts": [{"text": "Introduce Go in three sentences"}]
}
]
}'
Gemini Stream
curl "http://localhost:5001/v1beta/models/gemini-2.5-flash:streamGenerateContent" \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"contents": [
{
"role": "user",
"parts": [{"text": "Write a short summary"}]
}
]
}'
Claude Non-Stream
curl http://localhost:5001/anthropic/v1/messages \
-H "x-api-key: your-api-key" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Hello"}]
}'
Claude Stream
curl http://localhost:5001/anthropic/v1/messages \
-H "x-api-key: your-api-key" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-opus-4-6",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Explain relativity"}],
"stream": true
}'
Admin Login
curl http://localhost:5001/admin/login \
-H "Content-Type: application/json" \
-d '{"admin_key": "admin"}'
Pin Specific Account
curl http://localhost:5001/v1/chat/completions \
-H "Authorization: Bearer your-api-key" \
-H "X-Ds2-Target-Account: user@example.com" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash",
"messages": [{"role": "user", "content": "Hello"}]
}'