xxccdl/DeepSeek-Harness-Mobile--plugins-deepseek-ai-dsh-host-apiproxy ↗★ 3

@deepseek-ai/dsh-host-apiproxy

提供 API 网关契约、fetch 载体与宿主侧网关插件,暴露 ctx.apiProxy 供各客户端复用。 适合需要统一模型调用网关的宿主与客户端开发者;本包不注册路由,需自行用 HTTP 等载体包装。

包名
@deepseek-ai/dsh-host-apiproxy
兼容性
待验证
Harness 依赖范围
^0.1.0-rc.6
Cordis 依赖范围
^4.0.1
版本
0.1.0-rc.6
许可证
NOASSERTION
最近更新
2026年8月26日

同名包的其他仓库

安装

此插件尚未提供可验证的 bundle,或兼容性检查未通过。请先阅读仓库说明。 阅读完整 README ↗

@deepseek-ai/dsh-host-apiproxy

English | 中文

The API gateway shared by every client consists of the TypeScript API 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 {nativeOpen?, sessionExportCompressionLevel?, coldBlankProbeMaxBytes?}, provides ctx.apiProxy). This package registers no routes; carriers such as HTTP wrap ctx.apiProxy themselves. The shipped Web composition lives in packages/bundle/web-app/cordis.patch.yml, while its default Agent model selection belongs to @deepseek-ai/dsh-agent-default-model in the base bundle.

The shared Agent default (agent-default-model Settings section)

ApiProxyService consumes ctx.agentDefaultModel; it does not own a provider/model config or settings section. The shared service registers {provider, model, reasoningEffort?} under agent-default-model: 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: a selection made in this process, otherwise the session's latest logged request/header, otherwise this 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. It stores the resolved ModelSelection, including an adapter-materialized default effort. The complete-section write clears a stored effort when the selected model has none. A storage failure is logged without undoing the session selection. A deployment with no settings provider keeps the composition entry and the switch remains session-local.

The section's reasoningEffort has no counterpart in the agent-default-model plugin config, deliberately: the seam merges the user layer over the composition entry per field, so an absent key cannot override a present one and a composition-set effort would survive every later switch to a model without one. A deployment default for effort belongs on the adapter profile, which resolves per model.

The stored selection is independent of catalog membership. A default naming an unavailable provider still reaches session.models as the session's current, allowing the selector to request a replacement instead of silently choosing another model. Conversely, an adapter may serve a model that its catalog does not advertise.

Contract layer (/api)

Wire messages form a four-quadrant discriminated union — who initiates × request/response — decoupled from the physical channel: ClientRequest (POST /api/ 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/ResponseValue. Zod schemas anchor satisfies z.ZodType> 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. 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.

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.

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; 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.

session.history reads an attached Session in memory or inspects a cold log through persistence without resuming or publishing an Agent, then pages on append-origin message boundaries. maxMessages counts user/message and assistant/message events that entered the surface by appending, so a model-only replacement copy consumes no quota. Each page stays one contiguous raw event range, which keeps a compaction's log-only compaction/summary record on the same page as the replacement that cites it.

session.history's tail page (beforeSeq absent) additionally carries an optional projections block — the watermark snapshot of every unit registered on ctx.sessionProjections (@deepseek-ai/dsh-session-projection), with asOfSeq = the last event seq the values reflect (-1 on an empty log). The gateway also subscribes to the registry's change feed and mints a session/projection mux frame per changed unit ({sessionId, key, value, seq} — live push state, never logged; clients hold one generic per-session value store under higher-seq-wins). The carrier holds no other domain's knowledge (each value passed its unit's own schema inside the registry; the wire schemas keep values/value wide); loadOlder pages never carry the block, and a composition without the registry serves histories without either surface. The gateway owns two units: sessionListMetadata caches the monotonic blank-to-nonblank transition and latest human prompt time used by session.list, while imageLimits publishes the attachments config enforced at prompt admission as a per-boot constant (apply keeps the state reference, so baselines alone carry it — no change frames) so clients can refuse an over-limit intake before submit and label upload affordances; the latter activates only while both the registry and the attachments service are composed.

Session-log export is a host-only download surface, not an RPC: GET /api/session.export?sessionId=…&includeDescendants=true streams a ZIP whose files are each session's stored artifact text verbatim (the persistence backend's readRaw — exact durable bytes decoded from the physical encoding, never a reconstruction from parsed events), root under its original base name plus each subagent descendant under subagents//, and every image any included log references under media/. (read and verified from the attachment store; a shared image appears once). HEAD runs the same root preparation and returns its status and headers without a response body, so browser clients can detect pre-stream failures before handing the GET to the native download manager. Each live root or descendant crosses the authoritative SessionStore.flush durability barrier immediately before its raw artifact read; cold sessions have no in-memory work to flush. Compression runs on the host with fflate's streaming Zip API at validated sessionExportCompressionLevel 0–9 (default 6), so deployments can trade CPU and latency against archive size; the response is chunked as it is produced and the host never holds the whole archive in one buffer. Once the response queue reaches its 64 KiB byte high-water mark, production waits until consumer pull restores positive capacity; fflate's synchronous callback can overshoot that bound only by the output of one bounded input push. Request abort and response-body cancellation stop lineage and artifact work, terminate the active compressor, and propagate as cancellation rather than an HTTP 500. It requires the persistence, session-query, and attachment services: a deployment without any answers 500, a persistence backend without per-session raw artifacts answers 501, a missing root session answers 404, and a descendant without a stored artifact or a referenced image that cannot be read fails the stream (fail-loud, never silent under-export). The carrier mounts the endpoint; ApiProxy.downloads.sessionLog implements it.

