DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in
← Plugins
K

@local/dsh-kilo-gateway

Kilo Gateway

Kilo Code Gateway as a first-party model provider: a dedicated settings page for the API key, device sign-in, anonymous free tier, per-model context windows, streaming chat with self-healing retries, and account/completion/embedding/notification tools.

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add github:qianmo978/dsh-kilo-gateway#14639969d5701227a8ab521b1c79e20eb28ab18d
READMECompatibilityVersions

Compatibility and provenance

Kilo Gateway is published as @local/dsh-kilo-gateway and currently resolves to version 1.8.1. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
web
Release source
github
Registry updated
10/21/2026

Versions

1.8.1stable
10/21/2026

Related plugins

Loading related plugins…

Latest
1.8.1
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
Unavailable
Files
Unavailable
Surface
web
License
Not declared
Source
github
GitHub
★ 0
Weekly downloads
0
Last push
10/22/2026
View source ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue

Related plugins

More verified plugins in models-usage.

Experimental Cot Translation Bundle@deepseek-ai/dsh-experimental-cot-translation-bundleExperimental machine translation of expanded reasoningWhale Widgetdsh-whale-widgetDeepSeek balance whale widget in the bottom-right corner of the DSH Web interface: balance/today’s usage/peak-off-peak pricing, customizable bubble click sequence (text/balance/today/peak-off-peak/image/random phrases and parallel weighted selection), per-line styles and fonts, floating quick editinOur Free Model@ebony-vinyl/dsh-our-free-modelOur Free Model — Install this plugin in dsh to use cutting-edge models including DeepSeek V4.1 Flash and Kimi K3, with no login, registration, or API Key required. The desktop version also offers an EAC co-payment channel; starting with 2.0, it supports ten account channels: LobsterAI, Qoder, TRAE, Usage@linxin666/dsh-usageUsage statistics plugin for the dsh web GUI: per-provider balance and coding-plan quota detection plus a live token usage ledger, with a compact price glance card above the sidebar's Settings row

README

Kilo Gateway

A DeepSeek Harness plugin that turns the Kilo Code Gateway into a first-party model provider, ported from @kilocode/kilo-gateway (MIT).

Authentication

Three sources, in precedence order:

OrderSourceNotes
1Credential storeWritten by this bundle's own settings page, or by kilo_login. The normal path.
2KILO_API_KEYAmbient environment, for a shell or CI.
3KILOCODE_TOKENOlder device/session token.
4AnonymousNothing configured. Bearer anonymous, free models only, 200 requests/hour per IP.

KILOCODE_ORGANIZATIONID sets X-KiloCode-OrganizationId for organization-scoped requests.

Entering the key

The key is entered on this bundle's own page in the Settings panel — Settings → Kilo Gateway — which the browser half of this bundle contributes. The page writes the secret through the generated remote.credentials namespace (the same surface the built-in Models page uses) under the KILO_API_KEY reference, so the value lands in the credential store and never in a patch file.

Why a page of its own, instead of the Models settings card: the Models page is not a generic schema renderer. Its editor picks one of exactly two curated layouts from the settings namespace, and renders nothing but a hint for any other:

function layoutOf(ns) {
  if (ns === 'llm-deepseek') return 'deepseek'
  if (ns === 'llm-pi-ai')   return 'pi-ai'
  return 'unknown'   // <- "Other fields live in cordis.patch.yml" and no inputs at all
}

A private namespace can therefore never show an API-key input there, whatever a plugin's own Config schema marks as role('credential-ref'). That marker only tells a renderer how to treat a field it is already being asked to render; it does not enroll a namespace into the curated families. So this bundle does not declare itself into the Models directory at all: it serves the kilo route with its own adapter (below) and puts the key form where a form is actually renderable.

Three ways to supply the key

  1. Settings page — open Settings, pick Kilo Gateway, and paste the key into the secret field. This is the intended path. The secret is stored under the KILO_API_KEY reference.
  2. Reuse an existing reference — set apiKeyEnv to a name you already store the key under, and the plugin reads it in place. No copy is made.
  3. Environment — KILO_API_KEY or KILOCODE_TOKEN, layered under the store, for a shell or CI.

