billion-context 0.1.68 → 0.1.69-pr.444.270

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
@@ -50,6 +50,67 @@ Agent (Claude Code / Codex / Cursor / Aider ...)
50
50
 
51
51
  The proxy injects four context-management tools (`compress`, `decompress`, `search_context`, `acp_status`) into the conversation. The model calls `compress` when the conversation grows, and the proxy executes it server-side — the compressed ranges are folded into the conversation history before the next turn.
52
52
 
53
+ ### Two compression modes — who executes `compress`
54
+
55
+ The proxy runs in one of two modes, and **the mode decides who executes
56
+ `compress`, which in turn decides how the summary travels to the model** (the
57
+ "carrier"). This distinction is the root of #377.
58
+
59
+ | | **Launcher / plugin mode** (`bili pi`, `bili codex`, …) | **Proxy mode** (plain client → `/bili/`) |
60
+ |---|---|---|
61
+ | Client | ACP-native agent with the bili extension (pi/omp) | Any OpenAI/Anthropic client, no extension |
62
+ | Who executes `compress` | **The agent** (pi runs it locally) | **The proxy** (server-side compress loop) |
63
+ | `compress` tool call in the re-sent history? | Yes — part of the agent's own conversation | No — ephemeral proxy-loop traffic |
64
+ | Preflight blocks (no tool call)? | No — overflow forces the model to call `compress` | Yes — `src/preflight.ts` compresses behind the client's back |
65
+ | **Summary carrier on the wire** | **the `compress` tool call** | **an `acp_summary` user message** |
66
+ | System messages on the wire | always exactly 1 (client + prompt) | always exactly 1 (client + prompt) — summaries ride on user messages |
67
+ | SGLang "single system" 400 (#377) | cannot happen | cannot happen (summaries are user messages, not system) |
68
+ | Proxy-injected tools + nudge | none (agent owns compression) | the 4 context tools + nudge (when enabled) |
69
+
70
+ **Why the carriers differ.** In plugin mode the agent owns compression: the
71
+ `compress` call + result live in the agent's own history and are re-sent every
72
+ turn, so the summary rides on the tool call and the agent's view never renders
73
+ the kernel's `acp_summary` fallback (`billion-context-pi` `src/messages.ts`
74
+ skips `acp_summary_*`). In proxy mode the client is not ACP-native, so the
75
+ proxy executes `compress` server-side; the tool call never enters the client's
76
+ history, and preflight blocks have no tool call at all — so the kernel's
77
+ `acp_summary` message is the only carrier. The kernel renders it as role
78
+ `system`, but strict OpenAI-compatible backends (SGLang) require exactly one
79
+ system message at index 0, so `systemToUser` (`src/util.ts`) re-voices it as a
80
+ `user` message, leaving it at its anchor position. This keeps the head system
81
+ message (the prefix-cache anchor) byte-stable across compress turns, so a new
82
+ block does not invalidate the whole-conversation prefix.
83
+
84
+ **Why `user`, not `system` or a forged tool call.** A mid-stream `system`
85
+ message is what SGLang rejects (#377). A forged `compress` tool call would be
86
+ the "pure" carrier, but in proxy mode it requires fabricating an
87
+ assistant `tool_calls` + `user` `tool_result` pair by id, declaring the tool in
88
+ the request, and handling preflight blocks that have no authentic call — far
89
+ more invasive than re-voicing a standalone note. A `user` message is allowed
90
+ anywhere in the conversation, so it is the minimal change that satisfies both
91
+ SGLang's one-system rule and prefix-cache stability. The accepted trade-off:
92
+ a summary is a stand-in for the folded history, and re-voicing it as a user
93
+ turn is a semantic mismatch the model tolerates (it is clearly marked
94
+ `[Compressed conversation section]`).
95
+
96
+ **Do the two modes coexist?**
97
+
98
+ - **Same proxy instance: yes, by design.** One proxy serves plugin and plain
99
+ clients at once; `pluginMode` is decided per request (`x-bili-plugin` header)
100
+ and bound per session (`session.metadata.pluginAgent`). The launcher reuses a
101
+ running proxy.
102
+ - **Same session: the mode is sticky.** A session created in plugin mode stays
103
+ plugin mode (metadata inheritance); a plain session can only be *upgraded* to
104
+ plugin mode if a plugin request arrives with a matching conversation id (the
105
+ header outranks) — and never downgraded. In practice a plain→plugin upgrade
106
+ requires the plugin client's conversation id to match an existing plain
107
+ session id, which doesn't happen (each client generates its own id).
108
+ - **Cross-mode block hazard: theoretical only.** It would require the same
109
+ conversation id to span a mode switch. plugin→proxy is safe (the tool call is
110
+ in the shared history); proxy→plugin could orphan proxy-created block
111
+ summaries (their tool call isn't in the agent's history and the agent's view
112
+ skips `acp_summary`) — but that needs the id match above, which doesn't occur.
113
+
53
114
  ## Install
54
115
 
55
116
  ```bash
package/dist/index.js CHANGED
@@ -19050,7 +19050,7 @@ var require_response = __commonJS({
19050
19050
  var assert = __require("assert");
19051
19051
  var { isomorphicEncode, serializeJavascriptValueToJSONString } = require_infra();
19052
19052
  var textEncoder = new TextEncoder("utf-8");
19053
- var Response = class _Response {
19053
+ var Response2 = class _Response {
19054
19054
  /** @type {Headers} */
19055
19055
  #headers;
19056
19056
  #state;
@@ -19222,13 +19222,13 @@ var require_response = __commonJS({
19222
19222
  response.#state = newState;
19223
19223
  }
19224
19224
  };
19225
- var { getResponseHeaders, setResponseHeaders, getResponseState, setResponseState } = Response;
19226
- Reflect.deleteProperty(Response, "getResponseHeaders");
19227
- Reflect.deleteProperty(Response, "setResponseHeaders");
19228
- Reflect.deleteProperty(Response, "getResponseState");
19229
- Reflect.deleteProperty(Response, "setResponseState");
19230
- mixinBody(Response, getResponseState);
19231
- Object.defineProperties(Response.prototype, {
19225
+ var { getResponseHeaders, setResponseHeaders, getResponseState, setResponseState } = Response2;
19226
+ Reflect.deleteProperty(Response2, "getResponseHeaders");
19227
+ Reflect.deleteProperty(Response2, "setResponseHeaders");
19228
+ Reflect.deleteProperty(Response2, "getResponseState");
19229
+ Reflect.deleteProperty(Response2, "setResponseState");
19230
+ mixinBody(Response2, getResponseState);
19231
+ Object.defineProperties(Response2.prototype, {
19232
19232
  type: kEnumerableProperty,
19233
19233
  url: kEnumerableProperty,
19234
19234
  status: kEnumerableProperty,
@@ -19244,7 +19244,7 @@ var require_response = __commonJS({
19244
19244
  configurable: true
19245
19245
  }
19246
19246
  });
19247
- Object.defineProperties(Response, {
19247
+ Object.defineProperties(Response2, {
19248
19248
  json: kEnumerableProperty,
19249
19249
  redirect: kEnumerableProperty,
19250
19250
  error: kEnumerableProperty
@@ -19377,7 +19377,7 @@ var require_response = __commonJS({
19377
19377
  }
19378
19378
  }
19379
19379
  function fromInnerResponse(innerResponse, guard) {
19380
- const response = new Response(kConstruct);
19380
+ const response = new Response2(kConstruct);
19381
19381
  setResponseState(response, innerResponse);
19382
19382
  const headers = new Headers(kConstruct);
19383
19383
  setResponseHeaders(response, headers);
@@ -19431,14 +19431,14 @@ var require_response = __commonJS({
19431
19431
  converter: webidl.converters.HeadersInit
19432
19432
  }
19433
19433
  ]);
19434
- webidl.is.Response = webidl.util.MakeTypeAssertion(Response);
19434
+ webidl.is.Response = webidl.util.MakeTypeAssertion(Response2);
19435
19435
  module.exports = {
19436
19436
  isNetworkError,
19437
19437
  makeNetworkError,
19438
19438
  makeResponse,
19439
19439
  makeAppropriateNetworkError,
19440
19440
  filterResponse,
19441
- Response,
19441
+ Response: Response2,
19442
19442
  cloneResponse,
19443
19443
  fromInnerResponse,
19444
19444
  getResponseState
@@ -48010,7 +48010,11 @@ var MAX_REQUEST_BYTES = 100 * 1024 * 1024;
48010
48010
  var UPSTREAM_TIMEOUT_MS = 10 * 60 * 1e3;
48011
48011
  async function fetchWithTimeout(url, opts, timeoutMs = UPSTREAM_TIMEOUT_MS, externalSignal) {
48012
48012
  const controller = new AbortController();
48013
- const timer2 = setTimeout(() => controller.abort(), timeoutMs);
48013
+ let timer2 = setTimeout(() => controller.abort(), timeoutMs);
48014
+ const rearm = () => {
48015
+ clearTimeout(timer2);
48016
+ timer2 = setTimeout(() => controller.abort(), timeoutMs);
48017
+ };
48014
48018
  let onExternalAbort = null;
48015
48019
  if (externalSignal) {
48016
48020
  if (externalSignal.aborted) controller.abort();
@@ -48019,22 +48023,54 @@ async function fetchWithTimeout(url, opts, timeoutMs = UPSTREAM_TIMEOUT_MS, exte
48019
48023
  externalSignal.addEventListener("abort", onExternalAbort, { once: true });
48020
48024
  }
48021
48025
  }
48026
+ const cleanup = () => {
48027
+ clearTimeout(timer2);
48028
+ if (onExternalAbort && externalSignal) externalSignal.removeEventListener("abort", onExternalAbort);
48029
+ };
48022
48030
  try {
48023
48031
  const finalOpts = { ...opts, signal: controller.signal };
48024
- const response = await fetch(url, finalOpts);
48025
- return {
48026
- response,
48027
- clearTimer: () => {
48028
- clearTimeout(timer2);
48029
- if (onExternalAbort && externalSignal) externalSignal.removeEventListener("abort", onExternalAbort);
48030
- }
48031
- };
48032
+ const raw = await fetch(url, finalOpts);
48033
+ if (raw.body) {
48034
+ const wrapped = armIdleBody(raw.body, rearm);
48035
+ return {
48036
+ response: new Response(wrapped, {
48037
+ status: raw.status,
48038
+ statusText: raw.statusText,
48039
+ headers: raw.headers
48040
+ }),
48041
+ clearTimer: cleanup
48042
+ };
48043
+ }
48044
+ return { response: raw, clearTimer: cleanup };
48032
48045
  } catch (e) {
48033
- clearTimeout(timer2);
48034
- if (onExternalAbort && externalSignal) externalSignal.removeEventListener("abort", onExternalAbort);
48046
+ cleanup();
48035
48047
  throw e;
48036
48048
  }
48037
48049
  }
48050
+ function armIdleBody(body, rearm) {
48051
+ const reader = body.getReader();
48052
+ return new ReadableStream({
48053
+ async pull(controller) {
48054
+ try {
48055
+ const result = await reader.read();
48056
+ if (result.done) {
48057
+ controller.close();
48058
+ return;
48059
+ }
48060
+ rearm();
48061
+ controller.enqueue(result.value);
48062
+ } catch (e) {
48063
+ controller.error(e);
48064
+ }
48065
+ },
48066
+ async cancel(reason) {
48067
+ try {
48068
+ await reader.cancel(reason);
48069
+ } catch {
48070
+ }
48071
+ }
48072
+ });
48073
+ }
48038
48074
  var UpstreamHttpError = class extends Error {
48039
48075
  status;
48040
48076
  body;
@@ -51718,25 +51754,18 @@ function reserveOutputHeadroom(window2, maxOutput) {
51718
51754
  }
51719
51755
  return window2;
51720
51756
  }
51721
- function hoistMidSystemMessages(messages) {
51722
- let lead = 0;
51723
- while (lead < messages.length && (messages[lead].role === "system" || messages[lead].role === "developer")) lead++;
51724
- if (lead === messages.length) return messages;
51725
- let midCount = 0;
51726
- for (let i = lead; i < messages.length; i++) {
51727
- const r = messages[i].role;
51728
- if (r === "system" || r === "developer") midCount++;
51729
- }
51730
- if (midCount === 0) return messages;
51731
- const head = messages.slice(0, lead);
51732
- const mid = [];
51733
- const rest = [];
51734
- for (let i = lead; i < messages.length; i++) {
51735
- const r = messages[i].role;
51736
- if (r === "system" || r === "developer") mid.push(messages[i]);
51737
- else rest.push(messages[i]);
51738
- }
51739
- return [...head, ...mid, ...rest];
51757
+ function systemToUser(messages) {
51758
+ let hasSys = false;
51759
+ for (const m2 of messages) {
51760
+ if (m2.role === "system" || m2.role === "developer") {
51761
+ hasSys = true;
51762
+ break;
51763
+ }
51764
+ }
51765
+ if (!hasSys) return messages;
51766
+ return messages.map(
51767
+ (m2) => m2.role === "system" || m2.role === "developer" ? { ...m2, role: "user" } : m2
51768
+ );
51740
51769
  }
51741
51770
  function shouldReserveOutputHeadroom(protocol) {
51742
51771
  return protocol !== "anthropic";
@@ -51966,6 +51995,20 @@ function createResponsesAdapter(textProtocol, projection) {
51966
51995
  let responseObj = null;
51967
51996
  let terminalRaw = null;
51968
51997
  let terminalKind = null;
51998
+ let createdForwarded = false;
51999
+ let createdRespId = null;
52000
+ const ensureCreated = () => {
52001
+ if (createdForwarded) return null;
52002
+ createdForwarded = true;
52003
+ if (!createdRespId) createdRespId = `resp-proxy-${Date.now()}`;
52004
+ return Buffer.from(
52005
+ `event: response.created
52006
+ data: ${JSON.stringify({ type: "response.created", response: { id: createdRespId, status: "in_progress", output: [] } })}
52007
+
52008
+ `,
52009
+ "utf8"
52010
+ );
52011
+ };
51969
52012
  return {
51970
52013
  buildRequest(coreMessages, systemPrompt, requestBody) {
51971
52014
  const customToolCallIds = /* @__PURE__ */ new Set();
@@ -52020,6 +52063,11 @@ function createResponsesAdapter(textProtocol, projection) {
52020
52063
  outputIndex = Math.max(outputIndex, obj.output_index + 1);
52021
52064
  }
52022
52065
  if (type === "response.created" || type === "response.in_progress") {
52066
+ if (type === "response.created") {
52067
+ const resp = obj.response;
52068
+ if (resp && typeof resp.id === "string") createdRespId = resp.id;
52069
+ if (round === 1) createdForwarded = true;
52070
+ }
52023
52071
  yield { kind: "meta", chunk: rawBuf, firstRoundOnly: true };
52024
52072
  } else if (type === "response.output_item.added") {
52025
52073
  const item = obj.item;
@@ -52176,18 +52224,22 @@ function createResponsesAdapter(textProtocol, projection) {
52176
52224
  return terminalRaw;
52177
52225
  }
52178
52226
  if (!responseObj && opts?.finishReason === "failed") {
52227
+ const parts = [];
52228
+ const created = ensureCreated();
52229
+ if (created) parts.push(created);
52179
52230
  const failed = {
52180
- id: `resp-error-${Date.now()}`,
52231
+ id: createdRespId ?? `resp-error-${Date.now()}`,
52181
52232
  status: "failed",
52182
52233
  error: { code: "server_error", message: "upstream returned no response" }
52183
52234
  };
52184
- return Buffer.from(
52235
+ parts.push(Buffer.from(
52185
52236
  `event: response.failed
52186
52237
  data: ${JSON.stringify({ type: "response.failed", response: failed })}
52187
52238
 
52188
52239
  `,
52189
52240
  "utf8"
52190
- );
52241
+ ));
52242
+ return Buffer.concat(parts);
52191
52243
  }
52192
52244
  let resp = responseObj ? { ...responseObj } : { id: `resp-proxy-${Date.now()}`, status: "completed", output: [] };
52193
52245
  if (opts?.usage) {
@@ -52207,18 +52259,22 @@ data: ${JSON.stringify({ type: "response.failed", response: failed })}
52207
52259
  return buildCompleted(resp);
52208
52260
  },
52209
52261
  emitError(message) {
52262
+ const parts = [];
52263
+ const created = ensureCreated();
52264
+ if (created) parts.push(created);
52210
52265
  const resp = {
52211
- id: `resp-error-${Date.now()}`,
52266
+ id: createdRespId ?? `resp-error-${Date.now()}`,
52212
52267
  status: "failed",
52213
52268
  error: { code: "server_error", message }
52214
52269
  };
52215
- return Buffer.from(
52270
+ parts.push(Buffer.from(
52216
52271
  `event: response.failed
52217
52272
  data: ${JSON.stringify({ type: "response.failed", response: resp })}
52218
52273
 
52219
52274
  `,
52220
52275
  "utf8"
52221
- );
52276
+ ));
52277
+ return Buffer.concat(parts);
52222
52278
  },
52223
52279
  extractTextTriggers(text) {
52224
52280
  const calls = [];
@@ -52330,6 +52386,20 @@ function rewriteContentChunk(parsed, content) {
52330
52386
 
52331
52387
  `, "utf8");
