SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
34.8 KB

# 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 <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) 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
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:"<message id>"}, {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<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)