A configured reference that is blank, unknown, or not a legal reference name is skipped rather than raising, so a typo degrades to the next source instead of breaking dispatch.

The browser half's contract

The client loader is strict, and a violation of any of these rules fails the whole client boot (web boot: 1 entry did not activate), which takes the entire web shell down — not just this plugin's page. The rules, each taken from the built-in Models page (dsh-client-ui-settings-models), which registers into the same slot and works:

RuleConsequence of getting it wrong
The plugin's inject names full service keys (remote.credentials, not just remote).An undeclared service throws inside apply(), and the fiber ends in state failed.
A namespace is read through the declared property, never through ctx.get.The client context exposes a service only when the plugin declared it, and the remote namespaces are Typert remotes rather than cordis Services — ctx.get('remote') finds nothing, and the page silently shows "credential store unavailable".
The manifest's dsh.client.inject names the packages that provide those services (@deepseek-ai/dsh-api-remotes, @deepseek-ai/dsh-client-locale, @deepseek-ai/dsh-client-ui-renderer).The bundle is composed before its providers exist, and the services never arrive.
The manifest's dsh.client carries platform and inject, and no immediately.immediately: true is legal, but it makes the entry's activation part of the boot's critical path.
ctx.locale.register(ns, { zh, en }) takes one dictionary per language in a single call.The three-argument register(ns, locale, dict) form is a different overload with different rules.
Every contribution is guarded at both boundaries: the eager ctx.slots.inject(...) call, and the deferred callback the slot runtime invokes later.A throw in the deferred callback happens long after apply() returned, where no enclosing try/catch can catch it.
The slot registration options are { name, id, order, label }, and label is a thunk.A shape break here throws synchronously inside slots.inject.

The slots service notes one more trap, quoted from a sibling plugin:

Callback effects are synchronous disposers; iterable effects install transactionally and dispose in reverse order.

so the slots.inject callback must return the disposer slots.register hands back — never undefined.

What this plugin depends on

Only services the Harness itself ships. @deepseek-ai/dsh-api-remotes is a protected core module: it is a dependency of the app itself, and the plugin hub refuses to let anyone toggle it off, so every host that has a settings panel also has the credential remote. There is no dependency on a plugin that can be uninstalled, and nothing in this bundle reads or writes any file outside the profile's own credential store.

The host half's contract

The adapter is handed a GenerateOptions object by the Harness. Its shape is fixed by the shipped @deepseek-ai/dsh-llm, and one field in it is easy to get wrong:

interface GenerateOptions {
  provider: string
  model: string
  messages: RequestMessage[]
  system?: string
  tools?: ToolSchema[]
  toolHistory?: ToolHistory      // an OBJECT, not an array
  temperature?: number
  maxTokens?: number
  stop?: string[]
  signal?: AbortSignal
  purpose?: 'compaction' | 'session-title'
}

interface ToolHistory {
  readonly tools: readonly ToolSchema[]
  readonly updates: readonly { readonly messageId: MessageId; readonly additions: readonly ToolSchema[] }[]
}

toolHistory is that object. Reading it as an array of updates throws toolHistory is not iterable on every dispatch that carries a tool history — and a toolHistory.length === 0 guard does not catch the mistake, because the length of an object is undefined, so the comparison is false and the loop runs anyway.

The host reports that failure as LlmError: toolHistory is not iterable with code UNKNOWN, thrown from ReactLoopAgent.step. That stack is where the host re-throws an adapter failure, not where the failure originated: the host wraps a non-LlmError as { message: errorChain(error), code: 'UNKNOWN' }. A bare TypeError message with code UNKNOWN therefore points at the adapter, and the real throw site is wherever the plugin read the option.

