DeepSeek API (direct)
- Kind: raw API, OpenAI-compatible Chat Completions
- Official docs: https://api-docs.deepseek.com
- Cyberine adapter:
LoopAgentAdapter(idsloop-deepseek-direct, build workerloop-deepseek-build) - Preferred over OpenRouter when
DEEPSEEK_API_KEYis set (also backs the LLM/adversarial judges). - Researched/verified: 2026-08-25 — confidence high. Every page cited here is fetched with
curland grepped as raw HTML, never via an LLM-summarizing fetch (a summarizing fetch drops table cells, which is how thereasoning_effortenum stayed wrong for two revisions). The 2026-08-25 pass (T679) re-readquick_start/pricing,api/create-chat-completion,guides/thinking_mode,guides/vision,guides/files_api,guides/responses_api,guides/tool_calls,guides/json_mode,guides/kv_cache,guides/fim_completion,guides/chat_prefix_completion,quick_start/agent_integrations/codexandupdates, and re-probed the live API for every number below that a page cannot supply (enum error text, per-effortprompt_tokens, alias resolution, imageusageshape, Files/Responses status codes). - Partial re-verify 2026-09-24:
quick_start/pricingandupdatesre-read directly (not this file's usualcurl, but not an LLM-summarizing fetch either) to correct thedeepseek-v4-proredirect claim and the Flash pricing — see the "Models" table and "Pricing" section. Everything else in this file is still dated 2026-08-25 and was not re-checked in this pass.
DELISTED 2026-07-24, but NOT dead on the wire — corrected 2026-08-25 (T679). The legacy ids
deepseek-chatanddeepseek-reasonerwere announced for discontinuation on 2026-04-24 (api-docs.deepseek.com/updates, "DeepSeek-V4 Release") with a 2026-07-24 15:59 UTC cutoff, and that cutoff HAS passed — the sunset is a past-tense fact, not a pending deadline. They are gone fromGET https://api.deepseek.com/models, which returns exactlydeepseek-v4-flash,deepseek-v4-proanddeepseek-v4-flash-vision-exp, and they are absent from the/api/create-chat-completionmodelenum. ButPOST /chat/completionsstill accepts both — live-probed 2026-08-25, six calls, all HTTP 200 (see "Legacy aliases" under Gotchas). Earlier revisions of this file, and theconfig.tscomment + CLI message that cite it, claimed "every call using it fails at the provider"; that claim is FALSE and is corrected here and insrc/core/config.ts.The pin is still wrong to use, on grounds that survive the correction: the ids are undocumented, unlisted and unannounced, so DeepSeek can drop them without any further notice; and both now resolve to the SAME model (
deepseek-v4-flash), sodeepseek-reasonerno longer selects a distinct reasoner — it only preselects thinking mode. Cyberine's own defaults are already on the V4 names (deepseek-v4-pro/deepseek-v4-flash— seeconfig.tsdeepseekModel,judgeModel,openrouterModel,openCodeModel,smartRouterModel), so no shipped default is affected. IfDEEPSEEK_MODEL,JUDGE_MODEL,CYBERINE_JUDGE_MODEL*,OPENROUTER_MODEL,OPENCODE_MODEL, orCYBERINE_ROUTER_MODELis set to a legacy id, every subcommand that would actually dispatch or spend hard-stops at startup with a one-line CLI error (T681, 2026-08-24). That hard stop is KEPT after the T679 correction — it is now justified by "unlisted, unversioned and silently collapsed onto flash" rather than by a guaranteed 400.cyberine doctorandcyberine configare exempt from the stop — they are the commands that diagnose/inspect a config, sodoctorreports the same pin as a[FAIL]line instead of dying before it can. Switch it todeepseek-v4-proordeepseek-v4-flash.
Base URL & auth
| Base URL | https://api.deepseek.com (OpenAI-compatible) |
| Anthropic-format base | https://api.deepseek.com/anthropic |
| Beta (strict tool calling) | https://api.deepseek.com/beta |
| Auth header | Authorization: Bearer <DEEPSEEK_API_KEY> |
| Keys | https://platform.deepseek.com (funded directly; accepts PayPal) |
Endpoints
All paths are relative to https://api.deepseek.com. Every one below was called live on
2026-08-25 (T679) and returned the status shown.
| Method + path | Purpose | Live status |
|---|---|---|
POST /chat/completions | Chat completions (OpenAI-compatible). The endpoint Cyberine calls via the openai SDK. | 200 |
POST /responses | OpenAI Responses API, natively supported (shipped for deepseek-v4-flash 2026-07-31, for deepseek-v4-pro 2026-08-13). POST /v1/responses is the same endpoint — both 200, so an SDK that appends /v1 works unchanged. See "Responses API" below. | 200 |
GET /models | List available model ids. Returns exactly the three V4 ids — no legacy alias. | 200 |
POST /files | Files API upload (multipart). Returns 200, not 201. | 200 |
GET /files | List uploaded files (after / limit 1-1000 / order / purpose). | 200 |
GET /files/{file_id} | Retrieve one file's metadata. | 200 |
DELETE /files/{file_id} | Delete a file. Returns {"id":..., "object":"file", "deleted":true}. | 200 |
POST /anthropic/v1/messages | Anthropic-format chat (base https://api.deepseek.com/anthropic). | not re-probed |
POST / GET / DELETE /anthropic/v1/files[/{id}] | Anthropic-format Files API. Requires anthropic-beta: files-api-2025-04-14; the Anthropic SDK appends /v1 itself, a plain curl must write the full path. | not re-probed |
POST /beta/chat/completions, POST /beta/completions | Beta base URL: strict tool schemas, chat prefix completion, FIM. | not re-probed |
Models
| Model id | Notes |
|---|---|
deepseek-v4-flash | GA DeepSeek-V4-Flash-0731 (2026-07-31). 1M context, 384K max output. Fast/economical. Dual thinking + non-thinking. Concurrency 2500. Parameter count: see the unverified note below. |
deepseek-v4-pro | GA DeepSeek-V4-Pro-0813 (2026-08-13). 1M context, 384K max output. Higher capability. Concurrency 500. Parameter count: see the unverified note below. |
deepseek-v4-flash-vision-exp | DeepSeek-V4-Flash-Vision-Exp (shipped 2026-08-21). DeepSeek's ONLY vision-capable id — the other two hard-400 on an image ("This model does not support image"). 1M context, 384K max output, Concurrency 2500 — and priced identically to deepseek-v4-flash on every row (see Pricing). One capability gap vs the other two: FIM Completion (Beta) is "Not supported" here, where flash and pro both support it in non-thinking mode (live pricing/model-details table, 2026-08-25). Every other listed feature (Json Output, Tool Calls, Responses API, Anthropic API, Chat Prefix Completion (Beta)) is a checkmark for all three. |
deepseek-flash | DeepSeek-V4.1-Flash, released 2026-09-10. Announced on the updates page the same day: deepseek-v4-flash and deepseek-v4-flash-vision-exp are retired and their names are now ALIASES, temporarily routed to V4.1-Flash and billed at this model's price. deepseek-v4-pro is NOT redirected — the same 2026-09-10 changelog entry says, verbatim, "we have decided to continue providing API services for DeepSeek V4 Pro after September 14, 2026, with the billing method remaining unchanged" (an earlier revision of this row claimed the opposite; corrected 2026-09-24 after re-reading the changelog and pricing pages directly). Wire-confirmed 2026-09-24: GET /models returns exactly [deepseek-flash, deepseek-v4-pro]; model: deepseek-v4-flash returns response.model: deepseek-flash billed at the flash price, and model: deepseek-v4-pro returns response.model: deepseek-v4-pro unchanged. Pricing per 1M, off-peak/peak: cache-hit 0.003/0.006, cache-miss 0.15/0.30, output 0.60/1.20 — in modelPricing.ts since k9tvx. |
deepseek-chat | DELISTED at the 2026-07-24 15:59 UTC cutoff, still ACCEPTED on the wire. Absent from GET /models and from the model enum on /api/create-chat-completion, but POST /chat/completions returns 200 and the response body says "model": "deepseek-v4-flash" — i.e. it is still an alias → flash, non-thinking (live-probed 2026-08-25, T679; the earlier "every call fails at the provider" claim was wrong). Do not pin it: see the callout at the top. |
deepseek-reasoner | Same status, same cutoff, same live result: 200, resolves to deepseek-v4-flash, thinking mode preselected. It no longer selects a different model from deepseek-chat — only a different default thinking state. |
Cyberine default:
DEEPSEEK_MODEL=deepseek-flash(config.ts, since k9tvx 2026-09-11).
RESOLVED 2026-09-11 (k9tvx), live-probed on a funded account. The deadline callout that stood here is settled, and its central worry was wrong.
GET /modelsnow returns exactly[deepseek-flash, deepseek-v4-pro].POST /chat/completionsacceptsdeepseek-flash,deepseek-v4-flash,deepseek-v4-proanddeepseek-v4-flash-vision-exp— all four HTTP 200, no 400 anywhere — but the responsemodelfield readsdeepseek-flashfor every one of them exceptdeepseek-v4-pro, which still returns itself. So the delisted ids do not reject, they alias silently, which is the worse failure and the reasonDEEPSEEK_LEGACY_MODEL_IDSnow coversdeepseek-v4-flashanddeepseek-v4-flash-vision-exp.The default therefore moved to
deepseek-flasheverywhere (config.ts,workerProfiles.ts's model and BOTH variants, and the operator's.cyberine/fleet.json) — pinning the one real, wire-confirmed id over an aliasing legacy name.CORRECTED 2026-09-24. This paragraph previously assumed
deepseek-v4-prowould itself be redirected to Flash from 2026-09-14 04:00 UTC, so pinning it bought only three more distinct days andmodelPricing.ts's pro row would become a stale 3.3x over-count once the redirect landed. That did not happen: DeepSeek's 2026-09-10 changelog says V4 Pro's API service continues past 2026-09-14 with billing unchanged, and a live probe on 2026-09-24 confirmsdeepseek-v4-prostill returns itself, not Flash.modelPricing.ts'sdeepseek-v4-prorow was correct all along.loop-deepseek-build'sreasoningvariant is pinned todeepseek-v4-proagain as of this correction — see workerProfiles.ts.RESOLVED 2026-09-11 (k1k86), live image-probed same funded account, same day. The prior paragraph's worry is confirmed and no longer open: a real chat/completions call to
deepseek-v4-flash-vision-expcarrying an actual image part (a 1x1 PNG data URL, one request) returned HTTP 200,response.model=deepseek-flash, and a confident one-word answer ("White") to "what single color is this image" for a fully transparent pixel — the alias does not reject or describe-as-unseen, it silently drops the image and answers anyway.isMultimodalModel()inworkerProfiles.tsno longer matches this bare id, so DeepSeek's build-fleet router gate (hasImagesinrouter.ts, already excluding every DeepSeek profile — see "Routing scope" below) and the manual-pin default-compare-adapter path (buildAdapters.ts/architectEditor.ts) both now fall back to LoopAgentAdapter'smultimodal: falsebranch, which visibly tells the worker its image attachments were not forwarded instead of silently sending them to a model that cannot see them.modelPricing.ts'sdeepseek-v4-flash-vision-exprow was also stale (still the pre-dealiasing flash rate) and now mirrors the currentdeepseek-flashprice, since the id still dispatches and bills as that model. See "Vision" and "Routing scope" below for what this closes.
UNVERIFIED — parameter counts (T687, 2026-08-25). Earlier revisions of this table stated
deepseek-v4-flash= "284B total / 13B active" anddeepseek-v4-pro= "1.6T total / 49B active" with no source cited. Re-checked 2026-08-25 bycurl-ing the two official pages this doc treats as authoritative —api-docs.deepseek.com/quick_start/pricingandapi-docs.deepseek.com/updates— and grepping both for284,13B,1.6T,49B, and for any "total/active params" phrasing: zero hits on either page, while the same fetches DID carry the live V4 model ids and the pricing/context columns (so this is a genuine absence, not a JS-rendered page the grep missed). DeepSeek's API docs do not publish parameter counts at all. The figures are therefore removed from the model rows above rather than restated: treat any parameter count for these models as unsourced third-party reporting until a primary source (a DeepSeek model card or release paper) is found and cited HERE. Nothing insrc/reads or depends on a parameter count — routing and cost usecontextWindow+modelPricing.tsrates only.
Key request params
-
model(required). The/api/create-chat-completionschema's enum is now, verbatim,Possible values: [deepseek-v4-flash, deepseek-v4-pro, deepseek-v4-flash-vision-exp]— the vision id is in the chat-completions enum, it is not a separate endpoint. Passing an id outside the enum returns HTTP 400"The supported API model names are deepseek-v4-pro, deepseek-v4-flash, and deepseek-v4-flash-vision-exp, but you passed <x>."(live-probed 2026-08-25) — note that the two legacy aliases are NOT in the enum yet do NOT hit this error. -
messages(required: roles system/user/assistant/tool).contentis a plain string OR an array of content blocks —{"type":"text"},{"type":"image_url","image_url":{"url":..., "detail":...}},{"type":"file","file_id":...}or{"type":"file","file_data":..., "filename":...}. Image and file blocks are vision-only and user-message-only; see "Vision". -
thinking:{type:'enabled'}(default) or{type:'disabled'}— thinking-mode toggle ONLY, does not carry the effort level (correction 2026-08-21: earlier revisions of this doc nestedreasoning_effortinsidethinking; the live page shows it as a sibling top-level field). Re-confirmed by probe 2026-08-25: the nested form is accepted with 200 and has no effect — see thereasoning_effortentry below for why that made the error survive so long. -
reasoning_effort(top-level, sibling ofthinking, NOT nested inside it): controls thinking depth. The enum has SEVEN values, not six —minimalwas missing from every earlier revision of this doc. Corrected 2026-08-25 (T679). The authoritative list is the API's own deserialization error, not the docs page:reasoning_effort:"bogus"ondeepseek-v4-proreturns HTTP 400 with, verbatim,Failed to deserialize the JSON body into the target type: reasoning_effort: unknown variant `bogus`, expected one of `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` at line 1 column 97The docs page is NOT the source here and disagrees in both directions.
/api/create-chat-completiondisplays only three values but states the aliasing verbatim: "Possible values: [low,high,max] — Controls the reasoning effort of the model. The default effort ishigh. For compatibility,mediumandxhighare mapped tohigh."/guides/thinking_modeshows{"reasoning_effort": "low/high/max"}in its toggle table yet its footnote (2) maps FIVE inputs (low,medium,high,xhigh,max). Neither page displaysnoneorminimalanywhere. So the 2026-08-13 changelog's "three thinking effort levels: low / high / max" named the canonical BUCKETS, it did not remove the aliases — do not narrow this list to three.Anthropic-format equivalents (verbatim from
/guides/thinking_mode's "Thinking Mode Toggle and Effort Control" table):Purpose OpenAI format Anthropic format Responses API format Thinking-mode toggle {"thinking":{"type":"enabled"/"disabled"}}{"reasoning":{"effort":"none/low/high/max"}}(nonedisables thinking mode){"reasoning":{"effort":...}}Thinking effort {"reasoning_effort":"low/high/max"}{"output_config":{"effort":"low/high/max"}}{"reasoning":{"effort":...}}—effortsupported,summaryaccepted but never generatedNesting
reasoning_effortinsidethinkingis silently ignored (live-probed 2026-08-25).{"thinking":{"type":"enabled","reasoning_effort":"none"}}and..."max"both return 200 and both land on the DEFAULT bucket; even{"thinking":{"reasoning_effort":"bogus"}}returns 200, where the same bogus value at top level 400s. There is no error and no effect — the exact failure mode that let an earlier revision of this doc document the nested shape for months. Put it at top level. -
reasoning_effort— what the live probe measures, and the two metrics it takes to see it. Re-probed 2026-08-25 (T679) on both v4 models, prompt fixed at"What is 2+2?",max_tokensfixed. Two metrics, each answering a different question; conflating them gets the grouping wrong.(a) Bucket membership —
prompt_tokenson a fixed short prompt, i.e. the size of the boilerplate DeepSeek injects for a given effort. Fully deterministic: 3 runs x 7 values x 2 models produced 42/42 identical readings, and the numbers reproduce on a second machine with only a constant offset for prompt length (that run, on a 5-token-shorter prompt, read 6 / 85 / 98 with omitted=85):reasoning_effortprompt_tokens(v4-pro)prompt_tokens(v4-flash)Bucket none11 11 injects nothing minimal11 11 injects nothing low11 11 injects nothing medium90 90 identical to high, to the tokenhigh90 90 identical to medium/xhigh, to the tokenxhigh90 90 identical to high, to the tokenmax103 103 its own value omitted 90 90 lands on the highbucketthinking:{type:'disabled'}11 11 (control — same as the no-injection bucket) Two things this table settles that prose could not. The
medium/xhighcells matchinghighto the token is a deterministic confirmation of the schema page's "medium and xhigh are mapped to high", not an inference from it. And omitted landing on that same 90 is stronger evidence that the default ishighthan the page's own sentence — it is the observable, and it agrees.What this metric CANNOT do: separate
nonefromlow, orminimalfromlow. All three inject nothing and are indistinguishable at 11 tokens. Theminimal-vs-lowrelationship is therefore left unasserted here — nothing available separates them, and guessing would be the same mistake that produced the six-value list.(b) Is the parameter HONOURED at all — presence of
reasoning_content. This is the only signal that catches a silently-ignored effort field, the failure mode where a value is accepted, changes nothing, and looks identical to working. 4 runs each ondeepseek-v4-pro:reasoning_effortreasoning_contentlength, 4 runsnone0, 0, 0, 0 — and completion_tokens_details.reasoning_tokensabsent entirelyminimal115, 101, 61, 103 low113, 69, 103, 102 So
nonegenuinely disables CoT whileminimalandlowgenuinely produce it, even though all three are identical on metric (a).NEVER use reasoning-TOKEN counts for either job. On an easy prompt they do not separate the buckets at all: an independent run measured
low=81,medium=24,high=62,xhigh=75,max=33 — pure sampling noise, ordered wrongly, and reproducible only as noise.Cyberine's
LoopAgentAdapternever sets this field today; since T692 (2026-08-25) it also no longer forcesthinking:{type:'disabled'}on the tool path — see "Thinking mode + tool calling" in Gotchas for what actually constrains that combination.reasoning_effortis accepted alongsidetools+tool_choice:'auto'at every level (minimal|low|medium|highprobed live, all 200 with a correct tool call).reasoning_effort=lowis accepted end-to-end with no code change needed — grepped the wholesrc/tree for a rejecting enum/zod schema around DeepSeek's ownreasoning_effortand found none (the one'high'|'xhigh'|'max'-shapedSetinconfig.tsgates Claude Code CLI's unrelated--effortflag, and already includes'low'; OpenCode's--variantflag is a free string with no enum gate either). -
stream+stream_options.include_usage— Cyberine sets both; usage (incl.reasoning_tokens) appears in the final chunk -
max_tokens(caps total output including CoT tokens — so in thinking mode a tight cap can be spent entirely on CoT and returnfinish_reason:'length'with NO tool call at all; measured thresholds in "Thinking mode + tool calling" below),temperature(default 1; unsupported in thinking mode — though it is silently ACCEPTED, not rejected, alongsidetools),top_p -
tools(max 128),tool_choice(none/auto/required/named). In thinking mode onlynone,autoand an omittedtool_choiceare accepted:requiredAND a named function both return HTTP 400"Thinking mode does not support this tool_choice". See Gotchas. -
response_format.type:text|json_object -
stop(≤16),logprobs,top_logprobs(≤20),user_id(charset[a-zA-Z0-9\-_], ≤512 chars; also drives KVCache and scheduling isolation),prefix(beta) -
frequency_penaltyandpresence_penaltyare DEPRECATED and no-op. The schema page marks bothdeprecatedwith, verbatim, "This parameter is no longer supported. It will not take effect if you pass it to the API." They are still ACCEPTED, not rejected:frequency_penalty:1.5andpresence_penalty:1.5each returned HTTP 200 ondeepseek-v4-pro(live-probed 2026-08-25). So passing them is silent dead weight, never an error. Independently,/guides/thinking_modelists them alongsidetemperature/top_pas having no effect in thinking mode. -
prompt_tokens_details.cached_tokensis returned inusageon every chat-completions response — present in all ~60 live probe responses in the T679 pass, including non-thinking, vision and zero-cache-hit calls. It is the OpenAI-shaped mirror ofprompt_cache_hit_tokensand is documented nowhere on api-docs.deepseek.com (/guides/kv_cachenames only the twoprompt_cache_*fields). Treat it as observed-but-unspecified: safe to read, not safe to rely on.
Notable features
Every claim in this section was re-read against its own guide page on 2026-08-25 (T679); where the page has changed since this doc last quoted it, the delta is called out inline.
- Streaming: SSE, terminates with
data: [DONE]on Chat Completions. The Responses API is different — it has NO[DONE], it ends withresponse.completed/response.incomplete/response.failed. See "Responses API". - Reasoning/Thinking: both v4 models support dual mode. CoT streams as
chunk.choices[0].delta.reasoning_content(a DeepSeek extension, not in the OpenAI SDK delta type — Cyberine reads it via a cast). - Tool calling: supported, including with thinking left enabled —
tool_callsandreasoning_contentcome back in the same message (measured 2026-08-25, T692; full matrix in Gotchas)./guides/tool_callsAGREES on this point and always did: "From DeepSeek-V3.2, the API supports tool use in the thinking mode." (Correction 2026-08-25/T679: an earlier revision of this bullet said "DeepSeek's own docs say otherwise and are wrong on this point" — that is true only of thereasoning_content-echo 400, documented under Gotchas, not of tools-with-thinking as such.) The one real restriction is ontool_choice, not on tools as such. - Strict tool schemas (
/betabase URL +"strict": trueper function): re-read 2026-08-25./guides/tool_callsnow states explicitly that strict mode "is supported by both thinking and non-thinking mode" — this doc previously left that open. Supported JSON-Schema surface, verbatim from the page:object,string,number,integer,boolean,array,enum,anyOf, plus$ref/$deffor reuse and recursion. Per-type limits worth knowing before writing a schema:stringsupportspatternandformat(email,hostname,ipv4,ipv6,uuid) but notminLength/maxLength;arraydoes not supportminItems/maxItems;number/integersupportconst,default,minimum,maximum,exclusiveMinimum,exclusiveMaximum,multipleOf. Every object must set all propertiesrequiredandadditionalProperties: false. - Context caching: on by default; reported as
prompt_cache_hit_tokens/prompt_cache_miss_tokens(NOT the Anthropic/OpenAIcache_read_input_tokens). The hit rule changed and/guides/kv_cachenow documents it in terms of Sliding Window Attention: a cached prefix is "an independent, complete unit" and "a subsequent request can only hit the cache if it fully matches a cache prefix unit" — it is no longer a plain longest-common-prefix match. Units are persisted at request boundaries (one at the end of the user input, one at the end of the model output), by common-prefix detection across multiple requests, and at fixed token intervals for long inputs/outputs. Consequence for a fan-out: two requests sharing prefixAbut diverging (A+B,A+C) BOTH miss; only after the system notices the sharedAand persists it does a thirdA+Dhit. Best-effort, never guaranteed; construction takes seconds, eviction "usually within a few hours to a few days". - JSON mode (
response_format:{"type":"json_object"}): re-read 2026-08-25. Two page-stated requirements this doc previously omitted — the prompt must contain the word "json" (system or user) and should show an example of the desired shape, andmax_tokensmust be set high enough that the JSON is not truncated mid-string. The page also carries a standing caveat, verbatim: "When using the JSON Output feature, the API may occasionally return empty content." - FIM Completion (Beta) — non-thinking only,
/betabase URL, and not at all ondeepseek-v4-flash-vision-exp(its live feature row reads "Not supported"). Newly quoted here: "The max tokens of FIM completion is 4K", independent of the models' 384K normal ceiling. - Chat prefix completion (Beta) —
/betabase URL; the LAST message must haverole: "assistant"andprefix: true. Re-read 2026-08-25, unchanged. - Vision:
deepseek-v4-flash-vision-exponly. Three input paths, a hard 384-token-per-image ceiling, and no separate image-token billing axis — see the dedicated section below. - Files API:
POST/GET/DELETE /files,purpose=user_data,file-api-*ids — see below. - Responses API: natively supported, with a documented Codex integration — see below.
- OpenAI-SDK and Anthropic-SDK compatible (swap base URL).
Vision
deepseek-v4-flash-vision-exp is the only id that accepts images; source
https://api-docs.deepseek.com/guides/vision, re-read and live-probed 2026-08-25 (T679).
DEALIASED 2026-09-11 (k1k86) — accepts but does not see. This is now the only id
isMultimodalModel()inworkerProfiles.tsdoes NOT treat as vision-capable, because it no longer is:GET /modelsdelisted it (k9tvx, same day) and it resolves on the wire todeepseek-flash, which has never accepted images. A live probe carrying an actual image part got HTTP 200 and a confident but ungrounded answer, not a rejection — the everything-below-this- callout describes the format that used to work and may still be accepted syntactically, but the model behind the id no longer reads any of it. Do not pin this id for vision; there is currently no DeepSeek-direct id that genuinely accepts images.
Supported formats: JPEG, PNG, GIF, WebP — "detected from the actual file content, not from the file name or the declared MIME type."
Three input paths (all on the standard content-array shape, all also available on the
Responses API as input_image parts):
| Path | Shape | Ceiling |
|---|---|---|
| Inline base64 | {"type":"image_url","image_url":{"url":"data:image/png;base64,..."}} | 32 MiB per image; counts toward the 48 MiB body limit |
| Public https URL | {"type":"image_url","image_url":{"url":"https://..."}} | URL ≤8192 chars, image ≤32 MiB, download must complete within 60 s |
Files API file_id | {"type":"file","file_id":"file-api-..."} | up to 64 MiB — "not subject to the 32 MiB per-image check" |
A file block may alternatively carry the bytes inline as file_data (a data: URL) plus
filename; file_data and file_id are mutually exclusive, and filename is not allowed
together with file_id.
detail (on image_url inputs only, ignored when the image comes from a file_id):
| Value | Behavior (verbatim) |
|---|---|
low | "The image is downscaled to 512×512 before inference. Faster and cheaper when fine visual detail is not important." |
high | "Keeps the original image. (Provided for compatibility; equivalent to original.)" |
original | "Keeps the original image." |
auto | "Automatic selection. Currently equivalent to original." |
So only low actually changes anything today; the other three are the same behaviour under three
names. Live-probed: detail:"low" returns 200.
Token usage — a hard 384-token ceiling per image, billed as ordinary input tokens. Verbatim:
"Images are converted into tokens based on their dimensions, and these tokens are billed together
with your text tokens." Every image is resized before inference — anything under roughly 384×384
is scaled UP, anything larger is scaled DOWN to roughly an 800×800 pixel budget, both preserving
aspect ratio — so "there is an upper bound of 384 tokens per image: for example, a 2000×2000 image
and a 5000×5000 image consume the same number of tokens after resizing." Multiple images are each
counted independently under the same rule; there is no multi-image discount or special case. The
docs ship an interactive image-token calculator on /quick_start/token_usage for a specific
width/height.
There is NO image-specific usage field. Live dispatch 2026-08-25 (T679), one 8×8 RGB PNG plus
a one-line text prompt to deepseek-v4-flash-vision-exp, HTTP 200; the entire usage object was:
{
"prompt_tokens": 203,
"completion_tokens": 32,
"total_tokens": 235,
"prompt_tokens_details": { "cached_tokens": 0 },
"completion_tokens_details": { "reasoning_tokens": 32 },
"prompt_cache_hit_tokens": 0,
"prompt_cache_miss_tokens": 203
}No image_tokens key or any variant, anywhere in usage or in the response body; the image cost
is folded into prompt_tokens, and prompt_cache_miss_tokens equals prompt_tokens exactly.
(T683's earlier probe of the same shape read prompt_tokens: 211 with a slightly different prompt
— that object is the one pinned in the smoke; this run re-confirms the SHAPE, which is what
matters.) Consequence for cost accounting: nothing image-specific to track — see Pricing.
Limits (verbatim table from /guides/vision):
| Limit | Value |
|---|---|
| Supported formats | JPEG, PNG, GIF, WebP |
| External URL length | 8192 characters |
| Request body size | 48 MiB |
| Max single image size (base64 / external URL) | 32 MiB |
Max single image size (Files API file_id) | 64 MiB |
| Max images per request | 600 |
| Max total image size per request | 64 MiB without file_id images; up to 200 MiB including file_id images |
| Max image dimension | 8192 px per side; drops to 4096 px per side when a request contains 15 or more images |
Restrictions — both live-confirmed 2026-08-25 with the exact wire message:
| Violation | HTTP | Verbatim message |
|---|---|---|
Image in a system message | 400 | Image in system message is unsupported |
Image on deepseek-v4-pro / -flash | 400 | This model does not support image |
Images are accepted in user messages only — the page states system and assistant both 400 (only the system case was probed here). User text containing DeepSeek's reserved image-placeholder token is also rejected with a 400.
Anthropic format. The same three paths exist on https://api.deepseek.com/anthropic, with an
image block carrying a source object instead of image_url: source.type is base64
(requires media_type), url (≤8192 chars), or file (requires header
anthropic-beta: files-api-2025-04-14).
Routing scope — DECIDED 2026-08-28 (T689): manual pin, never a routed capability
CLOSED 2026-09-11 (k1k86): the manual-pin escape hatch this section describes no longer has a real destination.
deepseek-v4-flash-vision-expdealiased todeepseek-flash, which does not see images (see the "Vision" section callout above).isMultimodalModel()no longer recognises this bare id, so a manual pin now hits LoopAgentAdapter's text-only branch (visible "images not forwarded" note) instead of the silent-blind-answer behaviour the probe measured. Separately, any env-based pin (DEEPSEEK_MODELetc.) already hard-stops at CLI startup —config.ts'sDEEPSEEK_DIRECT_DEAD_MODEL_IDScovers this id since k9tvx. There is currently no DeepSeek-direct id this repo can point either mechanism at; the four reasons below (why vision stayed a manual pin rather than a routed axis) are kept for history but their premise — that a working manual pin exists — no longer holds.
DeepSeek vision is a manual-pin escape hatch. It will not become a routed capability. T680
already shipped the whole manual-pin half: isMultimodalModel() recognises
deepseek-v4-flash-vision-exp, so a pinned model carries its images through instead of silently
dropping them, and the TUI's @-mention attach path (ChatApp.tsx) gates on the same function.
That is the supported way to use DeepSeek vision, and it is sufficient. Four reasons, in the order
that decided it:
- The routed axis would have to be selected against the user's own tier signal.
variantsis keyed by task TIER (reasoning/flash) and resolved byresolveVariantModels()inrouter.ts. Avariants.visionaxis would have to WIN over the tier pick, so a complex image task would be silently downgraded fromdeepseek-v4-proto an experimental flash id — trading reasoning depth for image reading without the router having any way to know which the caller wanted. Vision is not a tier, so it does not belong on an axis selected by tier. - The one vision id is experimental and capability-reduced.
deepseek-v4-flash-vision-expcarries an-expsuffix and is the only DeepSeek id where FIM Completion is "Not supported". Routing to it autonomously would make an experimental model the automatic answer for a whole input shape. - The naive shortcut stays un-taken, and the gate is already safe. No DeepSeek profile
declares
multimodal: true(verified: the eight that do are the kimi / openai / gemini / xai / azure / copilot / claude / gpt build workers). So thehasImagesgate inroute()already filters every DeepSeek worker out of an image task, and the hard 400 the task warned about cannot happen. Declaringmultimodal: trueonloop-deepseek-build— the shortcut — is what would CREATE that bug, because the tier variant would then resolvedeepseek-v4-pro. - There is no consumer. Images reach this repo only through a human pinning a model, which is
the path that already works. Adding a shared-type change across every profile that declares
variants, plus crossing the image gate with variant resolution, plus theadapterMultimodalcall sites inbuildAdapters.ts, buys nothing that anything asks for today.
What would reopen it: a dispatch surface that receives images without a human choosing a model — e.g. an MCP or web-API caller attaching a screenshot to a task — AND a non-experimental DeepSeek vision id that is reachable from both tiers. Both, not either.
Files API
Source https://api-docs.deepseek.com/guides/files_api, re-read and live-probed end-to-end 2026-08-25 (T679): upload → list → retrieve → reference in a chat call → delete, every step 200.
-
POST /filesismultipart/form-data. Required fields:file, andpurpose— which must beuser_data(purpose=assistantsreturns 400The 'assistants' purpose is not supported. Supported purposes: ["user_data"]). Optional expiry is a PAIR:expires_after[anchor](must becreated_at) andexpires_after[seconds], between3600and2592000(1 hour to 30 days). Omit both to keep the file permanently. Upload must complete within 10 minutes. -
It returns HTTP 200, not 201. Confirmed live; worth pinning because the OpenAI-shaped create-a-resource convention would suggest 201 and a strict client could reject the real answer.
-
Ids are
file-api-<uuid>, e.g.file-api-adc4918a-6eda-4db9-8d20-00b90082631a— that is the literal prefix, not a placeholder. Live upload response:{"id":"file-api-adc4918a-...","object":"file","bytes":165,"created_at":1787599739, "filename":"t679-8x8.png","purpose":"user_data"}With
expires_afterset, the response gains"expires_at": <unix ts>; it is absent otherwise. -
GET /filestakesafter(afile_idcursor),limit(1–1000, default 1000),order(ascdefault /desc) andpurpose. Returns{"object":"list","data":[...],"first_id", "last_id","has_more"}. -
GET /files/{id}returns the same file object.DELETE /files/{id}returns{"id":...,"object":"file","deleted":true}. -
Reference it from a chat request with a
filecontent block —{"type":"file","file_id":"file-api-..."}— ondeepseek-v4-flash-vision-exp. Live-probed: 200. Files belong to the API key and can be referenced from either API family.
Limits (verbatim): max upload 64 MiB; max filename 512 characters; max storage per user 25 GiB; max stored files per user 10000; expiration 1 hour to 30 days, or permanent.
Anthropic-compatible variant at https://api.deepseek.com/anthropic/v1/files, requiring
anthropic-beta: files-api-2025-04-14 on every request, differs in shape: pagination is
after_id/before_id (mutually exclusive) instead of after; limit defaults to 20 instead of
1000; order/purpose are not supported; there is no top-level "object":"list"; the size field
is size_bytes not bytes; the type field is type not object; created_at is an RFC 3339
string not a Unix timestamp; and a delete returns {"id":..., "type":"file_deleted"}.
Cyberine touch-point: none today. No adapter, tool or judge uploads files or sends images —
LoopAgentAdapter builds text-only content strings. This section is reference for the first
caller that needs it, not a description of shipped behaviour.
Implementation scope — DECIDED 2026-08-28 (T690): deliberately DEFERRED
Files API support is deliberately not built. This is a recorded deferral, not an oversight, and not "we never decided". The 2026-08-24 audit's recommendation is taken as-is.
What building it would take, so the estimate is not re-derived: a small Files client
(upload / list / get / delete), a TaskImage or new TaskFile variant carrying a fileId (today
TaskImage in src/core/types.ts has no fileId or purpose field), and a LoopAgentAdapter
branch emitting a {type:'file', file:{file_id}} content part — the openai SDK already exports
that part type, the adapter just imports only the text and image ones.
Why defer: inline base64 covers every case this repo has. The Files API exists for exactly two
things neither of which has a consumer here — reusing one image across many requests without
re-uploading it, and a single image above the 32 MiB inline ceiling (64 MiB via file_id).
Building an upload lifecycle, including expiry and deletion, for zero callers means owning a
stateful remote resource with no test that exercises it.
The two triggers that reopen this, either one being sufficient:
- Cross-request image reuse — a caller sends the same image to more than one request (a judge panel over one screenshot, a multi-round vision loop), where re-uploading base64 per call is the actual cost.
- A single image over the 32 MiB inline cap — base64 simply cannot carry it, so
file_idis not an optimisation but the only path.
Responses API (native) and the Codex integration
DeepSeek natively serves the OpenAI Responses API at https://api.deepseek.com — shipped for
deepseek-v4-flash on 2026-07-31 and for deepseek-v4-pro on 2026-08-13, both changelog entries
saying it was "specifically adapted for Codex". Sources
https://api-docs.deepseek.com/guides/responses_api and
https://api-docs.deepseek.com/quick_start/agent_integrations/codex; live-probed 2026-08-25 (T679).
It is a real endpoint, not a shim. POST /responses and POST /v1/responses both returned
200. client.responses.create(model=..., instructions=..., input=...) works with the stock
openai SDK pointed at https://api.deepseek.com.
Streaming is NOT Chat-Completions-shaped. Semantic SSE events, each with an event type and a
monotonic sequence_number; there is no data: [DONE] — the stream ends with
response.completed, response.incomplete or response.failed, and that final event carries the
full response object including usage. Text arrives as response.output_text.delta, CoT as
response.reasoning_text.delta, tool args as response.function_call_arguments.delta.
Usage shape differs from Chat Completions — live-observed:
usage.input_tokens / input_tokens_details.cached_tokens / output_tokens /
output_tokens_details.reasoning_tokens. The prompt_cache_hit_tokens / prompt_cache_miss_tokens
pair does not appear on this surface, so any cost code that reads those two fields silently
sees nothing here.
Unsupported parameters are silently ignored, never rejected (verbatim: "Unsupported parameters
are silently ignored and do not cause errors, so existing Responses API clients can connect without
modification"). Live-confirmed: a request sending store:true, metadata:{"a":"b"} and
parallel_tool_calls:false returned 200 with store:false, metadata:{} and
parallel_tool_calls:true in the response. Read that as a hazard, not a convenience — a
misconfigured client gets a plausible answer instead of an error.
Not supported at all (from the page's compatibility table): previous_response_id and
conversation (the API is stateless), store, background, metadata, include, prompt,
truncation (an over-length request 400s instead), service_tier, safety_identifier,
prompt_cache_key / prompt_cache_retention, context_management, stream_options. Partially
supported: tools (only function and web_search), reasoning (effort yes, summary
accepted but never generated), text (format yes, verbosity accepted but inert). Ignored:
parallel_tool_calls (always on), max_tool_calls.
Server-side web_search is genuinely executed by DeepSeek (web_search /
web_search_2025_08_26); search_context_size and user_location are ignored and server-side
auto-continuation is capped at 10 rounds. DISAGREEMENT, unresolved: re-read on 2026-09-19, the
vendor's compatibility table at https://api-docs.deepseek.com/guides/responses_api lists
web_search together with file_search, code_interpreter, computer_use and mcp as
"Ignored", and the Codex guide sets web_search = "disabled" for DeepSeek models. Treat
server-side search as unconfirmed until a live Responses request shows source URLs in its output. The only custom tool accepted is
{"type":"custom","name":"apply_patch"} — for Codex compatibility; any other custom name 400s.
Codex integration. All Codex clients (CLI, ChatGPT desktop app, VS Code extension) share
~/.codex/config.toml. DeepSeek ships a one-click setup script —
bash <(curl -fsSL https://cdn.deepseek.com/api-docs/codex-deepseek-setup-en.sh) on macOS/Linux,
irm https://cdn.deepseek.com/api-docs/codex-deepseek-setup-en.ps1 | iex on Windows — which backs
up config.toml, writes a ~/.codex/models.json model catalog and adds a
[model_providers.deepseek] section (menu option 9 restores the pre-install state). The manual
equivalent:
model = "deepseek-v4-flash"
model_provider = "deepseek"
preferred_auth_method = "apikey"
forced_login_method = "api"
model_reasoning_effort = "high"
model_catalog_json = "~/.codex/models.json"
[model_providers.deepseek]
name = "deepseek"
base_url = "https://api.deepseek.com/"
wire_api = "responses"
experimental_bearer_token = "<your DeepSeek API Key>"Two details from the shipped models.json worth recording: each DeepSeek entry declares
"context_window": 1048576 and "supported_reasoning_levels": [low, high, max] — the same three
canonical buckets, again without the four aliases — and only the deepseek-v4-flash-vision-exp
entry declares "image" in input_modalities, which is what tells Codex that model takes images.
Cyberine touch-point: none today, and this is the notable gap. LoopAgentAdapter speaks Chat
Completions exclusively, and Cyberine's codex-cli worker (docs/providers/codex-cli.md) drives
Codex against its own OpenAI auth, not against DeepSeek. Routing a Codex worker at DeepSeek via
wire_api = "responses" is a real, documented, upstream-supported option that nothing in this
repo uses yet.
Driving Codex against DeepSeek (T682)
DeepSeek documents a Codex integration (changelog 2026-08-13,
https://api-docs.deepseek.com/quick_start/agent_integrations/codex) using wire_api = "responses".
Cyberine needs no adapter change to use it. The full operator recipe — including why DeepSeek's
own setup script must NOT be run, how CODEX_HOME scopes it safely, the undocumented models.json
schema, and the go/no-go on registering it as a standing worker — lives in
codex-cli.md, section "DeepSeek-backed Codex worker". Verified 2026-08-25 against
codex 0.149.0 with a recorded run (44320a3b).
Two Responses-API constraints worth repeating here: DeepSeek is fully stateless
(previous_response_id, conversation, store, background are all "Not supported"), and the
custom tool type is restricted to {"type":"custom","name":"apply_patch"} — which happens to be
exactly the tool Codex's own file-edit path uses, so that works, while any other custom or MCP tool
routed through DeepSeek is dropped.
Gotchas
-
Legacy aliases: delisted everywhere, still answered on the wire (live-probed 2026-08-25, T679). This corrects a claim this file and
src/core/config.tsboth carried.deepseek-chatanddeepseek-reasonerare absent fromGET /models, absent from the/api/create-chat-completionmodelenum, and absent from the 400 message an unknown model produces — and yetPOST /chat/completionsserves them:Request modelHTTP response.modelprompt_tokens("hi")reasoning_contentdeepseek-chat200 deepseek-v4-flash5 none (non-thinking) deepseek-reasoner200 deepseek-v4-flash84 present (thinking) deepseek-v4-flash(control)200 deepseek-v4-flash84 present deepseek-nonexistent-xyz(control)400 — — The supported API model names are deepseek-v4-pro, deepseek-v4-flash, and deepseek-v4-flash-vision-exp, but you passed deepseek-nonexistent-xyz.Two runs of every row, identical both times. The unknown-model control is what makes this conclusive: a genuinely dead id 400s with a message that names only the three V4 ids, so the aliases are not merely "not yet cleaned up from an error path" — they are actively routed. Both now collapse onto
deepseek-v4-flash, differing only in the thinking default they preselect (5 vs 84prompt_tokenson the same prompt is exactly the thinking-boilerplate injection measured in thereasoning_effortbucket table). Practical consequences: (1) a legacy pin does NOT fail fast — it silently bills you for flash while your config claims otherwise, which is worse than a 400, and is the real argument for keeping T681's startup hard-stop; (2) adeepseek-reasonerpin no longer gets a distinct reasoner model; (3) an undocumented alias can be withdrawn with no changelog entry, so this row must be re-probed each sweep rather than assumed stable in either direction. -
reasoning_contentecho policy — empirically NEITHER direction reproduces an HTTP 400, live-probed 2026-08-24 (T684). DeepSeek's own/guides/thinking_modestates twice, verbatim, that a tools-carrying request which fails to echoreasoning_contentback on every subsequent turn — "even for turns where the model did not perform a tool call" — "will return a 400 error". That claim is unreproduced as of 2026-08-24 across two probe passes ondeepseek-v4-pro:Probe Transport Calls Tools Thinking reasoning_contentechoPeak context ( prompt_tokens)Result Shallow (cyber-011/ds-params-probe) non-streaming 6 present / absent default echoed / omitted ~100-400 200, 6/6 Deep A — production shape streaming SSE 24 (12 cycles) present disabled(whatLoopAgentAdapteractually sends)n/a — never produced 172,962 200, 24/24 Deep B — guide's required shape streaming SSE 24 (12 cycles) present enabled (default) echoed every turn 173,088 200, 24/24 Deep C — guide's claimed 400 trigger streaming SSE 24 (12 cycles) present enabled (default) omitted every turn 172,981 200, 24/24 The deep probes forced a tool call every cycle (
tool_choice:'auto'— DeepSeek 400s'required'outright with"Thinking mode does not support this tool_choice", an unrelated but real constraint) and grew context via a large opaquepaddingfield on tool results (the model was told to ignore it), specifically to clear the ~150K-token depth an independent report (deepseek-harness discussion #4217) named as where a 400 starts appearing on every subsequent request. Zero 400s appeared in any cell, at any depth reached, in either echo direction. See.local/tasks/ACTIVE.mdT684 for the full task record; the probe scripts are throwaway (.local/tmp/ds-explore.ts,.local/tmp/ds-reasoning-replay-probe.ts, gitignored, left in place for re-run but not guaranteed to survive routine.local/cleanup).T692 addendum (2026-08-25) — the question is now LIVE, and the answer still holds. T684's verdict was safe by construction for Cyberine: production sent
thinking:{type:'disabled'}whenever tools were present, soreasoning_contentwas never produced and there was nothing to echo or omit. T692 removed that gate, so the field IS now emitted on tool-call turns and the echo policy became a real production decision. Re-probed under the new shape — thinking enabled alongside tools, both v4 models, 108 requests across 14 scenarios, zero non-200s:Scenario Echo direction Shape Result 6-round loop with an intermediate assistant turn carrying NO tool call and NO reasoning_contentomitted streaming 200, 6/6 (this is the exact shape DeepSeek's docs name as a 400: "even for turns where the model did not perform a tool call") same echoed every turn streaming 200, 6/6 12-round loop, tool results padded to ~6K prompt tokens omitted streaming 200, 12/12 same echoed every turn streaming 200, 12/12 8-round loop, echo on alternating turns (worst case for a strict validator) inconsistent streaming 200, 8/8 6-round loop omitted non-streaming 200, 6/6 4-round loop with stale reasoning_contentechoed intothinking:{type:'disabled'}requestsechoed streaming 200, 4/4 (mixed-mode history is accepted) needsReasoningReplaytherefore staysfalsefor DeepSeek — now on evidence rather than vacuity. Omitting the echo is also cheaper: the CoT never enters any later prompt. DeepSeek's documented 400 remains unreproduced in either direction, at any depth reached, in either transport. Only appendcontentto history in production (this is still the opposite gate direction from Moonshot Kimi's contract — see the integration note below). -
A round that streams only
reasoning_content(emptycontent, no tool calls) must never be replayed as an assistant turn — observed 2026-09-18 in autopilot,LOOP-EMPTY-ASSISTANT-TURN. Becausereasoning_contentis deliberately not echoed back (see above), a reasoning-only round left the rebuilt assistant message withcontent:nulland notool_calls, and the following request 400'd withInvalid assistant message: content or tool_calls must be set.LoopAgentAdapter.tsnow skips pushing that turn into the context entirely instead of pushing a null-content placeholder. -
temperature/top_punsupported in thinking mode — omit them.
Thinking mode + tool calling — VERDICT (T692, live-probed 2026-08-25)
Tool calling works with thinking ENABLED. The old "tools are incompatible with thinking mode" claim is FALSE and the hasTools -> thinking:disabled workaround it justified has been removed. What is real is a narrower constraint on tool_choice, plus a budget interaction with max_tokens. 312 live requests against api.deepseek.com, both v4 models; the ONLY non-200s in the entire sweep were the four tool_choice cells called out below.
1. Coexistence — tools and thinking in the same response (24 cells, all 200, all correct). With tool_choice:'auto' and a multiply(17,23) task, every cell returned finish_reason:'tool_calls' with correct arguments AND non-empty reasoning_content in the same message:
| Dimension varied | Values probed | Result |
|---|---|---|
| Model | deepseek-v4-pro, deepseek-v4-flash | 200 + tool_calls + reasoning_content, both |
thinking field | omitted (default), {type:'enabled'}, {type:'disabled'} | 200 + correct call in all three; reasoning_content non-empty for the first two, empty for disabled |
| Transport | streaming SSE, non-streaming | 200 + correct call both ways; streamed CoT arrives as delta.reasoning_content |
| Tool count | 1, 12 (1 real + 11 decoys), 32 (1 real + 31 decoys) | 200 + the RIGHT tool picked at every count |
| Schema complexity | flat 2-int schema; deeply nested (nested objects, array-of-objects, enums, 3 levels of required) | 200 + valid nested arguments |
reasoning_effort | minimal, low, medium, high (with tools, thinking on) | 200 + correct call at every level |
temperature | 0.2 with thinking on + tools | 200 (accepted, not rejected, despite the docs calling it unsupported) |
| Multi-round loop | 5 rounds, 12 rounds (padded to ~6.8K prompt tokens) | 200 on every round; reasoning_content on every tool-call round; the final answering round always put its answer in content, never only in reasoning_content |
2. The REAL constraint — tool_choice must not FORCE a call (the only 400s in the sweep). Verbatim message: Thinking mode does not support this tool_choice.
tool_choice | thinking enabled | thinking disabled |
|---|---|---|
'auto' | 200, finish_reason:'tool_calls' | 200, tool_calls |
omitted (with tools present) | 200, finish_reason:'tool_calls' | 200, tool_calls |
'none' | 200, finish_reason:'stop', no call (as asked) | 200, stop |
'required' | HTTP 400 "Thinking mode does not support this tool_choice" | 200, tool_calls |
{type:'function',function:{name}} (named) | HTTP 400, same message | 200, tool_calls |
Both rows reproduce identically on deepseek-v4-pro and deepseek-v4-flash. The named-function form 400ing is new information — T684 only recorded 'required'. So: any forcing tool_choice must keep thinking off; auto/none/omitted must not.
3. The budget interaction — max_tokens covers CoT AND the answer, so a tight cap can starve the tool call entirely. One hard multi-step planning task, one write_plan tool, streaming:
max_tokens | thinking enabled | thinking disabled |
|---|---|---|
| 256 | finish=length, 0 tool calls, 256 reasoning tokens | finish=length, 1 call, arguments TRUNCATED |
| 1024 | finish=length, 0 tool calls, 1024 reasoning tokens | finish=length, 1 call, arguments TRUNCATED |
| 4096 | finish=length, 0 tool calls, 4096 reasoning tokens | finish=tool_calls, 1 call, valid arguments |
| 16000 | finish=tool_calls, 1 call, valid arguments, ~3.5–3.9K reasoning tokens | finish=tool_calls, 1 call, valid arguments |
Same shape on both models. This is the one way leaving thinking on can make a round strictly worse than the old gate: below roughly 8K the whole budget goes to CoT and the round yields nothing to execute. It is NOT a 400 and NOT an error — just a wasted round. Cyberine's mitigation is DEEPSEEK_THINKING_MIN_MAX_TOKENS (see integration notes); the residual risk above the floor (an unusually long CoT still starving a round) is accepted and unmitigated.
4. What thinking BUYS on the tool path — no accuracy delta on an easy task, ~1.4–1.8x the completion tokens. A 3-step derived-operand word problem, 6 trials per cell:
| Cell | Correct | Avg rounds | Avg tool calls | Avg reasoning tokens | Avg completion tokens |
|---|---|---|---|---|---|
| pro, thinking enabled | 6/6 | 4.0 | 3.0 | 243 | 428 |
| pro, thinking disabled | 6/6 | 4.0 | 3.0 | 0 | 241 |
| flash, thinking enabled | 6/6 | 3.3 | 3.0 | 117 | 293 |
| flash, thinking disabled | 6/6 | 4.0 | 3.0 | 0 | 305 |
So the upside is not proven on easy work: both settings scored 6/6, and thinking costs real output tokens (billed as completion tokens). The case for enabling it is that the implementer fleet's actual tasks are much harder than this probe, and the gate was removing the option unconditionally. If DeepSeek spend matters more than reasoning quality for a given deployment, CYBERINE_DEEPSEEK_TOOL_THINKING=off restores the old always-disable behaviour with no code change.
5. Not re-probed / out of scope. Streaming vs non-streaming was covered at 1 and 5+ rounds but not at 150K+ context (T684 covered that depth with thinking enabled and found no 400). FIM's thinking-mode incompatibility was NOT re-probed — that remains a docs-only claim, and Cyberine does not use FIM. The /beta strict-schema base URL was not probed with thinking on. Prompt-cache behaviour with CoT in the prefix was not measured.
Probe scripts (throwaway, gitignored, may not survive .local/ cleanup): .local/tmp/ds-thinking-tools-probe.ts (phases A–E), .local/tmp/ds-replay-probe.ts (phase F), .local/tmp/ds-quality-probe.ts (phase G), .local/tmp/ds-budget-probe.ts (phase H); raw results in .local/tmp/t692-results-*.jsonl. The permanent guard is npm run selftest:deepseek-thinking-tools, which pins the request-body policy offline and additionally re-checks the live contract when DEEPSEEK_API_KEY is set.
- Strict tool calling needs the
/betabase URL + every propertyrequiredwithadditionalProperties:false. Supported/unsupported JSON-Schema keywords, and the page's own "supported by both thinking and non-thinking mode" statement, are in the Notable features bullet above (re-read 2026-08-25). - Streaming
usageonly present whenstream_options.include_usage:true.
Pricing
LIVE since 2026-08-16 16:00 UTC — peak/off-peak billing (confirmed 2026-08-21, knk0q; whole table + both footnotes re-read cell-by-cell 2026-08-25, T679 — unchanged). The
2026-07-10 doubt below has resolved: this is now DeepSeek's own documented pricing, not a
secondary-source report. Fetched directly (curl, not WebFetch's LLM summary — the raw HTML's
server-rendered pricing table was grepped verbatim) from
https://api-docs.deepseek.com/quick_start/pricing on 2026-08-21, and re-read column-by-column on
2026-08-25 (T683) when the vision model's rates were added. Verbatim table cells (the page now
carries THREE model columns: deepseek-v4-flash | deepseek-v4-pro |
deepseek-v4-flash-vision-exp; model versions shown: DeepSeek-V4-Flash-0731 /
DeepSeek-V4-Pro-0813 / DeepSeek-V4-Flash-Vision-Exp, GA 2026-07-31 / 2026-08-13 /
2026-08-21, ids unchanged):
| Per 1M tokens | OFF-PEAK (flash / pro / vision) | PEAK (flash / pro / vision) |
|---|---|---|
| Input, cache HIT | $0.007 / $0.022 / $0.007 | $0.014 / $0.044 / $0.014 |
| Input, cache MISS | $0.22 / $0.66 / $0.22 | $0.44 / $1.32 / $0.44 |
| Output | $0.66 / $1.98 / $0.66 | $1.32 / $3.96 / $1.32 |
| Concurrency limit | 2500 / 500 / 2500 | (same) |
The vision column is identical to the flash column on every single row — there is no vision surcharge and no separate image-token rate.
SUPERSEDED for Flash 2026-09-10, re-confirmed 2026-09-24. The table above is the pricing that launched with
DeepSeek-V4-Flash-0731on 2026-08-16 and is kept for history.DeepSeek-V4.1-Flash(changelog and pricing page, both re-read 2026-09-24) cut the flash rate to, per 1M tokens off-peak/peak: cache-hit $0.003/$0.006, cache-miss $0.15/$0.30, output $0.60/$1.20 — see thedeepseek-flashrow under "Models" above.deepseek-v4-flashanddeepseek-v4-flash-vision-expare legacy aliases now routed to V4.1-Flash and billed at this same new rate, so the flash and vision columns above are stale; thedeepseek-v4-procolumn is unaffected and still current.modelPricing.ts'sdeepseek-v4-flashrow is updated to match.
Footnote (1) verbatim (re-verified 2026-08-25, T685): "Off-peak rates are half of the peak
rates. Peak hours are 01:00 - 04:00 and 06:00 - 10:00 UTC, Monday through Friday (all other
hours are off-peak)." (Narrowed to weekdays-only since the 2026-08-21 quote above; see the
T685 anchoring note on isDeepSeekWeekday() in src/core/modelPricing.ts for the unresolved
UTC-vs-Beijing weekday-boundary question and why it turns out not to matter for these hours.)
Footnote (2) verbatim (read live 2026-08-25, T683): "Images sent to
deepseek-v4-flash-vision-exp are converted into tokens based on their dimensions and billed as
input tokens together with your text tokens." So image tokens need no separate accounting —
they arrive inside the ordinary cache-miss input axis the table above already prices. Confirmed
by a live dispatch rather than taken on the doc's word (T683: one 8x8 RGB PNG + a one-line text
prompt, HTTP 200), whose entire usage object was:
{
"prompt_tokens": 211,
"completion_tokens": 32,
"total_tokens": 243,
"prompt_tokens_details": { "cached_tokens": 0 },
"completion_tokens_details": { "reasoning_tokens": 32 },
"prompt_cache_hit_tokens": 0,
"prompt_cache_miss_tokens": 211
}No image_tokens field (or any variant) appears anywhere in usage or in the response body,
and prompt_cache_miss_tokens equals prompt_tokens exactly. That object is pinned verbatim in
scripts/knk0q-deepseek-peak-pricing-smoke.ts (section 11) as an object-shape assertion, so a
future provider-side change that DID introduce a separate image-token axis fails the smoke
instead of silently under-counting.
src/core/modelPricing.ts's direct DeepSeek rows model the cache-MISS input / output rate (this
table's existing single-rate convention, cache-HIT not modelled) and now carry both off-peak
(base) and peak (.peak) figures; getModelPricing()/computeChatCost() pick peak vs off-peak
from a Date (UTC hour, default now()) — time-of-day-aware, not a flat conservative bump.
src/store/creditLedger.ts's SEPARATE provider-level PROVIDER_RATES table is what actually feeds
estimateCostUsd/recordSpend for real LoopAgentAdapter dispatches (and therefore the
Orchestrator.ts / WorkflowEngine spend-cap gate -- modelPricing.ts only feeds the oracle tool and
the TUI cost chips directly). Fixed in the same wave (2026-08-21, knk0q follow-on, commit
27cd286): estimateCostUsd() now takes an optional modelId + at and, when modelPricing.ts
has a row for the model, bills at that per-model time-of-day-aware rate; LoopAgentAdapter passes
its model id on the own-cost path. The provider-level PROVIDER_RATES.deepseek fallback (used when
the model id is unknown or absent) was raised from the pre-V4 flat $0.27/$1.10 to the PEAK
deepseek-v4-pro rate $1.32/$3.96 so it can never under-count a DeepSeek direct worker. Guarded by
npm run selftest:deepseek-peak-pricing (section 8).
Vision row added 2026-08-25 (T683). deepseek-v4-flash-vision-exp previously had NO row in
modelPricing.ts at all, so getModelPricing() returned undefined and computeChatCost()
silently reported $0 for every vision dispatch (same silent-zero failure mode as the
pre-T645 MiniMax-M3 gap). It now mirrors the flash row exactly, off-peak and peak.
PROVIDER_RATES.deepseek in creditLedger.ts needed no change: it holds pro's PEAK rate as
the conservative ceiling and the vision model shares flash's cheaper rate, so the ceiling still
cannot under-count — asserted directly by the "provider fallback >= every DeepSeek direct row at
peak, vision included" check in section 8 of the smoke. Sections 10-11 cover the new row (its
existence, the flash-parity claim, the peak/off-peak flip, the weekday gate, the spend-cap path,
and the pinned live usage object).
Time-of-day-aware routing (decision record)
DECISION (2026-08-26): DECLINED — the router does NOT re-rank or skip DeepSeek workers
during the peak window. The peak/off-peak signal stays where it already is: it feeds
cost estimation (getModelPricing() / computeChatCost() in src/core/modelPricing.ts,
the credit ledger's estimateCostUsd(), the spend cap, and the TUI cost chips), and it
does NOT drive fleet selection in src/core/router.ts. This section records why, and what
would change the call.
The saving is a fraction of a cent per task. The peak multiplier doubles DeepSeek's rates (flash $0.44/$1.32 peak vs $0.22/$0.66 off-peak per 1M tokens; pro $1.32/$3.96 vs $0.66/$1.98). A typical implement task — tens of thousands of input tokens, a few thousand output — costs on the order of $0.01-$0.04 total, so the entire peak-window saving is pennies, and only if the task actually lands inside the window.
The comparable off-peak workers are not actually interchangeable. The preferred pool
the router races is claude (rank 1, subscription), codex (rank 2, subscription),
opencode-deepseek (rank 3, cheap metered), opencode-kimi (rank 4, dead as of
2026-08-25 — no credential AND suspended account, and filtered out of the candidate list
before the router sees it). opencode-deepseek is ALREADY the last available
preferred-pool worker, so a peak-hour "down-rank" is a no-op in the common case. Skipping
it entirely would mean either burning subscription quota on claude/codex (a premium
resource reserved for hard/critical tasks — a bad trade for pennies) or dropping out of
the preferred pool into the metered key-loop fallback, which is credential-gated and, for
code-small tasks, re-picks loop-deepseek-build (rank 7) — itself a DeepSeek worker, so
the fallback path defeats the purpose anyway. The only fallback worker cheaper than peak
flash is loop-minimax-build ($0.30/$1.20), and routing onto it (or loop-mimo-build,
$0.50/$1.50) would trade a known-good, always-available worker for a key-gated one to
save about a cent.
What would make this worth implementing: a non-DeepSeek, non-subscription worker in
the preferred pool with demonstrated availability (e.g. a revived opencode-kimi, or a
new cheap opencode CLI agent), or a DeepSeek peak multiplier large enough that the
per-task saving clears the noise floor, or a cheap-fleet mode whose dispatch could simply
be deferred to off-peak. The implementation would be a small deepseekPeakDownrank()
pass in src/core/router.ts after the credit gate, reusing isDeepSeekPeakHour() from
src/core/modelPricing.ts (no new time logic), emitting a reasons line like
deepseek-peak-hour: down-ranked opencode-deepseek during DeepSeek peak window (01:00-04:00 / 06:00-10:00 UTC weekday). Until then the signal stays in cost
estimation, which is the place it already does real work — the spend cap never under-counts
(provider fallback uses the peak deepseek-v4-pro rate as the ceiling).
Request isolation via user_id, and the keep-alive / 10-minute disconnect contract
user_id IS sent, keyed on the RUN id. DeepSeek's "Rate Limit & Isolation" page documents a
user_id field granting sub-identities under one account three things: content-safety isolation,
KVCache isolation, and scheduling isolation. On a regular account every user_id still pools into
one concurrency count; an account with increased quota gets PER-user_id concurrency at the full
numbers (500 pro / 2500 flash / 2500 vision), so one id exhausting its limit 429s only that id.
Format is [a-zA-Z0-9\-_]+, max 512 chars, and it must carry nothing user-identifying.
Helpers live in src/core/userId.ts. buildRunUserId(task.id) is used by
LoopAgentAdapter (only when deepSeekDirect), buildJudgeUserId() by
makeOpenAICompletion in src/judges/llmClient.ts (gated on an api.deepseek.com baseURL).
The keying decision is run id, not worker id. KVCache isolation pays off exactly when several
fan-out workers share a long prompt prefix, so a per-RUN id lets one fleet dispatch's workers reuse
each other's cached prefix; a per-WORKER id would fragment it. Scheduling isolation still holds at
run granularity: one run saturating its concurrency bucket cannot starve a different run. Judge
calls are single-turn with no shared prefix, so they take a per-call nonce id instead - the value
there is only the separate scheduling bucket. The id is a UUID behind a cyberine- prefix, so the
no-PII constraint holds by construction.
Transport: a TOP-LEVEL body field, never extra_body. extra_body is a Python-SDK concept.
openai-node 4.x has no such envelope - requestOptionsKeys in node_modules/openai/core.js does
not list it and the request body is JSON.stringify'd verbatim, so unknown fields pass through
as-is. Nesting user_id under extra_body would put a literal extra_body object on the wire and
DeepSeek would never receive the id - a silent no-op at best, a 400 from a strict endpoint at
worst. buildLoopRequestBody() therefore emits user_id at the top level, and
scripts/t688-user-id-wire-smoke.ts (npm run selftest:user-id-wire) pins that shape so a future
edit cannot quietly reintroduce the envelope. The Anthropic-SDK equivalent would be
metadata: {"user_id": ...}; this repo does not currently use that path for DeepSeek.
Keep-alive and the 10-minute no-inference disconnect. The same page documents that
non-streaming requests receive empty-line keepalives, streaming receives SSE : keep-alive
comments, and verbatim: "If the request has not started inference after 10 minutes, the server will
close the connection."
Our timeout handling is the idle-stream watchdog in src/adapters/LoopAgentAdapter.ts
(IdleStreamWatchdog, CYBERINE_STREAM_WATCHDOG_MS, default 5 minutes, 0 opts out). Two things
follow from putting these side by side, neither of which has been checked against a live 10-minute
stall:
- The watchdog's 5-minute default fires BEFORE the server's 10-minute close, so in the streaming path Cyberine aborts and retries on its own rather than ever observing the server-side disconnect. That is the desired ordering - but it means the 10-minute path is untested here.
- Whether a
: keep-alivecomment counts as a stream event (and so resets the watchdog) depends on the SDK's SSE parser. If it does, a request stalled pre-inference keeps the watchdog alive on keepalives alone and the server's 10-minute close becomes the real deadline; the abort then surfaces as a transport error, not as a watchdog timeout. RaisingCYBERINE_STREAM_WATCHDOG_MSabove 10 minutes hands control to the server in either case.
Cyberine integration notes
-
Wired in
buildAdapters.tsasloop-deepseek-direct(read-only tools) and the opt-inloop-deepseek-build(implementer tools, sandbox-pinned). Base/key/model fromDEEPSEEK_BASE_URL/DEEPSEEK_API_KEY/DEEPSEEK_MODEL. -
LoopAgentAdaptersurfacesreasoning_contentlive in the peek view but never folds it into the answer. -
✓ Mismatch FIXED (2026-06-07):
LoopAgentAdapterpreviously replayedreasoning_contenton the assistant tool-call turn for every provider — believed at the time to 400 on DeepSeek if echoed back. The replay is now gated byneedsReasoningReplay(provider, model), true only for Moonshot/Kimi; DeepSeek no longer echoes it. Verified with a DeepSeek tool-call loop (calculator) completing without 400. Override available via thereplayReasoningconstructor option for a gateway-routed thinking model. -
Re-verified at depth 2026-08-24 (T684): the never-echo default was never actually forced by an observed 400 — see the Gotchas matrix above. Streamed, 24-round, ~173K-token probes found no 400 whether
reasoning_contentwas echoed or omitted, in either direction, and production's ownthinking:disabled-when-tools shape never produces the field at all.needsReasoningReplayis left unchanged: DeepSeek staysfalse(never echo) as a conservative default, not because omission is required to avoid a 400. -
✓ Thinking gate REMOVED 2026-08-25 (T692).
LoopAgentAdapterno longer sendsthinking:{type:'disabled'}just because tools are present, so everyloop-deepseek-direct/loop-deepseek-builddispatch now keeps the model's reasoning. The request body is built by the exported pure functionbuildLoopRequestBody(same file), whose whole job is this policy:- non-DeepSeek endpoint → never send
thinking(it is a DeepSeek-native field). - DeepSeek-direct, no tools → send nothing; thinking stays at DeepSeek's default (enabled). Unchanged by T692, and this is already what the judges do:
LlmJudge/AdversarialJudgego throughmakeOpenAICompletion(src/judges/llmClient.ts), which sends NO tools at all, so thehasToolsgate never applied to them. The judges were never affected by this workaround — only the loop workers were. - DeepSeek-direct, tools, non-forcing
tool_choice(auto/none/ omitted) → send nothing; thinking stays ENABLED. This is the T692 change. - DeepSeek-direct, tools, FORCING
tool_choice('required'or a named function) → sendthinking:{type:'disabled'}. Mandatory: the request 400s otherwise. The forcing choice itself still reaches the wire unchanged. Exposed as the newLoopAgentAdapterOptions.toolChoice(default'auto'); nothing in the tree forces a call today, so this branch exists to keep the invariant true for the first caller that does. - DeepSeek-direct, tools,
maxTokensbelowDEEPSEEK_THINKING_MIN_MAX_TOKENS(8000) → sendthinking:{type:'disabled'}.max_tokenscaps CoT and the answer together, and below ~8K a hard task spends the whole budget on CoT and returns zero tool calls (matrix above).config.loopMaxTokensdefaults to 16000 so the standing fleet is above the floor; the floor catches tight-budget callers and this adapter's own 4096 default whenmaxTokensis unset. CYBERINE_DEEPSEEK_TOOL_THINKING=off(also0/disabled/false) → always disable with tools, i.e. the pre-T692 behaviour, as a no-code-change kill switch for cost or for an unexpected regression. Parsing fails OPEN toautoso a typo cannot silently re-cost the fleet its reasoning.
Guarded by
npm run selftest:deepseek-thinking-tools, which pins that whole truth table offline (CI has no DeepSeek key) and additionally asserts the live contract — coexistence, the'required'400, and'required'+thinking:disabledsucceeding — wheneverDEEPSEEK_API_KEYIS set. Known limitation, deliberately not fixed here: there is no runtime recovery for a round that comes backfinish_reason:'length'with zero tool calls because CoT ate the budget. The static floor prevents the pathological small-budget case; an unusually long CoT above the floor can still waste a round, and the loop will simply retry it. A retry-that-round-with-thinking-off path is the obvious follow-up. - non-DeepSeek endpoint → never send
Sources
Every URL below was fetched with curl and grepped as raw HTML on 2026-08-25 (T679); all 200.
- https://api-docs.deepseek.com/
- https://api-docs.deepseek.com/api/create-chat-completion — the
modelenum, thereasoning_effortdisplayed values + the "medium and xhigh are mapped to high" sentence, and thefrequency_penalty/presence_penaltydeprecation notices - https://api-docs.deepseek.com/guides/thinking_mode/ (corrected 2026-08-21 — the previously
listed
/guides/reasoning_modelURL 200s but actually serves the "Your First API Call" quickstart content, not the Thinking Mode guide; this is the real one, source of thereasoning_effortshape and the OpenAI/Anthropic/Responses format equivalents) - https://api-docs.deepseek.com/guides/vision — added 2026-08-25 (T679)
- https://api-docs.deepseek.com/guides/files_api — added 2026-08-25 (T679)
- https://api-docs.deepseek.com/guides/responses_api — added 2026-08-25 (T679)
- https://api-docs.deepseek.com/quick_start/agent_integrations/codex — added 2026-08-25 (T679)
- https://api-docs.deepseek.com/guides/tool_calls — strict mode's schema surface
- https://api-docs.deepseek.com/guides/function_calling
- https://api-docs.deepseek.com/guides/json_mode
- https://api-docs.deepseek.com/guides/fim_completion — the 4K FIM output cap
- https://api-docs.deepseek.com/guides/chat_prefix_completion
- https://api-docs.deepseek.com/guides/kv_cache — the Sliding-Window-Attention cache-unit rules
- https://api-docs.deepseek.com/quick_start/pricing — re-read 2026-09-24 for the Flash price cut
- https://api-docs.deepseek.com/quick_start/token_usage — the image token calculator
- https://api-docs.deepseek.com/updates — the 2026-07-31 / 2026-08-13 / 2026-08-21 entries;
re-read 2026-09-24 for the 2026-09-10
DeepSeek-V4.1-Flashentry and the V4 Pro continuation
Live API probes (2026-08-25, T679) — the source for every claim above that no page states:
the reasoning_effort enum error text, the per-effort prompt_tokens buckets, the legacy-alias
resolution table, the vision usage shape and its two 400 messages, the Files API status codes and
id format, and the Responses API's silent-ignore behaviour. Throwaway scripts, gitignored and not
guaranteed to survive .local/ cleanup: .local/tmp/t679-probe{,2,3,4,5,6,7}.sh.
Model strength reference (2026-09-17)
One sourced answer to "which enabled model is strongest", used by the router today and meant for planning, task splitting and judging. It ranks every model id the fleet can run (every...
OpenRouter API
No per-provider auth — OpenRouter wraps every provider under one key.