@deepseek-ai/dsh-llm-deepseek is the reference implementation of the harness LLM seam: a direct-fetch, OpenAI-compatible chat-completions adapter that registers the single provider route deepseek-official on ctx.llm. It is pure transport — serialization, SSE decoding, and stream translation — while connection facts and the credential are resolved per request through thunks owned by the registering plugin.
Package
| Package | Description |
|---|---|
@deepseek-ai/dsh-llm-deepseek | "DeepSeek chat-completions adapter for the DeepSeek Harness LLM seam" |
It depends on eventsource-parser@^3.1.0 (SSE framing) and @deepseek-ai/schemastery (config schema); peer-dependencies cover dsh-llm, dsh-credentials, dsh-settings, dsh-timeout, dsh-launch-environment, dsh-anonymous-user-id, dsh-invariants, and cordis. It talks to the DeepSeek platform with raw fetch — there is no separate platform API client package (the BFF/"remotes" package is a different concern; see API gateway).
The adapter
DeepSeekAdapter (packages/llm/llm-deepseek/src/adapter.ts) subclasses LlmAdapter. It serves every model it is registered under: the harness model id is the wire model id (listModels/resolveModel return exactly what was configured). One instance is constructed and bound to per-request resolution hooks:
export interface DeepSeekAdapterOptions {
options: () => DeepSeekConnectionOptions // validated connection facts, resolved per operation
resolveApiKey: (connection) => Promise<string> // throws LlmError MISSING_CREDENTIAL if none
resolveUserId: () => AnonymousUserId
}DeepSeekConnectionOptions freezes one resolution's endpoint facts: baseURL (/chat/completions appended), an apiKeyEnv credential reference (never a literal key), defaults (thinking/effort), maxTokens, defaultContextWindow, the advisory models catalog, streamIdleTimeoutMs, and the resolved retryPolicy. Each stream() re-runs options() and resolveApiKey(connection) from the same snapshot, so an in-flight stream never observes a config change and an endpoint can never be paired with a key from a different generation.
Base URL resolution
apply() (in packages/llm/llm-deepseek/src/index.ts) resolves baseURL with this precedence:
- explicit
config.baseURLfromcordis.ymlor thellm-deepseeksettings section; $DEEPSEEK_BASE_URLfrom a trusted launch-environment layer (so a checkout can point its agent at the gateway its checkout targets);- the public constant
PUBLIC_BASE_URL = 'https://api.deepseek.com'.
resolveAdapterOptions() re-validates every default/bound even when construction bypassed Schemastery (programmatic builds), failing loud at load or keeping the last good settings snapshot (keep the last good configuration) when a live edit fails a beyond-schema bound.
Exposed models
Defaults resolve to the two V4 models, each with the shared 1M-token context window:
| Model id | Display | Context |
|---|---|---|
deepseek-v4-flash | DeepSeek-V4-Flash | 1,000,000 |
deepseek-v4-pro | DeepSeek-V4-Pro | 1,000,000 |
Per-model maxTokens and contextWindow may be overridden in the catalog; a selected model without an exact value falls back to defaultContextWindow (default 1,000,000) and defaultMaxTokens (default 256,000). inputModalities is ['text'] for every route — the wire route is text-only.
Request mapping: harness → DeepSeek wire
serializeRequest / serializeMessages (serialize.ts) map the harness vocabulary to the OpenAI-compatible body of POST {baseURL}/chat/completions:
| Harness concept | DeepSeek wire |
|---|---|
Message.role | messages[].role (system/user/assistant/tool) |
| One tool result in a user message | expanded to a standalone {role:'tool', tool_call_id, content} entry |
assistant content | content; text-less turns send "" — never null |
| assistant reasoning | reasoning_content (CoT passback), only on tool-call turns |
| assistant tool calls | tool_calls[] (id, type:'function', function{name,arguments}) |
GenerateOptions.tools | tools[] with a JSON-Schema parameters |
thinking/reasoningEffort | top-level thinking:{type} + reasoning_effort (`'high' |
temperature / maxTokens / stop | temperature, max_tokens, stop |
| streaming | always stream:true, stream_options:{include_usage:true} |
Images are rejected by assertTextOnly (throws UNSUPPORTED_CONTENT) before any text-flattening can silently erase them. resolveThinking maps the adapter-owned effort: off ⇒ thinking:{type:'disabled'}; high/max ⇒ thinking:{type:'enabled'} + the effort; a session-title purpose always forces thinking off so a title call returns visible text fast.
Streaming & translation pipeline
fetch(baseURL/chat/completions)
→ HTTP 200 text/event-stream body
→ parseSse(): TextDecoder → EventSourceParserStream (eventsource-parser)
· yields each event `data` payload; yields literal "[DONE]" last
· EOF without [DONE] ⇒ throw LlmError('STREAM_CLOSED')
→ translate(): one stateful harness block per content/reasoning/tool index
· reasoning_content → reasoning-delta (first empty frame opens no block)
· content → text-delta
· tool_calls[] → tool-call-delta (fragments concatenate by index)
· finish_reason → finish reason (stop|tool_calls|length)
· usage → usage chunk (deferred to [DONE]; keeps latest)
→ adapter.stream(): wraps reads in an idle-watchdog AbortSignal
· no read inside streamIdleTimeoutMs (default 300_000) ⇒ LlmError('TIMEOUT')
· caller abort ⇒ LlmError('ABORTED')
· transport failure ⇒ LlmError('TRANSPORT')mapFinishReason maps stop → stop, tool_calls → tool-calls, length → max-tokens, and any unrecognized reason (e.g. content_filter) to an error finish with the uppercased reason as its code.
Token accounting
mapUsage converts DeepSeek's prompt_tokens (which include cache hits) into the harness disjoint convention:
// translate.ts — DeepSeek's prompt_tokens = cache hits + cache misses
const cacheRead = usage.prompt_tokens_details?.cached_tokens ?? usage.prompt_cache_hit_tokens
{
inputTokens: usage.prompt_tokens - (cacheRead ?? 0),
outputTokens: usage.completion_tokens,
...cacheRead !== undefined ? { cacheReadTokens: cacheRead } : {},
...reasoning !== undefined ? { reasoningTokens: reasoning } : {},
}Where the API key comes from
The provider references a credential by environment-variable name — apiKeyEnv, default DEEPSEEK_API_KEY — never by storing a literal key in config. Per request, resolveApiKey(connection):
- asks
ctx.credentials(the credential seam) for thatCredentialRef; if it resolves, use it; - else fall back to
launchEnvironmentOf(ctx).get(ref)(a shell-exported.envlayer) when no credentials seam is mounted; - else throw
LlmError('MISSING_CREDENTIAL').
assertUsableApiKey trims and rejects blank / non-header-safe keys (code INVALID_CREDENTIAL) without echoing any secret. For the file-backed story see page 6 and the credentials-local provider (.credentials.yaml + .env under $DSH_HOME).
Error handling summary
| Wire signal | LlmError code |
|---|---|
| HTTP 401 / 403 | AUTH |
Quota-exceeded body (error.code/type/message) | QUOTA |
| HTTP 429 | RATE_LIMIT |
| HTTP 400 + context-capacity signature | CONTEXT_WINDOW_EXCEEDED |
| HTTP 400 other | INVALID_REQUEST |
| HTTP ≥ 500 | SERVER |
| other non-2xx | HTTP_<status> |
Retry-After becomes providerRetryAfterMs; x-request-id/x-deepseek-request-id become the requestId, both used by the running retry policy. Attribute and anonymous-user headers are injected on every wire request (attributionHeaders(), x-deepseek-harness-user-id, plus sessionId/compact markers).
Configuration keys (cordis.yml / llm-deepseek settings section)
| Key | Default | Meaning |
|---|---|---|
apiKeyEnv | DEEPSEEK_API_KEY | credential reference (env-var name) |
baseURL | $DEEPSEEK_BASE_URL → public | endpoint base |
thinking | provider default | 'enabled' | 'disabled' |
reasoningEffort | high | 'off' | 'high' | 'max' |
maxTokens | 256,000 | per-request output cap |
defaultContextWindow | 1,000,000 | fallback context capacity |
models | V4 Flash + V4 Pro | advisory catalog |
streamIdleTimeoutMs | 300,000 | idle read watchdog |
retryPolicy | normal defaults | provider-owned retry policy |
Registered via Config schemastery schema (see packages/llm/llm-deepseek/src/index.ts), mounted through installSettingsSection so the plugin's config and the llm-deepseek settings section are one shape.
Further reading
- The LLM layer — the
LlmAdapter/LlmRuntimecontract this provider implements. - Settings system — the schemastery
Configschema and the settings seam it registers through. - Token meter — how the disjoint usage from
mapUsagefeeds the baseline. packages/llm/llm-deepseek/src/adapter.ts—DeepSeekAdapter,DeepSeekConnectionOptions, error-code mapping.packages/llm/llm-deepseek/src/serialize.ts,sse.ts,translate.ts— the request/SSE/stream pipeline.packages/llm/llm-deepseek/src/index.ts—apply(),Config,resolveAdapterOptions,PUBLIC_BASE_URL.