Files
deepseek-harness/packages/host/apiproxy
imccyu 3b04941830 Merge the concurrent selector adaptation; ui-model stays the model owner
The remote side re-merged master (i18n workflow round) and restored the
pre-architecture ModelSelector package with the Session-side state
machine. Reconciliation keeps that merge's master content and reasserts
this branch's model architecture: business state lives in ui-model's
shared per-session directory (ctx.models), not on Session/
ConversationSnapshot; the input.model seat entry and the /model popup
stay the two same-source entries; the history modelTarget piggyback stays
removed. The ui-model-selector package, its roster row, and its allowlist
entry go; the smoke-real roster names ui-model.
2026-07-27 13:36:25 +08:00
..

@deepseek-ai/dsh-host-apiproxy

English | 中文

The API gateway every client shape shares: the TS contract (src/api/, zero Node dependencies, importable from the browser), the fetch carrier pair (src/fetch/: toFetchHandler on the host side, AbstractApiClient plus platform subclasses on the client side), and the host-side implementation (src/api-proxy.ts: createApiProxy plus the default-exported ApiProxyService gateway plugin — config {provider, model, workspaceRoot?}, provides ctx.apiProxy). Transport-agnostic by design: this package registers no routes; carriers (HTTP today, IPC later) wrap ctx.apiProxy themselves. The shipped core composition lives in apps/cli/cordis.yml.

Contract layer (/api)

Wire messages form a four-quadrant discriminated union — who initiates × request/response — decoupled from the physical channel: ClientRequest (POST /api/<method> body), ServerResponse (that POST's response body), ServerRequest (SSE frame), ClientResponse (POST /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>. Zod schemas anchor satisfies z.ZodType<Wire<T>> and parse at two levels: envelope first, business payload second, dispatched per method. Business errors ride RpcResult's error branch (RpcErrorDetailsMap closes the code set); HTTP status expresses only the carrier.

The layering/protocol decisions are recorded in the GUI layering and RPC protocol RFC; the browser-side consumption architecture in the web client architecture RFC.

The mux stream projects the latest log-backed title as a validated session/title control frame after each attached-session subscription baseline and immediately after the corresponding live raw title event. This projection does not add titles to session.list; cold sessions remain metadata-only there until opening or resuming attaches their logs.

Session model routing is a session-domain contract. session.history returns the selected modelTarget, session.models returns that target with provider-grouped advisory model metadata and provider-local lookup failures, and session.selectModel replaces the target selected for the next prompt-assembly boundary. Catalog membership is not validation: a registered provider may accept an unlisted model, while an unregistered provider returns model-unavailable.

Workspace and Session lists are separate reconnect baselines. workspace.create creates a unique name or adopts an existing directory, session.create accepts an optional preallocated Session id, and host/workspace-changed plus host/session-added carry committed increments in either arrival order. SessionSummary.blank and the host/session-added frame carry the derived zero-events bit: clients hide blank sessions and reuse them per workspace, flip blank on the first host/session-status(running:true), and treat session.list as the reconnect authority; cold summaries are never blank because lazy persistence keeps never-appended sessions out of list().

The command.* and skill.* domains expose the host command registry and skill catalog to clients. Every method addresses one session's agent by sessionId (a served session always has an Agent; command.* resumes cold sessions through the same path as session.*, while skill.list resolves the project root from the session header without touching the Agent registry). command.execute runs a slash-command line host-side and returns a detached result; the carrier's request signal cancels the running handler. host/commands-changed is the catalog invalidation frame: clients refetch command.list instead of diffing.

Carrier layer (/client + root)

AbstractApiClient holds every protocol invariant — rpcId minting, envelope wrap/unwrap, zod parsing, SSE frame decoding, unary timeout, microtask-batched envelope observation (subscribeEnvelopes) — while platform subclasses supply only the doFetch transport aspect. InProcessApiClient over toFetchHandler(api) is the isomorphic point: the full wire serialization/validation path with no network, used by dsh -p headless.

Model Experience

None, as the package defines the client↔host wire contract and carriers; nothing here reaches a model request.

KV Cache effect

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

Known Limitations and Deferred Work

  • respond routing is shipped, but pending-interaction state is host-side work — the wire shape (POST /api/respond, RpcReceipt) is final; the pending table that makes late/duplicate answers meaningful lives in src/api-proxy.ts and is still minimal (questions only, no approvals).
  • Reserved seams stay out of RpcMethodMapsession.fork, prompt.mode: 'inject', task.list, host.listModels, and a describe hostInstanceId are documented reservations; 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.