# dsh-llm-call-inspector English | [中文](README.zh.md) [![CI](https://github.com/striveh/dsh-llm-call-inspector/actions/workflows/ci.yml/badge.svg)](https://github.com/striveh/dsh-llm-call-inspector/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) A local, session-scoped LLM request and response inspector for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) Web. It adds an **LLM Calls** view beside Chat and Trajectory so developers can inspect each session-associated normalized `llm/stream` call without changing what the model receives or what the caller receives. This is a community plugin, not an official DeepSeek Harness release. Version `0.2.1` supports DeepSeek Harness `0.1.2-alpha.2` only. Users remaining on `0.1.0-rc.8`, `0.1.1-rc.1`, or `0.1.1-rc.2` should pin plugin release `v0.2.0`. ![LLM Calls API Comparison showing a manual Anthropic structure reference beside the captured DSH call](docs/llm-calls.png) > [!WARNING] > Request and response bodies can contain prompts, source code, tool arguments, tool results, personal data, or secrets embedded in content. Installing this plugin opts the Host into body capture by default. Read [Privacy and data handling](docs/privacy.md) before using it with sensitive sessions. ## What it shows - A newest-first list of calls for the active DSH session. - Provider, model, purpose, status, start time, duration, chunk count, and body-capture state. - Search plus status and purpose filters. - A master/detail layout with Request, Response, and API Comparison tabs, expandable JSON, and copy actions. - Per-session restoration of the selected call, search, filters, detail tab, per-call manual API reference, and list/body scroll positions when you switch away from the view and return. - A side-by-side semantic comparison selected from call-level pi-ai protocol evidence, with explicit manual references and an exact `deepseek-official` route fallback, clearly separated from the DSH-normalized data actually observed. - Live polling, explicit refresh, session-local clear, error and empty states, keyboard focus, responsive layout, and English/Chinese labels. - Calls made for assistant work, compaction, session-title generation, or another normalized purpose when the call carries a `sessionId`. The captured normalized request allowlist is: `provider`, `model`, `reasoningEffort`, `messages`, `system`, `tools`, `temperature`, `maxTokens`, `stop`, `sessionId`, and `purpose`. The captured response is the ordered array of DSH `StreamChunk` values observed at `llm/stream`. The observer delegates exactly once, yields the original chunk objects in the original order, and preserves downstream throws. Whole-chunk capture also preserves JSON-compatible adapter replay metadata, including `finish.replayState` when an adapter emits it. Independently of body retention, the Host exposes narrow, body-free `apiEvidence` only when a terminal replay response identifies `kind: pi-ai`, `version: 2`, a bounded lowercase-hyphen API id, and the same provider/model as this call. Missing evidence stays `absent`; malformed, mismatched, getter-failing, and conflicting attributions become `rejected` or `conflicted` and are never accepted as protocol facts. The current built-in DSH image block carries attachment-reference metadata (opaque attachment id, media type, byte size, dimensions, and optional display name), and that reference is captured as part of `messages`. The plugin does not independently load attachment bytes or observe provider-side base64 wire payloads. Because normalized `messages` are copied whole, any extension that embeds bytes, base64, credentials, or other private fields inside a custom message block would place that data inside the capture boundary. ### Returning to the view Switching from **LLM Calls** to Chat, Trajectory, or another conversation view does not discard the current inspection context. A DSH session-scoped view store retains only interaction state: selected call id, search and filters, active detail tab, per-call manual API-reference choice, list scroll, and at most 300 recent call/tab body-scroll positions. Scroll positions and manual choices for calls no longer present in the authoritative snapshot are pruned. An apply-lifecycle-owned, least-recently-used browser-memory cache keeps the latest body-free state snapshot and one selected detail for at most four recent sessions, so a fresh cache can paint immediately without forcing another RPC read. Normal polling resumes when its interval is due; an already stale cache may therefore refresh after the restored view is shown. Neither layer uses `localStorage`, `sessionStorage`, IndexedDB, or another browser-persistent store. The client cache is cleared when its plugin apply lifecycle is disposed, and may evict older sessions before that. The Host memory store remains the authoritative retained record. ### API Comparison The API Comparison tab is a **semantic shape projection**, not a packet capture. Its left side is the DSH-normalized request and response chunks actually observed. Its right side is an HTTP JSON, SDK-parameter, or command-input reference derived from those normalized fields and a selected API protocol; endpoint templates are used where the adapter's resolved destination is not observable. For a successful pi-ai call, the preferred attribution is the exact call's validated replay-v2 `apiEvidence`. The protocol registry projects these eight adapter-reported protocols: - `openai-completions`; - `openai-responses`; - `azure-openai-responses`; - `anthropic-messages`; - `google-generative-ai`; - `google-vertex`; - `bedrock-converse-stream`; and - `mistral-conversations`. These are presented as mainstream shape families rather than provider-route names: OpenAI-compatible Chat Completions; OpenAI/Azure Responses; Anthropic Messages; Gemini GenerateContent for the Developer API or Vertex AI; Amazon Bedrock ConverseStream; and Mistral Chat Completions. The inspector recognizes adapter-reported `openai-codex-responses` and `pi-messages` but does not manufacture a projection for them. Unknown reported protocols are also shown as unsupported. If no accepted call-level evidence exists, the exact, case-sensitive `deepseek-official` route may use a versioned DeepSeek Harness `0.1.2-alpha.2` native-adapter reference as a fallback. Valid adapter-reported evidence always takes precedence over that route fallback. When evidence is absent or a reported protocol has no implemented projection, the user can choose a labeled manual shape reference. That choice is stored only as per-call session view state and restored across view remounts; it neither changes the captured facts nor claims which adapter protocol actually ran. Route names other than the exact DeepSeek fallback, model names, prefixes, and gateway branding are never guessed as protocol evidence. The projection does not read HTTP headers or API keys, preserve raw SSE frames, prove the adapter's runtime identity, reproduce adapter compatibility defaults, or reveal hidden transport attempts. It never replays or sends a provider request, so opening the tab, switching views, choosing a manual reference, or copying it causes no additional LLM call or provider charge. Direct request JSON is deliberately limited to history that can be materialized without adapter-private ids, signatures, compatibility metadata, or attachment bytes. Complex assistant/system/tool/reasoning history, images/files, and unknown extensions are marked unavailable. A provider field whose final value is adapter-owned—such as a model-clamped output-token limit or an Azure deployment mapping—uses an explicit `{$unobserved, dshInput}` marker; that marker is not a sendable API value. ## Scope boundary This plugin inspects the **DSH-normalized LLM boundary**. It is not a provider wire proxy. It does not capture: - provider-native HTTP request or response bodies; - HTTP headers, top-level API keys, abort signals, or undeclared adapter-private fields on the request object; - raw SSE frames, transport retries hidden inside an adapter, or provider-side processing; - the adapter's resolved endpoint or the number of physical HTTP attempts behind one normalized call; - sessionless `llm/stream` calls; or - hidden reasoning that the provider does not return as a normalized chunk. Top-level structural exclusion is not content redaction. A key pasted into a prompt, returned by a tool, or nested inside a plugin-defined message block can still be captured. Response chunks are preserved whole, so adapter-private JSON inside `finish.replayState` can also be captured and must be treated as sensitive content. The separately exposed `apiEvidence` is narrow protocol attribution extracted from replay metadata, not proof of the HTTP request or response bytes. ## Why a separate view instead of merging with Trajectory DeepSeek Harness `0.1.2-alpha.2` exposes `conversation.view` as the public additive UI seam. The built-in Trajectory uses that seam but does not expose a supported inner row or panel extension point. The earlier plugin `v0.2.0` made the same adjacent-view decision for DSH `0.1.0-rc.8`, `0.1.1-rc.1`, and `0.1.1-rc.2`. Trajectory and this inspector also answer different questions: - **Trajectory** explains the durable session story: user/assistant/tool events, steps, timing, usage, and outcomes. - **LLM Calls** exposes each normalized invocation boundary: the exact captured request snapshot and ordered response chunks observed for that invocation. The plugin therefore registers an adjacent view at order 20 instead of copying, patching, or depending on Trajectory internals. If Trajectory later publishes a stable cross-link or inner-extension seam, the two views can be connected without changing capture ownership. ## Architecture ```text session-associated GenerateOptions | v llm/stream observer | v bounded per-session memory store | v Connection RPC /dsh-llm-call-inspector (DSH authenticated browser channel) | v conversation.view / LLM Calls ``` One package contains both runtime faces: - **Host** injects `llm` and `connection`, prepends a transparent `llm/stream` observer, owns bounded memory, extracts validated call-level pi-ai replay-v2 protocol attribution into body-free `apiEvidence`, and registers one Connection RPC channel. On DSH `0.1.2-alpha.2`, that channel is reached through Connection's Host/Origin fence and browser token/cookie authentication. - **Client** uses the DSH `@deepseek-ai/dsh-client-store` platform store for session-scoped interaction state and the `@deepseek-ai/dsh-client-ui-renderer` slot runtime, while its Cordis runtime injections remain `connection`, `slots`, and `locale`. It registers one `conversation.view`, polls body-free summaries, and fetches the full body only for the selected call. A bounded four-session apply-lifecycle cache restores the latest summary/detail without browser-persistent storage. - **Bundle** declares `dsh.bundle.patch` and a Web client export, so `dsh plugin` adds both faces through the supported profile mechanism. The Host store never writes captured bodies to disk. The bounded client cache may temporarily retain the latest selected detail in browser memory when the view is unmounted, but writes nothing to browser-persistent storage. Host records disappear when you clear the current session in the UI, the configured per-session/session/global-body bounds evict them, the plugin reloads, or DSH restarts. The plugin adds no network listener and no export destination. It does not impose an additional loopback-only rule: if the DSH Web configuration admits a trusted non-loopback host and its browser session passes DSH authentication, that browser can also access this channel and its captured bodies. Keep DSH Web loopback-only for sensitive inspection unless remote browser access is deliberately required and protected. ## Install Prerequisites: - DeepSeek Harness `0.1.2-alpha.2` (use plugin `v0.2.0` instead for DSH `0.1.0-rc.8`, `0.1.1-rc.1`, or `0.1.1-rc.2`); - Node.js `22.19` or newer supported by the package engine; and - pnpm on `PATH`, as required by `dsh plugin`. Install the GitHub repository into the Web profile: ```sh dsh plugin --profile web add github:striveh/dsh-llm-call-inspector dsh --profile web --dump-config dsh web ``` Restart a running Web profile after adding, updating, or removing a bundle. The config dump should contain a `# == dsh-llm-call-inspector` layer. For reproducible use, pin a reviewed commit: ```sh dsh plugin --profile web add github:striveh/dsh-llm-call-inspector# ``` The repository ships checked-in `lib/` artifacts and deliberately has no `prepare` or install lifecycle script. A GitHub install therefore does not need pnpm `allowBuilds` permission. ## Configuration The bundle defaults are: | Field | Default | Meaning | |---|---:|---| | `captureBodies` | `true` | Capture allowlisted request fields and ordered response chunks. `false` keeps call metadata but marks both bodies omitted. | | `maxCallsPerSession` | `100` | Maximum retained calls in one session; oldest calls are evicted first. | | `maxSessions` | `32` | Maximum retained session buckets; least-recently-used buckets are evicted. | | `maxRequestBytes` | `524288` | UTF-8 JSON byte ceiling for one request snapshot. | | `maxResponseBytes` | `1048576` | UTF-8 JSON byte ceiling for one response chunk array. | | `maxTotalBodyBytes` | `67108864` | Global retained captured-JSON budget across all sessions; settled calls are evicted before running calls, then oldest creation order first. | | `pollIntervalMs` | `750` | Interval advertised to the mounted browser view. | To override them, add a later row to `$DSH_HOME/profiles/web/cordis.patch.yml`. A DSH patch replaces the row's complete `config`, so this example restates every field: ```yaml - id: dsh-llm-call-inspector config: captureBodies: true maxCallsPerSession: 50 maxSessions: 16 maxRequestBytes: 262144 maxResponseBytes: 524288 maxTotalBodyBytes: 33554432 pollIntervalMs: 1000 ``` If a body exceeds its limit, the plugin drops the whole body and exposes an explicit `size-limit` omission with measured bytes. Non-JSON-compatible values and disabled capture also produce explicit omission states; metadata and chunk counts remain available. ### Metadata-only mode Set `captureBodies: false` when provider/model, status, timing, and chunk counts are sufficient: ```yaml - id: dsh-llm-call-inspector config: captureBodies: false maxCallsPerSession: 100 maxSessions: 32 maxRequestBytes: 524288 maxResponseBytes: 1048576 maxTotalBodyBytes: 67108864 pollIntervalMs: 750 ``` Changing this configuration reloads the plugin and discards its current in-memory records. ## Disable or uninstall To keep the dependency installed but disable the plugin, add this later profile patch and restart Web: ```yaml - id: dsh-llm-call-inspector disabled: true ``` To remove the dependency and its bundle layer: ```sh dsh plugin --profile web remove dsh-llm-call-inspector ``` Restart the profile after removal. Uninstalling or disabling the plugin cannot recover already-evicted in-memory records; none are persisted by this plugin. ## Existing options and when to use them This landscape changes quickly; follow each linked project's current documentation before choosing one. | Option | Primary data and UI | Better fit when | |---|---|---| | Built-in [Trajectory](https://github.com/deepseek-ai/deepseek-harness) | Durable session events in the native UI | You need the agent/session narrative, tool flow, usage, and outcomes rather than call bodies. | | [dsh-devtools](https://github.com/izz-BLUE/dsh-devtools) | Metadata-first runtime profiler in a Web view; intentionally omits prompts and tool bodies | You need performance and runtime diagnostics with a smaller content-privacy surface. | | [dsh-llm-inspector](https://github.com/cdxiaodong/dsh-llm-inspector) | Reasoning controls, traffic statistics, a `think` workflow, and optional audit files; no native request/detail Web UI documented | You explicitly want those behavior-changing or file-audit features. | | [dsh-plugin-langfuse](https://github.com/linyp/dsh-plugin-langfuse) | Session events exported as OpenTelemetry traces to Langfuse | You need centralized, cross-session observability and accept/configure external export. | | **dsh-llm-call-inspector** | Local native master/detail UI over normalized call bodies; bounded process memory only | You need focused trace, debug, teaching, or research inspection on the local Web surface. | GitHub topics are discovery metadata, not a security review or official endorsement. ## Development ```sh pnpm install --frozen-lockfile pnpm verify pnpm pack --dry-run ``` `pnpm verify` runs host and client typechecks, automated tests, a clean build, and read-only package checks. The package verifier checks the public exports, committed build artifacts, Web loader identity, bundle patch, DSH client declaration, documentation install command, and absence of install-time lifecycle scripts. To test a local checkout after building it: ```sh dsh plugin --profile web add . dsh --profile web --dump-config dsh web ``` See [CONTRIBUTING.md](CONTRIBUTING.md) for change constraints and [SECURITY.md](SECURITY.md) for private vulnerability reporting. ## Compatibility DeepSeek Harness is in developer preview and does not promise plugin compatibility across prereleases. The declared compatibility matrix is intentionally exact: | Plugin release | Supported DSH release | |---|---| | `v0.2.1` | `0.1.2-alpha.2` only | | `v0.2.0` | `0.1.0-rc.8`, `0.1.1-rc.1`, and `0.1.1-rc.2` | DSH `0.1.2-alpha.2` moved JSON snapshotting to `@deepseek-ai/dsh-util-values`, session view stores to `@deepseek-ai/dsh-client-store`, and slot runtime ownership to `@deepseek-ai/dsh-client-ui-renderer`; it also changed the Connection handler and browser-authentication contract. Version `0.2.1` follows those alpha.2 surfaces and uses inspector RPC schema v3, so it does not declare compatibility with the earlier rc releases. Pin the plugin version that matches the DSH prerelease. On 2026-08-24, the `0.2.0` release candidate passed isolated local rc.8, rc.1, and rc.2 lanes: each asserted 69 uniformly versioned DSH packages, 8 test files / 100 tests, Host + Client typecheck, build, 12-file / 6-client-injection package verification, and dry-run pack. A fresh rc.2 Web profile with a no-network fixture reported `anthropic-messages` for its title call and `openai-completions` for its assistant call through the same provider route; switching through a real Trajectory unmount restored the selected call, search, API tab, and protocol view with no browser errors. This historical evidence is local offline acceptance, not a provider-wire test, and does not validate `v0.2.1` on alpha.2. Pin the exact reviewed release tag or commit. On 2026-08-31, the `v0.2.1` release passed the uniform `0.1.2-alpha.2` dependency assertion, Host + Client typecheck, 8 test files / 102 tests, build, package verification, and dry-run pack in hosted CI. Its publicly downloaded release asset was byte-identical to the locally verified pack, and a fresh alpha.2 Web profile installed, booted, and removed that public artifact successfully. A no-network browser journey produced assistant and session-title calls, rendered Request/Response/API Comparison, and preserved the selected call, API tab, and manual Anthropic reference after switching to Chat and back, with no browser warnings or errors. This remains offline normalized-boundary evidence, not provider-wire proof. ## License [MIT](LICENSE)