The first real LlmAdapter implementations, shipped as a deliberate pair:
same models and wire protocol, completely different internals, so the
StreamChunk protocol is verified across independent implementations.
- dsh-llm-deepseek: hand-rolled fetch + SSE parser + chunk-translation
state machine against the official chat-completions format (thinking
mode via top-level thinking/reasoning_effort; the empty-string
reasoning_content first chunk; usage attached to the finish chunk or
trailing; reasoning_content passback on tool-call turns; disjoint
cache-token accounting).
- dsh-llm-pi-ai: the same endpoint through @earendil-works/pi-ai,
mapping its event vocabulary (parsed tool arguments, in-stream error
events, folded reasoning tokens) onto the same chunks.
The agent loop now honors the in-band error path: an adapter that ends
its stream with finish {kind:error|aborted} (the only option for
adapters that can't throw mid-stream, like pi-ai) is translated into a
step error, so the turn ends error/aborted with a logged error event
instead of a normal completed assistant message. This makes the
StreamChunk error contract real for both adapters; docs/architecture.md
and the StreamChunk doc are updated accordingly.
New yarn test:e2e (vitest.e2e.config.ts, *.e2e.ts) runs key-gated
real-API matrices for both adapters across V4 Flash/Pro and all
thinking/effort levels; it self-skips without DEEPSEEK_API_KEY. Unit
suites run against local node:http mock SSE servers at 100% per-file
coverage.
@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 back tool-call
argumentsas parsed objects; the harness keeps raw JSON strings (re-stringified 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 stop sequences;
GenerateOptions.stopis injected via itsonPayloadhook.
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')
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 (yarn 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.