Files
deepseek-harness/packages/llm/llm-deepseek
Tianyi Cui 30cd67b8a1 simplify(llm): drop unconsumed adapter-change event and assembled call surfaces
The LLM service exposed three call surfaces (stream/streamBlocks/generate) but
the only production consumer — the agent loop — uses stream() exclusively,
feeding raw chunks through its own BlockAssembler for replay fidelity. Drop the
speculative convenience surfaces and the registry-change event that no listener
consumed, leaving stream() as the single model-call contract for both
production and tests.

- Remove LlmService.streamBlocks() and generate(), the llm/generate waterfall,
  and GenerateResult.
- Remove the llm/adapter-change event (declaration + emits) and the
  listener-throw rollback ordering that existed only to protect it; keep the
  HMR rollback disposer.
- Remove BlockAssembler.flushReady()/flushRemaining()/result() and the flushed
  cursor — the streaming-flush slice existed only for streamBlocks().
- Adapter tests drive a stream()+BlockAssembler helper (tests/assemble.ts)
  instead of generate(), exercising the same path production uses.
- Land the AGENTS.md "RFCs are proposals, not golden truth" principle and move
  both RFCs proposed -> implemented.

Implements:
- docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md
- docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md
2026-06-21 01:27:41 +08:00
..

@deepseek-ai/dsh-llm-deepseek

DeepSeek chat-completions adapter for the harness LLM seam: hand-rolled fetch + SSE translation from the official wire format (source of truth: the API docs — guides/thinking_mode, guides/tool_calls, api/create-chat-completion) into the StreamChunk protocol.

A second, independent implementation of the same seam exists in @deepseek-ai/dsh-llm-pi-ai (library-backed). Same Config shape — pick one per context (registering both for the same model names throws by design).

Config

- id: llm-deepseek
  name: '@deepseek-ai/dsh-llm-deepseek'
  config:
    apiKey: !!js process.env.DEEPSEEK_API_KEY    # or rely on the env fallback
    baseURL: !!js process.env.DEEPSEEK_BASE_URL  # default: https://api.deepseek.com
    models: [deepseek-v4-flash, deepseek-v4-pro] # one adapter, registered for each name
    thinking: enabled        # optional; provider default is enabled
    reasoningEffort: high    # optional; high | max — omitted ⇒ not sent

models lists every model name this one adapter instance serves: the adapter registers itself for each (the harness model name IS the wire model string), so a generate/stream call routes to it whenever options.model is any of them. Registering a second adapter for a name already taken throws LlmError('DUPLICATE_ADAPTER') (the LLM service enforces one adapter per model, all-or-nothing).

reasoningEffort is omitted by default — when unset, the reasoning_effort wire field is not sent and the server applies its own default for the model. The only accepted values are high and max (DeepSeek's official effort levels). It is meaningful only with thinking enabled (the provider default).

thinking/reasoningEffort are adapter-level request defaults serialized as the official top-level thinking: {type} / reasoning_effort wire fields. They live in adapter config (not GenerateOptions) to keep the core vocabulary provider-neutral.

Wire-format notes (verified live + against the official docs)

  • Streaming only (stream_options.include_usage always on). usage may arrive attached to the finish chunk or as a trailing usage-only chunk — the translator defers both to [DONE], so usage always precedes finish and nothing follows finish.
  • The first thinking-mode chunk carries reasoning_content: "" — handled (no spurious reasoning block).
  • Reasoning passback rule: on assistant turns that carried tool calls, reasoning_content is serialized back in history (required by the API in thinking mode); on tool-call-free turns it is dropped (ignored anyway — saves tokens).
  • strict on tool schemas passes through (officially Beta; the public API wants the /beta base URL for it, the internal endpoint accepts it directly).
  • Cache accounting: cacheReadTokensprompt_cache_hit_tokens / prompt_tokens_details.cached_tokens; DeepSeek reports no cache-write metric.

Limitations (MVP, documented deliberately)

  • prefill throws LlmError('UNSUPPORTED') — DeepSeek's chat-prefix completion is a Beta feature on the /beta base URL; future work.
  • image blocks are skipped (no vision support on these models).
  • tool_choice is not mapped (not part of the core vocabulary).

Errors

Non-2xx responses throw LlmError with stable codes: AUTH (401/403), RATE_LIMIT (429), INVALID_REQUEST (400), SERVER (5xx), HTTP_<status> otherwise. Protocol violations throw STREAM_CLOSED (no [DONE]) or MALFORMED_RESPONSE (bad JSON payload). Unknown wire finish_reasons (e.g. content_filter, insufficient_system_resource) become finish {kind: 'error', code: <REASON>} chunks.

Testing

Unit suites run against a local node:http mock SSE server (no network). Real-API coverage lives in tests/adapter.e2e.ts (pnpm run test:e2e, key-gated): V4 Flash + V4 Pro across thinking enabled/disabled and both official effort levels, including the thinking+tools round trip with reasoning passback.