billion-context 0.1.2 → 0.1.4
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 +31 -1
- package/dist/index.js +294 -148
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -28,14 +28,23 @@ function sessionsDir() {
|
|
|
28
28
|
if (env && env.length > 0) return path.resolve(env);
|
|
29
29
|
return path.join(dataDir(), "sessions");
|
|
30
30
|
}
|
|
31
|
+
function cacheDir() {
|
|
32
|
+
return path.join(xdg("XDG_CACHE_HOME", ".cache"), "billion-context");
|
|
33
|
+
}
|
|
34
|
+
function stateDir() {
|
|
35
|
+
return path.join(xdg("XDG_STATE_HOME", ".local/state"), "billion-context");
|
|
36
|
+
}
|
|
37
|
+
function defaultLogFile() {
|
|
38
|
+
return path.join(stateDir(), "bili.log");
|
|
39
|
+
}
|
|
31
40
|
|
|
32
41
|
// src/config.ts
|
|
33
|
-
function safeReadJson(
|
|
42
|
+
function safeReadJson(path6) {
|
|
34
43
|
try {
|
|
35
|
-
return JSON.parse(readFileSync(
|
|
44
|
+
return JSON.parse(readFileSync(path6, "utf8"));
|
|
36
45
|
} catch (e) {
|
|
37
46
|
if (e.code !== "ENOENT") {
|
|
38
|
-
console.error(`[acp-config] failed to parse ${
|
|
47
|
+
console.error(`[acp-config] failed to parse ${path6}: ${String(e)}`);
|
|
39
48
|
}
|
|
40
49
|
return void 0;
|
|
41
50
|
}
|
|
@@ -98,10 +107,6 @@ function loadOptions(env = process.env) {
|
|
|
98
107
|
}
|
|
99
108
|
}
|
|
100
109
|
const modelContextLimit = parseInt(env.ACP_MODEL_CONTEXT_LIMIT ?? `${fileConfig.modelContextLimit ?? 2e5}`, 10);
|
|
101
|
-
const enabled = (env.ACP_CONDENSE_ENABLED ?? (fileConfig.condense?.enabled === false ? "0" : "1")) !== "0";
|
|
102
|
-
const keepRecentToolResults = parseInt(env.ACP_KEEP_RECENT_TOOL_RESULTS ?? `${fileConfig.condense?.keepRecentToolResults ?? 6}`, 10);
|
|
103
|
-
const minCharsToCondense = parseInt(env.ACP_MIN_CHARS_TO_CONDENSE ?? `${fileConfig.condense?.minCharsToCondense ?? 1500}`, 10);
|
|
104
|
-
const maxKeptChars = parseInt(env.ACP_MAX_KEPT_CHARS ?? `${fileConfig.condense?.maxKeptChars ?? 400}`, 10);
|
|
105
110
|
return {
|
|
106
111
|
port: Number.isFinite(port) ? port : 8787,
|
|
107
112
|
host,
|
|
@@ -109,7 +114,6 @@ function loadOptions(env = process.env) {
|
|
|
109
114
|
routes,
|
|
110
115
|
modelContextLimit,
|
|
111
116
|
kernelConfig: defaultConfig(modelContextLimit),
|
|
112
|
-
condense: { enabled, keepRecentToolResults, minCharsToCondense, maxKeptChars },
|
|
113
117
|
compress: {
|
|
114
118
|
injectTool: (env.ACP_COMPRESS_TOOL ?? (fileConfig.compress?.injectTool === false ? "0" : "1")) !== "0",
|
|
115
119
|
injectNudge: (env.ACP_COMPRESS_NUDGE ?? (fileConfig.compress?.injectNudge === false ? "0" : "1")) !== "0"
|
|
@@ -118,7 +122,9 @@ function loadOptions(env = process.env) {
|
|
|
118
122
|
log: env.ACP_LOG !== "0" && fileConfig.log !== false,
|
|
119
123
|
debug: (env.ACP_DEBUG ?? (fileConfig.debug ? "1" : "0")) === "1",
|
|
120
124
|
dumpSse: env.ACP_DUMP_SSE || fileConfig.dumpSse || void 0,
|
|
121
|
-
passthrough: (env.ACP_PASSTHROUGH ?? (fileConfig.passthrough ? "1" : "0")) === "1"
|
|
125
|
+
passthrough: (env.ACP_PASSTHROUGH ?? (fileConfig.passthrough ? "1" : "0")) === "1",
|
|
126
|
+
autoUpdate: (env.ACP_AUTO_UPDATE ?? (fileConfig.autoUpdate === false ? "0" : "1")) !== "0",
|
|
127
|
+
logFile: env.ACP_LOG_FILE !== void 0 ? env.ACP_LOG_FILE || void 0 : fileConfig.logFile
|
|
122
128
|
};
|
|
123
129
|
}
|
|
124
130
|
function loadConfigFile() {
|
|
@@ -149,12 +155,12 @@ var MAX_REQUEST_BYTES = 100 * 1024 * 1024;
|
|
|
149
155
|
var UPSTREAM_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
150
156
|
async function fetchWithTimeout(url, init, timeoutMs = UPSTREAM_TIMEOUT_MS) {
|
|
151
157
|
const controller = new AbortController();
|
|
152
|
-
const
|
|
158
|
+
const timer2 = setTimeout(() => controller.abort(), timeoutMs);
|
|
153
159
|
try {
|
|
154
160
|
const response = await fetch(url, { ...init, signal: controller.signal });
|
|
155
|
-
return { response, clearTimer: () => clearTimeout(
|
|
161
|
+
return { response, clearTimer: () => clearTimeout(timer2) };
|
|
156
162
|
} catch (e) {
|
|
157
|
-
clearTimeout(
|
|
163
|
+
clearTimeout(timer2);
|
|
158
164
|
throw e;
|
|
159
165
|
}
|
|
160
166
|
}
|
|
@@ -187,7 +193,6 @@ var ClusterCounter = class {
|
|
|
187
193
|
};
|
|
188
194
|
|
|
189
195
|
// src/anthropic.ts
|
|
190
|
-
var CONDENSED_TAG = "[acp-proxy: condensed";
|
|
191
196
|
function extractSystem(system) {
|
|
192
197
|
if (!system) return "";
|
|
193
198
|
if (typeof system === "string") return system;
|
|
@@ -195,13 +200,14 @@ function extractSystem(system) {
|
|
|
195
200
|
}
|
|
196
201
|
function buildSystem(text, original) {
|
|
197
202
|
if (Array.isArray(original) && original.length > 0) {
|
|
198
|
-
const
|
|
199
|
-
return [{ type: "text", text, ...
|
|
203
|
+
const ccBlock = original.find((b) => b.cache_control);
|
|
204
|
+
return [{ type: "text", text, ...ccBlock ? { cache_control: ccBlock.cache_control } : {} }];
|
|
200
205
|
}
|
|
201
206
|
return text;
|
|
202
207
|
}
|
|
203
208
|
function anthropicToCore(body) {
|
|
204
209
|
const msgs = [];
|
|
210
|
+
const cacheControls = /* @__PURE__ */ new Map();
|
|
205
211
|
const clusters = new ClusterCounter();
|
|
206
212
|
for (const m of body.messages) {
|
|
207
213
|
const blocks = typeof m.content === "string" ? [{ type: "text", text: m.content }] : m.content;
|
|
@@ -209,7 +215,9 @@ function anthropicToCore(body) {
|
|
|
209
215
|
switch (b.type) {
|
|
210
216
|
case "text": {
|
|
211
217
|
const base = deriveMessageId(m.role, "text", b.text);
|
|
212
|
-
|
|
218
|
+
const id = clusters.next(base);
|
|
219
|
+
msgs.push({ id, role: m.role, contentType: "text", text: b.text });
|
|
220
|
+
if (b.cache_control) cacheControls.set(id, b.cache_control);
|
|
213
221
|
break;
|
|
214
222
|
}
|
|
215
223
|
case "tool_use": {
|
|
@@ -217,26 +225,30 @@ function anthropicToCore(body) {
|
|
|
217
225
|
toolCallId: b.id,
|
|
218
226
|
toolName: b.name
|
|
219
227
|
});
|
|
228
|
+
const id = clusters.next(base);
|
|
220
229
|
msgs.push({
|
|
221
|
-
id
|
|
230
|
+
id,
|
|
222
231
|
role: "assistant",
|
|
223
232
|
contentType: "tool-call",
|
|
224
233
|
toolName: b.name,
|
|
225
234
|
toolCallId: b.id,
|
|
226
235
|
text: safeStringify(b.input)
|
|
227
236
|
});
|
|
237
|
+
if (b.cache_control) cacheControls.set(id, b.cache_control);
|
|
228
238
|
break;
|
|
229
239
|
}
|
|
230
240
|
case "tool_result": {
|
|
231
241
|
const text = typeof b.content === "string" ? b.content : b.content.map((c) => c.text).join("\n");
|
|
232
242
|
const base = deriveMessageId("tool", "tool-result", text, { toolCallId: b.tool_use_id });
|
|
243
|
+
const id = clusters.next(base);
|
|
233
244
|
msgs.push({
|
|
234
|
-
id
|
|
245
|
+
id,
|
|
235
246
|
role: "tool",
|
|
236
247
|
contentType: "tool-result",
|
|
237
248
|
toolCallId: b.tool_use_id,
|
|
238
249
|
text
|
|
239
250
|
});
|
|
251
|
+
if (b.cache_control) cacheControls.set(id, b.cache_control);
|
|
240
252
|
break;
|
|
241
253
|
}
|
|
242
254
|
case "thinking": {
|
|
@@ -252,9 +264,9 @@ function anthropicToCore(body) {
|
|
|
252
264
|
}
|
|
253
265
|
}
|
|
254
266
|
}
|
|
255
|
-
return { msgs };
|
|
267
|
+
return { msgs, cacheControls };
|
|
256
268
|
}
|
|
257
|
-
function coreToAnthropic(messages) {
|
|
269
|
+
function coreToAnthropic(messages, cacheControls) {
|
|
258
270
|
const out = [];
|
|
259
271
|
let current = null;
|
|
260
272
|
const flush = () => {
|
|
@@ -263,6 +275,10 @@ function coreToAnthropic(messages) {
|
|
|
263
275
|
}
|
|
264
276
|
current = null;
|
|
265
277
|
};
|
|
278
|
+
const cc = (id) => {
|
|
279
|
+
const v = cacheControls?.get(id);
|
|
280
|
+
return v ? { cache_control: v } : {};
|
|
281
|
+
};
|
|
266
282
|
for (const m of messages) {
|
|
267
283
|
const target = m.role === "assistant" ? "assistant" : "user";
|
|
268
284
|
if (!current || current.role !== target) {
|
|
@@ -271,21 +287,23 @@ function coreToAnthropic(messages) {
|
|
|
271
287
|
}
|
|
272
288
|
switch (m.contentType) {
|
|
273
289
|
case "text":
|
|
274
|
-
current.blocks.push({ type: "text", text: m.text ?? "" });
|
|
290
|
+
current.blocks.push({ type: "text", text: m.text ?? "", ...cc(m.id) });
|
|
275
291
|
break;
|
|
276
292
|
case "tool-call":
|
|
277
293
|
current.blocks.push({
|
|
278
294
|
type: "tool_use",
|
|
279
295
|
id: m.toolCallId ?? `call_${m.id}`,
|
|
280
296
|
name: m.toolName ?? "unknown",
|
|
281
|
-
input: safeParse(m.text)
|
|
297
|
+
input: safeParse(m.text),
|
|
298
|
+
...cc(m.id)
|
|
282
299
|
});
|
|
283
300
|
break;
|
|
284
301
|
case "tool-result":
|
|
285
302
|
current.blocks.push({
|
|
286
303
|
type: "tool_result",
|
|
287
304
|
tool_use_id: m.toolCallId ?? "",
|
|
288
|
-
content: m.text ?? ""
|
|
305
|
+
content: m.text ?? "",
|
|
306
|
+
...cc(m.id)
|
|
289
307
|
});
|
|
290
308
|
break;
|
|
291
309
|
case "reasoning":
|
|
@@ -296,33 +314,6 @@ function coreToAnthropic(messages) {
|
|
|
296
314
|
flush();
|
|
297
315
|
return out;
|
|
298
316
|
}
|
|
299
|
-
function condenseOldToolResults(messages, opts) {
|
|
300
|
-
if (!opts.enabled) return { messages, condensedCount: 0, charsSaved: 0 };
|
|
301
|
-
const toolResultIndices = [];
|
|
302
|
-
for (let i = 0; i < messages.length; i++) {
|
|
303
|
-
const m = messages[i];
|
|
304
|
-
if (m && m.contentType === "tool-result") toolResultIndices.push(i);
|
|
305
|
-
}
|
|
306
|
-
if (toolResultIndices.length <= opts.keepRecent) {
|
|
307
|
-
return { messages, condensedCount: 0, charsSaved: 0 };
|
|
308
|
-
}
|
|
309
|
-
const toCondense = new Set(toolResultIndices.slice(0, toolResultIndices.length - opts.keepRecent));
|
|
310
|
-
let condensedCount = 0;
|
|
311
|
-
let charsSaved = 0;
|
|
312
|
-
const out = messages.map((m, i) => {
|
|
313
|
-
if (!toCondense.has(i)) return m;
|
|
314
|
-
const text = m.text ?? "";
|
|
315
|
-
if (text.length < opts.minChars) return m;
|
|
316
|
-
const head = text.slice(0, opts.maxKeptChars);
|
|
317
|
-
const stub = `${CONDENSED_TAG} ${text.length.toLocaleString()} chars]
|
|
318
|
-
${head}
|
|
319
|
-
[/acp-proxy]`;
|
|
320
|
-
charsSaved += text.length - stub.length;
|
|
321
|
-
condensedCount++;
|
|
322
|
-
return { ...m, text: stub };
|
|
323
|
-
});
|
|
324
|
-
return { messages: out, condensedCount, charsSaved };
|
|
325
|
-
}
|
|
326
317
|
function conversationSignalAnthropic(body, headerValue2) {
|
|
327
318
|
if (headerValue2 && headerValue2.trim()) return headerValue2.trim();
|
|
328
319
|
const firstUser = body.messages.find((m) => m.role === "user");
|
|
@@ -824,14 +815,14 @@ var SessionStore = class {
|
|
|
824
815
|
if (!this.enabled) return;
|
|
825
816
|
const existing = this.timers.get(session.id);
|
|
826
817
|
if (existing) clearTimeout(existing);
|
|
827
|
-
const
|
|
818
|
+
const timer2 = setTimeout(() => {
|
|
828
819
|
this.timers.delete(session.id);
|
|
829
820
|
void this.writeNow(session).catch((e) => {
|
|
830
821
|
this.log("error", `[persist] debounced write failed for ${session.id}: ${msg(e)}`);
|
|
831
822
|
});
|
|
832
823
|
}, this.debounceMs);
|
|
833
|
-
|
|
834
|
-
this.timers.set(session.id,
|
|
824
|
+
timer2.unref?.();
|
|
825
|
+
this.timers.set(session.id, timer2);
|
|
835
826
|
}
|
|
836
827
|
/** Asynchronously persist a session right now (skips the debounce). Throws
|
|
837
828
|
* on write failure so callers can react (e.g. avoid evicting). */
|
|
@@ -888,7 +879,7 @@ var SessionStore = class {
|
|
|
888
879
|
async flushAll(sessions2) {
|
|
889
880
|
if (!this.enabled) return;
|
|
890
881
|
const dirty = new Set(this.timers.keys());
|
|
891
|
-
for (const
|
|
882
|
+
for (const timer2 of this.timers.values()) clearTimeout(timer2);
|
|
892
883
|
this.timers.clear();
|
|
893
884
|
const pending = [];
|
|
894
885
|
for (const s of sessions2) {
|
|
@@ -907,7 +898,7 @@ var SessionStore = class {
|
|
|
907
898
|
}
|
|
908
899
|
/** Cancel all pending writes without flushing (e.g. for tests). */
|
|
909
900
|
cancelAll() {
|
|
910
|
-
for (const
|
|
901
|
+
for (const timer2 of this.timers.values()) clearTimeout(timer2);
|
|
911
902
|
this.timers.clear();
|
|
912
903
|
}
|
|
913
904
|
};
|
|
@@ -920,7 +911,6 @@ function buildRecord(session) {
|
|
|
920
911
|
upstreamOrigin: session.upstreamOrigin,
|
|
921
912
|
createdAt: session.createdAt,
|
|
922
913
|
requests: session.requests,
|
|
923
|
-
condensedToolResults: session.condensedToolResults,
|
|
924
914
|
tokensSaved: session.tokensSaved,
|
|
925
915
|
state: session.state,
|
|
926
916
|
blockContents: Object.fromEntries(session.blockContents)
|
|
@@ -939,7 +929,6 @@ function buildSession(parsed) {
|
|
|
939
929
|
createdAt: parsed.createdAt ?? Date.now(),
|
|
940
930
|
lastSeen: Date.now(),
|
|
941
931
|
requests: parsed.requests ?? 0,
|
|
942
|
-
condensedToolResults: parsed.condensedToolResults ?? 0,
|
|
943
932
|
tokensSaved: parsed.tokensSaved ?? 0,
|
|
944
933
|
blockContents,
|
|
945
934
|
inFlight: 0,
|
|
@@ -1026,7 +1015,6 @@ function getSession(id, meta) {
|
|
|
1026
1015
|
createdAt: Date.now(),
|
|
1027
1016
|
lastSeen: Date.now(),
|
|
1028
1017
|
requests: 0,
|
|
1029
|
-
condensedToolResults: 0,
|
|
1030
1018
|
tokensSaved: 0,
|
|
1031
1019
|
blockContents: /* @__PURE__ */ new Map(),
|
|
1032
1020
|
inFlight: 0,
|
|
@@ -1597,6 +1585,46 @@ ${body.slice(0, 4e3)}...`;
|
|
|
1597
1585
|
${body}`;
|
|
1598
1586
|
}
|
|
1599
1587
|
|
|
1588
|
+
// src/logger.ts
|
|
1589
|
+
import { createWriteStream, mkdirSync as mkdirSync3, statSync, renameSync as renameSync2 } from "fs";
|
|
1590
|
+
import path3 from "path";
|
|
1591
|
+
var MAX_BYTES = 10 * 1024 * 1024;
|
|
1592
|
+
var stream;
|
|
1593
|
+
var logPath;
|
|
1594
|
+
function open(pathStr) {
|
|
1595
|
+
mkdirSync3(path3.dirname(pathStr), { recursive: true });
|
|
1596
|
+
try {
|
|
1597
|
+
if (statSync(pathStr).size >= MAX_BYTES) {
|
|
1598
|
+
renameSync2(pathStr, pathStr + ".old");
|
|
1599
|
+
}
|
|
1600
|
+
} catch {
|
|
1601
|
+
}
|
|
1602
|
+
return createWriteStream(pathStr, { flags: "a" });
|
|
1603
|
+
}
|
|
1604
|
+
function configureLogger(file) {
|
|
1605
|
+
if (!file || file === "off") {
|
|
1606
|
+
logPath = void 0;
|
|
1607
|
+
stream = void 0;
|
|
1608
|
+
return void 0;
|
|
1609
|
+
}
|
|
1610
|
+
logPath = file;
|
|
1611
|
+
stream = open(file);
|
|
1612
|
+
return file;
|
|
1613
|
+
}
|
|
1614
|
+
var log = (level, msg2) => {
|
|
1615
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1616
|
+
const line = `${ts} [${level}] ${msg2}
|
|
1617
|
+
`;
|
|
1618
|
+
process.stderr.write(line);
|
|
1619
|
+
if (stream) {
|
|
1620
|
+
stream.write(line);
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
function closeLogger() {
|
|
1624
|
+
stream?.end();
|
|
1625
|
+
stream = void 0;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1600
1628
|
// src/compress-loop.ts
|
|
1601
1629
|
function executeProxyTool(toolName, args, ctx) {
|
|
1602
1630
|
if (toolName === "compress") {
|
|
@@ -1858,6 +1886,16 @@ async function* compressLoopStream(initialUpstream, ctx, requestBody, requestOpt
|
|
|
1858
1886
|
const proxyCalls = toolCalls.filter((tc) => PROXY_TOOL_NAMES.has(tc.name));
|
|
1859
1887
|
const realCalls = toolCalls.filter((tc) => !PROXY_TOOL_NAMES.has(tc.name));
|
|
1860
1888
|
const hasOnlyProxy = proxyCalls.length > 0 && realCalls.length === 0;
|
|
1889
|
+
if (usage) {
|
|
1890
|
+
const prompt = usage.prompt_tokens ?? usage.input_tokens;
|
|
1891
|
+
const det = usage.prompt_tokens_details ?? usage.prompt_cache_hit_tokens;
|
|
1892
|
+
const cached = det?.cached_tokens ?? usage.prompt_cache_hit_tokens;
|
|
1893
|
+
const out = usage.completion_tokens ?? usage.output_tokens;
|
|
1894
|
+
if (typeof prompt === "number") {
|
|
1895
|
+
const ch = typeof cached === "number" ? cached : 0;
|
|
1896
|
+
log("info", `[acp-usage] round ${loopCount} input=${prompt} cached=${typeof cached === "number" ? cached : "?"} output=${out ?? "?"}${ch > 0 ? ` (cache hit ${Math.round(ch / prompt * 100)}%)` : ""}`);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1861
1899
|
if (!hasOnlyProxy) {
|
|
1862
1900
|
for (const tc of realCalls) {
|
|
1863
1901
|
yield Buffer.from(buildToolCallSse(makeBase(), tc), "utf8");
|
|
@@ -1951,16 +1989,16 @@ function extractTextTriggers(text) {
|
|
|
1951
1989
|
let i = 0;
|
|
1952
1990
|
let n = 0;
|
|
1953
1991
|
while (i < text.length) {
|
|
1954
|
-
const
|
|
1955
|
-
if (
|
|
1992
|
+
const open2 = text.indexOf(ACP_TEXT_OPEN, i);
|
|
1993
|
+
if (open2 === -1) {
|
|
1956
1994
|
clean += text.slice(i);
|
|
1957
1995
|
break;
|
|
1958
1996
|
}
|
|
1959
|
-
clean += text.slice(i,
|
|
1960
|
-
const after =
|
|
1997
|
+
clean += text.slice(i, open2);
|
|
1998
|
+
const after = open2 + ACP_TEXT_OPEN.length;
|
|
1961
1999
|
const close = text.indexOf(ACP_TEXT_CLOSE, after);
|
|
1962
2000
|
if (close === -1) {
|
|
1963
|
-
clean += text.slice(
|
|
2001
|
+
clean += text.slice(open2);
|
|
1964
2002
|
break;
|
|
1965
2003
|
}
|
|
1966
2004
|
const payload = text.slice(after, close).trim();
|
|
@@ -2255,7 +2293,7 @@ async function* compressLoopResponsesStream(initialUpstream, ctx, requestBody, r
|
|
|
2255
2293
|
const prDet = usage.prompt_tokens_details;
|
|
2256
2294
|
const cached = inDet?.cached_tokens ?? prDet?.cached_tokens ?? "?";
|
|
2257
2295
|
const out = usage.output_tokens ?? "?";
|
|
2258
|
-
|
|
2296
|
+
log("info", `[acp-usage] round ${loopCount} input=${prompt} cached=${cached} output=${out}${cached !== "?" && cached !== 0 && prompt !== "?" ? ` (cache hit ${Math.round(Number(cached) / Number(prompt) * 100)}%)` : ""}`);
|
|
2259
2297
|
}
|
|
2260
2298
|
}
|
|
2261
2299
|
}
|
|
@@ -2453,10 +2491,10 @@ function safeWrite(res, chunk) {
|
|
|
2453
2491
|
} catch {
|
|
2454
2492
|
}
|
|
2455
2493
|
}
|
|
2456
|
-
function emitStreamError(res, protocol, message,
|
|
2494
|
+
function emitStreamError(res, protocol, message, log2) {
|
|
2457
2495
|
const visible = `
|
|
2458
2496
|
\u274C [ACP] stream error: ${message}`;
|
|
2459
|
-
|
|
2497
|
+
log2?.(`[acp-proxy: stream aborted mid-response: ${message}]`);
|
|
2460
2498
|
try {
|
|
2461
2499
|
if (protocol === "openai") {
|
|
2462
2500
|
safeWrite(res, `data: ${JSON.stringify({ choices: [{ index: 0, delta: { content: visible }, finish_reason: null }] })}
|
|
@@ -2556,17 +2594,21 @@ function resolveUpstream(opts, reqUrl) {
|
|
|
2556
2594
|
return void 0;
|
|
2557
2595
|
}
|
|
2558
2596
|
async function startServer(opts) {
|
|
2597
|
+
const filePath = configureLogger(opts.logFile ?? defaultLogFile());
|
|
2559
2598
|
const core = createCore();
|
|
2560
2599
|
const config = opts.kernelConfig;
|
|
2561
|
-
const
|
|
2600
|
+
const log2 = (level, msg2) => logMsg(opts, level, msg2);
|
|
2562
2601
|
await initSessions();
|
|
2563
|
-
|
|
2602
|
+
log2("info", `[persist] ${getStore().enabled ? "enabled" : "disabled"}`);
|
|
2603
|
+
if (filePath) {
|
|
2604
|
+
log2("info", `[log] writing to ${filePath}`);
|
|
2605
|
+
}
|
|
2564
2606
|
const server = http.createServer(async (req, res) => {
|
|
2565
2607
|
try {
|
|
2566
|
-
await handle(req, res, opts, core, config,
|
|
2608
|
+
await handle(req, res, opts, core, config, log2);
|
|
2567
2609
|
} catch (err) {
|
|
2568
2610
|
const msg2 = String(err);
|
|
2569
|
-
|
|
2611
|
+
log2("error", msg2);
|
|
2570
2612
|
if (!res.headersSent) {
|
|
2571
2613
|
const status = msg2.includes("exceeds") ? 413 : 502;
|
|
2572
2614
|
res.writeHead(status, { "content-type": "application/json" });
|
|
@@ -2577,7 +2619,7 @@ async function startServer(opts) {
|
|
|
2577
2619
|
}
|
|
2578
2620
|
});
|
|
2579
2621
|
server.listen(opts.port, opts.host, () => {
|
|
2580
|
-
|
|
2622
|
+
log2(
|
|
2581
2623
|
"info",
|
|
2582
2624
|
`acp-proxy listening on http://${opts.host}:${opts.port}` + (Object.keys(opts.routes).length ? ` \u2014 routes: ${Object.entries(opts.routes).map(([n, u]) => `${n}=${typeof u === "string" ? u : u.url}`).join(", ")}` : ` \u2192 ${opts.upstream}`)
|
|
2583
2625
|
);
|
|
@@ -2586,9 +2628,10 @@ async function startServer(opts) {
|
|
|
2586
2628
|
const shutdown = (sig) => {
|
|
2587
2629
|
if (shuttingDown) return;
|
|
2588
2630
|
shuttingDown = true;
|
|
2589
|
-
|
|
2631
|
+
log2("info", `${sig} received \u2014 flushing sessions\u2026`);
|
|
2590
2632
|
server.close();
|
|
2591
2633
|
void flushAllSessions().finally(() => {
|
|
2634
|
+
closeLogger();
|
|
2592
2635
|
process.exit(0);
|
|
2593
2636
|
});
|
|
2594
2637
|
};
|
|
@@ -2596,21 +2639,7 @@ async function startServer(opts) {
|
|
|
2596
2639
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
2597
2640
|
return server;
|
|
2598
2641
|
}
|
|
2599
|
-
function
|
|
2600
|
-
const condenseOpts = {
|
|
2601
|
-
enabled: opts.condense.enabled,
|
|
2602
|
-
keepRecent: opts.condense.keepRecentToolResults,
|
|
2603
|
-
minChars: opts.condense.minCharsToCondense,
|
|
2604
|
-
maxKeptChars: opts.condense.maxKeptChars
|
|
2605
|
-
};
|
|
2606
|
-
const { messages: out, condensedCount, charsSaved } = condenseOldToolResults(messages, condenseOpts);
|
|
2607
|
-
if (condensedCount > 0) {
|
|
2608
|
-
session.condensedToolResults += condensedCount;
|
|
2609
|
-
session.tokensSaved += Math.ceil(charsSaved / 4);
|
|
2610
|
-
}
|
|
2611
|
-
return out;
|
|
2612
|
-
}
|
|
2613
|
-
async function handle(req, res, opts, core, config, log) {
|
|
2642
|
+
async function handle(req, res, opts, core, config, log2) {
|
|
2614
2643
|
if (req.method === "GET" && req.url === "/__acp/stats") return sendStats(res);
|
|
2615
2644
|
if (req.method === "GET" && (req.url === "/" || req.url === "/__acp/health")) {
|
|
2616
2645
|
res.writeHead(200, { "content-type": "application/json" });
|
|
@@ -2622,12 +2651,12 @@ async function handle(req, res, opts, core, config, log) {
|
|
|
2622
2651
|
bodyBuffer = await readBody(req);
|
|
2623
2652
|
} catch (err) {
|
|
2624
2653
|
if (err instanceof BodyTooLargeError) {
|
|
2625
|
-
|
|
2654
|
+
log2("warn", `413: request body exceeds ${err.limit} bytes`);
|
|
2626
2655
|
res.writeHead(413, { "content-type": "application/json" });
|
|
2627
2656
|
res.end(JSON.stringify({ error: { type: "request_too_large", message: err.message } }));
|
|
2628
2657
|
return;
|
|
2629
2658
|
}
|
|
2630
|
-
|
|
2659
|
+
log2("warn", `read body failed: ${String(err)}`);
|
|
2631
2660
|
res.writeHead(400, { "content-type": "application/json" });
|
|
2632
2661
|
res.end(JSON.stringify({ error: { type: "invalid_request", message: String(err) } }));
|
|
2633
2662
|
return;
|
|
@@ -2663,10 +2692,10 @@ async function handle(req, res, opts, core, config, log) {
|
|
|
2663
2692
|
const session = getSession(sessionId, { protocol, upstreamOrigin });
|
|
2664
2693
|
const affinity = affinityToken(req.headers, conversation);
|
|
2665
2694
|
await withSessionLock(session, async () => {
|
|
2666
|
-
prepared = protocol === "anthropic" ? prepareAnthropic(parsed, req, opts, core, reqConfig,
|
|
2695
|
+
prepared = protocol === "anthropic" ? prepareAnthropic(parsed, req, opts, core, reqConfig, log2, session) : protocol === "openai" ? prepareOpenai(parsed, req, opts, core, reqConfig, log2, session) : prepareResponses(parsed, req, opts, core, reqConfig, log2, session);
|
|
2667
2696
|
acquireInFlight(session);
|
|
2668
2697
|
try {
|
|
2669
|
-
await forward(req, res, opts, prepared.body, prepared, core, reqConfig,
|
|
2698
|
+
await forward(req, res, opts, prepared.body, prepared, core, reqConfig, log2, route, affinity);
|
|
2670
2699
|
} finally {
|
|
2671
2700
|
releaseInFlight(session);
|
|
2672
2701
|
}
|
|
@@ -2674,9 +2703,9 @@ async function handle(req, res, opts, core, config, log) {
|
|
|
2674
2703
|
}
|
|
2675
2704
|
if (!prepared) {
|
|
2676
2705
|
if (protocol === null && !opts.passthrough) {
|
|
2677
|
-
|
|
2706
|
+
log2("warn", `unrecognized path ${url} \u2014 not a known protocol (/chat/completions, /v1/messages, /responses); forwarding unchanged`);
|
|
2678
2707
|
}
|
|
2679
|
-
await forward(req, res, opts, bodyBuffer, null, core, reqConfig,
|
|
2708
|
+
await forward(req, res, opts, bodyBuffer, null, core, reqConfig, log2, route, void 0);
|
|
2680
2709
|
}
|
|
2681
2710
|
}
|
|
2682
2711
|
var ACP_TAG_MARK = "<acp ";
|
|
@@ -2704,24 +2733,24 @@ function diagNudge(turn, sessionId, tokenCount, limit) {
|
|
|
2704
2733
|
const inject = n.shouldInject ? `INJECT T${n.tier ?? "?"}` : "idle";
|
|
2705
2734
|
return `[${sessionId}] nudge ${inject}: usage=${pct} (${tokenCount}/${limit}), growth=${growth}/${floor} (ref=${ref}, interval=${interval}), pendingT1=${pendingT1}/${interval}, reason="${n.reason.slice(0, 120)}"`;
|
|
2706
2735
|
}
|
|
2707
|
-
function prepareAnthropic(parsed, req, opts, core, config,
|
|
2736
|
+
function prepareAnthropic(parsed, req, opts, core, config, log2, session) {
|
|
2708
2737
|
const sessionId = session.id;
|
|
2709
|
-
const
|
|
2738
|
+
const stream2 = parsed.stream === true;
|
|
2710
2739
|
session.requests++;
|
|
2711
2740
|
let processedMessages = [];
|
|
2712
2741
|
let rebuiltMessages = parsed.messages;
|
|
2713
2742
|
let systemOut = parsed.system;
|
|
2714
2743
|
let toolsOut = parsed.tools;
|
|
2715
2744
|
try {
|
|
2716
|
-
const { msgs } = anthropicToCore(parsed);
|
|
2745
|
+
const { msgs, cacheControls } = anthropicToCore(parsed);
|
|
2717
2746
|
const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
|
|
2718
2747
|
const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: "text-only" });
|
|
2719
2748
|
session.state = turn.state;
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
processedMessages =
|
|
2749
|
+
log2("info", diagTagSummary(turn.messages, sessionId, "text-only"));
|
|
2750
|
+
log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
|
|
2751
|
+
processedMessages = turn.messages;
|
|
2723
2752
|
reapOrphanBlocks(session, msgs, deactivateBlock4);
|
|
2724
|
-
rebuiltMessages = coreToAnthropic(processedMessages);
|
|
2753
|
+
rebuiltMessages = coreToAnthropic(processedMessages, cacheControls);
|
|
2725
2754
|
systemOut = injectSystem(parsed, opts);
|
|
2726
2755
|
if (opts.compress.injectTool) {
|
|
2727
2756
|
toolsOut = injectTool(parsed.tools);
|
|
@@ -2736,31 +2765,31 @@ function prepareAnthropic(parsed, req, opts, core, config, log, session) {
|
|
|
2736
2765
|
}
|
|
2737
2766
|
}
|
|
2738
2767
|
} catch (err) {
|
|
2739
|
-
|
|
2768
|
+
log2("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
|
|
2740
2769
|
processedMessages = [];
|
|
2741
2770
|
}
|
|
2742
2771
|
const rebuilt = { ...parsed, messages: rebuiltMessages, system: systemOut, tools: toolsOut };
|
|
2743
2772
|
markDirty(session);
|
|
2744
|
-
return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "anthropic", stream, compressInjected: opts.compress.injectTool };
|
|
2773
|
+
return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "anthropic", stream: stream2, compressInjected: opts.compress.injectTool };
|
|
2745
2774
|
}
|
|
2746
|
-
function prepareOpenai(parsed, req, opts, core, config,
|
|
2775
|
+
function prepareOpenai(parsed, req, opts, core, config, log2, session) {
|
|
2747
2776
|
const sessionId = session.id;
|
|
2748
|
-
const
|
|
2777
|
+
const stream2 = parsed.stream === true;
|
|
2749
2778
|
session.requests++;
|
|
2750
2779
|
let processedMessages = [];
|
|
2751
2780
|
let rebuiltMessages = parsed.messages;
|
|
2752
2781
|
let toolsOut = parsed.tools;
|
|
2753
2782
|
const maxTokens = typeof parsed.max_tokens === "number" ? parsed.max_tokens : 8192;
|
|
2754
|
-
const isTitleGen = maxTokens <= 200
|
|
2783
|
+
const isTitleGen = maxTokens <= 200;
|
|
2755
2784
|
const shouldInject = opts.compress.injectTool && !isTitleGen;
|
|
2756
2785
|
try {
|
|
2757
2786
|
const { msgs } = openaiToCore(parsed);
|
|
2758
2787
|
const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
|
|
2759
2788
|
const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: "text-only" });
|
|
2760
2789
|
session.state = turn.state;
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
processedMessages =
|
|
2790
|
+
log2("info", diagTagSummary(turn.messages, sessionId, "text-only"));
|
|
2791
|
+
log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
|
|
2792
|
+
processedMessages = turn.messages;
|
|
2764
2793
|
reapOrphanBlocks(session, msgs, deactivateBlock4);
|
|
2765
2794
|
rebuiltMessages = coreToOpenai(processedMessages);
|
|
2766
2795
|
const sysParts = [];
|
|
@@ -2779,16 +2808,16 @@ function prepareOpenai(parsed, req, opts, core, config, log, session) {
|
|
|
2779
2808
|
}
|
|
2780
2809
|
}
|
|
2781
2810
|
} catch (err) {
|
|
2782
|
-
|
|
2811
|
+
log2("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
|
|
2783
2812
|
processedMessages = [];
|
|
2784
2813
|
}
|
|
2785
2814
|
const rebuilt = { ...parsed, messages: rebuiltMessages, tools: toolsOut };
|
|
2786
2815
|
markDirty(session);
|
|
2787
|
-
return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "openai", stream, compressInjected: shouldInject };
|
|
2816
|
+
return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "openai", stream: stream2, compressInjected: shouldInject };
|
|
2788
2817
|
}
|
|
2789
|
-
function prepareResponses(parsed, req, opts, core, config,
|
|
2818
|
+
function prepareResponses(parsed, req, opts, core, config, log2, session) {
|
|
2790
2819
|
const sessionId = session.id;
|
|
2791
|
-
const
|
|
2820
|
+
const stream2 = parsed.stream === true;
|
|
2792
2821
|
session.requests++;
|
|
2793
2822
|
let processedMessages = [];
|
|
2794
2823
|
let rebuiltInput = parsed.input;
|
|
@@ -2797,18 +2826,18 @@ function prepareResponses(parsed, req, opts, core, config, log, session) {
|
|
|
2797
2826
|
try {
|
|
2798
2827
|
const { msgs, systemParts, preamble, customToolCallIds } = responsesToCore(parsed);
|
|
2799
2828
|
if (process.env.ACP_DEBUG) {
|
|
2800
|
-
|
|
2829
|
+
log2("info", `[${sessionId}] input items: ${Array.isArray(parsed.input) ? parsed.input.map((i) => i.type).join(",") : "(string)"}`);
|
|
2801
2830
|
}
|
|
2802
2831
|
const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
|
|
2803
2832
|
const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: process.env.ACP_RENDER_NONE ? "none" : "text-only" });
|
|
2804
2833
|
session.state = turn.state;
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
processedMessages =
|
|
2834
|
+
log2("info", diagTagSummary(turn.messages, sessionId, "text-only"));
|
|
2835
|
+
log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
|
|
2836
|
+
processedMessages = turn.messages;
|
|
2808
2837
|
reapOrphanBlocks(session, msgs, deactivateBlock4);
|
|
2809
2838
|
const conversationItems = coreToResponses(processedMessages, customToolCallIds);
|
|
2810
2839
|
if (preamble.length > 0) {
|
|
2811
|
-
|
|
2840
|
+
log2("info", `[${sessionId}] preserved ${preamble.length} opaque preamble item(s): ${preamble.map((p) => p.type).join(",")}`);
|
|
2812
2841
|
}
|
|
2813
2842
|
const inputItems = [...preamble];
|
|
2814
2843
|
if (shouldInject && !process.env.ACP_NO_COMPRESS_PROMPT) {
|
|
@@ -2824,7 +2853,7 @@ function prepareResponses(parsed, req, opts, core, config, log, session) {
|
|
|
2824
2853
|
if (process.env.ACP_DEBUG) {
|
|
2825
2854
|
const ctcs = conversationItems.filter((i) => i.type === "custom_tool_call").length;
|
|
2826
2855
|
const ctcos = conversationItems.filter((i) => i.type === "custom_tool_call_output").length;
|
|
2827
|
-
|
|
2856
|
+
log2("info", `[${sessionId}] rebuilt: msgs=${msgs.length} -> conv=${conversationItems.length} (custom_tool_call=${ctcs} custom_tool_call_output=${ctcos})`);
|
|
2828
2857
|
}
|
|
2829
2858
|
if (turn.nudge?.shouldInject && shouldInject) {
|
|
2830
2859
|
try {
|
|
@@ -2837,7 +2866,7 @@ function prepareResponses(parsed, req, opts, core, config, log, session) {
|
|
|
2837
2866
|
}
|
|
2838
2867
|
rebuiltInput = inputItems;
|
|
2839
2868
|
} catch (err) {
|
|
2840
|
-
|
|
2869
|
+
log2("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
|
|
2841
2870
|
processedMessages = [];
|
|
2842
2871
|
}
|
|
2843
2872
|
const rebuilt = { ...parsed, input: rebuiltInput, tools: toolsOut };
|
|
@@ -2847,10 +2876,10 @@ function prepareResponses(parsed, req, opts, core, config, log, session) {
|
|
|
2847
2876
|
const sub = Array.isArray(r.tools) ? `(${r.tools.length} sub)` : "";
|
|
2848
2877
|
return `${r.type}:${r.name ?? "?"}${sub}`;
|
|
2849
2878
|
});
|
|
2850
|
-
|
|
2879
|
+
log2("info", `[${sessionId}] responses forward tools=[${fwdTools.join(",")}] injectTool=${shouldInject} NO_INJECT_TOOL=${!!process.env.ACP_NO_INJECT_TOOL} NO_COMPRESS_PROMPT=${!!process.env.ACP_NO_COMPRESS_PROMPT}`);
|
|
2851
2880
|
}
|
|
2852
2881
|
markDirty(session);
|
|
2853
|
-
return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "responses", stream, compressInjected: shouldInject };
|
|
2882
|
+
return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "responses", stream: stream2, compressInjected: shouldInject };
|
|
2854
2883
|
}
|
|
2855
2884
|
function injectSystem(parsed, opts) {
|
|
2856
2885
|
const baseText = extractSystem(parsed.system);
|
|
@@ -2886,19 +2915,19 @@ function injectResponsesTool(tools) {
|
|
|
2886
2915
|
const additions = ACP_TOOLS_RESPONSES.filter((t) => !present.has(t.name));
|
|
2887
2916
|
return [...tools, ...additions];
|
|
2888
2917
|
}
|
|
2889
|
-
async function forward(req, res, opts, body, prepared, core, config,
|
|
2918
|
+
async function forward(req, res, opts, body, prepared, core, config, log2, route, affinity) {
|
|
2890
2919
|
const upstreamUrl = route ? route.rewrittenUrl : opts.upstream + (req.url ?? "");
|
|
2891
|
-
|
|
2920
|
+
log2("info", `forward ${req.method} ${req.url ?? ""} \u2192 ${upstreamUrl}${route ? ` (${route.provider})` : ""}`);
|
|
2892
2921
|
if (process.env.ACP_DEBUG && prepared) {
|
|
2893
2922
|
const sid = prepared.session.id;
|
|
2894
2923
|
const hdrKeys = Object.keys(req.headers);
|
|
2895
|
-
|
|
2924
|
+
log2("info", `[${sid}] client headers: ${hdrKeys.join(",")}`);
|
|
2896
2925
|
for (const k of ["authorization", "x-api-key", "x-session-id", "x-session-affinity", "x-acp-session", "x-opencode-session", "prompt-cache-key", "anthropic-beta"]) {
|
|
2897
2926
|
const v = req.headers[k] ?? req.headers[k.toLowerCase()];
|
|
2898
2927
|
if (v) {
|
|
2899
2928
|
const s = Array.isArray(v) ? v.join(",") : String(v);
|
|
2900
2929
|
const masked = /key|auth|token/i.test(k) ? s.slice(0, 8) + "..." + s.slice(-4) + ` (${s.length} chars)` : s.slice(0, 60);
|
|
2901
|
-
|
|
2930
|
+
log2("info", `[${sid}] client hdr ${k}=${masked}`);
|
|
2902
2931
|
}
|
|
2903
2932
|
}
|
|
2904
2933
|
}
|
|
@@ -2909,11 +2938,11 @@ async function forward(req, res, opts, body, prepared, core, config, log, route,
|
|
|
2909
2938
|
const fn = t.function;
|
|
2910
2939
|
return fn?.name ?? t.name ?? "?";
|
|
2911
2940
|
});
|
|
2912
|
-
|
|
2941
|
+
log2("info", `[debug] tools=[${toolNames.join(",")}] msgs=${parsed.messages?.length ?? 0} stream=${parsed.stream ?? false} system_len=${JSON.stringify(parsed.messages?.find((m) => m.role === "system")?.content ?? "").length}`);
|
|
2913
2942
|
if (process.env.ACP_DUMP_REQ === "1") {
|
|
2914
2943
|
const out = `/tmp/acp-proxy-debug-req-${Date.now()}.json`;
|
|
2915
2944
|
fs2.writeFileSync(out, body.slice(0, 5e4));
|
|
2916
|
-
|
|
2945
|
+
log2("info", `[debug] forwarded body written to ${out}`);
|
|
2917
2946
|
}
|
|
2918
2947
|
} catch {
|
|
2919
2948
|
}
|
|
@@ -2961,7 +2990,7 @@ async function forward(req, res, opts, body, prepared, core, config, log, route,
|
|
|
2961
2990
|
config,
|
|
2962
2991
|
messages: prepared.processedMessages,
|
|
2963
2992
|
session: prepared.session,
|
|
2964
|
-
log: (msg2) =>
|
|
2993
|
+
log: (msg2) => log2("info", `[${prepared.session.id}] ${msg2}`),
|
|
2965
2994
|
debug: opts.debug
|
|
2966
2995
|
};
|
|
2967
2996
|
if (prepared.stream) {
|
|
@@ -2991,7 +3020,7 @@ async function forward(req, res, opts, body, prepared, core, config, log, route,
|
|
|
2991
3020
|
{
|
|
2992
3021
|
const s = chunk.toString("utf8");
|
|
2993
3022
|
if (s.includes("<acp ") || s.includes("</acp")) {
|
|
2994
|
-
|
|
3023
|
+
log2("warn", `[${prepared.session.id}] tag echo: openai response stream contains <acp tag`);
|
|
2995
3024
|
}
|
|
2996
3025
|
}
|
|
2997
3026
|
if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
|
|
@@ -3014,7 +3043,7 @@ async function forward(req, res, opts, body, prepared, core, config, log, route,
|
|
|
3014
3043
|
{
|
|
3015
3044
|
const s = chunk.toString("utf8");
|
|
3016
3045
|
if (s.includes("<acp ") || s.includes("</acp")) {
|
|
3017
|
-
|
|
3046
|
+
log2("warn", `[${prepared.session.id}] tag echo: responses response stream contains <acp tag`);
|
|
3018
3047
|
}
|
|
3019
3048
|
}
|
|
3020
3049
|
if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
|
|
@@ -3025,7 +3054,7 @@ async function forward(req, res, opts, body, prepared, core, config, log, route,
|
|
|
3025
3054
|
{
|
|
3026
3055
|
const s = chunk.toString("utf8");
|
|
3027
3056
|
if (s.includes("<acp ") || s.includes("</acp")) {
|
|
3028
|
-
|
|
3057
|
+
log2("warn", `[${prepared.session.id}] tag echo: anthropic response stream contains <acp tag`);
|
|
3029
3058
|
}
|
|
3030
3059
|
}
|
|
3031
3060
|
if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
|
|
@@ -3033,7 +3062,7 @@ async function forward(req, res, opts, body, prepared, core, config, log, route,
|
|
|
3033
3062
|
}
|
|
3034
3063
|
res.end();
|
|
3035
3064
|
} catch (e) {
|
|
3036
|
-
emitStreamError(res, prepared.protocol, e?.message ?? String(e), (m) =>
|
|
3065
|
+
emitStreamError(res, prepared.protocol, e?.message ?? String(e), (m) => log2("error", `[${prepared.session.id}] ${m}`));
|
|
3037
3066
|
} finally {
|
|
3038
3067
|
clearUpstreamTimer();
|
|
3039
3068
|
if (dumpRaw) await dumpRaw;
|
|
@@ -3058,8 +3087,8 @@ async function forward(req, res, opts, body, prepared, core, config, log, route,
|
|
|
3058
3087
|
}
|
|
3059
3088
|
markDirty(prepared.session);
|
|
3060
3089
|
}
|
|
3061
|
-
async function pipeThrough(
|
|
3062
|
-
const reader =
|
|
3090
|
+
async function pipeThrough(stream2, res) {
|
|
3091
|
+
const reader = stream2.getReader();
|
|
3063
3092
|
try {
|
|
3064
3093
|
for (; ; ) {
|
|
3065
3094
|
const { done, value } = await reader.read();
|
|
@@ -3073,13 +3102,13 @@ async function pipeThrough(stream, res) {
|
|
|
3073
3102
|
res.end();
|
|
3074
3103
|
}
|
|
3075
3104
|
}
|
|
3076
|
-
async function dumpStreamToFile(
|
|
3077
|
-
const { mkdirSync:
|
|
3105
|
+
async function dumpStreamToFile(stream2, dir, name) {
|
|
3106
|
+
const { mkdirSync: mkdirSync4, createWriteStream: createWriteStream2 } = await import("fs");
|
|
3078
3107
|
const { join: join3 } = await import("path");
|
|
3079
3108
|
try {
|
|
3080
|
-
|
|
3081
|
-
const ws =
|
|
3082
|
-
const reader =
|
|
3109
|
+
mkdirSync4(dir, { recursive: true });
|
|
3110
|
+
const ws = createWriteStream2(join3(dir, name));
|
|
3111
|
+
const reader = stream2.getReader();
|
|
3083
3112
|
try {
|
|
3084
3113
|
for (; ; ) {
|
|
3085
3114
|
const { done, value } = await reader.read();
|
|
@@ -3097,7 +3126,6 @@ function sendStats(res) {
|
|
|
3097
3126
|
const sessions2 = listSessions().map((s) => ({
|
|
3098
3127
|
id: s.id,
|
|
3099
3128
|
requests: s.requests,
|
|
3100
|
-
condensedToolResults: s.condensedToolResults,
|
|
3101
3129
|
tokensSaved: s.tokensSaved,
|
|
3102
3130
|
lastSeen: new Date(s.lastSeen).toISOString()
|
|
3103
3131
|
}));
|
|
@@ -3144,27 +3172,133 @@ function readBody(req) {
|
|
|
3144
3172
|
}
|
|
3145
3173
|
function logMsg(opts, level, msg2) {
|
|
3146
3174
|
if (!opts.log) return;
|
|
3147
|
-
|
|
3148
|
-
|
|
3175
|
+
log(level, msg2);
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
// src/update.ts
|
|
3179
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
3180
|
+
import { execFile } from "child_process";
|
|
3181
|
+
import path4 from "path";
|
|
3182
|
+
var REGISTRY_BASE = "https://registry.npmjs.org";
|
|
3183
|
+
var CHECK_INTERVAL_MS = 3 * 60 * 1e3;
|
|
3184
|
+
var THROTTLE_FILE = path4.join(cacheDir(), ".update-check");
|
|
3185
|
+
var SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z-.]+)?$/;
|
|
3186
|
+
var timer;
|
|
3187
|
+
var inFlight = false;
|
|
3188
|
+
function parseVersion(v) {
|
|
3189
|
+
return v.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
|
|
3190
|
+
}
|
|
3191
|
+
function isNewer(latest, current) {
|
|
3192
|
+
const l = parseVersion(latest);
|
|
3193
|
+
const c = parseVersion(current);
|
|
3194
|
+
for (let i = 0; i < 3; i++) {
|
|
3195
|
+
const lv = l[i] ?? 0;
|
|
3196
|
+
const cv = c[i] ?? 0;
|
|
3197
|
+
if (lv > cv) return true;
|
|
3198
|
+
if (lv < cv) return false;
|
|
3199
|
+
}
|
|
3200
|
+
return false;
|
|
3201
|
+
}
|
|
3202
|
+
async function readLastCheck() {
|
|
3203
|
+
try {
|
|
3204
|
+
const data = await readFile(THROTTLE_FILE, "utf-8");
|
|
3205
|
+
return parseInt(data.trim(), 10) || 0;
|
|
3206
|
+
} catch {
|
|
3207
|
+
return 0;
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
async function writeLastCheck(ts) {
|
|
3211
|
+
try {
|
|
3212
|
+
await mkdir(path4.dirname(THROTTLE_FILE), { recursive: true });
|
|
3213
|
+
await writeFile(THROTTLE_FILE, String(ts), "utf-8");
|
|
3214
|
+
} catch {
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
async function checkForUpdate(opts, force = false) {
|
|
3218
|
+
if (!opts.autoUpdate && !force) return;
|
|
3219
|
+
if (inFlight) return;
|
|
3220
|
+
inFlight = true;
|
|
3221
|
+
try {
|
|
3222
|
+
const now = Date.now();
|
|
3223
|
+
if (!force && now - await readLastCheck() < CHECK_INTERVAL_MS) return;
|
|
3224
|
+
await writeLastCheck(now);
|
|
3225
|
+
const url = `${REGISTRY_BASE}/${opts.packageName}/latest`;
|
|
3226
|
+
const res = await fetch(url, {
|
|
3227
|
+
signal: AbortSignal.timeout(5e3),
|
|
3228
|
+
headers: { Accept: "application/json" }
|
|
3229
|
+
});
|
|
3230
|
+
if (!res.ok) return;
|
|
3231
|
+
const data = await res.json();
|
|
3232
|
+
const latest = data.version;
|
|
3233
|
+
if (!latest || !isNewer(latest, opts.currentVersion)) return;
|
|
3234
|
+
const installed = await installLatest(opts.packageName, latest);
|
|
3235
|
+
if (installed) {
|
|
3236
|
+
console.error(
|
|
3237
|
+
`\x1B[32m\u2714 ${opts.packageName} auto-updated ${opts.currentVersion} \u2192 ${latest}. Restart bili to finish.\x1B[0m`
|
|
3238
|
+
);
|
|
3239
|
+
} else {
|
|
3240
|
+
console.error(
|
|
3241
|
+
`\x1B[33m${opts.packageName} ${latest} is available (you have ${opts.currentVersion}). Update with: npm install -g ${opts.packageName}@latest\x1B[0m`
|
|
3242
|
+
);
|
|
3243
|
+
}
|
|
3244
|
+
} catch {
|
|
3245
|
+
} finally {
|
|
3246
|
+
inFlight = false;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
async function installLatest(packageName, latest) {
|
|
3250
|
+
if (!SEMVER_RE.test(latest)) return false;
|
|
3251
|
+
try {
|
|
3252
|
+
const code = await new Promise((resolve) => {
|
|
3253
|
+
execFile(
|
|
3254
|
+
"npm",
|
|
3255
|
+
["install", "-g", `${packageName}@${latest}`, "--silent", "--no-audit", "--no-fund"],
|
|
3256
|
+
{ timeout: 12e4, shell: process.platform === "win32" },
|
|
3257
|
+
(err) => resolve(err ? 1 : 0)
|
|
3258
|
+
);
|
|
3259
|
+
});
|
|
3260
|
+
return code === 0;
|
|
3261
|
+
} catch {
|
|
3262
|
+
return false;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
function startAutoUpdate(opts) {
|
|
3266
|
+
setTimeout(() => {
|
|
3267
|
+
void checkForUpdate(opts);
|
|
3268
|
+
}, 1e4);
|
|
3269
|
+
timer = setInterval(() => {
|
|
3270
|
+
void checkForUpdate(opts);
|
|
3271
|
+
}, CHECK_INTERVAL_MS);
|
|
3272
|
+
timer.unref?.();
|
|
3149
3273
|
}
|
|
3150
3274
|
|
|
3151
3275
|
// src/cli.ts
|
|
3152
3276
|
import { readFileSync as readFileSync3 } from "fs";
|
|
3153
3277
|
import { fileURLToPath } from "url";
|
|
3154
|
-
import
|
|
3278
|
+
import path5 from "path";
|
|
3155
3279
|
var VERSION = (() => {
|
|
3156
3280
|
try {
|
|
3157
3281
|
const here = fileURLToPath(import.meta.url);
|
|
3158
|
-
const pkg =
|
|
3282
|
+
const pkg = path5.join(path5.dirname(here), "..", "package.json");
|
|
3159
3283
|
return JSON.parse(readFileSync3(pkg, "utf8")).version ?? "dev";
|
|
3160
3284
|
} catch {
|
|
3161
3285
|
return "dev";
|
|
3162
3286
|
}
|
|
3163
3287
|
})();
|
|
3288
|
+
var PACKAGE_NAME = (() => {
|
|
3289
|
+
try {
|
|
3290
|
+
const here = fileURLToPath(import.meta.url);
|
|
3291
|
+
const pkg = path5.join(path5.dirname(here), "..", "package.json");
|
|
3292
|
+
return JSON.parse(readFileSync3(pkg, "utf8")).name ?? "billion-context";
|
|
3293
|
+
} catch {
|
|
3294
|
+
return "billion-context";
|
|
3295
|
+
}
|
|
3296
|
+
})();
|
|
3164
3297
|
var HELP = `bili ${VERSION} \u2014 billion-context proxy
|
|
3165
3298
|
|
|
3166
3299
|
Usage:
|
|
3167
3300
|
bili [start] [options] start the proxy (default: reads ${configFile()})
|
|
3301
|
+
bili update check for & install a newer version now
|
|
3168
3302
|
bili --version print version
|
|
3169
3303
|
bili --help show this help
|
|
3170
3304
|
|
|
@@ -3175,9 +3309,10 @@ Options (override config file / env):
|
|
|
3175
3309
|
--debug verbose logging
|
|
3176
3310
|
--passthrough forward without compression
|
|
3177
3311
|
--no-passthrough force compression on (overrides config)
|
|
3312
|
+
--no-auto-update disable background self-update this run
|
|
3178
3313
|
|
|
3179
3314
|
Config: ${configFile()}
|
|
3180
|
-
Set port/host/debug/providers/
|
|
3315
|
+
Set port/host/debug/providers/compress/autoUpdate there. See README \xA7Configuration.
|
|
3181
3316
|
Env vars (ACP_*, BILI_*) also work and override the file; CLI flags win.
|
|
3182
3317
|
|
|
3183
3318
|
Docs: https://github.com/ranxianglei/billion-context
|
|
@@ -3200,6 +3335,9 @@ function parseArgs(argv) {
|
|
|
3200
3335
|
case "--debug":
|
|
3201
3336
|
overrides.ACP_DEBUG = "1";
|
|
3202
3337
|
break;
|
|
3338
|
+
case "--no-auto-update":
|
|
3339
|
+
overrides.ACP_AUTO_UPDATE = "0";
|
|
3340
|
+
break;
|
|
3203
3341
|
case "--passthrough":
|
|
3204
3342
|
overrides.ACP_PASSTHROUGH = "1";
|
|
3205
3343
|
break;
|
|
@@ -3237,6 +3375,8 @@ function parseArgs(argv) {
|
|
|
3237
3375
|
const cmd = positional[0];
|
|
3238
3376
|
if (cmd === "start") {
|
|
3239
3377
|
command = command === "help" || command === "version" ? command : "start";
|
|
3378
|
+
} else if (cmd === "update") {
|
|
3379
|
+
command = "update";
|
|
3240
3380
|
} else {
|
|
3241
3381
|
console.error(`bili: unknown command "${cmd}" (try "bili --help")`);
|
|
3242
3382
|
process.exit(2);
|
|
@@ -3254,17 +3394,23 @@ async function main() {
|
|
|
3254
3394
|
process.stdout.write(VERSION + "\n");
|
|
3255
3395
|
return;
|
|
3256
3396
|
}
|
|
3397
|
+
if (command === "update") {
|
|
3398
|
+
await checkForUpdate({ packageName: PACKAGE_NAME, currentVersion: VERSION, autoUpdate: true }, true);
|
|
3399
|
+
return;
|
|
3400
|
+
}
|
|
3257
3401
|
for (const [k, v] of Object.entries(overrides)) {
|
|
3258
3402
|
if (v !== void 0) process.env[k] = v;
|
|
3259
3403
|
}
|
|
3260
3404
|
const opts = loadOptions();
|
|
3261
3405
|
await startServer(opts);
|
|
3406
|
+
if (opts.autoUpdate) {
|
|
3407
|
+
startAutoUpdate({ packageName: PACKAGE_NAME, currentVersion: VERSION, autoUpdate: true });
|
|
3408
|
+
}
|
|
3262
3409
|
}
|
|
3410
|
+
|
|
3411
|
+
// src/index.ts
|
|
3263
3412
|
main().catch((err) => {
|
|
3264
3413
|
console.error("bili: failed to start:", err);
|
|
3265
3414
|
process.exit(1);
|
|
3266
3415
|
});
|
|
3267
|
-
|
|
3268
|
-
// src/index.ts
|
|
3269
|
-
main();
|
|
3270
3416
|
//# sourceMappingURL=index.js.map
|