Two more traps in the same interface:

  • streamCall(options, model) receives the model as a string id, not a model object. A model object stringifies to [object Object] in every message that mentions the model.
  • ToolSchema carries a deferLoading flag that is a Harness loading strategy, not provider wire content. It must not be forwarded to the provider.

The stream chunks the adapter emits

The adapter yields StreamChunks, and the host validates each one as it arrives:

type StreamChunk =
  | { type: 'block-start'; index: number; blockType: ContentBlockType }
  | { type: 'text-delta'; index: number; text: string }
  | { type: 'reasoning-delta'; index: number; text: string }
  | { type: 'tool-call-delta'; index: number; id: ToolCallId; name?: string; argumentsDelta: string }
  | { type: 'block-end'; index: number; block: ContentBlock }
  | { type: 'usage'; usage: TokenUsage }
  | { type: 'finish'; reason: FinishReason; replayState?: ReplayEnvelope }

The id on a tool-call-delta is required, and required on every delta:

if (typeof chunk.id !== "string") throw new TypeError("tool-call-delta id must be a string")

An OpenAI-shaped stream sends the id only on the first chunk of a call and carries nothing but arguments on the rest, so a delta that forwards the provider's id as-is omits it from every continuation chunk and the host rejects the stream. The adapter therefore remembers the id per block and repeats it, deriving a stable one for a provider that never sent any — the same id the finished tool-call block carries, so the deltas and the block agree.

The host's assembly is more forgiving than its validation: it falls back to brandString('call-' + index) when no id was seen. That fallback is unreachable for a provider that streams a tool call in more than one chunk, because the validation rejects the second chunk first.

Keeping a long-lived install working

A credential that worked once can stop working: a key gets rotated, a device grant expires, or a token is revoked out of band. Failing identically on every later turn would make the provider unusable until the process restarted, so a rejected credential is demoted rather than re-tried forever:

  • A token that comes back 401/403 is remembered as bad for ten minutes and skipped on later dispatches, so a stale KILO_API_KEY in the environment stops shadowing a valid stored grant.
  • A successful sign-in or key write clears that memory immediately.
  • The model catalogue is cached for fifteen minutes rather than per request, and a failed refresh keeps the previous catalogue, so a transient outage degrades to slightly stale metadata instead of an empty model list.

Anonymous vs. keyed

Anonymous access is genuinely usable: the plugin filters the catalog down to models it can actually reach, so kilo-auto/free and the :free ids are selectable without any credential. Selecting a paid model while anonymous fails immediately with a clear AUTH error naming the fix, instead of a bare 401 from the gateway.

Note Kilo's own warning about kilo-auto/free: it may route to providers that log prompts and use them for training. Do not send confidential data through the free tier.

Credential records

The record half of the credentials seam is a discriminated union, not a free-form bag:

type CredentialRecord = ApiKeyRecord | GrantRecord
interface ApiKeyRecord { kind: 'api-key'; key?: string; env?: Record<string,string> }
interface GrantRecord  { kind: 'grant';   payload: unknown }

Two consequences the plugin is careful about:

  • A record written without a kind passes the write guard (assertStorableApiKey only inspects key/env) and then throws at the next boot (record "..." has no kind), failing the credentials plugin's activation for the whole profile. describeRecord still reports configured: true in the meantime, so the mistake is silent until restart.
  • Readers must check kind === 'api-key'; a grant record is valid and configured but carries no readable secret.

Tokens are therefore written as { kind: 'api-key', key: token } and read with a kind check.

Per-model context windows

Every Kilo model declares its own context_length and top_provider.max_completion_tokens, and they differ by orders of magnitude — a 1M-token router sits next to a 256K one. Getting these into the harness correctly is what stops the gateway rejecting a request with an opaque 400.

resolveModel reports each model's real capacity in the exact shape the runtime validates:

{
  provider, id, name,
  inputModalities: ['text', 'image'],
  context: { contextWindow: 1_000_000 },   // nested — a top-level contextWindow is ignored
  defaultMaxTokens: 65_536,                // not `maxTokens`
}