52332
52388
  }
52389
+ function stripFinishReasonChunk(buf) {
52390
+ const m2 = /^data: (.*)\n\n$/s.exec(buf.toString("utf8"));
52391
+ if (!m2) return buf;
52392
+ try {
52393
+ const parsed = JSON.parse(m2[1]);
52394
+ if (!Array.isArray(parsed.choices)) return buf;
52395
+ parsed.choices = parsed.choices.map((c) => ({ ...c, finish_reason: null }));
52396
+ return Buffer.from(`data: ${JSON.stringify(parsed)}
52397
+
52398
+ `, "utf8");
52399
+ } catch {
52400
+ return buf;
52401
+ }
52402
+ }
52333
52403
  function createOpenaiAdapter(requestBody, clientSystem) {
52334
52404
  const model = requestBody.model ?? "unknown";
52335
52405
  let responseId = `chatcmpl-proxy-${Date.now()}`;
@@ -52399,7 +52469,7 @@ function createOpenaiAdapter(requestBody, clientSystem) {
52399
52469
  );
52400
52470
  return {
52401
52471
  buildRequest(coreMessages, systemPrompt, body) {
52402
- const messages = hoistMidSystemMessages(coreToOpenai(coreMessages));
52472
+ const messages = systemToUser(coreToOpenai(coreMessages));
52403
52473
  const withSys = injectOpenaiSystem(messages, [clientSystem, systemPrompt].filter((p2) => typeof p2 === "string" && p2.length > 0));
52404
52474
  return { ...body, messages: withSys };
52405
52475
  },
@@ -52526,6 +52596,7 @@ function createOpenaiAdapter(requestBody, clientSystem) {
52526
52596
  if (sawRealToolCall) {
52527
52597
  yield { kind: "meta", chunk: rawBuf };
52528
52598
  yield { kind: "done", finishReason, suppressCompletion: true };
52599
+ continue;
52529
52600
  } else {
52530
52601
  yield {
52531
52602
  kind: "done",
@@ -52535,7 +52606,7 @@ function createOpenaiAdapter(requestBody, clientSystem) {
52535
52606
  }
52536
52607
  if (!delta) continue;
52537
52608
  if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) {
52538
- yield { kind: "reasoning", delta: delta.reasoning_content, raw: rawBuf };
52609
+ yield { kind: "reasoning", delta: delta.reasoning_content, raw: finishReason ? stripFinishReasonChunk(rawBuf) : rawBuf };
52539
52610
  }
52540
52611
  if (delta.tool_calls) {
52541
52612
  const tcs = delta.tool_calls;
@@ -52570,10 +52641,10 @@ function createOpenaiAdapter(requestBody, clientSystem) {
52570
52641
  const clean = tagFilter.push(delta.content);
52571
52642
  if (clean.length > 0) {
52572
52643
  const raw = clean === delta.content ? rawBuf : rewriteContentChunk(parsed, clean);
52573
- yield { kind: "text", delta: clean, ...hasReasoning ? {} : { raw } };
52644
+ yield { kind: "text", delta: clean, ...hasReasoning ? {} : { raw: finishReason ? stripFinishReasonChunk(raw) : raw } };
52574
52645
  }
52575
52646
  } else if (!hasReasoning && (delta.role || Object.keys(delta).length === 0 && !finishReason)) {
52576
- yield { kind: "meta", chunk: rawBuf, firstRoundOnly: true };
52647
+ yield { kind: "meta", chunk: finishReason ? stripFinishReasonChunk(rawBuf) : rawBuf, firstRoundOnly: true };
52577
52648
  }
52578
52649
  }
52579
52650
  },
@@ -56593,7 +56664,7 @@ function prepareAnthropic(parsed, req, opts, core, config, prompts, log2, sessio
56593
56664
  if (injectTools) {
56594
56665
  toolsOut = injectTool(parsed.tools);
56595
56666
  }
56596
- if (opts.compress.injectNudge && turn.nudge?.shouldInject) {
56667
+ if (opts.compress.injectNudge && !pluginMode && turn.nudge?.shouldInject) {
56597
56668
  try {
56598
56669
  const rendered = renderNudgeText(turn.nudge, prompts);
56599
56670
  if (rendered.text) {
@@ -56645,7 +56716,7 @@ function prepareOpenai(parsed, req, opts, core, config, prompts, log2, session,
56645
56716
  log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit, parsed.model));
56646
56717
  processedMessages = stripKernelSummaries(turn.messages, turn.state);
56647
56718
  reapOrphanBlocks(session, msgs, deactivateBlock);
56648
- rebuiltMessages = hoistMidSystemMessages(coreToOpenai(processedMessages));
56719
+ rebuiltMessages = systemToUser(coreToOpenai(processedMessages));
56649
56720
  const sysParts = [];
56650
56721
  if (systemText) sysParts.push(systemText);
56651
56722
  if (shouldInject) sysParts.push(buildCompressSystemPrompt(prompts));
@@ -56653,7 +56724,7 @@ function prepareOpenai(parsed, req, opts, core, config, prompts, log2, session,
56653
56724
  if (injectTools) {
56654
56725
  toolsOut = injectOpenaiTool(parsed.tools);
56655
56726
  }
56656
- if (opts.compress.injectNudge && turn.nudge?.shouldInject && shouldInject) {
56727
+ if (opts.compress.injectNudge && !pluginMode && turn.nudge?.shouldInject && shouldInject) {
56657
56728
  try {
56658
56729
  const rendered = renderNudgeText(turn.nudge, prompts);
56659
56730
  if (rendered.text) {
@@ -56747,7 +56818,7 @@ function prepareResponses(parsed, req, opts, core, config, prompts, log2, sessio
56747
56818
  const devContent = [...projection.systemParts, ...forgedSummaries].join("\n\n---\n\n");
56748
56819
  rebuiltInput = injectResponsesDeveloperMessage(rebuiltInput, devContent);
56749
56820
  }
56750
- if (opts.compress.injectNudge && turn.nudge?.shouldInject && shouldInject && !isCompactionTrigger) {
56821
+ if (opts.compress.injectNudge && !pluginMode && turn.nudge?.shouldInject && shouldInject && !isCompactionTrigger) {
56751
56822
  try {
56752
56823
  const rendered = renderNudgeText(turn.nudge, prompts);
56753
56824
  if (rendered.text) {