dsh-llm owns the vocabulary (attribution.ts): AppIdentity with the version
read from the package manifest, userAgent(), and attributionHeaders(target,
identity) over a closed AttributionTarget union ('generic' | 'openrouter').
Both adapters send the headers on every provider request — llm-deepseek in
its fetch headers, llm-pi-ai through pi-ai's StreamOptions.headers — behind
an explicit attributionTarget config (never inferred from baseURL), with
mock-server tests asserting exact wire arrival and the absence of the
OpenRouter set by default.
The RFC moves to implemented/ amended with the settled identity (the
deepseek-harness token, the DeepSeek Harness title, the planned
deepseek-ai/deepseek-harness-sdk URL behind a FIXME until that repo exists)
and the explicit-config OpenRouter decision.
3.2 KiB
@deepseek-ai/dsh-llm-pi-ai
DeepSeek adapter for the harness LLM seam backed by @earendil-works/pi-ai (the LLM library behind the pi agent).
Why a second adapter exists
@deepseek-ai/dsh-llm-deepseek already talks to the same endpoint. This package is its design-verification twin: same models, same wire protocol, completely different internals — a unified LLM library with its own event vocabulary versus hand-rolled fetch/SSE. Anything the harness StreamChunk protocol cannot express for BOTH implementations is a core-vocabulary bug. The differences it exercised on purpose:
- pi-ai hands tool-call
argumentsaround as parsed objects; the harness keeps raw JSON strings. The adapter patches replay payloads back to the original raw strings before sending them, and re-stringifies parsed output tool calls atblock-end. - pi-ai reports failures as in-stream error events (it never throws mid-stream); these map to
finish {kind:'error'|'aborted'}chunks — the protocol's other sanctioned error path besides throwing (which llm-deepseek uses). - pi-ai folds reasoning tokens into
usage.output; there is no separate reasoning count to map. - pi-ai's options omit some DeepSeek/OpenAI-compatible details; the adapter uses its
onPayloadhook to preserve the harness contract (stop, per-toolstrict, omitted reasoning effort, raw replayed tool arguments).
Config
Same shape as llm-deepseek (one-line swap in cordis.yml), with pi-ai's thinking-level vocabulary:
- id: llm
name: '@deepseek-ai/dsh-llm-pi-ai'
config:
apiKey: !!js process.env.DEEPSEEK_API_KEY
baseURL: !!js process.env.DEEPSEEK_BASE_URL
models: [deepseek-v4-flash, deepseek-v4-pro]
reasoning: high # off | high | xhigh (xhigh → wire 'max')
attributionTarget: openrouter # optional; generic | openrouter — omitted ⇒ generic
App attribution
Every request carries the shared attribution headers from dsh-llm's attributionHeaders(), passed through pi-ai's headers stream option (pi-ai merges caller headers last, so they always reach the wire — the unit suite asserts arrival on the mock server, same as llm-deepseek). attributionTarget: openrouter adds OpenRouter's documented set (HTTP-Referer, X-OpenRouter-Title, X-OpenRouter-Categories) and is explicit config only — never inferred from baseURL. See dsh-llm § App attribution.
Dependency weight
pi-ai declares the openai/anthropic/google/mistral/AWS SDKs as install-time dependencies. They are lazy-loaded — only the openai SDK actually loads for this adapter — but they do land in node_modules. Accepted for a package whose purpose is design verification.
Limitations
Same MVP contract as llm-deepseek: prefill throws UNSUPPORTED, images are not representable, tool_choice is not mapped.
Testing
Unit suites run against a local node:http mock SSE server (pi-ai's openai SDK happily talks to any base URL). Real-API coverage in tests/adapter.e2e.ts (pnpm run test:e2e, key-gated): V4 Flash + V4 Pro across all exposed reasoning levels (off/high/xhigh), the thinking+tools round trip, and a cross-adapter structural-equivalence check against llm-deepseek.