# DeepSeek (V4 API) — provider research for PolyLLM Last documentation audit: **2026-09-08** Probes executed 2026-09-08 with a real key against `https://api.deepseek.com` (scripts in `research/deepseek/`, raw outputs in `research/deepseek/out/`, ~180 requests, total spend < $0.10). Everything marked **(probed)** was observed live; everything marked **(docs)** comes from the pages listed at the end. > Heads-up: several URLs given in the brief redirect to the landing page — the guides moved: `guides/reasoning_model` → **`guides/thinking_mode`**, `guides/function_calling` → **`guides/tool_calls`**, `news/` → **`/updates`** (+ `news/newsYYMMDD`), and `quick_start/pricing` only renders with a **trailing slash**. There is a new `guides/vision` page, a Files API and a Responses API. The FAQ is an external SPA (`static.deepseek.com/faq`) that cannot be fetched server-side. --- ## 1. Base URL, auth, headers | Item | Value | |---|---| | REST base URL | `https://api.deepseek.com` (docs). **`https://api.deepseek.com/v1` works identically** for `/models`, `/user/balance`, `/chat/completions` **(probed)** — the `v1` has nothing to do with the model version. Trailing slash also accepted **(probed)**. | | Beta base URL | `https://api.deepseek.com/beta` — required for chat **prefix completion**, FIM `/completions`, `strict` tools. Normal chat completions also work on `/beta` **(probed)**. | | Anthropic-compatible base URL | `https://api.deepseek.com/anthropic` (`/anthropic/v1/messages`, `x-api-key` header) **(probed 200)** — see §11 | | Auth | `Authorization: Bearer ` | | Content type | `application/json`. Malformed JSON → **400** with an OpenAI-shaped error (serde message, e.g. `Failed to deserialize the JSON body into the target type: messages: invalid type: string "nope", expected a sequence at line 1 column 46`); a syntactically broken body → 400 **text/plain** `Failed to parse the request body as JSON: …` **(probed)** | | Response headers **(probed)** | Only `x-ds-trace-id` (request id), `server: elb`, `via: … cloudfront.net`. **No rate-limit headers**, no `retry-after` observed. | | Key introspection | `GET /user/balance` → `{"is_available":true,"balance_infos":[{"currency":"USD","total_balance":"49.94","granted_balance":"0.00","topped_up_balance":"49.94"}]}` **(probed)**. `GET /models` also validates the key (401 on bad key). Both are free → good for a "validate key" button. | ## 2. SDK recommendation (TypeScript / Node) - DeepSeek has **no SDK of its own**; the official docs use the **OpenAI SDK** (`npm install openai`) with `baseURL: "https://api.deepseek.com"` and the **Anthropic SDK** with `ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic`. - The docs' Node sample passes `thinking: {type:"enabled"}` and `reasoning_effort: "high"` directly in `chat.completions.create()` (the TS SDK lets extra fields through; in Python they need `extra_body`). - **Recommendation for PolyLLM:** OpenAI SDK **`openai@7.10.0`** (probed) with `baseURL: https://api.deepseek.com`, `maxRetries: 0` (we handle retries), long `timeout` (thinking at `max` on hard prompts + the documented 10-minute queue → ≥ 10 min). The OpenAI SDK already tolerates the SSE `: keep-alive` comment lines DeepSeek sends under load (docs); a hand-written parser must skip lines starting with `:`. `delta.reasoning_content` is not in the SDK types → cast. ## 3. Endpoints | Endpoint | Status | Notes | |---|---|---| | `POST /chat/completions` | **primary** | OpenAI-compatible, thinking mode via `thinking` / `reasoning_effort`, `reasoning_content` in message and deltas. **(probed)** | | `GET /models` | active | `{object:"list", data:[{id, object:"model", owned_by:"deepseek"}]}` — 3 models, no metadata. **(probed)** | | `GET /user/balance` | active | See §1. **(probed)** | | `POST /responses` | active (Aug 2026) | OpenAI Responses API, **stateless** (`store:false` always, `previous_response_id` unsupported), `reasoning.effort` `none|low|high|max`, reasoning item `{type:"reasoning", content:[{type:"reasoning_text"}]}`, events `response.reasoning_text.delta` etc. Server-side `web_search` tool + `apply_patch`. **(probed: streaming + effort none)** | | `POST /beta/chat/completions` with `prefix: true` | beta | Chat prefix completion; outside `/beta` → 400 `prefix is only available when using beta api (set base_url="https://api.deepseek.com/beta")` **(probed)** | | `POST /beta/completions` | beta | FIM (`prompt` + `suffix`, `max_tokens` ≤ 4K docs); docs say pro only / non-thinking only, but **flash answered too** **(probed)**; vision-exp unsupported (docs). | | `POST/GET/DELETE /files` | active (free) | Images only (JPEG/PNG/GIF/WebP ≤ 64 MiB, 25 GiB / 10 000 files per user, optional `expires_after` 1 h–30 d); referenced as `{type:"file", file_id}` content parts for the vision model. `GET /files` → `{object:"list", data:[], has_more:false}` **(probed)** | | `/anthropic/v1/messages` | active | Anthropic Messages format; see §11. | | Batch API, embeddings, image generation, audio | **do not exist** | Text (+ image input on one model) only. | ## 4. Chat Completions request/response (probed shapes) Request body fields (docs API ref + probes): `model`, `messages`, `max_tokens` (integer, **valid range `[1, 393216]`** = 384K **(probed)**; **caps reasoning + visible output together**, see §8; default not documented), `temperature` 0–2 (default 1), `top_p` (0, 1] (default 1), `stop` (string | array, **≤ 16**), `stream`, `stream_options.include_usage`, `response_format` `{type:"text"|"json_object"}`, `tools` (≤ 128), `tool_choice` (`none|auto|required|{type:"function",function:{name}}`), `logprobs`, `top_logprobs` 0–20, `thinking` `{type:"enabled"|"disabled"|"adaptive"(undocumented), reasoning_effort?}`, `reasoning_effort` (`low|high|max`; `medium`/`xhigh` mapped to `high`; **`none` and `minimal` also accepted (undocumented)**), `user_id` (`[a-zA-Z0-9\-_]`, ≤ 512, used for rate-limit/KV-cache/safety isolation), deprecated & ignored: `frequency_penalty`, `presence_penalty`. Roles: `system`, `user`, `assistant`, `tool` (+ an undocumented internal `latest_reminder`). **`developer` role → 400** `messages[0].role: unknown variant `developer`, expected one of `system`, `user`, `assistant`, `tool`, `latest_reminder`` **(probed)** — map developer → system. `name` on messages accepted. Unknown top-level params (`foo_bar`, `user`, `seed`, `parallel_tool_calls`, `max_completion_tokens`) are **silently ignored** **(probed: `max_completion_tokens: 50` did not cap output — always send `max_tokens`)**. Non-streaming response **(probed, deepseek-v4-pro)**: ```json { "id": "f09673ec-…", "object": "chat.completion", "created": 1788849178, "model": "deepseek-v4-pro", "choices": [{ "index": 0, "finish_reason": "stop", "logprobs": null, "message": { "role": "assistant", "content": "2+2 equals 4.", "reasoning_content": "We need answer user asks simple. … final." } }], "usage": { "prompt_tokens": 96, "completion_tokens": 46, "total_tokens": 142, "prompt_tokens_details": { "cached_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 38 }, "prompt_cache_hit_tokens": 0, "prompt_cache_miss_tokens": 96 }, "system_fingerprint": "a307abda487cd1b463329ccb945ce396" } ``` - **`completion_tokens` INCLUDES reasoning tokens** (46 = 38 reasoning + 8 visible); `total_tokens = prompt + completion`. Bill all `completion_tokens` at the output price. - Cache: both the OpenAI-style `prompt_tokens_details.cached_tokens` and DeepSeek's `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens` are present (`hit + miss = prompt_tokens`). `completion_tokens_details` is **absent** when thinking is disabled **(probed)**. - `finish_reason`: `stop`, `length` **(probed)**, `tool_calls` **(probed)**, `content_filter`, `insufficient_system_resource` (docs: "request interrupted due to insufficient resource of the inference system" — treat as retryable). - Hidden system prompt is tiny: a 6-token user message costs `prompt_tokens: 6` **(probed)** — no hidden overhead (unlike xAI). - `system_fingerprint` is stable per model per day (`a26a79…` flash, `a307ab…` pro). ## 5. Streaming protocol **(probed)** SSE `text/event-stream; charset=utf-8`, `data: {json}` lines, terminated by `data: [DONE]`. Under load the server emits `: keep-alive` comment lines (streaming) or empty lines (non-streaming) (docs; not observed on short requests). Connections that have not started inference after **10 minutes** are closed (docs). Chunk sequence in thinking mode (deepseek-v4-flash): ```json data: {"id":"cb67…","object":"chat.completion.chunk","created":1788849177,"model":"deepseek-v4-flash","system_fingerprint":"a26a…", "choices":[{"index":0,"delta":{"role":"assistant","content":null,"reasoning_content":""},"logprobs":null,"finish_reason":null}],"usage":null} data: {…"choices":[{"index":0,"delta":{"content":null,"reasoning_content":"We"},…}],"usage":null} … data: {…"choices":[{"index":0,"delta":{"content":"Bon","reasoning_content":null},…}],"usage":null} … data: {…"choices":[{"index":0,"delta":{"content":"","reasoning_content":null},"logprobs":null,"finish_reason":"stop"}], "usage":{"prompt_tokens":93,"completion_tokens":48,"total_tokens":141,"prompt_tokens_details":{"cached_tokens":0}, "completion_tokens_details":{"reasoning_tokens":44},"prompt_cache_hit_tokens":0,"prompt_cache_miss_tokens":93}} data: [DONE] ``` - In thinking mode **both keys are always present** in every delta, one of them `null` (`content: null` while reasoning, `reasoning_content: null` while answering). In non-thinking mode (`thinking.disabled` / `reasoning_effort: "none"`) deltas only have `content` (first delta `{"role":"assistant","content":""}`) **(probed)**. Check `if (d.reasoning_content)` / `if (d.content)` — never `!== undefined`. - **Usage arrives on the `finish_reason` chunk itself, not in a separate `choices: []` chunk, and it is sent even WITHOUT `stream_options.include_usage`** **(probed both ways)**. `include_usage: true` is accepted and changes nothing. Adapter: read `chunk.usage` whenever non-null, don't require an empty-choices chunk. - Reasoning deltas start within ~300 ms — good liveness signal; a 5-word answer took 1.5 s (flash) / 2.6 s (pro) end to end incl. reasoning. ## 6. Tool / function calling **(probed on all 3 models, thinking on and off)** - OpenAI nested format `{type:"function", function:{name, description, parameters}}`; `tool_choice` `auto` (default) / `required` / `none` / `{type:"function",function:{name}}` all behave as expected **(probed)**; ≤ 128 tools (docs). `parallel_tool_calls` is ignored but **parallel calls happen natively**: "Montreal and Quebec City" → two `tool_calls` (`index` 0 and 1) in one message **(probed)**. Tool-call ids look like `call_00_<24 chars>`, `call_01_…` (the `_NN_` is the index). - Round trip `assistant.tool_calls` + `{role:"tool", tool_call_id, content}` works in both modes on all 3 models **(probed)**. - **Streaming: standard OpenAI incremental deltas** (unlike xAI): first chunk `{"index":0,"id":"call_00_…","type":"function","function":{"name":"get_weather","arguments":""}}`, then ~10 chunks `{"index":0,"function":{"arguments":"{"}}` … `{"arguments":"}"}`, then `finish_reason: "tool_calls"` with usage **(probed)**. Standard accumulation by `index` required. - In thinking mode the tool-call message also carries `reasoning_content` (and possibly `content` text before the call) — docs' "interleaved thinking": the model may reason → call → reason → call … inside one user turn. - **`strict: true` (beta base URL)**: works **(probed 200)**. Requires every property `required` + `additionalProperties: false`; supported schema: object/string/number/integer/boolean/array/enum/anyOf/$ref+$defs; `pattern`, `format` (email/hostname/ipv4/ipv6/uuid), numeric bounds/`multipleOf`; **not** `minLength`/`maxLength`/`minItems`/`maxItems` (docs). Non-conforming schema → error. - **`reasoning_content` replay rule (docs vs probes)**: docs say that when `tools` is present, *"the reasoning_content of all previous turns should be passed back… even for turns where the model did not perform a tool call. If your code does not correctly pass back reasoning_content, the API will return a 400 error."* **Probed on V4: NO 400** — stripping `reasoning_content` from the tool-call message, from the final answer, from all assistant messages, or sending `""`/`null`, all returned 200 and a sensible answer (`research/deepseek/out/09-…json`). The 400 is evidently a V3.2-era rule that V4 no longer enforces, but **follow the docs anyway**: store `reasoning_content` with every assistant message and send it back verbatim whenever the request has `tools` — it is free (cached prefix) and keeps chain-of-thought continuity across sub-turns. Without `tools`, replayed `reasoning_content` is accepted and *ignored* (docs + probed: bogus reasoning did not change the answer; cache hit identical). ## 7. Structured output **(probed)** - `response_format: {type:"json_object"}` works on all 3 models, thinking on **and** off → valid JSON **(probed)**. The prompt **must contain the word "json"** or → 400 `Prompt must contain the word 'json' in some form to use 'response_format' of type 'json_object'.` **(probed)**. Docs: also give a format example, set `max_tokens` high enough, and "the API may occasionally return empty content" (not observed). - **`json_schema` is NOT available**: 400 `This response_format type is unavailable now` on all models **(probed)**. Workaround for schema-constrained output: `strict` function calling on `/beta` (§6) with `tool_choice` forcing the function. - Responses API: `text.format` supported (docs, "format only"). ## 8. Reasoning controls **(probed matrix)** All three models are **hybrid**: thinking **enabled by default at effort `high`** (docs + probed: `reasoning_content` present with no flags). Same behaviour on flash, pro and vision-exp. | Control | Effect **(probed)** | |---|---| | `thinking: {type:"disabled"}` | no `reasoning_content`, no `completion_tokens_details`; fastest | | `thinking: {type:"enabled"}` | default | | `thinking: {type:"adaptive"}` | **accepted (undocumented; leaked from the Anthropic-format vocabulary)**, behaves like `enabled` (11 reasoning tokens on "pong", 90 on a riddle — same as enabled). Error text reveals the enum: `thinking.type: unknown variant `auto`, expected one of `adaptive`, `enabled`, `disabled`` | | `thinking: {type:"enabled", reasoning_effort:"low"}` (nested) | accepted | | `reasoning_effort: "low" \| "high" \| "max"` (top-level) | accepted; docs mapping `medium→high`, `xhigh→high` | | `reasoning_effort: "none"` | **disables thinking** (equivalent to `thinking.disabled`) — undocumented on chat completions, documented for Responses `reasoning.effort` | | `reasoning_effort: "minimal"` | accepted (undocumented; presumably → low) | | `reasoning_effort: "low"` + `thinking.disabled` | thinking stays disabled (disabled wins) | - Reasoning-token counts on trivial prompts are tiny (8–40 tokens) and effort levels are not monotonic on easy prompts (the model decides); on a riddle: low 367 / high 97 / max 184 reasoning tokens at ~2–3 s — treat `reasoning_effort` as a hint, not a budget. **No thinking-budget parameter.** - **`max_tokens` caps reasoning + answer together**: `max_tokens: 20` in thinking mode → `finish_reason: "length"`, `reasoning_tokens: 20`, **`content: ""`** **(probed)**; same with a 32×32 image at `max_tokens: 200` (all 200 spent on reasoning). Adapter: in thinking mode never send a small `max_tokens`; default to several thousand (range max 393 216) and surface "answer truncated during reasoning" when `content` is empty and `finish_reason === "length"`. - Thinking mode **ignores** `temperature`, `top_p`, `presence_penalty`, `frequency_penalty` silently (docs: "will not trigger an error but will also have no effect"; probed: accepted, still range-validated → 400 outside range). - Responses API `reasoning.effort` accepts `none|low|high|max` **(probed none/low)**; `reasoning.summary` / `encrypted_content` unsupported (docs) — the API nevertheless returns an `encrypted_content` string that is just an id. - `reasoning_content` in the *last* assistant message is also the CoT prefix input for **chat prefix completion** on `/beta` (API ref). ## 9. Sampling & other parameters — support matrix **(probed, chat completions, identical on the 3 models)** | Param | Result | Exact error | |---|---|---| | `temperature` | ✓ 0–2 (ignored in thinking mode) | `2.5`/`-1` → 400 `Invalid temperature value, the valid range of temperature is [0, 2]` | | `top_p` | ✓ (0, 1] (ignored in thinking mode) | `1.5` → 400 `Invalid top_p value, the valid range of top_p is (0, 1.0]` | | `max_tokens` | ✓ | `400000` → 400 `Invalid max_tokens value, the valid range of max_tokens is [1, 393216]` | | `max_completion_tokens` | **silently ignored** (no cap applied) | — | | `stop` (≤ 16) | ✓ applies to visible content (thinking mode too) | 17 items → 400 `Stop string array too long: 17` | | `frequency_penalty` / `presence_penalty` | accepted, **no effect** (deprecated) | — | | `logprobs` + `top_logprobs` 0–20 | ✓ (`choices[0].logprobs.content[]` with `token`, `logprob`, `bytes`, `top_logprobs[]`) | `25` → 400 `Invalid top_logprobs value, the valid range of top_logprobs is [0, 20].` | | `n` | **only 1** | `n: 2` → 400 `Invalid n value (currently only n = 1 is supported)` | | `seed` | silently ignored | — | | `response_format` `text` / `json_object` | ✓ | `json_schema` → 400 `This response_format type is unavailable now` | | `tools` / `tool_choice` | ✓ | — | | `parallel_tool_calls` | ignored (parallel calls always possible) | — | | `thinking`, `reasoning_effort` | see §8 | `thinking.type: "auto"` → 400 serde enum error | | `user_id` | ✓ (isolation) ; `user` ignored | — | | `developer` role | **400** | see §4 | | unknown params | silently ignored | — | **Adapter rule:** send `max_tokens` (never `max_completion_tokens`), map `developer`→`system`, drop `n>1`/`seed`/penalties, offer `temperature`/`top_p` only when thinking is off, expose `reasoning_effort` as `none|low|high|max` (translate `none` → `thinking:{type:"disabled"}` for clarity), expose `json_object` only (no `json_schema`). ## 10. Modalities, context, output limits - **Input**: text on all 3; **images only on `deepseek-v4-flash-vision-exp`**. **Output**: text only. No audio, no image generation, no files other than images. - **Images on non-vision models are NOT rejected**: `deepseek-v4-flash` / `deepseek-v4-pro` return 200 and **silently replace the image with a placeholder** (+5 prompt tokens; pro said *"No. I received text only, with a placeholder indicating an unsupported image."*, flash said the image "wasn't successfully attached", pro once **hallucinated** a description) **(probed)** — contradicts the vision guide ("400 This model does not support image"). PolyLLM must block image attachments client-side via `capabilities.vision` — the API will not tell the user. - Vision model **(probed)**: 32×32 PNG data URL → 200, correct description ("4x4 checkerboard … red and royal blue"); **2×2 accepted** (no minimum); `detail: low|high|original|auto` accepted (`low` = downscale to 512², `high`=`original`=`auto` keep original — docs); image in a **system** message → 400 `Image in system message is unsupported`; garbage bytes → 400 `.messages[0].image[0]: You have uploaded an unsupported image. Please make sure your image is valid and has one of the following formats: webp, png, jpeg, and gif.`; images in assistant messages also rejected (docs). Tools + JSON + thinking all work on the vision model **(probed)**. - Image tokens **(probed)**: a 32×32 image cost **~111 prompt tokens** (131 vs 20 text-only; 103 with `detail: low`) — small images are **upscaled to ~384×384** (docs) so there is a floor of ~100 tokens; ceiling **384 tokens per image** (images downscaled to ~800×800 area). Billed at the text input price. Limits (docs): JPEG/PNG/GIF/WebP (sniffed, not by MIME), ≤ 32 MiB inline / 64 MiB via Files API, request body ≤ 48 MiB, ≤ 600 images per request, side ≤ 8192 px (4096 px when ≥ 15 images), URL ≤ 8192 chars, download ≤ 60 s. - **Context window: 1M tokens on all 3 models** (docs pricing table). **Max output: 384K (`max_tokens` ≤ 393 216)**, no documented default — always send `max_tokens`. - Files API: images only, `{type:"file", file_id}` or `{type:"file", file_data:"data:image/…;base64,…", filename}` content parts (mutually exclusive), `detail` ignored for `file_id`. ## 11. Anthropic-compatible & Responses surfaces (secondary) - `POST /anthropic/v1/messages` with `x-api-key` + `anthropic-version` → 200 **(probed)**; response is a real Messages object with `content: [{type:"thinking", thinking:"…", signature:""}, {type:"text", text}]`, `usage: {input_tokens, cache_creation_input_tokens, cache_read_input_tokens, output_tokens, service_tier:"standard"}`. `thinking.budget_tokens` is **ignored** (docs; probed: accepted), `output_config.effort` `low|high|max` controls effort; `reasoning.effort: none` disables. **Claude model names are mapped**: `claude-sonnet-4-5` → `deepseek-v4-flash` (response `model` field says so) **(probed)**; Opus names → `deepseek-v4-pro`, everything else → flash (docs). Unsupported: `top_k`, `service_tier`, `container`, `mcp_servers`, document/search-result blocks, MCP/code-execution blocks (docs). Useful only if PolyLLM ever reuses its Anthropic adapter; not needed for the OpenAI-style adapter. - `POST /responses` **(probed)**: stateless (`store: false` echoed, `previous_response_id: null`), `reasoning.effort` `none|low|high|max`, output items `reasoning` (`content:[{type:"reasoning_text", text}]`, `summary: []`) + `message` (`output_text`, extra `phase: "final_answer"`), events `response.created`, `response.in_progress`, `response.output_item.added/done`, `response.content_part.added/done`, `response.reasoning_text.delta/done`, `response.output_text.delta/done`, `response.function_call_arguments.delta/done`, `response.completed|incomplete|failed`. `max_output_tokens: 100` on a reasoning request → `status: "incomplete"`, `incomplete_details.reason: "max_output_tokens"` (reasoning ate the budget). Server-side `web_search` tool exists here only (≤ 10 rounds, docs) — the only search option DeepSeek offers; no pricing published. ## 12. Model listing & pricing **(docs pricing page, fetched 2026-09-08; USD per 1M tokens)** `GET /models` returns only ids — **no context/pricing metadata**; hardcode from the table. | Model | Version (docs) | ctx | max out | cache-hit in (peak / off-peak) | cache-miss in (peak / off-peak) | out (peak / off-peak) | concurrency | |---|---|---|---|---|---|---|---| | deepseek-v4-flash | DeepSeek-V4-Flash-0731 | 1M | 384K | 0.014 / 0.007 | 0.44 / 0.22 | 1.32 / 0.66 | 2500 | | deepseek-v4-pro | DeepSeek-V4-Pro-0813 | 1M | 384K | 0.044 / 0.022 | 1.32 / 0.66 | 3.96 / 1.98 | 500 | | deepseek-v4-flash-vision-exp | DeepSeek-V4-Flash-Vision-Exp | 1M | 384K | 0.014 / 0.007 | 0.44 / 0.22 | 1.32 / 0.66 | 2500 | - **Peak hours: 01:00–04:00 and 06:00–10:00 UTC, Monday–Friday; all other hours are off-peak at 50 %** (effective 2026-08-16 16:00 UTC). Cost display must pick the tariff from the request's UTC timestamp + weekday. Cache-hit input is **1/32 of cache-miss** (≈ 97 % discount). - Reasoning tokens are part of `completion_tokens` → output price. Image tokens → input price (hit/miss applies). - Billing currency per account (`/user/balance` → USD here); granted balance consumed first (docs). 402 when out of balance. - Same architecture/tokenizer for flash and vision-exp ("matches DeepSeek-V4-Flash on text capabilities"). ## 13. Prompt caching & provider-side state - **Automatic for everyone, no code change, no minimum documented**; cache units are created after each user input, after each model output, and at fixed token intervals for long content; a hit requires **exact match of whole cache units** (partial overlaps don't hit — DeepSeek Sparse Attention constraint). Cache built in seconds, evicted "within a few hours to a few days" when unused; best effort (docs). - **(probed)**: turn 2 of a conversation reported `prompt_cache_hit_tokens: 256` / `miss 63` for a 319-token prompt; a tool-loop step 2 hit 256 then 384 — hits come in **multiples of 64 tokens** (256, 384). Cache hits are reported both as `prompt_cache_hit_tokens` and `prompt_tokens_details.cached_tokens`. Replaying `reasoning_content` did not change hit counts. - `user_id` isolates KV cache per end user (privacy) — for a multi-user BYOK app set `user_id` to a hashed PolyLLM user id (no PII, `[a-zA-Z0-9\-_]`, ≤ 512). - **No server-side state at all**: `/chat/completions` and `/responses` are stateless; nothing to delete. Files API stores uploaded images (set `expires_after`). ## 14. Errors, rate limits, retries Error body: OpenAI-shaped `{"error":{"message","type","param":null,"code"}}` (`type` `authentication_error` | `invalid_request_error`; `code` always `invalid_request_error` so far). Some 400s are served with `content-type: application/octet-stream` — parse the body as JSON regardless of the header **(probed)**. | Case **(probed)** | HTTP | body | |---|---|---| | Invalid key | **401** | `{"error":{"message":"Authentication Fails, Your api key: ****0000 is invalid","type":"authentication_error","param":null,"code":"invalid_request_error"}}` (same on `/models`) | | No Authorization header | 401 | text/plain `Authentication Fails (governor)` | | Unknown model | 400 | `The supported API model names are deepseek-v4-pro, deepseek-v4-flash, and deepseek-v4-flash-vision-exp, but you passed deepseek-v99.` | | Empty `messages` | 400 | `Empty input messages` | | Missing `model` | 400 | `Failed to deserialize the JSON body into the target type: missing field `model` …` | | Bad enum (role, thinking.type) | 400 | serde `unknown variant …, expected one of …` | | Out-of-range param | 400 | see §9 | | Malformed JSON | 400 | text/plain `Failed to parse the request body as JSON: …` | | Insufficient balance | 402 (docs) | — | | Invalid parameters | 422 (docs) | — | | Concurrency limit | 429 (docs) | 2500 (flash, vision) / 500 (pro) concurrent requests per account, per `user_id` when quota was expanded | | Server error / overloaded | 500 / 503 (docs) | retry after a short delay | **Retry/timeout recommendation for the adapter:** retry 429/500/503 (and `finish_reason: "insufficient_system_resource"`) with jittered exponential backoff; never retry 400/401/402/422; do not retry mid-stream; read timeout ≥ 10 min (server may queue up to 10 min before inference and sends keep-alive comments meanwhile). 401 maps cleanly to *invalid credentials*; 402 → *out of balance* (show link to `platform.deepseek.com`). ## 15. Lifecycle / aliases - **`deepseek-chat` and `deepseek-reasoner` were announced as retired 2026-07-24 15:59 UTC** (V4 launch note) and are no longer listed by `/models` or the docs, **but they still resolve** **(probed 2026-09-08)**: `deepseek-chat` → `deepseek-v4-flash` non-thinking, `deepseek-reasoner` → `deepseek-v4-flash` thinking (response `model: "deepseek-v4-flash"`). Do not list them; if a user types one, show the redirect and expect it to break any day. - Model names are **stable rolling aliases**: `deepseek-v4-flash` = V4-Flash-0731 (GA 2026-07-31), `deepseek-v4-pro` = V4-Pro-0813 (GA 2026-08-13), `deepseek-v4-flash-vision-exp` released 2026-08-21 as **experimental** (may change/disappear). No dated snapshot ids are exposed. - Timeline (`/updates`): V4 preview 2026-04-24 (1M ctx, dual modes, OpenAI + Anthropic APIs) → Flash GA 07-31 (Responses API) → Pro GA 08-13 (3 effort levels, peak/off-peak pricing from 08-16) → Vision-Exp + Files API 08-21. DeepSeek Harness (agent framework) in developer preview. - `latest_reminder` role and `thinking.type: "adaptive"` exist in the schema but are undocumented — do not rely on them. ## 16. Exact streaming code that worked OpenAI SDK (`openai@7.10.0`) — chat completions with reasoning + tools + usage: ```ts import OpenAI from "openai"; const client = new OpenAI({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com", timeout: 660_000, maxRetries: 0 }); const stream = await client.chat.completions.create({ model: "deepseek-v4-flash", messages, // assistant msgs keep `reasoning_content` when `tools` is sent tools, stream: true, max_tokens: 8192, // caps reasoning + answer together; never small in thinking mode ...(thinking ? { reasoning_effort: "high" } : { thinking: { type: "disabled" }, temperature: 0.7 }), user_id: hashedUserId, // KV-cache / rate-limit isolation } as any); const calls: Record = {}; for await (const chunk of stream) { const c = chunk.choices[0]; const d = (c?.delta ?? {}) as any; if (d.reasoning_content) onThinking(d.reasoning_content); // null while answering if (d.content) onText(d.content); // null while thinking for (const tc of d.tool_calls ?? []) { const s = (calls[tc.index] ??= { args: "" }); if (tc.id) s.id = tc.id; if (tc.function?.name) s.name = tc.function.name; if (tc.function?.arguments) s.args += tc.function.arguments; } if (c?.finish_reason) onFinish(c.finish_reason); // "stop" | "length" | "tool_calls" | … if (chunk.usage) onUsage(chunk.usage); // on the finish chunk, even without include_usage } ``` Raw fetch SSE (used in `research/deepseek/lib.ts` → `rawSSE`): POST JSON, read `res.body` with `TextDecoder`, split on `\n\n`, **skip lines starting with `:`** (keep-alive), take `data:` lines, `JSON.parse`, stop at `[DONE]`. ## 17. Probe results table | # | Probe | Model(s) | Result | |---|---|---|---| | 00 | `GET /models`, `/v1/models`, `/user/balance`, `/v1/user/balance`, `POST /v1/chat/completions`, `/beta/chat/completions`, `GET /files` | — | all 200; 3 models; `/v1` prefix and `/beta` accepted; balance USD; only `x-ds-trace-id` header | | 01a | tiny chat completion, default flags, `max_tokens: 200` | 3 models | 200, `reasoning_content` present on all (thinking default), 16–38 reasoning tokens for "2+2", 1.1–2.1 s | | 01b | streaming + `include_usage` | 3 models | delta keys `role`/`content`/`reasoning_content` (both keys present, one null); usage on the `finish_reason` chunk; `[DONE]`; no keep-alive comments seen | | 02 | param matrix (43 variants × 3 models) | 3 models | identical on all models — see §9; ranges `[0,2]`, `(0,1]`, `[1,393216]`, stop ≤ 16, top_logprobs ≤ 20, `n=1` only, `json_schema` unavailable, `developer` 400, `thinking.type` enum `adaptive|enabled|disabled`, `reasoning_effort none/minimal` accepted | | 03 | streamed function call round trip, thinking on (with/without reasoning replay) and off; `tool_choice` ×4; parallel; `strict` on `/beta` | 3 models | incremental `tool_calls` deltas (11 chunks), `finish_reason: tool_calls`; round 2 OK in all 9 combos (**no 400 without reasoning_content**); required/none/specific/auto OK; 2 parallel calls; strict 200 | | 04 | `json_object`, thinking on/off; prompt without "json" | 3 models | valid JSON ×6; 400 `Prompt must contain the word 'json'…` | | 05 | vision: 32×32 PNG on 3 models; 2×2; `detail` low/original; image in system; thinking+image; garbage bytes | 3 models | flash/pro **200 with image silently dropped** (+5 tokens); vision-exp correct (~111 image tokens, 103 at low); 2×2 OK; system image 400; thinking+image at `max_tokens 200` → empty content (all reasoning); bad bytes 400 | | 06 | invalid key / no auth / unknown model / retired slugs / malformed body / empty messages / bad role | — | 401 / 401 text / 400 / **200 served by v4-flash** / 400 / 400 / 400 | | 07 | `reasoning_effort` low/high/max at `max_tokens 2000`; `max_tokens 20` truncation; multi-turn with / without / bogus `reasoning_content` (no tools); `reasoning_content` while thinking disabled | flash | 367/97/184 reasoning tokens; `length` with `content: ""`; all 3 replays 200, same answer, 256 cached tokens; accepted | | 08 | Anthropic `/anthropic/v1/messages` (+ `claude-sonnet-4-5` alias); `/responses` stream + `effort: none`; `/beta` prefix (+ without beta); `/beta` FIM flash + pro | flash/pro | 200 thinking blocks, alias → flash; 9 event types, `incomplete` on small budget, none → 0 reasoning; prefix OK / 400 without beta; FIM OK on both | | 09 | tool loop turn 1 (call → result → answer) then turn 2 stripping `reasoning_content` in 6 ways; no `tools` on turn 2; thinking disabled on turn 2 | flash | **all 200** — the documented 400 is not enforced on V4 | | 10 | `thinking.adaptive` easy/hard; stream w/o `include_usage`; `effort none` stream; logprobs shape; image on pro; trailing slash | flash/pro | adaptive ≈ enabled; usage still present; deltas without `reasoning_content` key; logprobs `content[].{token,logprob,bytes,top_logprobs}`; pro: "placeholder indicating an unsupported image"; 200 | ## Documentation pages used (all fetched 2026-09-08) - https://api-docs.deepseek.com/ (Your First API Call) · https://api-docs.deepseek.com/quick_start/pricing/ (trailing slash required) · https://api-docs.deepseek.com/quick_start/rate_limit · https://api-docs.deepseek.com/quick_start/error_codes · https://api-docs.deepseek.com/quick_start/token_usage - https://api-docs.deepseek.com/api/deepseek-api · https://api-docs.deepseek.com/api/create-chat-completion · https://api-docs.deepseek.com/api/list-models · https://api-docs.deepseek.com/api/get-user-balance (also in sitemap: create-completion, create-response, create-file, list-files, retrieve-file, delete-file) - https://api-docs.deepseek.com/guides/thinking_mode (replaces `guides/reasoning_model`) · https://api-docs.deepseek.com/guides/tool_calls (replaces `guides/function_calling`) · https://api-docs.deepseek.com/guides/json_mode · https://api-docs.deepseek.com/guides/multi_round_chat · https://api-docs.deepseek.com/guides/kv_cache · https://api-docs.deepseek.com/guides/anthropic_api · https://api-docs.deepseek.com/guides/fim_completion · https://api-docs.deepseek.com/guides/chat_prefix_completion · https://api-docs.deepseek.com/guides/vision · https://api-docs.deepseek.com/guides/responses_api · https://api-docs.deepseek.com/guides/files_api - https://api-docs.deepseek.com/updates (changelog; `/news/` redirects) · https://api-docs.deepseek.com/news/news260424 (V4 launch, deprecation of deepseek-chat/reasoner) · https://api-docs.deepseek.com/news/news260813 (V4-Pro GA, effort levels, peak/off-peak) · https://api-docs.deepseek.com/news/news260821 (Vision-Exp + Files API) - https://api-docs.deepseek.com/api_samples/chat_nodejs · …/thinking_mode_api_example_streaming · …/thinking_mode_api_example_tool_call · …/thinking_mode_api_example_tool_call_output · https://api-docs.deepseek.com/sitemap.xml - Redirected/unfetchable at audit time: `guides/reasoning_model`, `guides/function_calling`, `news/` (→ landing page); `/faq` → external SPA `static.deepseek.com/faq` (no server-rendered content).