Session titles ride the generic projection pair like every other domain — the history-tail projections block plus session/projection frames under the title key. Titles do not join session.list; cold sessions remain metadata-only there until opening or resuming attaches their logs. session.rename accepts an explicit user title (resuming a cold session first), delegating to ctx.sessionTitle.rename — the accepted session/title event pins the title against automatic regeneration — and returns the normalized title plus its event seq so a client settles its title projection cell ahead of the push frame; a title that normalizes to empty returns title-invalid.

session.fork maps an optional event anchor to the first turn/end at or after it, letting a message action include that message's whole turn. An omitted or past-end anchor selects the last completed turn; an in-log anchor whose turn remains open returns fork-unavailable rather than clipping backward. The published child inherits the source's seeded history, cwd, latest logged ModelSelection, and lineage before joining the source Workspace. If Workspace attachment fails, workspace-attach-failed carries the already-published child id so clients can reconcile it. The SessionStore fork decision records why the anchor maps to that turn/end.

Session model selection is a session-domain contract. session.models returns the current ModelSelection separately from provider-grouped advisory models, exact-model reasoning metadata, and provider-local lookup failures. The selection may be absent from the groups and is never injected as a synthetic row; clients can prompt for another selection without turning the directory into a routing whitelist. session.selectModel validates the optional adapter-owned reasoning effort and assigns the complete selection for the next prompt assembly. Catalog membership is not validation: an adapter may resolve an unlisted model, while an unavailable provider or unsupported effort returns model-unavailable. session.models additionally reports routable: whether an adapter currently serves the selected provider. This is deliberately not derivable from the groups because an adapter may serve an unadvertised model. session.prompt refuses on the same fact with model-unavailable before opening a turn; a disabled composer is a client affordance, and the method remains callable.

session.prompt and subagent.prompt accept optional request-local clientTimeZone provenance. When present, the Host validates and canonicalizes UTC or an IANA Area/Location before Agent entry, rejects invalid input with invalid-time-zone, and records the canonical value on that exact user-rpc message beside its rpcId. The value is not Session, connection, create, resume, or fork state; non-browser callers may omit it.

Pending queued input is a live control-plane contract, not conversation history. The gateway derives the complete next-turn queue from durable agent/inbox/spliced mutations and broadcasts authoritative session/queue snapshots after each change and on reconnect; pending next-step steering stays outside this Web projection. Within next-step, user-origin messages carry the steering placement while injected context (approval notices, task completion, attached snapshots) carries context and is not surfaced until claimed. The message-local agent/inbox/inserted, claimed, and discarded notifications remain available to lifecycle observers but do not build the queue view. session.updateQueue addresses one MessageId; edit and remove mutate the attached Agent through Inbox.splice(). A claim's pure deletion splice wins races before pre-step admission, so a later operation returns queue-item-not-found. session.cancel aborts only the active turn and preserves pending inbox work; after cancellation reaches quiescence and the closing turn flushes, AgentLoop claims the next waking message in FIFO order, and the browser never resends or promotes it. Queue operations never resume a cold session, and the client never infers retirement from turn or status events.

Background jobs ride the same live-push posture. When ctx.jobs is composed, the gateway subscribes to its change feed and broadcasts a whole session/jobs snapshot after every registry commit that alters what a session can see — registration, the stopping transition, settlement, and owner-disposal removal — plus a subscription baseline for each session that already has tasks (an absent baseline is the empty set; a change that empties a set still sends []). A change carrying an owner reads through that exact Agent, so a push stays correct while its scope tears down; the baseline reads ctx.agents.get(sessionId), which yields only unowned tasks for a session with no live Agent and never resumes a cold one. An unowned change fans out to every subscribed session, because unowned tasks are visible to every caller. The wire JobView drops ownerSession, reported, and outputLimitBytes: the frame's own sessionId carries the first, and the other two are internal notice and model-presentation policy. A composition without the registry emits no such frames.

Workspace and Session lists are separate reconnect baselines. workspace.create({ path }) adopts an existing canonical directory and permits basename-derived titles to repeat. workspace.insertBefore({ workspaceId, beforeWorkspaceId? }) commits one registry-order move and answers the complete order; a pure reorder emits host/workspace-order-changed with that complete order, while unknown sources or anchors return workspace-not-found. workspace.delete removes only the Workspace registration, session.create accepts an optional preallocated Session id, and host/workspace-changed, host/workspace-removed, plus host/session-added carry committed increments in either arrival order. workspace.archiveSession adds one session to the registry-global archive set and answers the full updated set; workspace.list carries that set as the reconnect baseline and host/archived-sessions-changed pushes the full snapshot after every durable change. Archiving hides the session from grouping surfaces without touching its log or its workspace account; a session neither live nor persisted fails with session-not-found. Registration deletion preserves the directory and session logs; its Sessions remain in session.list and become Ungrouped. SessionSummary.blank and the host/session-added frame carry whether a turn has started: 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. Attached summaries fold the live log. A cold summary trusts cached blank: false, but treats cached true and a cache miss as unverified; when locate() reports an artifact no larger than the coldBlankProbeMaxBytes eligibility threshold (default 1 KiB), the gateway reads that Session with readFrom() and folds both blankness and the latest human prompt. A larger, location-less, vanished, or unreadable artifact remains visible. After an asynchronous cold read, a Session th