Files
deepseek-harness/packages/host/apiproxy

description, kind
description kind
The shared API gateway for web GUI host clients: the browser-safe API contract, the fetch carriers, and the host-side gateway service every client shape uses. package-reference

@deepseek-ai/dsh-host-apiproxy

English | 中文

Summary

Every client of the web GUI host calls one typed API through dsh-host-apiproxy — sessions and history, workspaces, directory picking, model selection, agent presets, skills, goals, settings, LLM catalogs, events, and session export — moved over HTTP or in-process by fetch carriers. The contract layer has zero Node dependencies and imports from the browser, so one typed API serves the Web server, Electron, and any future client shape. The shipped Web composition assembles the gateway in dsh-web-app. Choosing a carrier, calling the domain APIs, and configuring the gateway come first; the wire protocol internals live in a collapsible developer section below.

Table of Contents


Use this package

Compose the gateway when a client of the GUI host needs the session, workspace, and configuration APIs: load ApiProxyService, wrap ctx.apiProxy in a carrier, and call the typed domain methods.

Choosing a carrier

toFetchHandler(api) turns the gateway into a pure WHATWG fetch function for an HTTP server (the shipped Web composition exposes it behind /api/… routes), while InProcessApiClient runs the same serialization and validation path in-process — the isomorphic point for callers and tests that need the full wire path without a network.

const client = new InProcessApiClient(toFetchHandler(ctx.apiProxy))
const response = await client.sessions.list({})

The HTTP carrier refuses non-JSON POST bodies with 415 before dispatch, so cross-site simple requests can never run a side-effectful method blind. The browser carrier applies the same Host/Origin checks and signed-cookie authentication to every Host API method (dsh-client-connection); individual Client features may still withhold native or persistent operations on non-loopback pages.

What the gateway exposes

The API is grouped into domains: sessions (list, create, history, prompt, cancel, queue, models, selectModel, rename, fork, search, attachment), workspace, host (describe, openPath), skills, agentPresets, goals, settings, llm, events, and downloads. The sessions, workspace, and events contracts are owned by the Session Controller, Workspace Controller, and API Remotes packages respectively; the remaining domain contracts and the RpcMethodMap live in src/api/.

Sessions and history

session.history pages a session's appended message stream (maxMessages counts append-origin user/message and assistant/message events, so model-only replacement copies consume no quota) and keeps each page a contiguous raw event range, which keeps a compaction's log-only summary on the same page as the replacement that cites it. The tail page optionally carries a projections block — the watermark snapshot of every registered projection unit — while the gateway pushes live session/projection frames for units whose state changed. session.search is a bounded content-search projection over the sessions visible through session.list: at most 20 hits, snippets of at most 240 code points, and every hit revalidated against the visible set.

Workspaces and the session list

session.list and workspace.list are separate reconnect baselines. Blank sessions stay hidden until the first turn, archiving hides a session from grouping surfaces without touching its log, and registration deletion preserves the directory and session logs. Cold summaries verify blankness by probing a small eligible artifact; a projection-cache miss or stale hint falls back to createdAt, so a recently worked large session may sort lower until the next checkpoint.

Exporting sessions

GET /api/session.export?sessionId=…&includeDescendants=true streams a ZIP of the session's stored artifact text verbatim, every subagent descendant under subagents/<id>/, and each referenced image under media/<attachmentId>.<ext>. HEAD runs the same root preparation without a body, so browsers detect pre-stream failures before handing the GET to the download manager. The response is chunked as it is produced, and sessionExportCompressionLevel (09, default 6) trades CPU and latency against archive size. Missing persistence, session-query, or attachment services answer 500, a backend without per-session raw artifacts 501, and a missing root session 404.

Model selection, presets, commands, and configuration

session.models reports the current ModelSelection separately from provider-grouped advisory models, and session.selectModel saves an accepted switch as the deployment default through the shared agent-default-model settings section — a default naming an unavailable provider still reaches the selector as current instead of being silently replaced. Each access resolves an in-process selection first, then the session's latest request/header, then the deployment default. A logged reasoning effort marked as an adapter default remains absent from the restored selection, so the next resolution does not promote it into an explicit choice or record a false header change. agentPreset.list exposes the deployment's preset roster with each row's trust and a broken reason when a preset cannot compose a session; agentPreset.select swaps a blank session's composition and is refused once a turn has run. skill.list serves the composer menu with each skill's modelInvocable flag, and command.execute runs a slash command with pure admission semantics whose outcome rides the logged command/run/command/done pair. The remaining configuration operations are settings.openDocument and the llm.* domain; generated settings and credential methods belong to @deepseek-ai/dsh-api-settings-controller.

