billion-context 0.1.1 → 0.1.2
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 +186 -46
- package/dist/index.js +2134 -354
- package/dist/index.js.map +1 -1
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -3,10 +3,40 @@
|
|
|
3
3
|
// src/config.ts
|
|
4
4
|
import { defaultConfig } from "acp-kernel";
|
|
5
5
|
import { readFileSync } from "fs";
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
// src/paths.ts
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import path from "path";
|
|
10
|
+
function xdg(envVar, fallback) {
|
|
11
|
+
const v = process.env[envVar];
|
|
12
|
+
if (v && v.length > 0) return path.resolve(v);
|
|
13
|
+
return path.join(homedir(), fallback);
|
|
14
|
+
}
|
|
15
|
+
function configDir() {
|
|
16
|
+
return path.join(xdg("XDG_CONFIG_HOME", ".config"), "billion-context");
|
|
17
|
+
}
|
|
18
|
+
function configFile() {
|
|
19
|
+
const env = process.env.BILI_CONFIG_FILE;
|
|
20
|
+
if (env && env.length > 0) return path.resolve(env);
|
|
21
|
+
return path.join(configDir(), "billion-context.json");
|
|
22
|
+
}
|
|
23
|
+
function dataDir() {
|
|
24
|
+
return path.join(xdg("XDG_DATA_HOME", ".local/share"), "billion-context");
|
|
25
|
+
}
|
|
26
|
+
function sessionsDir() {
|
|
27
|
+
const env = process.env.BILI_SESSIONS_DIR;
|
|
28
|
+
if (env && env.length > 0) return path.resolve(env);
|
|
29
|
+
return path.join(dataDir(), "sessions");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/config.ts
|
|
33
|
+
function safeReadJson(path4) {
|
|
7
34
|
try {
|
|
8
|
-
return JSON.parse(readFileSync(
|
|
9
|
-
} catch {
|
|
35
|
+
return JSON.parse(readFileSync(path4, "utf8"));
|
|
36
|
+
} catch (e) {
|
|
37
|
+
if (e.code !== "ENOENT") {
|
|
38
|
+
console.error(`[acp-config] failed to parse ${path4}: ${String(e)}`);
|
|
39
|
+
}
|
|
10
40
|
return void 0;
|
|
11
41
|
}
|
|
12
42
|
}
|
|
@@ -20,6 +50,7 @@ var CONTEXT_LIMIT_TABLE = [
|
|
|
20
50
|
{ match: /^gemini-2\.5/i, limit: 1e6 },
|
|
21
51
|
{ match: /^gemini-1\.5/i, limit: 1e6 },
|
|
22
52
|
{ match: /^glm-4\.6/i, limit: 128e3 },
|
|
53
|
+
{ match: /^glm-5/i, limit: 1e6 },
|
|
23
54
|
{ match: /^glm-/i, limit: 128e3 },
|
|
24
55
|
{ match: /^deepseek/i, limit: 64e3 },
|
|
25
56
|
{ match: /^qwen/i, limit: 128e3 },
|
|
@@ -33,25 +64,44 @@ function lookupContextLimit(model) {
|
|
|
33
64
|
}
|
|
34
65
|
return void 0;
|
|
35
66
|
}
|
|
67
|
+
function resolveContextLimit(routes, provider, model) {
|
|
68
|
+
if (!model) return void 0;
|
|
69
|
+
if (provider) {
|
|
70
|
+
const route = routes[provider];
|
|
71
|
+
if (route?.models) {
|
|
72
|
+
const m = route.models[model];
|
|
73
|
+
if (m?.context && m.context > 0) return m.context;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return lookupContextLimit(model);
|
|
77
|
+
}
|
|
36
78
|
function loadOptions(env = process.env) {
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
79
|
+
const fileConfig = loadConfigFile();
|
|
80
|
+
const port = parseInt(env.ACP_PORT ?? env.PORT ?? `${fileConfig.port ?? 8787}`, 10);
|
|
81
|
+
const host = env.ACP_HOST ?? fileConfig.host ?? "127.0.0.1";
|
|
82
|
+
const upstream = (env.ACP_UPSTREAM ?? fileConfig.upstream ?? "https://api.anthropic.com").replace(/\/$/, "");
|
|
40
83
|
let routes = {};
|
|
41
|
-
const routesPath = env.ACP_PROVIDERS ?? "";
|
|
84
|
+
const routesPath = env.ACP_PROVIDERS ?? fileConfig.providersPath ?? "";
|
|
42
85
|
if (routesPath) {
|
|
43
86
|
const parsed = safeReadJson(routesPath);
|
|
44
87
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
45
88
|
for (const [k, v] of Object.entries(parsed)) {
|
|
46
|
-
|
|
89
|
+
const route = parseRouteEntry(v);
|
|
90
|
+
if (route) routes[k] = route;
|
|
47
91
|
}
|
|
48
92
|
}
|
|
49
93
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
94
|
+
if (fileConfig.providers) {
|
|
95
|
+
for (const [k, v] of Object.entries(fileConfig.providers)) {
|
|
96
|
+
const route = parseRouteEntry(v);
|
|
97
|
+
if (route && !routes[k]) routes[k] = route;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
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);
|
|
55
105
|
return {
|
|
56
106
|
port: Number.isFinite(port) ? port : 8787,
|
|
57
107
|
host,
|
|
@@ -61,23 +111,83 @@ function loadOptions(env = process.env) {
|
|
|
61
111
|
kernelConfig: defaultConfig(modelContextLimit),
|
|
62
112
|
condense: { enabled, keepRecentToolResults, minCharsToCondense, maxKeptChars },
|
|
63
113
|
compress: {
|
|
64
|
-
injectTool: (env.ACP_COMPRESS_TOOL ?? "1") !== "0",
|
|
65
|
-
injectNudge: (env.ACP_COMPRESS_NUDGE ?? "1") !== "0"
|
|
114
|
+
injectTool: (env.ACP_COMPRESS_TOOL ?? (fileConfig.compress?.injectTool === false ? "0" : "1")) !== "0",
|
|
115
|
+
injectNudge: (env.ACP_COMPRESS_NUDGE ?? (fileConfig.compress?.injectNudge === false ? "0" : "1")) !== "0"
|
|
66
116
|
},
|
|
67
|
-
sessionHeader: env.ACP_SESSION_HEADER ?? "x-acp-session",
|
|
68
|
-
log: env.ACP_LOG !== "0",
|
|
69
|
-
debug: (env.ACP_DEBUG ?? "0") === "1",
|
|
70
|
-
dumpSse: env.ACP_DUMP_SSE || void 0,
|
|
71
|
-
passthrough: (env.ACP_PASSTHROUGH ?? "0") === "1"
|
|
117
|
+
sessionHeader: env.ACP_SESSION_HEADER ?? fileConfig.sessionHeader ?? "x-acp-session",
|
|
118
|
+
log: env.ACP_LOG !== "0" && fileConfig.log !== false,
|
|
119
|
+
debug: (env.ACP_DEBUG ?? (fileConfig.debug ? "1" : "0")) === "1",
|
|
120
|
+
dumpSse: env.ACP_DUMP_SSE || fileConfig.dumpSse || void 0,
|
|
121
|
+
passthrough: (env.ACP_PASSTHROUGH ?? (fileConfig.passthrough ? "1" : "0")) === "1"
|
|
72
122
|
};
|
|
73
123
|
}
|
|
124
|
+
function loadConfigFile() {
|
|
125
|
+
const parsed = safeReadJson(configFile());
|
|
126
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
127
|
+
return parsed;
|
|
128
|
+
}
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
function parseRouteEntry(v) {
|
|
132
|
+
if (typeof v === "string" && v.length > 0) {
|
|
133
|
+
return { url: v.replace(/\/$/, "") };
|
|
134
|
+
}
|
|
135
|
+
if (v && typeof v === "object" && !Array.isArray(v) && typeof v.url === "string" && v.url.length > 0) {
|
|
136
|
+
const obj = v;
|
|
137
|
+
return { url: obj.url.replace(/\/$/, ""), models: obj.models };
|
|
138
|
+
}
|
|
139
|
+
return void 0;
|
|
140
|
+
}
|
|
74
141
|
|
|
75
142
|
// src/server.ts
|
|
76
143
|
import http from "http";
|
|
77
|
-
import
|
|
78
|
-
import { createCore, estimateTokensFast as
|
|
144
|
+
import fs2 from "fs";
|
|
145
|
+
import { createCore, estimateTokensFast as estimateTokensFast3, renderNudgeText, deactivateBlock as deactivateBlock4 } from "acp-kernel";
|
|
146
|
+
|
|
147
|
+
// src/fetch-util.ts
|
|
148
|
+
var MAX_REQUEST_BYTES = 100 * 1024 * 1024;
|
|
149
|
+
var UPSTREAM_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
150
|
+
async function fetchWithTimeout(url, init, timeoutMs = UPSTREAM_TIMEOUT_MS) {
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
153
|
+
try {
|
|
154
|
+
const response = await fetch(url, { ...init, signal: controller.signal });
|
|
155
|
+
return { response, clearTimer: () => clearTimeout(timer) };
|
|
156
|
+
} catch (e) {
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
throw e;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/util.ts
|
|
163
|
+
import { createHash } from "crypto";
|
|
164
|
+
function hashId(s) {
|
|
165
|
+
return createHash("sha256").update(s, "utf8").digest("hex").slice(0, 16);
|
|
166
|
+
}
|
|
167
|
+
function safeJsonParse(s) {
|
|
168
|
+
try {
|
|
169
|
+
return s ? JSON.parse(s) : {};
|
|
170
|
+
} catch {
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/message-id.ts
|
|
176
|
+
function deriveMessageId(role, contentType, text, options = {}) {
|
|
177
|
+
const seed = `${role}|${contentType}|${options.toolCallId ?? ""}|${options.toolName ?? ""}|${text}`;
|
|
178
|
+
return "h_" + hashId(seed);
|
|
179
|
+
}
|
|
180
|
+
var ClusterCounter = class {
|
|
181
|
+
counts = /* @__PURE__ */ new Map();
|
|
182
|
+
next(baseId) {
|
|
183
|
+
const n = this.counts.get(baseId) ?? 0;
|
|
184
|
+
this.counts.set(baseId, n + 1);
|
|
185
|
+
return n === 0 ? baseId : `${baseId}_${n}`;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
79
188
|
|
|
80
189
|
// src/anthropic.ts
|
|
190
|
+
var CONDENSED_TAG = "[acp-proxy: condensed";
|
|
81
191
|
function extractSystem(system) {
|
|
82
192
|
if (!system) return "";
|
|
83
193
|
if (typeof system === "string") return system;
|
|
@@ -92,19 +202,23 @@ function buildSystem(text, original) {
|
|
|
92
202
|
}
|
|
93
203
|
function anthropicToCore(body) {
|
|
94
204
|
const msgs = [];
|
|
95
|
-
|
|
205
|
+
const clusters = new ClusterCounter();
|
|
96
206
|
for (const m of body.messages) {
|
|
97
207
|
const blocks = typeof m.content === "string" ? [{ type: "text", text: m.content }] : m.content;
|
|
98
208
|
for (const b of blocks) {
|
|
99
|
-
const id = `raw-${idx}`;
|
|
100
|
-
idx++;
|
|
101
209
|
switch (b.type) {
|
|
102
|
-
case "text":
|
|
103
|
-
|
|
210
|
+
case "text": {
|
|
211
|
+
const base = deriveMessageId(m.role, "text", b.text);
|
|
212
|
+
msgs.push({ id: clusters.next(base), role: m.role, contentType: "text", text: b.text });
|
|
104
213
|
break;
|
|
105
|
-
|
|
214
|
+
}
|
|
215
|
+
case "tool_use": {
|
|
216
|
+
const base = deriveMessageId("assistant", "tool-call", safeStringify(b.input), {
|
|
217
|
+
toolCallId: b.id,
|
|
218
|
+
toolName: b.name
|
|
219
|
+
});
|
|
106
220
|
msgs.push({
|
|
107
|
-
id,
|
|
221
|
+
id: clusters.next(base),
|
|
108
222
|
role: "assistant",
|
|
109
223
|
contentType: "tool-call",
|
|
110
224
|
toolName: b.name,
|
|
@@ -112,10 +226,12 @@ function anthropicToCore(body) {
|
|
|
112
226
|
text: safeStringify(b.input)
|
|
113
227
|
});
|
|
114
228
|
break;
|
|
229
|
+
}
|
|
115
230
|
case "tool_result": {
|
|
116
231
|
const text = typeof b.content === "string" ? b.content : b.content.map((c) => c.text).join("\n");
|
|
232
|
+
const base = deriveMessageId("tool", "tool-result", text, { toolCallId: b.tool_use_id });
|
|
117
233
|
msgs.push({
|
|
118
|
-
id,
|
|
234
|
+
id: clusters.next(base),
|
|
119
235
|
role: "tool",
|
|
120
236
|
contentType: "tool-result",
|
|
121
237
|
toolCallId: b.tool_use_id,
|
|
@@ -123,12 +239,16 @@ function anthropicToCore(body) {
|
|
|
123
239
|
});
|
|
124
240
|
break;
|
|
125
241
|
}
|
|
126
|
-
case "thinking":
|
|
127
|
-
|
|
242
|
+
case "thinking": {
|
|
243
|
+
const base = deriveMessageId("assistant", "reasoning", b.thinking);
|
|
244
|
+
msgs.push({ id: clusters.next(base), role: "assistant", contentType: "reasoning", text: b.thinking });
|
|
128
245
|
break;
|
|
129
|
-
|
|
130
|
-
|
|
246
|
+
}
|
|
247
|
+
case "image": {
|
|
248
|
+
const base = deriveMessageId(m.role, "text", "[image]");
|
|
249
|
+
msgs.push({ id: clusters.next(base), role: m.role, contentType: "text", text: "[image]" });
|
|
131
250
|
break;
|
|
251
|
+
}
|
|
132
252
|
}
|
|
133
253
|
}
|
|
134
254
|
}
|
|
@@ -176,11 +296,38 @@ function coreToAnthropic(messages) {
|
|
|
176
296
|
flush();
|
|
177
297
|
return out;
|
|
178
298
|
}
|
|
179
|
-
function
|
|
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
|
+
function conversationSignalAnthropic(body, headerValue2) {
|
|
180
327
|
if (headerValue2 && headerValue2.trim()) return headerValue2.trim();
|
|
181
328
|
const firstUser = body.messages.find((m) => m.role === "user");
|
|
182
329
|
const seed = firstUser ? JSON.stringify(firstUser.content).slice(0, 200) : "default";
|
|
183
|
-
return
|
|
330
|
+
return hashId(seed);
|
|
184
331
|
}
|
|
185
332
|
function safeStringify(v) {
|
|
186
333
|
try {
|
|
@@ -197,62 +344,59 @@ function safeParse(s) {
|
|
|
197
344
|
return {};
|
|
198
345
|
}
|
|
199
346
|
}
|
|
200
|
-
function hash(s) {
|
|
201
|
-
let h = 2166136261;
|
|
202
|
-
for (let i = 0; i < s.length; i++) {
|
|
203
|
-
h ^= s.charCodeAt(i);
|
|
204
|
-
h = Math.imul(h, 16777619);
|
|
205
|
-
}
|
|
206
|
-
return (h >>> 0).toString(36);
|
|
207
|
-
}
|
|
208
347
|
|
|
209
348
|
// src/openai.ts
|
|
210
349
|
function openaiToCore(body) {
|
|
211
350
|
const msgs = [];
|
|
212
|
-
|
|
351
|
+
const clusters = new ClusterCounter();
|
|
213
352
|
for (const m of body.messages) {
|
|
214
353
|
switch (m.role) {
|
|
215
354
|
case "system":
|
|
216
355
|
case "developer": {
|
|
217
|
-
|
|
218
|
-
|
|
356
|
+
const base = deriveMessageId(m.role, "text", stringContent(m.content));
|
|
357
|
+
msgs.push({ id: clusters.next(base), role: "system", contentType: "text", text: stringContent(m.content) });
|
|
219
358
|
break;
|
|
220
359
|
}
|
|
221
360
|
case "user": {
|
|
222
|
-
|
|
223
|
-
|
|
361
|
+
const base = deriveMessageId("user", "text", stringContent(m.content));
|
|
362
|
+
msgs.push({ id: clusters.next(base), role: "user", contentType: "text", text: stringContent(m.content) });
|
|
224
363
|
break;
|
|
225
364
|
}
|
|
226
365
|
case "assistant": {
|
|
227
366
|
const text = stringContent(m.content);
|
|
228
367
|
if (text) {
|
|
229
|
-
|
|
230
|
-
|
|
368
|
+
const base = deriveMessageId("assistant", "text", text);
|
|
369
|
+
msgs.push({ id: clusters.next(base), role: "assistant", contentType: "text", text });
|
|
231
370
|
}
|
|
232
371
|
if (Array.isArray(m.tool_calls)) {
|
|
233
372
|
for (const tc of m.tool_calls) {
|
|
373
|
+
const base = deriveMessageId("assistant", "tool-call", tc.function.arguments ?? "", {
|
|
374
|
+
toolCallId: tc.id,
|
|
375
|
+
toolName: tc.function.name
|
|
376
|
+
});
|
|
234
377
|
msgs.push({
|
|
235
|
-
id:
|
|
378
|
+
id: clusters.next(base),
|
|
236
379
|
role: "assistant",
|
|
237
380
|
contentType: "tool-call",
|
|
238
381
|
toolName: tc.function.name,
|
|
239
382
|
toolCallId: tc.id,
|
|
240
383
|
text: tc.function.arguments ?? ""
|
|
241
384
|
});
|
|
242
|
-
idx++;
|
|
243
385
|
}
|
|
244
386
|
}
|
|
245
387
|
break;
|
|
246
388
|
}
|
|
247
389
|
case "tool": {
|
|
390
|
+
const base = deriveMessageId("tool", "tool-result", stringContent(m.content), {
|
|
391
|
+
toolCallId: m.tool_call_id ?? ""
|
|
392
|
+
});
|
|
248
393
|
msgs.push({
|
|
249
|
-
id:
|
|
394
|
+
id: clusters.next(base),
|
|
250
395
|
role: "tool",
|
|
251
396
|
contentType: "tool-result",
|
|
252
397
|
toolCallId: m.tool_call_id ?? "",
|
|
253
398
|
text: stringContent(m.content)
|
|
254
399
|
});
|
|
255
|
-
idx++;
|
|
256
400
|
break;
|
|
257
401
|
}
|
|
258
402
|
}
|
|
@@ -316,11 +460,11 @@ ${extra}` : extra;
|
|
|
316
460
|
}
|
|
317
461
|
return [{ role: "system", content: extra }, ...messages];
|
|
318
462
|
}
|
|
319
|
-
function
|
|
463
|
+
function conversationSignalOpenai(body, headerValue2) {
|
|
320
464
|
if (headerValue2 && headerValue2.trim()) return headerValue2.trim();
|
|
321
465
|
const firstUser = body.messages.find((m) => m.role === "user");
|
|
322
466
|
const seed = firstUser ? stringContent(firstUser.content).slice(0, 200) : "default";
|
|
323
|
-
return
|
|
467
|
+
return hashId(seed);
|
|
324
468
|
}
|
|
325
469
|
function stringContent(content) {
|
|
326
470
|
if (content == null) return "";
|
|
@@ -330,56 +474,623 @@ function stringContent(content) {
|
|
|
330
474
|
}
|
|
331
475
|
return "";
|
|
332
476
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
477
|
+
|
|
478
|
+
// src/responses.ts
|
|
479
|
+
var OPAQUE_ITEM_TYPES = /* @__PURE__ */ new Set([
|
|
480
|
+
"additional_tools",
|
|
481
|
+
"reasoning",
|
|
482
|
+
"computer_call",
|
|
483
|
+
"computer_call_output",
|
|
484
|
+
"file_search_call",
|
|
485
|
+
"web_search_call",
|
|
486
|
+
"image_generation_call",
|
|
487
|
+
"code_interpreter_call",
|
|
488
|
+
"mcp_list_tools",
|
|
489
|
+
"mcp_call"
|
|
490
|
+
]);
|
|
491
|
+
function isOpaqueItem(it) {
|
|
492
|
+
return OPAQUE_ITEM_TYPES.has(it.type);
|
|
493
|
+
}
|
|
494
|
+
function partText(p) {
|
|
495
|
+
if (p.type === "input_text" || p.type === "output_text") {
|
|
496
|
+
const t = p.text;
|
|
497
|
+
return typeof t === "string" ? t : "";
|
|
498
|
+
}
|
|
499
|
+
return "";
|
|
500
|
+
}
|
|
501
|
+
function messageContent(c) {
|
|
502
|
+
if (typeof c === "string") return c;
|
|
503
|
+
if (Array.isArray(c)) return c.map(partText).join("\n");
|
|
504
|
+
return "";
|
|
505
|
+
}
|
|
506
|
+
function responsesToCore(body) {
|
|
507
|
+
const msgs = [];
|
|
508
|
+
const systemParts = [];
|
|
509
|
+
const preamble = [];
|
|
510
|
+
const customToolCallIds = /* @__PURE__ */ new Set();
|
|
511
|
+
if (typeof body.instructions === "string" && body.instructions.trim()) {
|
|
512
|
+
systemParts.push(body.instructions);
|
|
513
|
+
}
|
|
514
|
+
let idx = 0;
|
|
515
|
+
const clusters = new ClusterCounter();
|
|
516
|
+
const items = Array.isArray(body.input) ? body.input : [];
|
|
517
|
+
if (typeof body.input === "string") {
|
|
518
|
+
const base = deriveMessageId("user", "text", body.input);
|
|
519
|
+
msgs.push({ id: clusters.next(base), role: "user", contentType: "text", text: body.input });
|
|
520
|
+
idx++;
|
|
521
|
+
return { msgs, systemParts, preamble, customToolCallIds };
|
|
522
|
+
}
|
|
523
|
+
for (const it of items) {
|
|
524
|
+
if (isOpaqueItem(it)) {
|
|
525
|
+
preamble.push(it);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
switch (it.type) {
|
|
529
|
+
case "message": {
|
|
530
|
+
const m = it;
|
|
531
|
+
const text = messageContent(m.content);
|
|
532
|
+
if (m.role === "system" || m.role === "developer") {
|
|
533
|
+
systemParts.push(text);
|
|
534
|
+
} else if (m.role === "user") {
|
|
535
|
+
const base = deriveMessageId("user", "text", text);
|
|
536
|
+
msgs.push({ id: clusters.next(base), role: "user", contentType: "text", text });
|
|
537
|
+
idx++;
|
|
538
|
+
} else if (m.role === "assistant") {
|
|
539
|
+
if (text) {
|
|
540
|
+
const base = deriveMessageId("assistant", "text", text);
|
|
541
|
+
msgs.push({ id: clusters.next(base), role: "assistant", contentType: "text", text });
|
|
542
|
+
idx++;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
case "function_call": {
|
|
548
|
+
const fc = it;
|
|
549
|
+
const base = deriveMessageId("assistant", "tool-call", fc.arguments ?? "", {
|
|
550
|
+
toolCallId: fc.call_id,
|
|
551
|
+
toolName: fc.name
|
|
552
|
+
});
|
|
553
|
+
msgs.push({
|
|
554
|
+
id: clusters.next(base),
|
|
555
|
+
role: "assistant",
|
|
556
|
+
contentType: "tool-call",
|
|
557
|
+
toolName: fc.name,
|
|
558
|
+
toolCallId: fc.call_id,
|
|
559
|
+
text: fc.arguments ?? ""
|
|
560
|
+
});
|
|
561
|
+
idx++;
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
case "function_call_output": {
|
|
565
|
+
const fco = it;
|
|
566
|
+
const outText = typeof fco.output === "string" ? fco.output : JSON.stringify(fco.output);
|
|
567
|
+
const base = deriveMessageId("tool", "tool-result", outText, { toolCallId: fco.call_id });
|
|
568
|
+
msgs.push({
|
|
569
|
+
id: clusters.next(base),
|
|
570
|
+
role: "tool",
|
|
571
|
+
contentType: "tool-result",
|
|
572
|
+
toolCallId: fco.call_id,
|
|
573
|
+
text: outText
|
|
574
|
+
});
|
|
575
|
+
idx++;
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
case "custom_tool_call": {
|
|
579
|
+
const ctc = it;
|
|
580
|
+
const callId = ctc.call_id ?? `call_${idx}`;
|
|
581
|
+
customToolCallIds.add(callId);
|
|
582
|
+
const argText = ctc.input ?? ctc.arguments ?? "";
|
|
583
|
+
const base = deriveMessageId("assistant", "tool-call", argText, {
|
|
584
|
+
toolCallId: callId,
|
|
585
|
+
toolName: ctc.name ?? "custom"
|
|
586
|
+
});
|
|
587
|
+
msgs.push({
|
|
588
|
+
id: clusters.next(base),
|
|
589
|
+
role: "assistant",
|
|
590
|
+
contentType: "tool-call",
|
|
591
|
+
toolName: ctc.name ?? "custom",
|
|
592
|
+
toolCallId: callId,
|
|
593
|
+
text: argText
|
|
594
|
+
});
|
|
595
|
+
idx++;
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
case "custom_tool_call_output": {
|
|
599
|
+
const ctco = it;
|
|
600
|
+
const callId = ctco.call_id ?? `call_${idx}`;
|
|
601
|
+
customToolCallIds.add(callId);
|
|
602
|
+
const outText = typeof ctco.output === "string" ? ctco.output : JSON.stringify(ctco.output ?? "");
|
|
603
|
+
const base = deriveMessageId("tool", "tool-result", outText, { toolCallId: callId });
|
|
604
|
+
msgs.push({
|
|
605
|
+
id: clusters.next(base),
|
|
606
|
+
role: "tool",
|
|
607
|
+
contentType: "tool-result",
|
|
608
|
+
toolCallId: callId,
|
|
609
|
+
text: outText
|
|
610
|
+
});
|
|
611
|
+
idx++;
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
default:
|
|
615
|
+
preamble.push(it);
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return { msgs, systemParts, preamble, customToolCallIds };
|
|
620
|
+
}
|
|
621
|
+
function coreToResponses(messages, customToolCallIds = /* @__PURE__ */ new Set()) {
|
|
622
|
+
const out = [];
|
|
623
|
+
for (const m of messages) {
|
|
624
|
+
if (m.role === "system") {
|
|
625
|
+
out.push({ type: "message", role: "developer", content: m.text ?? "" });
|
|
626
|
+
} else if (m.role === "user") {
|
|
627
|
+
out.push({ type: "message", role: "user", content: m.text ?? "" });
|
|
628
|
+
} else if (m.role === "assistant") {
|
|
629
|
+
if (m.contentType === "text") {
|
|
630
|
+
out.push({ type: "message", role: "assistant", content: m.text ?? "" });
|
|
631
|
+
} else if (m.contentType === "tool-call") {
|
|
632
|
+
const callId = m.toolCallId ?? `call_${m.id}`;
|
|
633
|
+
if (customToolCallIds.has(callId)) {
|
|
634
|
+
out.push({
|
|
635
|
+
type: "custom_tool_call",
|
|
636
|
+
call_id: callId,
|
|
637
|
+
name: m.toolName ?? "unknown",
|
|
638
|
+
input: m.text ?? "",
|
|
639
|
+
status: "completed"
|
|
640
|
+
});
|
|
641
|
+
} else {
|
|
642
|
+
out.push({
|
|
643
|
+
type: "function_call",
|
|
644
|
+
call_id: callId,
|
|
645
|
+
name: m.toolName ?? "unknown",
|
|
646
|
+
arguments: m.text ?? ""
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
} else if (m.role === "tool") {
|
|
651
|
+
const callId = m.toolCallId ?? "";
|
|
652
|
+
if (customToolCallIds.has(callId)) {
|
|
653
|
+
out.push({
|
|
654
|
+
type: "custom_tool_call_output",
|
|
655
|
+
call_id: callId,
|
|
656
|
+
output: m.text ?? ""
|
|
657
|
+
});
|
|
658
|
+
} else {
|
|
659
|
+
out.push({
|
|
660
|
+
type: "function_call_output",
|
|
661
|
+
call_id: callId,
|
|
662
|
+
output: m.text ?? ""
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return out;
|
|
668
|
+
}
|
|
669
|
+
function conversationSignalResponses(body, headerValue2) {
|
|
670
|
+
if (headerValue2 && headerValue2.trim()) return headerValue2.trim();
|
|
671
|
+
if (typeof body.session_id === "string" && body.session_id.length > 0) {
|
|
672
|
+
return `codex-${body.session_id}`;
|
|
338
673
|
}
|
|
339
|
-
|
|
674
|
+
if (typeof body.previous_response_id === "string" && body.previous_response_id.length > 0) {
|
|
675
|
+
return `resp-${body.previous_response_id}`;
|
|
676
|
+
}
|
|
677
|
+
let seed = "default";
|
|
678
|
+
if (Array.isArray(body.input)) {
|
|
679
|
+
const firstUser = body.input.find(
|
|
680
|
+
(i) => i.type === "message" && i.role === "user"
|
|
681
|
+
);
|
|
682
|
+
if (firstUser) seed = messageContent(firstUser.content).slice(0, 200);
|
|
683
|
+
} else if (typeof body.input === "string") {
|
|
684
|
+
seed = body.input.slice(0, 200);
|
|
685
|
+
}
|
|
686
|
+
return hashId(seed);
|
|
340
687
|
}
|
|
341
688
|
|
|
342
689
|
// src/session.ts
|
|
690
|
+
import { createInitialState as createInitialState2 } from "acp-kernel";
|
|
691
|
+
|
|
692
|
+
// src/persist.ts
|
|
693
|
+
import { promises as fs } from "fs";
|
|
694
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
695
|
+
import { createHash as createHash2 } from "crypto";
|
|
696
|
+
import * as path2 from "path";
|
|
343
697
|
import { createInitialState } from "acp-kernel";
|
|
698
|
+
var PERSIST_VERSION = 1;
|
|
699
|
+
function mergeState(parsed) {
|
|
700
|
+
const fresh = createInitialState();
|
|
701
|
+
return {
|
|
702
|
+
blocks: parsed.blocks ?? fresh.blocks,
|
|
703
|
+
messageRefs: parsed.messageRefs ?? fresh.messageRefs,
|
|
704
|
+
nudge: { ...fresh.nudge, ...parsed.nudge ?? {} },
|
|
705
|
+
stats: { ...fresh.stats, ...parsed.stats ?? {} },
|
|
706
|
+
nextBlockId: parsed.nextBlockId ?? fresh.nextBlockId,
|
|
707
|
+
nextRunId: parsed.nextRunId ?? fresh.nextRunId
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
function hostLabel(upstreamOrigin) {
|
|
711
|
+
if (!upstreamOrigin) return "unknown";
|
|
712
|
+
try {
|
|
713
|
+
const host = new URL(upstreamOrigin).hostname || "unknown";
|
|
714
|
+
return host.replace(/[^a-zA-Z0-9.-]/g, "-").slice(0, 48) || "unknown";
|
|
715
|
+
} catch {
|
|
716
|
+
return "unknown-" + createHash2("sha256").update(upstreamOrigin, "utf8").digest("hex").slice(0, 6);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
function relPathFor(id, protocol, upstreamOrigin) {
|
|
720
|
+
const proto = protocol ?? "_unknown";
|
|
721
|
+
const host = protocol ? hostLabel(upstreamOrigin) + "_" : "";
|
|
722
|
+
return path2.join(proto, `${host}${createHash2("sha256").update(id, "utf8").digest("hex").slice(0, 24)}.json`);
|
|
723
|
+
}
|
|
724
|
+
function legacyFileNameFor(id) {
|
|
725
|
+
return createHash2("sha256").update(id, "utf8").digest("hex").slice(0, 24) + ".json";
|
|
726
|
+
}
|
|
727
|
+
var SessionStore = class {
|
|
728
|
+
dir;
|
|
729
|
+
debounceMs;
|
|
730
|
+
enabled;
|
|
731
|
+
timers = /* @__PURE__ */ new Map();
|
|
732
|
+
/** Monotonic counter for unique temp filenames within a process. */
|
|
733
|
+
tmpSeq = 0;
|
|
734
|
+
log;
|
|
735
|
+
constructor(opts) {
|
|
736
|
+
this.dir = opts?.dir ?? defaultDir();
|
|
737
|
+
this.debounceMs = opts?.debounceMs ?? defaultDebounce();
|
|
738
|
+
this.enabled = (opts?.enabled ?? true) && this.debounceMs >= 0;
|
|
739
|
+
this.log = opts?.log ?? defaultLogger;
|
|
740
|
+
}
|
|
741
|
+
filePath(id, protocol, upstreamOrigin) {
|
|
742
|
+
return path2.join(this.dir, relPathFor(id, protocol, upstreamOrigin));
|
|
743
|
+
}
|
|
744
|
+
/** A unique temp path per write (per process). Two overlapping writes for
|
|
745
|
+
* the same session must not share a temp file, or one rename invalidates
|
|
746
|
+
* the other. */
|
|
747
|
+
tempPath(id) {
|
|
748
|
+
return path2.join(this.dir, `.tmp-${legacyFileNameFor(id)}-${process.pid}-${this.tmpSeq++}`);
|
|
749
|
+
}
|
|
750
|
+
/** Bulk-load every persisted session from disk into a map keyed by the
|
|
751
|
+
* REAL session id (read from the file body, not the filename). Called once
|
|
752
|
+
* at startup before the server accepts traffic. Corrupt individual files
|
|
753
|
+
* are skipped (logged) — one bad file never blocks boot. */
|
|
754
|
+
async loadAll() {
|
|
755
|
+
const out = /* @__PURE__ */ new Map();
|
|
756
|
+
if (!this.enabled) return out;
|
|
757
|
+
try {
|
|
758
|
+
await fs.mkdir(this.dir, { recursive: true });
|
|
759
|
+
} catch {
|
|
760
|
+
return out;
|
|
761
|
+
}
|
|
762
|
+
const files = [];
|
|
763
|
+
const walk = async (dir) => {
|
|
764
|
+
let entries;
|
|
765
|
+
try {
|
|
766
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
767
|
+
} catch {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
for (const e of entries) {
|
|
771
|
+
if (e.name.startsWith(".tmp-")) continue;
|
|
772
|
+
const full = path2.join(dir, e.name);
|
|
773
|
+
if (e.isDirectory()) {
|
|
774
|
+
await walk(full);
|
|
775
|
+
} else if (e.isFile() && e.name.endsWith(".json")) {
|
|
776
|
+
files.push(full);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
await walk(this.dir);
|
|
781
|
+
for (const full of files) {
|
|
782
|
+
const name = path2.basename(full);
|
|
783
|
+
try {
|
|
784
|
+
const parsed = JSON.parse(await fs.readFile(full, "utf8"));
|
|
785
|
+
if (!isValidRecord(parsed)) continue;
|
|
786
|
+
const proto = parsed.protocol;
|
|
787
|
+
const origin = parsed.upstreamOrigin;
|
|
788
|
+
const expectedNamespaced = path2.basename(relPathFor(parsed.id, proto, origin));
|
|
789
|
+
const expectedLegacy = legacyFileNameFor(parsed.id);
|
|
790
|
+
if (name !== expectedNamespaced && name !== expectedLegacy) {
|
|
791
|
+
this.log("warn", `[persist] skipping ${full}: filename does not match body id (expected ${expectedNamespaced})`);
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
out.set(parsed.id, buildSession(parsed));
|
|
795
|
+
} catch (e) {
|
|
796
|
+
this.log("warn", `[persist] skipping corrupt session file ${full}: ${msg(e)}`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return out;
|
|
800
|
+
}
|
|
801
|
+
/** Synchronous reload of a single session. Used on a memory miss (after
|
|
802
|
+
* LRU eviction). Sync fs is acceptable here because a miss is rare and
|
|
803
|
+
* reads a single small file (~1ms). Returns null if missing/corrupt or the
|
|
804
|
+
* body id does not match what we asked for. */
|
|
805
|
+
loadSync(id, meta) {
|
|
806
|
+
if (!this.enabled) return null;
|
|
807
|
+
const candidates = [this.filePath(id, meta?.protocol, meta?.upstreamOrigin)];
|
|
808
|
+
if (meta?.protocol) candidates.push(this.filePath(id));
|
|
809
|
+
for (const file of candidates) {
|
|
810
|
+
if (!existsSync(file)) continue;
|
|
811
|
+
try {
|
|
812
|
+
const parsed = JSON.parse(readFileSync2(file, "utf8"));
|
|
813
|
+
if (!isValidRecord(parsed) || parsed.id !== id) continue;
|
|
814
|
+
return buildSession(parsed);
|
|
815
|
+
} catch (e) {
|
|
816
|
+
this.log("warn", `[persist] failed to load session ${id}: ${msg(e)}`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
/** Schedule a debounced write for a session. Multiple calls within the
|
|
822
|
+
* window coalesce. Safe to call on the hot path. No-op if disabled. */
|
|
823
|
+
scheduleSave(session) {
|
|
824
|
+
if (!this.enabled) return;
|
|
825
|
+
const existing = this.timers.get(session.id);
|
|
826
|
+
if (existing) clearTimeout(existing);
|
|
827
|
+
const timer = setTimeout(() => {
|
|
828
|
+
this.timers.delete(session.id);
|
|
829
|
+
void this.writeNow(session).catch((e) => {
|
|
830
|
+
this.log("error", `[persist] debounced write failed for ${session.id}: ${msg(e)}`);
|
|
831
|
+
});
|
|
832
|
+
}, this.debounceMs);
|
|
833
|
+
timer.unref?.();
|
|
834
|
+
this.timers.set(session.id, timer);
|
|
835
|
+
}
|
|
836
|
+
/** Asynchronously persist a session right now (skips the debounce). Throws
|
|
837
|
+
* on write failure so callers can react (e.g. avoid evicting). */
|
|
838
|
+
async writeNow(session) {
|
|
839
|
+
if (!this.enabled) return;
|
|
840
|
+
const record = buildRecord(session);
|
|
841
|
+
const file = this.filePath(session.id, session.protocol, session.upstreamOrigin);
|
|
842
|
+
try {
|
|
843
|
+
await fs.mkdir(path2.dirname(file), { recursive: true });
|
|
844
|
+
} catch (e) {
|
|
845
|
+
this.log("warn", `[persist] could not create session dir ${this.dir}: ${msg(e)}`);
|
|
846
|
+
}
|
|
847
|
+
const tmp = this.tempPath(session.id);
|
|
848
|
+
const data = JSON.stringify(record);
|
|
849
|
+
await fs.writeFile(tmp, data, "utf8");
|
|
850
|
+
await fs.rename(tmp, file);
|
|
851
|
+
}
|
|
852
|
+
/** Synchronous flush for a single session. Used on memory eviction so a
|
|
853
|
+
* dirty evicted session is not lost. Sync because eviction runs in the
|
|
854
|
+
* sync getSession path; a single small write is acceptable.
|
|
855
|
+
* Returns true on success, false on failure (caller must NOT evict on
|
|
856
|
+
* failure for a never-persisted session or it is lost permanently). */
|
|
857
|
+
flushSync(session) {
|
|
858
|
+
if (!this.enabled) return true;
|
|
859
|
+
const existing = this.timers.get(session.id);
|
|
860
|
+
if (existing) {
|
|
861
|
+
clearTimeout(existing);
|
|
862
|
+
this.timers.delete(session.id);
|
|
863
|
+
}
|
|
864
|
+
const record = buildRecord(session);
|
|
865
|
+
const file = this.filePath(session.id, session.protocol, session.upstreamOrigin);
|
|
866
|
+
try {
|
|
867
|
+
mkdirSync(path2.dirname(file), { recursive: true });
|
|
868
|
+
} catch (e) {
|
|
869
|
+
this.log("warn", `[persist] could not create session dir ${this.dir}: ${msg(e)}`);
|
|
870
|
+
}
|
|
871
|
+
const tmp = this.tempPath(session.id);
|
|
872
|
+
try {
|
|
873
|
+
writeFileSync(tmp, JSON.stringify(record), "utf8");
|
|
874
|
+
renameSync(tmp, file);
|
|
875
|
+
return true;
|
|
876
|
+
} catch (e) {
|
|
877
|
+
this.log("error", `[persist] flushSync FAILED for ${session.id}: ${msg(e)} \u2014 session NOT evicted to prevent loss`);
|
|
878
|
+
try {
|
|
879
|
+
unlinkSync(tmp);
|
|
880
|
+
} catch {
|
|
881
|
+
}
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
/** Flush all dirty sessions with a pending debounce timer. Called on
|
|
886
|
+
* SIGTERM/SIGINT for graceful shutdown. Clears timers first, then writes
|
|
887
|
+
* every session that had a pending write. */
|
|
888
|
+
async flushAll(sessions2) {
|
|
889
|
+
if (!this.enabled) return;
|
|
890
|
+
const dirty = new Set(this.timers.keys());
|
|
891
|
+
for (const timer of this.timers.values()) clearTimeout(timer);
|
|
892
|
+
this.timers.clear();
|
|
893
|
+
const pending = [];
|
|
894
|
+
for (const s of sessions2) {
|
|
895
|
+
if (!dirty.has(s.id)) continue;
|
|
896
|
+
pending.push(
|
|
897
|
+
this.writeNow(s).catch((e) => {
|
|
898
|
+
this.log("error", `[persist] shutdown flush failed for ${s.id}: ${msg(e)}`);
|
|
899
|
+
})
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
await Promise.all(pending);
|
|
903
|
+
}
|
|
904
|
+
/** Whether a write is currently pending (debounce timer armed) for a id. */
|
|
905
|
+
hasPending(id) {
|
|
906
|
+
return this.timers.has(id);
|
|
907
|
+
}
|
|
908
|
+
/** Cancel all pending writes without flushing (e.g. for tests). */
|
|
909
|
+
cancelAll() {
|
|
910
|
+
for (const timer of this.timers.values()) clearTimeout(timer);
|
|
911
|
+
this.timers.clear();
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
function buildRecord(session) {
|
|
915
|
+
return {
|
|
916
|
+
version: PERSIST_VERSION,
|
|
917
|
+
savedAt: Date.now(),
|
|
918
|
+
id: session.id,
|
|
919
|
+
protocol: session.protocol,
|
|
920
|
+
upstreamOrigin: session.upstreamOrigin,
|
|
921
|
+
createdAt: session.createdAt,
|
|
922
|
+
requests: session.requests,
|
|
923
|
+
condensedToolResults: session.condensedToolResults,
|
|
924
|
+
tokensSaved: session.tokensSaved,
|
|
925
|
+
state: session.state,
|
|
926
|
+
blockContents: Object.fromEntries(session.blockContents)
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function buildSession(parsed) {
|
|
930
|
+
const blockContents = /* @__PURE__ */ new Map();
|
|
931
|
+
for (const [bid, content] of Object.entries(parsed.blockContents ?? {})) {
|
|
932
|
+
if (content && typeof content === "object") blockContents.set(bid, content);
|
|
933
|
+
}
|
|
934
|
+
return {
|
|
935
|
+
id: parsed.id,
|
|
936
|
+
protocol: parsed.protocol,
|
|
937
|
+
upstreamOrigin: parsed.upstreamOrigin,
|
|
938
|
+
state: mergeState(parsed.state),
|
|
939
|
+
createdAt: parsed.createdAt ?? Date.now(),
|
|
940
|
+
lastSeen: Date.now(),
|
|
941
|
+
requests: parsed.requests ?? 0,
|
|
942
|
+
condensedToolResults: parsed.condensedToolResults ?? 0,
|
|
943
|
+
tokensSaved: parsed.tokensSaved ?? 0,
|
|
944
|
+
blockContents,
|
|
945
|
+
inFlight: 0,
|
|
946
|
+
persisted: true
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
function isValidRecord(parsed) {
|
|
950
|
+
if (!parsed || typeof parsed !== "object") return false;
|
|
951
|
+
const r = parsed;
|
|
952
|
+
return typeof r.id === "string" && typeof r.state === "object" && r.state !== null && Array.isArray(r.state.blocks);
|
|
953
|
+
}
|
|
954
|
+
function msg(e) {
|
|
955
|
+
return e instanceof Error ? e.message : String(e);
|
|
956
|
+
}
|
|
957
|
+
function defaultDir() {
|
|
958
|
+
return sessionsDir();
|
|
959
|
+
}
|
|
960
|
+
function defaultDebounce() {
|
|
961
|
+
const env = process.env.BILI_PERSIST_DEBOUNCE_MS;
|
|
962
|
+
if (env) {
|
|
963
|
+
const n = Number.parseInt(env, 10);
|
|
964
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
965
|
+
}
|
|
966
|
+
return 500;
|
|
967
|
+
}
|
|
968
|
+
function persistEnabled() {
|
|
969
|
+
const env = process.env.BILI_PERSIST;
|
|
970
|
+
if (env === "0" || env === "false") return false;
|
|
971
|
+
return true;
|
|
972
|
+
}
|
|
973
|
+
function defaultLogger(level, m) {
|
|
974
|
+
console.error(`[${level}] ${m}`);
|
|
975
|
+
}
|
|
976
|
+
var _store = null;
|
|
977
|
+
function getStore() {
|
|
978
|
+
if (!_store) {
|
|
979
|
+
_store = new SessionStore({ enabled: persistEnabled() });
|
|
980
|
+
}
|
|
981
|
+
return _store;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// src/session.ts
|
|
344
985
|
var sessions = /* @__PURE__ */ new Map();
|
|
345
|
-
var MAX_SESSIONS = 256;
|
|
346
|
-
|
|
986
|
+
var MAX_SESSIONS = Number.parseInt(process.env.BILI_MAX_SESSIONS ?? "256", 10) || 256;
|
|
987
|
+
var initialized = false;
|
|
988
|
+
async function initSessions() {
|
|
989
|
+
if (initialized) return;
|
|
990
|
+
initialized = true;
|
|
991
|
+
const store = getStore();
|
|
992
|
+
if (!store.enabled) return;
|
|
993
|
+
const loaded = await store.loadAll();
|
|
994
|
+
if (loaded.size > MAX_SESSIONS) {
|
|
995
|
+
const entries = [...loaded.entries()].sort((a, b) => (b[1].createdAt ?? 0) - (a[1].createdAt ?? 0));
|
|
996
|
+
for (const [id, s] of entries) {
|
|
997
|
+
if (sessions.size >= MAX_SESSIONS) break;
|
|
998
|
+
sessions.set(id, s);
|
|
999
|
+
}
|
|
1000
|
+
} else {
|
|
1001
|
+
for (const [id, s] of loaded) sessions.set(id, s);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
function getSession(id, meta) {
|
|
347
1005
|
const existing = sessions.get(id);
|
|
348
1006
|
if (existing) {
|
|
349
1007
|
existing.lastSeen = Date.now();
|
|
1008
|
+
if (meta?.protocol && !existing.protocol) existing.protocol = meta.protocol;
|
|
1009
|
+
if (meta?.upstreamOrigin && !existing.upstreamOrigin) existing.upstreamOrigin = meta.upstreamOrigin;
|
|
350
1010
|
return existing;
|
|
351
1011
|
}
|
|
1012
|
+
const store = getStore();
|
|
1013
|
+
const reloaded = store.loadSync(id, meta);
|
|
1014
|
+
if (reloaded) {
|
|
1015
|
+
reloaded.lastSeen = Date.now();
|
|
1016
|
+
reloaded.persisted = true;
|
|
1017
|
+
sessions.set(id, reloaded);
|
|
1018
|
+
return reloaded;
|
|
1019
|
+
}
|
|
352
1020
|
if (sessions.size >= MAX_SESSIONS) evictOldest();
|
|
353
1021
|
const session = {
|
|
354
1022
|
id,
|
|
355
|
-
|
|
1023
|
+
protocol: meta?.protocol,
|
|
1024
|
+
upstreamOrigin: meta?.upstreamOrigin,
|
|
1025
|
+
state: createInitialState2(),
|
|
356
1026
|
createdAt: Date.now(),
|
|
357
1027
|
lastSeen: Date.now(),
|
|
358
1028
|
requests: 0,
|
|
359
1029
|
condensedToolResults: 0,
|
|
360
|
-
tokensSaved: 0
|
|
1030
|
+
tokensSaved: 0,
|
|
1031
|
+
blockContents: /* @__PURE__ */ new Map(),
|
|
1032
|
+
inFlight: 0,
|
|
1033
|
+
persisted: false
|
|
361
1034
|
};
|
|
362
1035
|
sessions.set(id, session);
|
|
363
1036
|
return session;
|
|
364
1037
|
}
|
|
1038
|
+
function acquireInFlight(session) {
|
|
1039
|
+
session.inFlight++;
|
|
1040
|
+
}
|
|
1041
|
+
function releaseInFlight(session) {
|
|
1042
|
+
if (session.inFlight > 0) session.inFlight--;
|
|
1043
|
+
}
|
|
1044
|
+
async function withSessionLock(session, fn) {
|
|
1045
|
+
const prev = session.lockChain ?? Promise.resolve();
|
|
1046
|
+
let release;
|
|
1047
|
+
const done = new Promise((resolve) => {
|
|
1048
|
+
release = resolve;
|
|
1049
|
+
});
|
|
1050
|
+
session.lockChain = prev.then(() => done);
|
|
1051
|
+
await prev;
|
|
1052
|
+
try {
|
|
1053
|
+
return await fn();
|
|
1054
|
+
} finally {
|
|
1055
|
+
release();
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
365
1058
|
function listSessions() {
|
|
366
1059
|
return [...sessions.values()].sort((a, b) => b.lastSeen - a.lastSeen);
|
|
367
1060
|
}
|
|
1061
|
+
function markDirty(session) {
|
|
1062
|
+
getStore().scheduleSave(session);
|
|
1063
|
+
}
|
|
1064
|
+
function cacheBlockContent(session, blockId, content) {
|
|
1065
|
+
session.blockContents.set(blockId, content);
|
|
1066
|
+
}
|
|
368
1067
|
function evictOldest() {
|
|
369
1068
|
let oldestId;
|
|
370
1069
|
let oldestSeen = Infinity;
|
|
371
|
-
for (const [id,
|
|
372
|
-
if (
|
|
373
|
-
|
|
1070
|
+
for (const [id, s2] of sessions) {
|
|
1071
|
+
if (s2.inFlight > 0) continue;
|
|
1072
|
+
if (s2.lastSeen < oldestSeen) {
|
|
1073
|
+
oldestSeen = s2.lastSeen;
|
|
374
1074
|
oldestId = id;
|
|
375
1075
|
}
|
|
376
1076
|
}
|
|
377
|
-
if (oldestId)
|
|
1077
|
+
if (!oldestId) return;
|
|
1078
|
+
const s = sessions.get(oldestId);
|
|
1079
|
+
const ok = getStore().flushSync(s);
|
|
1080
|
+
if (!ok && !s.persisted) {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
sessions.delete(oldestId);
|
|
1084
|
+
}
|
|
1085
|
+
async function flushAllSessions() {
|
|
1086
|
+
await getStore().flushAll(sessions.values());
|
|
378
1087
|
}
|
|
379
1088
|
|
|
380
1089
|
// src/compress-tool.ts
|
|
381
1090
|
import { COMPRESS_PHILOSOPHY, HOW_TO_COMPRESS_RULES } from "acp-kernel";
|
|
382
1091
|
var COMPRESS_TOOL_NAME = "compress";
|
|
1092
|
+
var ACP_TEXT_OPEN = "<acp_compress>";
|
|
1093
|
+
var ACP_TEXT_CLOSE = "</acp_compress>";
|
|
383
1094
|
var COMPRESS_TOOL = {
|
|
384
1095
|
name: COMPRESS_TOOL_NAME,
|
|
385
1096
|
description: "Replace a contiguous range of older conversation with a detailed summary you write. Use when content is genuinely consumed. Batch form: content=[{startId,endId,summary,topic?}].",
|
|
@@ -405,12 +1116,18 @@ var COMPRESS_TOOL = {
|
|
|
405
1116
|
}
|
|
406
1117
|
};
|
|
407
1118
|
function parseCompressInput(input) {
|
|
408
|
-
if (!input || typeof input !== "object")
|
|
1119
|
+
if (!input || typeof input !== "object") {
|
|
1120
|
+
console.error(`[acp-compress-input] rejected: not object (${typeof input})`);
|
|
1121
|
+
return [];
|
|
1122
|
+
}
|
|
409
1123
|
const obj = input;
|
|
410
1124
|
if (Array.isArray(obj.content)) {
|
|
411
|
-
|
|
1125
|
+
const out = obj.content.map((r) => toRange(r)).filter((r) => r !== null);
|
|
1126
|
+
if (out.length === 0) console.error(`[acp-compress-input] content array but 0 valid ranges. keys per item: ${obj.content.map((c) => Object.keys(c ?? {}).join(",")).join(" | ")}`);
|
|
1127
|
+
return out;
|
|
412
1128
|
}
|
|
413
1129
|
const single = toRange(obj);
|
|
1130
|
+
if (!single) console.error(`[acp-compress-input] no content array, single-parse failed. top keys: ${Object.keys(obj).join(",")}`);
|
|
414
1131
|
return single ? [single] : [];
|
|
415
1132
|
}
|
|
416
1133
|
function toRange(r) {
|
|
@@ -463,7 +1180,7 @@ ${HOW_TO_COMPRESS_RULES}
|
|
|
463
1180
|
|
|
464
1181
|
ACP TAGS
|
|
465
1182
|
|
|
466
|
-
Each message in the conversation is annotated with a <acp tokens="2.1K" type="tool:bash">m00175</acp> tag showing its reference ID, approximate token size, and content type.
|
|
1183
|
+
Each message in the conversation is annotated with a <acp tokens="2.1K" type="tool:bash">m00175</acp> tag showing its reference ID, approximate token size, and content type. These tags are system metadata injected by the proxy. NEVER echo, repeat, or reference these XML tags in your responses \u2014 the tags must not appear in your output. Use only the ref ID (e.g. m00005) inside compress calls, never the XML wrapper. The token size is approximate \u2014 treat it as a relative guide, not an exact count.
|
|
467
1184
|
|
|
468
1185
|
TOOLS
|
|
469
1186
|
|
|
@@ -533,6 +1250,36 @@ var ACP_TOOLS_OPENAI = [
|
|
|
533
1250
|
SEARCH_CONTEXT_TOOL_OPENAI,
|
|
534
1251
|
ACP_STATUS_TOOL_OPENAI
|
|
535
1252
|
];
|
|
1253
|
+
var COMPRESS_TOOL_RESPONSES = {
|
|
1254
|
+
type: "function",
|
|
1255
|
+
name: COMPRESS_TOOL_NAME,
|
|
1256
|
+
description: COMPRESS_TOOL.description,
|
|
1257
|
+
parameters: COMPRESS_TOOL_OPENAI.function.parameters
|
|
1258
|
+
};
|
|
1259
|
+
var DECOMPRESS_TOOL_RESPONSES = {
|
|
1260
|
+
type: "function",
|
|
1261
|
+
name: DECOMPRESS_TOOL_OPENAI.function.name,
|
|
1262
|
+
description: DECOMPRESS_TOOL_OPENAI.function.description,
|
|
1263
|
+
parameters: DECOMPRESS_TOOL_OPENAI.function.parameters
|
|
1264
|
+
};
|
|
1265
|
+
var SEARCH_CONTEXT_TOOL_RESPONSES = {
|
|
1266
|
+
type: "function",
|
|
1267
|
+
name: SEARCH_CONTEXT_TOOL_OPENAI.function.name,
|
|
1268
|
+
description: SEARCH_CONTEXT_TOOL_OPENAI.function.description,
|
|
1269
|
+
parameters: SEARCH_CONTEXT_TOOL_OPENAI.function.parameters
|
|
1270
|
+
};
|
|
1271
|
+
var ACP_STATUS_TOOL_RESPONSES = {
|
|
1272
|
+
type: "function",
|
|
1273
|
+
name: ACP_STATUS_TOOL_OPENAI.function.name,
|
|
1274
|
+
description: ACP_STATUS_TOOL_OPENAI.function.description,
|
|
1275
|
+
parameters: ACP_STATUS_TOOL_OPENAI.function.parameters
|
|
1276
|
+
};
|
|
1277
|
+
var ACP_TOOLS_RESPONSES = [
|
|
1278
|
+
COMPRESS_TOOL_RESPONSES,
|
|
1279
|
+
DECOMPRESS_TOOL_RESPONSES,
|
|
1280
|
+
SEARCH_CONTEXT_TOOL_RESPONSES,
|
|
1281
|
+
ACP_STATUS_TOOL_RESPONSES
|
|
1282
|
+
];
|
|
536
1283
|
var PROXY_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
537
1284
|
COMPRESS_TOOL_NAME,
|
|
538
1285
|
DECOMPRESS_TOOL_NAME,
|
|
@@ -540,6 +1287,15 @@ var PROXY_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
540
1287
|
ACP_STATUS_TOOL_NAME
|
|
541
1288
|
]);
|
|
542
1289
|
|
|
1290
|
+
// src/stream.ts
|
|
1291
|
+
import { collectBlockContent } from "acp-kernel";
|
|
1292
|
+
|
|
1293
|
+
// src/sse-util.ts
|
|
1294
|
+
function normalizeSseLineEndings(buf) {
|
|
1295
|
+
if (buf.indexOf("\r") === -1) return buf;
|
|
1296
|
+
return buf.replace(/\r\n|\r/g, "\n");
|
|
1297
|
+
}
|
|
1298
|
+
|
|
543
1299
|
// src/stream.ts
|
|
544
1300
|
var NOOP = /* @__PURE__ */ Symbol("noop");
|
|
545
1301
|
async function* rewriteSseStream(upstream, ctx) {
|
|
@@ -560,6 +1316,7 @@ async function* rewriteSseStream(upstream, ctx) {
|
|
|
560
1316
|
const { done, value } = await reader.read();
|
|
561
1317
|
if (done) break;
|
|
562
1318
|
buf += decoder.decode(value, { stream: true });
|
|
1319
|
+
buf = normalizeSseLineEndings(buf);
|
|
563
1320
|
let idx;
|
|
564
1321
|
while ((idx = buf.indexOf("\n\n")) !== -1) {
|
|
565
1322
|
const rawEvent = buf.slice(0, idx);
|
|
@@ -608,6 +1365,10 @@ function routeEvent(ev, blocks, ctx, markConverted, markRealToolUse, getConverte
|
|
|
608
1365
|
if (typeof partial === "string") st.json += partial;
|
|
609
1366
|
return NOOP;
|
|
610
1367
|
}
|
|
1368
|
+
const dt = d.delta;
|
|
1369
|
+
if (dt?.text && (dt.text.includes("<acp ") || dt.text.includes("</acp"))) {
|
|
1370
|
+
ctx.log(`[warn: tag echo] model emitted <acp tag in text delta: ${dt.text.slice(0, 120).replace(/\n/g, " ")}`);
|
|
1371
|
+
}
|
|
611
1372
|
return emitEvent(ev);
|
|
612
1373
|
}
|
|
613
1374
|
if (t === "content_block_stop") {
|
|
@@ -665,7 +1426,19 @@ function applyRanges(ranges, ctx) {
|
|
|
665
1426
|
state: ctx.session.state,
|
|
666
1427
|
config: ctx.config
|
|
667
1428
|
});
|
|
1429
|
+
const beforeIds = new Set(ctx.session.state.blocks.map((b) => b.blockId));
|
|
668
1430
|
ctx.session.state = res.state;
|
|
1431
|
+
for (const b of res.state.blocks) {
|
|
1432
|
+
if (beforeIds.has(b.blockId)) continue;
|
|
1433
|
+
const full = collectBlockContent(res.state, b, ctx.messages, { full: true });
|
|
1434
|
+
const one = collectBlockContent(res.state, b, ctx.messages, { full: false });
|
|
1435
|
+
if (full.count > 0 || one.count > 0) {
|
|
1436
|
+
cacheBlockContent(ctx.session, b.blockId, {
|
|
1437
|
+
one: { text: one.text, count: one.count },
|
|
1438
|
+
full: { text: full.text, count: full.count }
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
669
1442
|
const r = res.result;
|
|
670
1443
|
const detail = ranges.map((rg) => `${rg.startRef}\u2013${rg.endRef}`).join(", ");
|
|
671
1444
|
if (r.blocksCreated === 0) {
|
|
@@ -674,9 +1447,9 @@ function applyRanges(ranges, ctx) {
|
|
|
674
1447
|
return `[Compression FAILED: ${errs} Do not retry the same range.]`;
|
|
675
1448
|
}
|
|
676
1449
|
const warn = r.warnings.length > 0 ? ` ${r.warnings.join("; ")}` : "";
|
|
677
|
-
const
|
|
678
|
-
ctx.log(`[acp-proxy: ${
|
|
679
|
-
return
|
|
1450
|
+
const msg2 = `[Compressed ${detail} \u2192 ${r.blocksCreated} block(s), ~${r.tokensCompressed} tokens saved.${warn}]`;
|
|
1451
|
+
ctx.log(`[acp-proxy: ${msg2}]`);
|
|
1452
|
+
return msg2;
|
|
680
1453
|
} catch (err) {
|
|
681
1454
|
ctx.log(`[acp-proxy: compress failed: ${String(err)}]`);
|
|
682
1455
|
return `[Compression FAILED: ${String(err)} Do not retry the same range.]`;
|
|
@@ -730,56 +1503,110 @@ function rewriteJsonResponse(body, ctx) {
|
|
|
730
1503
|
}
|
|
731
1504
|
b.content = newContent;
|
|
732
1505
|
if (converted && !sawRealToolUse) b.stop_reason = "end_turn";
|
|
1506
|
+
for (const blk of newContent) {
|
|
1507
|
+
const t = blk.text;
|
|
1508
|
+
if (typeof t === "string" && (t.includes("<acp ") || t.includes("</acp"))) {
|
|
1509
|
+
ctx.log(`[warn: tag echo] non-stream model output contains <acp tag: ${t.slice(0, 120).replace(/\n/g, " ")}`);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
733
1512
|
return body;
|
|
734
1513
|
}
|
|
735
1514
|
|
|
1515
|
+
// src/orphan-gc.ts
|
|
1516
|
+
var ORPHAN_THRESHOLD = 3;
|
|
1517
|
+
var orphanStreaks = /* @__PURE__ */ new WeakMap();
|
|
1518
|
+
function reapOrphanBlocks(session, visible, deactivate) {
|
|
1519
|
+
if (session.state.blocks.length === 0) return { reaped: [] };
|
|
1520
|
+
const presentIds = new Set(visible.map((m) => m.id));
|
|
1521
|
+
const reaped = [];
|
|
1522
|
+
for (const block of session.state.blocks) {
|
|
1523
|
+
if (!block.active) continue;
|
|
1524
|
+
const hasHit = block.effectiveMessageIds.some((id) => presentIds.has(id));
|
|
1525
|
+
if (hasHit) {
|
|
1526
|
+
orphanStreaks.delete(block);
|
|
1527
|
+
continue;
|
|
1528
|
+
}
|
|
1529
|
+
const streak = (orphanStreaks.get(block) ?? 0) + 1;
|
|
1530
|
+
orphanStreaks.set(block, streak);
|
|
1531
|
+
if (streak >= ORPHAN_THRESHOLD) {
|
|
1532
|
+
reaped.push(block.blockId);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
if (reaped.length === 0) return { reaped: [] };
|
|
1536
|
+
session.state = deactivate(session.state, reaped);
|
|
1537
|
+
for (const id of reaped) session.blockContents.delete(id);
|
|
1538
|
+
return { reaped };
|
|
1539
|
+
}
|
|
1540
|
+
|
|
736
1541
|
// src/compress-loop.ts
|
|
737
1542
|
import {
|
|
738
1543
|
buildStatusReport,
|
|
739
|
-
collectBlockContent,
|
|
740
|
-
deactivateBlock,
|
|
741
1544
|
estimateTokensFast
|
|
742
1545
|
} from "acp-kernel";
|
|
743
|
-
|
|
744
|
-
|
|
1546
|
+
|
|
1547
|
+
// src/decompress-shared.ts
|
|
1548
|
+
import {
|
|
1549
|
+
collectBlockContent as collectBlockContent2,
|
|
1550
|
+
deactivateBlock
|
|
1551
|
+
} from "acp-kernel";
|
|
1552
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1553
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
745
1554
|
import { tmpdir } from "os";
|
|
746
|
-
function
|
|
747
|
-
|
|
748
|
-
|
|
1555
|
+
function resolveDecompress(args, ctx) {
|
|
1556
|
+
const rawBlockId = args.blockId;
|
|
1557
|
+
if (typeof rawBlockId !== "string" || rawBlockId.length === 0) {
|
|
1558
|
+
return "[decompress FAILED: blockId is required]";
|
|
1559
|
+
}
|
|
1560
|
+
const blockId = rawBlockId.trim();
|
|
1561
|
+
const block = ctx.core.decompress(blockId, ctx.session.state);
|
|
1562
|
+
if (!block) return `[Block ${blockId} not found]`;
|
|
1563
|
+
const full = args.full === true;
|
|
1564
|
+
const cached = ctx.session.blockContents.get(blockId);
|
|
1565
|
+
let body;
|
|
1566
|
+
let count;
|
|
1567
|
+
if (cached) {
|
|
1568
|
+
const view = full ? cached.full : cached.one;
|
|
1569
|
+
body = view.text;
|
|
1570
|
+
count = view.count;
|
|
1571
|
+
} else {
|
|
1572
|
+
const collected = collectBlockContent2(ctx.session.state, block, ctx.messages, { full });
|
|
1573
|
+
body = collected.text || block.summary;
|
|
1574
|
+
count = collected.count;
|
|
749
1575
|
}
|
|
750
|
-
if (
|
|
751
|
-
const rawBlockId = args.blockId;
|
|
752
|
-
if (typeof rawBlockId !== "string" || rawBlockId.length === 0) {
|
|
753
|
-
return "[decompress FAILED: blockId is required]";
|
|
754
|
-
}
|
|
755
|
-
const blockId = rawBlockId.trim();
|
|
756
|
-
const block = ctx.core.decompress(blockId, ctx.session.state);
|
|
757
|
-
if (!block) return `[Block ${blockId} not found]`;
|
|
758
|
-
const full = args.full === true;
|
|
759
|
-
const collected = collectBlockContent(ctx.session.state, block, ctx.messages, { full });
|
|
1576
|
+
if (count > 0 || cached) {
|
|
760
1577
|
ctx.session.state = deactivateBlock(ctx.session.state, [blockId]);
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1578
|
+
ctx.session.blockContents.delete(blockId);
|
|
1579
|
+
}
|
|
1580
|
+
const header = `[Restored block ${blockId} \u2014 ${count} item(s)${full ? ", full" : ""}]`;
|
|
1581
|
+
const safeBlockId = blockId.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
1582
|
+
const outPath = body.length > 1e4 ? join2(tmpdir(), `acp-decompress-${safeBlockId}-${Date.now()}.txt`) : null;
|
|
1583
|
+
if (outPath) {
|
|
1584
|
+
try {
|
|
1585
|
+
mkdirSync2(dirname2(outPath), { recursive: true });
|
|
1586
|
+
writeFileSync2(outPath, body, "utf8");
|
|
1587
|
+
return `${header}
|
|
770
1588
|
Content (${body.length} chars) written to: ${outPath}
|
|
771
1589
|
Use the read tool to access it.`;
|
|
772
|
-
|
|
773
|
-
|
|
1590
|
+
} catch (e) {
|
|
1591
|
+
return `${header}
|
|
774
1592
|
[Failed to write to ${outPath}: ${String(e)}]
|
|
775
1593
|
${body.slice(0, 4e3)}...`;
|
|
776
|
-
}
|
|
777
1594
|
}
|
|
778
|
-
return `${header}
|
|
779
|
-
${body}`;
|
|
780
1595
|
}
|
|
781
|
-
|
|
782
|
-
|
|
1596
|
+
return `${header}
|
|
1597
|
+
${body}`;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// src/compress-loop.ts
|
|
1601
|
+
function executeProxyTool(toolName, args, ctx) {
|
|
1602
|
+
if (toolName === "compress") {
|
|
1603
|
+
return applyRanges(parseCompressInput(args), ctx);
|
|
1604
|
+
}
|
|
1605
|
+
if (toolName === "decompress") {
|
|
1606
|
+
return resolveDecompress(args, ctx);
|
|
1607
|
+
}
|
|
1608
|
+
if (toolName === "search_context") {
|
|
1609
|
+
const query = typeof args.query === "string" ? args.query : "";
|
|
783
1610
|
if (query.length === 0) return "[search_context FAILED: query is required]";
|
|
784
1611
|
const limit = typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 5;
|
|
785
1612
|
const blocks = ctx.core.search(query, ctx.session.state).slice(0, limit);
|
|
@@ -912,27 +1739,469 @@ ${icon} [ACP] ${inner}
|
|
|
912
1739
|
}
|
|
913
1740
|
async function* compressLoopStream(initialUpstream, ctx, requestBody, requestOptions) {
|
|
914
1741
|
let upstream = initialUpstream;
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1742
|
+
let activeClearTimer = null;
|
|
1743
|
+
try {
|
|
1744
|
+
const model = requestBody.model ?? "unknown";
|
|
1745
|
+
let responseId = `chatcmpl-proxy-${Date.now()}`;
|
|
1746
|
+
const makeBase = () => ({
|
|
1747
|
+
id: responseId,
|
|
1748
|
+
object: "chat.completion.chunk",
|
|
1749
|
+
created: Date.now(),
|
|
1750
|
+
model
|
|
1751
|
+
});
|
|
1752
|
+
let loopCount = 0;
|
|
1753
|
+
for (; ; ) {
|
|
1754
|
+
loopCount++;
|
|
1755
|
+
if (loopCount > 10) {
|
|
1756
|
+
ctx.log("[acp-proxy: compress loop limit (10) reached, forwarding as-is]");
|
|
1757
|
+
yield Buffer.from(buildFinishSse(makeBase(), "stop", null), "utf8");
|
|
1758
|
+
yield Buffer.from("data: [DONE]\n\n", "utf8");
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
const toolCallByIndex = /* @__PURE__ */ new Map();
|
|
1762
|
+
let contentText = "";
|
|
1763
|
+
let finishReason = null;
|
|
1764
|
+
let usage = null;
|
|
1765
|
+
const isFirstRound = loopCount === 1;
|
|
1766
|
+
const reader = upstream.getReader();
|
|
1767
|
+
const decoder = new TextDecoder("utf-8");
|
|
1768
|
+
let sseBuffer = "";
|
|
1769
|
+
try {
|
|
1770
|
+
for (; ; ) {
|
|
1771
|
+
const { done, value } = await reader.read();
|
|
1772
|
+
if (done) break;
|
|
1773
|
+
sseBuffer += decoder.decode(value, { stream: true });
|
|
1774
|
+
sseBuffer = normalizeSseLineEndings(sseBuffer);
|
|
1775
|
+
let sep;
|
|
1776
|
+
while ((sep = sseBuffer.indexOf("\n\n")) >= 0) {
|
|
1777
|
+
const eventStr = sseBuffer.slice(0, sep);
|
|
1778
|
+
sseBuffer = sseBuffer.slice(sep + 2);
|
|
1779
|
+
if (!eventStr.trim()) continue;
|
|
1780
|
+
const d = classifySseEvent(eventStr);
|
|
1781
|
+
if (d.done) {
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
if (isFirstRound) {
|
|
1785
|
+
if (d.yieldChunk) {
|
|
1786
|
+
if (!responseId) {
|
|
1787
|
+
const dataLine = eventStr.split("\n").find((l) => l.startsWith("data:"));
|
|
1788
|
+
if (dataLine) {
|
|
1789
|
+
try {
|
|
1790
|
+
const p = JSON.parse(dataLine.slice(5).trim());
|
|
1791
|
+
if (typeof p.id === "string") responseId = p.id;
|
|
1792
|
+
} catch {
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
yield d.yieldChunk;
|
|
1797
|
+
}
|
|
1798
|
+
} else {
|
|
1799
|
+
if (d.contentDelta) {
|
|
1800
|
+
yield Buffer.from(buildContentSse(responseId, model, d.contentDelta), "utf8");
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (d.contentDelta) contentText += d.contentDelta;
|
|
1804
|
+
if (d.finishReason) finishReason = d.finishReason;
|
|
1805
|
+
if (d.usage !== void 0) usage = d.usage;
|
|
1806
|
+
if (d.toolCalls) {
|
|
1807
|
+
for (const tc of d.toolCalls) {
|
|
1808
|
+
const existing = toolCallByIndex.get(tc.index);
|
|
1809
|
+
if (existing) {
|
|
1810
|
+
if (tc.name) existing.name = tc.name;
|
|
1811
|
+
if (tc.id) existing.id = tc.id;
|
|
1812
|
+
existing.arguments += tc.arguments;
|
|
1813
|
+
} else {
|
|
1814
|
+
toolCallByIndex.set(tc.index, tc);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
sseBuffer += decoder.decode();
|
|
1821
|
+
sseBuffer = normalizeSseLineEndings(sseBuffer);
|
|
1822
|
+
let resSep;
|
|
1823
|
+
while ((resSep = sseBuffer.indexOf("\n\n")) >= 0) {
|
|
1824
|
+
const eventStr = sseBuffer.slice(0, resSep);
|
|
1825
|
+
sseBuffer = sseBuffer.slice(resSep + 2);
|
|
1826
|
+
if (!eventStr.trim()) continue;
|
|
1827
|
+
const d = classifySseEvent(eventStr);
|
|
1828
|
+
if (d.done) continue;
|
|
1829
|
+
if (isFirstRound) {
|
|
1830
|
+
if (d.yieldChunk) yield d.yieldChunk;
|
|
1831
|
+
} else {
|
|
1832
|
+
if (d.contentDelta) yield Buffer.from(buildContentSse(responseId, model, d.contentDelta), "utf8");
|
|
1833
|
+
}
|
|
1834
|
+
if (d.contentDelta) contentText += d.contentDelta;
|
|
1835
|
+
if (d.finishReason) finishReason = d.finishReason;
|
|
1836
|
+
if (d.usage !== void 0) usage = d.usage;
|
|
1837
|
+
if (d.toolCalls) {
|
|
1838
|
+
for (const tc of d.toolCalls) {
|
|
1839
|
+
const existing = toolCallByIndex.get(tc.index);
|
|
1840
|
+
if (existing) {
|
|
1841
|
+
if (tc.name) existing.name = tc.name;
|
|
1842
|
+
if (tc.id) existing.id = tc.id;
|
|
1843
|
+
existing.arguments += tc.arguments;
|
|
1844
|
+
} else {
|
|
1845
|
+
toolCallByIndex.set(tc.index, tc);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
} finally {
|
|
1851
|
+
reader.releaseLock();
|
|
1852
|
+
}
|
|
1853
|
+
const sortedIndices = [...toolCallByIndex.keys()].sort((a, b) => a - b);
|
|
1854
|
+
const toolCalls = sortedIndices.map((i) => {
|
|
1855
|
+
const tc = toolCallByIndex.get(i);
|
|
1856
|
+
return { ...tc, id: tc.id || `call_${tc.index}` };
|
|
1857
|
+
}).filter((tc) => tc.name.length > 0);
|
|
1858
|
+
const proxyCalls = toolCalls.filter((tc) => PROXY_TOOL_NAMES.has(tc.name));
|
|
1859
|
+
const realCalls = toolCalls.filter((tc) => !PROXY_TOOL_NAMES.has(tc.name));
|
|
1860
|
+
const hasOnlyProxy = proxyCalls.length > 0 && realCalls.length === 0;
|
|
1861
|
+
if (!hasOnlyProxy) {
|
|
1862
|
+
for (const tc of realCalls) {
|
|
1863
|
+
yield Buffer.from(buildToolCallSse(makeBase(), tc), "utf8");
|
|
1864
|
+
}
|
|
1865
|
+
const fr = realCalls.length > 0 ? "tool_calls" : finishReason ?? "stop";
|
|
1866
|
+
yield Buffer.from(buildFinishSse(makeBase(), fr, usage), "utf8");
|
|
1867
|
+
yield Buffer.from("data: [DONE]\n\n", "utf8");
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
const names = proxyCalls.map((c) => c.name).join(", ");
|
|
1871
|
+
ctx.log(`[acp-proxy: round ${loopCount} \u2014 ${proxyCalls.length} proxy call(s): ${names}]`);
|
|
1872
|
+
const messages = requestBody.messages ?? [];
|
|
1873
|
+
messages.push({
|
|
1874
|
+
role: "assistant",
|
|
1875
|
+
content: contentText || null,
|
|
1876
|
+
tool_calls: proxyCalls.map((tc) => ({
|
|
1877
|
+
id: tc.id,
|
|
1878
|
+
type: "function",
|
|
1879
|
+
function: { name: tc.name, arguments: tc.arguments }
|
|
1880
|
+
}))
|
|
1881
|
+
});
|
|
1882
|
+
for (const tc of proxyCalls) {
|
|
1883
|
+
let args = {};
|
|
1884
|
+
try {
|
|
1885
|
+
args = JSON.parse(tc.arguments);
|
|
1886
|
+
} catch {
|
|
1887
|
+
args = {};
|
|
1888
|
+
}
|
|
1889
|
+
const result = executeProxyTool(tc.name, args, ctx);
|
|
1890
|
+
const preview = result.length > 120 ? result.slice(0, 120) + "..." : result;
|
|
1891
|
+
ctx.log(`[acp-proxy: ${tc.name} (${tc.id}) \u2192 ${preview.replace(/\n/g, " ")}]`);
|
|
1892
|
+
yield Buffer.from(
|
|
1893
|
+
buildContentSse(responseId, model, buildVisibilityMarker(tc.name, result)),
|
|
1894
|
+
"utf8"
|
|
1895
|
+
);
|
|
1896
|
+
messages.push({
|
|
1897
|
+
role: "tool",
|
|
1898
|
+
tool_call_id: tc.id,
|
|
1899
|
+
content: result
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
requestBody.messages = messages;
|
|
1903
|
+
const { response: resp, clearTimer } = await fetchWithTimeout(requestOptions.url, {
|
|
1904
|
+
method: "POST",
|
|
1905
|
+
headers: requestOptions.headers,
|
|
1906
|
+
body: JSON.stringify(requestBody)
|
|
1907
|
+
});
|
|
1908
|
+
if (!resp.ok || !resp.body) {
|
|
1909
|
+
const errText = await resp.text().catch(() => "upstream error");
|
|
1910
|
+
ctx.log(`[acp-proxy: compress loop upstream error ${resp.status}: ${errText.slice(0, 200)}]`);
|
|
1911
|
+
yield Buffer.from(
|
|
1912
|
+
`data: ${JSON.stringify({
|
|
1913
|
+
...makeBase(),
|
|
1914
|
+
choices: [{
|
|
1915
|
+
index: 0,
|
|
1916
|
+
delta: { content: `
|
|
1917
|
+
[acp-proxy: upstream error ${resp.status}: ${errText.slice(0, 200)}]
|
|
1918
|
+
` },
|
|
1919
|
+
finish_reason: null
|
|
1920
|
+
}]
|
|
1921
|
+
})}
|
|
1922
|
+
|
|
1923
|
+
`,
|
|
1924
|
+
"utf8"
|
|
1925
|
+
);
|
|
1926
|
+
yield Buffer.from(buildFinishSse(makeBase(), "stop", null), "utf8");
|
|
1927
|
+
yield Buffer.from("data: [DONE]\n\n", "utf8");
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
upstream = resp.body;
|
|
1931
|
+
if (activeClearTimer) activeClearTimer();
|
|
1932
|
+
activeClearTimer = clearTimer;
|
|
1933
|
+
}
|
|
1934
|
+
} finally {
|
|
1935
|
+
if (activeClearTimer) {
|
|
1936
|
+
activeClearTimer();
|
|
1937
|
+
activeClearTimer = null;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// src/compress-loop-responses.ts
|
|
1943
|
+
import {
|
|
1944
|
+
buildStatusReport as buildStatusReport2,
|
|
1945
|
+
estimateTokensFast as estimateTokensFast2
|
|
1946
|
+
} from "acp-kernel";
|
|
1947
|
+
var TEXT_PROTOCOL = process.env.ACP_COMPRESS_PROTOCOL === "text";
|
|
1948
|
+
function extractTextTriggers(text) {
|
|
1949
|
+
const calls = [];
|
|
1950
|
+
let clean = "";
|
|
1951
|
+
let i = 0;
|
|
1952
|
+
let n = 0;
|
|
1953
|
+
while (i < text.length) {
|
|
1954
|
+
const open = text.indexOf(ACP_TEXT_OPEN, i);
|
|
1955
|
+
if (open === -1) {
|
|
1956
|
+
clean += text.slice(i);
|
|
1957
|
+
break;
|
|
1958
|
+
}
|
|
1959
|
+
clean += text.slice(i, open);
|
|
1960
|
+
const after = open + ACP_TEXT_OPEN.length;
|
|
1961
|
+
const close = text.indexOf(ACP_TEXT_CLOSE, after);
|
|
1962
|
+
if (close === -1) {
|
|
1963
|
+
clean += text.slice(open);
|
|
1964
|
+
break;
|
|
1965
|
+
}
|
|
1966
|
+
const payload = text.slice(after, close).trim();
|
|
1967
|
+
if (payload) {
|
|
1968
|
+
const stamp = `${Date.now()}_${n++}`;
|
|
1969
|
+
calls.push({
|
|
1970
|
+
itemId: `fc_text_${stamp}`,
|
|
1971
|
+
callId: `call_text_${stamp}`,
|
|
1972
|
+
name: COMPRESS_TOOL_NAME,
|
|
1973
|
+
arguments: payload
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
i = close + ACP_TEXT_CLOSE.length;
|
|
1977
|
+
}
|
|
1978
|
+
return { clean, calls };
|
|
1979
|
+
}
|
|
1980
|
+
function executeProxyTool2(toolName, args, ctx) {
|
|
1981
|
+
if (toolName === "compress") {
|
|
1982
|
+
return applyRanges(parseCompressInput(args), ctx);
|
|
1983
|
+
}
|
|
1984
|
+
if (toolName === "decompress") {
|
|
1985
|
+
return resolveDecompress(args, ctx);
|
|
1986
|
+
}
|
|
1987
|
+
if (toolName === "search_context") {
|
|
1988
|
+
const query = typeof args.query === "string" ? args.query : "";
|
|
1989
|
+
if (query.length === 0) return "[search_context FAILED: query is required]";
|
|
1990
|
+
const limit = typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 5;
|
|
1991
|
+
const blocks = ctx.core.search(query, ctx.session.state).slice(0, limit);
|
|
1992
|
+
if (blocks.length === 0) return `[No blocks matched "${query}"]`;
|
|
1993
|
+
const lines = blocks.map((b) => {
|
|
1994
|
+
const topic = b.topic ?? "(no topic)";
|
|
1995
|
+
const preview = b.summary.length > 200 ? b.summary.slice(0, 200) + "..." : b.summary;
|
|
1996
|
+
return `${b.blockId} (T${b.tier}) "${topic}"
|
|
1997
|
+
${preview}`;
|
|
1998
|
+
});
|
|
1999
|
+
return `Found ${blocks.length} block(s) for "${query}":
|
|
2000
|
+
|
|
2001
|
+
${lines.join("\n\n")}`;
|
|
2002
|
+
}
|
|
2003
|
+
if (toolName === "acp_status") {
|
|
2004
|
+
return buildStatusReport2(ctx.session.state, ctx.messages, estimateTokensFast2);
|
|
2005
|
+
}
|
|
2006
|
+
return `[Unknown proxy tool: ${toolName}]`;
|
|
2007
|
+
}
|
|
2008
|
+
function extractEventType(rawEvent) {
|
|
2009
|
+
for (const l of rawEvent.split("\n")) {
|
|
2010
|
+
if (l.startsWith("event:")) return l.slice(6).trim();
|
|
2011
|
+
}
|
|
2012
|
+
return null;
|
|
2013
|
+
}
|
|
2014
|
+
function extractDataLine(rawEvent) {
|
|
2015
|
+
const parts = [];
|
|
2016
|
+
for (const l of rawEvent.split("\n")) {
|
|
2017
|
+
if (l.startsWith("data:")) {
|
|
2018
|
+
let v = l.slice(5);
|
|
2019
|
+
if (v.startsWith(" ")) v = v.slice(1);
|
|
2020
|
+
parts.push(v);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
return parts.length ? parts.join("\n") : null;
|
|
2024
|
+
}
|
|
2025
|
+
function classifyResponsesSseEvent(eventStr) {
|
|
2026
|
+
const type = extractEventType(eventStr);
|
|
2027
|
+
const dataLine = extractDataLine(eventStr);
|
|
2028
|
+
if (!type || !dataLine) return {};
|
|
2029
|
+
let obj;
|
|
2030
|
+
try {
|
|
2031
|
+
obj = JSON.parse(dataLine);
|
|
2032
|
+
} catch {
|
|
2033
|
+
return {};
|
|
2034
|
+
}
|
|
2035
|
+
const out = {};
|
|
2036
|
+
switch (type) {
|
|
2037
|
+
case "response.created":
|
|
2038
|
+
case "response.in_progress":
|
|
2039
|
+
out.isMeta = true;
|
|
2040
|
+
out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
|
|
2041
|
+
return out;
|
|
2042
|
+
case "response.output_item.added": {
|
|
2043
|
+
const item = obj.item;
|
|
2044
|
+
if (item?.type === "function_call") {
|
|
2045
|
+
const name = typeof item.name === "string" ? item.name : "";
|
|
2046
|
+
out.fcStart = {
|
|
2047
|
+
itemId: typeof item.id === "string" ? item.id : "",
|
|
2048
|
+
callId: typeof item.call_id === "string" ? item.call_id : "",
|
|
2049
|
+
name
|
|
2050
|
+
};
|
|
2051
|
+
return out;
|
|
2052
|
+
}
|
|
2053
|
+
out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
|
|
2054
|
+
return out;
|
|
2055
|
+
}
|
|
2056
|
+
case "response.content_part.added":
|
|
2057
|
+
case "response.content_part.done":
|
|
2058
|
+
case "response.output_text.done":
|
|
2059
|
+
out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
|
|
2060
|
+
return out;
|
|
2061
|
+
case "response.output_text.delta": {
|
|
2062
|
+
const delta = typeof obj.delta === "string" ? obj.delta : "";
|
|
2063
|
+
if (delta) {
|
|
2064
|
+
out.contentDelta = delta;
|
|
2065
|
+
out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
|
|
2066
|
+
}
|
|
2067
|
+
return out;
|
|
2068
|
+
}
|
|
2069
|
+
case "response.function_call_arguments.delta": {
|
|
2070
|
+
const itemId = typeof obj.item_id === "string" ? obj.item_id : "";
|
|
2071
|
+
const delta = typeof obj.delta === "string" ? obj.delta : "";
|
|
2072
|
+
out.fcArgs = { itemId, delta };
|
|
2073
|
+
return out;
|
|
2074
|
+
}
|
|
2075
|
+
case "response.output_item.done": {
|
|
2076
|
+
const item = obj.item;
|
|
2077
|
+
if (item?.type === "function_call") {
|
|
2078
|
+
out.fcDone = { itemId: typeof item.id === "string" ? item.id : "" };
|
|
2079
|
+
return out;
|
|
2080
|
+
}
|
|
2081
|
+
out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
|
|
2082
|
+
return out;
|
|
2083
|
+
}
|
|
2084
|
+
case "response.completed":
|
|
2085
|
+
out.isMeta = true;
|
|
2086
|
+
out.terminal = true;
|
|
2087
|
+
out.terminalKind = "completed";
|
|
2088
|
+
out.responseObj = obj.response ?? null;
|
|
2089
|
+
return out;
|
|
2090
|
+
case "response.incomplete":
|
|
2091
|
+
out.isMeta = true;
|
|
2092
|
+
out.terminal = true;
|
|
2093
|
+
out.terminalKind = "incomplete";
|
|
2094
|
+
out.terminalRaw = eventStr;
|
|
2095
|
+
return out;
|
|
2096
|
+
case "response.failed":
|
|
2097
|
+
case "response.error":
|
|
2098
|
+
out.isMeta = true;
|
|
2099
|
+
out.terminal = true;
|
|
2100
|
+
out.terminalKind = "failed";
|
|
2101
|
+
out.terminalRaw = eventStr;
|
|
2102
|
+
return out;
|
|
2103
|
+
default:
|
|
2104
|
+
out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
|
|
2105
|
+
return out;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
function buildMessageItemSequence(itemId, outputIndex, text) {
|
|
2109
|
+
const item = { type: "message", id: itemId, role: "assistant", content: [] };
|
|
2110
|
+
const part = { type: "output_text", text: "" };
|
|
2111
|
+
const doneItem = { type: "message", id: itemId, role: "assistant", content: [{ type: "output_text", text }] };
|
|
2112
|
+
return [
|
|
2113
|
+
`event: response.output_item.added
|
|
2114
|
+
data: ${JSON.stringify({ type: "response.output_item.added", output_index: outputIndex, item })}
|
|
2115
|
+
|
|
2116
|
+
`,
|
|
2117
|
+
`event: response.content_part.added
|
|
2118
|
+
data: ${JSON.stringify({ type: "response.content_part.added", item_id: itemId, output_index: outputIndex, part })}
|
|
2119
|
+
|
|
2120
|
+
`,
|
|
2121
|
+
`event: response.output_text.delta
|
|
2122
|
+
data: ${JSON.stringify({ type: "response.output_text.delta", item_id: itemId, output_index: outputIndex, delta: text })}
|
|
2123
|
+
|
|
2124
|
+
`,
|
|
2125
|
+
`event: response.output_text.done
|
|
2126
|
+
data: ${JSON.stringify({ type: "response.output_text.done", item_id: itemId, output_index: outputIndex, text })}
|
|
2127
|
+
|
|
2128
|
+
`,
|
|
2129
|
+
`event: response.content_part.done
|
|
2130
|
+
data: ${JSON.stringify({ type: "response.content_part.done", item_id: itemId, output_index: outputIndex, part: { type: "output_text", text } })}
|
|
2131
|
+
|
|
2132
|
+
`,
|
|
2133
|
+
`event: response.output_item.done
|
|
2134
|
+
data: ${JSON.stringify({ type: "response.output_item.done", output_index: outputIndex, item: doneItem })}
|
|
2135
|
+
|
|
2136
|
+
`
|
|
2137
|
+
].join("");
|
|
2138
|
+
}
|
|
2139
|
+
function buildFunctionCallEvents(fc, outputIndex) {
|
|
2140
|
+
return [
|
|
2141
|
+
`event: response.output_item.added
|
|
2142
|
+
data: ${JSON.stringify({
|
|
2143
|
+
type: "response.output_item.added",
|
|
2144
|
+
output_index: outputIndex,
|
|
2145
|
+
item: { type: "function_call", id: fc.itemId, call_id: fc.callId, name: fc.name, arguments: "" }
|
|
2146
|
+
})}
|
|
2147
|
+
|
|
2148
|
+
`,
|
|
2149
|
+
`event: response.function_call_arguments.delta
|
|
2150
|
+
data: ${JSON.stringify({
|
|
2151
|
+
type: "response.function_call_arguments.delta",
|
|
2152
|
+
item_id: fc.itemId,
|
|
2153
|
+
delta: fc.arguments
|
|
2154
|
+
})}
|
|
2155
|
+
|
|
2156
|
+
`,
|
|
2157
|
+
`event: response.function_call_arguments.done
|
|
2158
|
+
data: ${JSON.stringify({
|
|
2159
|
+
type: "response.function_call_arguments.done",
|
|
2160
|
+
item_id: fc.itemId,
|
|
2161
|
+
arguments: fc.arguments
|
|
2162
|
+
})}
|
|
2163
|
+
|
|
2164
|
+
`,
|
|
2165
|
+
`event: response.output_item.done
|
|
2166
|
+
data: ${JSON.stringify({
|
|
2167
|
+
type: "response.output_item.done",
|
|
2168
|
+
output_index: outputIndex,
|
|
2169
|
+
item: { type: "function_call", id: fc.itemId, call_id: fc.callId, name: fc.name, arguments: fc.arguments }
|
|
2170
|
+
})}
|
|
2171
|
+
|
|
2172
|
+
`
|
|
2173
|
+
].join("");
|
|
2174
|
+
}
|
|
2175
|
+
function buildCompleted(responseObj) {
|
|
2176
|
+
const resp = responseObj ?? { id: `resp-proxy-${Date.now()}`, status: "completed", output: [] };
|
|
2177
|
+
return `event: response.completed
|
|
2178
|
+
data: ${JSON.stringify({
|
|
2179
|
+
type: "response.completed",
|
|
2180
|
+
response: resp
|
|
2181
|
+
})}
|
|
2182
|
+
|
|
2183
|
+
`;
|
|
2184
|
+
}
|
|
2185
|
+
async function* compressLoopResponsesStream(initialUpstream, ctx, requestBody, requestOptions) {
|
|
2186
|
+
let upstream = initialUpstream;
|
|
923
2187
|
let loopCount = 0;
|
|
2188
|
+
let responseObj = null;
|
|
2189
|
+
let activeClearTimer = null;
|
|
2190
|
+
let nextOutputIndex = 0;
|
|
924
2191
|
for (; ; ) {
|
|
925
2192
|
loopCount++;
|
|
926
|
-
if (loopCount >
|
|
927
|
-
ctx.log("[acp-proxy: compress loop limit (
|
|
928
|
-
|
|
929
|
-
yield Buffer.from("
|
|
2193
|
+
if (loopCount > 5) {
|
|
2194
|
+
ctx.log("[acp-proxy: responses compress loop limit (5) reached, forwarding completion as-is]");
|
|
2195
|
+
const limItemId = `msg_acp_limit_${Date.now()}`;
|
|
2196
|
+
yield Buffer.from(buildMessageItemSequence(limItemId, nextOutputIndex++, "\n[acp-proxy: compress loop limit reached]\n"), "utf8");
|
|
2197
|
+
yield Buffer.from(buildCompleted(responseObj), "utf8");
|
|
930
2198
|
return;
|
|
931
2199
|
}
|
|
932
|
-
const
|
|
2200
|
+
const fcByItemId = /* @__PURE__ */ new Map();
|
|
933
2201
|
let contentText = "";
|
|
934
|
-
let
|
|
935
|
-
let
|
|
2202
|
+
let completed = false;
|
|
2203
|
+
let terminalKind = null;
|
|
2204
|
+
let terminalRaw = null;
|
|
936
2205
|
const isFirstRound = loopCount === 1;
|
|
937
2206
|
const reader = upstream.getReader();
|
|
938
2207
|
const decoder = new TextDecoder("utf-8");
|
|
@@ -942,156 +2211,179 @@ async function* compressLoopStream(initialUpstream, ctx, requestBody, requestOpt
|
|
|
942
2211
|
const { done, value } = await reader.read();
|
|
943
2212
|
if (done) break;
|
|
944
2213
|
sseBuffer += decoder.decode(value, { stream: true });
|
|
2214
|
+
if (sseBuffer.indexOf("\r") !== -1) sseBuffer = sseBuffer.replace(/\r\n|\r/g, "\n");
|
|
945
2215
|
let sep;
|
|
946
2216
|
while ((sep = sseBuffer.indexOf("\n\n")) >= 0) {
|
|
947
2217
|
const eventStr = sseBuffer.slice(0, sep);
|
|
948
2218
|
sseBuffer = sseBuffer.slice(sep + 2);
|
|
949
2219
|
if (!eventStr.trim()) continue;
|
|
950
|
-
const d =
|
|
951
|
-
if (d.
|
|
952
|
-
|
|
2220
|
+
const d = classifyResponsesSseEvent(eventStr);
|
|
2221
|
+
if (d.yieldChunk && (isFirstRound || !d.isMeta) && !(TEXT_PROTOCOL && !d.isMeta)) {
|
|
2222
|
+
yield d.yieldChunk;
|
|
953
2223
|
}
|
|
954
|
-
if (
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
if (
|
|
970
|
-
|
|
2224
|
+
if (d.contentDelta) contentText += d.contentDelta;
|
|
2225
|
+
if (d.fcStart) {
|
|
2226
|
+
fcByItemId.set(d.fcStart.itemId, {
|
|
2227
|
+
itemId: d.fcStart.itemId,
|
|
2228
|
+
callId: d.fcStart.callId,
|
|
2229
|
+
name: d.fcStart.name,
|
|
2230
|
+
arguments: ""
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
if (d.fcArgs) {
|
|
2234
|
+
const existing = fcByItemId.get(d.fcArgs.itemId);
|
|
2235
|
+
if (existing) existing.arguments += d.fcArgs.delta;
|
|
2236
|
+
}
|
|
2237
|
+
if (d.fcDone) {
|
|
2238
|
+
const existing = fcByItemId.get(d.fcDone.itemId);
|
|
2239
|
+
if (existing && !existing.arguments) {
|
|
2240
|
+
const item = JSON.parse(extractDataLine(eventStr) ?? "{}").item;
|
|
2241
|
+
const args = typeof item?.arguments === "string" ? item.arguments : "";
|
|
2242
|
+
existing.arguments = args;
|
|
971
2243
|
}
|
|
972
2244
|
}
|
|
973
|
-
if (d.
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
2245
|
+
if (d.terminal) {
|
|
2246
|
+
completed = true;
|
|
2247
|
+
terminalKind = d.terminalKind ?? null;
|
|
2248
|
+
terminalRaw = d.terminalRaw ?? null;
|
|
2249
|
+
responseObj = d.responseObj ?? responseObj;
|
|
2250
|
+
const resp2 = d.responseObj ?? {};
|
|
2251
|
+
const usage = resp2.usage;
|
|
2252
|
+
if (usage && d.terminalKind === "completed") {
|
|
2253
|
+
const prompt = usage.input_tokens ?? usage.prompt_tokens ?? "?";
|
|
2254
|
+
const inDet = usage.input_tokens_details;
|
|
2255
|
+
const prDet = usage.prompt_tokens_details;
|
|
2256
|
+
const cached = inDet?.cached_tokens ?? prDet?.cached_tokens ?? "?";
|
|
2257
|
+
const out = usage.output_tokens ?? "?";
|
|
2258
|
+
console.error(`[acp-usage] round ${loopCount} input=${prompt} cached=${cached} output=${out}${cached !== "?" && cached !== 0 && prompt !== "?" ? ` (cache hit ${Math.round(Number(cached) / Number(prompt) * 100)}%)` : ""}`);
|
|
986
2259
|
}
|
|
987
2260
|
}
|
|
988
2261
|
}
|
|
989
2262
|
}
|
|
990
|
-
sseBuffer += decoder.decode();
|
|
991
2263
|
} finally {
|
|
992
2264
|
reader.releaseLock();
|
|
2265
|
+
if (activeClearTimer) {
|
|
2266
|
+
activeClearTimer();
|
|
2267
|
+
activeClearTimer = null;
|
|
2268
|
+
}
|
|
993
2269
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
2270
|
+
if (TEXT_PROTOCOL) {
|
|
2271
|
+
const extracted = extractTextTriggers(contentText);
|
|
2272
|
+
contentText = extracted.clean;
|
|
2273
|
+
for (const c of extracted.calls) {
|
|
2274
|
+
fcByItemId.set(c.itemId, c);
|
|
2275
|
+
}
|
|
2276
|
+
if (contentText.trim()) {
|
|
2277
|
+
const textItemId = `msg_acp_text_r${loopCount}_${Date.now()}`;
|
|
2278
|
+
yield Buffer.from(buildMessageItemSequence(textItemId, nextOutputIndex++, contentText), "utf8");
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
const allCalls = [...fcByItemId.values()].filter((c) => c.name.length > 0);
|
|
2282
|
+
const proxyCalls = allCalls.filter((c) => PROXY_TOOL_NAMES.has(c.name));
|
|
2283
|
+
const realCalls = allCalls.filter((c) => !PROXY_TOOL_NAMES.has(c.name));
|
|
2284
|
+
console.error(`[acp-diag] round ${loopCount} allCalls=[${allCalls.map((c) => c.name).join(",")}] realCalls=[${realCalls.map((c) => c.name).join(",")}] text=${JSON.stringify(contentText.slice(0, 120))}`);
|
|
1001
2285
|
const hasOnlyProxy = proxyCalls.length > 0 && realCalls.length === 0;
|
|
1002
2286
|
if (!hasOnlyProxy) {
|
|
1003
|
-
|
|
1004
|
-
|
|
2287
|
+
let oi = nextOutputIndex;
|
|
2288
|
+
for (const fc of realCalls) {
|
|
2289
|
+
yield Buffer.from(buildFunctionCallEvents(fc, oi), "utf8");
|
|
2290
|
+
oi++;
|
|
1005
2291
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
2292
|
+
nextOutputIndex = oi;
|
|
2293
|
+
if (terminalKind && terminalKind !== "completed" && terminalRaw) {
|
|
2294
|
+
yield Buffer.from(terminalRaw + "\n\n", "utf8");
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
if (!completed && contentText.length === 0 && realCalls.length === 0) {
|
|
2298
|
+
ctx.log("[acp-proxy: responses stream ended without completion]");
|
|
2299
|
+
}
|
|
2300
|
+
yield Buffer.from(buildCompleted(responseObj), "utf8");
|
|
1009
2301
|
return;
|
|
1010
2302
|
}
|
|
1011
2303
|
const names = proxyCalls.map((c) => c.name).join(", ");
|
|
1012
|
-
ctx.log(`[acp-proxy: round ${loopCount} \u2014 ${proxyCalls.length} proxy call(s): ${names}]`);
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
2304
|
+
ctx.log(`[acp-proxy: responses round ${loopCount} \u2014 ${proxyCalls.length} proxy call(s): ${names}]`);
|
|
2305
|
+
const inputItems = Array.isArray(requestBody.input) ? [...requestBody.input] : [];
|
|
2306
|
+
if (contentText) {
|
|
2307
|
+
inputItems.push({
|
|
2308
|
+
type: "message",
|
|
2309
|
+
role: "assistant",
|
|
2310
|
+
content: [{ type: "output_text", text: contentText }]
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
for (const fc of proxyCalls) {
|
|
2314
|
+
inputItems.push({
|
|
2315
|
+
type: "function_call",
|
|
2316
|
+
id: fc.itemId || `fc_${Date.now()}`,
|
|
2317
|
+
call_id: fc.callId || `call_${Date.now()}`,
|
|
2318
|
+
name: fc.name,
|
|
2319
|
+
arguments: fc.arguments
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
for (const fc of proxyCalls) {
|
|
1024
2323
|
let args = {};
|
|
1025
2324
|
try {
|
|
1026
|
-
args = JSON.parse(
|
|
1027
|
-
} catch {
|
|
2325
|
+
args = JSON.parse(fc.arguments);
|
|
2326
|
+
} catch (e) {
|
|
2327
|
+
console.error(`[acp-compress-args] ${fc.name} JSON.parse failed: ${String(e)}. raw arguments (len=${fc.arguments.length}): ${fc.arguments.slice(0, 300)}`);
|
|
1028
2328
|
args = {};
|
|
1029
2329
|
}
|
|
1030
|
-
|
|
2330
|
+
if (fc.name === "compress") {
|
|
2331
|
+
console.error(`[acp-compress-args] compress args parsed: ${JSON.stringify(args).slice(0, 400)}`);
|
|
2332
|
+
}
|
|
2333
|
+
const result = executeProxyTool2(fc.name, args, ctx);
|
|
1031
2334
|
const preview = result.length > 120 ? result.slice(0, 120) + "..." : result;
|
|
1032
|
-
ctx.log(`[acp-proxy: ${
|
|
2335
|
+
ctx.log(`[acp-proxy: responses ${fc.name} (${fc.callId}) \u2192 ${preview.replace(/\n/g, " ")}]`);
|
|
2336
|
+
const markerItemId = `msg_acp_${Date.now()}_${nextOutputIndex}`;
|
|
1033
2337
|
yield Buffer.from(
|
|
1034
|
-
|
|
2338
|
+
buildMessageItemSequence(markerItemId, nextOutputIndex++, buildVisibilityMarker(fc.name, result)),
|
|
1035
2339
|
"utf8"
|
|
1036
2340
|
);
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
2341
|
+
inputItems.push({
|
|
2342
|
+
type: "function_call_output",
|
|
2343
|
+
call_id: fc.callId || `call_${Date.now()}`,
|
|
2344
|
+
output: result
|
|
1041
2345
|
});
|
|
1042
2346
|
}
|
|
1043
|
-
requestBody.
|
|
1044
|
-
|
|
2347
|
+
requestBody.input = inputItems;
|
|
2348
|
+
if (!("stream" in requestBody)) requestBody.stream = true;
|
|
2349
|
+
const { response: resp, clearTimer } = await fetchWithTimeout(requestOptions.url, {
|
|
1045
2350
|
method: "POST",
|
|
1046
2351
|
headers: requestOptions.headers,
|
|
1047
2352
|
body: JSON.stringify(requestBody)
|
|
1048
2353
|
});
|
|
1049
2354
|
if (!resp.ok || !resp.body) {
|
|
2355
|
+
clearTimer();
|
|
1050
2356
|
const errText = await resp.text().catch(() => "upstream error");
|
|
1051
|
-
ctx.log(`[acp-proxy: compress loop upstream error ${resp.status}: ${errText.slice(0, 200)}]`);
|
|
2357
|
+
ctx.log(`[acp-proxy: responses compress loop upstream error ${resp.status}: ${errText.slice(0, 200)}]`);
|
|
2358
|
+
const errItemId = `msg_acp_err_${Date.now()}`;
|
|
1052
2359
|
yield Buffer.from(
|
|
1053
|
-
`
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
index: 0,
|
|
1057
|
-
delta: { content: `
|
|
1058
|
-
[acp-proxy: upstream error ${resp.status}]
|
|
1059
|
-
` },
|
|
1060
|
-
finish_reason: null
|
|
1061
|
-
}]
|
|
1062
|
-
})}
|
|
1063
|
-
|
|
1064
|
-
`,
|
|
2360
|
+
buildMessageItemSequence(errItemId, nextOutputIndex++, `
|
|
2361
|
+
[acp-proxy: upstream error ${resp.status}: ${errText.slice(0, 200)}]
|
|
2362
|
+
`),
|
|
1065
2363
|
"utf8"
|
|
1066
2364
|
);
|
|
1067
|
-
yield Buffer.from(
|
|
1068
|
-
yield Buffer.from("data: [DONE]\n\n", "utf8");
|
|
2365
|
+
yield Buffer.from(buildCompleted(responseObj), "utf8");
|
|
1069
2366
|
return;
|
|
1070
2367
|
}
|
|
1071
2368
|
upstream = resp.body;
|
|
2369
|
+
if (activeClearTimer) activeClearTimer();
|
|
2370
|
+
activeClearTimer = clearTimer;
|
|
1072
2371
|
}
|
|
1073
2372
|
}
|
|
1074
2373
|
|
|
1075
2374
|
// src/stream-openai.ts
|
|
1076
|
-
function safeJsonParse(s) {
|
|
1077
|
-
try {
|
|
1078
|
-
return s ? JSON.parse(s) : {};
|
|
1079
|
-
} catch {
|
|
1080
|
-
return {};
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
2375
|
function rewriteOpenaiJsonResponse(body, ctx) {
|
|
1084
2376
|
if (!body || typeof body !== "object") return body;
|
|
1085
2377
|
const b = body;
|
|
1086
2378
|
const choice = b.choices?.[0];
|
|
1087
|
-
const
|
|
1088
|
-
if (!choice || !
|
|
2379
|
+
const msg2 = choice?.message;
|
|
2380
|
+
if (!choice || !msg2) return body;
|
|
1089
2381
|
let converted = false;
|
|
1090
2382
|
let sawReal = false;
|
|
1091
2383
|
const noteParts = [];
|
|
1092
2384
|
const keepToolCalls = [];
|
|
1093
|
-
const existingText = typeof
|
|
1094
|
-
const toolCalls =
|
|
2385
|
+
const existingText = typeof msg2.content === "string" ? msg2.content : "";
|
|
2386
|
+
const toolCalls = msg2.tool_calls;
|
|
1095
2387
|
if (Array.isArray(toolCalls)) {
|
|
1096
2388
|
for (const tc of toolCalls) {
|
|
1097
2389
|
if (tc.function?.name === COMPRESS_TOOL_NAME) {
|
|
@@ -1103,14 +2395,17 @@ function rewriteOpenaiJsonResponse(body, ctx) {
|
|
|
1103
2395
|
}
|
|
1104
2396
|
}
|
|
1105
2397
|
}
|
|
2398
|
+
if (existingText && (existingText.includes("<acp ") || existingText.includes("</acp"))) {
|
|
2399
|
+
ctx.log(`[warn: tag echo] non-stream openai output contains <acp tag: ${existingText.slice(0, 120).replace(/\n/g, " ")}`);
|
|
2400
|
+
}
|
|
1106
2401
|
if (!converted) return body;
|
|
1107
2402
|
const note = noteParts.join("\n");
|
|
1108
|
-
|
|
2403
|
+
msg2.content = existingText ? `${existingText}
|
|
1109
2404
|
${note}` : note;
|
|
1110
2405
|
if (keepToolCalls.length > 0) {
|
|
1111
|
-
|
|
2406
|
+
msg2.tool_calls = keepToolCalls;
|
|
1112
2407
|
} else {
|
|
1113
|
-
delete
|
|
2408
|
+
delete msg2.tool_calls;
|
|
1114
2409
|
}
|
|
1115
2410
|
if (!sawReal) {
|
|
1116
2411
|
choice.finish_reason = "stop";
|
|
@@ -1118,16 +2413,132 @@ ${note}` : note;
|
|
|
1118
2413
|
return body;
|
|
1119
2414
|
}
|
|
1120
2415
|
|
|
2416
|
+
// src/stream-responses.ts
|
|
2417
|
+
var TEXT_PROTOCOL2 = process.env.ACP_COMPRESS_PROTOCOL === "text";
|
|
2418
|
+
function rewriteResponsesJsonResponse(body, ctx) {
|
|
2419
|
+
if (!body || typeof body !== "object") return body;
|
|
2420
|
+
const b = body;
|
|
2421
|
+
if (!Array.isArray(b.output)) return body;
|
|
2422
|
+
let converted = false;
|
|
2423
|
+
let sawReal = false;
|
|
2424
|
+
const noteParts = [];
|
|
2425
|
+
const keep = [];
|
|
2426
|
+
for (const item of b.output) {
|
|
2427
|
+
if (item.type === "function_call" && item.name === COMPRESS_TOOL_NAME) {
|
|
2428
|
+
converted = true;
|
|
2429
|
+
noteParts.push(applyRanges(parseCompressInput(safeJsonParse(String(item.arguments ?? ""))), ctx));
|
|
2430
|
+
} else {
|
|
2431
|
+
if (item.type === "function_call") sawReal = true;
|
|
2432
|
+
keep.push(item);
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
if (!converted) return body;
|
|
2436
|
+
const note = noteParts.join("\n");
|
|
2437
|
+
if (note) {
|
|
2438
|
+
keep.unshift({
|
|
2439
|
+
type: "message",
|
|
2440
|
+
role: "assistant",
|
|
2441
|
+
content: [{ type: "output_text", text: note }]
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
b.output = keep;
|
|
2445
|
+
if (!sawReal) b.status = "completed";
|
|
2446
|
+
return body;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
// src/stream-error.ts
|
|
2450
|
+
function safeWrite(res, chunk) {
|
|
2451
|
+
try {
|
|
2452
|
+
res.write(chunk);
|
|
2453
|
+
} catch {
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
function emitStreamError(res, protocol, message, log) {
|
|
2457
|
+
const visible = `
|
|
2458
|
+
\u274C [ACP] stream error: ${message}`;
|
|
2459
|
+
log?.(`[acp-proxy: stream aborted mid-response: ${message}]`);
|
|
2460
|
+
try {
|
|
2461
|
+
if (protocol === "openai") {
|
|
2462
|
+
safeWrite(res, `data: ${JSON.stringify({ choices: [{ index: 0, delta: { content: visible }, finish_reason: null }] })}
|
|
2463
|
+
|
|
2464
|
+
`);
|
|
2465
|
+
safeWrite(res, `data: ${JSON.stringify({ choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}
|
|
2466
|
+
|
|
2467
|
+
`);
|
|
2468
|
+
safeWrite(res, "data: [DONE]\n\n");
|
|
2469
|
+
} else if (protocol === "responses") {
|
|
2470
|
+
safeWrite(res, `event: response.output_text.delta
|
|
2471
|
+
data: ${JSON.stringify({ type: "response.output_text.delta", delta: visible })}
|
|
2472
|
+
|
|
2473
|
+
`);
|
|
2474
|
+
safeWrite(res, `event: response.completed
|
|
2475
|
+
data: ${JSON.stringify({ type: "response.completed", response: { status: "completed", output: [] } })}
|
|
2476
|
+
|
|
2477
|
+
`);
|
|
2478
|
+
} else {
|
|
2479
|
+
safeWrite(res, `event: content_block_delta
|
|
2480
|
+
data: ${JSON.stringify({ type: "content_block_delta", delta: { type: "text_delta", text: visible } })}
|
|
2481
|
+
|
|
2482
|
+
`);
|
|
2483
|
+
safeWrite(res, `event: message_delta
|
|
2484
|
+
data: ${JSON.stringify({ type: "message_delta", delta: { stop_reason: "end_turn" } })}
|
|
2485
|
+
|
|
2486
|
+
`);
|
|
2487
|
+
safeWrite(res, `event: message_stop
|
|
2488
|
+
data: ${JSON.stringify({ type: "message_stop" })}
|
|
2489
|
+
|
|
2490
|
+
`);
|
|
2491
|
+
}
|
|
2492
|
+
} catch {
|
|
2493
|
+
} finally {
|
|
2494
|
+
try {
|
|
2495
|
+
res.end();
|
|
2496
|
+
} catch {
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
// src/session-id.ts
|
|
2502
|
+
function extractKey(headers) {
|
|
2503
|
+
const auth = headers["authorization"];
|
|
2504
|
+
if (typeof auth === "string" && auth.length > 0) return auth.trim().toLowerCase();
|
|
2505
|
+
const apiKey = headers["x-api-key"];
|
|
2506
|
+
if (typeof apiKey === "string" && apiKey.length > 0) return `key:${apiKey.trim().toLowerCase()}`;
|
|
2507
|
+
return "(no-key)";
|
|
2508
|
+
}
|
|
2509
|
+
function clientConversationHeader(headers) {
|
|
2510
|
+
const names = ["x-session-affinity", "x-acp-session", "x-session-id", "x-opencode-session"];
|
|
2511
|
+
for (const name of names) {
|
|
2512
|
+
const v = headers[name];
|
|
2513
|
+
if (typeof v === "string" && v.trim().length > 0) return v.trim();
|
|
2514
|
+
}
|
|
2515
|
+
return void 0;
|
|
2516
|
+
}
|
|
2517
|
+
function deriveSessionId(headers, protocol, upstream, conversation) {
|
|
2518
|
+
if (!conversation) throw new Error("deriveSessionId: conversation dimension is required (pass the conversationSignal* output)");
|
|
2519
|
+
const key = extractKey(headers);
|
|
2520
|
+
return hashId(`${protocol}|${upstream}|${key}|${conversation}`);
|
|
2521
|
+
}
|
|
2522
|
+
function affinityToken(headers, conversation) {
|
|
2523
|
+
const client = clientConversationHeader(headers);
|
|
2524
|
+
if (client) return client;
|
|
2525
|
+
return `ses_${conversation}`;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
1121
2528
|
// src/server.ts
|
|
1122
2529
|
var UPSTREAM_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
1123
2530
|
"host",
|
|
1124
2531
|
"content-length",
|
|
1125
2532
|
"connection",
|
|
1126
2533
|
"keep-alive",
|
|
1127
|
-
"transfer-encoding"
|
|
2534
|
+
"transfer-encoding",
|
|
2535
|
+
// Node's fetch transparently decodes compressed responses. Do not
|
|
2536
|
+
// forward the upstream encoding marker when the body is rewritten or
|
|
2537
|
+
// streamed from fetch, otherwise clients try to decompress plain bytes.
|
|
2538
|
+
"content-encoding"
|
|
1128
2539
|
]);
|
|
1129
|
-
function resolveUpstream(
|
|
1130
|
-
const names = Object.keys(
|
|
2540
|
+
function resolveUpstream(opts, reqUrl) {
|
|
2541
|
+
const names = Object.keys(opts.routes);
|
|
1131
2542
|
if (names.length === 0) return void 0;
|
|
1132
2543
|
const RESERVED = /* @__PURE__ */ new Set(["v1", "v2", "v4", "chat", "completions", "messages", "models", "api"]);
|
|
1133
2544
|
const sorted = [...names].sort((a, b) => b.length - a.length);
|
|
@@ -1137,75 +2548,165 @@ function resolveUpstream(opts2, reqUrl) {
|
|
|
1137
2548
|
if (RESERVED.has(name.toLowerCase())) continue;
|
|
1138
2549
|
const idx = segments.indexOf(name);
|
|
1139
2550
|
if (idx < 0) continue;
|
|
1140
|
-
const base =
|
|
2551
|
+
const base = opts.routes[name].url.replace(/\/$/, "");
|
|
1141
2552
|
const rest = [...segments.slice(0, idx), ...segments.slice(idx + 1)].join("/");
|
|
1142
2553
|
const rewrittenUrl = base + rest;
|
|
1143
2554
|
return { upstream: base, rewrittenUrl, provider: name };
|
|
1144
2555
|
}
|
|
1145
2556
|
return void 0;
|
|
1146
2557
|
}
|
|
1147
|
-
function startServer(
|
|
2558
|
+
async function startServer(opts) {
|
|
1148
2559
|
const core = createCore();
|
|
1149
|
-
const config =
|
|
1150
|
-
const log = (level,
|
|
1151
|
-
|
|
2560
|
+
const config = opts.kernelConfig;
|
|
2561
|
+
const log = (level, msg2) => logMsg(opts, level, msg2);
|
|
2562
|
+
await initSessions();
|
|
2563
|
+
log("info", `[persist] ${getStore().enabled ? "enabled" : "disabled"}`);
|
|
2564
|
+
const server = http.createServer(async (req, res) => {
|
|
1152
2565
|
try {
|
|
1153
|
-
await handle(req, res,
|
|
2566
|
+
await handle(req, res, opts, core, config, log);
|
|
1154
2567
|
} catch (err) {
|
|
1155
|
-
|
|
2568
|
+
const msg2 = String(err);
|
|
2569
|
+
log("error", msg2);
|
|
1156
2570
|
if (!res.headersSent) {
|
|
1157
|
-
|
|
1158
|
-
res.
|
|
2571
|
+
const status = msg2.includes("exceeds") ? 413 : 502;
|
|
2572
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
2573
|
+
res.end(JSON.stringify({ error: "acp-proxy failure", detail: msg2 }));
|
|
1159
2574
|
} else {
|
|
1160
2575
|
res.end();
|
|
1161
2576
|
}
|
|
1162
2577
|
}
|
|
1163
2578
|
});
|
|
1164
|
-
|
|
2579
|
+
server.listen(opts.port, opts.host, () => {
|
|
1165
2580
|
log(
|
|
1166
2581
|
"info",
|
|
1167
|
-
`acp-proxy listening on http://${
|
|
2582
|
+
`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}`)
|
|
1168
2583
|
);
|
|
1169
2584
|
});
|
|
1170
|
-
|
|
2585
|
+
let shuttingDown = false;
|
|
2586
|
+
const shutdown = (sig) => {
|
|
2587
|
+
if (shuttingDown) return;
|
|
2588
|
+
shuttingDown = true;
|
|
2589
|
+
log("info", `${sig} received \u2014 flushing sessions\u2026`);
|
|
2590
|
+
server.close();
|
|
2591
|
+
void flushAllSessions().finally(() => {
|
|
2592
|
+
process.exit(0);
|
|
2593
|
+
});
|
|
2594
|
+
};
|
|
2595
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
2596
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
2597
|
+
return server;
|
|
2598
|
+
}
|
|
2599
|
+
function applyCondense(messages, opts, session) {
|
|
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;
|
|
1171
2612
|
}
|
|
1172
|
-
async function handle(req, res,
|
|
2613
|
+
async function handle(req, res, opts, core, config, log) {
|
|
1173
2614
|
if (req.method === "GET" && req.url === "/__acp/stats") return sendStats(res);
|
|
1174
2615
|
if (req.method === "GET" && (req.url === "/" || req.url === "/__acp/health")) {
|
|
1175
2616
|
res.writeHead(200, { "content-type": "application/json" });
|
|
1176
|
-
res.end(JSON.stringify({ ok: true, upstream:
|
|
2617
|
+
res.end(JSON.stringify({ ok: true, upstream: opts.upstream }));
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
let bodyBuffer;
|
|
2621
|
+
try {
|
|
2622
|
+
bodyBuffer = await readBody(req);
|
|
2623
|
+
} catch (err) {
|
|
2624
|
+
if (err instanceof BodyTooLargeError) {
|
|
2625
|
+
log("warn", `413: request body exceeds ${err.limit} bytes`);
|
|
2626
|
+
res.writeHead(413, { "content-type": "application/json" });
|
|
2627
|
+
res.end(JSON.stringify({ error: { type: "request_too_large", message: err.message } }));
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
2630
|
+
log("warn", `read body failed: ${String(err)}`);
|
|
2631
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
2632
|
+
res.end(JSON.stringify({ error: { type: "invalid_request", message: String(err) } }));
|
|
1177
2633
|
return;
|
|
1178
2634
|
}
|
|
1179
|
-
const bodyBuffer = await readBody(req);
|
|
1180
2635
|
const url = req.url ?? "";
|
|
1181
|
-
const
|
|
1182
|
-
|
|
2636
|
+
const urlPath = url.split("?", 2)[0];
|
|
2637
|
+
const protocol = req.method === "POST" && bodyBuffer.length > 0 ? urlPath.endsWith("/chat/completions") ? "openai" : urlPath.endsWith("/v1/messages") || urlPath.endsWith("/messages") ? "anthropic" : urlPath.endsWith("/responses") ? "responses" : null : null;
|
|
2638
|
+
const route = resolveUpstream(opts, req.url ?? "");
|
|
2639
|
+
const upstreamOrigin = route ? route.upstream : opts.upstream;
|
|
2640
|
+
let parsed = null;
|
|
1183
2641
|
if (protocol && bodyBuffer.length > 0) {
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
2642
|
+
try {
|
|
2643
|
+
parsed = JSON.parse(bodyBuffer.toString("utf8"));
|
|
2644
|
+
} catch {
|
|
2645
|
+
parsed = null;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
let reqConfig = config;
|
|
2649
|
+
if (parsed && typeof parsed === "object") {
|
|
2650
|
+
const model = parsed.model;
|
|
2651
|
+
if (model) {
|
|
2652
|
+
const limit = resolveContextLimit(opts.routes, route?.provider, model);
|
|
1187
2653
|
if (limit && limit !== config.modelContextLimit) {
|
|
1188
2654
|
reqConfig = { ...config, modelContextLimit: limit };
|
|
1189
2655
|
}
|
|
1190
2656
|
}
|
|
1191
2657
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
2658
|
+
let prepared = null;
|
|
2659
|
+
if (!opts.passthrough && protocol && parsed && typeof parsed === "object") {
|
|
2660
|
+
const sessionHeader = headerValue(req, opts.sessionHeader);
|
|
2661
|
+
const conversation = protocol === "anthropic" ? conversationSignalAnthropic(parsed, sessionHeader) : protocol === "openai" ? conversationSignalOpenai(parsed, sessionHeader) : conversationSignalResponses(parsed, sessionHeader);
|
|
2662
|
+
const sessionId = deriveSessionId(req.headers, protocol, upstreamOrigin, conversation);
|
|
2663
|
+
const session = getSession(sessionId, { protocol, upstreamOrigin });
|
|
2664
|
+
const affinity = affinityToken(req.headers, conversation);
|
|
2665
|
+
await withSessionLock(session, async () => {
|
|
2666
|
+
prepared = protocol === "anthropic" ? prepareAnthropic(parsed, req, opts, core, reqConfig, log, session) : protocol === "openai" ? prepareOpenai(parsed, req, opts, core, reqConfig, log, session) : prepareResponses(parsed, req, opts, core, reqConfig, log, session);
|
|
2667
|
+
acquireInFlight(session);
|
|
2668
|
+
try {
|
|
2669
|
+
await forward(req, res, opts, prepared.body, prepared, core, reqConfig, log, route, affinity);
|
|
2670
|
+
} finally {
|
|
2671
|
+
releaseInFlight(session);
|
|
2672
|
+
}
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2675
|
+
if (!prepared) {
|
|
2676
|
+
if (protocol === null && !opts.passthrough) {
|
|
2677
|
+
log("warn", `unrecognized path ${url} \u2014 not a known protocol (/chat/completions, /v1/messages, /responses); forwarding unchanged`);
|
|
2678
|
+
}
|
|
2679
|
+
await forward(req, res, opts, bodyBuffer, null, core, reqConfig, log, route, void 0);
|
|
2680
|
+
}
|
|
1195
2681
|
}
|
|
1196
|
-
var
|
|
1197
|
-
function
|
|
2682
|
+
var ACP_TAG_MARK = "<acp ";
|
|
2683
|
+
function diagTagSummary(messages, sessionId, strategy) {
|
|
2684
|
+
let textTagged = 0;
|
|
2685
|
+
let toolTagged = 0;
|
|
1198
2686
|
for (const m of messages) {
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
2687
|
+
const hasTag = (m.text ?? "").includes(ACP_TAG_MARK);
|
|
2688
|
+
if (!hasTag) continue;
|
|
2689
|
+
if (m.contentType === "tool-call" || m.contentType === "tool-result") toolTagged++;
|
|
2690
|
+
else textTagged++;
|
|
1202
2691
|
}
|
|
2692
|
+
return `[${sessionId}] processTurn: ${messages.length} msgs, renderTags=${strategy}, ${textTagged} text tagged, ${toolTagged} tool tagged (should be 0 with text-only)`;
|
|
1203
2693
|
}
|
|
1204
|
-
function
|
|
1205
|
-
const
|
|
2694
|
+
function diagNudge(turn, sessionId, tokenCount, limit) {
|
|
2695
|
+
const n = turn.nudge;
|
|
2696
|
+
if (!n) return `[${sessionId}] nudge: unavailable`;
|
|
2697
|
+
const b = n.breakdown ?? {};
|
|
2698
|
+
const pct = limit > 0 ? `${Math.round(tokenCount / limit * 100)}%` : "?";
|
|
2699
|
+
const growth = b["growth"] ?? 0;
|
|
2700
|
+
const floor = b["growthFloor"] ?? 0;
|
|
2701
|
+
const interval = b["nudgeGrowthTokens"] ?? 0;
|
|
2702
|
+
const pendingT1 = b["pendingT1"] ?? 0;
|
|
2703
|
+
const ref = b["growthReference"] ?? 0;
|
|
2704
|
+
const inject = n.shouldInject ? `INJECT T${n.tier ?? "?"}` : "idle";
|
|
2705
|
+
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
|
+
}
|
|
2707
|
+
function prepareAnthropic(parsed, req, opts, core, config, log, session) {
|
|
2708
|
+
const sessionId = session.id;
|
|
1206
2709
|
const stream = parsed.stream === true;
|
|
1207
|
-
const sessionId = deriveSessionId(parsed, headerValue(req, opts2.sessionHeader));
|
|
1208
|
-
const session = getSession(sessionId);
|
|
1209
2710
|
session.requests++;
|
|
1210
2711
|
let processedMessages = [];
|
|
1211
2712
|
let rebuiltMessages = parsed.messages;
|
|
@@ -1213,74 +2714,148 @@ function prepareAnthropic(bodyBuffer, req, opts2, core, config, log) {
|
|
|
1213
2714
|
let toolsOut = parsed.tools;
|
|
1214
2715
|
try {
|
|
1215
2716
|
const { msgs } = anthropicToCore(parsed);
|
|
1216
|
-
const tokenCount =
|
|
1217
|
-
const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount });
|
|
2717
|
+
const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
|
|
2718
|
+
const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: "text-only" });
|
|
1218
2719
|
session.state = turn.state;
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
2720
|
+
log("info", diagTagSummary(turn.messages, sessionId, "text-only"));
|
|
2721
|
+
log("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
|
|
2722
|
+
processedMessages = applyCondense(turn.messages, opts, session);
|
|
2723
|
+
reapOrphanBlocks(session, msgs, deactivateBlock4);
|
|
2724
|
+
rebuiltMessages = coreToAnthropic(processedMessages);
|
|
2725
|
+
systemOut = injectSystem(parsed, opts);
|
|
2726
|
+
if (opts.compress.injectTool) {
|
|
1224
2727
|
toolsOut = injectTool(parsed.tools);
|
|
1225
2728
|
}
|
|
2729
|
+
if (turn.nudge?.shouldInject) {
|
|
2730
|
+
try {
|
|
2731
|
+
const rendered = renderNudgeText(turn.nudge);
|
|
2732
|
+
if (rendered.text) {
|
|
2733
|
+
rebuiltMessages = [...rebuiltMessages, { role: "user", content: rendered.text }];
|
|
2734
|
+
}
|
|
2735
|
+
} catch {
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
1226
2738
|
} catch (err) {
|
|
1227
2739
|
log("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
|
|
1228
2740
|
processedMessages = [];
|
|
1229
2741
|
}
|
|
1230
2742
|
const rebuilt = { ...parsed, messages: rebuiltMessages, system: systemOut, tools: toolsOut };
|
|
1231
|
-
|
|
2743
|
+
markDirty(session);
|
|
2744
|
+
return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "anthropic", stream, compressInjected: opts.compress.injectTool };
|
|
1232
2745
|
}
|
|
1233
|
-
function prepareOpenai(
|
|
1234
|
-
const
|
|
2746
|
+
function prepareOpenai(parsed, req, opts, core, config, log, session) {
|
|
2747
|
+
const sessionId = session.id;
|
|
1235
2748
|
const stream = parsed.stream === true;
|
|
1236
|
-
const sessionId = deriveSessionIdOpenai(parsed, headerValue(req, opts2.sessionHeader));
|
|
1237
|
-
const session = getSession(sessionId);
|
|
1238
2749
|
session.requests++;
|
|
1239
2750
|
let processedMessages = [];
|
|
1240
2751
|
let rebuiltMessages = parsed.messages;
|
|
1241
2752
|
let toolsOut = parsed.tools;
|
|
1242
2753
|
const maxTokens = typeof parsed.max_tokens === "number" ? parsed.max_tokens : 8192;
|
|
1243
2754
|
const isTitleGen = maxTokens <= 200 || parsed.messages.length <= 2;
|
|
1244
|
-
const shouldInject =
|
|
2755
|
+
const shouldInject = opts.compress.injectTool && !isTitleGen;
|
|
1245
2756
|
try {
|
|
1246
2757
|
const { msgs } = openaiToCore(parsed);
|
|
1247
|
-
const tokenCount =
|
|
1248
|
-
const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount });
|
|
2758
|
+
const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
|
|
2759
|
+
const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: "text-only" });
|
|
1249
2760
|
session.state = turn.state;
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
2761
|
+
log("info", diagTagSummary(turn.messages, sessionId, "text-only"));
|
|
2762
|
+
log("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
|
|
2763
|
+
processedMessages = applyCondense(turn.messages, opts, session);
|
|
2764
|
+
reapOrphanBlocks(session, msgs, deactivateBlock4);
|
|
2765
|
+
rebuiltMessages = coreToOpenai(processedMessages);
|
|
1253
2766
|
const sysParts = [];
|
|
1254
2767
|
if (shouldInject) sysParts.push(buildCompressSystemPrompt());
|
|
2768
|
+
rebuiltMessages = injectOpenaiSystem(rebuiltMessages, sysParts);
|
|
2769
|
+
if (shouldInject) {
|
|
2770
|
+
toolsOut = injectOpenaiTool(parsed.tools);
|
|
2771
|
+
}
|
|
1255
2772
|
if (turn.nudge?.shouldInject && shouldInject) {
|
|
1256
2773
|
try {
|
|
1257
2774
|
const rendered = renderNudgeText(turn.nudge);
|
|
1258
|
-
if (rendered.text)
|
|
2775
|
+
if (rendered.text) {
|
|
2776
|
+
rebuiltMessages = [...rebuiltMessages, { role: "user", content: rendered.text }];
|
|
2777
|
+
}
|
|
1259
2778
|
} catch {
|
|
1260
2779
|
}
|
|
1261
2780
|
}
|
|
1262
|
-
rebuiltMessages = injectOpenaiSystem(rebuiltMessages, sysParts);
|
|
1263
|
-
if (shouldInject) {
|
|
1264
|
-
toolsOut = injectOpenaiTool(parsed.tools);
|
|
1265
|
-
}
|
|
1266
2781
|
} catch (err) {
|
|
1267
2782
|
log("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
|
|
1268
2783
|
processedMessages = [];
|
|
1269
2784
|
}
|
|
1270
2785
|
const rebuilt = { ...parsed, messages: rebuiltMessages, tools: toolsOut };
|
|
2786
|
+
markDirty(session);
|
|
1271
2787
|
return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "openai", stream, compressInjected: shouldInject };
|
|
1272
2788
|
}
|
|
1273
|
-
function
|
|
1274
|
-
const
|
|
1275
|
-
const
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
2789
|
+
function prepareResponses(parsed, req, opts, core, config, log, session) {
|
|
2790
|
+
const sessionId = session.id;
|
|
2791
|
+
const stream = parsed.stream === true;
|
|
2792
|
+
session.requests++;
|
|
2793
|
+
let processedMessages = [];
|
|
2794
|
+
let rebuiltInput = parsed.input;
|
|
2795
|
+
let toolsOut = parsed.tools;
|
|
2796
|
+
const shouldInject = opts.compress.injectTool;
|
|
2797
|
+
try {
|
|
2798
|
+
const { msgs, systemParts, preamble, customToolCallIds } = responsesToCore(parsed);
|
|
2799
|
+
if (process.env.ACP_DEBUG) {
|
|
2800
|
+
log("info", `[${sessionId}] input items: ${Array.isArray(parsed.input) ? parsed.input.map((i) => i.type).join(",") : "(string)"}`);
|
|
1282
2801
|
}
|
|
2802
|
+
const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
|
|
2803
|
+
const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: process.env.ACP_RENDER_NONE ? "none" : "text-only" });
|
|
2804
|
+
session.state = turn.state;
|
|
2805
|
+
log("info", diagTagSummary(turn.messages, sessionId, "text-only"));
|
|
2806
|
+
log("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
|
|
2807
|
+
processedMessages = applyCondense(turn.messages, opts, session);
|
|
2808
|
+
reapOrphanBlocks(session, msgs, deactivateBlock4);
|
|
2809
|
+
const conversationItems = coreToResponses(processedMessages, customToolCallIds);
|
|
2810
|
+
if (preamble.length > 0) {
|
|
2811
|
+
log("info", `[${sessionId}] preserved ${preamble.length} opaque preamble item(s): ${preamble.map((p) => p.type).join(",")}`);
|
|
2812
|
+
}
|
|
2813
|
+
const inputItems = [...preamble];
|
|
2814
|
+
if (shouldInject && !process.env.ACP_NO_COMPRESS_PROMPT) {
|
|
2815
|
+
const sysParts = [...systemParts, buildCompressSystemPrompt()];
|
|
2816
|
+
inputItems.push({ type: "message", role: "developer", content: sysParts.join("\n\n---\n\n") });
|
|
2817
|
+
if (!process.env.ACP_NO_INJECT_TOOL) {
|
|
2818
|
+
toolsOut = injectResponsesTool(parsed.tools);
|
|
2819
|
+
}
|
|
2820
|
+
} else if (systemParts.length > 0) {
|
|
2821
|
+
inputItems.push({ type: "message", role: "developer", content: systemParts.join("\n\n---\n\n") });
|
|
2822
|
+
}
|
|
2823
|
+
inputItems.push(...conversationItems);
|
|
2824
|
+
if (process.env.ACP_DEBUG) {
|
|
2825
|
+
const ctcs = conversationItems.filter((i) => i.type === "custom_tool_call").length;
|
|
2826
|
+
const ctcos = conversationItems.filter((i) => i.type === "custom_tool_call_output").length;
|
|
2827
|
+
log("info", `[${sessionId}] rebuilt: msgs=${msgs.length} -> conv=${conversationItems.length} (custom_tool_call=${ctcs} custom_tool_call_output=${ctcos})`);
|
|
2828
|
+
}
|
|
2829
|
+
if (turn.nudge?.shouldInject && shouldInject) {
|
|
2830
|
+
try {
|
|
2831
|
+
const rendered = renderNudgeText(turn.nudge);
|
|
2832
|
+
if (rendered.text) {
|
|
2833
|
+
inputItems.push({ type: "message", role: "user", content: rendered.text });
|
|
2834
|
+
}
|
|
2835
|
+
} catch {
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
rebuiltInput = inputItems;
|
|
2839
|
+
} catch (err) {
|
|
2840
|
+
log("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
|
|
2841
|
+
processedMessages = [];
|
|
1283
2842
|
}
|
|
2843
|
+
const rebuilt = { ...parsed, input: rebuiltInput, tools: toolsOut };
|
|
2844
|
+
if (process.env.ACP_DEBUG) {
|
|
2845
|
+
const fwdTools = (Array.isArray(toolsOut) ? toolsOut : []).map((t) => {
|
|
2846
|
+
const r = t;
|
|
2847
|
+
const sub = Array.isArray(r.tools) ? `(${r.tools.length} sub)` : "";
|
|
2848
|
+
return `${r.type}:${r.name ?? "?"}${sub}`;
|
|
2849
|
+
});
|
|
2850
|
+
log("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
|
+
}
|
|
2852
|
+
markDirty(session);
|
|
2853
|
+
return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "responses", stream, compressInjected: shouldInject };
|
|
2854
|
+
}
|
|
2855
|
+
function injectSystem(parsed, opts) {
|
|
2856
|
+
const baseText = extractSystem(parsed.system);
|
|
2857
|
+
const parts = [];
|
|
2858
|
+
if (opts.compress.injectTool) parts.push(buildCompressSystemPrompt());
|
|
1284
2859
|
if (parts.length === 0) return parsed.system;
|
|
1285
2860
|
const full = baseText ? `${baseText}
|
|
1286
2861
|
|
|
@@ -1302,21 +2877,44 @@ function injectOpenaiTool(tools) {
|
|
|
1302
2877
|
const additions = ACP_TOOLS_OPENAI.filter((t) => !present.has(t.function.name));
|
|
1303
2878
|
return [...tools, ...additions];
|
|
1304
2879
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
2880
|
+
var TEXT_PROTOCOL3 = process.env.ACP_COMPRESS_PROTOCOL === "text";
|
|
2881
|
+
function injectResponsesTool(tools) {
|
|
2882
|
+
if (!Array.isArray(tools)) return [...ACP_TOOLS_RESPONSES];
|
|
2883
|
+
const present = new Set(
|
|
2884
|
+
tools.map((t) => t?.name).filter((n) => typeof n === "string")
|
|
2885
|
+
);
|
|
2886
|
+
const additions = ACP_TOOLS_RESPONSES.filter((t) => !present.has(t.name));
|
|
2887
|
+
return [...tools, ...additions];
|
|
2888
|
+
}
|
|
2889
|
+
async function forward(req, res, opts, body, prepared, core, config, log, route, affinity) {
|
|
2890
|
+
const upstreamUrl = route ? route.rewrittenUrl : opts.upstream + (req.url ?? "");
|
|
1308
2891
|
log("info", `forward ${req.method} ${req.url ?? ""} \u2192 ${upstreamUrl}${route ? ` (${route.provider})` : ""}`);
|
|
1309
|
-
if (
|
|
2892
|
+
if (process.env.ACP_DEBUG && prepared) {
|
|
2893
|
+
const sid = prepared.session.id;
|
|
2894
|
+
const hdrKeys = Object.keys(req.headers);
|
|
2895
|
+
log("info", `[${sid}] client headers: ${hdrKeys.join(",")}`);
|
|
2896
|
+
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
|
+
const v = req.headers[k] ?? req.headers[k.toLowerCase()];
|
|
2898
|
+
if (v) {
|
|
2899
|
+
const s = Array.isArray(v) ? v.join(",") : String(v);
|
|
2900
|
+
const masked = /key|auth|token/i.test(k) ? s.slice(0, 8) + "..." + s.slice(-4) + ` (${s.length} chars)` : s.slice(0, 60);
|
|
2901
|
+
log("info", `[${sid}] client hdr ${k}=${masked}`);
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
if (opts.debug && typeof body === "string") {
|
|
1310
2906
|
try {
|
|
1311
2907
|
const parsed = JSON.parse(body);
|
|
1312
2908
|
const toolNames = (parsed.tools ?? []).map((t) => {
|
|
1313
2909
|
const fn = t.function;
|
|
1314
|
-
return fn?.name ?? "?";
|
|
2910
|
+
return fn?.name ?? t.name ?? "?";
|
|
1315
2911
|
});
|
|
1316
2912
|
log("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}`);
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
2913
|
+
if (process.env.ACP_DUMP_REQ === "1") {
|
|
2914
|
+
const out = `/tmp/acp-proxy-debug-req-${Date.now()}.json`;
|
|
2915
|
+
fs2.writeFileSync(out, body.slice(0, 5e4));
|
|
2916
|
+
log("info", `[debug] forwarded body written to ${out}`);
|
|
2917
|
+
}
|
|
1320
2918
|
} catch {
|
|
1321
2919
|
}
|
|
1322
2920
|
}
|
|
@@ -1325,26 +2923,37 @@ async function forward(req, res, opts2, body, prepared, core, config, log) {
|
|
|
1325
2923
|
if (UPSTREAM_HOP_HEADERS.has(k.toLowerCase()) || v === void 0) continue;
|
|
1326
2924
|
headers[k] = Array.isArray(v) ? v.join(", ") : v;
|
|
1327
2925
|
}
|
|
1328
|
-
headers["host"] = new URL(route ? route.upstream :
|
|
2926
|
+
headers["host"] = new URL(route ? route.upstream : opts.upstream).host;
|
|
2927
|
+
if (affinity && !clientConversationHeader(req.headers)) {
|
|
2928
|
+
headers["x-session-id"] = affinity;
|
|
2929
|
+
}
|
|
1329
2930
|
const init = {
|
|
1330
2931
|
method: req.method ?? "GET",
|
|
1331
2932
|
headers,
|
|
1332
2933
|
body: req.method === "GET" || req.method === "HEAD" ? void 0 : body
|
|
1333
2934
|
};
|
|
1334
|
-
const upstream = await
|
|
2935
|
+
const { response: upstream, clearTimer: clearUpstreamTimer } = await fetchWithTimeout(upstreamUrl, init);
|
|
1335
2936
|
const respHeaders = {};
|
|
1336
2937
|
upstream.headers.forEach((v, k) => {
|
|
1337
2938
|
if (UPSTREAM_HOP_HEADERS.has(k.toLowerCase())) return;
|
|
1338
2939
|
respHeaders[k] = v;
|
|
1339
2940
|
});
|
|
2941
|
+
if (!upstream.ok) {
|
|
2942
|
+
res.writeHead(upstream.status, respHeaders);
|
|
2943
|
+
if (upstream.body) await pipeThrough(upstream.body, res);
|
|
2944
|
+
clearUpstreamTimer();
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
1340
2947
|
res.writeHead(upstream.status, respHeaders);
|
|
1341
2948
|
if (!upstream.body) {
|
|
1342
2949
|
res.end();
|
|
2950
|
+
clearUpstreamTimer();
|
|
1343
2951
|
return;
|
|
1344
2952
|
}
|
|
1345
|
-
const useRewriter = prepared !== null && prepared.processedMessages.length > 0
|
|
2953
|
+
const useRewriter = prepared !== null && prepared.compressInjected && prepared.processedMessages.length > 0;
|
|
1346
2954
|
if (!useRewriter || prepared === null) {
|
|
1347
2955
|
await pipeThrough(upstream.body, res);
|
|
2956
|
+
clearUpstreamTimer();
|
|
1348
2957
|
return;
|
|
1349
2958
|
}
|
|
1350
2959
|
const ctx = {
|
|
@@ -1352,50 +2961,83 @@ async function forward(req, res, opts2, body, prepared, core, config, log) {
|
|
|
1352
2961
|
config,
|
|
1353
2962
|
messages: prepared.processedMessages,
|
|
1354
2963
|
session: prepared.session,
|
|
1355
|
-
log: (
|
|
1356
|
-
debug:
|
|
2964
|
+
log: (msg2) => log("info", `[${prepared.session.id}] ${msg2}`),
|
|
2965
|
+
debug: opts.debug
|
|
1357
2966
|
};
|
|
1358
2967
|
if (prepared.stream) {
|
|
1359
2968
|
let streamToRead = upstream.body;
|
|
1360
2969
|
let dumpRaw;
|
|
1361
|
-
if (
|
|
2970
|
+
if (opts.dumpSse) {
|
|
1362
2971
|
const [a, b] = upstream.body.tee();
|
|
1363
2972
|
streamToRead = a;
|
|
1364
|
-
dumpRaw = dumpStreamToFile(b,
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
2973
|
+
dumpRaw = dumpStreamToFile(b, opts.dumpSse, `${Date.now()}-${prepared.session.id}-raw.sse`);
|
|
2974
|
+
}
|
|
2975
|
+
try {
|
|
2976
|
+
if (prepared.protocol === "openai") {
|
|
2977
|
+
const parsedReq = JSON.parse(typeof body === "string" ? body : body.toString("utf8"));
|
|
2978
|
+
const reqHeaders = {};
|
|
2979
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
2980
|
+
if (k.toLowerCase() === "content-length" || k.toLowerCase() === "host") continue;
|
|
2981
|
+
reqHeaders[k] = v;
|
|
2982
|
+
}
|
|
2983
|
+
reqHeaders["content-type"] = "application/json";
|
|
2984
|
+
const loop = compressLoopStream(
|
|
2985
|
+
streamToRead,
|
|
2986
|
+
{ core, config, messages: prepared.processedMessages, session: prepared.session, log: ctx.log },
|
|
2987
|
+
parsedReq,
|
|
2988
|
+
{ url: upstreamUrl, headers: reqHeaders }
|
|
2989
|
+
);
|
|
2990
|
+
for await (const chunk of loop) {
|
|
2991
|
+
{
|
|
2992
|
+
const s = chunk.toString("utf8");
|
|
2993
|
+
if (s.includes("<acp ") || s.includes("</acp")) {
|
|
2994
|
+
log("warn", `[${prepared.session.id}] tag echo: openai response stream contains <acp tag`);
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
|
|
2998
|
+
}
|
|
2999
|
+
} else if (prepared.protocol === "responses") {
|
|
3000
|
+
const parsedReq = JSON.parse(typeof body === "string" ? body : body.toString("utf8"));
|
|
3001
|
+
const reqHeaders = {};
|
|
3002
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
3003
|
+
if (k.toLowerCase() === "content-length" || k.toLowerCase() === "host") continue;
|
|
3004
|
+
reqHeaders[k] = v;
|
|
3005
|
+
}
|
|
3006
|
+
reqHeaders["content-type"] = "application/json";
|
|
3007
|
+
const loop = compressLoopResponsesStream(
|
|
3008
|
+
streamToRead,
|
|
3009
|
+
{ core, config, messages: prepared.processedMessages, session: prepared.session, log: ctx.log },
|
|
3010
|
+
parsedReq,
|
|
3011
|
+
{ url: upstreamUrl, headers: reqHeaders }
|
|
3012
|
+
);
|
|
3013
|
+
for await (const chunk of loop) {
|
|
3014
|
+
{
|
|
3015
|
+
const s = chunk.toString("utf8");
|
|
3016
|
+
if (s.includes("<acp ") || s.includes("</acp")) {
|
|
3017
|
+
log("warn", `[${prepared.session.id}] tag echo: responses response stream contains <acp tag`);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
|
|
3021
|
+
}
|
|
3022
|
+
} else {
|
|
3023
|
+
const rewriter = rewriteSseStream(streamToRead, ctx);
|
|
3024
|
+
for await (const chunk of rewriter) {
|
|
3025
|
+
{
|
|
3026
|
+
const s = chunk.toString("utf8");
|
|
3027
|
+
if (s.includes("<acp ") || s.includes("</acp")) {
|
|
3028
|
+
log("warn", `[${prepared.session.id}] tag echo: anthropic response stream contains <acp tag`);
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
|
|
3032
|
+
}
|
|
1395
3033
|
}
|
|
3034
|
+
res.end();
|
|
3035
|
+
} catch (e) {
|
|
3036
|
+
emitStreamError(res, prepared.protocol, e?.message ?? String(e), (m) => log("error", `[${prepared.session.id}] ${m}`));
|
|
3037
|
+
} finally {
|
|
3038
|
+
clearUpstreamTimer();
|
|
3039
|
+
if (dumpRaw) await dumpRaw;
|
|
1396
3040
|
}
|
|
1397
|
-
res.end();
|
|
1398
|
-
if (dumpRaw) await dumpRaw;
|
|
1399
3041
|
} else {
|
|
1400
3042
|
const buf = await upstream.arrayBuffer();
|
|
1401
3043
|
const text = Buffer.from(buf).toString("utf8");
|
|
@@ -1403,6 +3045,8 @@ async function forward(req, res, opts2, body, prepared, core, config, log) {
|
|
|
1403
3045
|
const json = JSON.parse(text);
|
|
1404
3046
|
if (prepared.protocol === "openai") {
|
|
1405
3047
|
rewriteOpenaiJsonResponse(json, ctx);
|
|
3048
|
+
} else if (prepared.protocol === "responses") {
|
|
3049
|
+
rewriteResponsesJsonResponse(json, ctx);
|
|
1406
3050
|
} else {
|
|
1407
3051
|
rewriteJsonResponse(json, ctx);
|
|
1408
3052
|
}
|
|
@@ -1410,7 +3054,9 @@ async function forward(req, res, opts2, body, prepared, core, config, log) {
|
|
|
1410
3054
|
} catch {
|
|
1411
3055
|
res.end(text);
|
|
1412
3056
|
}
|
|
3057
|
+
clearUpstreamTimer();
|
|
1413
3058
|
}
|
|
3059
|
+
markDirty(prepared.session);
|
|
1414
3060
|
}
|
|
1415
3061
|
async function pipeThrough(stream, res) {
|
|
1416
3062
|
const reader = stream.getReader();
|
|
@@ -1428,11 +3074,11 @@ async function pipeThrough(stream, res) {
|
|
|
1428
3074
|
}
|
|
1429
3075
|
}
|
|
1430
3076
|
async function dumpStreamToFile(stream, dir, name) {
|
|
1431
|
-
const { mkdirSync:
|
|
1432
|
-
const { join:
|
|
3077
|
+
const { mkdirSync: mkdirSync3, createWriteStream } = await import("fs");
|
|
3078
|
+
const { join: join3 } = await import("path");
|
|
1433
3079
|
try {
|
|
1434
|
-
|
|
1435
|
-
const ws = createWriteStream(
|
|
3080
|
+
mkdirSync3(dir, { recursive: true });
|
|
3081
|
+
const ws = createWriteStream(join3(dir, name));
|
|
1436
3082
|
const reader = stream.getReader();
|
|
1437
3083
|
try {
|
|
1438
3084
|
for (; ; ) {
|
|
@@ -1465,26 +3111,160 @@ function headerValue(req, name) {
|
|
|
1465
3111
|
}
|
|
1466
3112
|
return void 0;
|
|
1467
3113
|
}
|
|
3114
|
+
var BodyTooLargeError = class extends Error {
|
|
3115
|
+
constructor(limit) {
|
|
3116
|
+
super(`request body exceeds ${limit} bytes`);
|
|
3117
|
+
this.limit = limit;
|
|
3118
|
+
this.name = "BodyTooLargeError";
|
|
3119
|
+
}
|
|
3120
|
+
limit;
|
|
3121
|
+
};
|
|
1468
3122
|
function readBody(req) {
|
|
1469
3123
|
return new Promise((resolve, reject) => {
|
|
1470
3124
|
const chunks = [];
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
req.on("
|
|
3125
|
+
let size = 0;
|
|
3126
|
+
let aborted = false;
|
|
3127
|
+
req.on("data", (c) => {
|
|
3128
|
+
if (aborted) return;
|
|
3129
|
+
size += c.length;
|
|
3130
|
+
if (size > MAX_REQUEST_BYTES) {
|
|
3131
|
+
aborted = true;
|
|
3132
|
+
reject(new BodyTooLargeError(MAX_REQUEST_BYTES));
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
chunks.push(c);
|
|
3136
|
+
});
|
|
3137
|
+
req.on("end", () => {
|
|
3138
|
+
if (!aborted) resolve(Buffer.concat(chunks));
|
|
3139
|
+
});
|
|
3140
|
+
req.on("error", (e) => {
|
|
3141
|
+
if (!aborted) reject(e);
|
|
3142
|
+
});
|
|
1474
3143
|
});
|
|
1475
3144
|
}
|
|
1476
|
-
function logMsg(
|
|
1477
|
-
if (!
|
|
3145
|
+
function logMsg(opts, level, msg2) {
|
|
3146
|
+
if (!opts.log) return;
|
|
1478
3147
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1479
|
-
console.error(`${ts} [${level}] ${
|
|
3148
|
+
console.error(`${ts} [${level}] ${msg2}`);
|
|
1480
3149
|
}
|
|
1481
3150
|
|
|
1482
|
-
// src/
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
3151
|
+
// src/cli.ts
|
|
3152
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
3153
|
+
import { fileURLToPath } from "url";
|
|
3154
|
+
import path3 from "path";
|
|
3155
|
+
var VERSION = (() => {
|
|
3156
|
+
try {
|
|
3157
|
+
const here = fileURLToPath(import.meta.url);
|
|
3158
|
+
const pkg = path3.join(path3.dirname(here), "..", "package.json");
|
|
3159
|
+
return JSON.parse(readFileSync3(pkg, "utf8")).version ?? "dev";
|
|
3160
|
+
} catch {
|
|
3161
|
+
return "dev";
|
|
3162
|
+
}
|
|
3163
|
+
})();
|
|
3164
|
+
var HELP = `bili ${VERSION} \u2014 billion-context proxy
|
|
3165
|
+
|
|
3166
|
+
Usage:
|
|
3167
|
+
bili [start] [options] start the proxy (default: reads ${configFile()})
|
|
3168
|
+
bili --version print version
|
|
3169
|
+
bili --help show this help
|
|
3170
|
+
|
|
3171
|
+
Options (override config file / env):
|
|
3172
|
+
--port <N> listen port (default 8787)
|
|
3173
|
+
--host <ADDR> listen host (default 127.0.0.1)
|
|
3174
|
+
--config <FILE> path to config JSON (default: XDG location)
|
|
3175
|
+
--debug verbose logging
|
|
3176
|
+
--passthrough forward without compression
|
|
3177
|
+
--no-passthrough force compression on (overrides config)
|
|
3178
|
+
|
|
3179
|
+
Config: ${configFile()}
|
|
3180
|
+
Set port/host/debug/providers/condense/compress there. See README \xA7Configuration.
|
|
3181
|
+
Env vars (ACP_*, BILI_*) also work and override the file; CLI flags win.
|
|
3182
|
+
|
|
3183
|
+
Docs: https://github.com/ranxianglei/billion-context
|
|
3184
|
+
`;
|
|
3185
|
+
function parseArgs(argv) {
|
|
3186
|
+
const overrides = {};
|
|
3187
|
+
let command = "start";
|
|
3188
|
+
const positional = [];
|
|
3189
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3190
|
+
const a = argv[i];
|
|
3191
|
+
switch (a) {
|
|
3192
|
+
case "--help":
|
|
3193
|
+
case "-h":
|
|
3194
|
+
command = "help";
|
|
3195
|
+
break;
|
|
3196
|
+
case "--version":
|
|
3197
|
+
case "-V":
|
|
3198
|
+
command = "version";
|
|
3199
|
+
break;
|
|
3200
|
+
case "--debug":
|
|
3201
|
+
overrides.ACP_DEBUG = "1";
|
|
3202
|
+
break;
|
|
3203
|
+
case "--passthrough":
|
|
3204
|
+
overrides.ACP_PASSTHROUGH = "1";
|
|
3205
|
+
break;
|
|
3206
|
+
case "--no-passthrough":
|
|
3207
|
+
overrides.ACP_PASSTHROUGH = "0";
|
|
3208
|
+
break;
|
|
3209
|
+
case "--port":
|
|
3210
|
+
case "--host":
|
|
3211
|
+
case "--config": {
|
|
3212
|
+
const val = argv[++i];
|
|
3213
|
+
if (val === void 0) {
|
|
3214
|
+
console.error(`bili: ${a} requires a value`);
|
|
3215
|
+
process.exit(2);
|
|
3216
|
+
}
|
|
3217
|
+
if (a === "--port") overrides.ACP_PORT = val;
|
|
3218
|
+
else if (a === "--host") overrides.ACP_HOST = val;
|
|
3219
|
+
else overrides.BILI_CONFIG_FILE = val;
|
|
3220
|
+
break;
|
|
3221
|
+
}
|
|
3222
|
+
default:
|
|
3223
|
+
if (a.startsWith("--")) {
|
|
3224
|
+
const eq = a.indexOf("=");
|
|
3225
|
+
if (eq > 0) {
|
|
3226
|
+
argv.splice(i, 1, a.slice(0, eq), a.slice(eq + 1));
|
|
3227
|
+
i--;
|
|
3228
|
+
break;
|
|
3229
|
+
}
|
|
3230
|
+
console.error(`bili: unknown option ${a}`);
|
|
3231
|
+
process.exit(2);
|
|
3232
|
+
}
|
|
3233
|
+
positional.push(a);
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
if (positional.length > 0) {
|
|
3237
|
+
const cmd = positional[0];
|
|
3238
|
+
if (cmd === "start") {
|
|
3239
|
+
command = command === "help" || command === "version" ? command : "start";
|
|
3240
|
+
} else {
|
|
3241
|
+
console.error(`bili: unknown command "${cmd}" (try "bili --help")`);
|
|
3242
|
+
process.exit(2);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
return { command, overrides };
|
|
3246
|
+
}
|
|
3247
|
+
async function main() {
|
|
3248
|
+
const { command, overrides } = parseArgs(process.argv.slice(2));
|
|
3249
|
+
if (command === "help") {
|
|
3250
|
+
process.stdout.write(HELP);
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3253
|
+
if (command === "version") {
|
|
3254
|
+
process.stdout.write(VERSION + "\n");
|
|
3255
|
+
return;
|
|
3256
|
+
}
|
|
3257
|
+
for (const [k, v] of Object.entries(overrides)) {
|
|
3258
|
+
if (v !== void 0) process.env[k] = v;
|
|
3259
|
+
}
|
|
3260
|
+
const opts = loadOptions();
|
|
3261
|
+
await startServer(opts);
|
|
1489
3262
|
}
|
|
3263
|
+
main().catch((err) => {
|
|
3264
|
+
console.error("bili: failed to start:", err);
|
|
3265
|
+
process.exit(1);
|
|
3266
|
+
});
|
|
3267
|
+
|
|
3268
|
+
// src/index.ts
|
|
3269
|
+
main();
|
|
1490
3270
|
//# sourceMappingURL=index.js.map
|