billion-context 0.1.53 → 0.1.54-pr.246.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,7 +67,7 @@ Two ways to use it — pick one:
67
67
  - **URL change (persistent):** prefix your client's baseURL with the proxy
68
68
  origin + `/bili/`.
69
69
 
70
- ### Option 1 — Launcher (`bili pi` / `bili codex` / `bili claude` / `bili omp` / `bili opencode` / `bili hermes`)
70
+ ### Option 1 — Launcher (`bili pi` / `bili codex` / `bili claude` / `bili omp` / `bili opencode` / `bili hermes` / `bili dsh`)
71
71
 
72
72
  The launcher wraps a client in one command: it starts a proxy on an
73
73
  independent port (a fresh instance is always spawned — a port is never
@@ -85,6 +85,7 @@ bili claude # launch claude through the proxy
85
85
  bili omp # pi-style: MITM env + persistent overlay home (~/.omp/agent-bili, real config untouched)
86
86
  bili opencode # MITM for HTTPS + temp opencode.json (/bili/ for HTTP) + thin /acp plugin
87
87
  bili hermes # no MITM possible (certifi CA) — persistent overlay home (~/.hermes-bili), all traffic /bili/
88
+ bili dsh # deepseek-harness: built-in deepseek route via DEEPSEEK_BASE_URL + overlay DSH_HOME (~/.dsh-bili), all traffic /bili/
88
89
  bili pi --mitm-domain api.foo.com # add a domain to the MITM whitelist
89
90
  ```
90
91
 
@@ -148,6 +149,23 @@ bili --no-auto-update # disable self-update for this run
148
149
 
149
150
  Flags override env vars and the config file. `bili --help` lists them all.
150
151
 
152
+ ### Remote agents (`--host`)
153
+
154
+ By default the proxy binds `127.0.0.1` and only accepts loopback
155
+ connections. To serve agents on other machines, bind a non-loopback host:
156
+
157
+ ```bash
158
+ bili --host 0.0.0.0 # all interfaces (or use your LAN IP)
159
+ ```
160
+
161
+ - Remote agents point their model `baseURL` at `http://<this-host>:<port>/bili/…`.
162
+ - MITM-mode `CONNECT` then also accepts remote clients — for **whitelisted
163
+ model hosts only**. Blind tunnels to arbitrary hosts stay loopback-only, so
164
+ the proxy can never be used as an open relay.
165
+ - There is **no authentication**: only do this on a trusted LAN or behind a
166
+ firewall. The `/__bili/` management endpoints remain loopback-only.
167
+ - A startup `[security]` warning reminds you of the above.
168
+
151
169
  ### Debugging
152
170
 
153
171
  Three ways to enable verbose logging (priority: flag > env > config):
package/README.zh-CN.md CHANGED
@@ -67,7 +67,7 @@ npm install -g billion-context
67
67
 
68
68
 
69
69
 
70
- ### 方式 1 —— 启动器(`bili pi` / `bili codex` / `bili claude` / `bili omp` / `bili opencode` / `bili hermes`)
70
+ ### 方式 1 —— 启动器(`bili pi` / `bili codex` / `bili claude` / `bili omp` / `bili opencode` / `bili hermes` / `bili dsh`)
71
71
 
72
72
  启动器把客户端包进一条命令:在独立端口拉起一个代理(总是全新实例,绝不复用端口),再按客户端支持的机制把它指向代理 —— 能吃代理/CA 环境变量的走**证书 MITM**,不吃的走隔离的**`/bili/` 配置重写**。真实配置文件从不被修改;客户端自己的配置只被**读取**,用来发现它实际连接的 HTTPS 上游主机,把这些主机加入 MITM 白名单 —— 代理只 TLS 终结它们,其余流量盲透传。
73
73
 
@@ -78,6 +78,7 @@ bili claude # 拉起 claude
78
78
  bili omp # pi 同款 MITM 环境变量 + 隔离临时 models.yml
79
79
  bili opencode # HTTPS 走 MITM + 临时 opencode.json(HTTP 走 /bili/)+ 轻量 /acp 插件
80
80
  bili hermes # 无法 MITM(certifi CA)—— 隔离 HERMES_HOME,全部流量 /bili/
81
+ bili dsh # deepseek-harness:内置 deepseek 路由走 DEEPSEEK_BASE_URL + 隔离 DSH_HOME(~/.dsh-bili),全部流量 /bili/
81
82
  bili pi --mitm-domain api.foo.com # 向 MITM 白名单追加域名
82
83
  ```
83
84
 
@@ -133,6 +134,21 @@ bili --no-auto-update # 本次启动禁用自动更新
133
134
 
134
135
  参数优先级高于环境变量和配置文件。`bili --help` 列出全部。
135
136
 
137
+ ### 远程 agent(`--host`)
138
+
139
+ 默认绑定 `127.0.0.1`,只接受本机连接。要给其他机器上的 agent 用,绑定非 loopback 地址:
140
+
141
+ ```bash
142
+ bili --host 0.0.0.0 # 所有网卡(或直接用局域网 IP)
143
+ ```
144
+
145
+ - 远程 agent 把模型 `baseURL` 指向 `http://<本机IP>:<端口>/bili/…`。
146
+ - MITM 模式的 `CONNECT` 也会接受远程客户端 —— 但仅限**白名单内的模型域名**;
147
+ 到任意主机的盲隧道仍仅限本机,代理不会沦为开放中继。
148
+ - **没有任何鉴权**: 只应在可信局域网或防火墙内使用。`/__bili/` 管理
149
+ 端点仍仅限本机访问。
150
+ - 启动时的 `[security]` 警告会提醒上述事项。
151
+
136
152
  ### 调试
137
153
 
138
154
  三种方式打开详细日志(优先级:参数 > 环境变量 > 配置):
package/dist/index.js CHANGED
@@ -50295,6 +50295,7 @@ async function* runCompressLoop(upstream, ctx, requestBody, requestOptions, adap
50295
50295
  const calls = [];
50296
50296
  let usage = {};
50297
50297
  let finishReason;
50298
+ let sawDone = false;
50298
50299
  let suppressCompletion = false;
50299
50300
  for await (const ev of adapter.parseStream(currentUpstream, round)) {
50300
50301
  if (signal?.aborted) break;
@@ -50332,6 +50333,7 @@ async function* runCompressLoop(upstream, ctx, requestBody, requestOptions, adap
50332
50333
  cachedTokens: ev.cachedTokens
50333
50334
  };
50334
50335
  } else if (ev.kind === "done") {
50336
+ sawDone = true;
50335
50337
  finishReason = ev.finishReason;
50336
50338
  suppressCompletion = ev.suppressCompletion === true;
50337
50339
  } else if (ev.kind === "meta") {
@@ -50453,6 +50455,15 @@ async function* runCompressLoop(upstream, ctx, requestBody, requestOptions, adap
50453
50455
  }
50454
50456
  const reRequest = proxyResults.length > 0 && realCalls === 0;
50455
50457
  if (!reRequest) {
50458
+ if (!sawDone) {
50459
+ const partialText = assistantText.length;
50460
+ const partialReasoning = assistantReasoning.length;
50461
+ const msg = `upstream stream truncated (no completion event; round ${round}, ${partialText} text chars + ${partialReasoning} reasoning chars received)`;
50462
+ ctx.log(`[acp-loop] round ${round}: ${msg}`);
50463
+ log("error", `[acp-loop] ${msg}`);
50464
+ yield adapter.emitError(msg);
50465
+ return;
50466
+ }
50456
50467
  if (!suppressCompletion) {
50457
50468
  yield adapter.emitCompletion({ finishReason, usage });
50458
50469
  }
@@ -50534,7 +50545,91 @@ async function* runCompressLoop(upstream, ctx, requestBody, requestOptions, adap
50534
50545
  }
50535
50546
  }
50536
50547
 
50548
+ // src/util.ts
50549
+ import { createHash as createHash4 } from "crypto";
50550
+ function hashId2(s3) {
50551
+ return createHash4("sha256").update(s3, "utf8").digest("hex").slice(0, 16);
50552
+ }
50553
+ function isLoopbackAddress(addr) {
50554
+ return !!addr && (addr.startsWith("127.") || addr === "::1" || addr.startsWith("::ffff:127."));
50555
+ }
50556
+ function usageTotals(protocol, usage) {
50557
+ const num3 = (v2) => typeof v2 === "number" && Number.isFinite(v2) ? v2 : void 0;
50558
+ if (protocol === "anthropic") {
50559
+ const fresh = num3(usage["input_tokens"]);
50560
+ const read = num3(usage["cache_read_input_tokens"]);
50561
+ const creation = num3(usage["cache_creation_input_tokens"]);
50562
+ const any = fresh !== void 0 || read !== void 0 || creation !== void 0;
50563
+ return {
50564
+ total: any ? (fresh ?? 0) + (read ?? 0) + (creation ?? 0) : void 0,
50565
+ cached: read
50566
+ };
50567
+ }
50568
+ if (protocol === "openai") {
50569
+ return {
50570
+ total: num3(usage["prompt_tokens"]),
50571
+ cached: num3(usage["prompt_tokens_details"]?.["cached_tokens"])
50572
+ };
50573
+ }
50574
+ return {
50575
+ total: num3(usage["input_tokens"]),
50576
+ cached: num3(usage["input_tokens_details"]?.["cached_tokens"])
50577
+ };
50578
+ }
50579
+ var CONTEXT_OVERFLOW_PATTERNS = [
50580
+ /context_length_exceeded/i,
50581
+ /context length exceeded/i,
50582
+ /maximum context length/i,
50583
+ /max context length/i,
50584
+ /maximum context size/i,
50585
+ /exceeds the context window/i,
50586
+ /exceeded model token limit/i,
50587
+ /prompt is too long/i,
50588
+ /prompt_too_long/i,
50589
+ /prompt_is_too_long/i,
50590
+ /request_too_large/i,
50591
+ /token limit exceeded/i
50592
+ ];
50593
+ function toTokenNumber(s3) {
50594
+ const n = parseInt(s3.replace(/,/g, ""), 10);
50595
+ return Number.isFinite(n) && n >= 1e3 ? n : void 0;
50596
+ }
50597
+ function parseOverflowWindow(text) {
50598
+ let m2 = text.match(/>\s*(\d[\d,]*)\s*maximum/i);
50599
+ if (m2) return toTokenNumber(m2[1]);
50600
+ m2 = text.match(/maximum context length is (\d[\d,]*)/i) ?? text.match(/maximum context length of (\d[\d,]*)/i) ?? text.match(/maximum context size (?:is|of) (\d[\d,]*)/i) ?? text.match(/(?:maximum|max)\s+(?:context\s+)?length\s+(?:is\s+)?(\d[\d,]*)/i) ?? text.match(/limit of (\d[\d,]*)\s*token/i) ?? text.match(/(\d[\d,]*)\s*maximum\b/i);
50601
+ if (m2) return toTokenNumber(m2[1]);
50602
+ return void 0;
50603
+ }
50604
+ function inspectContextOverflow(status, bodyText) {
50605
+ const message = (bodyText ?? "").slice(0, 300);
50606
+ if (status !== 400 && status !== 413) return { isOverflow: false, message };
50607
+ if (!bodyText) return { isOverflow: false, message };
50608
+ const isOverflow = CONTEXT_OVERFLOW_PATTERNS.some((p2) => p2.test(bodyText));
50609
+ if (!isOverflow) return { isOverflow: false, message };
50610
+ return { isOverflow: true, window: parseOverflowWindow(bodyText), message };
50611
+ }
50612
+ function reserveOutputHeadroom(window2, maxOutput) {
50613
+ if (Number.isFinite(window2) && window2 > 0 && Number.isFinite(maxOutput) && maxOutput > 0 && maxOutput < window2) {
50614
+ return window2 - maxOutput;
50615
+ }
50616
+ return window2;
50617
+ }
50618
+ function shouldReserveOutputHeadroom(protocol) {
50619
+ return protocol !== "anthropic";
50620
+ }
50621
+
50537
50622
  // src/loop/adapter-responses.ts
50623
+ var RESPONSES_ITEM_ID_MAX = 64;
50624
+ function sanitizeResponsesInputIds(input) {
50625
+ if (!Array.isArray(input)) return;
50626
+ for (const item of input) {
50627
+ const rec = item;
50628
+ if (typeof rec?.id === "string" && rec.id.length > RESPONSES_ITEM_ID_MAX) {
50629
+ rec.id = `msg-fix-${hashId2(rec.id)}`;
50630
+ }
50631
+ }
50632
+ }
50538
50633
  function rewriteItemEvent(type, obj, mapped) {
50539
50634
  const item = { ...obj.item ?? {}, id: mapped.id };
50540
50635
  return Buffer.from(`event: ${type}
@@ -50766,7 +50861,7 @@ function createResponsesAdapter(textProtocol, projection) {
50766
50861
  yield { kind: "meta", chunk: rawBuf, firstRoundOnly: false };
50767
50862
  } else if (item?.type === "message" && round > 1 && !suppressTextLifecycle) {
50768
50863
  const origId = typeof item.id === "string" ? item.id : "";
50769
- const mapped = { id: `msg-proxy-${round}-${origId || outputIndex}`, index: outputIndex++ };
50864
+ const mapped = { id: `msg-proxy-${round}-${hashId2(origId || String(outputIndex))}`, index: outputIndex++ };
50770
50865
  if (origId) remapped.set(origId, mapped);
50771
50866
  yield { kind: "meta", chunk: rewriteItemEvent(type, obj, mapped), firstRoundOnly: false };
50772
50867
  } else if (item?.type !== "message" || !suppressTextLifecycle) {
@@ -51317,8 +51412,8 @@ function createOpenaiAdapter(requestBody, clientSystem) {
51317
51412
  emitCompletion(opts) {
51318
51413
  const finishReason = opts?.finishReason ?? "stop";
51319
51414
  const usage = opts?.usage ? {
51320
- prompt_tokens: opts.usage.inputTokens,
51321
- completion_tokens: opts.usage.outputTokens,
51415
+ prompt_tokens: opts.usage.inputTokens ?? 0,
51416
+ completion_tokens: opts.usage.outputTokens ?? 0,
51322
51417
  total_tokens: (opts.usage.inputTokens ?? 0) + (opts.usage.outputTokens ?? 0),
51323
51418
  ...typeof opts.usage.cachedTokens === "number" ? { prompt_tokens_details: { cached_tokens: opts.usage.cachedTokens } } : {}
51324
51419
  } : null;
@@ -52017,80 +52112,6 @@ data: ${JSON.stringify({ type: "message_stop" })}
52017
52112
  }
52018
52113
  }
52019
52114
 
52020
- // src/util.ts
52021
- import { createHash as createHash4 } from "crypto";
52022
- function hashId2(s3) {
52023
- return createHash4("sha256").update(s3, "utf8").digest("hex").slice(0, 16);
52024
- }
52025
- function isLoopbackAddress(addr) {
52026
- return !!addr && (addr.startsWith("127.") || addr === "::1" || addr.startsWith("::ffff:127."));
52027
- }
52028
- function usageTotals(protocol, usage) {
52029
- const num3 = (v2) => typeof v2 === "number" && Number.isFinite(v2) ? v2 : void 0;
52030
- if (protocol === "anthropic") {
52031
- const fresh = num3(usage["input_tokens"]);
52032
- const read = num3(usage["cache_read_input_tokens"]);
52033
- const creation = num3(usage["cache_creation_input_tokens"]);
52034
- const any = fresh !== void 0 || read !== void 0 || creation !== void 0;
52035
- return {
52036
- total: any ? (fresh ?? 0) + (read ?? 0) + (creation ?? 0) : void 0,
52037
- cached: read
52038
- };
52039
- }
52040
- if (protocol === "openai") {
52041
- return {
52042
- total: num3(usage["prompt_tokens"]),
52043
- cached: num3(usage["prompt_tokens_details"]?.["cached_tokens"])
52044
- };
52045
- }
52046
- return {
52047
- total: num3(usage["input_tokens"]),
52048
- cached: num3(usage["input_tokens_details"]?.["cached_tokens"])
52049
- };
52050
- }
52051
- var CONTEXT_OVERFLOW_PATTERNS = [
52052
- /context_length_exceeded/i,
52053
- /context length exceeded/i,
52054
- /maximum context length/i,
52055
- /max context length/i,
52056
- /maximum context size/i,
52057
- /exceeds the context window/i,
52058
- /exceeded model token limit/i,
52059
- /prompt is too long/i,
52060
- /prompt_too_long/i,
52061
- /prompt_is_too_long/i,
52062
- /request_too_large/i,
52063
- /token limit exceeded/i
52064
- ];
52065
- function toTokenNumber(s3) {
52066
- const n = parseInt(s3.replace(/,/g, ""), 10);
52067
- return Number.isFinite(n) && n >= 1e3 ? n : void 0;
52068
- }
52069
- function parseOverflowWindow(text) {
52070
- let m2 = text.match(/>\s*(\d[\d,]*)\s*maximum/i);
52071
- if (m2) return toTokenNumber(m2[1]);
52072
- m2 = text.match(/maximum context length is (\d[\d,]*)/i) ?? text.match(/maximum context length of (\d[\d,]*)/i) ?? text.match(/maximum context size (?:is|of) (\d[\d,]*)/i) ?? text.match(/(?:maximum|max)\s+(?:context\s+)?length\s+(?:is\s+)?(\d[\d,]*)/i) ?? text.match(/limit of (\d[\d,]*)\s*token/i) ?? text.match(/(\d[\d,]*)\s*maximum\b/i);
52073
- if (m2) return toTokenNumber(m2[1]);
52074
- return void 0;
52075
- }
52076
- function inspectContextOverflow(status, bodyText) {
52077
- const message = (bodyText ?? "").slice(0, 300);
52078
- if (status !== 400 && status !== 413) return { isOverflow: false, message };
52079
- if (!bodyText) return { isOverflow: false, message };
52080
- const isOverflow = CONTEXT_OVERFLOW_PATTERNS.some((p2) => p2.test(bodyText));
52081
- if (!isOverflow) return { isOverflow: false, message };
52082
- return { isOverflow: true, window: parseOverflowWindow(bodyText), message };
52083
- }
52084
- function reserveOutputHeadroom(window2, maxOutput) {
52085
- if (Number.isFinite(window2) && window2 > 0 && Number.isFinite(maxOutput) && maxOutput > 0 && maxOutput < window2) {
52086
- return window2 - maxOutput;
52087
- }
52088
- return window2;
52089
- }
52090
- function shouldReserveOutputHeadroom(protocol) {
52091
- return protocol !== "anthropic";
52092
- }
52093
-
52094
52115
  // src/session-id.ts
52095
52116
  function extractKey(headers) {
52096
52117
  const auth = headers["authorization"];
@@ -52846,6 +52867,33 @@ function resolveHermesHome(env) {
52846
52867
  const h = os.homedir();
52847
52868
  return nonEmpty2(env.HERMES_HOME) ? env.HERMES_HOME : path8.join(h, ".hermes");
52848
52869
  }
52870
+ function resolveDshHome(env) {
52871
+ const h = os.homedir();
52872
+ return nonEmpty2(env.DSH_HOME) ? env.DSH_HOME : path8.join(h, ".dsh");
52873
+ }
52874
+ function parseDshSettingsYaml(text) {
52875
+ const out = [];
52876
+ const seen = /* @__PURE__ */ new Set();
52877
+ for (const rawLine of text.split(/\r?\n/)) {
52878
+ const m2 = /^\s*(?:baseURL|baseUrl|base_url):\s*(\S+)(?:\s+#.*)?$/.exec(rawLine);
52879
+ if (!m2) continue;
52880
+ const url = m2[1].replace(/^["']|["']$/g, "");
52881
+ if (!/^https?:\/\//i.test(url)) continue;
52882
+ if (seen.has(url)) continue;
52883
+ seen.add(url);
52884
+ out.push(url);
52885
+ }
52886
+ return out;
52887
+ }
52888
+ function readDshConfig(dshHome) {
52889
+ let text;
52890
+ try {
52891
+ text = fs4.readFileSync(path8.join(dshHome, "settings.yaml"), "utf8");
52892
+ } catch {
52893
+ return { baseUrls: [] };
52894
+ }
52895
+ return { baseUrls: parseDshSettingsYaml(text) };
52896
+ }
52849
52897
  function readClaudeSettings(homeDir, cwd, env = process.env) {
52850
52898
  const files = [
52851
52899
  path8.join(homeDir, ".claude", "settings.json"),
@@ -53112,6 +53160,7 @@ function loadClientConfig(env, cwd) {
53112
53160
  config.omp = readOmpConfig(resolveOmpHome(env));
53113
53161
  config.opencode = readOpencodeConfig(resolveOpencodeConfigFile(env));
53114
53162
  config.hermes = readHermesConfig(resolveHermesHome(env));
53163
+ config.dsh = readDshConfig(resolveDshHome(env));
53115
53164
  return config;
53116
53165
  }
53117
53166
 
@@ -53217,17 +53266,23 @@ function isMitmHost(host, extraDomains = []) {
53217
53266
  return all.some((d) => h === d || h.endsWith("." + d));
53218
53267
  }
53219
53268
  function setupMitm(server, extraDomains = [], log2 = () => {
53220
- }, resolveProxyUrl) {
53269
+ }, resolveProxyUrl, allowRemoteClients = false) {
53221
53270
  ensureRootCA();
53222
53271
  server.on("connect", (req, clientSocket, head) => {
53223
- if (!isLoopbackAddress(clientSocket.remoteAddress)) {
53224
- log2(`CONNECT ${req.url} rejected: non-loopback client ${clientSocket.remoteAddress}`);
53272
+ const remote = !isLoopbackAddress(clientSocket.remoteAddress);
53273
+ if (remote && !allowRemoteClients) {
53274
+ log2(`CONNECT ${req.url} rejected: non-loopback client ${clientSocket.remoteAddress} (bind a non-loopback --host to allow remote clients)`);
53225
53275
  clientSocket.end("HTTP/1.1 403 Forbidden\r\n\r\n");
53226
53276
  return;
53227
53277
  }
53228
53278
  const { hostname, port } = parseHostPort(req.url ?? "");
53229
53279
  const targetPort = port || 443;
53230
53280
  if (!isMitmHost(hostname, extraDomains)) {
53281
+ if (remote) {
53282
+ log2(`CONNECT ${req.url} rejected: remote client ${clientSocket.remoteAddress} tunneling non-whitelisted host`);
53283
+ clientSocket.end("HTTP/1.1 403 Forbidden\r\n\r\n");
53284
+ return;
53285
+ }
53231
53286
  const proxyUrl = resolveProxyUrl?.(hostname);
53232
53287
  tunnelThrough(clientSocket, hostname, targetPort, head, log2, proxyUrl);
53233
53288
  return;
@@ -54147,10 +54202,12 @@ Content-Length: ${Buffer.byteLength(body)}\r
54147
54202
  );
54148
54203
  });
54149
54204
  if (opts.mitm.enabled) {
54150
- setupMitm(server, opts.mitm.domains, (msg) => log2("info", msg), (host) => resolveProxy(opts.routes, opts.proxy, `https://${host}`, opts.proxyFallback));
54205
+ const allowRemoteConnect = opts.host === "0.0.0.0" || opts.host === "::" || !isLoopbackAddress(opts.host);
54206
+ setupMitm(server, opts.mitm.domains, (msg) => log2("info", msg), (host) => resolveProxy(opts.routes, opts.proxy, `https://${host}`, opts.proxyFallback), allowRemoteConnect);
54151
54207
  }
54152
54208
  server.listen(opts.port, opts.host, () => {
54153
- const displayHost = opts.host === "0.0.0.0" ? "localhost" : opts.host;
54209
+ const nonLoopbackBind = opts.host === "0.0.0.0" || opts.host === "::" || !isLoopbackAddress(opts.host);
54210
+ const displayHost = nonLoopbackBind ? opts.host : opts.host === "0.0.0.0" ? "localhost" : opts.host;
54154
54211
  try {
54155
54212
  fs6.mkdirSync(stateDir(), { recursive: true });
54156
54213
  const originHost = opts.host === "0.0.0.0" || opts.host === "::" || opts.host === "localhost" ? "127.0.0.1" : opts.host.includes(":") && !opts.host.startsWith("[") ? `[${opts.host}]` : opts.host;
@@ -54163,6 +54220,12 @@ Content-Length: ${Buffer.byteLength(body)}\r
54163
54220
  "info",
54164
54221
  `acp-proxy listening on http://${displayHost}:${opts.port} \u2014 web UI: http://${displayHost}:${opts.port}/__bili/ \u2014 zero-config: prefix any baseURL with http://${displayHost}:${opts.port}/bili/` + (nOverrides ? ` \u2014 context overrides for ${nOverrides} upstream URL(s)` : "") + (opts.mitm.enabled ? ` \u2014 MITM proxy on (whitelist)${opts.mitm.domains.length ? ` +${opts.mitm.domains.join(",")}` : ""}` : "")
54165
54222
  );
54223
+ if (nonLoopbackBind) {
54224
+ log2(
54225
+ "warn",
54226
+ `[security] bound to ${opts.host} \u2014 proxy endpoints (/bili/, CONNECT for whitelisted model hosts) are reachable from the network with NO authentication; /__bili/ management endpoints stay loopback-only. Restrict access with a firewall on untrusted networks. Remote agents: point baseURL at http://<this-host>:${opts.port}/bili/`
54227
+ );
54228
+ }
54166
54229
  if (opts.debug) {
54167
54230
  log2("info", `[debug] build features: raw-HTTP-capture(on) | remote_compaction_v2-strip(on) | cert-MITM-launcher(on) | strip-acp-summary(on) \u2014 seeing this line confirms the launcher build (not registry 0.1.34)`);
54168
54231
  }
@@ -54684,6 +54747,7 @@ function prepareResponses(parsed, req, opts, core, config, prompts, log2, sessio
54684
54747
  let responsesProjection;
54685
54748
  let rebuiltInput = parsed.input;
54686
54749
  let toolsOut = parsed.tools;
54750
+ sanitizeResponsesInputIds(parsed.input);
54687
54751
  const shouldInject = opts.compress.injectTool;
54688
54752
  const injectTools = shouldInject && !pluginMode;
54689
54753
  const responsesTextProtocol = FORCE_TEXT_PROTOCOL || resolveCompressProtocol(opts.routes, session.meta.upstreamOrigin) === "marker";
@@ -59207,7 +59271,7 @@ function selfDistFile(name) {
59207
59271
  }
59208
59272
  var LAUNCHER_DEFAULT_HOST = "127.0.0.1";
59209
59273
  var LAUNCHER_DEFAULT_PORT = 8787;
59210
- var LAUNCH_CLIENTS = ["pi", "codex", "claude", "omp", "opencode", "hermes", "pi-test"];
59274
+ var LAUNCH_CLIENTS = ["pi", "codex", "claude", "omp", "opencode", "hermes", "dsh", "pi-test"];
59211
59275
  var HEALTH_PATH = "/__bili/health";
59212
59276
  var HEALTH_POLL_INTERVAL_MS = 200;
59213
59277
  var SPAWN_WAIT_MS = 2e4;
@@ -59316,6 +59380,22 @@ function discoverRoutes(client, config) {
59316
59380
  } catch {
59317
59381
  }
59318
59382
  }
59383
+ } else if (client === "dsh") {
59384
+ const dshSeen = /* @__PURE__ */ new Set();
59385
+ let anon = 0;
59386
+ for (const raw of config.dsh?.baseUrls ?? []) {
59387
+ const real = unwrapUpstream2(raw);
59388
+ try {
59389
+ const url = new URL(real);
59390
+ if (url.protocol !== "http:" && url.protocol !== "https:") continue;
59391
+ if (dshSeen.has(real)) continue;
59392
+ dshSeen.add(real);
59393
+ anon += 1;
59394
+ rewriteKeys.add(`dsh-${anon}`);
59395
+ httpRewrites.push({ key: `dsh-${anon}`, realUpstream: real });
59396
+ } catch {
59397
+ }
59398
+ }
59319
59399
  } else {
59320
59400
  for (const [name, prov] of Object.entries(config.codex?.providers ?? {})) {
59321
59401
  classify(prov.baseUrl, `model_providers.${name}.base_url`);
@@ -59356,7 +59436,7 @@ function launcherDirectUrl(env) {
59356
59436
  return env.BILI_LAUNCHER_DIRECT === "1";
59357
59437
  }
59358
59438
  function launcherInjectMcp(env, base) {
59359
- return base !== "pi" && base !== "omp" && base !== "opencode" && base !== "hermes" && env.BILI_LAUNCHER_PLUGIN === "1";
59439
+ return base !== "pi" && base !== "omp" && base !== "opencode" && base !== "hermes" && base !== "dsh" && env.BILI_LAUNCHER_PLUGIN === "1";
59360
59440
  }
59361
59441
  function buildMcpConfig(origin) {
59362
59442
  const script = selfDistFile("mcp.js");
@@ -59705,6 +59785,34 @@ function prepareHermesHome(hermesHome, origin, rewrites) {
59705
59785
  writeOverlayFileAtomic(overlay, "config.yaml", lines.join(eol));
59706
59786
  return overlay;
59707
59787
  }
59788
+ function prepareDshHome(dshHome, origin, rewrites) {
59789
+ const cfgPath = path13.join(dshHome, "settings.yaml");
59790
+ let txt;
59791
+ try {
59792
+ txt = fs10.readFileSync(cfgPath, "utf8");
59793
+ } catch {
59794
+ return void 0;
59795
+ }
59796
+ const wrapSet = new Set(rewrites.map((r) => r.realUpstream));
59797
+ const eol = txt.includes("\r\n") ? "\r\n" : "\n";
59798
+ const lines = txt.split(/\r?\n/);
59799
+ let changed = false;
59800
+ for (let i = 0; i < lines.length; i++) {
59801
+ const m2 = /^(\s*(?:baseURL|baseUrl|base_url):\s*)(\S+)(\s+#.*)?$/.exec(lines[i]);
59802
+ if (!m2) continue;
59803
+ const rawUrl = m2[2].replace(/^["']|["']$/g, "");
59804
+ if (!/^https?:\/\//i.test(rawUrl)) continue;
59805
+ const real = unwrapUpstream2(rawUrl);
59806
+ if (!wrapSet.has(real)) continue;
59807
+ lines[i] = `${m2[1]}${wrapUpstream(origin, real)}${m2[3] ?? ""}`;
59808
+ changed = true;
59809
+ }
59810
+ if (!changed) return void 0;
59811
+ const overlay = `${dshHome}-bili`;
59812
+ if (!refreshOverlayHome(dshHome, overlay, "settings.yaml")) return void 0;
59813
+ writeOverlayFileAtomic(overlay, "settings.yaml", lines.join(eol));
59814
+ return overlay;
59815
+ }
59708
59816
  function prepareOpencodeHttpRewrite(configFile2, origin, httpRewrites, httpsRewrites, pluginPath) {
59709
59817
  if (httpRewrites.length === 0 && httpsRewrites.length === 0 && !pluginPath) return void 0;
59710
59818
  let root = {};
@@ -59944,6 +60052,7 @@ async function runLaunch(params, deps = {}) {
59944
60052
  let ompOverlayHome;
59945
60053
  let opencodeTmpFile;
59946
60054
  let hermesOverlayHome;
60055
+ let dshOverlayHome;
59947
60056
  const tmpFiles = [];
59948
60057
  const directUrl = launcherDirectUrl(process.env);
59949
60058
  if (directUrl) {
@@ -59990,6 +60099,21 @@ async function runLaunch(params, deps = {}) {
59990
60099
  routes.httpRewrites.length === 0 ? "bili: no hermes providers found in ~/.hermes/config.yaml \u2014 traffic will NOT go through the proxy (configure a provider first)." : "bili: hermes config.yaml could not be rewritten (unreadable or no matching provider endpoints) \u2014 traffic will NOT go through the proxy."
59991
60100
  );
59992
60101
  }
60102
+ } else if (base === "dsh") {
60103
+ env = { ...process.env, BILLION_CONTEXT_PROXY: origin };
60104
+ env.DEEPSEEK_BASE_URL = wrapUpstream(origin, "https://api.deepseek.com");
60105
+ dshOverlayHome = prepareDshHome(resolveDshHome(process.env), origin, routes.httpRewrites);
60106
+ if (dshOverlayHome) {
60107
+ env.DSH_HOME = dshOverlayHome;
60108
+ } else if (routes.httpRewrites.length > 0) {
60109
+ console.error(
60110
+ "bili: dsh settings.yaml could not be rewritten (unreadable or no matching endpoints) \u2014 custom providers will NOT go through the proxy; the built-in deepseek route still does."
60111
+ );
60112
+ } else {
60113
+ console.error(
60114
+ "bili: no custom providers found in ~/.dsh/settings.yaml \u2014 proxying the built-in deepseek route via DEEPSEEK_BASE_URL only."
60115
+ );
60116
+ }
59993
60117
  } else if (base === "codex") {
59994
60118
  const codexConversationId = injectMcp ? randomUUID2() : void 0;
59995
60119
  if (directUrl) {
@@ -60257,6 +60381,7 @@ Usage:
60257
60381
  bili omp [opts --] [args] start a proxy + launch omp against it (cert-MITM)
60258
60382
  bili opencode [opts --] [args] start a proxy + launch opencode against it (cert-MITM)
60259
60383
  bili hermes [opts --] [args] start a proxy + launch hermes-agent against it (/bili/ rewrite)
60384
+ bili dsh [opts --] [args] start a proxy + launch deepseek-harness against it (/bili/ rewrite)
60260
60385
  bili test pi non-polluting pi smoke test through the proxy
60261
60386
  bili export [session] [--full] list sessions / export one as a Markdown handoff
60262
60387
  (--full includes original messages; --output FILE)
@@ -60271,7 +60396,7 @@ Usage:
60271
60396
  bili --version print version
60272
60397
  bili --help show this help
60273
60398
 
60274
- Launcher (bili pi / bili codex / bili claude / bili omp / bili opencode / bili hermes):
60399
+ Launcher (bili pi / bili codex / bili claude / bili omp / bili opencode / bili hermes / bili dsh):
60275
60400
  Brings up a proxy on an independent port (a fresh instance every launch), then runs the client pointed at it via HTTPS_PROXY + the proxy's
60276
60401
  MITM CA \u2014 no config-file edits. Discovered HTTPS upstream domains are
60277
60402
  auto-whitelisted for MITM so the proxy TLS-terminates exactly the hosts the
@@ -60284,6 +60409,7 @@ Launcher (bili pi / bili codex / bili claude / bili omp / bili opencode / bili h
60284
60409
  bili claude # launch claude through the proxy
60285
60410
  bili omp # launch omp through the proxy (pi-based; /bili/ rewrite)
60286
60411
  bili hermes # launch hermes-agent through the proxy (/bili/ rewrite of ~/.hermes/config.yaml)
60412
+ bili dsh --profile web "task" # launch deepseek-harness through the proxy (/bili/ rewrite of ~/.dsh/settings.yaml)
60287
60413
  bili test pi # quick end-to-end check of the pi path
60288
60414
  bili pi --mitm-domain api.foo.com # add a domain to the MITM whitelist
60289
60415