billion-context 0.1.0 → 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 +218 -27
- package/dist/index.js +2186 -355
- package/dist/index.js.map +1 -1
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -3,28 +3,105 @@
|
|
|
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
|
}
|
|
43
|
+
var CONTEXT_LIMIT_TABLE = [
|
|
44
|
+
{ match: /^claude-/i, limit: 2e5 },
|
|
45
|
+
{ match: /^gpt-5/i, limit: 4e5 },
|
|
46
|
+
{ match: /^gpt-4\.1/i, limit: 1e6 },
|
|
47
|
+
{ match: /^gpt-4o/i, limit: 128e3 },
|
|
48
|
+
{ match: /^gpt-4-turbo/i, limit: 128e3 },
|
|
49
|
+
{ match: /^o[13]-/i, limit: 2e5 },
|
|
50
|
+
{ match: /^gemini-2\.5/i, limit: 1e6 },
|
|
51
|
+
{ match: /^gemini-1\.5/i, limit: 1e6 },
|
|
52
|
+
{ match: /^glm-4\.6/i, limit: 128e3 },
|
|
53
|
+
{ match: /^glm-5/i, limit: 1e6 },
|
|
54
|
+
{ match: /^glm-/i, limit: 128e3 },
|
|
55
|
+
{ match: /^deepseek/i, limit: 64e3 },
|
|
56
|
+
{ match: /^qwen/i, limit: 128e3 },
|
|
57
|
+
{ match: /^kimi/i, limit: 128e3 },
|
|
58
|
+
{ match: /^llama-/i, limit: 128e3 }
|
|
59
|
+
];
|
|
60
|
+
function lookupContextLimit(model) {
|
|
61
|
+
if (!model) return void 0;
|
|
62
|
+
for (const entry of CONTEXT_LIMIT_TABLE) {
|
|
63
|
+
if (entry.match.test(model)) return entry.limit;
|
|
64
|
+
}
|
|
65
|
+
return void 0;
|
|
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
|
+
}
|
|
13
78
|
function loadOptions(env = process.env) {
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
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(/\/$/, "");
|
|
83
|
+
let routes = {};
|
|
84
|
+
const routesPath = env.ACP_PROVIDERS ?? fileConfig.providersPath ?? "";
|
|
19
85
|
if (routesPath) {
|
|
20
86
|
const parsed = safeReadJson(routesPath);
|
|
21
|
-
if (
|
|
87
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
88
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
89
|
+
const route = parseRouteEntry(v);
|
|
90
|
+
if (route) routes[k] = route;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
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
|
+
}
|
|
22
99
|
}
|
|
23
|
-
const modelContextLimit = parseInt(env.ACP_MODEL_CONTEXT_LIMIT ??
|
|
24
|
-
const enabled = (env.ACP_CONDENSE_ENABLED ?? "1") !== "0";
|
|
25
|
-
const keepRecentToolResults = parseInt(env.ACP_KEEP_RECENT_TOOL_RESULTS ??
|
|
26
|
-
const minCharsToCondense = parseInt(env.ACP_MIN_CHARS_TO_CONDENSE ??
|
|
27
|
-
const maxKeptChars = parseInt(env.ACP_MAX_KEPT_CHARS ??
|
|
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);
|
|
28
105
|
return {
|
|
29
106
|
port: Number.isFinite(port) ? port : 8787,
|
|
30
107
|
host,
|
|
@@ -34,23 +111,83 @@ function loadOptions(env = process.env) {
|
|
|
34
111
|
kernelConfig: defaultConfig(modelContextLimit),
|
|
35
112
|
condense: { enabled, keepRecentToolResults, minCharsToCondense, maxKeptChars },
|
|
36
113
|
compress: {
|
|
37
|
-
injectTool: (env.ACP_COMPRESS_TOOL ?? "1") !== "0",
|
|
38
|
-
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"
|
|
39
116
|
},
|
|
40
|
-
sessionHeader: env.ACP_SESSION_HEADER ?? "x-acp-session",
|
|
41
|
-
log: env.ACP_LOG !== "0",
|
|
42
|
-
debug: (env.ACP_DEBUG ?? "0") === "1",
|
|
43
|
-
dumpSse: env.ACP_DUMP_SSE || void 0,
|
|
44
|
-
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"
|
|
45
122
|
};
|
|
46
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
|
+
}
|
|
47
141
|
|
|
48
142
|
// src/server.ts
|
|
49
143
|
import http from "http";
|
|
50
|
-
import
|
|
51
|
-
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
|
+
};
|
|
52
188
|
|
|
53
189
|
// src/anthropic.ts
|
|
190
|
+
var CONDENSED_TAG = "[acp-proxy: condensed";
|
|
54
191
|
function extractSystem(system) {
|
|
55
192
|
if (!system) return "";
|
|
56
193
|
if (typeof system === "string") return system;
|
|
@@ -65,19 +202,23 @@ function buildSystem(text, original) {
|
|
|
65
202
|
}
|
|
66
203
|
function anthropicToCore(body) {
|
|
67
204
|
const msgs = [];
|
|
68
|
-
|
|
205
|
+
const clusters = new ClusterCounter();
|
|
69
206
|
for (const m of body.messages) {
|
|
70
207
|
const blocks = typeof m.content === "string" ? [{ type: "text", text: m.content }] : m.content;
|
|
71
208
|
for (const b of blocks) {
|
|
72
|
-
const id = `raw-${idx}`;
|
|
73
|
-
idx++;
|
|
74
209
|
switch (b.type) {
|
|
75
|
-
case "text":
|
|
76
|
-
|
|
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 });
|
|
77
213
|
break;
|
|
78
|
-
|
|
214
|
+
}
|
|
215
|
+
case "tool_use": {
|
|
216
|
+
const base = deriveMessageId("assistant", "tool-call", safeStringify(b.input), {
|
|
217
|
+
toolCallId: b.id,
|
|
218
|
+
toolName: b.name
|
|
219
|
+
});
|
|
79
220
|
msgs.push({
|
|
80
|
-
id,
|
|
221
|
+
id: clusters.next(base),
|
|
81
222
|
role: "assistant",
|
|
82
223
|
contentType: "tool-call",
|
|
83
224
|
toolName: b.name,
|
|
@@ -85,10 +226,12 @@ function anthropicToCore(body) {
|
|
|
85
226
|
text: safeStringify(b.input)
|
|
86
227
|
});
|
|
87
228
|
break;
|
|
229
|
+
}
|
|
88
230
|
case "tool_result": {
|
|
89
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 });
|
|
90
233
|
msgs.push({
|
|
91
|
-
id,
|
|
234
|
+
id: clusters.next(base),
|
|
92
235
|
role: "tool",
|
|
93
236
|
contentType: "tool-result",
|
|
94
237
|
toolCallId: b.tool_use_id,
|
|
@@ -96,12 +239,16 @@ function anthropicToCore(body) {
|
|
|
96
239
|
});
|
|
97
240
|
break;
|
|
98
241
|
}
|
|
99
|
-
case "thinking":
|
|
100
|
-
|
|
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 });
|
|
101
245
|
break;
|
|
102
|
-
|
|
103
|
-
|
|
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]" });
|
|
104
250
|
break;
|
|
251
|
+
}
|
|
105
252
|
}
|
|
106
253
|
}
|
|
107
254
|
}
|
|
@@ -149,11 +296,38 @@ function coreToAnthropic(messages) {
|
|
|
149
296
|
flush();
|
|
150
297
|
return out;
|
|
151
298
|
}
|
|
152
|
-
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) {
|
|
153
327
|
if (headerValue2 && headerValue2.trim()) return headerValue2.trim();
|
|
154
328
|
const firstUser = body.messages.find((m) => m.role === "user");
|
|
155
329
|
const seed = firstUser ? JSON.stringify(firstUser.content).slice(0, 200) : "default";
|
|
156
|
-
return
|
|
330
|
+
return hashId(seed);
|
|
157
331
|
}
|
|
158
332
|
function safeStringify(v) {
|
|
159
333
|
try {
|
|
@@ -170,62 +344,59 @@ function safeParse(s) {
|
|
|
170
344
|
return {};
|
|
171
345
|
}
|
|
172
346
|
}
|
|
173
|
-
function hash(s) {
|
|
174
|
-
let h = 2166136261;
|
|
175
|
-
for (let i = 0; i < s.length; i++) {
|
|
176
|
-
h ^= s.charCodeAt(i);
|
|
177
|
-
h = Math.imul(h, 16777619);
|
|
178
|
-
}
|
|
179
|
-
return (h >>> 0).toString(36);
|
|
180
|
-
}
|
|
181
347
|
|
|
182
348
|
// src/openai.ts
|
|
183
349
|
function openaiToCore(body) {
|
|
184
350
|
const msgs = [];
|
|
185
|
-
|
|
351
|
+
const clusters = new ClusterCounter();
|
|
186
352
|
for (const m of body.messages) {
|
|
187
353
|
switch (m.role) {
|
|
188
354
|
case "system":
|
|
189
355
|
case "developer": {
|
|
190
|
-
|
|
191
|
-
|
|
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) });
|
|
192
358
|
break;
|
|
193
359
|
}
|
|
194
360
|
case "user": {
|
|
195
|
-
|
|
196
|
-
|
|
361
|
+
const base = deriveMessageId("user", "text", stringContent(m.content));
|
|
362
|
+
msgs.push({ id: clusters.next(base), role: "user", contentType: "text", text: stringContent(m.content) });
|
|
197
363
|
break;
|
|
198
364
|
}
|
|
199
365
|
case "assistant": {
|
|
200
366
|
const text = stringContent(m.content);
|
|
201
367
|
if (text) {
|
|
202
|
-
|
|
203
|
-
|
|
368
|
+
const base = deriveMessageId("assistant", "text", text);
|
|
369
|
+
msgs.push({ id: clusters.next(base), role: "assistant", contentType: "text", text });
|
|
204
370
|
}
|
|
205
371
|
if (Array.isArray(m.tool_calls)) {
|
|
206
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
|
+
});
|
|
207
377
|
msgs.push({
|
|
208
|
-
id:
|
|
378
|
+
id: clusters.next(base),
|
|
209
379
|
role: "assistant",
|
|
210
380
|
contentType: "tool-call",
|
|
211
381
|
toolName: tc.function.name,
|
|
212
382
|
toolCallId: tc.id,
|
|
213
383
|
text: tc.function.arguments ?? ""
|
|
214
384
|
});
|
|
215
|
-
idx++;
|
|
216
385
|
}
|
|
217
386
|
}
|
|
218
387
|
break;
|
|
219
388
|
}
|
|
220
389
|
case "tool": {
|
|
390
|
+
const base = deriveMessageId("tool", "tool-result", stringContent(m.content), {
|
|
391
|
+
toolCallId: m.tool_call_id ?? ""
|
|
392
|
+
});
|
|
221
393
|
msgs.push({
|
|
222
|
-
id:
|
|
394
|
+
id: clusters.next(base),
|
|
223
395
|
role: "tool",
|
|
224
396
|
contentType: "tool-result",
|
|
225
397
|
toolCallId: m.tool_call_id ?? "",
|
|
226
398
|
text: stringContent(m.content)
|
|
227
399
|
});
|
|
228
|
-
idx++;
|
|
229
400
|
break;
|
|
230
401
|
}
|
|
231
402
|
}
|
|
@@ -289,11 +460,11 @@ ${extra}` : extra;
|
|
|
289
460
|
}
|
|
290
461
|
return [{ role: "system", content: extra }, ...messages];
|
|
291
462
|
}
|
|
292
|
-
function
|
|
463
|
+
function conversationSignalOpenai(body, headerValue2) {
|
|
293
464
|
if (headerValue2 && headerValue2.trim()) return headerValue2.trim();
|
|
294
465
|
const firstUser = body.messages.find((m) => m.role === "user");
|
|
295
466
|
const seed = firstUser ? stringContent(firstUser.content).slice(0, 200) : "default";
|
|
296
|
-
return
|
|
467
|
+
return hashId(seed);
|
|
297
468
|
}
|
|
298
469
|
function stringContent(content) {
|
|
299
470
|
if (content == null) return "";
|
|
@@ -303,56 +474,623 @@ function stringContent(content) {
|
|
|
303
474
|
}
|
|
304
475
|
return "";
|
|
305
476
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
+
}
|
|
311
666
|
}
|
|
312
|
-
return
|
|
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}`;
|
|
673
|
+
}
|
|
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);
|
|
313
687
|
}
|
|
314
688
|
|
|
315
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";
|
|
316
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
|
|
317
985
|
var sessions = /* @__PURE__ */ new Map();
|
|
318
|
-
var MAX_SESSIONS = 256;
|
|
319
|
-
|
|
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) {
|
|
320
1005
|
const existing = sessions.get(id);
|
|
321
1006
|
if (existing) {
|
|
322
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;
|
|
323
1010
|
return existing;
|
|
324
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
|
+
}
|
|
325
1020
|
if (sessions.size >= MAX_SESSIONS) evictOldest();
|
|
326
1021
|
const session = {
|
|
327
1022
|
id,
|
|
328
|
-
|
|
1023
|
+
protocol: meta?.protocol,
|
|
1024
|
+
upstreamOrigin: meta?.upstreamOrigin,
|
|
1025
|
+
state: createInitialState2(),
|
|
329
1026
|
createdAt: Date.now(),
|
|
330
1027
|
lastSeen: Date.now(),
|
|
331
1028
|
requests: 0,
|
|
332
1029
|
condensedToolResults: 0,
|
|
333
|
-
tokensSaved: 0
|
|
1030
|
+
tokensSaved: 0,
|
|
1031
|
+
blockContents: /* @__PURE__ */ new Map(),
|
|
1032
|
+
inFlight: 0,
|
|
1033
|
+
persisted: false
|
|
334
1034
|
};
|
|
335
1035
|
sessions.set(id, session);
|
|
336
1036
|
return session;
|
|
337
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
|
+
}
|
|
338
1058
|
function listSessions() {
|
|
339
1059
|
return [...sessions.values()].sort((a, b) => b.lastSeen - a.lastSeen);
|
|
340
1060
|
}
|
|
1061
|
+
function markDirty(session) {
|
|
1062
|
+
getStore().scheduleSave(session);
|
|
1063
|
+
}
|
|
1064
|
+
function cacheBlockContent(session, blockId, content) {
|
|
1065
|
+
session.blockContents.set(blockId, content);
|
|
1066
|
+
}
|
|
341
1067
|
function evictOldest() {
|
|
342
1068
|
let oldestId;
|
|
343
1069
|
let oldestSeen = Infinity;
|
|
344
|
-
for (const [id,
|
|
345
|
-
if (
|
|
346
|
-
|
|
1070
|
+
for (const [id, s2] of sessions) {
|
|
1071
|
+
if (s2.inFlight > 0) continue;
|
|
1072
|
+
if (s2.lastSeen < oldestSeen) {
|
|
1073
|
+
oldestSeen = s2.lastSeen;
|
|
347
1074
|
oldestId = id;
|
|
348
1075
|
}
|
|
349
1076
|
}
|
|
350
|
-
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());
|
|
351
1087
|
}
|
|
352
1088
|
|
|
353
1089
|
// src/compress-tool.ts
|
|
354
1090
|
import { COMPRESS_PHILOSOPHY, HOW_TO_COMPRESS_RULES } from "acp-kernel";
|
|
355
1091
|
var COMPRESS_TOOL_NAME = "compress";
|
|
1092
|
+
var ACP_TEXT_OPEN = "<acp_compress>";
|
|
1093
|
+
var ACP_TEXT_CLOSE = "</acp_compress>";
|
|
356
1094
|
var COMPRESS_TOOL = {
|
|
357
1095
|
name: COMPRESS_TOOL_NAME,
|
|
358
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?}].",
|
|
@@ -378,12 +1116,18 @@ var COMPRESS_TOOL = {
|
|
|
378
1116
|
}
|
|
379
1117
|
};
|
|
380
1118
|
function parseCompressInput(input) {
|
|
381
|
-
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
|
+
}
|
|
382
1123
|
const obj = input;
|
|
383
1124
|
if (Array.isArray(obj.content)) {
|
|
384
|
-
|
|
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;
|
|
385
1128
|
}
|
|
386
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(",")}`);
|
|
387
1131
|
return single ? [single] : [];
|
|
388
1132
|
}
|
|
389
1133
|
function toRange(r) {
|
|
@@ -436,7 +1180,7 @@ ${HOW_TO_COMPRESS_RULES}
|
|
|
436
1180
|
|
|
437
1181
|
ACP TAGS
|
|
438
1182
|
|
|
439
|
-
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.
|
|
440
1184
|
|
|
441
1185
|
TOOLS
|
|
442
1186
|
|
|
@@ -506,6 +1250,36 @@ var ACP_TOOLS_OPENAI = [
|
|
|
506
1250
|
SEARCH_CONTEXT_TOOL_OPENAI,
|
|
507
1251
|
ACP_STATUS_TOOL_OPENAI
|
|
508
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
|
+
];
|
|
509
1283
|
var PROXY_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
510
1284
|
COMPRESS_TOOL_NAME,
|
|
511
1285
|
DECOMPRESS_TOOL_NAME,
|
|
@@ -513,6 +1287,15 @@ var PROXY_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
513
1287
|
ACP_STATUS_TOOL_NAME
|
|
514
1288
|
]);
|
|
515
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
|
+
|
|
516
1299
|
// src/stream.ts
|
|
517
1300
|
var NOOP = /* @__PURE__ */ Symbol("noop");
|
|
518
1301
|
async function* rewriteSseStream(upstream, ctx) {
|
|
@@ -533,6 +1316,7 @@ async function* rewriteSseStream(upstream, ctx) {
|
|
|
533
1316
|
const { done, value } = await reader.read();
|
|
534
1317
|
if (done) break;
|
|
535
1318
|
buf += decoder.decode(value, { stream: true });
|
|
1319
|
+
buf = normalizeSseLineEndings(buf);
|
|
536
1320
|
let idx;
|
|
537
1321
|
while ((idx = buf.indexOf("\n\n")) !== -1) {
|
|
538
1322
|
const rawEvent = buf.slice(0, idx);
|
|
@@ -581,6 +1365,10 @@ function routeEvent(ev, blocks, ctx, markConverted, markRealToolUse, getConverte
|
|
|
581
1365
|
if (typeof partial === "string") st.json += partial;
|
|
582
1366
|
return NOOP;
|
|
583
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
|
+
}
|
|
584
1372
|
return emitEvent(ev);
|
|
585
1373
|
}
|
|
586
1374
|
if (t === "content_block_stop") {
|
|
@@ -638,7 +1426,19 @@ function applyRanges(ranges, ctx) {
|
|
|
638
1426
|
state: ctx.session.state,
|
|
639
1427
|
config: ctx.config
|
|
640
1428
|
});
|
|
1429
|
+
const beforeIds = new Set(ctx.session.state.blocks.map((b) => b.blockId));
|
|
641
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
|
+
}
|
|
642
1442
|
const r = res.result;
|
|
643
1443
|
const detail = ranges.map((rg) => `${rg.startRef}\u2013${rg.endRef}`).join(", ");
|
|
644
1444
|
if (r.blocksCreated === 0) {
|
|
@@ -647,9 +1447,9 @@ function applyRanges(ranges, ctx) {
|
|
|
647
1447
|
return `[Compression FAILED: ${errs} Do not retry the same range.]`;
|
|
648
1448
|
}
|
|
649
1449
|
const warn = r.warnings.length > 0 ? ` ${r.warnings.join("; ")}` : "";
|
|
650
|
-
const
|
|
651
|
-
ctx.log(`[acp-proxy: ${
|
|
652
|
-
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;
|
|
653
1453
|
} catch (err) {
|
|
654
1454
|
ctx.log(`[acp-proxy: compress failed: ${String(err)}]`);
|
|
655
1455
|
return `[Compression FAILED: ${String(err)} Do not retry the same range.]`;
|
|
@@ -703,55 +1503,109 @@ function rewriteJsonResponse(body, ctx) {
|
|
|
703
1503
|
}
|
|
704
1504
|
b.content = newContent;
|
|
705
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
|
+
}
|
|
706
1512
|
return body;
|
|
707
1513
|
}
|
|
708
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
|
+
|
|
709
1541
|
// src/compress-loop.ts
|
|
710
1542
|
import {
|
|
711
1543
|
buildStatusReport,
|
|
712
|
-
collectBlockContent,
|
|
713
|
-
deactivateBlock,
|
|
714
1544
|
estimateTokensFast
|
|
715
1545
|
} from "acp-kernel";
|
|
716
|
-
|
|
717
|
-
|
|
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";
|
|
718
1554
|
import { tmpdir } from "os";
|
|
719
|
-
function
|
|
720
|
-
|
|
721
|
-
|
|
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;
|
|
722
1575
|
}
|
|
723
|
-
if (
|
|
724
|
-
const rawBlockId = args.blockId;
|
|
725
|
-
if (typeof rawBlockId !== "string" || rawBlockId.length === 0) {
|
|
726
|
-
return "[decompress FAILED: blockId is required]";
|
|
727
|
-
}
|
|
728
|
-
const blockId = rawBlockId.trim();
|
|
729
|
-
const block = ctx.core.decompress(blockId, ctx.session.state);
|
|
730
|
-
if (!block) return `[Block ${blockId} not found]`;
|
|
731
|
-
const full = args.full === true;
|
|
732
|
-
const collected = collectBlockContent(ctx.session.state, block, ctx.messages, { full });
|
|
1576
|
+
if (count > 0 || cached) {
|
|
733
1577
|
ctx.session.state = deactivateBlock(ctx.session.state, [blockId]);
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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}
|
|
743
1588
|
Content (${body.length} chars) written to: ${outPath}
|
|
744
1589
|
Use the read tool to access it.`;
|
|
745
|
-
|
|
746
|
-
|
|
1590
|
+
} catch (e) {
|
|
1591
|
+
return `${header}
|
|
747
1592
|
[Failed to write to ${outPath}: ${String(e)}]
|
|
748
1593
|
${body.slice(0, 4e3)}...`;
|
|
749
|
-
}
|
|
750
1594
|
}
|
|
751
|
-
return `${header}
|
|
752
|
-
${body}`;
|
|
753
1595
|
}
|
|
754
|
-
|
|
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") {
|
|
755
1609
|
const query = typeof args.query === "string" ? args.query : "";
|
|
756
1610
|
if (query.length === 0) return "[search_context FAILED: query is required]";
|
|
757
1611
|
const limit = typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 5;
|
|
@@ -885,27 +1739,469 @@ ${icon} [ACP] ${inner}
|
|
|
885
1739
|
}
|
|
886
1740
|
async function* compressLoopStream(initialUpstream, ctx, requestBody, requestOptions) {
|
|
887
1741
|
let upstream = initialUpstream;
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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;
|
|
896
2187
|
let loopCount = 0;
|
|
2188
|
+
let responseObj = null;
|
|
2189
|
+
let activeClearTimer = null;
|
|
2190
|
+
let nextOutputIndex = 0;
|
|
897
2191
|
for (; ; ) {
|
|
898
2192
|
loopCount++;
|
|
899
|
-
if (loopCount >
|
|
900
|
-
ctx.log("[acp-proxy: compress loop limit (
|
|
901
|
-
|
|
902
|
-
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");
|
|
903
2198
|
return;
|
|
904
2199
|
}
|
|
905
|
-
const
|
|
2200
|
+
const fcByItemId = /* @__PURE__ */ new Map();
|
|
906
2201
|
let contentText = "";
|
|
907
|
-
let
|
|
908
|
-
let
|
|
2202
|
+
let completed = false;
|
|
2203
|
+
let terminalKind = null;
|
|
2204
|
+
let terminalRaw = null;
|
|
909
2205
|
const isFirstRound = loopCount === 1;
|
|
910
2206
|
const reader = upstream.getReader();
|
|
911
2207
|
const decoder = new TextDecoder("utf-8");
|
|
@@ -915,156 +2211,179 @@ async function* compressLoopStream(initialUpstream, ctx, requestBody, requestOpt
|
|
|
915
2211
|
const { done, value } = await reader.read();
|
|
916
2212
|
if (done) break;
|
|
917
2213
|
sseBuffer += decoder.decode(value, { stream: true });
|
|
2214
|
+
if (sseBuffer.indexOf("\r") !== -1) sseBuffer = sseBuffer.replace(/\r\n|\r/g, "\n");
|
|
918
2215
|
let sep;
|
|
919
2216
|
while ((sep = sseBuffer.indexOf("\n\n")) >= 0) {
|
|
920
2217
|
const eventStr = sseBuffer.slice(0, sep);
|
|
921
2218
|
sseBuffer = sseBuffer.slice(sep + 2);
|
|
922
2219
|
if (!eventStr.trim()) continue;
|
|
923
|
-
const d =
|
|
924
|
-
if (d.
|
|
925
|
-
|
|
2220
|
+
const d = classifyResponsesSseEvent(eventStr);
|
|
2221
|
+
if (d.yieldChunk && (isFirstRound || !d.isMeta) && !(TEXT_PROTOCOL && !d.isMeta)) {
|
|
2222
|
+
yield d.yieldChunk;
|
|
926
2223
|
}
|
|
927
|
-
if (
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
if (
|
|
943
|
-
|
|
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;
|
|
944
2243
|
}
|
|
945
2244
|
}
|
|
946
|
-
if (d.
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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)}%)` : ""}`);
|
|
959
2259
|
}
|
|
960
2260
|
}
|
|
961
2261
|
}
|
|
962
2262
|
}
|
|
963
|
-
sseBuffer += decoder.decode();
|
|
964
2263
|
} finally {
|
|
965
2264
|
reader.releaseLock();
|
|
2265
|
+
if (activeClearTimer) {
|
|
2266
|
+
activeClearTimer();
|
|
2267
|
+
activeClearTimer = null;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
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
|
+
}
|
|
966
2280
|
}
|
|
967
|
-
const
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
}).filter((tc) => tc.name.length > 0);
|
|
972
|
-
const proxyCalls = toolCalls.filter((tc) => PROXY_TOOL_NAMES.has(tc.name));
|
|
973
|
-
const realCalls = toolCalls.filter((tc) => !PROXY_TOOL_NAMES.has(tc.name));
|
|
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))}`);
|
|
974
2285
|
const hasOnlyProxy = proxyCalls.length > 0 && realCalls.length === 0;
|
|
975
2286
|
if (!hasOnlyProxy) {
|
|
976
|
-
|
|
977
|
-
|
|
2287
|
+
let oi = nextOutputIndex;
|
|
2288
|
+
for (const fc of realCalls) {
|
|
2289
|
+
yield Buffer.from(buildFunctionCallEvents(fc, oi), "utf8");
|
|
2290
|
+
oi++;
|
|
978
2291
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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");
|
|
982
2301
|
return;
|
|
983
2302
|
}
|
|
984
2303
|
const names = proxyCalls.map((c) => c.name).join(", ");
|
|
985
|
-
ctx.log(`[acp-proxy: round ${loopCount} \u2014 ${proxyCalls.length} proxy call(s): ${names}]`);
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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) {
|
|
997
2323
|
let args = {};
|
|
998
2324
|
try {
|
|
999
|
-
args = JSON.parse(
|
|
1000
|
-
} 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)}`);
|
|
1001
2328
|
args = {};
|
|
1002
2329
|
}
|
|
1003
|
-
|
|
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);
|
|
1004
2334
|
const preview = result.length > 120 ? result.slice(0, 120) + "..." : result;
|
|
1005
|
-
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}`;
|
|
1006
2337
|
yield Buffer.from(
|
|
1007
|
-
|
|
2338
|
+
buildMessageItemSequence(markerItemId, nextOutputIndex++, buildVisibilityMarker(fc.name, result)),
|
|
1008
2339
|
"utf8"
|
|
1009
2340
|
);
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
2341
|
+
inputItems.push({
|
|
2342
|
+
type: "function_call_output",
|
|
2343
|
+
call_id: fc.callId || `call_${Date.now()}`,
|
|
2344
|
+
output: result
|
|
1014
2345
|
});
|
|
1015
2346
|
}
|
|
1016
|
-
requestBody.
|
|
1017
|
-
|
|
2347
|
+
requestBody.input = inputItems;
|
|
2348
|
+
if (!("stream" in requestBody)) requestBody.stream = true;
|
|
2349
|
+
const { response: resp, clearTimer } = await fetchWithTimeout(requestOptions.url, {
|
|
1018
2350
|
method: "POST",
|
|
1019
2351
|
headers: requestOptions.headers,
|
|
1020
2352
|
body: JSON.stringify(requestBody)
|
|
1021
2353
|
});
|
|
1022
2354
|
if (!resp.ok || !resp.body) {
|
|
2355
|
+
clearTimer();
|
|
1023
2356
|
const errText = await resp.text().catch(() => "upstream error");
|
|
1024
|
-
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()}`;
|
|
1025
2359
|
yield Buffer.from(
|
|
1026
|
-
`
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
index: 0,
|
|
1030
|
-
delta: { content: `
|
|
1031
|
-
[acp-proxy: upstream error ${resp.status}]
|
|
1032
|
-
` },
|
|
1033
|
-
finish_reason: null
|
|
1034
|
-
}]
|
|
1035
|
-
})}
|
|
1036
|
-
|
|
1037
|
-
`,
|
|
2360
|
+
buildMessageItemSequence(errItemId, nextOutputIndex++, `
|
|
2361
|
+
[acp-proxy: upstream error ${resp.status}: ${errText.slice(0, 200)}]
|
|
2362
|
+
`),
|
|
1038
2363
|
"utf8"
|
|
1039
2364
|
);
|
|
1040
|
-
yield Buffer.from(
|
|
1041
|
-
yield Buffer.from("data: [DONE]\n\n", "utf8");
|
|
2365
|
+
yield Buffer.from(buildCompleted(responseObj), "utf8");
|
|
1042
2366
|
return;
|
|
1043
2367
|
}
|
|
1044
2368
|
upstream = resp.body;
|
|
2369
|
+
if (activeClearTimer) activeClearTimer();
|
|
2370
|
+
activeClearTimer = clearTimer;
|
|
1045
2371
|
}
|
|
1046
2372
|
}
|
|
1047
2373
|
|
|
1048
2374
|
// src/stream-openai.ts
|
|
1049
|
-
function safeJsonParse(s) {
|
|
1050
|
-
try {
|
|
1051
|
-
return s ? JSON.parse(s) : {};
|
|
1052
|
-
} catch {
|
|
1053
|
-
return {};
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
2375
|
function rewriteOpenaiJsonResponse(body, ctx) {
|
|
1057
2376
|
if (!body || typeof body !== "object") return body;
|
|
1058
2377
|
const b = body;
|
|
1059
2378
|
const choice = b.choices?.[0];
|
|
1060
|
-
const
|
|
1061
|
-
if (!choice || !
|
|
2379
|
+
const msg2 = choice?.message;
|
|
2380
|
+
if (!choice || !msg2) return body;
|
|
1062
2381
|
let converted = false;
|
|
1063
2382
|
let sawReal = false;
|
|
1064
2383
|
const noteParts = [];
|
|
1065
2384
|
const keepToolCalls = [];
|
|
1066
|
-
const existingText = typeof
|
|
1067
|
-
const toolCalls =
|
|
2385
|
+
const existingText = typeof msg2.content === "string" ? msg2.content : "";
|
|
2386
|
+
const toolCalls = msg2.tool_calls;
|
|
1068
2387
|
if (Array.isArray(toolCalls)) {
|
|
1069
2388
|
for (const tc of toolCalls) {
|
|
1070
2389
|
if (tc.function?.name === COMPRESS_TOOL_NAME) {
|
|
@@ -1076,14 +2395,17 @@ function rewriteOpenaiJsonResponse(body, ctx) {
|
|
|
1076
2395
|
}
|
|
1077
2396
|
}
|
|
1078
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
|
+
}
|
|
1079
2401
|
if (!converted) return body;
|
|
1080
2402
|
const note = noteParts.join("\n");
|
|
1081
|
-
|
|
2403
|
+
msg2.content = existingText ? `${existingText}
|
|
1082
2404
|
${note}` : note;
|
|
1083
2405
|
if (keepToolCalls.length > 0) {
|
|
1084
|
-
|
|
2406
|
+
msg2.tool_calls = keepToolCalls;
|
|
1085
2407
|
} else {
|
|
1086
|
-
delete
|
|
2408
|
+
delete msg2.tool_calls;
|
|
1087
2409
|
}
|
|
1088
2410
|
if (!sawReal) {
|
|
1089
2411
|
choice.finish_reason = "stop";
|
|
@@ -1091,70 +2413,300 @@ ${note}` : note;
|
|
|
1091
2413
|
return body;
|
|
1092
2414
|
}
|
|
1093
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
|
+
|
|
1094
2528
|
// src/server.ts
|
|
1095
2529
|
var UPSTREAM_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
1096
2530
|
"host",
|
|
1097
2531
|
"content-length",
|
|
1098
2532
|
"connection",
|
|
1099
2533
|
"keep-alive",
|
|
1100
|
-
"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"
|
|
1101
2539
|
]);
|
|
1102
|
-
function resolveUpstream(
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
2540
|
+
function resolveUpstream(opts, reqUrl) {
|
|
2541
|
+
const names = Object.keys(opts.routes);
|
|
2542
|
+
if (names.length === 0) return void 0;
|
|
2543
|
+
const RESERVED = /* @__PURE__ */ new Set(["v1", "v2", "v4", "chat", "completions", "messages", "models", "api"]);
|
|
2544
|
+
const sorted = [...names].sort((a, b) => b.length - a.length);
|
|
2545
|
+
const segments = reqUrl.split("/");
|
|
2546
|
+
for (const name of sorted) {
|
|
2547
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name)) continue;
|
|
2548
|
+
if (RESERVED.has(name.toLowerCase())) continue;
|
|
2549
|
+
const idx = segments.indexOf(name);
|
|
2550
|
+
if (idx < 0) continue;
|
|
2551
|
+
const base = opts.routes[name].url.replace(/\/$/, "");
|
|
2552
|
+
const rest = [...segments.slice(0, idx), ...segments.slice(idx + 1)].join("/");
|
|
2553
|
+
const rewrittenUrl = base + rest;
|
|
2554
|
+
return { upstream: base, rewrittenUrl, provider: name };
|
|
2555
|
+
}
|
|
2556
|
+
return void 0;
|
|
2557
|
+
}
|
|
2558
|
+
async function startServer(opts) {
|
|
1110
2559
|
const core = createCore();
|
|
1111
|
-
const config =
|
|
1112
|
-
const log = (level,
|
|
1113
|
-
|
|
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) => {
|
|
1114
2565
|
try {
|
|
1115
|
-
await handle(req, res,
|
|
2566
|
+
await handle(req, res, opts, core, config, log);
|
|
1116
2567
|
} catch (err) {
|
|
1117
|
-
|
|
2568
|
+
const msg2 = String(err);
|
|
2569
|
+
log("error", msg2);
|
|
1118
2570
|
if (!res.headersSent) {
|
|
1119
|
-
|
|
1120
|
-
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 }));
|
|
1121
2574
|
} else {
|
|
1122
2575
|
res.end();
|
|
1123
2576
|
}
|
|
1124
2577
|
}
|
|
1125
2578
|
});
|
|
1126
|
-
|
|
1127
|
-
log(
|
|
2579
|
+
server.listen(opts.port, opts.host, () => {
|
|
2580
|
+
log(
|
|
2581
|
+
"info",
|
|
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}`)
|
|
2583
|
+
);
|
|
1128
2584
|
});
|
|
1129
|
-
|
|
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;
|
|
1130
2612
|
}
|
|
1131
|
-
async function handle(req, res,
|
|
2613
|
+
async function handle(req, res, opts, core, config, log) {
|
|
1132
2614
|
if (req.method === "GET" && req.url === "/__acp/stats") return sendStats(res);
|
|
1133
2615
|
if (req.method === "GET" && (req.url === "/" || req.url === "/__acp/health")) {
|
|
1134
2616
|
res.writeHead(200, { "content-type": "application/json" });
|
|
1135
|
-
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) } }));
|
|
1136
2633
|
return;
|
|
1137
2634
|
}
|
|
1138
|
-
const bodyBuffer = await readBody(req);
|
|
1139
2635
|
const url = req.url ?? "";
|
|
1140
|
-
const
|
|
1141
|
-
const
|
|
1142
|
-
const
|
|
1143
|
-
|
|
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;
|
|
2641
|
+
if (protocol && bodyBuffer.length > 0) {
|
|
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);
|
|
2653
|
+
if (limit && limit !== config.modelContextLimit) {
|
|
2654
|
+
reqConfig = { ...config, modelContextLimit: limit };
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
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
|
+
}
|
|
1144
2681
|
}
|
|
1145
|
-
var
|
|
1146
|
-
function
|
|
2682
|
+
var ACP_TAG_MARK = "<acp ";
|
|
2683
|
+
function diagTagSummary(messages, sessionId, strategy) {
|
|
2684
|
+
let textTagged = 0;
|
|
2685
|
+
let toolTagged = 0;
|
|
1147
2686
|
for (const m of messages) {
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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++;
|
|
1151
2691
|
}
|
|
2692
|
+
return `[${sessionId}] processTurn: ${messages.length} msgs, renderTags=${strategy}, ${textTagged} text tagged, ${toolTagged} tool tagged (should be 0 with text-only)`;
|
|
1152
2693
|
}
|
|
1153
|
-
function
|
|
1154
|
-
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;
|
|
1155
2709
|
const stream = parsed.stream === true;
|
|
1156
|
-
const sessionId = deriveSessionId(parsed, headerValue(req, opts2.sessionHeader));
|
|
1157
|
-
const session = getSession(sessionId);
|
|
1158
2710
|
session.requests++;
|
|
1159
2711
|
let processedMessages = [];
|
|
1160
2712
|
let rebuiltMessages = parsed.messages;
|
|
@@ -1162,74 +2714,148 @@ function prepareAnthropic(bodyBuffer, req, opts2, core, config, log) {
|
|
|
1162
2714
|
let toolsOut = parsed.tools;
|
|
1163
2715
|
try {
|
|
1164
2716
|
const { msgs } = anthropicToCore(parsed);
|
|
1165
|
-
const tokenCount =
|
|
1166
|
-
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" });
|
|
1167
2719
|
session.state = turn.state;
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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) {
|
|
1173
2727
|
toolsOut = injectTool(parsed.tools);
|
|
1174
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
|
+
}
|
|
1175
2738
|
} catch (err) {
|
|
1176
2739
|
log("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
|
|
1177
2740
|
processedMessages = [];
|
|
1178
2741
|
}
|
|
1179
2742
|
const rebuilt = { ...parsed, messages: rebuiltMessages, system: systemOut, tools: toolsOut };
|
|
1180
|
-
|
|
2743
|
+
markDirty(session);
|
|
2744
|
+
return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "anthropic", stream, compressInjected: opts.compress.injectTool };
|
|
1181
2745
|
}
|
|
1182
|
-
function prepareOpenai(
|
|
1183
|
-
const
|
|
2746
|
+
function prepareOpenai(parsed, req, opts, core, config, log, session) {
|
|
2747
|
+
const sessionId = session.id;
|
|
1184
2748
|
const stream = parsed.stream === true;
|
|
1185
|
-
const sessionId = deriveSessionIdOpenai(parsed, headerValue(req, opts2.sessionHeader));
|
|
1186
|
-
const session = getSession(sessionId);
|
|
1187
2749
|
session.requests++;
|
|
1188
2750
|
let processedMessages = [];
|
|
1189
2751
|
let rebuiltMessages = parsed.messages;
|
|
1190
2752
|
let toolsOut = parsed.tools;
|
|
1191
2753
|
const maxTokens = typeof parsed.max_tokens === "number" ? parsed.max_tokens : 8192;
|
|
1192
2754
|
const isTitleGen = maxTokens <= 200 || parsed.messages.length <= 2;
|
|
1193
|
-
const shouldInject =
|
|
2755
|
+
const shouldInject = opts.compress.injectTool && !isTitleGen;
|
|
1194
2756
|
try {
|
|
1195
2757
|
const { msgs } = openaiToCore(parsed);
|
|
1196
|
-
const tokenCount =
|
|
1197
|
-
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" });
|
|
1198
2760
|
session.state = turn.state;
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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);
|
|
1202
2766
|
const sysParts = [];
|
|
1203
2767
|
if (shouldInject) sysParts.push(buildCompressSystemPrompt());
|
|
2768
|
+
rebuiltMessages = injectOpenaiSystem(rebuiltMessages, sysParts);
|
|
2769
|
+
if (shouldInject) {
|
|
2770
|
+
toolsOut = injectOpenaiTool(parsed.tools);
|
|
2771
|
+
}
|
|
1204
2772
|
if (turn.nudge?.shouldInject && shouldInject) {
|
|
1205
2773
|
try {
|
|
1206
2774
|
const rendered = renderNudgeText(turn.nudge);
|
|
1207
|
-
if (rendered.text)
|
|
2775
|
+
if (rendered.text) {
|
|
2776
|
+
rebuiltMessages = [...rebuiltMessages, { role: "user", content: rendered.text }];
|
|
2777
|
+
}
|
|
1208
2778
|
} catch {
|
|
1209
2779
|
}
|
|
1210
2780
|
}
|
|
1211
|
-
rebuiltMessages = injectOpenaiSystem(rebuiltMessages, sysParts);
|
|
1212
|
-
if (shouldInject) {
|
|
1213
|
-
toolsOut = injectOpenaiTool(parsed.tools);
|
|
1214
|
-
}
|
|
1215
2781
|
} catch (err) {
|
|
1216
2782
|
log("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
|
|
1217
2783
|
processedMessages = [];
|
|
1218
2784
|
}
|
|
1219
2785
|
const rebuilt = { ...parsed, messages: rebuiltMessages, tools: toolsOut };
|
|
2786
|
+
markDirty(session);
|
|
1220
2787
|
return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "openai", stream, compressInjected: shouldInject };
|
|
1221
2788
|
}
|
|
1222
|
-
function
|
|
1223
|
-
const
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
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)"}`);
|
|
1231
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 = [];
|
|
1232
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());
|
|
1233
2859
|
if (parts.length === 0) return parsed.system;
|
|
1234
2860
|
const full = baseText ? `${baseText}
|
|
1235
2861
|
|
|
@@ -1251,21 +2877,44 @@ function injectOpenaiTool(tools) {
|
|
|
1251
2877
|
const additions = ACP_TOOLS_OPENAI.filter((t) => !present.has(t.function.name));
|
|
1252
2878
|
return [...tools, ...additions];
|
|
1253
2879
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
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 ?? "");
|
|
2891
|
+
log("info", `forward ${req.method} ${req.url ?? ""} \u2192 ${upstreamUrl}${route ? ` (${route.provider})` : ""}`);
|
|
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") {
|
|
1259
2906
|
try {
|
|
1260
2907
|
const parsed = JSON.parse(body);
|
|
1261
2908
|
const toolNames = (parsed.tools ?? []).map((t) => {
|
|
1262
2909
|
const fn = t.function;
|
|
1263
|
-
return fn?.name ?? "?";
|
|
2910
|
+
return fn?.name ?? t.name ?? "?";
|
|
1264
2911
|
});
|
|
1265
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}`);
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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
|
+
}
|
|
1269
2918
|
} catch {
|
|
1270
2919
|
}
|
|
1271
2920
|
}
|
|
@@ -1274,26 +2923,37 @@ async function forward(req, res, opts2, body, prepared, core, config, log) {
|
|
|
1274
2923
|
if (UPSTREAM_HOP_HEADERS.has(k.toLowerCase()) || v === void 0) continue;
|
|
1275
2924
|
headers[k] = Array.isArray(v) ? v.join(", ") : v;
|
|
1276
2925
|
}
|
|
1277
|
-
headers["host"] = new URL(
|
|
2926
|
+
headers["host"] = new URL(route ? route.upstream : opts.upstream).host;
|
|
2927
|
+
if (affinity && !clientConversationHeader(req.headers)) {
|
|
2928
|
+
headers["x-session-id"] = affinity;
|
|
2929
|
+
}
|
|
1278
2930
|
const init = {
|
|
1279
2931
|
method: req.method ?? "GET",
|
|
1280
2932
|
headers,
|
|
1281
2933
|
body: req.method === "GET" || req.method === "HEAD" ? void 0 : body
|
|
1282
2934
|
};
|
|
1283
|
-
const upstream = await
|
|
2935
|
+
const { response: upstream, clearTimer: clearUpstreamTimer } = await fetchWithTimeout(upstreamUrl, init);
|
|
1284
2936
|
const respHeaders = {};
|
|
1285
2937
|
upstream.headers.forEach((v, k) => {
|
|
1286
2938
|
if (UPSTREAM_HOP_HEADERS.has(k.toLowerCase())) return;
|
|
1287
2939
|
respHeaders[k] = v;
|
|
1288
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
|
+
}
|
|
1289
2947
|
res.writeHead(upstream.status, respHeaders);
|
|
1290
2948
|
if (!upstream.body) {
|
|
1291
2949
|
res.end();
|
|
2950
|
+
clearUpstreamTimer();
|
|
1292
2951
|
return;
|
|
1293
2952
|
}
|
|
1294
|
-
const useRewriter = prepared !== null && prepared.processedMessages.length > 0
|
|
2953
|
+
const useRewriter = prepared !== null && prepared.compressInjected && prepared.processedMessages.length > 0;
|
|
1295
2954
|
if (!useRewriter || prepared === null) {
|
|
1296
2955
|
await pipeThrough(upstream.body, res);
|
|
2956
|
+
clearUpstreamTimer();
|
|
1297
2957
|
return;
|
|
1298
2958
|
}
|
|
1299
2959
|
const ctx = {
|
|
@@ -1301,50 +2961,83 @@ async function forward(req, res, opts2, body, prepared, core, config, log) {
|
|
|
1301
2961
|
config,
|
|
1302
2962
|
messages: prepared.processedMessages,
|
|
1303
2963
|
session: prepared.session,
|
|
1304
|
-
log: (
|
|
1305
|
-
debug:
|
|
2964
|
+
log: (msg2) => log("info", `[${prepared.session.id}] ${msg2}`),
|
|
2965
|
+
debug: opts.debug
|
|
1306
2966
|
};
|
|
1307
2967
|
if (prepared.stream) {
|
|
1308
2968
|
let streamToRead = upstream.body;
|
|
1309
2969
|
let dumpRaw;
|
|
1310
|
-
if (
|
|
2970
|
+
if (opts.dumpSse) {
|
|
1311
2971
|
const [a, b] = upstream.body.tee();
|
|
1312
2972
|
streamToRead = a;
|
|
1313
|
-
dumpRaw = dumpStreamToFile(b,
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
+
}
|
|
1344
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;
|
|
1345
3040
|
}
|
|
1346
|
-
res.end();
|
|
1347
|
-
if (dumpRaw) await dumpRaw;
|
|
1348
3041
|
} else {
|
|
1349
3042
|
const buf = await upstream.arrayBuffer();
|
|
1350
3043
|
const text = Buffer.from(buf).toString("utf8");
|
|
@@ -1352,6 +3045,8 @@ async function forward(req, res, opts2, body, prepared, core, config, log) {
|
|
|
1352
3045
|
const json = JSON.parse(text);
|
|
1353
3046
|
if (prepared.protocol === "openai") {
|
|
1354
3047
|
rewriteOpenaiJsonResponse(json, ctx);
|
|
3048
|
+
} else if (prepared.protocol === "responses") {
|
|
3049
|
+
rewriteResponsesJsonResponse(json, ctx);
|
|
1355
3050
|
} else {
|
|
1356
3051
|
rewriteJsonResponse(json, ctx);
|
|
1357
3052
|
}
|
|
@@ -1359,7 +3054,9 @@ async function forward(req, res, opts2, body, prepared, core, config, log) {
|
|
|
1359
3054
|
} catch {
|
|
1360
3055
|
res.end(text);
|
|
1361
3056
|
}
|
|
3057
|
+
clearUpstreamTimer();
|
|
1362
3058
|
}
|
|
3059
|
+
markDirty(prepared.session);
|
|
1363
3060
|
}
|
|
1364
3061
|
async function pipeThrough(stream, res) {
|
|
1365
3062
|
const reader = stream.getReader();
|
|
@@ -1377,11 +3074,11 @@ async function pipeThrough(stream, res) {
|
|
|
1377
3074
|
}
|
|
1378
3075
|
}
|
|
1379
3076
|
async function dumpStreamToFile(stream, dir, name) {
|
|
1380
|
-
const { mkdirSync:
|
|
1381
|
-
const { join:
|
|
3077
|
+
const { mkdirSync: mkdirSync3, createWriteStream } = await import("fs");
|
|
3078
|
+
const { join: join3 } = await import("path");
|
|
1382
3079
|
try {
|
|
1383
|
-
|
|
1384
|
-
const ws = createWriteStream(
|
|
3080
|
+
mkdirSync3(dir, { recursive: true });
|
|
3081
|
+
const ws = createWriteStream(join3(dir, name));
|
|
1385
3082
|
const reader = stream.getReader();
|
|
1386
3083
|
try {
|
|
1387
3084
|
for (; ; ) {
|
|
@@ -1414,26 +3111,160 @@ function headerValue(req, name) {
|
|
|
1414
3111
|
}
|
|
1415
3112
|
return void 0;
|
|
1416
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
|
+
};
|
|
1417
3122
|
function readBody(req) {
|
|
1418
3123
|
return new Promise((resolve, reject) => {
|
|
1419
3124
|
const chunks = [];
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
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
|
+
});
|
|
1423
3143
|
});
|
|
1424
3144
|
}
|
|
1425
|
-
function logMsg(
|
|
1426
|
-
if (!
|
|
3145
|
+
function logMsg(opts, level, msg2) {
|
|
3146
|
+
if (!opts.log) return;
|
|
1427
3147
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1428
|
-
console.error(`${ts} [${level}] ${
|
|
3148
|
+
console.error(`${ts} [${level}] ${msg2}`);
|
|
1429
3149
|
}
|
|
1430
3150
|
|
|
1431
|
-
// src/
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
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);
|
|
1438
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();
|
|
1439
3270
|
//# sourceMappingURL=index.js.map
|