🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

Kilo 模型网关 qianmo978/dsh-kilo-gateway

DeepSeek Harness
qianmo9789 天前有提交已提供中文文档扫描:未发现敏感能力

把 Kilo 网关接入为模型服务商,统一管理密钥与路由

30/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/23 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add qianmo978/dsh-kilo-gateway
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · model
装得上吗
本站已在 dsh 0.2.1-rc.2 下真实安装成功
非静态推断
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 9 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🟢 扫描:未发现敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描未发现敏感能力:最多只发网络请求、读取自身元数据或只读文件,未发现执行命令、文件写入或敏感信息读取。

发起外部网络请求
查看扫描证据(共 13 处,展示前 13 条)
  • 发起网络请求
    lib/adapter.js:932
    response = await fetch(`${baseURL}/chat/completions`, {
  • 发起网络请求
    lib/login-tool.js:37
    const response = await fetch(`${resolveKiloApiBase()}/api/device-auth/codes`, {
  • 发起网络请求
    lib/login-tool.js:72
    const response = await fetch(
  • 发起网络请求
    lib/models.js:92
    const response = await fetch(modelsURL, {
  • 发起网络请求
    lib/api.js:24
    const response = await fetch(`${resolveKiloApiBase()}/api/profile`, {
  • 发起网络请求
    lib/api.js:85
    const response = await fetch(`${resolveKiloApiBase()}/api/profile/balance`, {
  • 发起网络请求
    lib/api.js:112
    const response = await fetch(`${resolveKiloApiBase()}${path}`, {
  • 发起网络请求
    lib/api.js:130
    const response = await fetch(`${resolveKiloApiBase()}/api/device-auth/codes`, {
  • 发起网络请求
    lib/api.js:153
    const response = await fetch(`${resolveKiloApiBase()}/api/device-auth/codes/${encodeURIComponent(code)}`, {
  • 发起网络请求
    lib/api.js:275
    const response = await fetch(new URL('embedding-models', resolveKiloGatewayBaseUrl({ ...(options.token ? { token: options.token } : {}) })), {
  • 发起网络请求
    lib/account.js:103
    response = await fetch(`${resolveKiloApiBase()}/api/trpc/${procedure}${suffix}`, {
  • 发起网络请求
    lib/account.js:183
    const response = await fetch(`${resolveKiloApiBase()}/api/trpc/kiloPass.getState?${params}`, {
  • 发起网络请求
    lib/account.js:304
    const response = await fetch(`${resolveKiloApiBase()}/api/users/notifications`, {
扫描于 2026/10/31(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

README

由 DeepSeek 最新模型翻译生成
Kilo Gateway 一个 DeepSeek Harness 插件,把 Kilo Code Gateway 变成第一方模型服务商,移植自 @kilocode/kilo-gateway(MIT)。 认证 三个来源,按优先级排列: | 顺序 | 来源 | 说明 | | 1 | 凭据存储 | 由本包自带的设置页写入,或由 kilo_login 写入。常规路径。 | | 2 | KILO_API_KEY | 环境变量,供 shell 或 CI 使用。 | | 3 | KILOCODE_TOKEN | 较早的设备/会话 token。 | | 4 | 匿名 | 什么都没配。Bearer anonymous,仅限免费模型,每 IP 每小时 200 次请求。 | KILOCODE_ORGANIZATIONID 设置 X-KiloCode-OrganizationId,用于组织范围的请求。 录入密钥 密钥在设置面板中本包自带的页面录入 —— 设置 → Kilo Gateway —— 该页面由本包的浏览器端提供。页面通过生成的 remote.credentials 命名空间(内置 Models 页面用的也是这个接口)写入密钥,引用名为 KILO_API_KEY,因此值会落到凭据存储里,绝不会写进补丁文件。
展开完整文档(README 剩余 1 节)
Kilo Gateway

一个 DeepSeek Harness 插件,把 Kilo Code Gateway 变成第一方模型服务商,移植自 @kilocode/kilo-gateway(MIT)。

认证

三个来源,按优先级排列:

| 顺序 | 来源 | 说明 |
| 1 | 凭据存储 | 由本包自带的设置页写入,或由 kilo_login 写入。常规路径。 |
| 2 | KILO_API_KEY | 环境变量,供 shell 或 CI 使用。 |
| 3 | KILOCODE_TOKEN | 较早的设备/会话 token。 |
| 4 | 匿名 | 什么都没配。Bearer anonymous,仅限免费模型,每 IP 每小时 200 次请求。 |

KILOCODE_ORGANIZATIONID 设置 X-KiloCode-OrganizationId,用于组织范围的请求。

录入密钥

密钥在设置面板中本包自带的页面录入 —— 设置 → Kilo Gateway —— 该页面由本包的浏览器端提供。页面通过生成的 remote.credentials 命名空间(内置 Models 页面用的也是这个接口)写入密钥,引用名为 KILO_API_KEY,因此值会落到凭据存储里,绝不会写进补丁文件。

为什么单独做一个页面,而不用 Models 设置卡片:Models 页面不是通用的 schema 渲染器。它的编辑器只从设置命名空间里挑两种固定布局之一,其他命名空间一律只渲染一句提示:

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
}

所以私有命名空间在那里永远显示不出 API 密钥输入框,无论插件的 Config schema 把某个字段标成 role('credential-ref') 都没用。这个标记只是告诉渲染器怎么处理一个它已经被要求渲染的字段,并不会把命名空间纳入固定布局家族。因此本包干脆不把自己声明进 Models 目录:它用自己的适配器(见下文)提供 kilo 路由,把密钥表单放在真正能渲染表单的地方。

提供密钥的三种方式

1. 设置页 —— 打开设置,选 Kilo Gateway,把密钥粘贴进密钥字段。这是推荐路径。密钥存在 KILO_API_KEY 引用下。
2. 复用已有引用 —— 把 apiKeyEnv 设成你已存密钥的名字,插件就地读取。不会复制。
3. 环境变量 —— KILO_API_KEY 或 KILOCODE_TOKEN,位于存储之下,供 shell 或 CI 使用。

已配置的引用如果为空、未知或不是合法引用名,会被跳过而不是报错,所以拼错只会降级到下一个来源,不会中断分发。

浏览器端的约定

客户端加载器很严格,违反以下任何一条都会导致整个客户端启动失败(web boot: 1 entry did not activate),整个 web shell 都会挂掉 —— 不只是这个插件的页面。这些规则逐条取自内置的 Models 页面(dsh-client-ui-settings-models),它注册进同一个插槽,并且能正常工作:

| 规则 | 弄错的后果 |
| 插件的 inject 写的是完整服务键(remote.credentials,不是只写 remote)。 | 未声明的服务会在 apply() 里抛错,fiber 最终停在 failed 状态。 |
| 命名空间要通过声明的属性读取,绝不要走 ctx.get。 | 只有插件声明过的服务,客户端上下文才会暴露;而 remote 命名空间是 Typert remote,不是 cordis Service —— ctx.get('remote') 什么也找不到,页面会静默显示「credential store unavailable」。 |
| manifest 的 dsh.client.inject 写的是提供这些服务的包(@deepseek-ai/dsh-api-remotes、@deepseek-ai/dsh-client-locale、@deepseek-ai/dsh-client-ui-renderer)。 | bundle 会在其服务商存在之前就完成组装,服务永远到不了。 |
| manifest 的 dsh.client 带 platform 和 inject,不带 immediately。 | immediately: true 合法,但会让该条目的激活进入启动的关键路径。 |
| ctx.locale.register(ns, { zh, en }) 在单次调用里传每种语言一份字典。 | 三参数形式 register(ns, locale, dict) 是另一个重载,规则不同。 |
| 每处贡献都要在两个边界上做防护:立即执行的 ctx.slots.inject(...) 调用,以及插槽运行时稍后调用的延迟回调。 | 延迟回调里抛错时,apply() 早已返回,外层没有任何 try/catch 能接住。 |
| 插槽注册选项是 { name, id, order, label },label 是 thunk。 | 这里形状不对,会在 slots.inject 内同步抛错。 |

slots 服务还记了另一个坑,引自一个同类插件:

回调型 effect 是同步 disposer;可迭代型 effect 以事务方式安装,并按相反顺序 dispose。

所以 slots.inject 的回调必须返回 slots.register 交回的 disposer —— 绝不能返回 undefined。

这个插件依赖什么

只依赖 Harness 自身随附的服务。@deepseek-ai/dsh-api-remotes 是受保护的核心模块:它是应用自身的依赖,插件中心不允许任何人把它关掉,所以每个带设置面板的宿主也都有 credential remote。它不依赖任何可卸载的插件,这个 bundle 里也没有任何东西读写配置档自身凭据存储之外的文件。

没有外部包。 bundle 只导入 Node 内置模块,以及在浏览器端导入 react(由 web shell 的模块表提供)。schema 库以 vendor 形式放在 vendor/ 下,tools/verify-installable.mjs 负责强制这条规则。

为什么 schema 库要 vendor

导入 @deepseek-ai/schemastery 的插件在大多数机器上都无法激活:

导入失败:Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@deepseek-ai/schemastery'

Harness 会把插件里 @deepseek-ai/ 的导入路由到宿主自己的副本——但只限宿主以 host-core 形式随附的包:

for (const entry of readdirSync(join(hostNodeModulesRoot(dir), '@deepseek-ai'))) {
if (/^(?:dsh|cordis)/.test(entry.name)) names.add(@deepseek-ai/${entry.name})
}

schemastery 两个前缀都不匹配,所以永远不会被路由。它必须能从插件自己的目录解析到,按 Node 的解析方式。

用 link: 安装的插件——Harness 文档里写的形式,dsh plugin add "link:$PWD"——没有自己的 node_modules:pnpm 不会安装被链接包的依赖。于是这个 bundle 只在某台机器上碰巧有别的插件装过 schemastery 时才能激活,在其他机器上全都失败。把它声明为 dependency 也没用,原因相同:依赖根本不会被安装。

修法是把 bundle 导入的东西一起带上:

vendor/schemastery/lib/index.mjs # @deepseek-ai/schemastery 3.18.4, MIT
vendor/cosmokit/lib/index.js # @deepseek-ai/cosmokit 1.8.5, MIT

tools/vendor-schemastery.mjs 记录了它们的来源:从本地安装重新 vendor,把 schemastery 里裸的 @deepseek-ai/cosmokit 导入改写成相对路径,--check 校验副本是否最新。上游两个 LICENSE 文件都保留。

宿主侧的契约

Harness 会把 GenerateOptions 对象交给适配器。它的形状由随附的 @deepseek-ai/dsh-llm 固定,其中有一个字段很容易弄错:

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 就是那个对象。把它当成 updates 数组来读,每次带工具历史的派发都会抛 toolHistory is not iterable——而 toolHistory.length === 0 这种守卫拦不住这个错误,因为对象的 length 是 undefined,比较结果为 false,循环照样跑。

宿主把这种失败报成 LlmError: toolHistory is not iterable,code 为 UNKNOWN,从 ReactLoopAgent.step 抛出。那个栈是宿主重新抛出适配器失败的位置,不是失败源头:宿主把非 LlmError 包成 { message: errorChain(error), code: 'UNKNOWN' }。所以一条裸 TypeError 消息配上 code UNKNOWN,指向的是适配器,真正的抛出点在插件读取该选项的地方。

同一个接口里还有两个坑:
- streamCall(options, model) 接收的 model 是字符串 id,不是模型对象。模型对象在每条提到 model 的消息里都会被字符串化成 [object Object]。
- ToolSchema 带一个 deferLoading 标志,这是 Harness 的加载策略,不是服务商的线上内容。不能把它转发给服务商。

适配器发出的流式分块

适配器产出 StreamChunk,宿主在每块到达时逐块校验:

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 }

tool-call-delta 上的 id 是必填的,而且每个 delta 都必须带:

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

OpenAI 风格的流只在一次调用的第一个分块里发 id,其余分块只带 arguments。所以,如果 delta 原样转发服务商的 id,后续每个分块都会缺 id,宿主就会拒掉整条流。因此适配器按块记住 id 并重复发送;服务商从没发过 id 时,就推导一个稳定的 id——与最终 tool-call 块携带的 id 相同,这样 delta 和块对得上。

宿主的组装比校验宽松:没见到 id 时,它会回退到 brandString('call-' + index)。但对一个把工具调用拆成多个分块流式发送的服务商来说,这个回退永远走不到,因为校验会先拒掉第二个分块。

让长期运行的安装保持可用

一个凭据曾经能用,后来可能失效:密钥被轮换、设备授权过期,或者 token 在带外被吊销。如果之后每一轮都同样失败,服务商就会一直不可用,直到进程重启。所以,被拒的凭据会被降级,而不是无限重试:

- 返回 401/403 的 token 会被记为坏凭据,保留十分钟,后续派发时跳过。这样环境里一个过期的 KILO_API_KEY 就不会再遮蔽有效的已存授权。
- 登录成功或写入密钥会立即清除这条记录。
- 模型目录缓存十五分钟,而不是每个请求都拉一次;刷新失败时保留上一份目录,所以短暂故障只会让元数据稍旧,而不是变成空模型列表。

匿名与带密钥

匿名访问确实可用:插件会把目录过滤到它真正能访问的模型,所以 kilo-auto/free 和 :free 这类 id 无需任何凭据就能选。
匿名状态下选择付费模型会立刻失败,并抛出明确的 AUTH 错误,指出修复办法,而不是网关返回一个光秃秃的 401。

注意 Kilo 自己对 kilo-auto/free 的警告:它可能路由到会记录提示词并用于训练的服务商。不要通过免费层发送机密数据。

凭据记录

凭据接缝的记录部分是一个可辨识联合,不是自由形态的袋子:

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

插件对此格外小心的两个后果:

- 写入记录时若不带 kind,会通过写入守卫(assertStorableApiKey 只检查 key/env),然后在下次启动时抛错(record "..." has no kind),导致凭据插件在整个配置档上激活失败。与此同时 describeRecord 仍报告 configured: true,所以这个错误在重启前一直悄无声息。
- 读取方必须检查 kind === 'api-key';grant 记录有效且已配置,但不携带可读的密钥。

因此 token 写成 { kind: 'api-key', key: token },读取时做 kind 检查。

各模型的上下文窗口

每个 Kilo 模型都声明自己的 context_length 和 top_provider.max_completion_tokens,彼此相差几个数量级——1M token 的路由器旁边就挨着 256K 的。把这些正确送进 harness,才能避免网关用含糊的 400 拒绝请求。

resolveModel 按运行时校验的确切形状报告每个模型的真实容量:

{
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 主动压缩:每一步都读取 context.contextWindow,预算为 contextWindow − reservedCompletionTokens − 65536 headroom,一旦实测历史超过 min(window × 0.8, that budget) 就做摘要。

两个后果驱动了 lib/limits.js:

1. 窗口小到装不下 harness 的 64K headroom 时,压缩会抛错。 错误被捕获并记录,本轮继续,请求不截断,直到服务商拒绝。所以 limitsOf 从不报告低于 MINIMUM_REPORTABLE_CONTEXT_WINDOW(98 304)的窗口,并给输出预留设上限,既留出 headroom,也留出 32 768 token 的压力下限。declaredContextWindow 则保留目录中的真实数字。
2. 输出上限是推导出来的,不是每次调用现挤。 harness 在压缩时已经预留了输出,所以 max_tokens 就是模型自己的上限,只有当组装后的请求会溢出时才裁剪。

压力下限对某个具体真实案例很重要:mistralai/devstral-2512 声明了一个 209 715 token 的补全上限,对上 262 144 token 的窗口。只把输出钳到 window − headroom,预留量正好落在 65 536,pressureBudget = 0,压缩就会抛错——这正是这套账要防的故障。再额外预留 32 768,它才能保持为正。

compactionBudget(limits) 复刻 harness 的算术,你可以用它检查某个模型到底会不会压缩,而不是等到某一轮失败才发现。

listModels 刻意不带容量字段:运行时按 { provider, id, name, description?, inputModalities? } 校验这个投影,其他字段一律静默丢弃。

流式与推理

SSE 读取器按行处理,不按空行切分:SSE 允许 \n、\r\n 或 \r 作为终止符,而 Kilo 的网关流的是 CRLF。天真的 indexOf('\n\n') 在这种流里找不到边界,静默返回空,下游读到的就是空响应。帧以空行结束,连续的 data: 行按规范用 \n 拼接,流结束时若缺少终止空行也会被冲刷出来。

Kilo 会把推理发两遍——一份纯字符串 delta.reasoning,一份结构化的 reasoning_details 数组,里面混着可读的 reasoning.text 条目和加密的 reasoning.encrypted 条目。适配器优先用字符串,取不到再退回可读条目。

所以没有答案的响应并不自动算失败,要按实际到达的内容分类:

| 结果 | 上报为 |
| 推理到了,答案没到 | finish,kind 为 max-tokens |
| 服务商计了输出费,但流里没有可读内容 | MALFORMED_RESPONSE |
| 整个流为空 | EMPTY_RESPONSE(会重试) |

token 估算刻意偏悲观(每 token 3 个字符);高估只损失余量,低估则会引发这套机制要防的故障。

块生命周期与用量

每个打开的块都用 block-end 关闭,并带上完成的 ContentBlock;token 用量作为独立的 usage 块发出,不挂在 finish 上。这两点都很关键:

- block-end 是块内容进入会话日志的唯一途径。流始终不关闭块,用户能看到文本,但它永远进不了历史,智能体循环就丢掉了这条回复。
- 在 OpenRouter 兼容的流上,用量在终止的 finish_reason 之后才到。因此适配器先记下 reason,继续读到流真正结束,再按块、用量、finish 的顺序发出——否则这类响应全都会报零 token。

失败处理

lib/resilience.js 把失败分成两类,因为它们的处理方式正好相反。

参数故障是确定性的。 同一个请求体每次都会被同样拒绝,原样重放只会浪费钱。每种故障都有精确的修复方式:

| 故障 | 修复 |
| 上下文长度超限 | 裁掉历史记录最旧的一半,保留系统消息 |
| 工具定义被拒 | 丢弃工具声明 |
| 纯文本模型拒绝图片 | 去掉图片部分,保留每条消息 |
| 某个具名字段无效 | 丢弃该字段 |
| 其他校验器拒绝 | 裁剪历史记录 |

修复只会删除内容,正因如此,重放修复才是安全的:删除不会引入原始请求体本就没有的拒绝。默认预算为两轮(repairBudget),因为需要三种不同修复的请求,应该让智能体看到,而不是继续为它付费。

流式故障是随机发生的。 连接重置、响应体截断、5xx 或连接停滞都会重试。重试是静默的:不打印任何内容,不向智能体传递任何内容,除非所有尝试都失败,否则不记录错误。

判断重试是否可接受,取决于用户是否已经在看答案:

| 状态 | 操作 |
| 尚未输出任何内容 | 静默重试——故障不可见 |
| 文本已流式输出 | 绝不重新开始;保留部分答案 |
| 工具调用已发出 | 绝不重试——第二次调用可能重复副作用 |

调用方取消绝不重试,而是作为调用方自己的错误重新抛出,这样 harness 看到的是取消,而不是被错误标记的网关故障。流在 streamIdleTimeoutMs 内没有交付任何内容,就视为停滞,这样卡住的连接会表现为重试,而不是长时间冻结。

MALFORMED_RESPONSE 被有意排除在 retryableCodes 之外:服务商为它从未发送的输出计费,重放会为同样的空结果花同样的钱。

功能

- 服务商路由 kilo —— 注册在 ctx.llm 上,使用本 bundle 自己的适配器,它使用 https://ai-gateway.kilo.ai/api/openrouter 暴露的 OpenRouter 兼容线协议(聊天补全、流式 SSE、工具调用、推理增量、用量核算、自愈重试)。
- 专用设置页 —— 浏览器端在设置面板中贡献一个「Kilo Gateway」页面,承载 API 密钥表单,因为模型页面无法为私有命名空间渲染一个。
- 设备授权登录 —— 在 authorization 接缝上的流程,以它写入的凭据记录为键,因此 Kilo token 落入 Harness 凭据存储,每个渲染登录流程的界面都会在其他项旁边显示「Kilo Gateway」。
- 智能体工具 —— 账户状态、代码补全、嵌入目录和通知。见下文。
- 匿名回退 —— 路由在登录前就注册,因此会话可以立即选择 kilo,并在 Kilo 的免费层上运行。

智能体工具

| 工具 | 用途 |
| kilo_account | 配置档、所选组织、Kilo Pass 额度、编码计划订阅和配额、BYOK 条目 |
| kilo_complete | 通过 Kilo FIM(fim/completions)或 Next Edit(edit)做代码补全,带 prompt/suffix |
| kilo_embedding_models | Kilo 对外公布的嵌入模型目录,含维度和分数阈值 |
| kilo_notifications | 账号通知:套餐变更、额度发放、服务故障 |
| kilo_login | 智能体需要续期 token 时,用设备码登录 |

kilo_complete 默认用 inception/mercury-next-edit,也接受 kilo/mistralai/codestral-2508 和 kilo/inception/mercury-edit-2。直接 BYOK 的自动补全模型(Mistral、Inception)在上游有列出,但这里刻意不路由:它们需要调用方自己的服务商密钥,而不是 Kilo Gateway 凭据,所以工具会以 provider_not_supported 拒绝。

登录

kilo_login 需要已有凭据才能授权设备码请求,和 Harness 自带 OAuth 工具里的 kilo_login 完全一样:它续期一个已存在但被拒绝的 token,无法创建首个凭据。首次登录请用账号页上的 Kilo Gateway 入口——authorization 流程才会写入第一个 token。

为什么不导入 @deepseek-ai/dsh-

bundle 会把 peer 依赖解析到配置档 node_modules 下自己的副本。若为 LlmAdapter/resolveRetryPolicy 导入 @deepseek-ai/dsh-llm,或为 defineTool 导入 @deepseek-ai/dsh-tools,这个包拿到的 Service/Context 类身份就和 Loader 正在运行的那份不同,于是 TypertRemoteService 里的
Context.is() 失败,激活过程以 Cannot read properties of undefined (reading 'ctx') 报错终止。

因此适配器对 LlmAdapter 接口做鸭子类型,并按结构重建它需要的两样东西(携带稳定 code 的 KiloLlmError,以及 DEFAULT_RETRY_POLICY)。工具定义写成原始 JSON Schema,ctx.tools.register 直接校验的就是它。ctx.llm、ctx.authorization、ctx.credentials、ctx.tools 都通过上下文访问,不走导入。已发布的 @opencode2dsh/dsh-plugin bundle 用的也是同样的形态。

目录结构

| 文件 | 作用 |
| index.js | 宿主侧:注册 kilo 适配器、认证流程和工具 |
| client.js | 浏览器侧:「Kilo Gateway」设置页,放 API 密钥表单 |
| lib/constants.js | 端点、请求头、超时、默认值 |
| lib/url.js | Gateway / OpenRouter / API base URL 解析 |
| lib/headers.js | X-KILOCODE- 归属请求头 |
| lib/token-store.js | 通过凭据接缝存取 token 和组织 id |
| lib/api.js | 配置档、余额、设备授权、嵌入模型目录 |
| lib/models.js | 聊天模型目录 |
| lib/adapter.js | 聊天适配器:流式输出、块生命周期、用量、自愈重试 |
| lib/resilience.js | 失败分类、修复、自愈策略 |
| lib/authorization.js | 设备码登录流程 |
| lib/account.js | Kilo Pass、coding-plan 配额、BYOK、通知 |
| lib/fim.js | 自动补全模型目录,以及 FIM / Next Edit 请求 |
| lib/limits.js | 各模型上下文窗口、免费层限制、请求适配 |
| lib/tools.js | kilo_account、kilo_complete、kilo_embedding_models、kilo_notifications |
| lib/login-tool.js | kilo_login |
| tools/verify.mjs | 离线验证流式与容错契约 |

这个 bundle 的早期版本通过 @deepseek-ai/dsh-llm-pi-ai 提供聊天服务,并向该插件的设置文档里写入了一份 kilo 配置档。这错了两处:它写了另一个插件的命名空间,还让 pi-ai 占用了本 bundle 需要的 kilo 路由。现在 index.js 在激活时会精确删除那份写入的配置档(按它写入的 endpoint 识别),所以升级安装后残留的配置不会引发 DUPLICATE_ADAPTER。

配置

环境变量,在请求时读取:

| 变量 | 默认值 | 含义 |
| KILO_API_KEY | — | Kilo Gateway API key。通常改为在 bundle 自己的 Settings 页面填写。 |
| KILOCODE_TOKEN | — | 较早的设备/会话 token,同样生效。 |
| KILOCODE_ORGANIZATIONID | — | 组织范围,以 X-KiloCode-OrganizationId 发送。 |
| KILO_AI_GATEWAY_URL | https://ai-gateway.kilo.ai/api/v1 | Gateway 流量根地址。 |
| KILO_API_URL | https://api.kilo.ai | Profile / 余额 / 设备认证根地址。 |
| KILOCODE_EDITOR_NAME | DeepSeek Harness | X-KILOCODE-EDITORNAME 的值。 |
| KILOCODE_FEATURE | — | 可选加入的 X-KILOCODE-FEATURE 归因。 |

| 配置字段 | 默认值 | 含义 |
| apiKeyEnv | KILO_API_KEY | 保存密钥的凭据引用。解析时先试它,再试 KILO_API_KEY。 |
| baseURL | — | 覆盖 gateway 基础 URL。 |
| organizationId | — | 组织范围,覆盖环境变量和已存记录。 |
| repairBudget | 2 | 每个请求的修复重放轮数。0 表示关闭自愈。 |
| streamIdleTimeoutMs | 90000 | 流静默超过这个时长即视为卡住,并重试。 |

所有字段都是 volatile(),改一个字段立即生效,无需重写补丁。

验证

node tools/verify.mjs
node tools/verify-client.mjs
node tools/verify-publishable.mjs
node tools/verify-host-contract.mjs
node tools/verify-installable.mjs
node tools/vendor-schemastery.mjs --check

tools/verify.mjs 让真实适配器跑在打桩的 fetch 上:断言 chunk 序列与顺序、block-end 与 usage 契约、分类器对参数与流的拆分、恢复策略,以及重试静默、修复丢弃正确的对象、中止永不重放。

tools/verify-client.mjs 让真实浏览器端跑在打桩的 React 和客户端形态的 ctx 上:断言插件的服务列表、slot 注册,以及缺失的远程命名空间缺失、locale 服务缺失、插槽接缝损坏,每种情况都降级为一条消息,而不是让客户端启动失败。

tools/verify-publishable.mjs 拿随包发布的 Harness 自身的规则,对照本 bundle 的声明做检查:清单 inject 列表里出现未知包时跳过而非致命;缺少 ./client 导出确实*致命;dsh.client.inject 和 dsh.client.immediately 都是可选项。

tools/verify-host-contract.mjs 从应用的 asar 里读出随包发布的 @deepseek-ai/dsh-llm,断言适配器仍与它一致——尤其是 toolHistory 是 ToolHistory 对象,绝不会被当作数组遍历。打桩的 fetch 抓不到这类错误,因为桩喂给适配器的形状取决于测试作者相信什么;这个探针读的是宿主实际发出的形状。

tools/verify-installable.mjs 断言本 bundle 不导入它自身未携带的任何东西:每个裸导入要么是 Node 内置模块,要么是 react;随附的副本连同许可证都在;整个宿主侧目录树在未安装任何同级包的情况下也能干净导入——也就是机器上只有这个插件的情形。

两套检查都不需要网络或凭据。

安装

plugin_manager install_bundle <absolute path to this directory>

归属

Kilo Code 由 Kilo-Org 开发,采用 MIT 协议。
本插件是其网关客户端的独立移植版本,与 Kilo-Org 无隶属关系,也未获其背书。
预览分享卡片长什么样 ↓qianmo978/dsh-kilo-gateway
分享 · DeepSeek Harness Hub
qianmo978/dsh-kilo-gateway

把 Kilo 网关接入为模型服务商,统一管理密钥与路由

回到顶部