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), andquick_start/pricingonly renders with a trailing slash. There is a newguides/visionpage, 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 <DEEPSEEK_API_KEY> |
| 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) withbaseURL: "https://api.deepseek.com"and the Anthropic SDK withANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic. - The docs' Node sample passes
thinking: {type:"enabled"}andreasoning_effort: "high"directly inchat.completions.create()(the TS SDK lets extra fields through; in Python they needextra_body). - Recommendation for PolyLLM: OpenAI SDK
openai@7.10.0(probed) withbaseURL: https://api.deepseek.com,maxRetries: 0(we handle retries), longtimeout(thinking atmaxon hard prompts + the documented 10-minute queue → ≥ 10 min). The OpenAI SDK already tolerates the SSE: keep-alivecomment lines DeepSeek sends under load (docs); a hand-written parser must skip lines starting with:.delta.reasoning_contentis 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 |
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):
{
"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_tokensINCLUDES reasoning tokens (46 = 38 reasoning + 8 visible);total_tokens = prompt + completion. Bill allcompletion_tokensat the output price.- Cache: both the OpenAI-style
prompt_tokens_details.cached_tokensand DeepSeek'sprompt_cache_hit_tokens/prompt_cache_miss_tokensare present (hit + miss = prompt_tokens).completion_tokens_detailsis 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_fingerprintis 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):
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: nullwhile reasoning,reasoning_content: nullwhile answering). In non-thinking mode (thinking.disabled/reasoning_effort: "none") deltas only havecontent(first delta{"role":"assistant","content":""}) (probed). Checkif (d.reasoning_content)/if (d.content)— never!== undefined. - Usage arrives on the
finish_reasonchunk itself, not in a separatechoices: []chunk, and it is sent even WITHOUTstream_options.include_usage(probed both ways).include_usage: trueis accepted and changes nothing. Adapter: readchunk.usagewhenever 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_choiceauto(default) /required/none/{type:"function",function:{name}}all behave as expected (probed); ≤ 128 tools (docs).parallel_tool_callsis ignored but parallel calls happen natively: "Montreal and Quebec City" → twotool_calls(index0 and 1) in one message (probed). Tool-call ids look likecall_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":"}"}, thenfinish_reason: "tool_calls"with usage (probed). Standard accumulation byindexrequired. - In thinking mode the tool-call message also carries
reasoning_content(and possiblycontenttext 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 propertyrequired+additionalProperties: false; supported schema: object/string/number/integer/boolean/array/enum/anyOf/$ref+$defs;pattern,format(email/hostname/ipv4/ipv6/uuid), numeric bounds/multipleOf; notminLength/maxLength/minItems/maxItems(docs). Non-conforming schema → error.reasoning_contentreplay rule (docs vs probes): docs say that whentoolsis 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 — strippingreasoning_contentfrom 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: storereasoning_contentwith every assistant message and send it back verbatim whenever the request hastools— it is free (cached prefix) and keeps chain-of-thought continuity across sub-turns. Withouttools, replayedreasoning_contentis 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 → 400Prompt must contain the word 'json' in some form to use 'response_format' of type 'json_object'.(probed). Docs: also give a format example, setmax_tokenshigh enough, and "the API may occasionally return empty content" (not observed).json_schemais NOT available: 400This response_format type is unavailable nowon all models (probed). Workaround for schema-constrained output:strictfunction calling on/beta(§6) withtool_choiceforcing the function.- Responses API:
text.formatsupported (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_effortas a hint, not a budget. No thinking-budget parameter. max_tokenscaps reasoning + answer together:max_tokens: 20in thinking mode →finish_reason: "length",reasoning_tokens: 20,content: ""(probed); same with a 32×32 image atmax_tokens: 200(all 200 spent on reasoning). Adapter: in thinking mode never send a smallmax_tokens; default to several thousand (range max 393 216) and surface "answer truncated during reasoning" whencontentis empty andfinish_reason === "length".- Thinking mode ignores
temperature,top_p,presence_penalty,frequency_penaltysilently (docs: "will not trigger an error but will also have no effect"; probed: accepted, still range-validated → 400 outside range). - Responses API
reasoning.effortacceptsnone|low|high|max(probed none/low);reasoning.summary/encrypted_contentunsupported (docs) — the API nevertheless returns anencrypted_contentstring that is just an id. reasoning_contentin 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-proreturn 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 viacapabilities.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|autoaccepted (low= downscale to 512²,high=original=autokeep original — docs); image in a system message → 400Image 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 sendmax_tokens. - Files API: images only,
{type:"file", file_id}or{type:"file", file_data:"data:image/…;base64,…", filename}content parts (mutually exclusive),detailignored forfile_id.
11. Anthropic-compatible & Responses surfaces (secondary)
POST /anthropic/v1/messageswithx-api-key+anthropic-version→ 200 (probed); response is a real Messages object withcontent: [{type:"thinking", thinking:"…", signature:"<message id>"}, {type:"text", text}],usage: {input_tokens, cache_creation_input_tokens, cache_read_input_tokens, output_tokens, service_tier:"standard"}.thinking.budget_tokensis ignored (docs; probed: accepted),output_config.effortlow|high|maxcontrols effort;reasoning.effort: nonedisables. Claude model names are mapped:claude-sonnet-4-5→deepseek-v4-flash(responsemodelfield 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: falseechoed,previous_response_id: null),reasoning.effortnone|low|high|max, output itemsreasoning(content:[{type:"reasoning_text", text}],summary: []) +message(output_text, extraphase: "final_answer"), eventsresponse.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: 100on a reasoning request →status: "incomplete",incomplete_details.reason: "max_output_tokens"(reasoning ate the budget). Server-sideweb_searchtool 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 63for 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 asprompt_cache_hit_tokensandprompt_tokens_details.cached_tokens. Replayingreasoning_contentdid not change hit counts. user_idisolates KV cache per end user (privacy) — for a multi-user BYOK app setuser_idto a hashed PolyLLM user id (no PII,[a-zA-Z0-9\-_], ≤ 512).- No server-side state at all:
/chat/completionsand/responsesare stateless; nothing to delete. Files API stores uploaded images (setexpires_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-chatanddeepseek-reasonerwere announced as retired 2026-07-24 15:59 UTC (V4 launch note) and are no longer listed by/modelsor the docs, but they still resolve (probed 2026-09-08):deepseek-chat→deepseek-v4-flashnon-thinking,deepseek-reasoner→deepseek-v4-flashthinking (responsemodel: "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-expreleased 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_reminderrole andthinking.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:
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<number, { id?: string; name?: string; args: string }> = {};
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 |
| 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 (replacesguides/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 SPAstatic.deepseek.com/faq(no server-rendered content).