@deepseek-ai/dsh-compaction-basic then compacts proactively: on every step it reads context.contextWindow, budgets contextWindow − reservedCompletionTokens − 65536 headroom, and summarizes once measured history passes min(window × 0.8, that budget).

Two consequences drive lib/limits.js:

  1. A window too small to absorb the harness's 64K headroom would make compaction throw. The error is caught and logged, and the turn continues with the request untruncated until the provider rejects it. So limitsOf never reports a window below MINIMUM_REPORTABLE_CONTEXT_WINDOW (98 304), and caps the output reservation to leave both the headroom and a 32 768-token pressure floor. declaredContextWindow keeps the true catalog figure alongside.
  2. The output cap is derived, not squeezed per-call. The harness already reserved output while compacting, so max_tokens is the model's own cap, trimmed only if the assembled request would otherwise overflow.

The pressure floor matters for a specific real case: mistralai/devstral-2512 declares a 209 715-token completion cap against a 262 144-token window. Clamping output to window − headroom alone lands the reservation at exactly 65 536, leaving pressureBudget = 0 and making compaction throw — the very failure the accounting exists to prevent. Reserving 32 768 on top keeps it positive.

compactionBudget(limits) mirrors the harness arithmetic, so you can check whether a given model will actually compact rather than discovering it from a failed turn.

listModels deliberately carries no capacity fields: the runtime validates that projection as { provider, id, name, description?, inputModalities? } and silently drops anything else.

Streaming and reasoning

The SSE reader is line-based, not blank-line-split: SSE permits \n, \r\n, or \r terminators, and Kilo's gateway streams CRLF. A naive indexOf('\n\n') finds no boundary in that stream and silently yields nothing, which reads downstream as an empty response. Frames are terminated by an empty line, consecutive data: lines are joined with \n per spec, and a stream that ends without its terminating blank line is flushed.

Kilo sends reasoning twice — a plain delta.reasoning string and a structured reasoning_details array mixing readable reasoning.text entries with encrypted reasoning.encrypted ones. The adapter prefers the string and falls back to the readable entries.

An answer-less response is therefore not automatically a failure, and is classified by what actually arrived:

OutcomeReported as
Reasoning arrived, answer did notfinish with kind max-tokens
Provider billed output but streamed nothing readableMALFORMED_RESPONSE
Entirely empty streamEMPTY_RESPONSE (retried)

Token estimation is deliberately pessimistic (3 characters per token); over-estimating only costs margin, under-estimating causes the failure this exists to prevent.

Block lifecycle and usage

Every opened block is closed with block-end carrying the finished ContentBlock, and token usage is emitted as its own usage chunk rather than attached to finish. Both matter:

  • block-end is the only way a block's content reaches the session log. A stream that never closes its blocks streams text the user can see but that never becomes history, so the agent loop loses the reply.
  • Usage arrives after the terminal finish_reason on OpenRouter-compatible streams. The adapter therefore records the reason and keeps reading until the stream actually ends, then emits blocks, usage, and finish in that order — otherwise every such response reports zero tokens.

Failure handling

lib/resilience.js splits failures into two families, because they need opposite treatment.

Parameter faults are deterministic. The same body is rejected the same way every time, so replaying it verbatim only wastes money. Each carries an exact repair:

FaultRepair
Context length exceededTrim the oldest half of the history, keeping the system message
Tool definitions rejectedDrop the tool declarations
Images rejected by a text-only modelStrip image parts, keep every message
A named invalid fieldDrop that field
Any other validator rejectionTrim history

Repairs only ever remove something, which is what makes replaying one safe: removal cannot introduce a rejection the original body did not already have. The budget is two rounds by default (repairBudget), because a request needing three different repairs is one the agent should see rather than keep paying for.

Stream faults are stochastic. A reset socket, a truncated body, a 5xx, or a stalled connection is retried. Retries are silent: nothing is printed, nothing reaches the agent, and no error is logged unless every attempt fails.

What decides whether a retry is acceptable is whether the user is already looking at an answer:

