Skip to content

The API proxy (packages/host/apiproxy) is the host-side implementation of the shared /api surface every client speaks. Despite its name it is not a model-API forwarder only: it is the application API carrier — the contract layer that the web UI, and any future client, uses to reach sessions, host capabilities, and events. The Typert gateway (see API Gateway) claims the typed endpoints on the same route; everything unclaimed falls through to the proxy.

PackageRole
@deepseek-ai/dsh-host-apiproxyApiProxyService (ctx.apiProxy), createApiProxy, contract layer, fetch carriers, session export

Package layout

From packages/host/apiproxy/README.md, the package has three layers:

  • Contract layer (src/api/) — the TypeScript API contract with zero Node dependencies, importable from the browser. This is what keeps the wire honest: the same types compile into both faces.
  • Fetch carriers (src/fetch/) — toFetchHandler on the host side (wraps the proxy as a fetch handler for Connection's HTTP bridge), and AbstractApiClient plus platform subclasses on the client side.
  • Host implementation (src/api-proxy.ts) — createApiProxy plus the default-exported ApiProxyService gateway plugin, config { nativeOpen?, sessionExportCompressionLevel?, coldBlankProbeMaxBytes? }, providing ctx.apiProxy.

The package registers no routes itself; carriers such as HTTP wrap ctx.apiProxy. The shipped Web composition wires it in packages/bundle/web-app/cordis.patch.yml.

The four-quadrant wire

Wire messages form a discriminated union over who initiates × request/response, decoupled from the physical channel:

MessageDirectionCarried as
ClientRequestClient → HostPOST /api/<method> body
ServerResponseHost → Clientthat POST's response body
ServerRequestHost → ClientSSE frame
ClientResponseClient → HostPOST /api/respond body

Responses always echo the matching request's rpcId and never mint a new one. Method parameter/return structures live only in the domain interface signatures (SessionsApi, HostApi, EventsApi); RpcMethodMap registers the methods and every other position derives via RequestPayload<K> / ResponseValue<K>.

Validation is two-level: Zod schemas anchor satisfies z.ZodType<Wire<T>>, parsing the envelope first and the business payload second, dispatched per method. Business errors ride the RpcResult error branch (RpcErrorDetailsMap closes the code set); HTTP status expresses only the carrier.

One security-relevant detail from the README: every /api POST must declare the application/json media type — anything else is refused with 415 before dispatch, so cross-site "simple" requests (which browsers send without a CORS preflight) can never execute a side-effectful method blind.

Server requests and the respond channel

The proxy is not request/response only. Host-initiated events — session updates, agent status, question prompts — stream as ServerRequest SSE frames. The client answers questions through ClientResponse POSTs to /api/respond. Question responses are validated against their pending request before the first answer claims it; a multi-select item may carry both requested option labels in selected and non-empty custom text, while a single-select item must use one or the other. Duplicate labels, unknown labels, mismatched ids, incomplete batches, and empty custom text are rejected as bad-response.

The layering and protocol decisions are recorded in the RFC notes docs/../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md and 2026-07-19-gui-web-client-architecture.md (repo-relative paths under .agents/notes/).

Model-request relay and agent-default-model

The proxy is also where the client's model traffic lands. ApiProxyService consumes ctx.agentDefaultModel (see Model Selection & Defaults); it does not own a provider/model config or settings section. The shared service registers { provider, model, reasoningEffort? } under the agent-default-model settings section: the base bundle's composition entry is the lower layer and settings.yaml layers the user's choice over it.

A session resolves its model selection from three tiers on every access:

  1. a selection made in this process,
  2. otherwise the session's latest logged request/header,
  3. otherwise the deployment default.

A session that has run a turn derives its selection from its log, while a blank session observes a default saved after it was created. session.selectModel saves an accepted switch as the deployment default — there is no separate gesture — and stores the resolved ModelSelection including an adapter-materialized default effort.

Native open and session export

Two host-only capabilities ride along in this package:

  • nativePathOpener (src/native-path-opener.ts) — opening local paths with the OS (the native directory-picker counterpart; see Host Platform).
  • sessionExport (src/session-export.ts) — producing the downloadable session export (ZIP) served to the client, with sessionExportCompressionLevel config.

Key source files

Repo-relative pathWhat it provides
packages/host/apiproxy/src/api-proxy.tscreateApiProxy, ApiProxyService, ctx.apiProxy
packages/host/apiproxy/src/api/Browser-safe contract layer (zero Node deps)
packages/host/apiproxy/src/fetch/toFetchHandler, AbstractApiClient + platform subclasses
packages/host/apiproxy/src/session-export.tsSession export (ZIP)
packages/bundle/web-app/cordis.patch.ymlWhere the shipped Web composition mounts it

Further reading