Skip to content

@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

PackageDescription
@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:

ts
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:

  1. explicit config.baseURL from cordis.yml or the llm-deepseek settings section;
  2. $DEEPSEEK_BASE_URL from a trusted launch-environment layer (so a checkout can point its agent at the gateway its checkout targets);
  3. 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 idDisplayContext
deepseek-v4-flashDeepSeek-V4-Flash1,000,000
deepseek-v4-proDeepSeek-V4-Pro1,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 conceptDeepSeek wire
Message.rolemessages[].role (system/user/assistant/tool)
One tool result in a user messageexpanded to a standalone {role:'tool', tool_call_id, content} entry
assistant contentcontent; text-less turns send ""never null
assistant reasoningreasoning_content (CoT passback), only on tool-call turns
assistant tool callstool_calls[] (id, type:'function', function{name,arguments})
GenerateOptions.toolstools[] with a JSON-Schema parameters
thinking/reasoningEfforttop-level thinking:{type} + reasoning_effort (`'high'
temperature / maxTokens / stoptemperature, max_tokens, stop
streamingalways 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: offthinking:{type:'disabled'}; high/maxthinking:{type:'enabled'} + the effort; a session-title purpose always forces thinking off so a title call returns visible text fast.

Streaming & translation pipeline

text
 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:

ts
// 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 nameapiKeyEnv, default DEEPSEEK_API_KEY — never by storing a literal key in config. Per request, resolveApiKey(connection):

  1. asks ctx.credentials (the credential seam) for that CredentialRef; if it resolves, use it;
  2. else fall back to launchEnvironmentOf(ctx).get(ref) (a shell-exported .env layer) when no credentials seam is mounted;
  3. 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 signalLlmError code
HTTP 401 / 403AUTH
Quota-exceeded body (error.code/type/message)QUOTA
HTTP 429RATE_LIMIT
HTTP 400 + context-capacity signatureCONTEXT_WINDOW_EXCEEDED
HTTP 400 otherINVALID_REQUEST
HTTP ≥ 500SERVER
other non-2xxHTTP_<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)

KeyDefaultMeaning
apiKeyEnvDEEPSEEK_API_KEYcredential reference (env-var name)
baseURL$DEEPSEEK_BASE_URL → publicendpoint base
thinkingprovider default'enabled' | 'disabled'
reasoningEfforthigh'off' | 'high' | 'max'
maxTokens256,000per-request output cap
defaultContextWindow1,000,000fallback context capacity
modelsV4 Flash + V4 Proadvisory catalog
streamIdleTimeoutMs300,000idle read watchdog
retryPolicynormal defaultsprovider-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/LlmRuntime contract this provider implements.
  • Settings system — the schemastery Config schema and the settings seam it registers through.
  • Token meter — how the disjoint usage from mapUsage feeds the baseline.
  • packages/llm/llm-deepseek/src/adapter.tsDeepSeekAdapter, 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.tsapply(), Config, resolveAdapterOptions, PUBLIC_BASE_URL.