StateAction
Nothing emitted yetRetry silently — the failure is invisible
Text already streamedNever restart it; keep the partial answer
A tool call already emittedNever retry — a second call could duplicate a side effect

A caller cancel is never retried and is rethrown as the caller's own error, so the harness sees a cancellation rather than a mislabelled gateway failure. A stream that delivers nothing for streamIdleTimeoutMs is treated as stalled, so a wedged connection surfaces as a retry rather than a long freeze.

MALFORMED_RESPONSE is deliberately excluded from retryableCodes: the provider billed output it never sent, so replaying would spend the same money for the same empty result.

What it does

  • Provider route kilo — registered on ctx.llm with this bundle's own adapter, which speaks the OpenRouter-compatible wire protocol exposed at https://ai-gateway.kilo.ai/api/openrouter (chat completions, streaming SSE, tool calls, reasoning deltas, usage accounting, self-healing retries).
  • Dedicated settings page — the browser half contributes a "Kilo Gateway" page in the Settings panel holding the API-key form, because the Models page cannot render one for a private namespace.
  • Device-authorization sign-in — a flow on the authorization seam keyed to the credential record it writes, so the Kilo token lands in the Harness credential store and every surface that renders sign-in flows shows "Kilo Gateway" beside the others.
  • Agent tools — account state, code completion, the embedding catalogue, and notifications. See below.
  • Anonymous fallback — the route is registered before sign-in, so a session can select kilo immediately and run on Kilo's free tier.

Agent tools

ToolPurpose
kilo_accountProfile, selected organization, Kilo Pass credits, coding-plan subscriptions and quotas, BYOK entries
kilo_completeCode completion via Kilo FIM (fim/completions) or Next Edit (edit), with prompt/suffix
kilo_embedding_modelsThe embedding catalogue Kilo advertises, with dimensions and score thresholds
kilo_notificationsAccount notifications: plan changes, credit grants, service incidents
kilo_loginDevice-code sign-in when the agent needs to renew a token

kilo_complete defaults to inception/mercury-next-edit and accepts kilo/mistralai/codestral-2508 and kilo/inception/mercury-edit-2. The direct-BYOK autocomplete models (Mistral, Inception) are listed upstream but deliberately not routable here: they need the caller's own provider key, not the Kilo Gateway credential, so the tool refuses them with provider_not_supported.

Sign-in

kilo_login needs an existing credential to authorize the device-code request, exactly like kilo_login in the Harness's own OAuth tool: it renews a token that exists but was rejected, and cannot create a first credential. For a first-time sign-in, use the Kilo Gateway entry on the account page — the authorization flow is what writes the first token.

Why it does not import @deepseek-ai/dsh-*

A bundle resolves its peer dependencies to its own copies under the profile's node_modules. Importing @deepseek-ai/dsh-llm for LlmAdapter/resolveRetryPolicy, or @deepseek-ai/dsh-tools for defineTool, would give this package a different Service/Context class identity than the one the Loader is running, so Context.is() inside TypertRemoteService fails and activation dies with Cannot read properties of undefined (reading 'ctx').

The adapter therefore duck-types the LlmAdapter surface and reconstructs the two things it needs structurally (KiloLlmError carrying a stable code, and DEFAULT_RETRY_POLICY). Tool definitions are written as raw JSON Schema, which is what ctx.tools.register validates directly. ctx.llm, ctx.authorization, ctx.credentials, and ctx.tools are reached through the context, not through imports. This is the same shape the published @opencode2dsh/dsh-plugin bundle uses.

Layout