Configuration

Field Default Meaning
nativeOpen platform-detected Whether the deployment can hand paths to a native desktop opener
sessionExportCompressionLevel 6 DEFLATE level for every session-log ZIP entry, 09

The generated configuration catalog is the exhaustive source for every accepted field and its JSDoc.


Understand the implementation

Implementation internals — click to expand

Design concept

The package is built on one separation: the API contract is channel-independent, and physical transports are carriers around it. Wire messages form a two-member discriminated union — ClientRequest (the POST /api/<method> body) and ServerResponse (that POST's response body) — decoupled from the physical channel. Responses always echo the matching request's rpcId and never mint a new one. Business errors ride the RpcResult error branch with a closed RpcErrorDetailsMap; HTTP status expresses only the carrier. The layering and protocol decisions are recorded in the GUI layering and RPC protocol RFC.

Source map

File Role
src/api/ Contract layer: domain interfaces, payload types, zod schemas, RpcMethodMap — zero Node dependencies
src/fetch/handler.ts Host carrier: toFetchHandler, envelope parsing, unary dispatch, session export
src/fetch/client.ts Client carrier: AbstractApiClient plus platform subclasses, InProcessApiClient
src/api-proxy.ts Gateway implementation: createApiProxy over the composed host context
src/session-export.ts Session-log ZIP export: raw artifact reads, media collection, fflate streaming
src/native-path-opener.ts Platform opener for paths (open/Invoke-Item/xdg-open, WSL translation)

The gateway service

ApiProxyService provides ctx.apiProxy and implements the contract over the composed host context — sessions, workspace registry, directory picker, agent presets, settings, LLM, events, and downloads. The Host cwd is the default project directory. The gateway consumes ctx.agentDefaultModel only for the deployment metadata host.describe reports; session.selectModel (Session Controller) saves an accepted switch as the deployment default through the shared agent-default-model settings section. Product dsh --profile headless is a direct core entry point and does not mount this package.

Request flow

A request enters a carrier, which parses the envelope and the business payload in two levels, dispatches per method, and returns a response echoing the request's rpcId. Server pushes — the session and workspace follow streams — ride the API Gateway's /api/remote.mux WebSocket and deliver opened then gap-free event frames the client decodes. Unary requests carry the carrier's abort signal, so caller/connection cancellation propagates to the underlying work.

What the gateway owns

The gateway is the wire contract plus a host-side projection over services owned elsewhere: it emits no cordis events, and the session/agent event streams it projects are asserted by their owning packages' companions. The carrier holds no other domain's knowledge — each projection value already passed its unit's own schema inside the registry.


Further Exploration

Read these when the package-level contract is not enough. They move from the layering decision to the browser-side consumption architecture and the adjacent subsystems.


Model Experience

None, as the wire contract and fetch carriers move already-composed messages and register nothing model-facing.

KV Cache effect

None; this package neither assembles nor sends a provider request.

Known Limitations and Deferred Work

These limits define where the gateway is a poor fit; they are current package constraints, not a task backlog.

  • Forwarded Remote events ride the gateway stream framing — the delivery path reuses the API Gateway's Remote stream mux instead of opening a third downlink, which reads as if this package owned the Remote event contract. It does not: the allowlist belongs to dsh-api-remotes and the consumer verb is ctx.remote.$on (rationale).
  • Pending-interaction state is host-side — the browser's pending-interaction snapshot folds plugin-registered pending domains (user questions and approvals); the wire defines no dedicated respond route and no RpcReceipt type.
  • Reserved seams stay out of RpcMethodMapprompt.mode: 'inject', job.list, and a describe hostInstanceId are documented reservations; model discovery uses llm.models. An unknown method fails loud at envelope parse rather than getting a not-implemented code.
  • No protocol version field — client and host ship together; host.describe gains a version negotiation field only when an independently released client exists.
  • Search failures include provider diagnostics — the gateway is a single-user local service; a carrier that exposes it to multiple users must replace internal search details with a public-safe diagnostic.
  • Cold-list hints degrade only toward visibility and older ordering — a projection-cache miss or stale lastPromptAt falls back to createdAt unless an eligible small artifact supplies an exact fold. The bounded blank-verification decision owns this safety direction; an authoritative exact recency index remains scoped in the last-activity-index proposal.

Dev Note

Working context for maintainers — click to expand

This Dev Note is working context for maintainers: open directions. It is explicitly non-authoritative — shipped behavior and limits live in the sections above. A protocol version field waits for an independently released client; a multi-user carrier must replace provider search diagnostics with public-safe text; per-connection picker adaptivity (native for a local browser, browse for a remote one) remains an undecided direction for the host surface.