dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Cloudflare tools, an AI Gateway model provider, and MCP passthrough for DeepSeek Harness.
Cloudflare tools, an AI Gateway model provider, and MCP passthrough for DeepSeek Harness.
Imagine you have a very capable assistant who can write and run code for you. That assistant is DeepSeek Harness (DSH).
Now imagine you also rent a huge amount of internet plumbing from Cloudflare: somewhere to keep files, somewhere to keep a database, a robot that can open web pages for you, and a bank of GPUs that can run AI models.
Your assistant cannot touch any of that on its own. It does not know your account exists.
This project is the set of hands. Install it, and three things happen:
That third one is the interesting part, and it is the reason this project exists rather than just being a list of API wrappers.
DSH plugins are Cordis fibers. This repository ships a bundle — an npm
package declaring dsh.bundle.patch, which contributes a layer of rows to a
profile's plugin tree. One row provides a service (ctx.cloudflare); the others
consume it to register tools and one LlmAdapter.
The provider is what makes session-level cost attribution possible: DSH passes
sessionId on every GenerateOptions, the provider maps it into the
cf-aig-metadata header, and AI Gateway then reports usage and cost keyed by
that value. The correlation is exact, not inferred from timestamps.
| 36 tools | Workers AI, AI Gateway, AI Search, Vectorize, KV, D1, Queues, R2, Browser Rendering, plus one bounded generic REST tool |
| A model provider | Two routes — cloudflare-workers-ai and cloudflare-ai-gateway — registered as a real LlmAdapter, streaming SSE into the harness chunk contract |
| Session cost attribution | cf-aig-metadata carries the harness session id and call purpose, so gateway logs and billing join to sessions exactly |
| Web Client surfaces | A settings card, a per-session usage chip, and three tool views, all WCAG 2.2 AA |
| MCP passthrough | Patch rows for Cloudflare's hosted MCP servers, off by default |
| Package | Role |
|---|---|
@d4551/dsh-cloudflare-core | The ctx.cloudflare capability seam — credential resolution, scoping, request construction, error normalization, pagination, retry |
cloudflare-dsh | The bundle — four tool groups, the model provider, MCP rows, presets, and the cordis.patch.yml layer |
@d4551/dsh-cloudflare-client | Web Client surfaces — settings card, session usage chip, tool views |
The three packages exist because they have genuinely different consumers.
Another bundle may want ctx.cloudflare without any of these tools; the client
package has its own build requirements (React, a client tsconfig, dsh.client)
that the headless packages must not inherit.
dsh plugin --profile <name> add cloudflare-dsh @d4551/dsh-cloudflare-core
Then supply the API token through the environment. The configuration names the variable; it never holds the value:
export CLOUDFLARE_API_TOKEN=...
For the Web Client surfaces, also install the client package into the profile:
dsh plugin --profile <name> add @d4551/dsh-cloudflare-client
Minimum permission groups for the shipped tools:
Workers AI Read/Write · AI Gateway Read/Write · Vectorize Write ·
Workers KV Storage Write · D1 Write · Workers R2 Storage Write ·
Queues Write · Account Settings Read
[!NOTE] The Cloudflare dashboard labels these groups Edit where the API calls them Write. Use
Writewhen minting a token through the API.
The account id is optional when the token can reach exactly one account: it is
discovered once and reused. A token that can reach several makes discovery
ambiguous, so the plugin refuses to guess and asks for accountId, naming the
accounts it saw.
The bundle is organised around a single capability seam. Everything that talks
to Cloudflare goes through ctx.cloudflare; everything else is a consumer of
it. That is what keeps credentials in one place, error handling in one place,
and the tool modules free of transport concerns.
graph TD
subgraph harness["DeepSeek Harness"]
agent["Agent loop"]
tools["ctx.tools"]
llm["ctx.llm"]
slots["ctx.slots"]
creds["ctx.credentials"]
end
subgraph seam["@d4551/dsh-cloudflare-core"]
service["CloudflareService<br/>(ctx.cloudflare)"]
client["HTTP client<br/>request · retry · paginate · errors"]
service --> client
end
subgraph bundle["cloudflare-dsh"]
tai["tools/ai — 18 tools"]
tdata["tools/data — 13 tools"]
tweb["tools/web — 3 tools"]
tmeta["tools/meta — 2 tools"]
provider["ai — CloudflareAiProvider"]
end
subgraph clientpkg["@d4551/dsh-cloudflare-client"]
ui["SettingsCard · SessionCostChip · 3 tool views"]
end
cf["Cloudflare REST API<br/>api.cloudflare.com/client/v4"]
gw["AI Gateway<br/>OpenAI-compatible endpoint"]
creds -.->|"resolve per operation"| service
tai & tdata & tweb & tmeta -->|"inject: cloudflare"| service
tai & tdata & tweb & tmeta -->|register| tools
provider -->|"inject: cloudflare"| service
provider -->|registerAdapter| llm
ui -->|register| slots
agent --> tools
agent --> llm
client --> cf
provider --> gw
DSH's own convention is that a capability is complete only when all three roles exist: a service definition, a provider, and a consumer. Here:
| Role | Where |
|---|---|
| Definition | CloudflareService class and its typed surface |
| Provider | packages/core — registers itself as ctx.cloudflare |
| Consumers | Every tool group and the model provider, via inject: ['cloudflare'] |
In packages/core, exactly one module dispatches a request: client.ts. The
service supplies the default fetch and nothing else there touches the network.
Everything else — path building, scope resolution, query serialization, error
mapping, pagination stepping, retry policy — is a pure function. The same is
true of the provider: transducer.ts holds the entire streaming contract as a
state machine with no clock, no network and no randomness, and provider.ts is
a thin shell around it.
This is not stylistic. It is what makes a 100% mutation score reachable honestly: every branch that matters can be driven from a literal array of fixture inputs, so a surviving mutant means a real gap rather than an untestable seam.
Cloudflare's REST surface is not uniformly JSON, so a RequestSpec says how it
is read and how it is written, and the client does exactly that.
| Direction | Shapes |
|---|---|
| Read | The envelope (request, requestEnvelope); raw text, for a KV value (requestText); bytes with the media type the server declared, for a screenshot (requestBytes) |
| Write | A body the client serializes as JSON; or an encodedBody the caller already encoded, sent verbatim under its own media type — Vectorize's writes take NDJSON, not JSON |
accept names what the caller can read, so an endpoint that answers with an
image is asked for an image. Every one of these paths goes through the same
retry policy and the same failure classification: an error body is read as an
envelope when the endpoint sends one, whatever the successful shape would be.
sequenceDiagram
participant A as Agent loop
participant R as ctx.tools registry
participant T as Tool (e.g. cloudflare_d1_query)
participant S as ctx.cloudflare
participant C as HTTP client
participant CF as Cloudflare REST
A->>R: call cloudflare_d1_query({...})
R->>R: validate args against ParameterSchemaSpec
R->>T: execute(args)
T->>S: accountRequest(spec)
S->>S: resolve account id — discovery, cached, not a credential
S->>C: request(spec)
C->>C: resolve API token from ctx.credentials
Note over C: Re-resolved every request.<br/>Never cached, so rotation<br/>needs no restart.
C->>CF: HTTPS with Bearer token
CF-->>C: Cloudflare envelope
C->>C: retry on 429/5xx and transport failures, capped backoff + jitter
C->>C: normalize envelope to typed error or result
C-->>T: result
T-->>R: one canonical JSON value
R->>R: snapshot, validate against output.schema, freeze
R-->>A: frozen value
R->>R: output.render(args, value) for the transcript
Two properties are worth calling out.
The canonical value is a programmatic API. DSH runs tools in PTC mode,
meaning generated code can call await tools.cloudflare_kv_list_keys({...}) and
receive the value directly. So results carry ids and cursors that feed the next
call rather than prose a model has to parse. cloudflare_kv_list_keys returns
{ keys, cursor, complete } precisely so a generated loop can page, and every
list tool pages the way its endpoint does: page-numbered endpoints take page
and perPage and return page, perPage, total (when Cloudflare reports
one, else null) and complete; cursor endpoints (cloudflare_kv_list_keys,
cloudflare_r2_bucket_list) return cursor and complete; and
cloudflare_queue_list, whose endpoint takes no paging parameters, returns the
whole listing and says if the API reported more than it returned. Every tool
declares a closed output object with each field described and required; the
registry validates a value against it before the model or generated code sees
it, and a presenter reads typed fields rather than casting.
Presenters are pure. output.render runs during session-log replay, so it
performs no I/O, reads no clock and uses no randomness.
Cancellation reaches Cloudflare. timeoutMs on a tool is declarative — the
registry does not interrupt a body — so every tool forwards exec.signal into
its request, and a tool with a budget of its own (inferenceTimeoutMs,
renderTimeoutMs) makes that budget the request deadline as well, so a 120 s
inference is not cut off by the 30 s client default.
The provider's job is to turn one HTTP response into the harness's StreamChunk
sequence, honouring the ordering guarantees the harness depends on.
sequenceDiagram
participant L as ctx.llm
participant AD as CloudflareAiProvider
participant EP as Endpoint resolver
participant GW as AI Gateway / Workers AI
participant TR as StreamTransducer
L->>AD: prepareCall(provider, model)
AD->>EP: resolveEndpoint + resolveModel, once per generation
EP->>GW: GET ai-gateway gateways URL endpoint (first call only)
Note over EP: Base URL comes from the API and is remembered<br/>per plugin instance. The token never is.
EP-->>AD: { url, token } + model facts (context window, modalities)
L->>AD: prepared.stream(GenerateOptions)
AD->>AD: buildWireRequest — images projected to text, reasoning left out, unsupported options rejected
AD->>AD: attributionHeaders() first, then buildGatewayHeaders — cf-aig-metadata carries sessionId + purpose
AD->>GW: POST chat/completions (stream, caller AbortSignal forwarded)
loop while the stream is alive
GW-->>AD: SSE frames
AD->>TR: push(parsed frame)
TR-->>AD: StreamChunk[]
AD-->>L: yield chunks
end
GW-->>AD: data: [DONE]
AD->>AD: no content block opened? → EMPTY_RESPONSE
AD->>TR: end()
TR-->>AD: block-end… usage… finish
AD-->>L: final chunks
Note over AD: Reader is cancelled in `finally`,<br/>so an abandoned turn frees the connection.
The transducer is where the contract lives, and it is enforced by construction rather than convention:
stateDiagram-v2
[*] --> Open
Open --> Open: text-delta / reasoning-delta / tool-call-delta
Open --> Open: usage
Open --> Closing: finish_reason seen
Open --> Closing: transport ended, end called
Closing --> Finished: block-end for every open block, in index order
Finished --> [*]: finish
Finished --> Finished: later input ignored
| Obligation | How it holds |
|---|---|
usage is emitted before finish | The finish reason closes the blocks but is held back until the stream ends, so a provider that reports usage in a trailing chunk — the shape stream_options.include_usage produces — is still observed, and finish is always last |
Nothing follows finish | A finished flag makes every later push/end a no-op |
| Block indices allocated in first-seen order and reused | One allocator, one map keyed by wire index |
Tool-call arguments stay raw JSON strings | Fragments accumulate as argumentsDelta, re-joined at block-end, never parsed |
| One provider call is one attempt | No internal retry — the harness owns retry policy |
options.signal is honoured | Forwarded to fetch unconditionally (null is the documented "no signal") |
| A quiet stream fails as a timeout | Every read races the configurable idle budget |
| Unsupported options fail loudly | Rejected before any request is issued |
| Every provider request carries attribution | attributionHeaders() is the first thing set on the request; nothing after it can replace the header |
| A completion with no content is a failure | EMPTY_RESPONSE, whether it ended with a finish reason, the [DONE] sentinel, or the socket; usage alone is not content |
| Every finish reason has a meaning | stop, tool_calls and length map to the harness kinds; content_filter and anything unknown are an error finish naming the reason |
| Every content block has a stated fate | Text is sent; tool calls and results round-trip, text beside results included; images become the harness's text through its own helper; reasoning is left out, as OpenAI-compatible reasoning APIs refuse it in input |
| A tool call always has an id | The provider's when it sends one; a deterministic call_<index> when it never does, since an empty id cannot be correlated with its result |
This is the feature the architecture is shaped around.
graph LR
A["Agent turn<br/>sessionId: abc123"] --> B["stream(GenerateOptions)"]
B --> C["cf-aig-metadata:<br/>{sessionId, purpose}"]
C --> D["AI Gateway"]
D --> E["Gateway logs<br/>tagged with metadata"]
D --> F["Billing:<br/>usage · cost"]
E --> G["cloudflare_aigateway_session_cost"]
F --> G
G --> H["SessionCostChip<br/>in the session header"]
GenerateOptions.purpose is the bonus: DSH marks auxiliary calls as
compaction or session-title, so housekeeping is attributable separately from
real agent turns.
[!IMPORTANT] The declarative path (
presets/pi-ai.yaml, using thellm-pi-aiplugin DSH already ships) works on day one and costs nothing to adopt — but it cannot setcf-aig-*headers, so it gets no session correlation. That is the concrete reason this bundle ships a real provider rather than only a preset.
Tools are grouped into four modules, mounted as separate patch rows, so a profile takes only the groups it wants.
cloudflare-dsh/tools/ai (18)| Tool | Purpose |
|---|---|
cloudflare_ai_run | Run any Workers AI model, one complete response |
cloudflare_ai_models_search | Search the model catalogue |
cloudflare_ai_model_schema | Fetch a model's live JSON schema |
cloudflare_aigateway_list / cloudflare_aigateway_get | Enumerate and inspect gateways |
cloudflare_aigateway_logs | Page gateway request logs |
cloudflare_aigateway_log_body | Fetch a logged request or response body |
cloudflare_aigateway_routes | Dynamic routing configuration |
cloudflare_aigateway_cost | Credit balance, usage history, invoice preview |
cloudflare_aigateway_session_cost | Usage and cost for one harness session |
cloudflare_aisearch_search / cloudflare_aisearch_chat / cloudflare_aisearch_sync | AI Search query, chat completion, index sync |
cloudflare_vectorize_index_list / cloudflare_vectorize_query | Vector index listing and similarity query |
cloudflare_vectorize_upsert | Write vectors as NDJSON, upsert or insert |
cloudflare_vectorize_delete / cloudflare_vectorize_get | Delete and read vectors back by id |
cloudflare-dsh/tools/data (13)| Tool | Purpose |
|---|