FileRole
index.jsHost half: registers the kilo adapter, the auth flow, and the tools
client.jsBrowser half: the "Kilo Gateway" settings page holding the API-key form
lib/constants.jsEndpoints, headers, timeouts, defaults
lib/url.jsGateway / OpenRouter / API base-URL resolution
lib/headers.jsX-KILOCODE-* attribution headers
lib/token-store.jsToken and organization id over the credentials seam
lib/api.jsProfile, balance, device authorization, embedding catalogue
lib/models.jsChat model catalog
lib/adapter.jsThe chat adapter: streaming, block lifecycle, usage, self-healing retries
lib/resilience.jsFailure classification, repairs, and the self-healing policy
lib/authorization.jsThe device-code sign-in flow
lib/account.jsKilo Pass, coding-plan quotas, BYOK, notifications
lib/fim.jsAutocomplete model catalogue and FIM / Next Edit requests
lib/limits.jsPer-model context windows, free-tier gating, request fitting
lib/tools.jskilo_account, kilo_complete, kilo_embedding_models, kilo_notifications
lib/login-tool.jskilo_login
tools/verify.mjsOffline verification of the streaming and resilience contracts

An earlier revision of this bundle served chat through @deepseek-ai/dsh-llm-pi-ai and seeded a kilo profile into that plugin's settings document. That was wrong twice over — it wrote another plugin's namespace, and it made pi-ai claim the kilo route this bundle needs. index.js now removes exactly that seeded profile on activation (identified by the endpoint it wrote), so the leftover cannot cause DUPLICATE_ADAPTER on an upgraded install.

Configuration

Environment variables, read at request time:

VariableDefaultMeaning
KILO_API_KEY—Kilo Gateway API key. Normally entered on the bundle's own Settings page instead.
KILOCODE_TOKEN—Older device/session token, also honoured.
KILOCODE_ORGANIZATIONID—Organization scope, sent as X-KiloCode-OrganizationId.
KILO_AI_GATEWAY_URLhttps://ai-gateway.kilo.ai/api/v1Gateway traffic root.
KILO_API_URLhttps://api.kilo.aiProfile / balance / device-auth root.
KILOCODE_EDITOR_NAMEDeepSeek HarnessX-KILOCODE-EDITORNAME value.
KILOCODE_FEATURE—Opt-in X-KILOCODE-FEATURE attribution.
Config fieldDefaultMeaning
apiKeyEnvKILO_API_KEYCredential reference holding the key. Resolution tries this first, then KILO_API_KEY.
baseURL—Override the gateway base URL.
organizationId—Organization scope, overriding the environment and the stored record.
repairBudget2Repair-and-replay rounds per request. 0 disables self-healing.
streamIdleTimeoutMs90000Silence after which a stream counts as stalled and is retried.

All fields are volatile(), so editing one takes effect without rewriting the patch.

Verify

node tools/verify.mjs
node tools/verify-client.mjs
node tools/verify-publishable.mjs
node tools/verify-host-contract.mjs

tools/verify.mjs runs the real adapter against a stubbed fetch: it asserts the chunk sequence and ordering, the block-end and usage contracts, the classifier's parameter-versus-stream split, the recovery policy, and that a retry is silent, a repair drops the right thing, and an abort is never replayed.

tools/verify-client.mjs runs the real browser half against a stub React and a client-shaped ctx: it asserts the plugin's service list, the slot registration, and that a missing remote namespace, a missing locale service, or a broken slot seam each degrade to a message instead of failing the client boot.

tools/verify-publishable.mjs checks the shipped Harness's own rules against this bundle's declarations: that an unknown package in the manifest's inject list is skipped rather than fatal, that a missing ./client export is fatal, and that dsh.client.inject and dsh.client.immediately are both optional.

tools/verify-host-contract.mjs reads the shipped @deepseek-ai/dsh-llm out of the app's asar and asserts the adapter still agrees with it — in particular that toolHistory is the ToolHistory object and is never iterated as an array. A stubbed fetch cannot catch that class of mistake, because the stub feeds the adapter whatever shape the test author believed in; this probe reads the shape the host actually sends.

Neither suite needs a network or a credential.

Install

plugin_manager install_bundle <absolute path to this directory>

Attribution

Kilo Code is developed by Kilo-Org and licensed MIT. This plugin is an independent port of its gateway client and is not affiliated with or endorsed by Kilo-Org.